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