diff --git a/packages/twenty-front/src/modules/companies/components/HooksCompanyBoardEffect.tsx b/packages/twenty-front/src/modules/companies/components/HooksCompanyBoardEffect.tsx index 7de6633ab..eb955d344 100644 --- a/packages/twenty-front/src/modules/companies/components/HooksCompanyBoardEffect.tsx +++ b/packages/twenty-front/src/modules/companies/components/HooksCompanyBoardEffect.tsx @@ -9,7 +9,6 @@ import { availableRecordBoardDeprecatedCardFieldsScopedState } from '@/object-re import { recordBoardCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedCardFieldsScopedState'; import { recordBoardFiltersScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedFiltersScopedState'; import { recordBoardSortsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedSortsScopedState'; -import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; import { useSetRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useSetRecoilScopedStateV2'; import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates'; import { useViewBar } from '@/views/hooks/useViewBar'; @@ -61,11 +60,7 @@ export const HooksCompanyBoardEffect = ({ ]); useEffect(() => { - const availableTableColumns = columnDefinitions.filter( - filterAvailableTableColumns, - ); - - setAvailableBoardCardFields(availableTableColumns); + setAvailableBoardCardFields(columnDefinitions); }, [columnDefinitions, setAvailableBoardCardFields]); useEffect(() => { diff --git a/packages/twenty-front/src/modules/companies/utils/mapBoardFieldDefinitionsToViewFields.ts b/packages/twenty-front/src/modules/companies/utils/mapBoardFieldDefinitionsToViewFields.ts index 3dd49c95c..f02d992af 100644 --- a/packages/twenty-front/src/modules/companies/utils/mapBoardFieldDefinitionsToViewFields.ts +++ b/packages/twenty-front/src/modules/companies/utils/mapBoardFieldDefinitionsToViewFields.ts @@ -1,3 +1,5 @@ +import { v4 } from 'uuid'; + import { BoardFieldDefinition } from '@/object-record/record-board-deprecated/types/BoardFieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { ViewField } from '@/views/types/ViewField'; @@ -7,7 +9,7 @@ export const mapBoardFieldDefinitionsToViewFields = ( ): ViewField[] => { return fieldsDefinitions.map( (fieldDefinition): ViewField => ({ - id: fieldDefinition.viewFieldId || '', + id: fieldDefinition.viewFieldId || v4(), fieldMetadataId: fieldDefinition.fieldMetadataId, size: 0, position: fieldDefinition.position, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts index b161fe418..5f516def5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; import { Nullable } from '~/types/Nullable'; import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition'; @@ -25,13 +26,15 @@ export const useColumnDefinitionsFromFieldMetadata = ( const columnDefinitions: ColumnDefinition[] = useMemo( () => objectMetadataItem - ? activeFieldMetadataItems.map((field, index) => - formatFieldMetadataItemAsColumnDefinition({ - position: index, - field, - objectMetadataItem, - }), - ) + ? activeFieldMetadataItems + .map((field, index) => + formatFieldMetadataItemAsColumnDefinition({ + position: index, + field, + objectMetadataItem, + }), + ) + .filter(filterAvailableTableColumns) : [], [activeFieldMetadataItems, objectMetadataItem], ); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition.ts index f55f97f0d..12a188917 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition.ts @@ -2,6 +2,7 @@ import { FieldMetadataItemAsFieldDefinitionProps, formatFieldMetadataItemAsFieldDefinition, } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; +import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; @@ -15,14 +16,22 @@ export const formatFieldMetadataItemAsColumnDefinition = ({ objectMetadataItem, showLabel, labelWidth, -}: FieldMetadataItemAsColumnDefinitionProps): ColumnDefinition => ({ - ...formatFieldMetadataItemAsFieldDefinition({ - field, +}: FieldMetadataItemAsColumnDefinitionProps): ColumnDefinition => { + const isLabelIdentifier = isLabelIdentifierField({ + fieldMetadataItem: field, objectMetadataItem, - showLabel, - labelWidth, - }), - position, - size: 100, - isVisible: true, -}); + }); + + return { + ...formatFieldMetadataItemAsFieldDefinition({ + field, + objectMetadataItem, + showLabel, + labelWidth, + }), + position: isLabelIdentifier ? 0 : position, + size: 100, + isLabelIdentifier, + isVisible: true, + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldMetadataItem.ts index d66daa2dc..ee3f97139 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldMetadataItem.ts @@ -3,7 +3,10 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; export const getLabelIdentifierFieldMetadataItem = ( - objectMetadataItem: ObjectMetadataItem, + objectMetadataItem: Pick< + ObjectMetadataItem, + 'fields' | 'labelIdentifierFieldMetadataId' + >, ): FieldMetadataItem | undefined => objectMetadataItem.fields.find((fieldMetadataItem) => isLabelIdentifierField({ diff --git a/packages/twenty-front/src/modules/object-record/record-board-deprecated/options/components/RecordBoardDeprecatedOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-board-deprecated/options/components/RecordBoardDeprecatedOptionsDropdownContent.tsx index e673f1de3..912d8135a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board-deprecated/options/components/RecordBoardDeprecatedOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board-deprecated/options/components/RecordBoardDeprecatedOptionsDropdownContent.tsx @@ -26,6 +26,7 @@ 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 { moveArrayItem } from '~/utils/array/moveArrayItem'; import { useRecordBoardDeprecatedCardFieldsInternal } from '../../hooks/internal/useRecordBoardDeprecatedCardFieldsInternal'; import { BoardColumnDefinition } from '../../types/BoardColumnDefinition'; @@ -113,11 +114,12 @@ export const RecordBoardDeprecatedOptionsDropdownContent = ({ return; } - const reorderFields = [...visibleBoardCardFields]; - const [removed] = reorderFields.splice(result.source.index - 1, 1); - reorderFields.splice(result.destination.index - 1, 0, removed); + const reorderedFields = moveArrayItem(visibleBoardCardFields, { + fromIndex: result.source.index - 1, + toIndex: result.destination.index - 1, + }); - handleFieldsReorder(reorderFields); + handleFieldsReorder(reorderedFields); }, [handleFieldsReorder, visibleBoardCardFields], ); @@ -217,10 +219,9 @@ export const RecordBoardDeprecatedOptionsDropdownContent = ({ )} {hasVisibleFields && hasHiddenFields && } @@ -228,9 +229,8 @@ export const RecordBoardDeprecatedOptionsDropdownContent = ({ )} diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index dd67a17e3..6c522797f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -74,25 +74,29 @@ export const RecordIndexContainer = ({ const onViewFieldsChange = useRecoilCallback( ({ set, snapshot }) => (viewFields: ViewField[]) => { - setTableColumns( - mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions), + const newFieldDefinitions = mapViewFieldsToColumnDefinitions({ + viewFields, + columnDefinitions, + }); + + setTableColumns(newFieldDefinitions); + + const newRecordIndexFieldDefinitions = newFieldDefinitions.filter( + (boardField) => !boardField.isLabelIdentifier, ); const existingRecordIndexFieldDefinitions = snapshot .getLoadable(recordIndexFieldDefinitionsState) .getValue(); - const newFieldDefinitions = mapViewFieldsToColumnDefinitions( - viewFields, - columnDefinitions, - ); if ( !isDeeplyEqual( existingRecordIndexFieldDefinitions, - newFieldDefinitions, + newRecordIndexFieldDefinitions, ) - ) - set(recordIndexFieldDefinitionsState, newFieldDefinitions); + ) { + set(recordIndexFieldDefinitionsState, newRecordIndexFieldDefinitions); + } }, [columnDefinitions, setTableColumns], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index eab492600..69d2a50ed 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -5,7 +5,6 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; -import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; import { useViewBar } from '@/views/hooks/useViewBar'; type RecordIndexTableContainerEffectProps = { @@ -32,7 +31,7 @@ export const RecordIndexTableContainerEffect = ({ objectNameSingular, }); - const { columnDefinitions, filterDefinitions, sortDefinitions } = + const { columnDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); const { setEntityCountInCurrentView } = useViewBar({ @@ -40,18 +39,8 @@ export const RecordIndexTableContainerEffect = ({ }); useEffect(() => { - const availableTableColumns = columnDefinitions.filter( - filterAvailableTableColumns, - ); - - setAvailableTableColumns(availableTableColumns); - }, [ - columnDefinitions, - objectMetadataItem, - sortDefinitions, - filterDefinitions, - setAvailableTableColumns, - ]); + setAvailableTableColumns(columnDefinitions); + }, [columnDefinitions, setAvailableTableColumns]); const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index f731724e7..59831da29 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -159,10 +159,9 @@ export const RecordIndexOptionsDropdownContent = ({ {hiddenRecordFields.length > 0 && ( <> @@ -170,9 +169,8 @@ export const RecordIndexOptionsDropdownContent = ({ )} diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard.ts index 6d27f8f1f..0b53cbd7f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard.ts @@ -9,10 +9,12 @@ import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoar 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 { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; import { useViewFields } from '@/views/hooks/internal/useViewFields'; import { useViews } from '@/views/hooks/internal/useViews'; import { GraphQLView } from '@/views/types/GraphQLView'; +import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; +import { moveArrayItem } from '~/utils/array/moveArrayItem'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type useRecordIndexOptionsForBoardParams = { objectNameSingular: string; @@ -40,74 +42,82 @@ export const useRecordIndexOptionsForBoard = ({ objectNameSingular, }); - const { columnDefinitions: availableColumnDefinitions } = + const { columnDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); - // Todo replace this with label identifier logic - const columnDefinitions = availableColumnDefinitions - .filter( - (columnDefinition) => columnDefinition.metadata.fieldName !== 'name', - ) - .filter(filterAvailableTableColumns); + const availableColumnDefinitions = useMemo( + () => + columnDefinitions.filter(({ isLabelIdentifier }) => !isLabelIdentifier), + [columnDefinitions], + ); + + const recordIndexFieldDefinitionsByKey = useMemo( + () => + mapArrayToObject( + recordIndexFieldDefinitions, + ({ fieldMetadataId }) => fieldMetadataId, + ), + [recordIndexFieldDefinitions], + ); const visibleBoardFields = useMemo( () => - columnDefinitions.filter((columnDefinition) => { - return recordIndexFieldDefinitions.some( - (existingRecordFieldDefinition) => { - return ( - columnDefinition.fieldMetadataId === - existingRecordFieldDefinition.fieldMetadataId && - existingRecordFieldDefinition.isVisible - ); - }, - ); - }), - [columnDefinitions, recordIndexFieldDefinitions], + recordIndexFieldDefinitions + .filter((boardField) => boardField.isVisible) + .sort( + (boardFieldA, boardFieldB) => + boardFieldA.position - boardFieldB.position, + ), + [recordIndexFieldDefinitions], ); const hiddenBoardFields = useMemo( () => - columnDefinitions.filter((columnDefinition) => { - return !recordIndexFieldDefinitions.some( - (existingRecordFieldDefinition) => { - return ( - columnDefinition.fieldMetadataId === - existingRecordFieldDefinition.fieldMetadataId && - existingRecordFieldDefinition.isVisible - ); - }, - ); - }), - [columnDefinitions, recordIndexFieldDefinitions], + availableColumnDefinitions + .filter( + ({ fieldMetadataId }) => + !recordIndexFieldDefinitionsByKey[fieldMetadataId]?.isVisible, + ) + .map((availableColumnDefinition) => { + const { fieldMetadataId } = availableColumnDefinition; + const existingBoardField = + recordIndexFieldDefinitionsByKey[fieldMetadataId]; + + return { + ...(existingBoardField || availableColumnDefinition), + isVisible: false, + }; + }), + [availableColumnDefinitions, recordIndexFieldDefinitionsByKey], ); const handleReorderBoardFields: OnDragEndResponder = useCallback( (result) => { - if ( - !result.destination || - result.destination.index === 1 || - result.source.index === 1 - ) { + if (!result.destination) { return; } - const reorderFields = [...recordIndexFieldDefinitions]; - const [removed] = reorderFields.splice(result.source.index - 1, 1); - reorderFields.splice(result.destination.index - 1, 0, removed); + const reorderedVisibleBoardFields = moveArrayItem(visibleBoardFields, { + fromIndex: result.source.index - 1, + toIndex: result.destination.index - 1, + }); - const updatedFields = reorderFields.map((field, index) => ({ - ...field, - position: index, - })); + if (isDeeplyEqual(visibleBoardFields, reorderedVisibleBoardFields)) + return; + + const updatedFields = [ + ...reorderedVisibleBoardFields, + ...hiddenBoardFields, + ].map((field, index) => ({ ...field, position: index })); setRecordIndexFieldDefinitions(updatedFields); persistViewFields(mapBoardFieldDefinitionsToViewFields(updatedFields)); }, [ + hiddenBoardFields, persistViewFields, - recordIndexFieldDefinitions, setRecordIndexFieldDefinitions, + visibleBoardFields, ], ); @@ -120,36 +130,43 @@ export const useRecordIndexOptionsForBoard = ({ 'size' | 'position' >, ) => { - const isNewViewField = !recordIndexFieldDefinitions.some( - (fieldDefinition) => - fieldDefinition.fieldMetadataId === - updatedFieldDefinition.fieldMetadataId, + const isNewViewField = !( + updatedFieldDefinition.fieldMetadataId in + recordIndexFieldDefinitionsByKey ); let updatedFieldsDefinitions: ColumnDefinition[]; if (isNewViewField) { - const correspondingFieldDefinition = columnDefinitions.find( - (availableTableColumn) => - availableTableColumn.fieldMetadataId === + const correspondingFieldDefinition = availableColumnDefinitions.find( + (availableColumnDefinition) => + availableColumnDefinition.fieldMetadataId === updatedFieldDefinition.fieldMetadataId, ); + if (!correspondingFieldDefinition) return; + const lastVisibleBoardField = + visibleBoardFields[visibleBoardFields.length - 1]; + updatedFieldsDefinitions = [ ...recordIndexFieldDefinitions, - { ...correspondingFieldDefinition, isVisible: true }, + { + ...correspondingFieldDefinition, + position: lastVisibleBoardField.position + 1, + isVisible: true, + }, ]; } else { updatedFieldsDefinitions = recordIndexFieldDefinitions.map( - (exitingFieldDefinition) => - exitingFieldDefinition.fieldMetadataId === + (existingFieldDefinition) => + existingFieldDefinition.fieldMetadataId === updatedFieldDefinition.fieldMetadataId ? { - ...exitingFieldDefinition, - isVisible: !exitingFieldDefinition.isVisible, + ...existingFieldDefinition, + isVisible: !existingFieldDefinition.isVisible, } - : exitingFieldDefinition, + : existingFieldDefinition, ); } @@ -160,10 +177,12 @@ export const useRecordIndexOptionsForBoard = ({ ); }, [ - recordIndexFieldDefinitions, + recordIndexFieldDefinitionsByKey, setRecordIndexFieldDefinitions, persistViewFields, - columnDefinitions, + availableColumnDefinitions, + visibleBoardFields, + recordIndexFieldDefinitions, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForTable.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForTable.ts index f095e5ca1..011c6f87d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForTable.ts @@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; +import { moveArrayItem } from '~/utils/array/moveArrayItem'; export const useRecordIndexOptionsForTable = (recordTableId: string) => { const { getHiddenTableColumnsSelector, getVisibleTableColumnsSelector } = @@ -26,11 +27,12 @@ export const useRecordIndexOptionsForTable = (recordTableId: string) => { return; } - const reorderFields = [...visibleTableColumns]; - const [removed] = reorderFields.splice(result.source.index - 1, 1); - reorderFields.splice(result.destination.index - 1, 0, removed); + const reorderedFields = moveArrayItem(visibleTableColumns, { + fromIndex: result.source.index - 1, + toIndex: result.destination.index - 1, + }); - handleColumnReorder(reorderFields); + handleColumnReorder(reorderedFields); }, [visibleTableColumns, handleColumnReorder], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHeadWithDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHeadWithDropdown.tsx index d33e652fa..475fb4beb 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHeadWithDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHeadWithDropdown.tsx @@ -9,9 +9,6 @@ import { RecordTableColumnDropdownMenu } from './RecordTableColumnDropdownMenu'; type ColumnHeadWithDropdownProps = { column: ColumnDefinition; - isFirstColumn: boolean; - isLastColumn: boolean; - primaryColumnKey: string; }; const StyledDropdown = styled(Dropdown)` @@ -21,22 +18,12 @@ const StyledDropdown = styled(Dropdown)` export const ColumnHeadWithDropdown = ({ column, - isFirstColumn, - isLastColumn, - primaryColumnKey, }: ColumnHeadWithDropdownProps) => { return ( } - dropdownComponents={ - - } + dropdownComponents={} dropdownOffset={{ x: -1 }} dropdownPlacement="bottom-start" dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }} diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx index c92f4da5d..911bb3d87 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx @@ -1,4 +1,7 @@ +import { useRecoilValue } from 'recoil'; + import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { IconArrowLeft, IconArrowRight, IconEyeOff } from '@/ui/display/icon'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; @@ -9,17 +12,23 @@ import { ColumnDefinition } from '../types/ColumnDefinition'; export type RecordTableColumnDropdownMenuProps = { column: ColumnDefinition; - isFirstColumn: boolean; - isLastColumn: boolean; - primaryColumnKey: string; }; export const RecordTableColumnDropdownMenu = ({ column, - isFirstColumn, - isLastColumn, - primaryColumnKey, }: RecordTableColumnDropdownMenuProps) => { + const { getVisibleTableColumnsSelector } = useRecordTableStates(); + + const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector()); + + const secondVisibleColumn = visibleTableColumns[1]; + const canMoveLeft = + column.fieldMetadataId !== secondVisibleColumn?.fieldMetadataId; + + const lastVisibleColumn = visibleTableColumns[visibleTableColumns.length - 1]; + const canMoveRight = + column.fieldMetadataId !== lastVisibleColumn?.fieldMetadataId; + const { handleColumnVisibilityChange, handleMoveTableColumn } = useTableColumns(); @@ -27,17 +36,17 @@ export const RecordTableColumnDropdownMenu = ({ const handleColumnMoveLeft = () => { closeDropdown(); - if (isFirstColumn) { - return; - } + + if (!canMoveLeft) return; + handleMoveTableColumn('left', column); }; const handleColumnMoveRight = () => { closeDropdown(); - if (isLastColumn) { - return; - } + + if (!canMoveRight) return; + handleMoveTableColumn('right', column); }; @@ -46,18 +55,16 @@ export const RecordTableColumnDropdownMenu = ({ handleColumnVisibilityChange(column); }; - return column.fieldMetadataId === primaryColumnKey ? ( - <> - ) : ( + return ( - {!isFirstColumn && ( + {canMoveLeft && ( )} - {!isLastColumn && ( + {canMoveRight && ( ; createRecord: () => void; }) => { - const { - getResizeFieldOffsetState, - getTableColumnsState, - getTableColumnsByKeySelector, - getVisibleTableColumnsSelector, - } = useRecordTableStates(); + const { getResizeFieldOffsetState, getTableColumnsState } = + useRecordTableStates(); const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState( getResizeFieldOffsetState(), ); const tableColumns = useRecoilValue(getTableColumnsState()); - const tableColumnsByKey = useRecoilValue(getTableColumnsByKeySelector()); - const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector()); + const tableColumnsByKey = useMemo( + () => + mapArrayToObject(tableColumns, ({ fieldMetadataId }) => fieldMetadataId), + [tableColumns], + ); const [initialPointerPositionX, setInitialPointerPositionX] = useState< number | null @@ -108,10 +109,6 @@ export const RecordTableHeaderCell = ({ const [iconVisibility, setIconVisibility] = useState(false); - const primaryColumn = visibleTableColumns.find( - (column) => column.position === 0, - ); - const handleResizeHandlerMove = useCallback( (positionX: number) => { if (!initialPointerPositionX) return; @@ -182,13 +179,12 @@ export const RecordTableHeaderCell = ({ onMouseEnter={() => setIconVisibility(true)} onMouseLeave={() => setIconVisibility(false)} > - - {iconVisibility && column.position === 0 && ( + {column.isLabelIdentifier ? ( + + ) : ( + + )} + {iconVisibility && !!column.isLabelIdentifier && ( { - {[...visibleTableColumns] - .sort((columnA, columnB) => columnA.position - columnB.position) - .map((column, columnIndex) => { - return inView ? ( - - - - ) : ( - - ); - })} + {visibleTableColumns.map((column, columnIndex) => + inView ? ( + + + + ) : ( + + ), + )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts index 1b5b4e681..7238fec44 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts @@ -14,7 +14,6 @@ import { allRowsSelectedStatusSelectorScopeMap } from '@/object-record/record-ta import { hiddenTableColumnsSelectorScopeMap } from '@/object-record/record-table/states/selectors/hiddenTableColumnsSelectorScopeMap'; import { numberOfTableColumnsSelectorScopeMap } from '@/object-record/record-table/states/selectors/numberOfTableColumnsSelectorScopeMap'; import { selectedRowIdsSelectorScopeMap } from '@/object-record/record-table/states/selectors/selectedRowIdsSelectorScopeMap'; -import { tableColumnsByKeySelectorScopeMap } from '@/object-record/record-table/states/selectors/tableColumnsByKeySelectorScopeMap'; import { visibleTableColumnsSelectorScopeMap } from '@/object-record/record-table/states/selectors/visibleTableColumnsSelectorScopeMap'; import { softFocusPositionStateScopeMap } from '@/object-record/record-table/states/softFocusPositionStateScopeMap'; import { tableColumnsStateScopeMap } from '@/object-record/record-table/states/tableColumnsStateScopeMap'; @@ -106,10 +105,6 @@ export const useRecordTableStates = (recordTableId?: string) => { selectedRowIdsSelectorScopeMap, scopeId, ), - getTableColumnsByKeySelector: getSelectorReadOnly( - tableColumnsByKeySelectorScopeMap, - scopeId, - ), getVisibleTableColumnsSelector: getSelectorReadOnly( visibleTableColumnsSelectorScopeMap, scopeId, diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/hiddenTableColumnsSelectorScopeMap.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/hiddenTableColumnsSelectorScopeMap.ts index 221ffb2cd..1fcd14587 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/hiddenTableColumnsSelectorScopeMap.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/hiddenTableColumnsSelectorScopeMap.ts @@ -1,7 +1,7 @@ +import { availableTableColumnsStateScopeMap } from '@/object-record/record-table/states/availableTableColumnsStateScopeMap'; +import { tableColumnsStateScopeMap } from '@/object-record/record-table/states/tableColumnsStateScopeMap'; import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap'; - -import { availableTableColumnsStateScopeMap } from '../availableTableColumnsStateScopeMap'; -import { tableColumnsStateScopeMap } from '../tableColumnsStateScopeMap'; +import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; export const hiddenTableColumnsSelectorScopeMap = createSelectorReadOnlyScopeMap({ @@ -9,19 +9,30 @@ export const hiddenTableColumnsSelectorScopeMap = get: ({ scopeId }) => ({ get }) => { - const columns = get(tableColumnsStateScopeMap({ scopeId })); - const columnKeys = columns.map( + const tableColumns = get(tableColumnsStateScopeMap({ scopeId })); + const availableColumns = get( + availableTableColumnsStateScopeMap({ scopeId }), + ); + const tableColumnsByKey = mapArrayToObject( + tableColumns, ({ fieldMetadataId }) => fieldMetadataId, ); - const otherAvailableColumns = get( - availableTableColumnsStateScopeMap({ scopeId }), - ).filter( - ({ fieldMetadataId }) => !columnKeys.includes(fieldMetadataId), - ); - return [ - ...columns.filter((column) => !column.isVisible), - ...otherAvailableColumns, - ]; + const hiddenColumns = availableColumns + .filter( + ({ fieldMetadataId }) => + !tableColumnsByKey[fieldMetadataId]?.isVisible, + ) + .map((availableColumn) => { + const { fieldMetadataId } = availableColumn; + const existingTableColumn = tableColumnsByKey[fieldMetadataId]; + + return { + ...(existingTableColumn || availableColumn), + isVisible: false, + }; + }); + + return hiddenColumns; }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/tableColumnsByKeySelectorScopeMap.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/tableColumnsByKeySelectorScopeMap.ts deleted file mode 100644 index 6fdd93aa2..000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/tableColumnsByKeySelectorScopeMap.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap'; - -import { tableColumnsStateScopeMap } from '../tableColumnsStateScopeMap'; - -export const tableColumnsByKeySelectorScopeMap = createSelectorReadOnlyScopeMap( - { - key: 'tableColumnsByKeySelectorScopeMap', - get: - ({ scopeId }) => - ({ get }) => - get(tableColumnsStateScopeMap({ scopeId })).reduce< - Record> - >( - (result, column) => ({ ...result, [column.fieldMetadataId]: column }), - {}, - ), - }, -); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/visibleTableColumnsSelectorScopeMap.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/visibleTableColumnsSelectorScopeMap.ts index 1b7699967..01a818edb 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/visibleTableColumnsSelectorScopeMap.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/visibleTableColumnsSelectorScopeMap.ts @@ -1,8 +1,6 @@ -import { availableTableColumnsStateScopeMap } from '@/object-record/record-table/states/availableTableColumnsStateScopeMap'; +import { tableColumnsStateScopeMap } from '@/object-record/record-table/states/tableColumnsStateScopeMap'; import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap'; -import { tableColumnsStateScopeMap } from '../tableColumnsStateScopeMap'; - export const visibleTableColumnsSelectorScopeMap = createSelectorReadOnlyScopeMap({ key: 'visibleTableColumnsSelectorScopeMap', @@ -10,16 +8,8 @@ export const visibleTableColumnsSelectorScopeMap = ({ scopeId }) => ({ get }) => { const columns = get(tableColumnsStateScopeMap({ scopeId })); - const availableColumnKeys = get( - availableTableColumnsStateScopeMap({ scopeId }), - ).map(({ fieldMetadataId }) => fieldMetadataId); - - return [...columns] - .filter( - (column) => - column.isVisible && - availableColumnKeys.includes(column.fieldMetadataId), - ) - .sort((a, b) => a.position - b.position); + return columns + .filter((column) => column.isVisible) + .sort((columnA, columnB) => columnA.position - columnB.position); }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/types/ColumnDefinition.ts b/packages/twenty-front/src/modules/object-record/record-table/types/ColumnDefinition.ts index 902ba38d8..d65d838e4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/types/ColumnDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/types/ColumnDefinition.ts @@ -4,6 +4,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' export type ColumnDefinition = FieldDefinition & { size: number; position: number; + isLabelIdentifier?: boolean; isVisible?: boolean; viewFieldId?: string; }; diff --git a/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx b/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx index e970fb0d5..cfa7dd013 100644 --- a/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldSelectForm.tsx @@ -9,6 +9,7 @@ import { CardFooter } from '@/ui/layout/card/components/CardFooter'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { mainColorNames, ThemeColor } from '@/ui/theme/constants/colors'; +import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption'; @@ -61,10 +62,10 @@ export const SettingsObjectFieldSelectForm = ({ const handleDragEnd = (result: DropResult) => { if (!result.destination) return; - const nextOptions = [...values]; - const [movedOption] = nextOptions.splice(result.source.index, 1); - - nextOptions.splice(result.destination.index, 0, movedOption); + const nextOptions = moveArrayItem(values, { + fromIndex: result.source.index, + toIndex: result.destination.index, + }); onChange(nextOptions); }; diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx index 2d9c93baa..a65e5b444 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx @@ -4,7 +4,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; -import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; import { signInBackgroundMockColumnDefinitions, signInBackgroundMockFilterDefinitions, @@ -57,17 +56,13 @@ export const SignInBackgroundMockContainerEffect = ({ setAvailableFilterDefinitions?.(signInBackgroundMockFilterDefinitions); setAvailableFieldDefinitions?.(signInBackgroundMockColumnDefinitions); - const availableTableColumns = signInBackgroundMockColumnDefinitions.filter( - filterAvailableTableColumns, - ); - - setAvailableTableColumns(availableTableColumns); + setAvailableTableColumns(signInBackgroundMockColumnDefinitions); setTableColumns( - mapViewFieldsToColumnDefinitions( - signInBackgroundMockViewFields, - signInBackgroundMockColumnDefinitions, - ), + mapViewFieldsToColumnDefinitions({ + viewFields: signInBackgroundMockViewFields, + columnDefinitions: signInBackgroundMockColumnDefinitions, + }), ); }, [ setViewObjectMetadataId, diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/signInBackgroundMockDefinitions.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/signInBackgroundMockDefinitions.ts index d8d3868bc..80a0cb1ce 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/signInBackgroundMockDefinitions.ts +++ b/packages/twenty-front/src/modules/sign-in-background-mock/constants/signInBackgroundMockDefinitions.ts @@ -1,255 +1,259 @@ import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition'; +import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; -export const signInBackgroundMockColumnDefinitions = [ - { - position: 0, - fieldMetadataId: '20202020-5e4e-4007-a630-8a2617914889', - label: 'Domain Name', - size: 100, - type: 'TEXT', - metadata: { - fieldName: 'domainName', - placeHolder: 'Domain Name', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', +export const signInBackgroundMockColumnDefinitions = ( + [ + { + position: 0, + fieldMetadataId: '20202020-5e4e-4007-a630-8a2617914889', + label: 'Domain Name', + size: 100, + type: 'TEXT', + metadata: { + fieldName: 'domainName', + placeHolder: 'Domain Name', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconLink', + isVisible: true, }, - iconName: 'IconLink', - isVisible: true, - }, - { - position: 1, - fieldMetadataId: '20202020-7fbd-41ad-b64d-25a15ff62f04', - label: 'Employees', - size: 100, - type: 'NUMBER', - metadata: { - fieldName: 'employees', - placeHolder: 'Employees', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 1, + fieldMetadataId: '20202020-7fbd-41ad-b64d-25a15ff62f04', + label: 'Employees', + size: 100, + type: 'NUMBER', + metadata: { + fieldName: 'employees', + placeHolder: 'Employees', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconUsers', + isVisible: true, }, - iconName: 'IconUsers', - isVisible: true, - }, - { - position: 2, - fieldMetadataId: '20202020-6d30-4111-9f40-b4301906fd3c', - label: 'Name', - size: 100, - type: 'TEXT', - metadata: { - fieldName: 'name', - placeHolder: 'Name', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 2, + fieldMetadataId: '20202020-6d30-4111-9f40-b4301906fd3c', + label: 'Name', + size: 100, + type: 'TEXT', + metadata: { + fieldName: 'name', + placeHolder: 'Name', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconBuildingSkyscraper', + isVisible: true, }, - iconName: 'IconBuildingSkyscraper', - isVisible: true, - }, - { - position: 3, - fieldMetadataId: '20202020-e7c8-4771-8cc4-ce0e8c36a3c0', - label: 'Favorites', - size: 100, - type: 'RELATION', - metadata: { - fieldName: 'favorites', - placeHolder: 'Favorites', - relationType: 'FROM_MANY_OBJECTS', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 3, + fieldMetadataId: '20202020-e7c8-4771-8cc4-ce0e8c36a3c0', + label: 'Favorites', + size: 100, + type: 'RELATION', + metadata: { + fieldName: 'favorites', + placeHolder: 'Favorites', + relationType: 'FROM_MANY_OBJECTS', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconHeart', + isVisible: true, }, - iconName: 'IconHeart', - isVisible: true, - }, - { - position: 4, - fieldMetadataId: '20202020-ad10-4117-a039-3f04b7a5f939', - label: 'Address', - size: 100, - type: 'TEXT', - metadata: { - fieldName: 'address', - placeHolder: 'Address', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 4, + fieldMetadataId: '20202020-ad10-4117-a039-3f04b7a5f939', + label: 'Address', + size: 100, + type: 'TEXT', + metadata: { + fieldName: 'address', + placeHolder: 'Address', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconMap', + isVisible: true, }, - iconName: 'IconMap', - isVisible: true, - }, - { - position: 5, - fieldMetadataId: '20202020-0739-495d-8e70-c0807f6b2268', - label: 'Account Owner', - size: 100, - type: 'RELATION', - metadata: { - fieldName: 'accountOwner', - placeHolder: 'Account Owner', - relationType: 'TO_ONE_OBJECT', - relationObjectMetadataNameSingular: 'workspaceMember', - relationObjectMetadataNamePlural: 'workspaceMembers', - objectMetadataNameSingular: 'company', + { + position: 5, + fieldMetadataId: '20202020-0739-495d-8e70-c0807f6b2268', + label: 'Account Owner', + size: 100, + type: 'RELATION', + metadata: { + fieldName: 'accountOwner', + placeHolder: 'Account Owner', + relationType: 'TO_ONE_OBJECT', + relationObjectMetadataNameSingular: 'workspaceMember', + relationObjectMetadataNamePlural: 'workspaceMembers', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconUserCircle', + isVisible: true, }, - iconName: 'IconUserCircle', - isVisible: true, - }, - { - position: 6, - fieldMetadataId: '20202020-68b4-4c8e-af19-738eba2a42a5', - label: 'People', - size: 100, - type: 'RELATION', - metadata: { - fieldName: 'people', - placeHolder: 'People', - relationType: 'FROM_MANY_OBJECTS', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 6, + fieldMetadataId: '20202020-68b4-4c8e-af19-738eba2a42a5', + label: 'People', + size: 100, + type: 'RELATION', + metadata: { + fieldName: 'people', + placeHolder: 'People', + relationType: 'FROM_MANY_OBJECTS', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconUsers', + isVisible: true, }, - iconName: 'IconUsers', - isVisible: true, - }, - { - position: 7, - fieldMetadataId: '20202020-61af-4ffd-b79b-baed6db8ad11', - label: 'Attachments', - size: 100, - type: 'RELATION', - metadata: { - fieldName: 'attachments', - placeHolder: 'Attachments', - relationType: 'FROM_MANY_OBJECTS', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 7, + fieldMetadataId: '20202020-61af-4ffd-b79b-baed6db8ad11', + label: 'Attachments', + size: 100, + type: 'RELATION', + metadata: { + fieldName: 'attachments', + placeHolder: 'Attachments', + relationType: 'FROM_MANY_OBJECTS', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconFileImport', + isVisible: true, }, - iconName: 'IconFileImport', - isVisible: true, - }, - { - position: 8, - fieldMetadataId: '20202020-4dc2-47c9-bb15-6e6f19ba9e46', - label: 'Creation date', - size: 100, - type: 'DATE_TIME', - metadata: { - fieldName: 'createdAt', - placeHolder: 'Creation date', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 8, + fieldMetadataId: '20202020-4dc2-47c9-bb15-6e6f19ba9e46', + label: 'Creation date', + size: 100, + type: 'DATE_TIME', + metadata: { + fieldName: 'createdAt', + placeHolder: 'Creation date', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconCalendar', + isVisible: true, }, - iconName: 'IconCalendar', - isVisible: true, - }, - { - position: 9, - fieldMetadataId: '20202020-9e9f-4235-98b2-c76f3e2d281e', - label: 'ICP', - size: 100, - type: 'BOOLEAN', - metadata: { - fieldName: 'idealCustomerProfile', - placeHolder: 'ICP', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 9, + fieldMetadataId: '20202020-9e9f-4235-98b2-c76f3e2d281e', + label: 'ICP', + size: 100, + type: 'BOOLEAN', + metadata: { + fieldName: 'idealCustomerProfile', + placeHolder: 'ICP', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconTarget', + isVisible: true, }, - iconName: 'IconTarget', - isVisible: true, - }, - { - position: 10, - fieldMetadataId: '20202020-a61d-4b78-b998-3fd88b4f73a1', - label: 'Linkedin', - size: 100, - type: 'LINK', - metadata: { - fieldName: 'linkedinLink', - placeHolder: 'Linkedin', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 10, + fieldMetadataId: '20202020-a61d-4b78-b998-3fd88b4f73a1', + label: 'Linkedin', + size: 100, + type: 'LINK', + metadata: { + fieldName: 'linkedinLink', + placeHolder: 'Linkedin', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconBrandLinkedin', + isVisible: true, }, - iconName: 'IconBrandLinkedin', - isVisible: true, - }, - { - position: 11, - fieldMetadataId: '20202020-e3fc-46ff-b552-3e757843f06e', - label: 'Opportunities', - size: 100, - type: 'RELATION', - metadata: { - fieldName: 'opportunities', - placeHolder: 'Opportunities', - relationType: 'FROM_MANY_OBJECTS', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 11, + fieldMetadataId: '20202020-e3fc-46ff-b552-3e757843f06e', + label: 'Opportunities', + size: 100, + type: 'RELATION', + metadata: { + fieldName: 'opportunities', + placeHolder: 'Opportunities', + relationType: 'FROM_MANY_OBJECTS', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconTargetArrow', + isVisible: true, }, - iconName: 'IconTargetArrow', - isVisible: true, - }, - { - position: 12, - fieldMetadataId: '20202020-46e3-479a-b8f4-77137c74daa6', - label: 'X', - size: 100, - type: 'LINK', - metadata: { - fieldName: 'xLink', - placeHolder: 'X', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 12, + fieldMetadataId: '20202020-46e3-479a-b8f4-77137c74daa6', + label: 'X', + size: 100, + type: 'LINK', + metadata: { + fieldName: 'xLink', + placeHolder: 'X', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconBrandX', + isVisible: true, }, - iconName: 'IconBrandX', - isVisible: true, - }, - { - position: 13, - fieldMetadataId: '20202020-4a2e-4b41-8562-279963e8947e', - label: 'Activities', - size: 100, - type: 'RELATION', - metadata: { - fieldName: 'activityTargets', - placeHolder: 'Activities', - relationType: 'FROM_MANY_OBJECTS', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 13, + fieldMetadataId: '20202020-4a2e-4b41-8562-279963e8947e', + label: 'Activities', + size: 100, + type: 'RELATION', + metadata: { + fieldName: 'activityTargets', + placeHolder: 'Activities', + relationType: 'FROM_MANY_OBJECTS', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconCheckbox', + isVisible: true, }, - iconName: 'IconCheckbox', - isVisible: true, - }, - { - position: 14, - fieldMetadataId: '20202020-4a5a-466f-92d9-c3870d9502a9', - label: 'ARR', - size: 100, - type: 'CURRENCY', - metadata: { - fieldName: 'annualRecurringRevenue', - placeHolder: 'ARR', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', + { + position: 14, + fieldMetadataId: '20202020-4a5a-466f-92d9-c3870d9502a9', + label: 'ARR', + size: 100, + type: 'CURRENCY', + metadata: { + fieldName: 'annualRecurringRevenue', + placeHolder: 'ARR', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconMoneybag', + isVisible: true, }, - iconName: 'IconMoneybag', - isVisible: true, - }, -] as ColumnDefinition[]; + ] satisfies ColumnDefinition[] +).filter(filterAvailableTableColumns); export const signInBackgroundMockFilterDefinitions = [ { diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx index 9e8c6d0ee..32a784192 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx @@ -19,26 +19,25 @@ import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/Sty import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy'; import { isDefined } from '~/utils/isDefined'; type ViewFieldsVisibilityDropdownSectionProps = { fields: Omit, 'size'>[]; + isDraggable: boolean; + onDragEnd?: OnDragEndResponder; onVisibilityChange: ( field: Omit, 'size' | 'position'>, ) => void; title: string; - isVisible: boolean; - isDraggable: boolean; - onDragEnd?: OnDragEndResponder; }; export const ViewFieldsVisibilityDropdownSection = ({ fields, - onVisibilityChange, - title, - isVisible, isDraggable, onDragEnd, + onVisibilityChange, + title, }: ViewFieldsVisibilityDropdownSectionProps) => { const handleOnDrag = (result: DropResult, provided: ResponderProvided) => { onDragEnd?.(result, provided); @@ -47,8 +46,7 @@ export const ViewFieldsVisibilityDropdownSection = ({ const [openToolTipIndex, setOpenToolTipIndex] = useState(); const handleInfoButtonClick = (index: number) => { - if (index === openToolTipIndex) setOpenToolTipIndex(undefined); - else setOpenToolTipIndex(index); + setOpenToolTipIndex(index === openToolTipIndex ? undefined : index); }; const { getIcon } = useIcons(); @@ -57,27 +55,23 @@ export const ViewFieldsVisibilityDropdownSection = ({ index: number, field: Omit, 'size' | 'position'>, ) => { - if (field.infoTooltipContent) { - return [ - { - Icon: IconInfoCircle, - onClick: () => handleInfoButtonClick(index), - isActive: openToolTipIndex === index, - }, - { - Icon: field.isVisible ? IconMinus : IconPlus, - onClick: () => onVisibilityChange(field), - }, - ]; - } - if (!field.infoTooltipContent) { - return [ - { - Icon: isVisible ? IconMinus : IconPlus, - onClick: () => onVisibilityChange(field), - }, - ]; - } + const iconButtons = [ + field.infoTooltipContent + ? { + Icon: IconInfoCircle, + onClick: () => handleInfoButtonClick(index), + isActive: openToolTipIndex === index, + } + : null, + field.isLabelIdentifier + ? null + : { + Icon: field.isVisible ? IconMinus : IconPlus, + onClick: () => onVisibilityChange(field), + }, + ].filter(isDefined); + + return iconButtons.length ? iconButtons : undefined; }; const ref = useRef(null); @@ -89,50 +83,55 @@ export const ViewFieldsVisibilityDropdownSection = ({ }, }); + const { nonDraggableItems = [], draggableItems = [] } = isDraggable + ? groupArrayItemsBy(fields, ({ isLabelIdentifier }) => + isLabelIdentifier ? 'nonDraggableItems' : 'draggableItems', + ) + : { nonDraggableItems: fields, draggableItems: [] }; + return (
{title} - {isDraggable ? ( + {nonDraggableItems.map((field, fieldIndex) => ( + + ))} + {!!draggableItems.length && ( - {[...fields] - .sort((a, b) => a.position - b.position) - .map((field, index) => ( + {draggableItems.map((field, index) => { + const fieldIndex = index + nonDraggableItems.length; + + return ( } /> - ))} + ); + })} } /> - ) : ( - fields.map((field, index) => ( - - )) )} {isDefined(openToolTipIndex) && diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts index b003b324e..74d1f6d66 100644 --- a/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts +++ b/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts @@ -133,7 +133,7 @@ describe('mapViewFieldsToColumnDefinitions', () => { }, ]; - const fieldsMetadata: ColumnDefinition[] = [ + const columnDefinitions: ColumnDefinition[] = [ { fieldMetadataId: '1', label: 'label 1', @@ -183,10 +183,10 @@ describe('mapViewFieldsToColumnDefinitions', () => { }, ]; - const actualColumnDefinitions = mapViewFieldsToColumnDefinitions( + const actualColumnDefinitions = mapViewFieldsToColumnDefinitions({ + columnDefinitions, viewFields, - fieldsMetadata, - ); + }); expect(actualColumnDefinitions).toEqual(expectedColumnDefinitions); }); diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts index 638574dc9..66fe0e0f7 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts @@ -1,33 +1,66 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; +import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { assertNotNull } from '~/utils/assert'; import { ViewField } from '../types/ViewField'; -export const mapViewFieldsToColumnDefinitions = ( - viewFields: ViewField[], - fieldsMetadata: ColumnDefinition[], -): ColumnDefinition[] => { - return viewFields - .map((viewField) => { - const correspondingFieldMetadata = fieldsMetadata.find( - ({ fieldMetadataId }) => viewField.fieldMetadataId === fieldMetadataId, - ); +export const mapViewFieldsToColumnDefinitions = ({ + columnDefinitions, + viewFields, +}: { + columnDefinitions: ColumnDefinition[]; + viewFields: ViewField[]; +}): ColumnDefinition[] => { + let labelIdentifierFieldMetadataId = ''; - return correspondingFieldMetadata - ? { - fieldMetadataId: viewField.fieldMetadataId, - label: correspondingFieldMetadata.label, - metadata: correspondingFieldMetadata.metadata, - infoTooltipContent: correspondingFieldMetadata.infoTooltipContent, - iconName: correspondingFieldMetadata.iconName, - type: correspondingFieldMetadata.type, - position: viewField.position, - size: viewField.size ?? correspondingFieldMetadata.size, - isVisible: viewField.isVisible, - viewFieldId: viewField.id, - } - : null; + const columnDefinitionsByFieldMetadataId = mapArrayToObject( + columnDefinitions, + ({ fieldMetadataId }) => fieldMetadataId, + ); + + const columnDefinitionsFromViewFields = viewFields + .map((viewField) => { + const correspondingColumnDefinition = + columnDefinitionsByFieldMetadataId[viewField.fieldMetadataId]; + + if (!correspondingColumnDefinition) return null; + + const { isLabelIdentifier } = correspondingColumnDefinition; + + if (isLabelIdentifier) { + labelIdentifierFieldMetadataId = + correspondingColumnDefinition.fieldMetadataId; + } + + return { + fieldMetadataId: viewField.fieldMetadataId, + label: correspondingColumnDefinition.label, + metadata: correspondingColumnDefinition.metadata, + infoTooltipContent: correspondingColumnDefinition.infoTooltipContent, + iconName: correspondingColumnDefinition.iconName, + type: correspondingColumnDefinition.type, + position: isLabelIdentifier ? 0 : viewField.position, + size: viewField.size ?? correspondingColumnDefinition.size, + isLabelIdentifier, + isVisible: isLabelIdentifier || viewField.isVisible, + viewFieldId: viewField.id, + }; }) .filter(assertNotNull); + + // No label identifier set for this object + if (!labelIdentifierFieldMetadataId) return columnDefinitionsFromViewFields; + + const labelIdentifierIndex = columnDefinitionsFromViewFields.findIndex( + ({ fieldMetadataId }) => fieldMetadataId === labelIdentifierFieldMetadataId, + ); + + // Label identifier field found in view fields + // => move it to the start of the list + return moveArrayItem(columnDefinitionsFromViewFields, { + fromIndex: labelIdentifierIndex, + toIndex: 0, + }); }; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index 466180017..4c19183a0 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -4,6 +4,7 @@ import { useSetRecoilState } from 'recoil'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { IconBuildingSkyscraper } from '@/ui/display/icon'; @@ -14,10 +15,9 @@ import { PageHeader } from '@/ui/layout/page/PageHeader'; import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton'; import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton'; import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; -import { useFindOneRecord } from '../../modules/object-record/hooks/useFindOneRecord'; - export const RecordShowPage = () => { const { objectNameSingular, objectRecordId } = useParams<{ objectNameSingular: string; @@ -32,9 +32,8 @@ export const RecordShowPage = () => { throw new Error(`Record id is not defined`); } - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); + const { labelIdentifierFieldMetadata, objectMetadataItem } = + useObjectMetadataItem({ objectNameSingular }); const { favorites, createFavorite, deleteFavorite } = useFavorites(); @@ -68,10 +67,15 @@ export const RecordShowPage = () => { } }; + const labelIdentifierFieldValue = + record?.[labelIdentifierFieldMetadata?.name ?? '']; const pageName = - objectNameSingular === 'person' - ? record?.name.firstName + ' ' + record?.name.lastName - : record?.name; + labelIdentifierFieldMetadata?.type === FieldMetadataType.FullName + ? [ + labelIdentifierFieldValue?.firstName, + labelIdentifierFieldValue?.lastName, + ].join(' ') + : labelIdentifierFieldValue; return ( diff --git a/packages/twenty-front/src/utils/array/groupArrayItemsBy.ts b/packages/twenty-front/src/utils/array/groupArrayItemsBy.ts new file mode 100644 index 000000000..61ff135d3 --- /dev/null +++ b/packages/twenty-front/src/utils/array/groupArrayItemsBy.ts @@ -0,0 +1,13 @@ +export const groupArrayItemsBy = ( + array: Item[], + computeGroupKey: (item: Item) => Key, +) => + array.reduce>>((result, item) => { + const groupKey = computeGroupKey(item); + const previousGroup = result[groupKey] || []; + + return { + ...result, + [groupKey]: [...previousGroup, item], + }; + }, {}); diff --git a/packages/twenty-front/src/utils/array/mapArrayToObject.ts b/packages/twenty-front/src/utils/array/mapArrayToObject.ts new file mode 100644 index 000000000..816ee7dc3 --- /dev/null +++ b/packages/twenty-front/src/utils/array/mapArrayToObject.ts @@ -0,0 +1,4 @@ +export const mapArrayToObject = ( + array: ArrayItem[], + computeItemKey: (item: ArrayItem) => string, +) => Object.fromEntries(array.map((item) => [computeItemKey(item), item])); diff --git a/packages/twenty-front/src/utils/array/moveArrayItem.ts b/packages/twenty-front/src/utils/array/moveArrayItem.ts new file mode 100644 index 000000000..1f4aa7127 --- /dev/null +++ b/packages/twenty-front/src/utils/array/moveArrayItem.ts @@ -0,0 +1,14 @@ +export const moveArrayItem = ( + array: Item[], + { fromIndex, toIndex }: { fromIndex: number; toIndex: number }, +) => { + if (!(fromIndex in array) || !(toIndex in array) || fromIndex === toIndex) { + return array; + } + + const arrayWithMovedItem = [...array]; + const [itemToMove] = arrayWithMovedItem.splice(fromIndex, 1); + arrayWithMovedItem.splice(toIndex, 0, itemToMove); + + return arrayWithMovedItem; +};