Added SSE effect for view relations objects (#18386)

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 <felix.malfait@gmail.com>
This commit is contained in:
Lucas Bordeau
2026-03-05 12:20:33 +01:00
committed by GitHub
parent 57499342f1
commit 1b9d188e4a
22 changed files with 382 additions and 158 deletions
@@ -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();
@@ -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();
@@ -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<ViewFilter>,
) => {
const objectMetadataItemId =
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
if (!isDefined(objectMetadataItemId)) {
return;
}
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewSort,
onMetadataOperationBrowserEvent: (
detail: MetadataOperationBrowserEventDetail<ViewSort>,
) => {
const objectMetadataItemId =
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
if (!isDefined(objectMetadataItemId)) {
return;
}
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewFilterGroup,
onMetadataOperationBrowserEvent: (
detail: MetadataOperationBrowserEventDetail<ViewFilterGroup>,
) => {
const objectMetadataItemId =
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
if (!isDefined(objectMetadataItemId)) {
return;
}
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewField,
onMetadataOperationBrowserEvent: (
detail: MetadataOperationBrowserEventDetail<ViewField>,
) => {
const objectMetadataItemId =
getObjectMetadataItemIdFromViewRelationEventDetail(detail);
if (!isDefined(objectMetadataItemId)) {
return;
}
debouncedRefreshCoreViewsByObjectMetadataId(objectMetadataItemId);
},
});
return null;
};
@@ -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': {
@@ -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<T>,
) => {
const viewId = getViewIdFromViewSSERelationEventDetail(eventDetail);
if (!isDefined(viewId)) {
return null;
}
const { view } = getViewById(viewId);
if (!isDefined(view)) {
return null;
}
return view.objectMetadataId;
};
return {
getObjectMetadataItemIdFromViewRelationEventDetail,
};
};
@@ -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<T>,
) => {
switch (eventDetail.operation.type) {
case 'create': {
return eventDetail.operation.createdRecord?.viewId as Nullable<string>;
}
case 'update': {
return eventDetail.operation.updatedRecord?.viewId as Nullable<string>;
}
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,
};
};
@@ -93,18 +93,15 @@ export const AdvancedFilterFieldSelectMenu = ({
const [, setObjectFilterDropdownSubMenuFieldType] = useAtomComponentState(
objectFilterDropdownSubMenuFieldTypeComponentState,
advancedFilterFieldSelectDropdownId,
);
const [, setObjectFilterDropdownIsSelectingCompositeField] =
useAtomComponentState(
objectFilterDropdownIsSelectingCompositeFieldComponentState,
advancedFilterFieldSelectDropdownId,
);
const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
fieldMetadataItemIdUsedInDropdownComponentState,
advancedFilterFieldSelectDropdownId,
);
const handleFieldMetadataItemSelect = (
@@ -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 = () => {
>
<DropdownContent widthInPixels={ADVANCED_FILTER_DROPDOWN_CONTENT_WIDTH}>
<AdvancedFilterContext.Provider value={{ objectMetadataItem }}>
<AdvancedFilterSSEEffect />
<StyledContainer>
{childRecordFiltersAndRecordFilterGroups.map(
(recordFilterGroupChild, recordFilterGroupChildIndex) =>
@@ -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;
};
@@ -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();
@@ -41,7 +41,6 @@ export const RecordTableContextProvider = ({
const visibleRecordFields = useAtomComponentSelectorValue(
visibleRecordFieldsComponentSelector,
recordTableId,
);
const recordIndexOpenRecordIn = useAtomStateValue(
@@ -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,
});
@@ -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,
});
@@ -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) => {
<PageLayoutSSEEffect />
<PageLayoutTabSSEEffect />
<PageLayoutWidgetSSEEffect />
<ViewFieldSSEEffect />
<ViewRelationsSSEEffect />
<ViewSSEEffect />
{children}
</>
@@ -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([]);
});
});
@@ -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(
@@ -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,
};
};
@@ -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,
};
};
@@ -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 {
@@ -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 {
@@ -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,
};
};
@@ -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;