From 1b9d188e4a5d0e74d01feb6f7debcf2ca409b59d Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 5 Mar 2026 12:20:33 +0100 Subject: [PATCH] Added SSE effect for view relations objects (#18386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds what is necessary for having SSE working for view relations : fields, filters, filter groups and sorts. This should allow to have AI working well while creating views with detailed filtering and sorting. ## Demo https://github.com/user-attachments/assets/026c7fb5-8e1a-4498-b7f4-d16993e5a7c4 ## Fixes Also fixed in this PR while working on the filter area : - Advanced filter does not update - Advanced filter sub field selection is broken (due to Jotai migration) - No view fields when creating a new view - Error on advanced filter deletion (cascade delete wasn't taken into account on the frontend) - Bug advanced filter creation --------- Co-authored-by: Félix Malfait --- .../PageLayoutTabSSEEffect.tsx | 2 +- .../PageLayoutWidgetSSEEffect.tsx | 2 +- .../ViewRelationsSSEEffect.tsx | 130 ++++++++++++++++++ .../effect-components/ViewSSEEffect.tsx | 14 +- ...tadataItemIdFromViewRelationEventDetail.ts | 39 ++++++ ...GetViewIdFromViewSSERelationEventDetail.ts | 64 +++++++++ .../AdvancedFilterFieldSelectMenu.tsx | 3 - .../AdvancedFilterRootRecordFilterGroup.tsx | 2 + .../components/AdvancedFilterSSEEffect.tsx | 17 +++ ...eSetAdvancedFilterDropdownAllRowsStates.ts | 2 +- .../components/RecordTableContextProvider.tsx | 1 - .../hooks/useFieldsWidgetEditorGroupsData.ts | 6 +- .../fields/hooks/useFieldsWidgetGroups.ts | 4 +- .../sse-db-event/components/SSEProvider.tsx | 4 +- ...ViewFiltersToCurrentRecordFilters.test.tsx | 108 --------------- .../useApplyCoreViewsForObjectMetadataId.ts | 24 ++++ ...eApplyViewFiltersToCurrentRecordFilters.ts | 24 ---- .../src/modules/views/hooks/useGetViewById.ts | 24 +++- ...aveRecordFilterGroupsToViewFilterGroups.ts | 34 ++++- .../useSaveRecordFiltersToViewFilters.ts | 23 +++- .../src/modules/views/hooks/useViewById.ts | 12 ++ .../src/modules/views/types/ViewField.ts | 1 + 22 files changed, 382 insertions(+), 158 deletions(-) create mode 100644 packages/twenty-front/src/modules/metadata-store/effect-components/ViewRelationsSSEEffect.tsx create mode 100644 packages/twenty-front/src/modules/metadata-store/hooks/useGetObjectMetadataItemIdFromViewRelationEventDetail.ts create mode 100644 packages/twenty-front/src/modules/metadata-store/hooks/useGetViewIdFromViewSSERelationEventDetail.ts create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSSEEffect.tsx delete mode 100644 packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewFiltersToCurrentRecordFilters.test.tsx delete mode 100644 packages/twenty-front/src/modules/views/hooks/useApplyViewFiltersToCurrentRecordFilters.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/useViewById.ts diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutTabSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutTabSSEEffect.tsx index e07798f2a1..55093348cd 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutTabSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutTabSSEEffect.tsx @@ -4,7 +4,7 @@ import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEvent import { AllMetadataName } from '~/generated-metadata/graphql'; export const PageLayoutTabSSEEffect = () => { - const queryId = 'page-layout-tab-metadata-sse-effect'; + const queryId = 'page-layout-sse-effect'; const { refreshPageLayouts } = useRefreshPageLayouts(); diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutWidgetSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutWidgetSSEEffect.tsx index 88d7a7f07b..2b67f6f7e6 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutWidgetSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/PageLayoutWidgetSSEEffect.tsx @@ -4,7 +4,7 @@ import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEvent import { AllMetadataName } from '~/generated-metadata/graphql'; export const PageLayoutWidgetSSEEffect = () => { - const queryId = 'page-layout-widget-metadata-sse-effect'; + const queryId = 'page-layout-widget-sse-effect'; const { refreshPageLayouts } = useRefreshPageLayouts(); diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewRelationsSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewRelationsSSEEffect.tsx new file mode 100644 index 0000000000..fbed2437b5 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewRelationsSSEEffect.tsx @@ -0,0 +1,130 @@ +import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent'; +import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail'; +import { useGetObjectMetadataItemIdFromViewRelationEventDetail } from '@/metadata-store/hooks/useGetObjectMetadataItemIdFromViewRelationEventDetail'; +import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; +import { type ViewField } from '@/views/types/ViewField'; +import { type ViewFilter } from '@/views/types/ViewFilter'; +import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { type ViewSort } from '@/views/types/ViewSort'; +import { isDefined } from 'twenty-shared/utils'; +import { useDebouncedCallback } from 'use-debounce'; +import { AllMetadataName } from '~/generated-metadata/graphql'; + +export const ViewRelationsSSEEffect = () => { + const { refreshCoreViewsByObjectMetadataId } = + useRefreshCoreViewsByObjectMetadataId(); + + const { getObjectMetadataItemIdFromViewRelationEventDetail } = + useGetObjectMetadataItemIdFromViewRelationEventDetail(); + + useListenToEventsForQuery({ + queryId: 'view-filters-sse-effect', + operationSignature: { + metadataName: AllMetadataName.viewFilter, + variables: {}, + }, + }); + + useListenToEventsForQuery({ + queryId: 'view-sorts-sse-effect', + operationSignature: { + metadataName: AllMetadataName.viewSort, + variables: {}, + }, + }); + + useListenToEventsForQuery({ + queryId: 'view-fields-sse-effect', + operationSignature: { + metadataName: AllMetadataName.viewField, + variables: {}, + }, + }); + + useListenToEventsForQuery({ + queryId: 'view-filter-groups-sse-effect', + operationSignature: { + metadataName: AllMetadataName.viewFilterGroup, + variables: {}, + }, + }); + + // TODO: replace this with detailed SSE optimistic effects + const debouncedRefreshCoreViewsByObjectMetadataId = useDebouncedCallback( + (objectMetadataId: string) => { + refreshCoreViewsByObjectMetadataId(objectMetadataId); + }, + 500, + { + leading: false, + trailing: true, + }, + ); + + useListenToMetadataOperationBrowserEvent({ + metadataName: AllMetadataName.viewFilter, + onMetadataOperationBrowserEvent: ( + detail: MetadataOperationBrowserEventDetail, + ) => { + const objectMetadataItemId = + getObjectMetadataItemIdFromViewRelationEventDetail(detail); + + if (!isDefined(objectMetadataItemId)) { + return; + } + + debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId); + }, + }); + + useListenToMetadataOperationBrowserEvent({ + metadataName: AllMetadataName.viewSort, + onMetadataOperationBrowserEvent: ( + detail: MetadataOperationBrowserEventDetail, + ) => { + const objectMetadataItemId = + getObjectMetadataItemIdFromViewRelationEventDetail(detail); + + if (!isDefined(objectMetadataItemId)) { + return; + } + + debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId); + }, + }); + + useListenToMetadataOperationBrowserEvent({ + metadataName: AllMetadataName.viewFilterGroup, + onMetadataOperationBrowserEvent: ( + detail: MetadataOperationBrowserEventDetail, + ) => { + const objectMetadataItemId = + getObjectMetadataItemIdFromViewRelationEventDetail(detail); + + if (!isDefined(objectMetadataItemId)) { + return; + } + + debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId); + }, + }); + + useListenToMetadataOperationBrowserEvent({ + metadataName: AllMetadataName.viewField, + onMetadataOperationBrowserEvent: ( + detail: MetadataOperationBrowserEventDetail, + ) => { + const objectMetadataItemId = + getObjectMetadataItemIdFromViewRelationEventDetail(detail); + + if (!isDefined(objectMetadataItemId)) { + return; + } + + debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId); + }, + }); + + return null; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSSEEffect.tsx index e9387cb767..b4e7024ffb 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/ViewSSEEffect.tsx @@ -1,15 +1,20 @@ import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent'; import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; import { coreViewsState } from '@/views/states/coreViewState'; import { type CoreViewWithoutRelations } from '@/views/types/CoreViewWithoutRelations'; import { useStore } from 'jotai'; import { AllMetadataName } from '~/generated-metadata/graphql'; +import { sleep } from '~/utils/sleep'; export const ViewSSEEffect = () => { - const queryId = 'view-metadata-sse-effect'; + const queryId = 'views-sse-effect'; const store = useStore(); + const { refreshCoreViewsByObjectMetadataId } = + useRefreshCoreViewsByObjectMetadataId(); + useListenToEventsForQuery({ queryId, operationSignature: { @@ -20,7 +25,7 @@ export const ViewSSEEffect = () => { useListenToMetadataOperationBrowserEvent({ metadataName: AllMetadataName.view, - onMetadataOperationBrowserEvent: (eventDetail) => { + onMetadataOperationBrowserEvent: async (eventDetail) => { switch (eventDetail.operation.type) { case 'create': { const createdView = eventDetail.operation @@ -38,6 +43,11 @@ export const ViewSSEEffect = () => { viewGroups: [], }, ]); + + await sleep(50); + + refreshCoreViewsByObjectMetadataId(createdView.objectMetadataId); + break; } case 'update': { diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useGetObjectMetadataItemIdFromViewRelationEventDetail.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useGetObjectMetadataItemIdFromViewRelationEventDetail.ts new file mode 100644 index 0000000000..2c3192faca --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useGetObjectMetadataItemIdFromViewRelationEventDetail.ts @@ -0,0 +1,39 @@ +import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail'; +import { useGetViewIdFromViewSSERelationEventDetail } from '@/metadata-store/hooks/useGetViewIdFromViewSSERelationEventDetail'; +import { useGetViewById } from '@/views/hooks/useGetViewById'; +import { type ViewField } from '@/views/types/ViewField'; +import { type ViewFilter } from '@/views/types/ViewFilter'; +import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { type ViewSort } from '@/views/types/ViewSort'; +import { isDefined } from 'twenty-shared/utils'; + +export const useGetObjectMetadataItemIdFromViewRelationEventDetail = () => { + const { getViewIdFromViewSSERelationEventDetail } = + useGetViewIdFromViewSSERelationEventDetail(); + + const { getViewById } = useGetViewById(); + + const getObjectMetadataItemIdFromViewRelationEventDetail = < + T extends ViewFilter | ViewFilterGroup | ViewField | ViewSort, + >( + eventDetail: MetadataOperationBrowserEventDetail, + ) => { + const viewId = getViewIdFromViewSSERelationEventDetail(eventDetail); + + if (!isDefined(viewId)) { + return null; + } + + const { view } = getViewById(viewId); + + if (!isDefined(view)) { + return null; + } + + return view.objectMetadataId; + }; + + return { + getObjectMetadataItemIdFromViewRelationEventDetail, + }; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useGetViewIdFromViewSSERelationEventDetail.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useGetViewIdFromViewSSERelationEventDetail.ts new file mode 100644 index 0000000000..45350753ec --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useGetViewIdFromViewSSERelationEventDetail.ts @@ -0,0 +1,64 @@ +import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { coreViewsState } from '@/views/states/coreViewState'; +import { type ViewField } from '@/views/types/ViewField'; +import { type ViewFilter } from '@/views/types/ViewFilter'; +import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { type ViewSort } from '@/views/types/ViewSort'; +import { type Nullable } from 'twenty-shared/types'; +import { AllMetadataName } from '~/generated-metadata/graphql'; + +export const useGetViewIdFromViewSSERelationEventDetail = () => { + const coreViews = useAtomStateValue(coreViewsState); + + const getViewIdFromViewSSERelationEventDetail = < + T extends ViewFilter | ViewFilterGroup | ViewField | ViewSort, + >( + eventDetail: MetadataOperationBrowserEventDetail, + ) => { + switch (eventDetail.operation.type) { + case 'create': { + return eventDetail.operation.createdRecord?.viewId as Nullable; + } + case 'update': { + return eventDetail.operation.updatedRecord?.viewId as Nullable; + } + case 'delete': { + const deletedRecordId = eventDetail.operation.deletedRecordId; + + switch (eventDetail.metadataName) { + case AllMetadataName.viewFilter: + return coreViews.find((view) => + view.viewFilters.some( + (viewFilter) => viewFilter.id === deletedRecordId, + ), + )?.id; + case AllMetadataName.viewField: + return coreViews.find((view) => + view.viewFields.some( + (viewField) => viewField.id === deletedRecordId, + ), + )?.id; + case AllMetadataName.viewFilterGroup: + return coreViews.find((view) => + view.viewFilterGroups?.some( + (viewFilterGroup) => viewFilterGroup.id === deletedRecordId, + ), + )?.id; + case AllMetadataName.viewSort: + return coreViews.find((view) => + view.viewSorts.some( + (viewSort) => viewSort.id === deletedRecordId, + ), + )?.id; + default: + return null; + } + } + } + }; + + return { + getViewIdFromViewSSERelationEventDetail, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx index cc9d8431a0..924203d302 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx @@ -93,18 +93,15 @@ export const AdvancedFilterFieldSelectMenu = ({ const [, setObjectFilterDropdownSubMenuFieldType] = useAtomComponentState( objectFilterDropdownSubMenuFieldTypeComponentState, - advancedFilterFieldSelectDropdownId, ); const [, setObjectFilterDropdownIsSelectingCompositeField] = useAtomComponentState( objectFilterDropdownIsSelectingCompositeFieldComponentState, - advancedFilterFieldSelectDropdownId, ); const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState( fieldMetadataItemIdUsedInDropdownComponentState, - advancedFilterFieldSelectDropdownId, ); const handleFieldMetadataItemSelect = ( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootRecordFilterGroup.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootRecordFilterGroup.tsx index 0c4cf193a9..ce544c4203 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootRecordFilterGroup.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootRecordFilterGroup.tsx @@ -2,6 +2,7 @@ import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/ import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect'; import { AdvancedFilterRecordFilterGroupRow } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupRow'; import { AdvancedFilterRecordFilterRow } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterRow'; +import { AdvancedFilterSSEEffect } from '@/object-record/advanced-filter/components/AdvancedFilterSSEEffect'; import { ADVANCED_FILTER_DROPDOWN_CONTENT_WIDTH } from '@/object-record/advanced-filter/constants/AdvancedFilterDropdownContentWidth'; import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; @@ -46,6 +47,7 @@ export const AdvancedFilterRootRecordFilterGroup = () => { > + {childRecordFiltersAndRecordFilterGroups.map( (recordFilterGroupChild, recordFilterGroupChildIndex) => diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSSEEffect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSSEEffect.tsx new file mode 100644 index 0000000000..aeb3e959f1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSSEEffect.tsx @@ -0,0 +1,17 @@ +import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent'; +import { useSetAdvancedFilterDropdownStates } from '@/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates'; +import { AllMetadataName } from '~/generated-metadata/graphql'; + +export const AdvancedFilterSSEEffect = () => { + const { setAdvancedFilterDropdownStates } = + useSetAdvancedFilterDropdownStates(); + + useListenToMetadataOperationBrowserEvent({ + metadataName: AllMetadataName.viewFilterGroup, + onMetadataOperationBrowserEvent: () => { + setAdvancedFilterDropdownStates(); + }, + }); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates.ts index 71f7e61cce..ec53bad79c 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates.ts @@ -8,8 +8,8 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; -import { useCallback } from 'react'; import { useStore } from 'jotai'; +import { useCallback } from 'react'; export const useSetAdvancedFilterDropdownStates = () => { const store = useStore(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx index 46f00d3e60..8bcc7d0732 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx @@ -41,7 +41,6 @@ export const RecordTableContextProvider = ({ const visibleRecordFields = useAtomComponentSelectorValue( visibleRecordFieldsComponentSelector, - recordTableId, ); const recordIndexOpenRecordIn = useAtomStateValue( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts index 479a26938e..6ac5a8d0ff 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts @@ -4,7 +4,7 @@ import { type FieldsWidgetGroup, type FieldsWidgetGroupField, } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; -import { useGetViewById } from '@/views/hooks/useGetViewById'; +import { useViewById } from '@/views/hooks/useViewById'; import { useMemo } from 'react'; import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; @@ -23,8 +23,8 @@ type UseFieldsWidgetEditorGroupsDataResult = { export const useFieldsWidgetEditorGroupsData = ({ viewId, objectNameSingular, -}: UseFieldsWidgetEditorGroupsDataParams): UseFieldsWidgetEditorGroupsDataResult => { - const { view } = useGetViewById(viewId); +}: UseFieldsWidgetEditorGroupsDataParams) => { + const { view } = useViewById(viewId); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts index 3ffdd9847c..a09406773d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts @@ -5,7 +5,7 @@ import { type FieldsWidgetGroup, type FieldsWidgetGroupField, } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; -import { useGetViewById } from '@/views/hooks/useGetViewById'; +import { useViewById } from '@/views/hooks/useViewById'; import { useLingui } from '@lingui/react/macro'; import { useMemo } from 'react'; import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; @@ -20,7 +20,7 @@ export const useFieldsWidgetGroups = ({ objectNameSingular, }: UseFieldsWidgetGroupsParams) => { const { t } = useLingui(); - const { view } = useGetViewById(viewId); + const { view } = useViewById(viewId); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); diff --git a/packages/twenty-front/src/modules/sse-db-event/components/SSEProvider.tsx b/packages/twenty-front/src/modules/sse-db-event/components/SSEProvider.tsx index 74d690a93d..8e841ce3e0 100644 --- a/packages/twenty-front/src/modules/sse-db-event/components/SSEProvider.tsx +++ b/packages/twenty-front/src/modules/sse-db-event/components/SSEProvider.tsx @@ -4,7 +4,7 @@ import { ObjectMetadataItemSSEEffect } from '@/metadata-store/effect-components/ import { PageLayoutSSEEffect } from '@/metadata-store/effect-components/PageLayoutSSEEffect'; import { PageLayoutTabSSEEffect } from '@/metadata-store/effect-components/PageLayoutTabSSEEffect'; import { PageLayoutWidgetSSEEffect } from '@/metadata-store/effect-components/PageLayoutWidgetSSEEffect'; -import { ViewFieldSSEEffect } from '@/metadata-store/effect-components/ViewFieldSSEEffect'; +import { ViewRelationsSSEEffect } from '@/metadata-store/effect-components/ViewRelationsSSEEffect'; import { ViewSSEEffect } from '@/metadata-store/effect-components/ViewSSEEffect'; import { SSEClientEffect } from '@/sse-db-event/components/SSEClientEffect'; import { SSEEventStreamEffect } from '@/sse-db-event/components/SSEEventStreamEffect'; @@ -27,7 +27,7 @@ export const SSEProvider = ({ children }: SSEProviderProps) => { - + {children} diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewFiltersToCurrentRecordFilters.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewFiltersToCurrentRecordFilters.test.tsx deleted file mode 100644 index 196012f2b2..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewFiltersToCurrentRecordFilters.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; - -import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; -import { type ViewFilter } from '@/views/types/ViewFilter'; -import { ViewFilterOperand } from 'twenty-shared/types'; -import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils'; -import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; -import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; -import { useApplyViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyViewFiltersToCurrentRecordFilters'; - -const mockObjectMetadataItemNameSingular = 'company'; - -describe('useApplyViewFiltersToCurrentRecordFilters', () => { - 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[0]; - - const mockViewFilter: ViewFilter = { - __typename: 'ViewFilter', - id: 'filter-1', - fieldMetadataId: mockFieldMetadataItem.id, - operand: ViewFilterOperand.CONTAINS, - value: 'test', - displayValue: mockFieldMetadataItem.label, - viewFilterGroupId: 'group-1', - positionInViewFilterGroup: 0, - }; - - it('should apply view filters to current record filters', () => { - const { result } = renderHook( - () => { - const { applyViewFiltersToCurrentRecordFilters } = - useApplyViewFiltersToCurrentRecordFilters(); - - const currentRecordFilters = useAtomComponentStateValue( - currentRecordFiltersComponentState, - ); - - return { applyViewFiltersToCurrentRecordFilters, currentRecordFilters }; - }, - { - wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ - apolloMocks: [], - componentInstanceId: 'instanceId', - contextStoreCurrentObjectMetadataNameSingular: - mockObjectMetadataItemNameSingular, - }), - }, - ); - - act(() => { - result.current.applyViewFiltersToCurrentRecordFilters([mockViewFilter]); - }); - - expect(result.current.currentRecordFilters).toEqual([ - { - id: mockViewFilter.id, - fieldMetadataId: mockViewFilter.fieldMetadataId, - value: mockViewFilter.value, - displayValue: mockViewFilter.displayValue, - operand: mockViewFilter.operand, - recordFilterGroupId: mockViewFilter.viewFilterGroupId, - positionInRecordFilterGroup: mockViewFilter.positionInViewFilterGroup, - label: mockFieldMetadataItem.label, - type: getFilterTypeFromFieldType(mockFieldMetadataItem.type), - } satisfies RecordFilter, - ]); - }); - - it('should handle empty view filters array', () => { - const { result } = renderHook( - () => { - const { applyViewFiltersToCurrentRecordFilters } = - useApplyViewFiltersToCurrentRecordFilters(); - - const currentRecordFilters = useAtomComponentStateValue( - currentRecordFiltersComponentState, - ); - - return { applyViewFiltersToCurrentRecordFilters, currentRecordFilters }; - }, - { - wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ - apolloMocks: [], - componentInstanceId: 'instanceId', - contextStoreCurrentObjectMetadataNameSingular: - mockObjectMetadataItemNameSingular, - }), - }, - ); - - act(() => { - result.current.applyViewFiltersToCurrentRecordFilters([]); - }); - - expect(result.current.currentRecordFilters).toEqual([]); - }); -}); diff --git a/packages/twenty-front/src/modules/views/hooks/useApplyCoreViewsForObjectMetadataId.ts b/packages/twenty-front/src/modules/views/hooks/useApplyCoreViewsForObjectMetadataId.ts index eb7936d63e..bd3e611d76 100644 --- a/packages/twenty-front/src/modules/views/hooks/useApplyCoreViewsForObjectMetadataId.ts +++ b/packages/twenty-front/src/modules/views/hooks/useApplyCoreViewsForObjectMetadataId.ts @@ -1,5 +1,6 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; @@ -9,6 +10,7 @@ import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/select import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; import { getFilterableFields } from '@/views/utils/getFilterableFields'; import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField'; +import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { useCallback } from 'react'; import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; @@ -106,6 +108,27 @@ export const useApplyCoreViewsForObjectMetadataId = () => { ); } + if ( + !isDeeplyEqual( + coreView.viewFilterGroups, + existingView.viewFilterGroups, + ) + ) { + const view = convertCoreViewToView(coreView); + + jotaiStore.set( + currentRecordFilterGroupsComponentState.atomFamily({ + instanceId: getRecordIndexIdFromObjectNamePluralAndViewId( + objectMetadataItem.namePlural, + view.id, + ), + }), + mapViewFilterGroupsToRecordFilterGroups( + view.viewFilterGroups ?? [], + ), + ); + } + if ( !isDeeplyEqual( coreView.viewSorts.map((viewSort) => @@ -115,6 +138,7 @@ export const useApplyCoreViewsForObjectMetadataId = () => { ) ) { const view = convertCoreViewToView(coreView); + jotaiStore.set( currentRecordSortsComponentState.atomFamily({ instanceId: getRecordIndexIdFromObjectNamePluralAndViewId( diff --git a/packages/twenty-front/src/modules/views/hooks/useApplyViewFiltersToCurrentRecordFilters.ts b/packages/twenty-front/src/modules/views/hooks/useApplyViewFiltersToCurrentRecordFilters.ts deleted file mode 100644 index b57fe366d9..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/useApplyViewFiltersToCurrentRecordFilters.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; -import { type ViewFilter } from '@/views/types/ViewFilter'; -import { useMapViewFiltersToFilters } from './useMapViewFiltersToFilters'; - -export const useApplyViewFiltersToCurrentRecordFilters = () => { - const setCurrentRecordFilters = useSetAtomComponentState( - currentRecordFiltersComponentState, - ); - - const { mapViewFiltersToRecordFilters } = useMapViewFiltersToFilters(); - - const applyViewFiltersToCurrentRecordFilters = ( - viewFilters: ViewFilter[], - ) => { - const recordFiltersToApply = mapViewFiltersToRecordFilters(viewFilters); - - setCurrentRecordFilters(recordFiltersToApply); - }; - - return { - applyViewFiltersToCurrentRecordFilters, - }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/useGetViewById.ts b/packages/twenty-front/src/modules/views/hooks/useGetViewById.ts index 3696ec979a..ed43ee1049 100644 --- a/packages/twenty-front/src/modules/views/hooks/useGetViewById.ts +++ b/packages/twenty-front/src/modules/views/hooks/useGetViewById.ts @@ -1,12 +1,24 @@ -import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; -export const useGetViewById = (viewId: string | null) => { - const view = useAtomFamilySelectorValue(coreViewFromViewIdFamilySelector, { - viewId: viewId ?? '', - }); +export const useGetViewById = () => { + const store = useStore(); + + const getViewById = useCallback( + (viewId: string | null) => { + const view = store.get( + coreViewFromViewIdFamilySelector.selectorFamily({ + viewId: viewId ?? '', + }), + ); + + return { view }; + }, + [store], + ); return { - view: viewId ? view : undefined, + getViewById, }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups.ts index b4870f4af3..ac4a64abc9 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups.ts @@ -1,8 +1,10 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist'; import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { coreViewsState } from '@/views/states/coreViewState'; import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate'; import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete'; import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate'; @@ -26,6 +28,8 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => { const store = useStore(); + const setCoreViews = useSetAtomState(coreViewsState); + const saveRecordFilterGroupsToViewFilterGroups = useCallback(async () => { if (!canPersistChanges || !isDefined(currentView)) { return; @@ -55,6 +59,33 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => { newViewFilterGroups, ); + const viewFiltersToOptimisticallyCascadeDelete = + currentView.viewFilters.filter((viewFilter) => + viewFilterGroupsToDelete.some( + (viewFilterGroupToDelete) => + viewFilterGroupToDelete.id === viewFilter.viewFilterGroupId, + ), + ); + + for (const viewFilterToCascadeDelete of viewFiltersToOptimisticallyCascadeDelete) { + setCoreViews((currentCoreViews) => { + const updatedCoreViews = currentCoreViews.map((coreView) => { + if (coreView.id !== currentView.id) { + return coreView; + } + + return { + ...coreView, + viewFilters: coreView.viewFilters.filter( + (viewFilter) => viewFilter.id !== viewFilterToCascadeDelete.id, + ), + }; + }); + + return updatedCoreViews; + }); + } + const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate( currentViewFilterGroups, newViewFilterGroups, @@ -71,13 +102,14 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => { await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate); await performViewFilterGroupAPIDelete(viewFilterGroupIdsToDelete); }, [ - store, canPersistChanges, currentView, + store, currentRecordFilterGroupsCallbackState, performViewFilterGroupAPICreate, performViewFilterGroupAPIUpdate, performViewFilterGroupAPIDelete, + setCoreViews, ]); return { diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts index 640af055cb..3f70424b50 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts @@ -1,8 +1,11 @@ +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist'; import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges'; -import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { coreViewsState } from '@/views/states/coreViewState'; +import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate'; import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete'; import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate'; @@ -19,7 +22,9 @@ export const useSaveRecordFiltersToViewFilters = () => { performViewFilterAPIDelete, } = usePerformViewFilterAPIPersist(); - const { currentView } = useGetCurrentViewOnly(); + const contextStoreCurrentViewId = useAtomComponentStateValue( + contextStoreCurrentViewIdComponentState, + ); const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState( currentRecordFiltersComponentState, @@ -28,6 +33,18 @@ export const useSaveRecordFiltersToViewFilters = () => { const store = useStore(); const saveRecordFiltersToViewFilters = useCallback(async () => { + const views = store.get(coreViewsState.atom); + + const currentCoreView = views.find( + (view) => view.id === contextStoreCurrentViewId, + ); + + if (!isDefined(currentCoreView)) { + return; + } + + const currentView = convertCoreViewToView(currentCoreView); + if (!canPersistChanges || !isDefined(currentView)) { return; } @@ -110,11 +127,11 @@ export const useSaveRecordFiltersToViewFilters = () => { }, [ store, canPersistChanges, - currentView, currentRecordFiltersCallbackState, performViewFilterAPICreate, performViewFilterAPIUpdate, performViewFilterAPIDelete, + contextStoreCurrentViewId, ]); return { diff --git a/packages/twenty-front/src/modules/views/hooks/useViewById.ts b/packages/twenty-front/src/modules/views/hooks/useViewById.ts new file mode 100644 index 0000000000..25cb8feca7 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useViewById.ts @@ -0,0 +1,12 @@ +import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; +import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector'; + +export const useViewById = (viewId: string | null) => { + const view = useAtomFamilySelectorValue(coreViewFromViewIdFamilySelector, { + viewId: viewId ?? '', + }); + + return { + view: viewId ? view : undefined, + }; +}; diff --git a/packages/twenty-front/src/modules/views/types/ViewField.ts b/packages/twenty-front/src/modules/views/types/ViewField.ts index b3d8a8766a..c5abb4ae97 100644 --- a/packages/twenty-front/src/modules/views/types/ViewField.ts +++ b/packages/twenty-front/src/modules/views/types/ViewField.ts @@ -6,6 +6,7 @@ import { type ColumnDefinition } from '@/object-record/record-table/types/Column export type ViewField = { __typename: 'ViewField'; id: string; + viewId?: string; fieldMetadataId: string; position: number; isVisible: boolean;