From 00478152e00ae7037391afa4e2c9c195ae42b89e Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 20 Aug 2025 12:03:12 +0200 Subject: [PATCH] Refactor field definition and column definition manipulation with record field (#13992) This PR introduces the usage of record fields in the manipulation of table columns and board fields. Since all of the actual system relies on states like tableColumns, recordIndexFieldDefinitions and the likes, it was required to implement temporary utils that modifies those states in parallel of the new generic currentRecordFields, to keep a working product. With this PR though, currentRecordFields becomes the single source of truth that gets saved to DB, and the remaining work is just to make the switch with this new state on all components that are plugged to tableColumns and the like. This will be done in another PR. --- .../hooks/useAvailableFieldMetadataItems.ts | 29 +++ ...nedReadableFieldMetadataItemIdsSelector.ts | 18 ++ .../useObjectOptionsForBoard.test.tsx | 119 ------------ .../hooks/useObjectOptionsForBoard.ts | 172 +++++++++++++----- .../hooks/useObjectOptionsForTable.ts | 93 ++++++++-- .../hooks/useCreateTableColumn.ts | 77 ++++++++ .../useFilterVisibleAndReadableRecordField.ts | 26 +++ .../record-field/hooks/useMoveRecordField.ts | 64 +++++-- .../hooks/useReorderRecordFields.ts | 39 ---- .../hooks/useReorderVisibleRecordFields.ts | 64 +++++++ .../hooks/useUpdateRecordField.ts | 3 +- .../hooks/useUpdateTableColumn.ts | 56 ++++++ .../hooks/useUpsertRecordField.ts | 3 +- ...ndReadableRecordFieldsComponentSelector.ts | 41 +++++ .../hooks/useLoadRecordIndexStates.ts | 1 + .../hooks/useHandleColumnsChange.ts | 4 - .../record-table/hooks/useTableColumns.ts | 168 ++++++----------- .../RecordTableColumnHeadDropdownMenu.tsx | 7 +- .../components/RecordTableHeaderCell.tsx | 1 - .../RecordTableHeaderPlusButtonContent.tsx | 3 +- ...uteNewPositionOfRecordWithPosition.test.ts | 65 +++++++ .../computeNewPositionOfRecordWithPosition.ts | 86 +++++++++ .../ViewFieldsHiddenDropdownSection.tsx | 89 +++------ .../ViewFieldsVisibleDropdownSection.tsx | 57 +++--- .../internal/usePersistViewFieldRecords.ts | 7 +- .../views/hooks/useSaveCurrentViewFields.ts | 2 +- .../views/utils/mapRecordFieldToViewField.ts | 16 ++ .../twenty-shared/src/utils/array/findById.ts | 5 + .../src/utils/array/findByProperty.ts | 5 + packages/twenty-shared/src/utils/index.ts | 2 + packages/twenty-ui/package.json | 4 +- 31 files changed, 867 insertions(+), 459 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/hooks/useAvailableFieldMetadataItems.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector.ts delete mode 100644 packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForBoard.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useCreateTableColumn.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useFilterVisibleAndReadableRecordField.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderRecordFields.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateTableColumn.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/__tests__/computeNewPositionOfRecordWithPosition.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/computeNewPositionOfRecordWithPosition.ts create mode 100644 packages/twenty-front/src/modules/views/utils/mapRecordFieldToViewField.ts create mode 100644 packages/twenty-shared/src/utils/array/findById.ts create mode 100644 packages/twenty-shared/src/utils/array/findByProperty.ts diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useAvailableFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useAvailableFieldMetadataItems.ts new file mode 100644 index 0000000000..596544b317 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useAvailableFieldMetadataItems.ts @@ -0,0 +1,29 @@ +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { TABLE_COLUMNS_DENY_LIST } from '@/object-record/constants/TableColumnsDenyList'; +import { useMemo } from 'react'; +import { FieldMetadataType } from 'twenty-shared/types'; + +export const useAvailableFieldMetadataItems = ({ + objectMetadataItemId, +}: { + objectMetadataItemId: string; +}) => { + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: objectMetadataItemId, + }); + + const availableFieldMetadataItems = useMemo( + () => + objectMetadataItem.readableFields.filter((fieldMetadataItemToFilter) => { + return ( + fieldMetadataItemToFilter.isActive && + !fieldMetadataItemToFilter.isSystem && + !TABLE_COLUMNS_DENY_LIST.includes(fieldMetadataItemToFilter.name) && + fieldMetadataItemToFilter.type !== FieldMetadataType.UUID + ); + }), + [objectMetadataItem], + ); + + return { availableFieldMetadataItems }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector.ts new file mode 100644 index 0000000000..5d1aca5ed2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector.ts @@ -0,0 +1,18 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { selector } from 'recoil'; + +export const flattenedReadableFieldMetadataItemsSelector = selector< + FieldMetadataItem[] +>({ + key: 'flattenedReadableFieldMetadataItemsSelector', + get: ({ get }) => { + const objectMetadataItems = get(objectMetadataItemsState); + + const flattenedReadableFieldMetadataItems = objectMetadataItems.flatMap( + (objectMetadataItem) => objectMetadataItem.readableFields, + ); + + return flattenedReadableFieldMetadataItems; + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForBoard.test.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForBoard.test.tsx deleted file mode 100644 index 4aa31783fc..0000000000 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForBoard.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; -import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd'; -import { renderHook } from '@testing-library/react'; -import { act } from 'react'; -import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; -import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; - -jest.mock('@/views/hooks/useSaveCurrentViewFields', () => ({ - useSaveCurrentViewFields: jest.fn(() => ({ - saveViewFields: jest.fn(), - })), -})); - -jest.mock('@/views/hooks/useUpdateCurrentView', () => ({ - useUpdateCurrentView: jest.fn(() => ({ - updateCurrentView: jest.fn(), - })), -})); - -const objectNameSingular = 'company'; - -describe('useObjectOptionsForBoard', () => { - const mockObjectMetadataItem = generatedMockObjectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === objectNameSingular, - ); - - if (!mockObjectMetadataItem) { - throw new Error('Mock object metadata item not found'); - } - - const mockFieldMetadataItem1 = mockObjectMetadataItem.fields.find( - (field) => field.name === 'name', - ); - - if (!mockFieldMetadataItem1) { - throw new Error('Mock field metadata item not found for "name"'); - } - - const mockFieldMetadataItem2 = mockObjectMetadataItem.fields.find( - (field) => field.name === 'createdAt', - ); - - if (!mockFieldMetadataItem2) { - throw new Error('Mock field metadata item not found for "createdAt"'); - } - - const initialRecoilState = [ - { - fieldMetadataId: mockFieldMetadataItem1.id, - isVisible: true, - position: 0, - }, - { - fieldMetadataId: mockFieldMetadataItem2.id, - isVisible: true, - position: 1, - }, - ]; - - const renderWithRecoil = () => - renderHook( - () => - useObjectOptionsForBoard({ - objectNameSingular, - recordBoardId: 'boardId', - viewBarId: 'viewBarId', - }), - { - wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ - apolloMocks: [], - onInitializeRecoilSnapshot: (snapshot) => { - snapshot.set( - recordIndexFieldDefinitionsState, - initialRecoilState as any, - ); - }, - componentInstanceId: 'test', - contextStoreCurrentObjectMetadataNameSingular: objectNameSingular, - }), - }, - ); - - it('reorders fields correctly', () => { - const { result } = renderWithRecoil(); - - const dropResult: DropResult = { - source: { droppableId: 'droppable', index: 1 }, - destination: { droppableId: 'droppable', index: 2 }, - draggableId: mockFieldMetadataItem1.id, - type: 'TYPE', - mode: 'FLUID', - reason: 'DROP', - combine: null, - }; - - const responderProvided: ResponderProvided = { - announce: jest.fn(), - }; - - act(() => { - result.current.handleReorderBoardFields(dropResult, responderProvided); - }); - - expect(result.current.visibleBoardFields).toEqual([ - { - fieldMetadataId: mockFieldMetadataItem2.id, - isVisible: true, - position: 0, - }, - { - fieldMetadataId: mockFieldMetadataItem1.id, - isVisible: true, - position: 1, - }, - ]); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts index 547d75db53..188fccb2b1 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts @@ -5,17 +5,25 @@ import { useRecoilState } from 'recoil'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields'; +import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; +import { useUpsertRecordField } from '@/object-record/record-field/hooks/useUpsertRecordField'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView'; import { type GraphQLView } from '@/views/types/GraphQLView'; -import { mapBoardFieldDefinitionsToViewFields } from '@/views/utils/mapBoardFieldDefinitionsToViewFields'; +import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField'; +import { produce } from 'immer'; +import { findByProperty, isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; -import { moveArrayItem } from '~/utils/array/moveArrayItem'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { sortByProperty } from '~/utils/array/sortByProperty'; type useObjectOptionsForBoardParams = { objectNameSingular: string; @@ -71,6 +79,9 @@ export const useObjectOptionsForBoard = ({ [recordIndexFieldDefinitions], ); + const { reorderVisibleRecordFields } = + useReorderVisibleRecordFields(recordBoardId); + const hiddenBoardFields = useMemo( () => availableColumnDefinitions @@ -97,24 +108,46 @@ export const useObjectOptionsForBoard = ({ return; } - const reorderedVisibleBoardFields = moveArrayItem(visibleBoardFields, { + const updatedRecordField = reorderVisibleRecordFields({ fromIndex: result.source.index - 1, toIndex: result.destination.index - 1, }); - if (isDeeplyEqual(visibleBoardFields, reorderedVisibleBoardFields)) - return; + saveViewFields([mapRecordFieldToViewField(updatedRecordField)]); - const updatedFields = [...reorderedVisibleBoardFields].map( - (field, index) => ({ ...field, position: index }), + const modifiedRecordIndexFieldDefinitions = produce( + recordIndexFieldDefinitions, + (draftRecordIndexFieldDefinitions) => { + const indexToModify = draftRecordIndexFieldDefinitions.findIndex( + (recordIndexFieldDefinitionToModify) => + recordIndexFieldDefinitionToModify.fieldMetadataId === + updatedRecordField.fieldMetadataItemId, + ); + + draftRecordIndexFieldDefinitions[indexToModify].position = + updatedRecordField.position; + }, ); - setRecordIndexFieldDefinitions(updatedFields); - saveViewFields(mapBoardFieldDefinitionsToViewFields(updatedFields)); + // TODO: remove after refactor + setRecordIndexFieldDefinitions(modifiedRecordIndexFieldDefinitions); }, - [saveViewFields, setRecordIndexFieldDefinitions, visibleBoardFields], + [ + saveViewFields, + setRecordIndexFieldDefinitions, + recordIndexFieldDefinitions, + reorderVisibleRecordFields, + ], ); + const currentRecordFields = useRecoilComponentValue( + currentRecordFieldsComponentState, + recordBoardId, + ); + + const { updateRecordField } = useUpdateRecordField(recordBoardId); + const { upsertRecordField } = useUpsertRecordField(recordBoardId); + // 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( @@ -124,59 +157,98 @@ export const useObjectOptionsForBoard = ({ 'fieldMetadataId' | 'isVisible' >, ) => { - const isNewViewField = !( - updatedFieldDefinition.fieldMetadataId in - recordIndexFieldDefinitionsByKey + const lastPosition = currentRecordFields.toSorted( + sortByProperty('position', 'desc'), + )[0].position; + + const shouldShowFieldMetadataItem = + updatedFieldDefinition.isVisible === true; + const corresponingRecordField = currentRecordFields.find( + (recordFieldToFind) => + recordFieldToFind.fieldMetadataItemId === + updatedFieldDefinition.fieldMetadataId, ); - let updatedFieldsDefinitions: ColumnDefinition[]; + const noExistingRecordField = !isDefined(corresponingRecordField); - if (isNewViewField) { - const correspondingFieldDefinition = availableColumnDefinitions.find( - (availableColumnDefinition) => - availableColumnDefinition.fieldMetadataId === - updatedFieldDefinition.fieldMetadataId, - ); + if (noExistingRecordField) { + const recordFieldToUpsert: RecordField = { + id: v4(), + fieldMetadataItemId: updatedFieldDefinition.fieldMetadataId, + size: 100, + isVisible: shouldShowFieldMetadataItem, + position: lastPosition + 1, + }; - if (!correspondingFieldDefinition) return; + upsertRecordField(recordFieldToUpsert); - const lastVisibleBoardField = - visibleBoardFields[visibleBoardFields.length - 1]; + saveViewFields([mapRecordFieldToViewField(recordFieldToUpsert)]); - updatedFieldsDefinitions = [ - ...recordIndexFieldDefinitions, - { - ...correspondingFieldDefinition, - position: (lastVisibleBoardField?.position || 0) + 1, - isVisible: true, + const correspondingAvailableColumnDefinition = + availableColumnDefinitions.find( + findByProperty( + 'fieldMetadataId', + updatedFieldDefinition.fieldMetadataId, + ), + ); + + const modifiedRecordIndexFieldDefinitions = produce( + recordIndexFieldDefinitions, + (draftRecordIndexFieldDefinitions) => { + if (!isDefined(correspondingAvailableColumnDefinition)) { + throw new Error( + `correspondingAvailableColumnDefinition is not defined this should not happen.`, + ); + } + + draftRecordIndexFieldDefinitions.push({ + ...correspondingAvailableColumnDefinition, + fieldMetadataId: updatedFieldDefinition.fieldMetadataId, + isVisible: shouldShowFieldMetadataItem, + }); }, - ]; - } else { - updatedFieldsDefinitions = recordIndexFieldDefinitions.map( - (existingFieldDefinition) => - existingFieldDefinition.fieldMetadataId === - updatedFieldDefinition.fieldMetadataId - ? { - ...existingFieldDefinition, - isVisible: !existingFieldDefinition.isVisible, - } - : existingFieldDefinition, ); + + // TODO: remove after refactor + setRecordIndexFieldDefinitions(modifiedRecordIndexFieldDefinitions); + } else { + updateRecordField(updatedFieldDefinition.fieldMetadataId, { + isVisible: shouldShowFieldMetadataItem, + }); + + const updatedRecordField: RecordField = { + ...corresponingRecordField, + isVisible: shouldShowFieldMetadataItem, + }; + + saveViewFields([mapRecordFieldToViewField(updatedRecordField)]); + + const modifiedRecordIndexFieldDefinitions = produce( + recordIndexFieldDefinitions, + (draftRecordIndexFieldDefinitions) => { + const indexToModify = draftRecordIndexFieldDefinitions.findIndex( + (recordIndexFieldDefinitionToModify) => + recordIndexFieldDefinitionToModify.fieldMetadataId === + updatedRecordField.fieldMetadataItemId, + ); + + draftRecordIndexFieldDefinitions[indexToModify].isVisible = + shouldShowFieldMetadataItem; + }, + ); + + // TODO: remove after refactor + setRecordIndexFieldDefinitions(modifiedRecordIndexFieldDefinitions); } - - setRecordIndexFieldDefinitions(updatedFieldsDefinitions); - - saveViewFields( - mapBoardFieldDefinitionsToViewFields(updatedFieldsDefinitions), - ); }, [ - recordIndexFieldDefinitionsByKey, + currentRecordFields, + updateRecordField, + upsertRecordField, setRecordIndexFieldDefinitions, saveViewFields, - availableColumnDefinitions, - visibleBoardFields, recordIndexFieldDefinitions, + availableColumnDefinitions, ], ); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts index a24a8e2540..3752d3d94b 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts @@ -1,12 +1,19 @@ import { type OnDragEndResponder } from '@hello-pangea/dnd'; -import { useCallback } from 'react'; -import { useReorderRecordFields } from '@/object-record/record-field/hooks/useReorderRecordFields'; +import { useFilterVisibleAndReadableRecordField } from '@/object-record/record-field/hooks/useFilterVisibleAndReadableRecordField'; +import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; +import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { moveArrayItem } from '~/utils/array/moveArrayItem'; +import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; +import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField'; +import { produce } from 'immer'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { sortByProperty } from '~/utils/array/sortByProperty'; export const useObjectOptionsForTable = ( recordTableId: string, @@ -16,19 +23,37 @@ export const useObjectOptionsForTable = ( hiddenTableColumnsComponentSelector, recordTableId, ); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + + const tableColumns = useRecoilComponentValue( + tableColumnsComponentState, recordTableId, ); - const { handleColumnVisibilityChange, handleColumnReorder } = useTableColumns( - { recordTableId, objectMetadataId }, + const currentRecordFields = useRecoilComponentValue( + currentRecordFieldsComponentState, + recordTableId, ); - const { reorderRecordFields } = useReorderRecordFields(); + const { filterVisibleAndReadableRecordField } = + useFilterVisibleAndReadableRecordField(); - const handleReorderColumns: OnDragEndResponder = useCallback( - async (result) => { + const visibleRecordFields = currentRecordFields + .filter(filterVisibleAndReadableRecordField) + .toSorted(sortByProperty('position')); + + const { handleColumnVisibilityChange } = useTableColumns({ + recordTableId, + }); + + const { reorderVisibleRecordFields } = + useReorderVisibleRecordFields(recordTableId); + + const { saveViewFields } = useSaveCurrentViewFields(); + + const { setTableColumns } = useSetTableColumns(); + + const handleReorderColumns: OnDragEndResponder = useRecoilCallback( + () => async (result) => { if ( !result.destination || result.destination.index === 1 || @@ -37,25 +62,55 @@ export const useObjectOptionsForTable = ( return; } - const reorderedFields = moveArrayItem(visibleTableColumns, { + const updatedRecordField = reorderVisibleRecordFields({ fromIndex: result.source.index - 1, toIndex: result.destination.index - 1, }); - reorderRecordFields({ - fromIndex: result.source.index - 1, - toIndex: result.destination.index - 1, - }); + saveViewFields([mapRecordFieldToViewField(updatedRecordField)]); - handleColumnReorder(reorderedFields); + // TODO: remove this after refactor + const modifiedVisibleTableColumns = produce( + tableColumns, + (draftTableColumns) => { + const indexToModify = draftTableColumns.findIndex( + (tableColumnToFind) => + tableColumnToFind.fieldMetadataId === + updatedRecordField.fieldMetadataItemId, + ); + + if (isDefined(draftTableColumns[indexToModify])) { + draftTableColumns[indexToModify].position = + updatedRecordField.position; + } else { + throw new Error( + `Undefined draftTableColumns this should not happen`, + ); + } + }, + ); + + // TODO: remove after refactor + setTableColumns( + modifiedVisibleTableColumns, + recordTableId, + objectMetadataId, + ); }, - [visibleTableColumns, handleColumnReorder, reorderRecordFields], + [ + reorderVisibleRecordFields, + saveViewFields, + objectMetadataId, + recordTableId, + setTableColumns, + tableColumns, + ], ); return { handleReorderColumns, handleColumnVisibilityChange, - visibleTableColumns, + visibleTableColumns: visibleRecordFields, hiddenTableColumns, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useCreateTableColumn.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useCreateTableColumn.ts new file mode 100644 index 0000000000..11536ae65f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useCreateTableColumn.ts @@ -0,0 +1,77 @@ +import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState'; +import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; +import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { produce } from 'immer'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +// TODO: remove after refactor +export const useCreateTableColumn = (recordTableId?: string) => { + const tableColumnsCallbackState = useRecoilComponentCallbackState( + tableColumnsComponentState, + recordTableId, + ); + + const availableTableColumnsCallbackState = useRecoilComponentCallbackState( + availableTableColumnsComponentState, + recordTableId, + ); + + const createTableColumn = useRecoilCallback( + ({ set, snapshot }) => + ( + fieldMetadataItemId: string, + tableColumnToCreateInfo: Pick< + ColumnDefinition, + 'isVisible' | 'position' | 'size' + >, + ) => { + const tableColumns = getSnapshotValue( + snapshot, + tableColumnsCallbackState, + ); + + const availableTableColumns = getSnapshotValue( + snapshot, + availableTableColumnsCallbackState, + ); + + const modifiedTableColumns = produce( + tableColumns, + (draftTableColumns) => { + const indexToModify = draftTableColumns.findIndex( + (tableColumnToFind) => + tableColumnToFind.fieldMetadataId === fieldMetadataItemId, + ); + + const correspondingAvailableTableColumn = + availableTableColumns.find( + (tableColumnToFind) => + tableColumnToFind.fieldMetadataId === fieldMetadataItemId, + ); + + if ( + indexToModify === -1 && + isDefined(correspondingAvailableTableColumn) + ) { + draftTableColumns.push({ + ...correspondingAvailableTableColumn, + fieldMetadataId: fieldMetadataItemId, + position: tableColumnToCreateInfo.position, + isVisible: tableColumnToCreateInfo.isVisible, + }); + } + }, + ); + + set(tableColumnsCallbackState, modifiedTableColumns); + }, + [tableColumnsCallbackState, availableTableColumnsCallbackState], + ); + + return { + createTableColumn, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useFilterVisibleAndReadableRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useFilterVisibleAndReadableRecordField.ts new file mode 100644 index 0000000000..0bccb3b9a4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useFilterVisibleAndReadableRecordField.ts @@ -0,0 +1,26 @@ +import { flattenedReadableFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { useRecoilValue } from 'recoil'; + +export const useFilterVisibleAndReadableRecordField = () => { + const flattenedReadableFieldMetadataItems = useRecoilValue( + flattenedReadableFieldMetadataItemsSelector, + ); + + const filterVisibleAndReadableRecordField = ( + recordFieldToFilter: RecordField, + ) => { + return ( + recordFieldToFilter.isVisible === true && + flattenedReadableFieldMetadataItems.some( + (fieldMetadataItemToFilter) => + fieldMetadataItemToFilter.id === + recordFieldToFilter.fieldMetadataItemId && + fieldMetadataItemToFilter.isActive === true && + fieldMetadataItemToFilter.isSystem !== true, + ) + ); + }; + + return { filterVisibleAndReadableRecordField }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts index c72742c4d0..bda62ea713 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts @@ -1,14 +1,26 @@ +import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; +import { useUpdateTableColumn } from '@/object-record/record-field/hooks/useUpdateTableColumn'; import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; +import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField'; import { useRecoilCallback } from 'recoil'; +import { sortByProperty } from '~/utils/array/sortByProperty'; -export const useMoveRecordField = () => { +export const useMoveRecordField = (recordTableId?: string) => { const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( currentRecordFieldsComponentState, + recordTableId, ); + const { saveViewFields } = useSaveCurrentViewFields(); + + const { updateRecordField } = useUpdateRecordField(recordTableId); + + const { updateTableColumn } = useUpdateTableColumn(recordTableId); + const moveRecordField = useRecoilCallback( - ({ set, snapshot }) => + ({ snapshot }) => ({ direction, fieldMetadataItemIdToMove, @@ -20,7 +32,11 @@ export const useMoveRecordField = () => { .getLoadable(currentRecordFieldsCallbackState) .getValue(); - const indexOfRecordFieldToMove = currentRecordFields.findIndex( + const sortedRecordFields = currentRecordFields.toSorted( + sortByProperty('position'), + ); + + const indexOfRecordFieldToMove = sortedRecordFields.findIndex( (recordField) => recordField.fieldMetadataItemId === fieldMetadataItemIdToMove, ); @@ -29,7 +45,7 @@ export const useMoveRecordField = () => { return; } - const newRecordFields = [...currentRecordFields]; + const newRecordFields = [...sortedRecordFields]; const targetArrayIndex = direction === 'before' @@ -50,20 +66,40 @@ export const useMoveRecordField = () => { const targetRecordFieldNewPosition = currentRecordField.position; const currentRecordFieldNewPosition = targetRecordField.position; - newRecordFields[indexOfRecordFieldToMove] = { - ...newRecordFields[indexOfRecordFieldToMove], - position: currentRecordFieldNewPosition, - }; - - newRecordFields[targetRecordFieldNewPosition] = { - ...newRecordFields[targetRecordFieldNewPosition], + updateRecordField(targetRecordField.fieldMetadataItemId, { position: targetRecordFieldNewPosition, - }; + }); - set(currentRecordFieldsCallbackState, newRecordFields); + updateRecordField(currentRecordField.fieldMetadataItemId, { + position: currentRecordFieldNewPosition, + }); + + saveViewFields([ + mapRecordFieldToViewField({ + ...targetRecordField, + position: targetRecordFieldNewPosition, + }), + mapRecordFieldToViewField({ + ...currentRecordField, + position: currentRecordFieldNewPosition, + }), + ]); + + updateTableColumn(targetRecordField.fieldMetadataItemId, { + position: targetRecordFieldNewPosition, + }); + + updateTableColumn(currentRecordField.fieldMetadataItemId, { + position: currentRecordFieldNewPosition, + }); } }, - [currentRecordFieldsCallbackState], + [ + currentRecordFieldsCallbackState, + updateTableColumn, + saveViewFields, + updateRecordField, + ], ); return { moveRecordField }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderRecordFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderRecordFields.ts deleted file mode 100644 index c71ca91edc..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderRecordFields.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; -import { type RecordField } from '@/object-record/record-field/types/RecordField'; -import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { useRecoilCallback } from 'recoil'; -import { moveArrayItem } from '~/utils/array/moveArrayItem'; - -export const useReorderRecordFields = () => { - const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( - currentRecordFieldsComponentState, - ); - - const reorderRecordFields = useRecoilCallback( - ({ set, snapshot }) => - ({ fromIndex, toIndex }: { fromIndex: number; toIndex: number }) => { - const currentRecordFields = snapshot - .getLoadable(currentRecordFieldsCallbackState) - .getValue(); - - const reorderedRecordFields = moveArrayItem(currentRecordFields, { - fromIndex, - toIndex, - }); - - const reorderedRecordFieldsWithNewPosition = - reorderedRecordFields.map((recordField, index) => ({ - ...recordField, - position: index, - })); - - set( - currentRecordFieldsCallbackState, - reorderedRecordFieldsWithNewPosition, - ); - }, - [currentRecordFieldsCallbackState], - ); - - return { reorderRecordFields }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts new file mode 100644 index 0000000000..5fb06c2451 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts @@ -0,0 +1,64 @@ +import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { visibleAndReadableRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { computeNewPositionOfRecordWithPosition } from '@/object-record/utils/computeNewPositionOfRecordWithPosition'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilCallback } from 'recoil'; + +export const useReorderVisibleRecordFields = (recordTableId: string) => { + const visibleRecordFieldsCallbackState = useRecoilComponentCallbackState( + visibleAndReadableRecordFieldsComponentSelector, + recordTableId, + ); + + const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( + currentRecordFieldsComponentState, + recordTableId, + ); + + const { updateRecordField } = useUpdateRecordField(recordTableId); + + const reorderVisibleRecordFields = useRecoilCallback( + ({ snapshot }) => + ({ fromIndex, toIndex }: { fromIndex: number; toIndex: number }) => { + const visibleRecordFields = snapshot + .getLoadable(visibleRecordFieldsCallbackState) + .getValue(); + + const currentRecordFields = snapshot + .getLoadable(currentRecordFieldsCallbackState) + .getValue(); + + const idOfRecordToMove = visibleRecordFields[fromIndex].id; + const idOfTargetRecord = visibleRecordFields[toIndex].id; + + const recordToMove = visibleRecordFields[fromIndex]; + + const newPositionOfTargetRecord = + computeNewPositionOfRecordWithPosition({ + arrayOfRecordsWithPosition: currentRecordFields, + idOfItemToMove: idOfRecordToMove, + idOfTargetItem: idOfTargetRecord, + }); + + updateRecordField(recordToMove.fieldMetadataItemId, { + position: newPositionOfTargetRecord, + }); + + const updatedRecordField: RecordField = { + ...recordToMove, + position: newPositionOfTargetRecord, + }; + + return updatedRecordField; + }, + [ + currentRecordFieldsCallbackState, + visibleRecordFieldsCallbackState, + updateRecordField, + ], + ); + + return { reorderVisibleRecordFields }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts index a252426c8d..090d70dd50 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts @@ -4,9 +4,10 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component- import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useRecoilCallback } from 'recoil'; -export const useUpdateRecordField = () => { +export const useUpdateRecordField = (recordTableId?: string) => { const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( currentRecordFieldsComponentState, + recordTableId, ); const updateRecordField = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateTableColumn.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateTableColumn.ts new file mode 100644 index 0000000000..1bdfdf2c5f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateTableColumn.ts @@ -0,0 +1,56 @@ +import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; +import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { produce } from 'immer'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useUpdateTableColumn = (recordTableId?: string) => { + const tableColumnsCallbackState = useRecoilComponentCallbackState( + tableColumnsComponentState, + recordTableId, + ); + + const updateTableColumn = useRecoilCallback( + ({ set, snapshot }) => + ( + fieldMetadataItemId: string, + partiableTableColumn: Partial< + Pick, 'isVisible' | 'position'> + >, + ) => { + const tableColumns = getSnapshotValue( + snapshot, + tableColumnsCallbackState, + ); + + const modifiedTableColumns = produce( + tableColumns, + (draftTableColumns) => { + const indexToModify = draftTableColumns.findIndex( + (tableColumnToFind) => + tableColumnToFind.fieldMetadataId === fieldMetadataItemId, + ); + + if (isDefined(partiableTableColumn.position)) { + draftTableColumns[indexToModify].position = + partiableTableColumn.position; + } + + if (isDefined(partiableTableColumn.isVisible)) { + draftTableColumns[indexToModify].isVisible = + partiableTableColumn.isVisible; + } + }, + ); + + set(tableColumnsCallbackState, modifiedTableColumns); + }, + [tableColumnsCallbackState], + ); + + return { + updateTableColumn, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpsertRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpsertRecordField.ts index 71fe1b2e07..4a0e8bf848 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpsertRecordField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpsertRecordField.ts @@ -4,9 +4,10 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component- import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useRecoilCallback } from 'recoil'; -export const useUpsertRecordField = () => { +export const useUpsertRecordField = (recordTableId?: string) => { const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( currentRecordFieldsComponentState, + recordTableId, ); const upsertRecordField = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector.ts new file mode 100644 index 0000000000..dece01cfa6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector.ts @@ -0,0 +1,41 @@ +import { flattenedReadableFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector'; +import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; +import { sortByProperty } from '~/utils/array/sortByProperty'; + +export const visibleAndReadableRecordFieldsComponentSelector = + createComponentSelector({ + key: 'visibleAndReadableRecordFieldsComponentSelector', + componentInstanceContext: RecordFieldsComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const currentRecordFields = get( + currentRecordFieldsComponentState.atomFamily({ + instanceId, + }), + ); + + const readableFieldMetadataItems = get( + flattenedReadableFieldMetadataItemsSelector, + ); + + const filteredVisibleAndReadableRecordFields = + currentRecordFields.filter( + (recordFieldToFilter) => + recordFieldToFilter.isVisible === true && + readableFieldMetadataItems.some( + (fieldMetadataItemToFilter) => + fieldMetadataItemToFilter.id === + recordFieldToFilter.fieldMetadataItemId && + fieldMetadataItemToFilter.isActive === true && + fieldMetadataItemToFilter.isSystem !== true, + ), + ); + + return filteredVisibleAndReadableRecordFields.toSorted( + sortByProperty('position'), + ); + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts index 58bb470236..184a08df88 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts @@ -117,6 +117,7 @@ export const useLoadRecordIndexStates = () => { const existingRecordIndexFieldDefinitions = snapshot .getLoadable(recordIndexFieldDefinitionsState) .getValue(); + if ( !isDeeplyEqual( existingRecordIndexFieldDefinitions, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts index fc4b006eb9..c3dc3447e7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts @@ -1,5 +1,4 @@ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { useSaveColumnsToView } from '@/object-record/record-table/hooks/useSaveColumnsToView'; import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns'; import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; @@ -7,7 +6,6 @@ import { type ColumnDefinition } from '@/object-record/record-table/types/Column // but for now it allows to have a cleaner API globally. // TODO: should be solved with new RecordField abstraction export const useHandleColumnsChange = () => { - const { saveColumnsToView } = useSaveColumnsToView(); const { setTableColumns } = useSetTableColumns(); const handleColumnsChange = async ({ @@ -20,8 +18,6 @@ export const useHandleColumnsChange = () => { objectMetadataId: string; }) => { setTableColumns(columns, recordTableId, objectMetadataId); - - await saveColumnsToView(columns); }; return { diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts index 755c2c8821..c726c6d140 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts @@ -2,50 +2,44 @@ import { useCallback } from 'react'; import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell'; -import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns'; +import { useCreateTableColumn } from '@/object-record/record-field/hooks/useCreateTableColumn'; import { useMoveRecordField } from '@/object-record/record-field/hooks/useMoveRecordField'; import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; +import { useUpdateTableColumn } from '@/object-record/record-field/hooks/useUpdateTableColumn'; import { useUpsertRecordField } from '@/object-record/record-field/hooks/useUpsertRecordField'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { useHandleColumnsChange } from '@/object-record/record-table/hooks/useHandleColumnsChange'; -import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; +import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField'; +import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { sortByProperty } from '~/utils/array/sortByProperty'; import { type ColumnDefinition } from '../types/ColumnDefinition'; type useRecordTableProps = { recordTableId: string; - objectMetadataId: string; }; -export const useTableColumns = ({ - objectMetadataId, - recordTableId, -}: useRecordTableProps) => { - const availableTableColumns = useRecoilComponentValue( - availableTableColumnsComponentState, +export const useTableColumns = ({ recordTableId }: useRecordTableProps) => { + const currentRecordFields = useRecoilComponentValue( + currentRecordFieldsComponentState, recordTableId, ); - const tableColumns = useRecoilComponentValue( - tableColumnsComponentState, - recordTableId, - ); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, - recordTableId, - ); - - const { handleColumnMove } = useMoveViewColumns(); - const { unfocusRecordTableCell } = useUnfocusRecordTableCell(recordTableId); const { handleColumnsChange } = useHandleColumnsChange(); - const { updateRecordField } = useUpdateRecordField(); - const { upsertRecordField } = useUpsertRecordField(); + const { updateRecordField } = useUpdateRecordField(recordTableId); + const { upsertRecordField } = useUpsertRecordField(recordTableId); + + const { saveViewFields } = useSaveCurrentViewFields(); + + const { updateTableColumn } = useUpdateTableColumn(recordTableId); + const { createTableColumn } = useCreateTableColumn(recordTableId); const handleColumnVisibilityChange = useCallback( async ( @@ -54,74 +48,64 @@ export const useTableColumns = ({ 'fieldMetadataId' | 'isVisible' >, ) => { - const shouldShowColumn = !visibleTableColumns.some( - (visibleColumn) => - visibleColumn.fieldMetadataId === viewField.fieldMetadataId, + const lastPosition = currentRecordFields.toSorted( + sortByProperty('position', 'desc'), + )[0].position; + + const shouldShowFieldMetadataItem = viewField.isVisible === true; + const corresponingRecordField = currentRecordFields.find( + (recordFieldToFind) => + recordFieldToFind.fieldMetadataItemId === viewField.fieldMetadataId, ); - const tableColumnPositions = [...tableColumns] - .sort((a, b) => b.position - a.position) - .map((column) => column.position); + const noExistingRecordField = !isDefined(corresponingRecordField); - const lastPosition = tableColumnPositions[0] ?? 0; - - if (shouldShowColumn) { - const newColumn = availableTableColumns.find( - (availableTableColumn) => - availableTableColumn.fieldMetadataId === viewField.fieldMetadataId, - ); - - if (!newColumn) return; - - const nextColumns = [ - ...tableColumns, - { ...newColumn, isVisible: true, position: lastPosition + 1 }, - ]; - - upsertRecordField({ + if (noExistingRecordField) { + const recordFieldToUpsert: RecordField = { id: v4(), fieldMetadataItemId: viewField.fieldMetadataId, size: 100, - isVisible: true, + isVisible: shouldShowFieldMetadataItem, position: lastPosition + 1, + }; + + upsertRecordField(recordFieldToUpsert); + + createTableColumn(viewField.fieldMetadataId, { + position: lastPosition + 1, + size: 100, + isVisible: shouldShowFieldMetadataItem, }); - await handleColumnsChange({ - columns: nextColumns, - objectMetadataId, - recordTableId, - }); + saveViewFields([mapRecordFieldToViewField(recordFieldToUpsert)]); } else { - const nextColumns = visibleTableColumns.map((previousColumn) => - previousColumn.fieldMetadataId === viewField.fieldMetadataId - ? { ...previousColumn, isVisible: !viewField.isVisible } - : previousColumn, - ); - updateRecordField(viewField.fieldMetadataId, { - isVisible: !viewField.isVisible, + isVisible: shouldShowFieldMetadataItem, }); - await handleColumnsChange({ - columns: nextColumns, - objectMetadataId, - recordTableId, + const updatedRecordField: RecordField = { + ...corresponingRecordField, + isVisible: shouldShowFieldMetadataItem, + }; + + updateTableColumn(viewField.fieldMetadataId, { + isVisible: shouldShowFieldMetadataItem, }); + + saveViewFields([mapRecordFieldToViewField(updatedRecordField)]); } }, [ + updateTableColumn, + saveViewFields, + currentRecordFields, upsertRecordField, - tableColumns, - availableTableColumns, - handleColumnsChange, - visibleTableColumns, - objectMetadataId, - recordTableId, updateRecordField, + createTableColumn, ], ); - const { moveRecordField } = useMoveRecordField(); + const { moveRecordField } = useMoveRecordField(recordTableId); const handleMoveTableColumn = useCallback( async ( @@ -130,59 +114,17 @@ export const useTableColumns = ({ ) => { unfocusRecordTableCell(); - const currentColumnArrayIndex = visibleTableColumns.findIndex( - (visibleColumn) => - visibleColumn.fieldMetadataId === column.fieldMetadataId, - ); - - const columns = handleColumnMove( - direction, - currentColumnArrayIndex, - visibleTableColumns, - ); - moveRecordField({ direction: direction === 'left' ? 'before' : 'after', fieldMetadataItemIdToMove: column.fieldMetadataId, }); - - await handleColumnsChange({ - columns, - objectMetadataId, - recordTableId, - }); }, - [ - unfocusRecordTableCell, - visibleTableColumns, - handleColumnMove, - handleColumnsChange, - objectMetadataId, - recordTableId, - moveRecordField, - ], - ); - - const handleColumnReorder = useCallback( - async (columns: ColumnDefinition[]) => { - const updatedColumns = columns.map((column, index) => ({ - ...column, - position: index, - })); - - await handleColumnsChange({ - columns: updatedColumns, - objectMetadataId, - recordTableId, - }); - }, - [handleColumnsChange, objectMetadataId, recordTableId], + [unfocusRecordTableCell, moveRecordField], ); return { handleColumnVisibilityChange, handleMoveTableColumn, - handleColumnReorder, handleColumnsChange, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx index 7894f6f871..3e9e308792 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx @@ -57,7 +57,7 @@ export const RecordTableColumnHeadDropdownMenu = ({ const { recordTableId } = useRecordTableContextOrThrow(); const { handleColumnVisibilityChange, handleMoveTableColumn } = - useTableColumns({ objectMetadataId, recordTableId }); + useTableColumns({ recordTableId }); const dropdownId = column.fieldMetadataId + '-header'; @@ -86,7 +86,10 @@ export const RecordTableColumnHeadDropdownMenu = ({ const handleColumnVisibility = () => { closeDropdownAndToggleScroll(); - handleColumnVisibilityChange(column); + handleColumnVisibilityChange({ + ...column, + isVisible: false, + }); }; const handleToggleColumnSort = useHandleToggleColumnSort({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 1d1109162e..4fc61b1c7b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -141,7 +141,6 @@ export const RecordTableHeaderCell = ({ >(null); const { handleColumnsChange } = useTableColumns({ - objectMetadataId: objectMetadataItem.id, recordTableId, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx index 33b4b548fc..c0b104a8b6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx @@ -31,14 +31,13 @@ export const RecordTableHeaderPlusButtonContent = () => { const { getIcon } = useIcons(); const { handleColumnVisibilityChange } = useTableColumns({ - objectMetadataId: objectMetadataItem.id, recordTableId, }); const handleAddColumn = useCallback( (column: ColumnDefinition) => { closeDropdown(); - handleColumnVisibilityChange(column); + handleColumnVisibilityChange({ ...column, isVisible: true }); }, [handleColumnVisibilityChange, closeDropdown], ); diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/computeNewPositionOfRecordWithPosition.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeNewPositionOfRecordWithPosition.test.ts new file mode 100644 index 0000000000..e4be2aea4f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeNewPositionOfRecordWithPosition.test.ts @@ -0,0 +1,65 @@ +import { + computeNewPositionOfRecordWithPosition, + type RecordWithPosition, +} from '@/object-record/utils/computeNewPositionOfRecordWithPosition'; + +const mockRecordsWithPosition: RecordWithPosition[] = [ + { + id: 'A', + position: 0, + }, + { + id: 'B', + position: 1, + }, + { + id: 'C', + position: 2, + }, + { + id: 'D', + position: 3, + }, +]; + +describe('computeNewPositionOfRecordWithPosition', () => { + it('should compute first position', () => { + const newPosition = computeNewPositionOfRecordWithPosition({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'B', + idOfTargetItem: 'A', + }); + + expect(newPosition).toEqual(-1); + }); + + it('should compute last position', () => { + const newPosition = computeNewPositionOfRecordWithPosition({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'B', + idOfTargetItem: 'D', + }); + + expect(newPosition).toEqual(4); + }); + + it('should compute intermediary position after target item', () => { + const newPosition = computeNewPositionOfRecordWithPosition({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'A', + idOfTargetItem: 'B', + }); + + expect(newPosition).toEqual(1.5); + }); + + it('should compute intermediary position before target item', () => { + const newPosition = computeNewPositionOfRecordWithPosition({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'A', + idOfTargetItem: 'C', + }); + + expect(newPosition).toEqual(2.5); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/utils/computeNewPositionOfRecordWithPosition.ts b/packages/twenty-front/src/modules/object-record/utils/computeNewPositionOfRecordWithPosition.ts new file mode 100644 index 0000000000..88af56f5c6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/computeNewPositionOfRecordWithPosition.ts @@ -0,0 +1,86 @@ +import { isDefined } from 'twenty-shared/utils'; +import { sortByProperty } from '~/utils/array/sortByProperty'; + +export type RecordWithPosition = { + id: string; + position: number; +}; + +export const computeNewPositionOfRecordWithPosition = ({ + arrayOfRecordsWithPosition, + idOfItemToMove, + idOfTargetItem, +}: { + arrayOfRecordsWithPosition: RecordWithPosition[]; + idOfItemToMove: string; + idOfTargetItem: string; +}) => { + const itemToMove = arrayOfRecordsWithPosition.find( + (recordToFind) => recordToFind.id === idOfItemToMove, + ); + + const targetItem = arrayOfRecordsWithPosition.find( + (recordToFind) => recordToFind.id === idOfTargetItem, + ); + + if (!isDefined(itemToMove)) { + throw new Error(`Cannot find item to move for id : ${idOfItemToMove}`); + } + + if (!isDefined(targetItem)) { + throw new Error(`Cannot find item to move for id : ${idOfTargetItem}`); + } + + if (itemToMove.id === targetItem.id) { + return itemToMove.position; + } + + const targetPosition = targetItem.position; + + const sortedRecordsByAscendingPosition = arrayOfRecordsWithPosition.toSorted( + sortByProperty('position'), + ); + + const indexOfItemToMove = sortedRecordsByAscendingPosition.findIndex( + (recordToFind) => recordToFind.id === idOfItemToMove, + ); + const indexOfTargetItem = sortedRecordsByAscendingPosition.findIndex( + (recordToFind) => recordToFind.id === idOfTargetItem, + ); + + const lastIndex = sortedRecordsByAscendingPosition.length - 1; + + const shouldGoToFirstPosition = + indexOfItemToMove > 0 && indexOfTargetItem === 0; + + const shouldGoToLastPosition = + indexOfItemToMove < lastIndex && indexOfTargetItem === lastIndex; + + if (shouldGoToFirstPosition) { + return targetPosition - 1; + } else if (shouldGoToLastPosition) { + return targetPosition + 1; + } else { + const shouldGoAfterTargetItem = indexOfItemToMove < indexOfTargetItem; + + if (shouldGoAfterTargetItem) { + const itemAfterTargetItem = + sortedRecordsByAscendingPosition[indexOfTargetItem + 1]; + + const intermediaryPosition = + targetItem.position + + (itemAfterTargetItem.position - targetItem.position) / 2; + + return intermediaryPosition; + } else { + const itemBeforeTargetItem = + sortedRecordsByAscendingPosition[indexOfTargetItem - 1]; + + const intermediaryPosition = + targetItem.position - + (targetItem.position - itemBeforeTargetItem.position) / 2; + + return intermediaryPosition; + } + } +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx index 937455cf05..d4950abae5 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx @@ -1,48 +1,36 @@ -import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd'; - import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFieldMetadataItems'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; -import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; -import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { ViewType } from '@/views/types/ViewType'; import { useContext } from 'react'; import { IconEye, useIcons } from 'twenty-ui/display'; -import { MenuItemDraggable } from 'twenty-ui/navigation'; +import { MenuItem } from 'twenty-ui/navigation'; export const ViewFieldsHiddenDropdownSection = () => { const { viewType, objectMetadataItem, recordIndexId } = useContext( ObjectOptionsDropdownContext, ); - const { handleReorderColumns, handleColumnVisibilityChange } = - useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); + const { handleColumnVisibilityChange } = useObjectOptionsForTable( + recordIndexId, + objectMetadataItem.id, + ); - const { handleReorderBoardFields, handleBoardFieldVisibilityChange } = - useObjectOptionsForBoard({ - objectNameSingular: objectMetadataItem.nameSingular, - recordBoardId: recordIndexId, - viewBarId: recordIndexId, - }); - - const handleReorderFields = - viewType === ViewType.Kanban - ? handleReorderBoardFields - : handleReorderColumns; + const { handleBoardFieldVisibilityChange } = useObjectOptionsForBoard({ + objectNameSingular: objectMetadataItem.nameSingular, + recordBoardId: recordIndexId, + viewBarId: recordIndexId, + }); const handleChangeFieldVisibility = viewType === ViewType.Kanban ? handleBoardFieldVisibilityChange : handleColumnVisibilityChange; - const handleDragEnd = (result: DropResult, provided: ResponderProvided) => { - handleReorderFields(result, provided); - }; - const currentRecordFields = useRecoilComponentValue( currentRecordFieldsComponentState, ); @@ -67,45 +55,26 @@ export const ViewFieldsHiddenDropdownSection = () => { return ( <> - {availableFieldMetadataItemsToShow.length > 0 && ( - - {availableFieldMetadataItemsToShow.map( - (fieldMetadataItem, index) => { - const fieldIndex = index; - - return ( - - handleChangeFieldVisibility({ - fieldMetadataId: fieldMetadataItem.id, - isVisible: false, - }), - }, - ]} - text={fieldMetadataItem.label} - /> - } - /> - ); + {availableFieldMetadataItemsToShow.length > 0 && + availableFieldMetadataItemsToShow.map((fieldMetadataItem) => { + return ( + + handleChangeFieldVisibility({ + fieldMetadataId: fieldMetadataItem.id, + isVisible: true, + }), }, - )} - - } - /> - )} + ]} + text={fieldMetadataItem.label} + /> + ); + })} ); diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx index 1203a6959f..e1caf1560d 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx @@ -5,9 +5,11 @@ import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/get import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; +import { visibleAndReadableRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { ViewType } from '@/views/types/ViewType'; import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -20,24 +22,15 @@ export const ViewFieldsVisibleDropdownSection = () => { ObjectOptionsDropdownContext, ); - const { - handleReorderColumns, - handleColumnVisibilityChange, - visibleTableColumns, - } = useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); + const { handleReorderColumns, handleColumnVisibilityChange } = + useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); - const { - handleReorderBoardFields, - handleBoardFieldVisibilityChange, - visibleBoardFields, - } = useObjectOptionsForBoard({ - objectNameSingular: objectMetadataItem.nameSingular, - recordBoardId: recordIndexId, - viewBarId: recordIndexId, - }); - - const visibleRecordFields = - viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns; + const { handleReorderBoardFields, handleBoardFieldVisibilityChange } = + useObjectOptionsForBoard({ + objectNameSingular: objectMetadataItem.nameSingular, + recordBoardId: recordIndexId, + viewBarId: recordIndexId, + }); const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); @@ -60,19 +53,23 @@ export const ViewFieldsVisibleDropdownSection = () => { const fieldMetadataItemLabelIdentifier = getLabelIdentifierFieldMetadataItem(objectMetadataItem); + const visibleRecordFields = useRecoilComponentValue( + visibleAndReadableRecordFieldsComponentSelector, + ); + const nonDraggableRecordField = visibleRecordFields.find( (recordFieldToFilter) => - recordFieldToFilter.fieldMetadataId === + recordFieldToFilter.fieldMetadataItemId === fieldMetadataItemLabelIdentifier?.id, ); const draggableRecordFields = visibleRecordFields .filter( (recordFieldToFilter) => - recordFieldToFilter.fieldMetadataId !== - nonDraggableRecordField?.fieldMetadataId, + nonDraggableRecordField?.fieldMetadataItemId !== + recordFieldToFilter.fieldMetadataItemId, ) - .sort(sortByProperty('position')); + .toSorted(sortByProperty('position')); return ( <> @@ -97,26 +94,28 @@ export const ViewFieldsVisibleDropdownSection = () => { (isDefined(fieldMetadataItemLabelIdentifier) ? 1 : 0); const fieldMetadataItem = getFieldMetadataItemById( - recordField.fieldMetadataId, + recordField.fieldMetadataItemId, ); return ( + onClick: () => { handleChangeFieldVisibility({ - fieldMetadataId: recordField.fieldMetadataId, - isVisible: recordField.isVisible, - }), + fieldMetadataId: + recordField.fieldMetadataItemId, + isVisible: false, + }); + }, }, ]} text={fieldMetadataItem.label} diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts index f404c91fa7..2c0ccefe05 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts @@ -38,7 +38,10 @@ export const usePersistViewFieldRecords = () => { const apolloCoreClient = useApolloCoreClient(); const createViewFieldRecords = useCallback( - (viewFieldsToCreate: ViewField[], view: GraphQLView) => { + ( + viewFieldsToCreate: Omit[], + view: GraphQLView, + ) => { if (!viewFieldsToCreate.length) return; return Promise.all( viewFieldsToCreate.map((viewField) => @@ -80,7 +83,7 @@ export const usePersistViewFieldRecords = () => { ); const updateViewFieldRecords = useCallback( - (viewFieldsToUpdate: ViewField[]) => { + (viewFieldsToUpdate: Omit[]) => { if (!viewFieldsToUpdate.length) return; return Promise.all( diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts index 459c8acc78..cad4fc41ef 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts @@ -22,7 +22,7 @@ export const useSaveCurrentViewFields = () => { const saveViewFields = useRecoilCallback( ({ set, snapshot }) => - async (viewFieldsToSave: ViewField[]) => { + async (viewFieldsToSave: Omit[]) => { const currentViewId = snapshot .getLoadable(currentViewIdCallbackState) .getValue(); diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordFieldToViewField.ts b/packages/twenty-front/src/modules/views/utils/mapRecordFieldToViewField.ts new file mode 100644 index 0000000000..0485e41de3 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/mapRecordFieldToViewField.ts @@ -0,0 +1,16 @@ +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { type ViewField } from '@/views/types/ViewField'; + +export const mapRecordFieldToViewField = (recordField: RecordField) => { + const viewField: Omit = { + id: recordField.id, + fieldMetadataId: recordField.fieldMetadataItemId, + isVisible: recordField.isVisible, + position: recordField.position, + size: recordField.size, + aggregateOperation: recordField.aggregateOperation, + __typename: 'ViewField', + }; + + return viewField; +}; diff --git a/packages/twenty-shared/src/utils/array/findById.ts b/packages/twenty-shared/src/utils/array/findById.ts new file mode 100644 index 0000000000..63a51abda3 --- /dev/null +++ b/packages/twenty-shared/src/utils/array/findById.ts @@ -0,0 +1,5 @@ +export const findById = (idToMatch: string) => { + return (itemToFind: T) => { + return itemToFind.id === idToMatch + } +} \ No newline at end of file diff --git a/packages/twenty-shared/src/utils/array/findByProperty.ts b/packages/twenty-shared/src/utils/array/findByProperty.ts new file mode 100644 index 0000000000..046d52d19c --- /dev/null +++ b/packages/twenty-shared/src/utils/array/findByProperty.ts @@ -0,0 +1,5 @@ +export const findByProperty = (property: K, valueToMatch: T[K]) => { + return (itemToFind: T) => { + return itemToFind[property] === valueToMatch + } +} \ No newline at end of file diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 86448ed42e..a2fa7a1eb5 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -7,6 +7,8 @@ * |___/ */ +export { findById } from './array/findById'; +export { findByProperty } from './array/findByProperty'; export { assertUnreachable } from './assertUnreachable'; export { deepMerge } from './deepMerge'; export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields'; diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index 3867bba9ee..4a6d5309bc 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -46,8 +46,8 @@ }, "files": [ "dist", - "assets", "accessibility", + "assets", "components", "display", "feedback", @@ -170,4 +170,4 @@ ] } } -} +} \ No newline at end of file