diff --git a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx index 4d0e22b10..4cc33451e 100644 --- a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx +++ b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx @@ -11,15 +11,15 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { UPDATE_VIEW_BUTTON_DROPDOWN_ID } from '@/views/constants/UpdateViewButtonDropdownId'; import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; +import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters'; +import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts'; import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; -import { canPersistViewComponentFamilySelector } from '@/views/states/selectors/canPersistViewComponentFamilySelector'; import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId'; import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState'; @@ -46,11 +46,6 @@ export const UpdateViewButtonGroup = ({ const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState); - const canPersistView = useRecoilComponentFamilyValueV2( - canPersistViewComponentFamilySelector, - { viewId: currentViewId }, - ); - const { closeDropdown: closeUpdateViewButtonDropdown } = useDropdown( UPDATE_VIEW_BUTTON_DROPDOWN_ID, ); @@ -89,7 +84,16 @@ export const UpdateViewButtonGroup = ({ const { hasFiltersQueryParams } = useViewFromQueryParams(); - const canShowButton = canPersistView && !hasFiltersQueryParams; + const { viewFiltersAreDifferentFromRecordFilters } = + useAreViewFiltersDifferentFromRecordFilters(); + + const { viewSortsAreDifferentFromRecordSorts } = + useAreViewSortsDifferentFromRecordSorts(); + + const canShowButton = + (viewFiltersAreDifferentFromRecordFilters || + viewSortsAreDifferentFromRecordSorts) && + !hasFiltersQueryParams; if (!canShowButton) { return <>; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx index d81bacc00..1a915825e 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx @@ -4,10 +4,8 @@ import { ReactNode, useMemo } from 'react'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; -import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; -import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { AdvancedFilterDropdownButton } from '@/views/components/AdvancedFilterDropdownButton'; import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton'; @@ -15,14 +13,14 @@ import { EditableSortChip } from '@/views/components/EditableSortChip'; import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect'; import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyCurrentViewFiltersToCurrentRecordFilters'; +import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters'; +import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates'; -import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState'; import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState'; -import { canPersistViewComponentFamilySelector } from '@/views/states/selectors/canPersistViewComponentFamilySelector'; -import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; import { isDefined } from 'twenty-ui'; import { VariantFilterChip } from './VariantFilterChip'; @@ -118,13 +116,8 @@ export const ViewBarDetails = ({ const { hasFiltersQueryParams } = useViewFromQueryParams(); - const canPersistView = useRecoilComponentFamilyValueV2( - canPersistViewComponentFamilySelector, - { viewId }, - ); - - const availableFilterDefinitions = useRecoilComponentValueV2( - availableFilterDefinitionsComponentState, + const currentRecordFilters = useRecoilComponentValueV2( + currentRecordFiltersComponentState, ); const availableSortDefinitions = useRecoilComponentValueV2( @@ -139,35 +132,34 @@ export const ViewBarDetails = ({ viewBarId: viewBarId, }); const { resetUnsavedViewStates } = useResetUnsavedViewStates(); - const canResetView = canPersistView && !hasFiltersQueryParams; - const { otherViewFilters, defaultViewFilters } = useMemo(() => { - if (!currentViewWithCombinedFiltersAndSorts) { - return { - otherViewFilters: [], - defaultViewFilters: [], - }; - } + const { viewFiltersAreDifferentFromRecordFilters } = + useAreViewFiltersDifferentFromRecordFilters(); - const otherViewFilters = - currentViewWithCombinedFiltersAndSorts.viewFilters.filter( - (viewFilter) => - viewFilter.variant && - viewFilter.variant !== 'default' && - !viewFilter.viewFilterGroupId, - ); - const defaultViewFilters = - currentViewWithCombinedFiltersAndSorts.viewFilters.filter( - (viewFilter) => - (!viewFilter.variant || viewFilter.variant === 'default') && - !viewFilter.viewFilterGroupId, - ); + const { viewSortsAreDifferentFromRecordSorts } = + useAreViewSortsDifferentFromRecordSorts(); - return { - otherViewFilters, - defaultViewFilters, - }; - }, [currentViewWithCombinedFiltersAndSorts]); + const canResetView = + (viewFiltersAreDifferentFromRecordFilters || + viewSortsAreDifferentFromRecordSorts) && + !hasFiltersQueryParams; + + const otherViewFilters = useMemo(() => { + return currentRecordFilters.filter( + (viewFilter) => + viewFilter.variant && + viewFilter.variant !== 'default' && + !viewFilter.viewFilterGroupId, + ); + }, [currentRecordFilters]); + + const defaultViewFilters = useMemo(() => { + return currentRecordFilters.filter( + (viewFilter) => + (!viewFilter.variant || viewFilter.variant === 'default') && + !viewFilter.viewFilterGroupId, + ); + }, [currentRecordFilters]); const { applyCurrentViewFiltersToCurrentRecordFilters } = useApplyCurrentViewFiltersToCurrentRecordFilters(); @@ -181,9 +173,9 @@ export const ViewBarDetails = ({ }; const shouldExpandViewBar = - canPersistView || + viewFiltersAreDifferentFromRecordFilters || ((currentViewWithCombinedFiltersAndSorts?.viewSorts?.length || - currentViewWithCombinedFiltersAndSorts?.viewFilters?.length) && + currentRecordFilters?.length) && isViewBarExpanded); if (!shouldExpandViewBar) { @@ -201,11 +193,7 @@ export const ViewBarDetails = ({ {otherViewFilters.map((viewFilter) => ( ))} @@ -228,10 +216,7 @@ export const ViewBarDetails = ({ )} {showAdvancedFilterDropdownButton && } - {mapViewFiltersToFilters( - defaultViewFilters, - availableFilterDefinitions, - ).map((viewFilter) => ( + {defaultViewFilters.map((viewFilter) => ( { + const { currentView } = useGetCurrentViewOnly(); + const currentRecordFilters = useRecoilComponentValueV2( + currentRecordFiltersComponentState, + ); + + const viewFiltersAreDifferentFromRecordFilters = useMemo(() => { + const currentViewFilters = currentView?.viewFilters ?? []; + const viewFiltersFromCurrentRecordFilters = currentRecordFilters.map( + mapRecordFilterToViewFilter, + ); + + const viewFiltersToCreate = getViewFiltersToCreate( + currentViewFilters, + viewFiltersFromCurrentRecordFilters, + ); + + const viewFiltersToDelete = getViewFiltersToDelete( + currentViewFilters, + viewFiltersFromCurrentRecordFilters, + ); + + const viewFiltersToUpdate = getViewFiltersToUpdate( + currentViewFilters, + viewFiltersFromCurrentRecordFilters, + ); + + const filtersHaveChanged = + viewFiltersToCreate.length > 0 || + viewFiltersToDelete.length > 0 || + viewFiltersToUpdate.length > 0; + + return filtersHaveChanged; + }, [currentRecordFilters, currentView]); + + return { viewFiltersAreDifferentFromRecordFilters }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts b/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts new file mode 100644 index 000000000..b19896b0e --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts @@ -0,0 +1,14 @@ +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { areViewSortsDifferentFromRecordSortsSelector } from '@/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector'; + +export const useAreViewSortsDifferentFromRecordSorts = () => { + const { currentView } = useGetCurrentViewOnly(); + + const viewSortsAreDifferentFromRecordSorts = useRecoilComponentFamilyValueV2( + areViewSortsDifferentFromRecordSortsSelector, + { viewId: currentView?.id }, + ); + + return { viewSortsAreDifferentFromRecordSorts }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useGetCurrentViewOnly.ts b/packages/twenty-front/src/modules/views/hooks/useGetCurrentViewOnly.ts new file mode 100644 index 000000000..e6fb9bed9 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useGetCurrentViewOnly.ts @@ -0,0 +1,22 @@ +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; +import { View } from '@/views/types/View'; + +import { useMemo } from 'react'; + +export const useGetCurrentViewOnly = () => { + const { records: views } = usePrefetchedData(PrefetchKey.AllViews); + + const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState); + + const currentView = useMemo( + () => views.find((view) => view.id === currentViewId), + [views, currentViewId], + ); + + return { + currentView, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts index f2f94e240..aa04c5533 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts @@ -3,16 +3,14 @@ import { useRecoilCallback } from 'recoil'; 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 { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates'; +import { useSaveRecordFiltersToViewFilters } from '@/views/hooks/useSaveRecordFiltersToViewFilters'; import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState'; -import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState'; import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState'; import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState'; -import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState'; import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState'; import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -39,18 +37,6 @@ export const useSaveCurrentViewFiltersAndSorts = ( viewBarComponentId, ); - const unsavedToDeleteViewFilterIdsCallbackState = - useRecoilComponentCallbackStateV2( - unsavedToDeleteViewFilterIdsComponentFamilyState, - viewBarComponentId, - ); - - const unsavedToUpsertViewFiltersCallbackState = - useRecoilComponentCallbackStateV2( - unsavedToUpsertViewFiltersComponentFamilyState, - viewBarComponentId, - ); - const unsavedToUpsertViewFilterGroupsCallbackState = useRecoilComponentCallbackStateV2( unsavedToUpsertViewFilterGroupsComponentFamilyState, @@ -69,12 +55,6 @@ export const useSaveCurrentViewFiltersAndSorts = ( deleteViewSortRecords, } = usePersistViewSortRecords(); - const { - createViewFilterRecords, - updateViewFilterRecords, - deleteViewFilterRecords, - } = usePersistViewFilterRecords(); - const { createViewFilterGroupRecords, deleteViewFilterGroupRecords, @@ -130,53 +110,6 @@ export const useSaveCurrentViewFiltersAndSorts = ( ], ); - const saveViewFilters = useRecoilCallback( - ({ snapshot }) => - async (viewId: string) => { - const unsavedToDeleteViewFilterIds = getSnapshotValue( - snapshot, - unsavedToDeleteViewFilterIdsCallbackState({ viewId }), - ); - - const unsavedToUpsertViewFilters = getSnapshotValue( - snapshot, - unsavedToUpsertViewFiltersCallbackState({ viewId }), - ); - - const view = await getViewFromCache(viewId); - - if (isUndefinedOrNull(view)) { - return; - } - - const viewFiltersToCreate = unsavedToUpsertViewFilters.filter( - (viewFilter) => - !view.viewFilters.some( - (viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id, - ), - ); - - const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter( - (viewFilter) => - view.viewFilters.some( - (viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id, - ), - ); - - await createViewFilterRecords(viewFiltersToCreate, view); - await updateViewFilterRecords(viewFiltersToUpdate); - await deleteViewFilterRecords(unsavedToDeleteViewFilterIds); - }, - [ - createViewFilterRecords, - deleteViewFilterRecords, - getViewFromCache, - unsavedToDeleteViewFilterIdsCallbackState, - unsavedToUpsertViewFiltersCallbackState, - updateViewFilterRecords, - ], - ); - const saveViewFilterGroups = useRecoilCallback( ({ snapshot }) => async (viewId: string) => { @@ -226,6 +159,9 @@ export const useSaveCurrentViewFiltersAndSorts = ( ], ); + const { saveRecordFiltersToViewFilters } = + useSaveRecordFiltersToViewFilters(); + const saveCurrentViewFilterAndSorts = useRecoilCallback( ({ snapshot }) => async (viewIdFromProps?: string) => { @@ -240,17 +176,18 @@ export const useSaveCurrentViewFiltersAndSorts = ( const viewId = viewIdFromProps ?? currentViewId; await saveViewFilterGroups(viewId); - await saveViewFilters(viewId); await saveViewSorts(viewId); + await saveRecordFiltersToViewFilters(); + resetUnsavedViewStates(viewId); }, [ currentViewIdCallbackState, resetUnsavedViewStates, - saveViewFilters, saveViewSorts, saveViewFilterGroups, + saveRecordFiltersToViewFilters, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts new file mode 100644 index 000000000..3a3e8b903 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts @@ -0,0 +1,79 @@ +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; +import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate'; +import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete'; +import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate'; +import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const useSaveRecordFiltersToViewFilters = () => { + const { + createViewFilterRecords, + updateViewFilterRecords, + deleteViewFilterRecords, + } = usePersistViewFilterRecords(); + + const { currentView } = useGetCurrentViewOnly(); + + const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2( + currentRecordFiltersComponentState, + ); + + const saveRecordFiltersToViewFilters = useRecoilCallback( + ({ snapshot }) => + async () => { + if (!isDefined(currentView)) { + return; + } + + const currentViewFilters = currentView?.viewFilters ?? []; + + const currentRecordFilters = getSnapshotValue( + snapshot, + currentRecordFiltersCallbackState, + ); + + const newViewFilters = currentRecordFilters.map( + mapRecordFilterToViewFilter, + ); + + const viewFiltersToCreate = getViewFiltersToCreate( + currentViewFilters, + newViewFilters, + ); + + const viewFiltersToDelete = getViewFiltersToDelete( + currentViewFilters, + newViewFilters, + ); + + const viewFiltersToUpdate = getViewFiltersToUpdate( + currentViewFilters, + newViewFilters, + ); + + const viewFilterIdsToDelete = viewFiltersToDelete.map( + (viewFilter) => viewFilter.id, + ); + + await createViewFilterRecords(viewFiltersToCreate, currentView); + await updateViewFilterRecords(viewFiltersToUpdate); + await deleteViewFilterRecords(viewFilterIdsToDelete); + }, + [ + createViewFilterRecords, + deleteViewFilterRecords, + updateViewFilterRecords, + currentRecordFiltersCallbackState, + currentView, + ], + ); + + return { + saveRecordFiltersToViewFilters, + }; +}; diff --git a/packages/twenty-front/src/modules/views/states/selectors/canPersistViewComponentFamilySelector.ts b/packages/twenty-front/src/modules/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector.ts similarity index 59% rename from packages/twenty-front/src/modules/views/states/selectors/canPersistViewComponentFamilySelector.ts rename to packages/twenty-front/src/modules/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector.ts index 51cc6d230..a25d1b397 100644 --- a/packages/twenty-front/src/modules/views/states/selectors/canPersistViewComponentFamilySelector.ts +++ b/packages/twenty-front/src/modules/views/states/selectors/areViewSortsDifferentFromRecordSortsFamilySelector.ts @@ -1,35 +1,21 @@ import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState'; import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState'; -import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState'; import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState'; -export const canPersistViewComponentFamilySelector = +export const areViewSortsDifferentFromRecordSortsSelector = createComponentFamilySelectorV2({ - key: 'canPersistViewComponentFamilySelector', + key: 'areViewSortsDifferentFromRecordSortsSelector', get: ({ familyKey, instanceId }) => ({ get }) => { return ( - get( - unsavedToUpsertViewFiltersComponentFamilyState.atomFamily({ - familyKey, - instanceId, - }), - ).length > 0 || get( unsavedToUpsertViewSortsComponentFamilyState.atomFamily({ familyKey, instanceId, }), ).length > 0 || - get( - unsavedToDeleteViewFilterIdsComponentFamilyState.atomFamily({ - familyKey, - instanceId, - }), - ).length > 0 || get( unsavedToDeleteViewSortIdsComponentFamilyState.atomFamily({ familyKey, diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/areViewFiltersEqual.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/areViewFiltersEqual.test.ts new file mode 100644 index 000000000..28db1a0c0 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/areViewFiltersEqual.test.ts @@ -0,0 +1,89 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { areViewFiltersEqual } from '../areViewFiltersEqual'; + +describe('areViewFiltersEqual', () => { + const baseFilter: ViewFilter = { + __typename: 'ViewFilter', + id: 'filter-1', + fieldMetadataId: 'field-1', + operand: ViewFilterOperand.Contains, + value: 'test', + displayValue: 'test', + viewFilterGroupId: 'group-1', + positionInViewFilterGroup: 0, + }; + + it('should return true when all comparable properties are equal', () => { + const filterA = { ...baseFilter }; + const filterB = { ...baseFilter }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(true); + }); + + it('should return false when displayValue is different', () => { + const filterA = { ...baseFilter }; + const filterB = { ...baseFilter, displayValue: 'different' }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(false); + }); + + it('should return false when fieldMetadataId is different', () => { + const filterA = { ...baseFilter }; + const filterB = { ...baseFilter, fieldMetadataId: 'field-2' }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(false); + }); + + it('should return false when viewFilterGroupId is different', () => { + const filterA = { ...baseFilter }; + const filterB = { ...baseFilter, viewFilterGroupId: 'group-2' }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(false); + }); + + it('should return false when operand is different', () => { + const filterA = { ...baseFilter }; + const filterB = { + ...baseFilter, + operand: ViewFilterOperand.DoesNotContain, + }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(false); + }); + + it('should return false when positionInViewFilterGroup is different', () => { + const filterA = { ...baseFilter }; + const filterB = { ...baseFilter, positionInViewFilterGroup: 1 }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(false); + }); + + it('should return false when value is different', () => { + const filterA = { ...baseFilter }; + const filterB = { ...baseFilter, value: 'different' }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(false); + }); + + it('should ignore non-comparable properties', () => { + const filterA = { ...baseFilter, id: 'id-1', createdAt: '2023-01-01' }; + const filterB = { ...baseFilter, id: 'id-2', createdAt: '2023-01-02' }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(true); + }); + + it('should handle undefined optional properties', () => { + const filterA = { ...baseFilter, viewFilterGroupId: undefined }; + const filterB = { ...baseFilter, viewFilterGroupId: undefined }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(true); + }); + + it('should handle one filter having optional property and other not', () => { + const filterA = { ...baseFilter, viewFilterGroupId: 'group-1' }; + const filterB = { ...baseFilter, viewFilterGroupId: undefined }; + + expect(areViewFiltersEqual(filterA, filterB)).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToCreate.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToCreate.test.ts new file mode 100644 index 000000000..5b87e142e --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToCreate.test.ts @@ -0,0 +1,117 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { getViewFiltersToCreate } from '../getViewFiltersToCreate'; + +describe('getViewFiltersToCreate', () => { + const baseFilter: ViewFilter = { + __typename: 'ViewFilter', + id: 'filter-1', + fieldMetadataId: 'field-1', + operand: ViewFilterOperand.Contains, + value: 'test', + displayValue: 'test', + viewFilterGroupId: 'group-1', + positionInViewFilterGroup: 0, + }; + + it('should return all filters when current filters array is empty', () => { + const currentViewFilters: ViewFilter[] = []; + const newViewFilters: ViewFilter[] = [ + { ...baseFilter }, + { ...baseFilter, id: 'filter-2', fieldMetadataId: 'field-2' }, + ]; + + const result = getViewFiltersToCreate(currentViewFilters, newViewFilters); + + expect(result).toEqual(newViewFilters); + }); + + it('should return empty array when new filters array is empty', () => { + const currentViewFilters: ViewFilter[] = [baseFilter]; + const newViewFilters: ViewFilter[] = []; + + const result = getViewFiltersToCreate(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should return only filters that do not exist in current filters', () => { + const existingFilter = { ...baseFilter }; + const newFilterWithDifferentFieldMetadata = { + ...baseFilter, + id: 'filter-2', + fieldMetadataId: 'field-2', + }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + + const newViewFilters: ViewFilter[] = [ + existingFilter, + newFilterWithDifferentFieldMetadata, + ]; + + const result = getViewFiltersToCreate(currentViewFilters, newViewFilters); + + expect(result).toEqual([newFilterWithDifferentFieldMetadata]); + }); + + it('should handle filters with different viewFilterGroupIds', () => { + const existingFilter = { ...baseFilter }; + const filterWithDifferentGroup = { + ...baseFilter, + viewFilterGroupId: 'group-2', + }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + + const newViewFilters: ViewFilter[] = [ + existingFilter, + filterWithDifferentGroup, + ]; + + const result = getViewFiltersToCreate(currentViewFilters, newViewFilters); + + expect(result).toEqual([filterWithDifferentGroup]); + }); + + it('should handle empty arrays for both inputs', () => { + const currentViewFilters: ViewFilter[] = []; + const newViewFilters: ViewFilter[] = []; + + const result = getViewFiltersToCreate(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should consider filters with same fieldMetadataId but different viewFilterGroupId as new', () => { + const currentViewFilters: ViewFilter[] = [baseFilter]; + const filterWithSameFieldMetadataIdButDifferentGroup = { + ...baseFilter, + id: 'filter-2', + viewFilterGroupId: 'group-2', + }; + const newViewFilters: ViewFilter[] = [ + filterWithSameFieldMetadataIdButDifferentGroup, + ]; + + const result = getViewFiltersToCreate(currentViewFilters, newViewFilters); + + expect(result).toEqual([filterWithSameFieldMetadataIdButDifferentGroup]); + }); + + it('should consider filters with same viewFilterGroupId but different fieldMetadataId as new', () => { + const currentViewFilters: ViewFilter[] = [baseFilter]; + const filterWithSameGroupButDifferentFieldMetadata = { + ...baseFilter, + id: 'filter-2', + fieldMetadataId: 'field-2', + }; + const newViewFilters: ViewFilter[] = [ + filterWithSameGroupButDifferentFieldMetadata, + ]; + + const result = getViewFiltersToCreate(currentViewFilters, newViewFilters); + + expect(result).toEqual([filterWithSameGroupButDifferentFieldMetadata]); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToDelete.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToDelete.test.ts new file mode 100644 index 000000000..0440b5236 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToDelete.test.ts @@ -0,0 +1,99 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { getViewFiltersToDelete } from '../getViewFiltersToDelete'; + +describe('getViewFiltersToDelete', () => { + const baseFilter: ViewFilter = { + __typename: 'ViewFilter', + id: 'filter-1', + fieldMetadataId: 'field-1', + operand: ViewFilterOperand.Contains, + value: 'test', + displayValue: 'test', + viewFilterGroupId: 'group-1', + positionInViewFilterGroup: 0, + }; + + it('should return empty array when current filters array is empty', () => { + const currentViewFilters: ViewFilter[] = []; + const newViewFilters: ViewFilter[] = [baseFilter]; + + const result = getViewFiltersToDelete(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should return all current filters when new filters array is empty', () => { + const existingFilter = { ...baseFilter }; + const currentViewFilters: ViewFilter[] = [existingFilter]; + const newViewFilters: ViewFilter[] = []; + + const result = getViewFiltersToDelete(currentViewFilters, newViewFilters); + + expect(result).toEqual([existingFilter]); + }); + + it('should return filters that exist in current but not in new filters', () => { + const filterToDelete = { ...baseFilter }; + const filterToKeep = { + ...baseFilter, + id: 'filter-2', + fieldMetadataId: 'field-2', + }; + + const currentViewFilters: ViewFilter[] = [filterToDelete, filterToKeep]; + const newViewFilters: ViewFilter[] = [filterToKeep]; + + const result = getViewFiltersToDelete(currentViewFilters, newViewFilters); + + expect(result).toEqual([filterToDelete]); + }); + + it('should handle empty arrays for both inputs', () => { + const currentViewFilters: ViewFilter[] = []; + const newViewFilters: ViewFilter[] = []; + + const result = getViewFiltersToDelete(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should identify filters to delete based on fieldMetadataId and viewFilterGroupId', () => { + const filterInGroup1 = { ...baseFilter }; + const filterInGroup2 = { + ...baseFilter, + viewFilterGroupId: 'group-2', + }; + const filterWithDifferentField = { + ...baseFilter, + fieldMetadataId: 'field-2', + }; + + const currentViewFilters: ViewFilter[] = [ + filterInGroup1, + filterInGroup2, + filterWithDifferentField, + ]; + const newViewFilters: ViewFilter[] = [filterInGroup1]; + + const result = getViewFiltersToDelete(currentViewFilters, newViewFilters); + + expect(result).toEqual([filterInGroup2, filterWithDifferentField]); + }); + + it('should not delete filters that match in both fieldMetadataId and viewFilterGroupId', () => { + const existingFilter = { ...baseFilter }; + const matchingFilter = { + ...baseFilter, + value: 'different-value', + displayValue: 'different-value', + }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + const newViewFilters: ViewFilter[] = [matchingFilter]; + + const result = getViewFiltersToDelete(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToUpdate.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToUpdate.test.ts new file mode 100644 index 000000000..275f0d6c0 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewFiltersToUpdate.test.ts @@ -0,0 +1,133 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { getViewFiltersToUpdate } from '../getViewFiltersToUpdate'; + +describe('getViewFiltersToUpdate', () => { + const baseFilter: ViewFilter = { + __typename: 'ViewFilter', + id: 'filter-1', + fieldMetadataId: 'field-1', + operand: ViewFilterOperand.Contains, + value: 'test', + displayValue: 'test', + viewFilterGroupId: 'group-1', + positionInViewFilterGroup: 0, + }; + + it('should return empty array when current filters array is empty', () => { + const currentViewFilters: ViewFilter[] = []; + const newViewFilters: ViewFilter[] = [baseFilter]; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should return empty array when new filters array is empty', () => { + const currentViewFilters: ViewFilter[] = [baseFilter]; + const newViewFilters: ViewFilter[] = []; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should return filters that exist in both arrays but have different values', () => { + const existingFilter = { ...baseFilter }; + const updatedFilter = { + ...baseFilter, + value: 'updated-value', + displayValue: 'updated-value', + }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + const newViewFilters: ViewFilter[] = [updatedFilter]; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([updatedFilter]); + }); + + it('should not return filters that exist in both arrays with same values', () => { + const existingFilter = { ...baseFilter }; + const sameFilter = { ...baseFilter }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + const newViewFilters: ViewFilter[] = [sameFilter]; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should handle empty arrays for both inputs', () => { + const currentViewFilters: ViewFilter[] = []; + const newViewFilters: ViewFilter[] = []; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should not update filters with same fieldMetadataId but different viewFilterGroupId', () => { + const existingFilter = { ...baseFilter }; + const filterInDifferentGroup = { + ...baseFilter, + viewFilterGroupId: 'group-2', + value: 'updated-value', + }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + const newViewFilters: ViewFilter[] = [filterInDifferentGroup]; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should not update filters with same viewFilterGroupId but different fieldMetadataId', () => { + const existingFilter = { ...baseFilter }; + const filterWithDifferentField = { + ...baseFilter, + fieldMetadataId: 'field-2', + value: 'updated-value', + }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + const newViewFilters: ViewFilter[] = [filterWithDifferentField]; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([]); + }); + + it('should update filter when operand changes', () => { + const existingFilter = { ...baseFilter }; + const filterWithNewOperand = { + ...baseFilter, + operand: ViewFilterOperand.DoesNotContain, + }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + const newViewFilters: ViewFilter[] = [filterWithNewOperand]; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([filterWithNewOperand]); + }); + + it('should update filter when position changes', () => { + const existingFilter = { ...baseFilter }; + const filterWithNewPosition = { + ...baseFilter, + positionInViewFilterGroup: 1, + }; + + const currentViewFilters: ViewFilter[] = [existingFilter]; + const newViewFilters: ViewFilter[] = [filterWithNewPosition]; + + const result = getViewFiltersToUpdate(currentViewFilters, newViewFilters); + + expect(result).toEqual([filterWithNewPosition]); + }); +}); diff --git a/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts b/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts new file mode 100644 index 000000000..5087bf44a --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts @@ -0,0 +1,19 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; + +export const areViewFiltersEqual = ( + viewFilterA: ViewFilter, + viewFilterB: ViewFilter, +) => { + const propertiesToCompare: (keyof ViewFilter)[] = [ + 'fieldMetadataId', + 'viewFilterGroupId', + 'positionInViewFilterGroup', + 'value', + 'displayValue', + 'operand', + ]; + + return propertiesToCompare.every( + (property) => viewFilterA[property] === viewFilterB[property], + ); +}; diff --git a/packages/twenty-front/src/modules/views/utils/getViewFiltersToCreate.ts b/packages/twenty-front/src/modules/views/utils/getViewFiltersToCreate.ts new file mode 100644 index 000000000..a222a1daf --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/getViewFiltersToCreate.ts @@ -0,0 +1,21 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; +import { isDefined } from 'twenty-ui'; + +export const getViewFiltersToCreate = ( + currentViewFilters: ViewFilter[], + newViewFilters: ViewFilter[], +) => { + return newViewFilters.filter((newViewFilter) => { + const correspondingViewFilter = currentViewFilters.find( + (currentViewFilter) => + currentViewFilter.fieldMetadataId === newViewFilter.fieldMetadataId && + currentViewFilter.viewFilterGroupId === newViewFilter.viewFilterGroupId, + ); + + const shouldCreateBecauseViewFilterIsNew = !isDefined( + correspondingViewFilter, + ); + + return shouldCreateBecauseViewFilterIsNew; + }); +}; diff --git a/packages/twenty-front/src/modules/views/utils/getViewFiltersToDelete.ts b/packages/twenty-front/src/modules/views/utils/getViewFiltersToDelete.ts new file mode 100644 index 000000000..d6ee34cff --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/getViewFiltersToDelete.ts @@ -0,0 +1,16 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; + +export const getViewFiltersToDelete = ( + currentViewFilters: ViewFilter[], + newViewFilters: ViewFilter[], +) => { + return currentViewFilters.filter( + (currentViewFilter) => + !newViewFilters.some( + (newViewFilter) => + newViewFilter.fieldMetadataId === currentViewFilter.fieldMetadataId && + newViewFilter.viewFilterGroupId === + currentViewFilter.viewFilterGroupId, + ), + ); +}; diff --git a/packages/twenty-front/src/modules/views/utils/getViewFiltersToUpdate.ts b/packages/twenty-front/src/modules/views/utils/getViewFiltersToUpdate.ts new file mode 100644 index 000000000..d30ee10b2 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/getViewFiltersToUpdate.ts @@ -0,0 +1,27 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; +import { areViewFiltersEqual } from '@/views/utils/areViewFiltersEqual'; +import { isDefined } from 'twenty-ui'; + +export const getViewFiltersToUpdate = ( + currentViewFilters: ViewFilter[], + newViewFilters: ViewFilter[], +) => { + return newViewFilters.filter((newViewFilter) => { + const correspondingViewFilter = currentViewFilters.find( + (currentViewFilter) => + currentViewFilter.fieldMetadataId === newViewFilter.fieldMetadataId && + currentViewFilter.viewFilterGroupId === newViewFilter.viewFilterGroupId, + ); + + if (!isDefined(correspondingViewFilter)) { + return false; + } + + const shouldUpdateBecauseViewFilterIsDifferent = !areViewFiltersEqual( + newViewFilter, + correspondingViewFilter, + ); + + return shouldUpdateBecauseViewFilterIsDifferent; + }); +}; diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts b/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts new file mode 100644 index 000000000..64847493d --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/mapRecordFilterToViewFilter.ts @@ -0,0 +1,11 @@ +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { ViewFilter } from '@/views/types/ViewFilter'; + +export const mapRecordFilterToViewFilter = ( + recordFilter: RecordFilter, +): ViewFilter => { + return { + __typename: 'ViewFilter', + ...recordFilter, + } satisfies ViewFilter; +};