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:
+1
-1
@@ -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();
|
||||
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
|
||||
+130
@@ -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;
|
||||
};
|
||||
+12
-2
@@ -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': {
|
||||
|
||||
+39
@@ -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,
|
||||
};
|
||||
};
|
||||
+64
@@ -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,
|
||||
};
|
||||
};
|
||||
-3
@@ -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
@@ -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) =>
|
||||
|
||||
+17
@@ -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;
|
||||
};
|
||||
+1
-1
@@ -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();
|
||||
|
||||
-1
@@ -41,7 +41,6 @@ export const RecordTableContextProvider = ({
|
||||
|
||||
const visibleRecordFields = useAtomComponentSelectorValue(
|
||||
visibleRecordFieldsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const recordIndexOpenRecordIn = useAtomStateValue(
|
||||
|
||||
+3
-3
@@ -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,
|
||||
});
|
||||
|
||||
+2
-2
@@ -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}
|
||||
</>
|
||||
|
||||
-108
@@ -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(
|
||||
|
||||
-24
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+33
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user