From 3c80e2601f643c3ef11c5e9ca610c39448de0f96 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 20 Feb 2025 16:24:02 +0100 Subject: [PATCH] Add initialization of new record sorts state and added remove record sorts util (#10358) This PR adds the same synchronization utils and hooks from view to record sorts, as we did with view and record filters. The goal is to apply what's in the view sorts only when needed. Also added tests for those utils and hooks. We also add useRemoveRecordSorts. --- .../components/ObjectSortDropdownButton.tsx | 6 +- .../hooks/useResetSortDropdown.ts | 11 +- ...tionDropdownMenuUnfoldedComponentState.ts} | 4 +- .../RecordIndexRemoveSortingModal.tsx | 4 + .../hooks/useHandleToggleColumnSort.ts | 5 +- .../record-sort/hooks/useRemoveRecordSort.ts | 48 +++++ .../views/components/EditableSortChip.tsx | 4 + .../views/components/ViewBarDetails.tsx | 5 + ...rentViewSortsToCurrentRecordSorts.test.tsx | 196 ++++++++++++++++++ ...pplyViewSortsToCurrentRecordSorts.test.tsx | 110 ++++++++++ ...plyCurrentViewSortsToCurrentRecordSorts.ts | 46 ++++ .../useApplyViewSortsToCurrentRecordSorts.ts | 29 +++ 12 files changed, 456 insertions(+), 12 deletions(-) rename packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/{isRecordSortDirectionMenuUnfoldedComponentState.ts => isRecordSortDirectionDropdownMenuUnfoldedComponentState.ts} (74%) create mode 100644 packages/twenty-front/src/modules/object-record/record-sort/hooks/useRemoveRecordSort.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewSortsToCurrentRecordSorts.test.tsx create mode 100644 packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewSortsToCurrentRecordSorts.test.tsx create mode 100644 packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/useApplyViewSortsToCurrentRecordSorts.ts diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx index b07d5dfc6..c4bba66f7 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx @@ -6,7 +6,7 @@ import { useCloseSortDropdown } from '@/object-record/object-sort-dropdown/hooks import { useResetRecordSortDropdownSearchInput } from '@/object-record/object-sort-dropdown/hooks/useResetRecordSortDropdownSearchInput'; import { useResetSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useResetSortDropdown'; import { useToggleSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useToggleSortDropdown'; -import { isRecordSortDirectionMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isRecordSortDirectionMenuUnfoldedComponentState'; +import { isRecordSortDirectionDropdownMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isRecordSortDirectionDropdownMenuUnfoldedComponentState'; import { objectSortDropdownSearchInputComponentState } from '@/object-record/object-sort-dropdown/states/objectSortDropdownSearchInputComponentState'; import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState'; import { selectedRecordSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedRecordSortDirectionComponentState'; @@ -84,7 +84,7 @@ export const ObjectSortDropdownButton = ({ ); const isRecordSortDirectionMenuUnfolded = useRecoilComponentValueV2( - isRecordSortDirectionMenuUnfoldedComponentState, + isRecordSortDirectionDropdownMenuUnfoldedComponentState, ); const { resetSortDropdown } = useResetSortDropdown(); @@ -168,7 +168,7 @@ export const ObjectSortDropdownButton = ({ useRecoilComponentStateV2(selectedRecordSortDirectionComponentState); const setIsRecordSortDirectionMenuUnfolded = useSetRecoilComponentStateV2( - isRecordSortDirectionMenuUnfoldedComponentState, + isRecordSortDirectionDropdownMenuUnfoldedComponentState, ); const handleSortDirectionClick = (sortDirection: RecordSortDirection) => { diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useResetSortDropdown.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useResetSortDropdown.ts index 2642176a5..a46bb9876 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useResetSortDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useResetSortDropdown.ts @@ -1,18 +1,19 @@ -import { isRecordSortDirectionMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isRecordSortDirectionMenuUnfoldedComponentState'; +import { isRecordSortDirectionDropdownMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isRecordSortDirectionDropdownMenuUnfoldedComponentState'; import { selectedRecordSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedRecordSortDirectionComponentState'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; export const useResetSortDropdown = () => { - const setIsRecordSortDirectionMenuUnfolded = useSetRecoilComponentStateV2( - isRecordSortDirectionMenuUnfoldedComponentState, - ); + const setIsRecordSortDirectionDropdownMenuUnfolded = + useSetRecoilComponentStateV2( + isRecordSortDirectionDropdownMenuUnfoldedComponentState, + ); const setSelectedRecordSortDirection = useSetRecoilComponentStateV2( selectedRecordSortDirectionComponentState, ); const resetSortDropdown = () => { - setIsRecordSortDirectionMenuUnfolded(false); + setIsRecordSortDirectionDropdownMenuUnfolded(false); setSelectedRecordSortDirection('asc'); }; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isRecordSortDirectionMenuUnfoldedComponentState.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isRecordSortDirectionDropdownMenuUnfoldedComponentState.ts similarity index 74% rename from packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isRecordSortDirectionMenuUnfoldedComponentState.ts rename to packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isRecordSortDirectionDropdownMenuUnfoldedComponentState.ts index 26012b1b9..51c2d3ee3 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isRecordSortDirectionMenuUnfoldedComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isRecordSortDirectionDropdownMenuUnfoldedComponentState.ts @@ -1,9 +1,9 @@ import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -export const isRecordSortDirectionMenuUnfoldedComponentState = +export const isRecordSortDirectionDropdownMenuUnfoldedComponentState = createComponentStateV2({ - key: 'isRecordSortDirectionMenuUnfoldedComponentState', + key: 'isRecordSortDirectionDropdownMenuUnfoldedComponentState', defaultValue: false, componentInstanceContext: ObjectSortDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRemoveSortingModal.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRemoveSortingModal.tsx index 9f3d0e82a..79a66702f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRemoveSortingModal.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRemoveSortingModal.tsx @@ -1,5 +1,6 @@ import { useRecoilState } from 'recoil'; +import { useRemoveRecordSort } from '@/object-record/record-sort/hooks/useRemoveRecordSort'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useDeleteCombinedViewSorts } from '@/views/hooks/useDeleteCombinedViewSorts'; @@ -22,9 +23,12 @@ export const RecordIndexRemoveSortingModal = ({ const { deleteCombinedViewSort } = useDeleteCombinedViewSorts(recordIndexId); + const { removeRecordSort } = useRemoveRecordSort(); + const handleRemoveClick = () => { fieldMetadataIds.forEach((id) => { deleteCombinedViewSort(id); + removeRecordSort(id); }); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnSort.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnSort.ts index 634d0b95a..3ddcc99bd 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnSort.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnSort.ts @@ -29,7 +29,7 @@ export const useHandleToggleColumnSort = ({ const { upsertRecordSort } = useUpsertRecordSort(); const handleToggleColumnSort = useCallback( - (fieldMetadataId: string) => { + async (fieldMetadataId: string) => { const correspondingColumnDefinition = columnDefinitions.find( (columnDefinition) => columnDefinition.fieldMetadataId === fieldMetadataId, @@ -48,8 +48,9 @@ export const useHandleToggleColumnSort = ({ direction: 'asc', }; - upsertCombinedViewSort(newSort); upsertRecordSort(newSort); + + await upsertCombinedViewSort(newSort); }, [columnDefinitions, upsertCombinedViewSort, upsertRecordSort], ); diff --git a/packages/twenty-front/src/modules/object-record/record-sort/hooks/useRemoveRecordSort.ts b/packages/twenty-front/src/modules/object-record/record-sort/hooks/useRemoveRecordSort.ts new file mode 100644 index 000000000..58d410132 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-sort/hooks/useRemoveRecordSort.ts @@ -0,0 +1,48 @@ +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; + +export const useRemoveRecordSort = () => { + const currentRecordSortsCallbackState = useRecoilComponentCallbackStateV2( + currentRecordSortsComponentState, + ); + + const removeRecordSort = useRecoilCallback( + ({ set, snapshot }) => + (fieldMetadataId: string) => { + const currentRecordSorts = getSnapshotValue( + snapshot, + currentRecordSortsCallbackState, + ); + + const hasFoundRecordSortInCurrentRecordSorts = currentRecordSorts.some( + (existingSort) => existingSort.fieldMetadataId === fieldMetadataId, + ); + + if (hasFoundRecordSortInCurrentRecordSorts) { + set(currentRecordSortsCallbackState, (currentRecordSorts) => { + const newCurrentRecordSorts = [...currentRecordSorts]; + + const indexOfSortToRemove = newCurrentRecordSorts.findIndex( + (existingSort) => + existingSort.fieldMetadataId === fieldMetadataId, + ); + + if (indexOfSortToRemove < 0) { + return newCurrentRecordSorts; + } + + newCurrentRecordSorts.splice(indexOfSortToRemove, 1); + + return newCurrentRecordSorts; + }); + } + }, + [currentRecordSortsCallbackState], + ); + + return { + removeRecordSort, + }; +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx b/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx index 8e55d117d..f829211a6 100644 --- a/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx @@ -1,5 +1,6 @@ import { IconArrowDown, IconArrowUp } from 'twenty-ui'; +import { useRemoveRecordSort } from '@/object-record/record-sort/hooks/useRemoveRecordSort'; import { useUpsertRecordSort } from '@/object-record/record-sort/hooks/useUpsertRecordSort'; import { RecordSort } from '@/object-record/record-sort/types/RecordSort'; import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; @@ -13,12 +14,15 @@ type EditableSortChipProps = { export const EditableSortChip = ({ recordSort }: EditableSortChipProps) => { const { deleteCombinedViewSort } = useDeleteCombinedViewSorts(); + const { removeRecordSort } = useRemoveRecordSort(); + const { upsertCombinedViewSort } = useUpsertCombinedViewSorts(); const { upsertRecordSort } = useUpsertRecordSort(); const handleRemoveClick = () => { deleteCombinedViewSort(recordSort.fieldMetadataId); + removeRecordSort(recordSort.fieldMetadataId); }; const handleClick = () => { diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx index 0363c6248..54b5fbef6 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx @@ -17,6 +17,7 @@ import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/ import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { SoftDeleteFilterChip } from '@/views/components/SoftDeleteFilterChip'; import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyCurrentViewFiltersToCurrentRecordFilters'; +import { useApplyCurrentViewSortsToCurrentRecordSorts } from '@/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts'; import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters'; import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; @@ -163,10 +164,14 @@ export const ViewBarDetails = ({ const { applyCurrentViewFiltersToCurrentRecordFilters } = useApplyCurrentViewFiltersToCurrentRecordFilters(); + const { applyCurrentViewSortsToCurrentRecordSorts } = + useApplyCurrentViewSortsToCurrentRecordSorts(); + const handleCancelClick = () => { if (isDefined(viewId)) { resetUnsavedViewStates(viewId); applyCurrentViewFiltersToCurrentRecordFilters(); + applyCurrentViewSortsToCurrentRecordSorts(); toggleSoftDeleteFilterState(false); } }; diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewSortsToCurrentRecordSorts.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewSortsToCurrentRecordSorts.test.tsx new file mode 100644 index 000000000..911e129f2 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewSortsToCurrentRecordSorts.test.tsx @@ -0,0 +1,196 @@ +import { act, renderHook } from '@testing-library/react'; + +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { RecordSort } from '@/object-record/record-sort/types/RecordSort'; + +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +import { ViewSort } from '@/views/types/ViewSort'; +import { isDefined } from 'twenty-shared'; + +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState'; + +import { View } from '@/views/types/View'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +import { mockedViewsData } from '~/testing/mock-data/views'; +import { useApplyCurrentViewSortsToCurrentRecordSorts } from '../useApplyCurrentViewSortsToCurrentRecordSorts'; + +const mockObjectMetadataItemNameSingular = 'company'; + +describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => { + const mockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === mockObjectMetadataItemNameSingular, + ); + + if (!isDefined(mockObjectMetadataItem)) { + throw new Error( + 'Missing mock object metadata item with name singular "company"', + ); + } + + const mockFieldMetadataItem = mockObjectMetadataItem.fields.find( + (field) => field.name === 'name', + ); + + if (!isDefined(mockFieldMetadataItem)) { + throw new Error('Missing mock field metadata item with type TEXT'); + } + + const mockViewSort: ViewSort = { + __typename: 'ViewSort', + id: 'sort-1', + fieldMetadataId: mockFieldMetadataItem.id, + direction: 'asc', + }; + + const allCompaniesView = mockedViewsData[0]; + + const mockView = { + ...allCompaniesView, + viewSorts: [mockViewSort], + } satisfies View; + + it('should apply sorts from current view', () => { + const { result } = renderHook( + () => { + const { applyCurrentViewSortsToCurrentRecordSorts } = + useApplyCurrentViewSortsToCurrentRecordSorts(); + + const currentSorts = useRecoilComponentValueV2( + currentRecordSortsComponentState, + ); + + return { + applyCurrentViewSortsToCurrentRecordSorts, + currentSorts, + }; + }, + { + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, + onInitializeRecoilSnapshot: (snapshot) => { + snapshot.set( + contextStoreCurrentViewIdComponentState.atomFamily({ + instanceId: 'instanceId', + }), + mockView.id, + ); + + snapshot.set(prefetchViewsState, [mockView]); + }, + }), + }, + ); + + act(() => { + result.current.applyCurrentViewSortsToCurrentRecordSorts(); + }); + + expect(result.current.currentSorts).toEqual([ + { + id: mockViewSort.id, + fieldMetadataId: mockViewSort.fieldMetadataId, + direction: mockViewSort.direction, + definition: { + fieldMetadataId: mockViewSort.fieldMetadataId, + iconName: mockFieldMetadataItem.icon ?? '', + label: mockFieldMetadataItem.label, + }, + } satisfies RecordSort, + ]); + }); + + it('should not apply sorts when current view is not found', () => { + const { result } = renderHook( + () => { + const { applyCurrentViewSortsToCurrentRecordSorts } = + useApplyCurrentViewSortsToCurrentRecordSorts(); + + const currentSorts = useRecoilComponentValueV2( + currentRecordSortsComponentState, + ); + + return { + applyCurrentViewSortsToCurrentRecordSorts, + currentSorts, + }; + }, + { + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, + onInitializeRecoilSnapshot: (snapshot) => { + snapshot.set( + contextStoreCurrentViewIdComponentState.atomFamily({ + instanceId: 'instanceId', + }), + mockView.id, + ); + + snapshot.set(prefetchViewsState, []); + }, + }), + }, + ); + + act(() => { + result.current.applyCurrentViewSortsToCurrentRecordSorts(); + }); + + expect(result.current.currentSorts).toEqual([]); + }); + + it('should handle view with empty sorts', () => { + const viewWithNoSorts = { + ...mockView, + viewSorts: [], + }; + + const { result } = renderHook( + () => { + const { applyCurrentViewSortsToCurrentRecordSorts } = + useApplyCurrentViewSortsToCurrentRecordSorts(); + + const currentSorts = useRecoilComponentValueV2( + currentRecordSortsComponentState, + ); + + return { + applyCurrentViewSortsToCurrentRecordSorts, + currentSorts, + }; + }, + { + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, + onInitializeRecoilSnapshot: (snapshot) => { + snapshot.set( + contextStoreCurrentViewIdComponentState.atomFamily({ + instanceId: 'instanceId', + }), + mockView.id, + ); + + snapshot.set(prefetchViewsState, [viewWithNoSorts]); + }, + }), + }, + ); + + act(() => { + result.current.applyCurrentViewSortsToCurrentRecordSorts(); + }); + + expect(result.current.currentSorts).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewSortsToCurrentRecordSorts.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewSortsToCurrentRecordSorts.test.tsx new file mode 100644 index 000000000..196a5fffe --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewSortsToCurrentRecordSorts.test.tsx @@ -0,0 +1,110 @@ +import { act, renderHook } from '@testing-library/react'; + +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { RecordSort } from '@/object-record/record-sort/types/RecordSort'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { ViewSort } from '@/views/types/ViewSort'; +import { isDefined } from 'twenty-shared'; + +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; +import { useApplyViewSortsToCurrentRecordSorts } from '../useApplyViewSortsToCurrentRecordSorts'; + +const mockObjectMetadataItemNameSingular = 'company'; + +describe('useApplyViewSortsToCurrentRecordSorts', () => { + const mockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === mockObjectMetadataItemNameSingular, + ); + + if (!isDefined(mockObjectMetadataItem)) { + throw new Error( + `Missing mock object metadata item with name singular ${mockObjectMetadataItemNameSingular}`, + ); + } + + const mockFieldMetadataItem = mockObjectMetadataItem.fields.find( + (field) => field.name === 'name', + ); + + if (!isDefined(mockFieldMetadataItem)) { + throw new Error(`Missing mock field metadata Name`); + } + + const mockViewSort: ViewSort = { + __typename: 'ViewSort', + id: 'sort-1', + fieldMetadataId: mockFieldMetadataItem.id, + direction: 'asc', + }; + + it('should apply view sorts to current record sorts', () => { + const { result } = renderHook( + () => { + const { applyViewSortsToCurrentRecordSorts } = + useApplyViewSortsToCurrentRecordSorts(); + + const currentSorts = useRecoilComponentValueV2( + currentRecordSortsComponentState, + ); + + return { applyViewSortsToCurrentRecordSorts, currentSorts }; + }, + { + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, + }), + }, + ); + + act(() => { + result.current.applyViewSortsToCurrentRecordSorts([mockViewSort]); + }); + + expect(result.current.currentSorts).toEqual([ + { + id: mockViewSort.id, + fieldMetadataId: mockViewSort.fieldMetadataId, + direction: mockViewSort.direction, + definition: { + fieldMetadataId: mockViewSort.fieldMetadataId, + label: mockFieldMetadataItem.label, + iconName: mockFieldMetadataItem.icon ?? '', + }, + } satisfies RecordSort, + ]); + }); + + it('should handle empty view sorts array', () => { + const { result } = renderHook( + () => { + const { applyViewSortsToCurrentRecordSorts } = + useApplyViewSortsToCurrentRecordSorts(); + + const currentSorts = useRecoilComponentValueV2( + currentRecordSortsComponentState, + ); + + return { applyViewSortsToCurrentRecordSorts, currentSorts }; + }, + { + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, + }), + }, + ); + + act(() => { + result.current.applyViewSortsToCurrentRecordSorts([]); + }); + + expect(result.current.currentSorts).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts.ts b/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts.ts new file mode 100644 index 000000000..51992580f --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts.ts @@ -0,0 +1,46 @@ +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions'; +import { useSortableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-sort/hooks/useSortableFieldMetadataItemsInRecordIndexContext'; +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; +import { useRecoilValue } from 'recoil'; + +import { isDefined } from 'twenty-shared'; + +export const useApplyCurrentViewSortsToCurrentRecordSorts = () => { + const currentViewId = useRecoilComponentValueV2( + contextStoreCurrentViewIdComponentState, + ); + + const currentView = useRecoilValue( + prefetchViewFromViewIdFamilySelector({ + viewId: currentViewId ?? '', + }), + ); + + const setCurrentRecordSorts = useSetRecoilComponentStateV2( + currentRecordSortsComponentState, + ); + + const { sortableFieldMetadataItems } = + useSortableFieldMetadataItemsInRecordIndexContext(); + + const applyCurrentViewSortsToCurrentRecordSorts = () => { + const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ + fields: sortableFieldMetadataItems, + }); + + if (isDefined(currentView)) { + setCurrentRecordSorts( + mapViewSortsToSorts(currentView.viewSorts, sortDefinitions), + ); + } + }; + + return { + applyCurrentViewSortsToCurrentRecordSorts, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useApplyViewSortsToCurrentRecordSorts.ts b/packages/twenty-front/src/modules/views/hooks/useApplyViewSortsToCurrentRecordSorts.ts new file mode 100644 index 000000000..f9ab2d558 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useApplyViewSortsToCurrentRecordSorts.ts @@ -0,0 +1,29 @@ +import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions'; +import { useSortableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-sort/hooks/useSortableFieldMetadataItemsInRecordIndexContext'; +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { ViewSort } from '@/views/types/ViewSort'; +import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; + +export const useApplyViewSortsToCurrentRecordSorts = () => { + const setCurrentRecordSorts = useSetRecoilComponentStateV2( + currentRecordSortsComponentState, + ); + + const { sortableFieldMetadataItems } = + useSortableFieldMetadataItemsInRecordIndexContext(); + + const applyViewSortsToCurrentRecordSorts = (viewSorts: ViewSort[]) => { + const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ + fields: sortableFieldMetadataItems, + }); + + const recordSortsToApply = mapViewSortsToSorts(viewSorts, sortDefinitions); + + setCurrentRecordSorts(recordSortsToApply); + }; + + return { + applyViewSortsToCurrentRecordSorts, + }; +};