From 970aa4c5a127fa1c98f06a7d708a2b6a2e5325c1 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 24 Feb 2025 16:46:00 +0100 Subject: [PATCH] Implements new record sort CRUD (#10448) This PR implements new record sorts CRUD as already done on record filters, which is based on record sorts state instead of combined view sorts. It implements a new useSaveRecordSortsToViewSorts with its underlying utils, to compute diff between two view sorts array. The associated unit tests have also been written. This PR also fixes the bug where the view bar disappeared when deleting the already saved record sort of a view. --- .../views/components/ViewBarDetails.tsx | 4 +- ...useAreViewSortsDifferentFromRecordSorts.ts | 44 ++++++++-- .../useSaveCurrentViewFiltersAndSorts.ts | 74 ++-------------- .../hooks/useSaveRecordSortsToViewSorts.ts | 77 +++++++++++++++++ .../utils/__tests__/areViewSortsEqual.test.ts | 40 +++++++++ .../__tests__/getViewSortsToCreate.test.ts | 84 +++++++++++++++++++ .../__tests__/getViewSortsToDelete.test.ts | 72 ++++++++++++++++ .../__tests__/getViewSortsToUpdate.test.ts | 66 +++++++++++++++ .../modules/views/utils/areViewSortsEqual.ts | 12 +++ .../views/utils/getViewSortsToCreate.ts | 18 ++++ .../views/utils/getViewSortsToDelete.ts | 14 ++++ .../views/utils/getViewSortsToUpdate.ts | 26 ++++++ .../views/utils/mapRecordSortToViewSort.ts | 9 ++ 13 files changed, 463 insertions(+), 77 deletions(-) create mode 100644 packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts create mode 100644 packages/twenty-front/src/modules/views/utils/__tests__/areViewSortsEqual.test.ts create mode 100644 packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToCreate.test.ts create mode 100644 packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToDelete.test.ts create mode 100644 packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToUpdate.test.ts create mode 100644 packages/twenty-front/src/modules/views/utils/areViewSortsEqual.ts create mode 100644 packages/twenty-front/src/modules/views/utils/getViewSortsToCreate.ts create mode 100644 packages/twenty-front/src/modules/views/utils/getViewSortsToDelete.ts create mode 100644 packages/twenty-front/src/modules/views/utils/getViewSortsToUpdate.ts create mode 100644 packages/twenty-front/src/modules/views/utils/mapRecordSortToViewSort.ts diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx index 7ff02c7af..e279270f7 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx @@ -179,8 +179,8 @@ export const ViewBarDetails = ({ const shouldExpandViewBar = viewFiltersAreDifferentFromRecordFilters || - ((currentViewWithCombinedFiltersAndSorts?.viewSorts?.length || - currentRecordFilters?.length) && + viewSortsAreDifferentFromRecordSorts || + ((currentRecordSorts?.length || currentRecordFilters?.length) && isViewBarExpanded); if (!shouldExpandViewBar) { diff --git a/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts b/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts index b19896b0e..b8517725a 100644 --- a/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts +++ b/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts @@ -1,14 +1,46 @@ -import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; -import { areViewSortsDifferentFromRecordSortsSelector } from '@/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector'; +import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate'; +import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete'; +import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate'; +import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort'; +import { useMemo } from 'react'; export const useAreViewSortsDifferentFromRecordSorts = () => { const { currentView } = useGetCurrentViewOnly(); - - const viewSortsAreDifferentFromRecordSorts = useRecoilComponentFamilyValueV2( - areViewSortsDifferentFromRecordSortsSelector, - { viewId: currentView?.id }, + const currentRecordSorts = useRecoilComponentValueV2( + currentRecordSortsComponentState, ); + const viewSortsAreDifferentFromRecordSorts = useMemo(() => { + const currentViewSorts = currentView?.viewSorts ?? []; + const viewSortsFromCurrentRecordSorts = currentRecordSorts.map( + mapRecordSortToViewSort, + ); + + const viewSortsToCreate = getViewSortsToCreate( + currentViewSorts, + viewSortsFromCurrentRecordSorts, + ); + + const viewSortsToDelete = getViewSortsToDelete( + currentViewSorts, + viewSortsFromCurrentRecordSorts, + ); + + const viewSortsToUpdate = getViewSortsToUpdate( + currentViewSorts, + viewSortsFromCurrentRecordSorts, + ); + + const sortsHaveChanged = + viewSortsToCreate.length > 0 || + viewSortsToDelete.length > 0 || + viewSortsToUpdate.length > 0; + + return sortsHaveChanged; + }, [currentRecordSorts, currentView]); + return { viewSortsAreDifferentFromRecordSorts }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts index ef8d7b0e7..0e8020d32 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts @@ -4,14 +4,12 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords'; -import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates'; import { useSaveRecordFiltersToViewFilters } from '@/views/hooks/useSaveRecordFiltersToViewFilters'; +import { useSaveRecordSortsToViewSorts } from '@/views/hooks/useSaveRecordSortsToViewSorts'; import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState'; -import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState'; import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState'; -import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState'; import { isDefined } from 'twenty-shared'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -25,18 +23,6 @@ export const useSaveCurrentViewFiltersAndSorts = ( viewBarComponentId, ); - const unsavedToDeleteViewSortIdsCallbackState = - useRecoilComponentCallbackStateV2( - unsavedToDeleteViewSortIdsComponentFamilyState, - viewBarComponentId, - ); - - const unsavedToUpsertViewSortsCallbackState = - useRecoilComponentCallbackStateV2( - unsavedToUpsertViewSortsComponentFamilyState, - viewBarComponentId, - ); - const unsavedToUpsertViewFilterGroupsCallbackState = useRecoilComponentCallbackStateV2( unsavedToUpsertViewFilterGroupsComponentFamilyState, @@ -49,12 +35,6 @@ export const useSaveCurrentViewFiltersAndSorts = ( viewBarComponentId, ); - const { - createViewSortRecords, - updateViewSortRecords, - deleteViewSortRecords, - } = usePersistViewSortRecords(); - const { createViewFilterGroupRecords, deleteViewFilterGroupRecords, @@ -64,52 +44,6 @@ export const useSaveCurrentViewFiltersAndSorts = ( const { resetUnsavedViewStates } = useResetUnsavedViewStates(viewBarComponentId); - const saveViewSorts = useRecoilCallback( - ({ snapshot }) => - async (viewId: string) => { - const unsavedToDeleteViewSortIds = getSnapshotValue( - snapshot, - unsavedToDeleteViewSortIdsCallbackState({ viewId }), - ); - - const unsavedToUpsertViewSorts = getSnapshotValue( - snapshot, - unsavedToUpsertViewSortsCallbackState({ viewId }), - ); - - const view = await getViewFromPrefetchState(viewId); - - if (isUndefinedOrNull(view)) { - return; - } - - const viewSortsToCreate = unsavedToUpsertViewSorts.filter( - (viewSort) => - !view.viewSorts.some( - (vs) => vs.fieldMetadataId === viewSort.fieldMetadataId, - ), - ); - - const viewSortsToUpdate = unsavedToUpsertViewSorts.filter((viewSort) => - view.viewSorts.some( - (vs) => vs.fieldMetadataId === viewSort.fieldMetadataId, - ), - ); - - await createViewSortRecords(viewSortsToCreate, view); - await updateViewSortRecords(viewSortsToUpdate); - await deleteViewSortRecords(unsavedToDeleteViewSortIds); - }, - [ - createViewSortRecords, - deleteViewSortRecords, - getViewFromPrefetchState, - unsavedToDeleteViewSortIdsCallbackState, - unsavedToUpsertViewSortsCallbackState, - updateViewSortRecords, - ], - ); - const saveViewFilterGroups = useRecoilCallback( ({ snapshot }) => async (viewId: string) => { @@ -162,6 +96,8 @@ export const useSaveCurrentViewFiltersAndSorts = ( const { saveRecordFiltersToViewFilters } = useSaveRecordFiltersToViewFilters(); + const { saveRecordSortsToViewSorts } = useSaveRecordSortsToViewSorts(); + const saveCurrentViewFilterAndSorts = useRecoilCallback( ({ snapshot }) => async (viewIdFromProps?: string) => { @@ -176,8 +112,8 @@ export const useSaveCurrentViewFiltersAndSorts = ( const viewId = viewIdFromProps ?? currentViewId; await saveViewFilterGroups(viewId); - await saveViewSorts(viewId); + await saveRecordSortsToViewSorts(); await saveRecordFiltersToViewFilters(); resetUnsavedViewStates(viewId); @@ -185,9 +121,9 @@ export const useSaveCurrentViewFiltersAndSorts = ( [ currentViewIdCallbackState, resetUnsavedViewStates, - saveViewSorts, saveViewFilterGroups, saveRecordFiltersToViewFilters, + saveRecordSortsToViewSorts, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts new file mode 100644 index 000000000..bdc7016e0 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts @@ -0,0 +1,77 @@ +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 { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; +import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate'; +import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete'; +import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate'; +import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared'; + +export const useSaveRecordSortsToViewSorts = () => { + const { + createViewSortRecords, + updateViewSortRecords, + deleteViewSortRecords, + } = usePersistViewSortRecords(); + + const { currentView } = useGetCurrentViewOnly(); + + const currentRecordSortsCallbackState = useRecoilComponentCallbackStateV2( + currentRecordSortsComponentState, + ); + + const saveRecordSortsToViewSorts = useRecoilCallback( + ({ snapshot }) => + async () => { + if (!isDefined(currentView)) { + return; + } + + const currentViewSorts = currentView?.viewSorts ?? []; + + const currentRecordSorts = getSnapshotValue( + snapshot, + currentRecordSortsCallbackState, + ); + + const newViewSorts = currentRecordSorts.map(mapRecordSortToViewSort); + + const viewSortsToCreate = getViewSortsToCreate( + currentViewSorts, + newViewSorts, + ); + + const viewSortsToDelete = getViewSortsToDelete( + currentViewSorts, + newViewSorts, + ); + + const viewSortsToUpdate = getViewSortsToUpdate( + currentViewSorts, + newViewSorts, + ); + + const viewSortIdsToDelete = viewSortsToDelete.map( + (viewSort) => viewSort.id, + ); + + await createViewSortRecords(viewSortsToCreate, currentView); + await updateViewSortRecords(viewSortsToUpdate); + await deleteViewSortRecords(viewSortIdsToDelete); + }, + [ + createViewSortRecords, + deleteViewSortRecords, + updateViewSortRecords, + currentRecordSortsCallbackState, + currentView, + ], + ); + + return { + saveRecordSortsToViewSorts, + }; +}; diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/areViewSortsEqual.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/areViewSortsEqual.test.ts new file mode 100644 index 000000000..9720133e8 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/areViewSortsEqual.test.ts @@ -0,0 +1,40 @@ +import { RecordSortDirection } from '@/object-record/record-sort/types/RecordSortDirection'; +import { ViewSort } from '@/views/types/ViewSort'; +import { areViewSortsEqual } from '@/views/utils/areViewSortsEqual'; + +describe('areViewSortsEqual', () => { + const baseSort: ViewSort = { + __typename: 'ViewSort', + id: 'sort-1', + fieldMetadataId: 'field-1', + direction: 'asc', + }; + + it('should return true when all comparable properties are equal', () => { + const sortA = { ...baseSort }; + const sortB = { ...baseSort }; + + expect(areViewSortsEqual(sortA, sortB)).toBe(true); + }); + + it('should return false when displayValue is different', () => { + const sortA = { ...baseSort }; + const sortB = { ...baseSort, direction: 'desc' as RecordSortDirection }; + + expect(areViewSortsEqual(sortA, sortB)).toBe(false); + }); + + it('should return false when fieldMetadataId is different', () => { + const sortA = { ...baseSort }; + const sortB = { ...baseSort, fieldMetadataId: 'field-2' }; + + expect(areViewSortsEqual(sortA, sortB)).toBe(false); + }); + + it('should ignore non-comparable properties', () => { + const sortA = { ...baseSort, id: 'id-1', createdAt: '2023-01-01' }; + const sortB = { ...baseSort, id: 'id-2', createdAt: '2023-01-02' }; + + expect(areViewSortsEqual(sortA, sortB)).toBe(true); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToCreate.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToCreate.test.ts new file mode 100644 index 000000000..ebc8f5790 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToCreate.test.ts @@ -0,0 +1,84 @@ +import { ViewSort } from '@/views/types/ViewSort'; +import { getViewSortsToCreate } from '../getViewSortsToCreate'; + +describe('getViewSortsToCreate', () => { + const baseSort: ViewSort = { + __typename: 'ViewSort', + id: 'sort-1', + fieldMetadataId: 'field-1', + direction: 'asc', + }; + + it('should return all sorts when current sorts array is empty', () => { + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = [ + { ...baseSort }, + { + ...baseSort, + id: 'sort-2', + fieldMetadataId: 'field-2', + } satisfies ViewSort, + ]; + + const result = getViewSortsToCreate(currentViewSorts, newViewSorts); + + expect(result).toEqual(newViewSorts); + }); + + it('should return empty array when new sorts array is empty', () => { + const currentViewSorts: ViewSort[] = [baseSort]; + const newViewSorts: ViewSort[] = []; + + const result = getViewSortsToCreate(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); + + it('should return only sorts that do not exist in current sorts', () => { + const existingSort = { ...baseSort }; + const newSortWithDifferentFieldMetadataId = { + ...baseSort, + id: 'sort-2', + fieldMetadataId: 'field-2', + } satisfies ViewSort; + + const currentViewSorts: ViewSort[] = [existingSort]; + + const newViewSorts: ViewSort[] = [ + existingSort, + newSortWithDifferentFieldMetadataId, + ]; + + const result = getViewSortsToCreate(currentViewSorts, newViewSorts); + + expect(result).toEqual([newSortWithDifferentFieldMetadataId]); + }); + + it('should handle sorts with different fieldMetadataIds', () => { + const existingSort = { ...baseSort }; + const sortWithDifferentFieldMetadataId = { + ...baseSort, + fieldMetadataId: 'group-2', + } satisfies ViewSort; + + const currentViewSorts: ViewSort[] = [existingSort]; + + const newViewSorts: ViewSort[] = [ + existingSort, + sortWithDifferentFieldMetadataId, + ]; + + const result = getViewSortsToCreate(currentViewSorts, newViewSorts); + + expect(result).toEqual([sortWithDifferentFieldMetadataId]); + }); + + it('should handle empty arrays for both inputs', () => { + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = []; + + const result = getViewSortsToCreate(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToDelete.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToDelete.test.ts new file mode 100644 index 000000000..42972f74d --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToDelete.test.ts @@ -0,0 +1,72 @@ +import { ViewSort } from '@/views/types/ViewSort'; +import { getViewSortsToDelete } from '../getViewSortsToDelete'; + +describe('getViewSortsToDelete', () => { + const baseSort: ViewSort = { + __typename: 'ViewSort', + id: 'sort-1', + fieldMetadataId: 'field-1', + direction: 'asc', + }; + + it('should return empty array when current sorts array is empty', () => { + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = [baseSort]; + + const result = getViewSortsToDelete(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); + + it('should return all current sorts when new sorts array is empty', () => { + const existingSort = { ...baseSort }; + const currentViewSorts: ViewSort[] = [existingSort]; + const newViewSorts: ViewSort[] = []; + + const result = getViewSortsToDelete(currentViewSorts, newViewSorts); + + expect(result).toEqual([existingSort]); + }); + + it('should return sorts that exist in current but not in new sorts', () => { + const sortToDelete = { ...baseSort }; + const sortToKeep = { + ...baseSort, + id: 'filter-2', + fieldMetadataId: 'field-2', + } satisfies ViewSort; + + const currentViewSorts: ViewSort[] = [sortToDelete, sortToKeep]; + const newViewSorts: ViewSort[] = [sortToKeep]; + + const result = getViewSortsToDelete(currentViewSorts, newViewSorts); + + expect(result).toEqual([sortToDelete]); + }); + + it('should handle empty arrays for both inputs', () => { + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = []; + + const result = getViewSortsToDelete(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); + + it('should not delete sorts that match in both fieldMetadataId and direction', () => { + const existingSort = { ...baseSort }; + const matchingSort = { + __typename: 'ViewSort', + id: 'sort-2', + fieldMetadataId: 'field-1', + direction: 'asc', + } satisfies ViewSort; + + const currentViewSorts: ViewSort[] = [existingSort]; + const newViewSorts: ViewSort[] = [matchingSort]; + + const result = getViewSortsToDelete(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToUpdate.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToUpdate.test.ts new file mode 100644 index 000000000..5d715fae4 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToUpdate.test.ts @@ -0,0 +1,66 @@ +import { RecordSortDirection } from '@/object-record/record-sort/types/RecordSortDirection'; +import { ViewSort } from '@/views/types/ViewSort'; +import { getViewSortsToUpdate } from '../getViewSortsToUpdate'; + +describe('getViewSortsToUpdate', () => { + const baseSort: ViewSort = { + __typename: 'ViewSort', + id: 'sort-1', + fieldMetadataId: 'field-1', + direction: 'asc' as RecordSortDirection, + }; + + it('should return empty array when current sorts array is empty', () => { + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = [baseSort]; + + const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); + + it('should return empty array when new sorts array is empty', () => { + const currentViewSorts: ViewSort[] = [baseSort]; + const newViewSorts: ViewSort[] = []; + + const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); + + it('should return sorts that exist in both arrays but have different direction', () => { + const existingSort = { ...baseSort }; + const updatedSort = { + ...baseSort, + direction: 'desc', + } satisfies ViewSort; + + const currentViewSorts: ViewSort[] = [existingSort]; + const newViewSorts: ViewSort[] = [updatedSort]; + + const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); + + expect(result).toEqual([updatedSort]); + }); + + it('should not return sorts that exist in both arrays with same values', () => { + const existingSort = { ...baseSort }; + const sameSort = { ...baseSort }; + + const currentViewSorts: ViewSort[] = [existingSort]; + const newViewSorts: ViewSort[] = [sameSort]; + + const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); + + it('should handle empty arrays for both inputs', () => { + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = []; + + const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/areViewSortsEqual.ts b/packages/twenty-front/src/modules/views/utils/areViewSortsEqual.ts new file mode 100644 index 000000000..d028c2375 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/areViewSortsEqual.ts @@ -0,0 +1,12 @@ +import { ViewSort } from '@/views/types/ViewSort'; + +export const areViewSortsEqual = (viewSortA: ViewSort, viewSortB: ViewSort) => { + const propertiesToCompare: (keyof ViewSort)[] = [ + 'fieldMetadataId', + 'direction', + ]; + + return propertiesToCompare.every( + (property) => viewSortA[property] === viewSortB[property], + ); +}; diff --git a/packages/twenty-front/src/modules/views/utils/getViewSortsToCreate.ts b/packages/twenty-front/src/modules/views/utils/getViewSortsToCreate.ts new file mode 100644 index 000000000..72b14e70b --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/getViewSortsToCreate.ts @@ -0,0 +1,18 @@ +import { ViewSort } from '@/views/types/ViewSort'; +import { isDefined } from 'twenty-shared'; + +export const getViewSortsToCreate = ( + currentViewSorts: ViewSort[], + newViewSorts: ViewSort[], +) => { + return newViewSorts.filter((newViewSort) => { + const correspondingViewSort = currentViewSorts.find( + (currentViewSort) => + currentViewSort.fieldMetadataId === newViewSort.fieldMetadataId, + ); + + const shouldCreateBecauseViewSortIsNew = !isDefined(correspondingViewSort); + + return shouldCreateBecauseViewSortIsNew; + }); +}; diff --git a/packages/twenty-front/src/modules/views/utils/getViewSortsToDelete.ts b/packages/twenty-front/src/modules/views/utils/getViewSortsToDelete.ts new file mode 100644 index 000000000..778b53c0f --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/getViewSortsToDelete.ts @@ -0,0 +1,14 @@ +import { ViewSort } from '@/views/types/ViewSort'; + +export const getViewSortsToDelete = ( + currentViewSorts: ViewSort[], + newViewSorts: ViewSort[], +) => { + return currentViewSorts.filter( + (currentViewSort) => + !newViewSorts.some( + (newViewSort) => + newViewSort.fieldMetadataId === currentViewSort.fieldMetadataId, + ), + ); +}; diff --git a/packages/twenty-front/src/modules/views/utils/getViewSortsToUpdate.ts b/packages/twenty-front/src/modules/views/utils/getViewSortsToUpdate.ts new file mode 100644 index 000000000..1d9076698 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/getViewSortsToUpdate.ts @@ -0,0 +1,26 @@ +import { ViewSort } from '@/views/types/ViewSort'; +import { areViewSortsEqual } from '@/views/utils/areViewSortsEqual'; +import { isDefined } from 'twenty-shared'; + +export const getViewSortsToUpdate = ( + currentViewSorts: ViewSort[], + newViewSorts: ViewSort[], +) => { + return newViewSorts.filter((newViewSort) => { + const correspondingViewSort = currentViewSorts.find( + (currentViewSort) => + currentViewSort.fieldMetadataId === newViewSort.fieldMetadataId, + ); + + if (!isDefined(correspondingViewSort)) { + return false; + } + + const shouldUpdateBecauseViewSortIsDifferent = !areViewSortsEqual( + newViewSort, + correspondingViewSort, + ); + + return shouldUpdateBecauseViewSortIsDifferent; + }); +}; diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordSortToViewSort.ts b/packages/twenty-front/src/modules/views/utils/mapRecordSortToViewSort.ts new file mode 100644 index 000000000..1d84ecfe8 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/mapRecordSortToViewSort.ts @@ -0,0 +1,9 @@ +import { RecordSort } from '@/object-record/record-sort/types/RecordSort'; +import { ViewSort } from '@/views/types/ViewSort'; + +export const mapRecordSortToViewSort = (recordSort: RecordSort): ViewSort => { + return { + __typename: 'ViewSort', + ...recordSort, + } satisfies ViewSort; +};