Refactor board and table options (#3700)

* Refactor board and table options

* Fix

* Fix
This commit is contained in:
Charles Bochet
2024-01-30 18:38:31 +01:00
committed by GitHub
parent 64b2ef3dc2
commit 2e4f2d54aa
13 changed files with 358 additions and 131 deletions

View File

@ -14,7 +14,6 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
isFirstColumn ? 'none' : theme.border.color.light};
display: flex;
flex-direction: column;
height: fit-content;
max-width: 200px;
min-width: 200px;

View File

@ -2,7 +2,6 @@ import { useState } from 'react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
@ -11,13 +10,12 @@ import { RecordIndexBoardContainerEffect } from '@/object-record/record-index/co
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewType } from '@/views/types/ViewType';
@ -66,37 +64,23 @@ export const RecordIndexContainer = ({
const setRecordIndexFilters = useSetRecoilState(recordIndexFiltersState);
const setRecordIndexSorts = useSetRecoilState(recordIndexSortsState);
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
const { setTableFilters, setTableSorts, setTableColumns } = useRecordTable({
recordTableId: recordIndexId,
});
const handleImport = () => {
const openImport =
objectNamePlural === 'companies'
? openCompanySpreadsheetImport
: openPersonSpreadsheetImport;
openImport();
};
return (
<StyledContainer>
<SpreadsheetImportProvider>
<ViewBar
viewBarId={recordIndexId}
optionsDropdownButton={
<TableOptionsDropdown
recordTableId={recordIndexId}
onImport={
['companies', 'people'].includes(recordIndexId)
? handleImport
: undefined
}
<RecordIndexOptionsDropdown
recordIndexId={recordIndexId}
objectNameSingular={objectNameSingular}
viewType={recordIndexViewType ?? ViewType.Table}
/>
}
optionsDropdownScopeId={TableOptionsDropdownId}
optionsDropdownScopeId={RECORD_INDEX_OPTIONS_DROPDOWN_ID}
onViewFieldsChange={(viewFields) => {
setTableColumns(
mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions),

View File

@ -0,0 +1,38 @@
import { RecordIndexOptionsDropdownButton } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownButton';
import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownContent';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useViewBar } from '@/views/hooks/useViewBar';
import { ViewType } from '@/views/types/ViewType';
type RecordIndexOptionsDropdownProps = {
viewType: ViewType;
objectNameSingular: string;
recordIndexId: string;
};
export const RecordIndexOptionsDropdown = ({
recordIndexId,
objectNameSingular,
viewType,
}: RecordIndexOptionsDropdownProps) => {
const { setViewEditMode } = useViewBar();
return (
<Dropdown
dropdownId={RECORD_INDEX_OPTIONS_DROPDOWN_ID}
clickableComponent={<RecordIndexOptionsDropdownButton />}
dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
dropdownOffset={{ y: 8 }}
dropdownComponents={
<RecordIndexOptionsDropdownContent
viewType={viewType}
objectNameSingular={objectNameSingular}
recordIndexId={recordIndexId}
/>
}
onClickOutside={() => setViewEditMode('none')}
/>
);
};

View File

@ -1,10 +1,10 @@
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
export const TableOptionsDropdownButton = () => {
export const RecordIndexOptionsDropdownButton = () => {
const { isDropdownOpen, toggleDropdown } = useDropdown(
TableOptionsDropdownId,
RECORD_INDEX_OPTIONS_DROPDOWN_ID,
);
return (

View File

@ -1,10 +1,12 @@
import { useCallback, useRef, useState } from 'react';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
import { useRecordIndexOptionsImport } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsImport';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { IconChevronLeft, IconFileImport, IconTag } from '@/ui/display/icon';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
@ -16,69 +18,42 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
import { useViewBar } from '@/views/hooks/useViewBar';
import { ViewType } from '@/views/types/ViewType';
import { useTableColumns } from '../../hooks/useTableColumns';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
type RecordIndexOptionsMenu = 'fields';
type TableOptionsMenu = 'fields';
type RecordIndexOptionsDropdownContentProps = {
recordIndexId: string;
objectNameSingular: string;
viewType: ViewType;
};
export const TableOptionsDropdownContent = ({
onImport,
recordTableId,
}: {
onImport?: () => void;
recordTableId: string;
}) => {
const { setViewEditMode, handleViewNameSubmit } = useViewBar();
export const RecordIndexOptionsDropdownContent = ({
viewType,
recordIndexId,
objectNameSingular,
}: RecordIndexOptionsDropdownContentProps) => {
const { setViewEditMode, handleViewNameSubmit } = useViewBar({
viewBarId: recordIndexId,
});
const { viewEditModeState, currentViewSelector } = useViewScopedStates();
const viewEditMode = useRecoilValue(viewEditModeState);
const currentView = useRecoilValue(currentViewSelector);
const { closeDropdown } = useDropdown(TableOptionsDropdownId);
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
const [currentMenu, setCurrentMenu] = useState<TableOptionsMenu | undefined>(
undefined,
);
const [currentMenu, setCurrentMenu] = useState<
RecordIndexOptionsMenu | undefined
>(undefined);
const resetMenu = () => setCurrentMenu(undefined);
const viewEditInputRef = useRef<HTMLInputElement>(null);
const { getHiddenTableColumnsSelector, getVisibleTableColumnsSelector } =
useRecordTableStates(recordTableId);
const hiddenTableColumns = useRecoilValue(getHiddenTableColumnsSelector());
const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector());
const { handleColumnVisibilityChange, handleColumnReorder } = useTableColumns(
{ recordTableId },
);
const handleSelectMenu = (option: TableOptionsMenu) => {
const name = viewEditInputRef.current?.value;
handleViewNameSubmit(name);
const handleSelectMenu = (option: RecordIndexOptionsMenu) => {
setCurrentMenu(option);
};
const handleReorderField: OnDragEndResponder = useCallback(
(result) => {
if (
!result.destination ||
result.destination.index === 1 ||
result.source.index === 1
) {
return;
}
const reorderFields = [...visibleTableColumns];
const [removed] = reorderFields.splice(result.source.index - 1, 1);
reorderFields.splice(result.destination.index - 1, 0, removed);
handleColumnReorder(reorderFields);
},
[visibleTableColumns, handleColumnReorder],
);
const resetMenu = () => setCurrentMenu(undefined);
useScopedHotkeys(
[Key.Escape],
() => {
@ -99,6 +74,41 @@ export const TableOptionsDropdownContent = ({
TableOptionsHotkeyScope.Dropdown,
);
const {
handleColumnVisibilityChange,
handleReorderColumns,
visibleTableColumns,
hiddenTableColumns,
} = useRecordIndexOptionsForTable(recordIndexId);
const {
visibleBoardFields,
hiddenBoardFields,
handleReorderBoardFields,
handleBoardFieldVisibilityChange,
} = useRecordIndexOptionsForBoard({
objectNameSingular,
viewBarId: recordIndexId,
});
const visibleRecordFields =
viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns;
const hiddenRecordFields =
viewType === ViewType.Kanban ? hiddenBoardFields : hiddenTableColumns;
const handleReorderFields =
viewType === ViewType.Kanban
? handleReorderBoardFields
: handleReorderColumns;
const handleChangeFieldVisibility =
viewType === ViewType.Kanban
? handleBoardFieldVisibilityChange
: handleColumnVisibilityChange;
const { handleImport } = useRecordIndexOptionsImport({ objectNameSingular });
return (
<>
{!currentMenu && (
@ -122,9 +132,9 @@ export const TableOptionsDropdownContent = ({
LeftIcon={IconTag}
text="Fields"
/>
{onImport && (
{handleImport && (
<MenuItem
onClick={onImport}
onClick={() => handleImport()}
LeftIcon={IconFileImport}
text="Import"
/>
@ -140,20 +150,20 @@ export const TableOptionsDropdownContent = ({
<DropdownMenuSeparator />
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleTableColumns}
fields={visibleRecordFields}
isVisible={true}
onVisibilityChange={handleColumnVisibilityChange}
onVisibilityChange={handleChangeFieldVisibility}
isDraggable={true}
onDragEnd={handleReorderField}
onDragEnd={handleReorderFields}
/>
{hiddenTableColumns.length > 0 && (
{hiddenRecordFields.length > 0 && (
<>
<DropdownMenuSeparator />
<ViewFieldsVisibilityDropdownSection
title="Hidden"
fields={hiddenTableColumns}
fields={hiddenRecordFields}
isVisible={false}
onVisibilityChange={handleColumnVisibilityChange}
onVisibilityChange={handleChangeFieldVisibility}
isDraggable={false}
/>
</>

View File

@ -0,0 +1,2 @@
export const RECORD_INDEX_BOARD_OPTIONS_DROPDOWN_ID =
'record-index-table-options-dropdown-id';

View File

@ -0,0 +1,2 @@
export const RECORD_INDEX_OPTIONS_DROPDOWN_ID =
'record-index-options-dropdown-id';

View File

@ -0,0 +1,156 @@
import { useCallback, useMemo } from 'react';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useRecoilState } from 'recoil';
import { mapBoardFieldDefinitionsToViewFields } from '@/companies/utils/mapBoardFieldDefinitionsToViewFields';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { useViewFields } from '@/views/hooks/internal/useViewFields';
type useRecordIndexOptionsForBoardParams = {
objectNameSingular: string;
viewBarId: string;
};
export const useRecordIndexOptionsForBoard = ({
objectNameSingular,
viewBarId,
}: useRecordIndexOptionsForBoardParams) => {
const [recordIndexFieldDefinitions, setRecordIndexFieldDefinitions] =
useRecoilState(recordIndexFieldDefinitionsState);
const { persistViewFields } = useViewFields(viewBarId);
const { objectMetadataItem } = useObjectMetadataItemOnly({
objectNameSingular,
});
const { columnDefinitions } =
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const visibleBoardFields = useMemo(
() =>
columnDefinitions.filter((columnDefinition) => {
return recordIndexFieldDefinitions.some(
(existingRecordFieldDefinition) => {
return (
columnDefinition.fieldMetadataId ===
existingRecordFieldDefinition.fieldMetadataId &&
existingRecordFieldDefinition.isVisible
);
},
);
}),
[columnDefinitions, recordIndexFieldDefinitions],
);
const hiddenBoardFields = useMemo(
() =>
columnDefinitions.filter((columnDefinition) => {
return !recordIndexFieldDefinitions.some(
(existingRecordFieldDefinition) => {
return (
columnDefinition.fieldMetadataId ===
existingRecordFieldDefinition.fieldMetadataId &&
existingRecordFieldDefinition.isVisible
);
},
);
}),
[columnDefinitions, recordIndexFieldDefinitions],
);
const handleReorderBoardFields: OnDragEndResponder = useCallback(
(result) => {
if (
!result.destination ||
result.destination.index === 1 ||
result.source.index === 1
) {
return;
}
const reorderFields = [...recordIndexFieldDefinitions];
const [removed] = reorderFields.splice(result.source.index - 1, 1);
reorderFields.splice(result.destination.index - 1, 0, removed);
const updatedFields = reorderFields.map((field, index) => ({
...field,
position: index,
}));
setRecordIndexFieldDefinitions(updatedFields);
persistViewFields(mapBoardFieldDefinitionsToViewFields(updatedFields));
},
[
persistViewFields,
recordIndexFieldDefinitions,
setRecordIndexFieldDefinitions,
],
);
// Todo : this seems over complex and should at least be extracted to an util with unit test.
// Let's refactor this as we introduce the new viewBar
const handleBoardFieldVisibilityChange = useCallback(
async (
updatedFieldDefinition: Omit<
ColumnDefinition<FieldMetadata>,
'size' | 'position'
>,
) => {
const isNewViewField = !recordIndexFieldDefinitions.some(
(fieldDefinition) =>
fieldDefinition.fieldMetadataId ===
updatedFieldDefinition.fieldMetadataId,
);
let updatedFieldsDefinitions: ColumnDefinition<FieldMetadata>[];
if (isNewViewField) {
const correspondingFieldDefinition = columnDefinitions.find(
(availableTableColumn) =>
availableTableColumn.fieldMetadataId ===
updatedFieldDefinition.fieldMetadataId,
);
if (!correspondingFieldDefinition) return;
updatedFieldsDefinitions = [
...recordIndexFieldDefinitions,
{ ...correspondingFieldDefinition, isVisible: true },
];
} else {
updatedFieldsDefinitions = recordIndexFieldDefinitions.map(
(exitingFieldDefinition) =>
exitingFieldDefinition.fieldMetadataId ===
updatedFieldDefinition.fieldMetadataId
? {
...exitingFieldDefinition,
isVisible: !updatedFieldDefinition.isVisible,
}
: exitingFieldDefinition,
);
}
setRecordIndexFieldDefinitions(updatedFieldsDefinitions);
persistViewFields(
mapBoardFieldDefinitionsToViewFields(updatedFieldsDefinitions),
);
},
[
recordIndexFieldDefinitions,
setRecordIndexFieldDefinitions,
persistViewFields,
columnDefinitions,
],
);
return {
handleReorderBoardFields,
handleBoardFieldVisibilityChange,
visibleBoardFields,
hiddenBoardFields,
};
};

View File

@ -0,0 +1,44 @@
import { useCallback } from 'react';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
export const useRecordIndexOptionsForTable = (recordTableId: string) => {
const { getHiddenTableColumnsSelector, getVisibleTableColumnsSelector } =
useRecordTableStates(recordTableId);
const hiddenTableColumns = useRecoilValue(getHiddenTableColumnsSelector());
const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector());
const { handleColumnVisibilityChange, handleColumnReorder } = useTableColumns(
{ recordTableId: recordTableId },
);
const handleReorderColumns: OnDragEndResponder = useCallback(
(result) => {
if (
!result.destination ||
result.destination.index === 1 ||
result.source.index === 1
) {
return;
}
const reorderFields = [...visibleTableColumns];
const [removed] = reorderFields.splice(result.source.index - 1, 1);
reorderFields.splice(result.destination.index - 1, 0, removed);
handleColumnReorder(reorderFields);
},
[visibleTableColumns, handleColumnReorder],
);
return {
handleReorderColumns,
handleColumnVisibilityChange,
visibleTableColumns,
hiddenTableColumns,
};
};

View File

@ -0,0 +1,23 @@
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
type useRecordIndexOptionsImportParams = {
objectNameSingular: string;
};
export const useRecordIndexOptionsImport = ({
objectNameSingular,
}: useRecordIndexOptionsImportParams) => {
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
const handleImport =
CoreObjectNameSingular.Company === objectNameSingular
? openCompanySpreadsheetImport
: CoreObjectNameSingular.Person === objectNameSingular
? openPersonSpreadsheetImport
: undefined;
return { handleImport };
};

View File

@ -1,2 +0,0 @@
// We should either apply the constant all caps case or maybe define a more general enum to store those ids ?
export const TableOptionsDropdownId = 'table-options-dropdown-id';

View File

@ -1,34 +0,0 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useViewBar } from '@/views/hooks/useViewBar';
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
import { TableOptionsDropdownButton } from './TableOptionsDropdownButton';
import { TableOptionsDropdownContent } from './TableOptionsDropdownContent';
export const TableOptionsDropdown = ({
onImport,
recordTableId,
}: {
onImport?: () => void;
recordTableId: string;
}) => {
const { setViewEditMode } = useViewBar();
return (
<Dropdown
dropdownId={TableOptionsDropdownId}
clickableComponent={<TableOptionsDropdownButton />}
dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
dropdownOffset={{ y: 8 }}
dropdownComponents={
<TableOptionsDropdownContent
onImport={onImport}
recordTableId={recordTableId}
/>
}
onClickOutside={() => setViewEditMode('none')}
/>
);
};

View File

@ -1,10 +1,11 @@
import styled from '@emotion/styled';
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewType } from '@/views/types/ViewType';
const StyledContainer = styled.div`
display: flex;
@ -16,7 +17,7 @@ const StyledContainer = styled.div`
export const SignInBackgroundMockContainer = () => {
const objectNamePlural = 'companies';
const objectNameSingular = 'company';
const recordTableId = 'sign-up-mock-record-table-id';
const recordIndexId = 'sign-up-mock-record-table-id';
const viewBarId = 'companies-mock';
return (
@ -24,18 +25,22 @@ export const SignInBackgroundMockContainer = () => {
<ViewBar
viewBarId={viewBarId}
optionsDropdownButton={
<TableOptionsDropdown recordTableId={recordTableId} />
<RecordIndexOptionsDropdown
recordIndexId={recordIndexId}
objectNameSingular={objectNameSingular}
viewType={ViewType.Table}
/>
}
optionsDropdownScopeId={TableOptionsDropdownId}
optionsDropdownScopeId={RECORD_INDEX_OPTIONS_DROPDOWN_ID}
/>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordTableId}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordTableId}
recordTableId={recordIndexId}
viewBarId={viewBarId}
createRecord={async () => {}}
updateRecordMutation={() => {}}