diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/utils/buildHeadlessCommandContextApi.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/utils/buildHeadlessCommandContextApi.ts index 47d45ef8c3..25c50f4a78 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/utils/buildHeadlessCommandContextApi.ts +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/utils/buildHeadlessCommandContextApi.ts @@ -9,6 +9,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; @@ -89,8 +90,8 @@ export const buildHeadlessCommandContextApi = ({ ? (currentWorkspaceMember?.timeZone ?? systemTimeZone) : systemTimeZone; - const flattenedFieldMetadataItems = objectMetadataItems.flatMap( - (objectMetadataItem) => objectMetadataItem.fields, + const fieldMetadataItemByIdMap = store.get( + fieldMetadataItemByIdMapSelector.atom, ); const graphqlFilter = isDefined(objectMetadataItem) @@ -99,7 +100,7 @@ export const buildHeadlessCommandContextApi = ({ contextStoreFilters: filters, contextStoreFilterGroups: filterGroups, objectMetadataItem, - flattenedFieldMetadataItems, + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), filterValueDependencies: { currentWorkspaceMemberId: currentWorkspaceMember?.id, timeZone: userTimezone, diff --git a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts index 1bf0e96242..b6c3ae6d0c 100644 --- a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts +++ b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts @@ -5,7 +5,7 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; -import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; @@ -50,29 +50,22 @@ export const useFindManyRecordsSelectedInContextStore = ({ const { filterValueDependencies } = useFilterValueDependencies(); - const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); - - const allFieldMetadataItems = objectMetadataItems.flatMap( - (objectMetadataItem) => objectMetadataItem.fields, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); - const isSoftDeleteFilterActive = contextStoreFilters.some((filter) => { - const foundFieldMetadataItem = allFieldMetadataItems.find( - (fieldMetadataItem) => fieldMetadataItem.id === filter.fieldMetadataId, - ); - - return ( - foundFieldMetadataItem?.name === 'deletedAt' && - filter.operand === RecordFilterOperand.IS_NOT_EMPTY - ); - }); + const isSoftDeleteFilterActive = contextStoreFilters.some( + (filter) => + fieldMetadataItemByIdMap.get(filter.fieldMetadataId)?.name === + 'deletedAt' && filter.operand === RecordFilterOperand.IS_NOT_EMPTY, + ); const queryFilter = computeContextStoreFilters({ contextStoreTargetedRecordsRule, contextStoreFilters, contextStoreFilterGroups, objectMetadataItem, - flattenedFieldMetadataItems: allFieldMetadataItems, + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), filterValueDependencies, contextStoreAnyFieldFilterValue, }); diff --git a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts index 53850a6ad2..d5b62d8fb1 100644 --- a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts +++ b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts @@ -29,7 +29,8 @@ describe('computeContextStoreFilters', () => { contextStoreFilters: [], contextStoreFilterGroups: [], objectMetadataItem: personObjectMetadataItem, - flattenedFieldMetadataItems: personObjectMetadataItem.fields, + findFieldMetadataItemById: (id) => + personObjectMetadataItem.fields.find((field) => field.id === id), filterValueDependencies: mockFilterValueDependencies, contextStoreAnyFieldFilterValue: '', }); @@ -74,7 +75,8 @@ describe('computeContextStoreFilters', () => { contextStoreFilters, contextStoreFilterGroups: [], objectMetadataItem: personObjectMetadataItem, - flattenedFieldMetadataItems: personObjectMetadataItem.fields, + findFieldMetadataItemById: (id) => + personObjectMetadataItem.fields.find((field) => field.id === id), filterValueDependencies: mockFilterValueDependencies, contextStoreAnyFieldFilterValue: '', }); diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts index 329bdc92f9..099b34df49 100644 --- a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -1,9 +1,7 @@ import { type ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { type RecordFilterValueDependencies, @@ -11,6 +9,7 @@ import { } from 'twenty-shared/types'; import { computeRecordGqlOperationFilter, + type FindFieldMetadataItemById, turnAnyFieldFilterIntoRecordGqlFilter, } from 'twenty-shared/utils'; @@ -19,7 +18,7 @@ type ComputeContextStoreFiltersProps = { contextStoreFilters: RecordFilter[]; contextStoreFilterGroups: RecordFilterGroup[]; objectMetadataItem: EnrichedObjectMetadataItem; - flattenedFieldMetadataItems: FieldMetadataItem[]; + findFieldMetadataItemById: FindFieldMetadataItemById; filterValueDependencies: RecordFilterValueDependencies; contextStoreAnyFieldFilterValue: string; }; @@ -29,7 +28,7 @@ export const computeContextStoreFilters = ({ contextStoreFilters, contextStoreFilterGroups, objectMetadataItem, - flattenedFieldMetadataItems, + findFieldMetadataItemById, filterValueDependencies, contextStoreAnyFieldFilterValue, }: ComputeContextStoreFiltersProps) => { @@ -41,18 +40,12 @@ export const computeContextStoreFilters = ({ fields: objectMetadataItem.fields, }); - const fields = augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem?.fields ?? [], - recordFilters: contextStoreFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }); - if (contextStoreTargetedRecordsRule.mode === 'exclusion') { queryFilter = makeAndFilterVariables([ recordGqlFilterForAnyFieldFilter, computeRecordGqlOperationFilter({ filterValueDependencies, - fields, + findFieldMetadataItemById, recordFilters: contextStoreFilters, recordFilterGroups: contextStoreFilterGroups, }), @@ -81,7 +74,7 @@ export const computeContextStoreFilters = ({ }, computeRecordGqlOperationFilter({ filterValueDependencies, - fields, + findFieldMetadataItemById, recordFilters: contextStoreFilters, recordFilterGroups: contextStoreFilterGroups, }), diff --git a/packages/twenty-front/src/modules/object-metadata/states/fieldMetadataItemByIdMapSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/fieldMetadataItemByIdMapSelector.ts new file mode 100644 index 0000000000..ae09742122 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/states/fieldMetadataItemByIdMapSelector.ts @@ -0,0 +1,16 @@ +import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector'; + +export const fieldMetadataItemByIdMapSelector = createAtomSelector< + Map +>({ + key: 'fieldMetadataItemByIdMapSelector', + get: ({ get }) => + new Map( + get(flattenedFieldMetadataItemsSelector).map((field) => [ + field.id, + field, + ]), + ), +}); diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter.tsx index 416684b574..30af581dbd 100644 --- a/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter.tsx @@ -1,4 +1,4 @@ -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext'; import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; @@ -7,7 +7,6 @@ import { anyFieldFilterValueComponentState } from '@/object-record/record-filter import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; @@ -49,8 +48,8 @@ export const useRecordCalendarQueryDateRangeFilter = ( const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const anyFieldFilterValue = useAtomComponentStateValue( @@ -111,11 +110,7 @@ export const useRecordCalendarQueryDateRangeFilter = ( filterValueDependencies, recordFilters: calendarRecordFilters, recordFilterGroups: currentRecordFilterGroups, - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem.fields, - recordFilters: calendarRecordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), }); const { recordGqlOperationFilter: anyFieldFilter } = diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts index 070b9ecaed..6f951d8f90 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts @@ -22,6 +22,15 @@ const petMockObjectMetadataItem = getMockObjectMetadataItemOrThrow('pet'); const personMockObjectMetadataItem = getMockObjectMetadataItemOrThrow('person'); +const findCompanyFieldById = (id: string) => + companyMockObjectMetadataItem.fields.find((field) => field.id === id); + +const findPersonFieldById = (id: string) => + personMockObjectMetadataItem.fields.find((field) => field.id === id); + +const findPetFieldById = (id: string) => + petMockObjectMetadataItem.fields.find((field) => field.id === id); + const mockFilterValueDependencies: RecordFilterValueDependencies = { currentWorkspaceMemberId: '32219445-f587-4c40-b2b1-6d3205ed96da', timeZone: 'Europe/Paris', @@ -54,7 +63,7 @@ describe('computeViewRecordGqlOperationFilter', () => { filterValueDependencies: mockFilterValueDependencies, recordFilters: [nameFilter], recordFilterGroups: [], - fields: companyMockObjectMetadataItem.fields, + findFieldMetadataItemById: findCompanyFieldById, }); expect(result).toEqual({ @@ -107,7 +116,7 @@ describe('computeViewRecordGqlOperationFilter', () => { filterValueDependencies: mockFilterValueDependencies, recordFilters: [nameFilter, employeesFilter], recordFilterGroups: [], - fields: companyMockObjectMetadataItem.fields, + findFieldMetadataItemById: findCompanyFieldById, }); expect(result).toEqual({ @@ -187,7 +196,7 @@ describe('should work as expected for the different field types', () => { addressFilterIsNotEmpty, ], recordFilterGroups: [], - fields: companyMockObjectMetadataItem.fields, + findFieldMetadataItemById: findCompanyFieldById, }); expect(result).toEqual({ @@ -651,7 +660,7 @@ describe('should work as expected for the different field types', () => { phonesFilterIsNotEmpty, ], recordFilterGroups: [], - fields: personMockObjectMetadataItem.fields, + findFieldMetadataItemById: findPersonFieldById, }); expect(result).toEqual({ @@ -848,7 +857,7 @@ describe('should work as expected for the different field types', () => { emailsFilterIsNotEmpty, ], recordFilterGroups: [], - fields: personMockObjectMetadataItem.fields, + findFieldMetadataItemById: findPersonFieldById, }); expect(result).toEqual({ @@ -1060,7 +1069,7 @@ describe('should work as expected for the different field types', () => { dateFilterIsNotEmpty, ], recordFilterGroups: [], - fields: companyMockObjectMetadataItem.fields, + findFieldMetadataItemById: findCompanyFieldById, }); expect(result).toEqual({ @@ -1162,7 +1171,7 @@ describe('should work as expected for the different field types', () => { employeesFilterIsNotEmpty, ], recordFilterGroups: [], - fields: companyMockObjectMetadataItem.fields, + findFieldMetadataItemById: findCompanyFieldById, }); expect(result).toEqual({ @@ -1264,7 +1273,7 @@ describe('should work as expected for the different field types', () => { ARRFilterIsNot, ], recordFilterGroups: [], - fields: companyMockObjectMetadataItem.fields, + findFieldMetadataItemById: findCompanyFieldById, }); expect(result).toEqual({ @@ -1341,7 +1350,7 @@ describe('should work as expected for the different field types', () => { filterValueDependencies: mockFilterValueDependencies, recordFilters: [ARRFilterIn, ARRFilterNotIn], recordFilterGroups: [], - fields: companyMockObjectMetadataItem.fields, + findFieldMetadataItemById: findCompanyFieldById, }); expect(result).toEqual({ @@ -1401,7 +1410,7 @@ describe('should work as expected for the different field types', () => { filterValueDependencies: mockFilterValueDependencies, recordFilters: [selectFilterIs, selectFilterIsNot], recordFilterGroups: [], - fields: petMockObjectMetadataItem.fields, + findFieldMetadataItemById: findPetFieldById, }); expect(result).toEqual({ @@ -1474,7 +1483,7 @@ describe('should work as expected for the different field types', () => { multiSelectFilterDoesNotContain, ], recordFilterGroups: [], - fields: companyMockObjectMetadataItem.fields, + findFieldMetadataItemById: findCompanyFieldById, }); expect(result).toEqual({ diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/augmentFieldsWithRelationTargets.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/augmentFieldsWithRelationTargets.ts deleted file mode 100644 index 082cb5262b..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/augmentFieldsWithRelationTargets.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { isDefined } from 'twenty-shared/utils'; - -// Relation-traversal filters reference target fields that live on a -// different object than the one being queried, so they aren't in the -// source object's own field list. The shared GraphQL filter dispatcher -// looks the target up by id in `fieldMetadataItems` and drops the filter -// when it can't find it — this helper merges the resolved target fields -// in from the workspace-wide flat list so callers don't silently lose -// relation-traversal filters. -export const augmentFieldsWithRelationTargets = ({ - baseFields, - recordFilters, - allFieldMetadataItems, -}: { - baseFields: FieldMetadataItem[]; - recordFilters: Pick[]; - allFieldMetadataItems: FieldMetadataItem[]; -}): FieldMetadataItem[] => { - const targetFieldIds = new Set( - recordFilters - .map((filter) => filter.relationTargetFieldMetadataId) - .filter(isDefined), - ); - - if (targetFieldIds.size === 0) { - return baseFields; - } - - const baseFieldIds = new Set(baseFields.map((field) => field.id)); - const additionalTargetFields = allFieldMetadataItems.filter( - (field) => targetFieldIds.has(field.id) && !baseFieldIds.has(field.id), - ); - - if (additionalTargetFields.length === 0) { - return baseFields; - } - - return [...baseFields, ...additionalTargetFields]; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx index 9998f62ffb..6bc251e617 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx @@ -6,7 +6,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; @@ -54,8 +54,8 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const computedFilter = computeContextStoreFilters({ @@ -63,7 +63,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = contextStoreFilters, contextStoreFilterGroups, objectMetadataItem, - flattenedFieldMetadataItems, + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), filterValueDependencies, contextStoreAnyFieldFilterValue, }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts index ad10e56432..fde8c1c42c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts @@ -7,7 +7,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; @@ -89,8 +89,8 @@ export const useRecordIndexLazyFetchRecords = ({ const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const findManyRecordsParams = useFindManyRecordIndexTableParams( @@ -107,7 +107,7 @@ export const useRecordIndexLazyFetchRecords = ({ contextStoreFilters, contextStoreFilterGroups, objectMetadataItem, - flattenedFieldMetadataItems, + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), filterValueDependencies, contextStoreAnyFieldFilterValue, }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts index cb5217f83c..d3ddab5759 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts @@ -1,12 +1,11 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; @@ -50,16 +49,12 @@ export const useFindManyRecordIndexTableParams = ( const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const currentFilters = computeRecordGqlOperationFilter({ - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem?.fields ?? [], - recordFilters: currentRecordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), recordFilterGroups: currentRecordFilterGroups, recordFilters: currentRecordFilters, filterValueDependencies, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts index cb8a274bdc..b7c2ec86f2 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts @@ -1,12 +1,11 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { useRelevantRecordsGqlFields } from '@/object-record/record-field/hooks/useRelevantRecordsGqlFields'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; import { computeRecordGroupOptionsFilter } from '@/object-record/record-group/utils/computeRecordGroupOptionsFilter'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; @@ -39,19 +38,15 @@ export const useRecordIndexGroupCommonQueryVariables = () => { const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const requestFilters = computeRecordGqlOperationFilter({ filterValueDependencies, recordFilters: currentRecordFilters, recordFilterGroups: currentRecordFilterGroups, - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem.fields, - recordFilters: currentRecordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), }); const anyFieldFilterValue = useAtomComponentStateValue( diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsAggregatesGroupBy.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsAggregatesGroupBy.ts index 946eab6318..06a02e02bb 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsAggregatesGroupBy.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsAggregatesGroupBy.ts @@ -1,5 +1,5 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; @@ -9,7 +9,6 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record- import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { useAggregateGqlFieldsFromRecordIndexGroupAggregates } from '@/object-record/record-index/hooks/useAggregateGqlFieldsFromRecordIndexGroupAggregates'; import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject'; @@ -49,19 +48,15 @@ export const useRecordIndexGroupsAggregatesGroupBy = ({ const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const requestFilters = computeRecordGqlOperationFilter({ filterValueDependencies, recordFilters: currentRecordFilters, recordFilterGroups: currentRecordFilterGroups, - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem.fields, - recordFilters: currentRecordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), }); const { recordAggregateGqlField } = diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableEmptyHasNewRecordEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableEmptyHasNewRecordEffect.tsx index 78600ed6fa..a7bbca50d0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableEmptyHasNewRecordEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableEmptyHasNewRecordEffect.tsx @@ -1,11 +1,10 @@ import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent'; import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { recordIndexHasRecordsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexHasRecordsComponentSelector'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; @@ -41,8 +40,8 @@ export const RecordTableEmptyHasNewRecordEffect = () => { const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const currentRecordFilters = useAtomComponentStateValue( @@ -64,11 +63,7 @@ export const RecordTableEmptyHasNewRecordEffect = () => { objectNameSingular: objectMetadataItem.nameSingular, variables: { filter: computeRecordGqlOperationFilter({ - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem.fields, - recordFilters: currentRecordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), recordFilters: currentRecordFilters, recordFilterGroups: currentRecordFilterGroups, filterValueDependencies, @@ -82,7 +77,7 @@ export const RecordTableEmptyHasNewRecordEffect = () => { currentRecordFilterGroups, filterValueDependencies, currentRecordSorts, - flattenedFieldMetadataItems, + fieldMetadataItemByIdMap, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx index 4ff95b09ec..4cce755586 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx @@ -1,4 +1,4 @@ -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { transformAggregateRawValueIntoAggregateDisplayValue } from '@/object-record/record-aggregate/utils/transformAggregateRawValueIntoAggregateDisplayValue'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; @@ -7,7 +7,6 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record- import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; import { getRecordAggregateDisplayLabel } from '@/object-record/record-index/utils/getRecordndexAggregateDisplayLabel'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; @@ -47,18 +46,14 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( const dateLocale = useAtomStateValue(dateLocaleState); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const { filterValueDependencies } = useFilterValueDependencies(); const requestFilters = computeRecordGqlOperationFilter({ - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem.fields, - recordFilters: currentRecordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), filterValueDependencies, recordFilterGroups: currentRecordFilterGroups, recordFilters: currentRecordFilters, diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/components/RecordTableVirtualizedSSESubscribeEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/virtualization/components/RecordTableVirtualizedSSESubscribeEffect.tsx index 3015734999..85d2150d17 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/virtualization/components/RecordTableVirtualizedSSESubscribeEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/components/RecordTableVirtualizedSSESubscribeEffect.tsx @@ -1,11 +1,10 @@ import { useMemo } from 'react'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery'; @@ -17,8 +16,8 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => { const { objectMetadataItem } = useRecordIndexContextOrThrow(); const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const currentRecordFilters = useAtomComponentStateValue( @@ -40,11 +39,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => { objectNameSingular: objectMetadataItem.nameSingular, variables: { filter: computeRecordGqlOperationFilter({ - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem.fields, - recordFilters: currentRecordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), recordFilters: currentRecordFilters, recordFilterGroups: currentRecordFilterGroups, filterValueDependencies, @@ -58,7 +53,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => { currentRecordFilterGroups, filterValueDependencies, currentRecordSorts, - flattenedFieldMetadataItems, + fieldMetadataItemByIdMap, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions.ts b/packages/twenty-front/src/modules/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions.ts index f12c57b458..729073ae17 100644 --- a/packages/twenty-front/src/modules/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions.ts @@ -4,7 +4,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { useIncrementalUpdateManyRecords } from '@/object-record/hooks/useIncrementalUpdateManyRecords'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; @@ -45,8 +45,8 @@ export const useUpdateMultipleRecordsActions = ({ const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const graphqlFilter = computeContextStoreFilters({ @@ -54,7 +54,7 @@ export const useUpdateMultipleRecordsActions = ({ contextStoreFilters, contextStoreFilterGroups, objectMetadataItem, - flattenedFieldMetadataItems, + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), filterValueDependencies, contextStoreAnyFieldFilterValue, }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts index e61f45985d..6adf0cb300 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts @@ -1,7 +1,6 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { computeRecordGqlOperationFilter, @@ -41,18 +40,14 @@ export const useGraphWidgetQueryCommon = ({ const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const widgetRecordFilters = configuration.filter?.recordFilters ?? []; const gqlOperationFilter = computeRecordGqlOperationFilter({ - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem.fields, - recordFilters: widgetRecordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), filterValueDependencies, recordFilters: widgetRecordFilters, recordFilterGroups: configuration.filter?.recordFilterGroups ?? [], diff --git a/packages/twenty-front/src/modules/views/hooks/internal/useGetRecordIndexTotalCount.ts b/packages/twenty-front/src/modules/views/hooks/internal/useGetRecordIndexTotalCount.ts index dacaa05299..6d878ee8f0 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/useGetRecordIndexTotalCount.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/useGetRecordIndexTotalCount.ts @@ -1,11 +1,10 @@ import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; @@ -28,8 +27,8 @@ export const useGetRecordIndexTotalCount = () => { const { filterValueDependencies } = useFilterValueDependencies(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const recordGroupsVisibilityFilter = useGetViewGroupsFilters(); @@ -38,11 +37,7 @@ export const useGetRecordIndexTotalCount = () => { filterValueDependencies, recordFilters: [...currentRecordFilters, ...recordGroupsVisibilityFilter], recordFilterGroups: currentRecordFilterGroups, - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem.fields, - recordFilters: [...currentRecordFilters, ...recordGroupsVisibilityFilter], - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), }); const anyFieldFilterValue = useAtomComponentStateValue( diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index c46b590964..2bd7985931 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -213,6 +213,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { viewFilterGroupId: viewFilter.viewFilterGroupId, positionInViewFilterGroup: viewFilter.positionInViewFilterGroup, subFieldName: viewFilter.subFieldName ?? null, + relationTargetFieldMetadataId: + viewFilter.relationTargetFieldMetadataId ?? null, }, }), ); diff --git a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromParentView.ts b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromParentView.ts index 9a2887005a..cbd52d651e 100644 --- a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromParentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromParentView.ts @@ -1,7 +1,7 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; @@ -15,8 +15,8 @@ export const useQueryVariablesFromParentView = ({ }) => { const { objectMetadataItems } = useObjectMetadataItems(); - const flattenedFieldMetadataItems = useAtomStateValue( - flattenedFieldMetadataItemsSelector, + const fieldMetadataItemByIdMap = useAtomStateValue( + fieldMetadataItemByIdMapSelector, ); const contextStoreRecordShowParentView = useAtomComponentStateValue( @@ -33,7 +33,7 @@ export const useQueryVariablesFromParentView = ({ recordSorts: contextStoreRecordShowParentView?.parentViewSorts ?? [], objectMetadataItem, objectMetadataItems, - flattenedFieldMetadataItems, + findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id), filterValueDependencies, }); diff --git a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromFiltersAndSorts.ts index 9b9b4beb6d..f2674c9e2d 100644 --- a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromFiltersAndSorts.ts +++ b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromFiltersAndSorts.ts @@ -1,12 +1,13 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets'; import { type RecordSort } from '@/object-record/record-sort/types/RecordSort'; import { type RecordFilterValueDependencies } from 'twenty-shared/types'; -import { computeRecordGqlOperationFilter } from 'twenty-shared/utils'; +import { + computeRecordGqlOperationFilter, + type FindFieldMetadataItemById, +} from 'twenty-shared/utils'; export const getQueryVariablesFromFiltersAndSorts = ({ recordFilterGroups, @@ -14,7 +15,7 @@ export const getQueryVariablesFromFiltersAndSorts = ({ recordSorts, objectMetadataItem, objectMetadataItems = [], - flattenedFieldMetadataItems, + findFieldMetadataItemById, filterValueDependencies, }: { recordFilterGroups: RecordFilterGroup[]; @@ -22,15 +23,11 @@ export const getQueryVariablesFromFiltersAndSorts = ({ recordSorts: RecordSort[]; objectMetadataItem: EnrichedObjectMetadataItem; objectMetadataItems?: EnrichedObjectMetadataItem[]; - flattenedFieldMetadataItems: FieldMetadataItem[]; + findFieldMetadataItemById: FindFieldMetadataItemById; filterValueDependencies: RecordFilterValueDependencies; }) => { const filter = computeRecordGqlOperationFilter({ - fields: augmentFieldsWithRelationTargets({ - baseFields: objectMetadataItem?.fields ?? [], - recordFilters, - allFieldMetadataItems: flattenedFieldMetadataItems, - }), + findFieldMetadataItemById, filterValueDependencies, recordFilterGroups, recordFilters, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts index fb805e29fa..c233c544dc 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts @@ -256,37 +256,14 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic options: field.options as PartialFieldMetadataItemOption[], })); - // Relation-traversal filters reference target fields on related objects - // that aren't in the source object's field list above. Resolve and add - // them so the shared dispatcher can look them up by id. - const relationTargetFieldsFromFilters = recordFilters - .map((filter) => filter.relationTargetFieldMetadataId) - .filter(isDefined) - .filter((id) => !fields.some((field) => field.id === id)) - .map((id) => { - const field = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: id, - flatEntityMaps: flatFieldMetadataMaps, - }); - - if (!field) return null; - - return { - id: field.id, - name: field.name, - type: field.type, - label: field.label, - options: field.options as PartialFieldMetadataItemOption[], - }; - }) - .filter(isDefined); - - fields.push(...relationTargetFieldsFromFilters); - const filtersFromView = computeRecordGqlOperationFilter({ recordFilters, recordFilterGroups: recordFilterGroups, - fields, + findFieldMetadataItemById: (id) => + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatFieldMetadataMaps, + }), filterValueDependencies: { timeZone: 'UTC', // TODO: see if we use workspace member timezone here }, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts index 4cba8cc57b..5ed89b87e5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts @@ -107,39 +107,12 @@ export class ViewQueryParamsService { : RecordFilterGroupLogicalOperator.AND, })); - const filterFieldMetadataIds = recordFilters.flatMap((filter) => - isDefined(filter.relationTargetFieldMetadataId) - ? [filter.fieldMetadataId, filter.relationTargetFieldMetadataId] - : [filter.fieldMetadataId], - ); - - const fields = filterFieldMetadataIds - .map((fieldMetadataId) => { - const field = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: fieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - }); - - if (!field) return null; - - return { - id: field.id, - name: field.name, - type: field.type, - label: field.label, - options: field.options?.map((opt) => ({ - id: opt.id ?? '', - label: opt.label, - value: opt.value, - color: 'color' in opt ? opt.color : undefined, - position: opt.position, - })), - }; - }) - .filter(isDefined); - const filter = computeRecordGqlOperationFilter({ - fields, + findFieldMetadataItemById: (id) => + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatFieldMetadataMaps, + }), recordFilters, recordFilterGroups, filterValueDependencies: { currentWorkspaceMemberId, timeZone }, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util.ts index 58b4fbecf3..8cf536180e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util.ts @@ -5,7 +5,6 @@ import { RecordFilterGroupLogicalOperator, RowLevelPermissionPredicateGroupLogicalOperator, type CompositeFieldSubFieldName, - type PartialFieldMetadataItemOption, type RecordGqlOperationFilter, type RowLevelPermissionPredicateValue, } from 'twenty-shared/types'; @@ -216,26 +215,14 @@ export const buildRowLevelPermissionRecordFilter = ({ predicateGroup.parentRowLevelPermissionPredicateGroupId, })); - const fieldMetadataItems = predicates - .map((predicate) => - findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: predicate.fieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - }), - ) - .filter(isDefined) - .map((field) => ({ - id: field.id, - name: field.name, - type: field.type, - label: field.label, - options: field.options as PartialFieldMetadataItemOption[], - })); - return computeRecordGqlOperationFilter({ recordFilters, recordFilterGroups, - fields: fieldMetadataItems, + findFieldMetadataItemById: (id) => + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatFieldMetadataMaps, + }), filterValueDependencies: { currentWorkspaceMemberId: workspaceMember?.id, }, diff --git a/packages/twenty-server/src/modules/dashboard/chart-data/services/chart-data-query.service.ts b/packages/twenty-server/src/modules/dashboard/chart-data/services/chart-data-query.service.ts index 573a7fee95..e29170db39 100644 --- a/packages/twenty-server/src/modules/dashboard/chart-data/services/chart-data-query.service.ts +++ b/packages/twenty-server/src/modules/dashboard/chart-data/services/chart-data-query.service.ts @@ -93,7 +93,6 @@ export class ChartDataQueryService { }: ExecuteGroupByQueryParams): Promise { const gqlOperationFilter = convertChartFilterToGqlOperationFilter({ filter, - flatObjectMetadata, flatFieldMetadataMaps, userTimezone, }); diff --git a/packages/twenty-server/src/modules/dashboard/chart-data/utils/convert-chart-filter-to-gql-operation-filter.util.ts b/packages/twenty-server/src/modules/dashboard/chart-data/utils/convert-chart-filter-to-gql-operation-filter.util.ts index fefb97fe49..34cb760819 100644 --- a/packages/twenty-server/src/modules/dashboard/chart-data/utils/convert-chart-filter-to-gql-operation-filter.util.ts +++ b/packages/twenty-server/src/modules/dashboard/chart-data/utils/convert-chart-filter-to-gql-operation-filter.util.ts @@ -2,7 +2,6 @@ import { type ChartFilter, type CompositeFieldSubFieldName, type FilterableAndTSVectorFieldType, - type PartialFieldMetadataItem, type RecordFilterGroupLogicalOperator, type ViewFilterOperand, } from 'twenty-shared/types'; @@ -18,18 +17,15 @@ import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query- import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type ConvertChartFilterToGqlOperationFilterParams = { filter: ChartFilter | undefined; - flatObjectMetadata: FlatObjectMetadata; flatFieldMetadataMaps: FlatEntityMaps; userTimezone: string; }; export const convertChartFilterToGqlOperationFilter = ({ filter, - flatObjectMetadata, flatFieldMetadataMaps, userTimezone, }: ConvertChartFilterToGqlOperationFilterParams): ObjectRecordFilter => { @@ -44,34 +40,6 @@ export const convertChartFilterToGqlOperationFilter = ({ return {}; } - const fieldIds = flatObjectMetadata.fieldIds ?? []; - const fields: PartialFieldMetadataItem[] = fieldIds - .map((fieldId: string) => { - const field = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: fieldId, - flatEntityMaps: flatFieldMetadataMaps, - }); - - if (!isDefined(field)) { - return null; - } - - return { - id: field.id, - name: field.name, - type: field.type, - label: field.label, - options: field.options?.map((opt) => ({ - id: opt.id ?? '', - label: opt.label, - value: opt.value, - color: 'color' in opt ? opt.color : undefined, - position: opt.position, - })), - }; - }) - .filter(isDefined); - const convertedRecordFilters: Omit[] = recordFilters.map( (recordFilter) => { const field = findFlatEntityByIdInFlatEntityMaps({ @@ -90,6 +58,8 @@ export const convertChartFilterToGqlOperationFilter = ({ subFieldName: (recordFilter.subFieldName ?? undefined) as | CompositeFieldSubFieldName | undefined, + relationTargetFieldMetadataId: + recordFilter.relationTargetFieldMetadataId ?? null, }; }, ); @@ -104,7 +74,11 @@ export const convertChartFilterToGqlOperationFilter = ({ })); return computeRecordGqlOperationFilter({ - fields, + findFieldMetadataItemById: (id) => + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatFieldMetadataMaps, + }), recordFilters: convertedRecordFilters, recordFilterGroups: convertedRecordFilterGroups, filterValueDependencies: { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index 0b5fbbc148..ec91229535 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -1,12 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { - type FieldMetadataComplexOption, - type FieldMetadataDefaultOption, -} from 'twenty-shared/types'; import { computeRecordGqlOperationFilter, - isDefined, isRecordFilterValueValid, resolveInput, } from 'twenty-shared/utils'; @@ -63,38 +58,12 @@ export class FindRecordsWorkflowAction implements WorkflowAction { const executionContext = await this.workflowExecutionContextService.getExecutionContext(runInfo); - const { flatObjectMetadata, flatFieldMetadataMaps } = + const { flatFieldMetadataMaps } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( workflowActionInput.objectName, workspaceId, ); - const fields = flatObjectMetadata.fieldIds - .map((fieldId) => { - const field = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: fieldId, - flatEntityMaps: flatFieldMetadataMaps, - }); - - if (!field) { - return null; - } - - return { - id: field.id, - name: field.name, - type: field.type, - label: field.label, - // Note: force cast is required until we deprecate the CreateFieldInput and UpdateFieldInput - // type derivation from the FieldMetadataDto - options: field.options as - | (FieldMetadataDefaultOption & { id: string })[] - | (FieldMetadataComplexOption & { id: string })[] - | null, - }; - }) - .filter(isDefined); - if (workflowActionInput.filter?.recordFilters) { for (const filter of workflowActionInput.filter.recordFilters) { if (!isRecordFilterValueValid(filter)) { @@ -110,7 +79,11 @@ export class FindRecordsWorkflowAction implements WorkflowAction { workflowActionInput.filter?.recordFilters && workflowActionInput.filter?.recordFilterGroups ? computeRecordGqlOperationFilter({ - fields, + findFieldMetadataItemById: (id) => + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatFieldMetadataMaps, + }), recordFilters: workflowActionInput.filter.recordFilters, recordFilterGroups: workflowActionInput.filter.recordFilterGroups, filterValueDependencies: { diff --git a/packages/twenty-server/test/integration/graphql/suites/chart-data-relation-traversal-filter.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/chart-data-relation-traversal-filter.integration-spec.ts new file mode 100644 index 0000000000..77db8ec307 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/chart-data-relation-traversal-filter.integration-spec.ts @@ -0,0 +1,230 @@ +import gql from 'graphql-tag'; +import request from 'supertest'; +import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util'; +import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { v4 as uuidv4 } from 'uuid'; + +const client = request(`http://localhost:${APP_PORT}`); + +const TEST_COMPANY_AIRBNB_ID = '20202020-aaaa-4000-8000-000000000001'; +const TEST_COMPANY_STRIPE_ID = '20202020-aaaa-4000-8000-000000000002'; +const TEST_PERSON_AIRBNB_1_ID = '20202020-bbbb-4000-8000-000000000001'; +const TEST_PERSON_AIRBNB_2_ID = '20202020-bbbb-4000-8000-000000000002'; +const TEST_PERSON_STRIPE_1_ID = '20202020-bbbb-4000-8000-000000000003'; +const SHARED_JOB_TITLE = 'chart-test-relation-traversal'; +const ALL_TEST_PERSON_IDS = [ + TEST_PERSON_AIRBNB_1_ID, + TEST_PERSON_AIRBNB_2_ID, + TEST_PERSON_STRIPE_1_ID, +]; +const ALL_TEST_COMPANY_IDS = [TEST_COMPANY_AIRBNB_ID, TEST_COMPANY_STRIPE_ID]; + +describe('BarChartData with relation-traversal filter (e2e)', () => { + let personObjectMetadataId: string | null = null; + let personIdFieldMetadataId: string | null = null; + let personJobTitleFieldMetadataId: string | null = null; + let personCompanyFieldMetadataId: string | null = null; + let companyNameFieldMetadataId: string | null = null; + + const lookupMetadataIds = async () => { + const objectsResponse = await makeMetadataAPIRequest({ + query: gql` + query Objects($filter: ObjectFilter!, $paging: CursorPaging!) { + objects(filter: $filter, paging: $paging) { + edges { + node { + id + nameSingular + fieldsList { + id + name + } + } + } + } + } + `, + variables: { paging: { first: 1000 }, filter: {} }, + }); + + expect(objectsResponse.body.errors).toBeUndefined(); + + const objects: Array<{ + id: string; + nameSingular: string; + fieldsList: Array<{ id: string; name: string }>; + }> = objectsResponse.body.data.objects.edges.map( + (edge: { node: unknown }) => edge.node, + ); + + const personObject = objects.find((o) => o.nameSingular === 'person'); + const companyObject = objects.find((o) => o.nameSingular === 'company'); + + personObjectMetadataId = personObject?.id ?? null; + personIdFieldMetadataId = + personObject?.fieldsList.find((f) => f.name === 'id')?.id ?? null; + personJobTitleFieldMetadataId = + personObject?.fieldsList.find((f) => f.name === 'jobTitle')?.id ?? null; + personCompanyFieldMetadataId = + personObject?.fieldsList.find((f) => f.name === 'company')?.id ?? null; + companyNameFieldMetadataId = + companyObject?.fieldsList.find((f) => f.name === 'name')?.id ?? null; + + if ( + !personObjectMetadataId || + !personIdFieldMetadataId || + !personJobTitleFieldMetadataId || + !personCompanyFieldMetadataId || + !companyNameFieldMetadataId + ) { + throw new Error('Failed to resolve required metadata ids for chart test'); + } + }; + + const seedTestRecords = async () => { + await makeGraphqlAPIRequest( + createManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: 'id', + data: [ + { id: TEST_COMPANY_AIRBNB_ID, name: 'AirbnbChartTest' }, + { id: TEST_COMPANY_STRIPE_ID, name: 'StripeChartTest' }, + ], + upsert: true, + }), + ); + + await makeGraphqlAPIRequest( + createManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: 'id', + data: [ + { + id: TEST_PERSON_AIRBNB_1_ID, + companyId: TEST_COMPANY_AIRBNB_ID, + jobTitle: SHARED_JOB_TITLE, + }, + { + id: TEST_PERSON_AIRBNB_2_ID, + companyId: TEST_COMPANY_AIRBNB_ID, + jobTitle: SHARED_JOB_TITLE, + }, + { + id: TEST_PERSON_STRIPE_1_ID, + companyId: TEST_COMPANY_STRIPE_ID, + jobTitle: SHARED_JOB_TITLE, + }, + ], + upsert: true, + }), + ); + }; + + const queryBarChartCount = async (extraRecordFilters: object[] = []) => { + const filterGroupId = uuidv4(); + const allRecordFilters = [ + { + id: uuidv4(), + type: 'TEXT', + operand: 'CONTAINS', + value: SHARED_JOB_TITLE, + fieldMetadataId: personJobTitleFieldMetadataId, + recordFilterGroupId: filterGroupId, + }, + ...extraRecordFilters.map((filter) => ({ + ...filter, + recordFilterGroupId: filterGroupId, + })), + ]; + + const response = await client + .post('/metadata') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: ` + query BarChartData($input: BarChartDataInput!) { + barChartData(input: $input) { + data + indexBy + keys + } + } + `, + variables: { + input: { + objectMetadataId: personObjectMetadataId, + configuration: { + configurationType: 'BAR_CHART', + layout: 'VERTICAL', + aggregateFieldMetadataId: personIdFieldMetadataId, + aggregateOperation: 'COUNT', + primaryAxisGroupByFieldMetadataId: personJobTitleFieldMetadataId, + primaryAxisOrderBy: 'VALUE_DESC', + filter: { + recordFilters: allRecordFilters, + recordFilterGroups: [ + { id: filterGroupId, logicalOperator: 'AND' }, + ], + }, + }, + }, + }, + }); + + expect(response.body.errors).toBeUndefined(); + const data: Array> = + response.body.data.barChartData.data; + const row = data.find((entry) => entry.jobTitle === SHARED_JOB_TITLE); + + return typeof row?.id === 'number' ? row.id : 0; + }; + + beforeAll(async () => { + await lookupMetadataIds(); + await seedTestRecords(); + }); + + afterAll(async () => { + await makeGraphqlAPIRequest( + deleteManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: 'id', + filter: { id: { in: ALL_TEST_PERSON_IDS } }, + }), + ); + await makeGraphqlAPIRequest( + deleteManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: 'id', + filter: { id: { in: ALL_TEST_COMPANY_IDS } }, + }), + ); + }); + + it('should count all 3 test people without a relation-traversal filter', async () => { + const count = await queryBarChartCount(); + + expect(count).toBe(3); + }); + + it('should apply a one-hop relation-traversal filter and only count Airbnb people', async () => { + const count = await queryBarChartCount([ + { + id: uuidv4(), + type: 'TEXT', + operand: 'CONTAINS', + value: 'AirbnbChartTest', + fieldMetadataId: personCompanyFieldMetadataId, + relationTargetFieldMetadataId: companyNameFieldMetadataId, + }, + ]); + + expect(count).toBe(2); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/workflow/find-records-relation-traversal-workflow.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workflow/find-records-relation-traversal-workflow.integration-spec.ts new file mode 100644 index 0000000000..3efc463b78 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/workflow/find-records-relation-traversal-workflow.integration-spec.ts @@ -0,0 +1,348 @@ +import gql from 'graphql-tag'; +import request from 'supertest'; +import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util'; +import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { + destroyWorkflowRun, + runWorkflowVersion, + waitForWorkflowCompletion, +} from 'test/integration/graphql/suites/workflow/utils/workflow-run-test.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { v4 as uuidv4 } from 'uuid'; + +const client = request(`http://localhost:${APP_PORT}`); + +const TEST_COMPANY_AIRBNB_ID = '20202020-eeee-4000-8000-000000000001'; +const TEST_COMPANY_STRIPE_ID = '20202020-eeee-4000-8000-000000000002'; +const TEST_PERSON_AIRBNB_1_ID = '20202020-ffff-4000-8000-000000000001'; +const TEST_PERSON_AIRBNB_2_ID = '20202020-ffff-4000-8000-000000000002'; +const TEST_PERSON_STRIPE_1_ID = '20202020-ffff-4000-8000-000000000003'; +const ALL_TEST_PERSON_IDS = [ + TEST_PERSON_AIRBNB_1_ID, + TEST_PERSON_AIRBNB_2_ID, + TEST_PERSON_STRIPE_1_ID, +]; +const ALL_TEST_COMPANY_IDS = [TEST_COMPANY_AIRBNB_ID, TEST_COMPANY_STRIPE_ID]; + +describe('FindRecords workflow action with relation-traversal filter (e2e)', () => { + let createdWorkflowId: string | null = null; + let createdWorkflowVersionId: string | null = null; + let findRecordsStepId: string | null = null; + let createdWorkflowRunId: string | null = null; + let personCompanyFieldMetadataId: string | null = null; + let companyNameFieldMetadataId: string | null = null; + + const lookupFieldMetadataIds = async () => { + const objectsResponse = await makeMetadataAPIRequest({ + query: gql` + query Objects($filter: ObjectFilter!, $paging: CursorPaging!) { + objects(filter: $filter, paging: $paging) { + edges { + node { + nameSingular + fieldsList { + id + name + } + } + } + } + } + `, + variables: { paging: { first: 1000 }, filter: {} }, + }); + + expect(objectsResponse.body.errors).toBeUndefined(); + + const objects: Array<{ + nameSingular: string; + fieldsList: Array<{ id: string; name: string }>; + }> = objectsResponse.body.data.objects.edges.map( + (edge: { node: unknown }) => edge.node, + ); + + const personObject = objects.find((o) => o.nameSingular === 'person'); + const companyObject = objects.find((o) => o.nameSingular === 'company'); + + personCompanyFieldMetadataId = + personObject?.fieldsList.find((f) => f.name === 'company')?.id ?? null; + companyNameFieldMetadataId = + companyObject?.fieldsList.find((f) => f.name === 'name')?.id ?? null; + + if (!personCompanyFieldMetadataId || !companyNameFieldMetadataId) { + throw new Error( + `Could not resolve required field metadata ids — person.company=${personCompanyFieldMetadataId}, company.name=${companyNameFieldMetadataId}`, + ); + } + }; + + const seedTestRecords = async () => { + await makeGraphqlAPIRequest( + createManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: 'id', + data: [ + { id: TEST_COMPANY_AIRBNB_ID, name: 'AirbnbWorkflowTest' }, + { id: TEST_COMPANY_STRIPE_ID, name: 'StripeWorkflowTest' }, + ], + upsert: true, + }), + ); + + await makeGraphqlAPIRequest( + createManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: 'id', + data: [ + { + id: TEST_PERSON_AIRBNB_1_ID, + companyId: TEST_COMPANY_AIRBNB_ID, + jobTitle: 'workflow-test-airbnb-1', + }, + { + id: TEST_PERSON_AIRBNB_2_ID, + companyId: TEST_COMPANY_AIRBNB_ID, + jobTitle: 'workflow-test-airbnb-2', + }, + { + id: TEST_PERSON_STRIPE_1_ID, + companyId: TEST_COMPANY_STRIPE_ID, + jobTitle: 'workflow-test-stripe-1', + }, + ], + upsert: true, + }), + ); + }; + + const buildWorkflow = async () => { + const createWorkflowResponse = await client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: ` + mutation CreateWorkflow { + createWorkflow(data: { name: "Relation Traversal Find Records Test" }) { + id + } + } + `, + }); + + expect(createWorkflowResponse.body.errors).toBeUndefined(); + createdWorkflowId = createWorkflowResponse.body.data.createWorkflow.id; + + const getWorkflowResponse = await client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: ` + query GetWorkflow($id: UUID!) { + workflow(filter: { id: { eq: $id } }) { + id + versions { edges { node { id } } } + } + } + `, + variables: { id: createdWorkflowId }, + }); + + createdWorkflowVersionId = + getWorkflowResponse.body.data.workflow.versions.edges[0].node.id; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: ` + mutation UpdateWorkflowVersion($id: UUID!, $data: WorkflowVersionUpdateInput!) { + updateWorkflowVersion(id: $id, data: $data) { id } + } + `, + variables: { + id: createdWorkflowVersionId, + data: { + trigger: { + name: 'Manual Trigger', + type: 'MANUAL', + settings: { outputSchema: {} }, + nextStepIds: [], + position: { x: 0, y: 0 }, + }, + }, + }, + }); + + const createStepResponse = await client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: ` + mutation CreateWorkflowVersionStep($input: CreateWorkflowVersionStepInput!) { + createWorkflowVersionStep(input: $input) { stepsDiff } + } + `, + variables: { + input: { + workflowVersionId: createdWorkflowVersionId, + stepType: 'FIND_RECORDS', + parentStepId: 'trigger', + position: { x: 200, y: 0 }, + }, + }, + }); + + expect(createStepResponse.body.errors).toBeUndefined(); + + const getVersionResponse = await client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: ` + query GetWorkflowVersion($id: UUID!) { + workflowVersion(filter: { id: { eq: $id } }) { steps } + } + `, + variables: { id: createdWorkflowVersionId }, + }); + + const steps = getVersionResponse.body.data.workflowVersion.steps; + const findRecordsStep = steps.find( + (step: { type: string }) => step.type === 'FIND_RECORDS', + ); + + expect(findRecordsStep).toBeDefined(); + findRecordsStepId = findRecordsStep.id; + + const filterGroupId = uuidv4(); + const filterId = uuidv4(); + + const updateStepResponse = await client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: ` + mutation UpdateWorkflowVersionStep($input: UpdateWorkflowVersionStepInput!) { + updateWorkflowVersionStep(input: $input) { id } + } + `, + variables: { + input: { + workflowVersionId: createdWorkflowVersionId, + step: { + ...findRecordsStep, + settings: { + ...findRecordsStep.settings, + input: { + ...findRecordsStep.settings.input, + objectName: 'person', + limit: 25, + filter: { + recordFilters: [ + { + id: filterId, + type: 'TEXT', + label: 'Company → Name', + value: 'AirbnbWorkflowTest', + operand: 'CONTAINS', + displayValue: 'AirbnbWorkflowTest', + fieldMetadataId: personCompanyFieldMetadataId, + relationTargetFieldMetadataId: + companyNameFieldMetadataId, + recordFilterGroupId: filterGroupId, + }, + ], + recordFilterGroups: [ + { id: filterGroupId, logicalOperator: 'AND' }, + ], + }, + }, + }, + }, + }, + }, + }); + + expect(updateStepResponse.body.errors).toBeUndefined(); + + const activateResponse = await client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: ` + mutation ActivateWorkflowVersion($workflowVersionId: UUID!) { + activateWorkflowVersion(workflowVersionId: $workflowVersionId) + } + `, + variables: { workflowVersionId: createdWorkflowVersionId }, + }); + + expect(activateResponse.body.errors).toBeUndefined(); + }; + + beforeAll(async () => { + await lookupFieldMetadataIds(); + await seedTestRecords(); + await buildWorkflow(); + }); + + afterAll(async () => { + if (createdWorkflowRunId) { + await destroyWorkflowRun(createdWorkflowRunId); + } + if (createdWorkflowId) { + await client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ + query: `mutation DestroyWorkflow($id: ID!) { destroyWorkflow(id: $id) { id } }`, + variables: { id: createdWorkflowId }, + }); + } + await makeGraphqlAPIRequest( + deleteManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: 'id', + filter: { id: { in: ALL_TEST_PERSON_IDS } }, + }), + ); + await makeGraphqlAPIRequest( + deleteManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: 'id', + filter: { id: { in: ALL_TEST_COMPANY_IDS } }, + }), + ); + }); + + it('should apply a one-hop relation-traversal filter and return only matching records', async () => { + const workflowRunId = await runWorkflowVersion({ + workflowVersionId: createdWorkflowVersionId!, + }); + + createdWorkflowRunId = workflowRunId; + + const workflowRun = await waitForWorkflowCompletion(workflowRunId); + + expect(workflowRun?.status).toBe('COMPLETED'); + expect(workflowRun?.state?.stepInfos?.[findRecordsStepId!]?.status).toBe( + 'SUCCESS', + ); + + const result = workflowRun?.state?.stepInfos?.[findRecordsStepId!] + ?.result as + | { all?: Array<{ id: string }>; totalCount?: number } + | undefined; + + const returnedIds = (result?.all ?? []).map((record) => record.id); + + expect(returnedIds).toContain(TEST_PERSON_AIRBNB_1_ID); + expect(returnedIds).toContain(TEST_PERSON_AIRBNB_2_ID); + expect(returnedIds).not.toContain(TEST_PERSON_STRIPE_1_ID); + }); +}); diff --git a/packages/twenty-shared/src/types/page-layout/chart-filter.type.ts b/packages/twenty-shared/src/types/page-layout/chart-filter.type.ts index 94c693f599..b1e7ce8fe4 100644 --- a/packages/twenty-shared/src/types/page-layout/chart-filter.type.ts +++ b/packages/twenty-shared/src/types/page-layout/chart-filter.type.ts @@ -8,6 +8,7 @@ export type ChartRecordFilter = { type?: string; recordFilterGroupId?: string | null; subFieldName?: string | null; + relationTargetFieldMetadataId?: SerializedRelation | null; }; export type ChartRecordFilterGroup = { diff --git a/packages/twenty-shared/src/utils/filter/__tests__/computeRecordGqlOperationFilter.test.ts b/packages/twenty-shared/src/utils/filter/__tests__/computeRecordGqlOperationFilter.test.ts index 3e6f5beb47..f99cf65397 100644 --- a/packages/twenty-shared/src/utils/filter/__tests__/computeRecordGqlOperationFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/__tests__/computeRecordGqlOperationFilter.test.ts @@ -27,7 +27,8 @@ describe('computeRecordGqlOperationFilter', () => { ]; const filter = computeRecordGqlOperationFilter({ - fields: [companyIdField], + findFieldMetadataItemById: (id) => + id === companyIdField.id ? companyIdField : undefined, recordFilters, recordFilterGroups: [], filterValueDependencies: { diff --git a/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterGroupIntoGqlOperationFilter.test.ts b/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterGroupIntoGqlOperationFilter.test.ts index 8f43880e8c..ec49cbc727 100644 --- a/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterGroupIntoGqlOperationFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterGroupIntoGqlOperationFilter.test.ts @@ -21,11 +21,14 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => { }, ]; + const fieldById = new Map(fields.map((field) => [field.id, field])); + const findFieldMetadataItemById = (id: string) => fieldById.get(id); + it('should return undefined when group is not found', () => { const result = turnRecordFilterGroupsIntoGqlOperationFilter({ filterValueDependencies: {}, filters: [], - fields, + findFieldMetadataItemById, recordFilterGroups: [], currentRecordFilterGroupId: 'nonexistent', }); @@ -45,7 +48,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => { recordFilterGroupId: 'group1', }, ], - fields, + findFieldMetadataItemById, recordFilterGroups: [ { id: 'group1', @@ -70,7 +73,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => { recordFilterGroupId: 'group1', }, ], - fields, + findFieldMetadataItemById, recordFilterGroups: [ { id: 'group1', @@ -95,7 +98,7 @@ describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => { recordFilterGroupId: 'subgroup1', }, ], - fields, + findFieldMetadataItemById, recordFilterGroups: [ { id: 'group1', diff --git a/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts b/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts index c1ca70f0e3..e5c7f8d29d 100644 --- a/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts @@ -131,6 +131,9 @@ const fields = [ const filterValueDependencies = { timeZone: 'UTC' }; +const fieldById = new Map(fields.map((field) => [field.id, field])); +const findFieldMetadataItemById = (id: string) => fieldById.get(id); + const makeFilter = ( fieldMetadataId: string, operand: RecordFilterOperand, @@ -156,7 +159,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, 'x', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toBeUndefined(); @@ -166,7 +169,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { const result = turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: makeFilter('f-text', RecordFilterOperand.CONTAINS, ''), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toBeUndefined(); @@ -181,7 +184,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, 'test', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ name: { ilike: '%test%' } }); @@ -195,7 +198,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.DOES_NOT_CONTAIN, 'test', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ not: { name: { ilike: '%test%' } } }); @@ -207,7 +210,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { const result = turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: makeFilter('f-number', RecordFilterOperand.IS, '42'), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ amount: { eq: 42 } }); @@ -217,7 +220,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { const result = turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: makeFilter('f-number', RecordFilterOperand.IS_NOT, '42'), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ not: { amount: { eq: 42 } } }); @@ -231,7 +234,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.GREATER_THAN_OR_EQUAL, '10', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ amount: { gte: 10 } }); @@ -245,7 +248,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.LESS_THAN_OR_EQUAL, '100', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ amount: { lte: 100 } }); @@ -261,7 +264,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_AFTER, '2024-03-15', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ createdAt: { gte: '2024-03-15' } }); @@ -275,7 +278,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_BEFORE, '2024-03-15', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ createdAt: { lt: '2024-03-15' } }); @@ -289,7 +292,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS, '2024-03-15', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ createdAt: { eq: '2024-03-15' } }); @@ -299,7 +302,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { const result = turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: makeFilter('f-date', RecordFilterOperand.IS_IN_PAST, ''), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('createdAt.lt'); @@ -313,7 +316,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_IN_FUTURE, '', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('createdAt.gte'); @@ -323,7 +326,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { const result = turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: makeFilter('f-date', RecordFilterOperand.IS_TODAY, ''), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('createdAt.eq'); @@ -337,7 +340,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_RELATIVE, 'PAST_7_DAY', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('and'); @@ -353,7 +356,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_AFTER, '2024-03-15T10:00:00Z', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('updatedAt.gte'); @@ -367,7 +370,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_BEFORE, '2024-03-15T10:00:00Z', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('updatedAt.lt'); @@ -381,7 +384,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS, '2024-03-15T10:00:00Z', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('and'); @@ -395,7 +398,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_IN_PAST, '', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('updatedAt.lt'); @@ -409,7 +412,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_IN_FUTURE, '', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('updatedAt.gt'); @@ -423,7 +426,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_TODAY, '', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('and'); @@ -437,7 +440,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_RELATIVE, `PAST_7_DAY;;UTC;;`, ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('and'); @@ -449,7 +452,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { const result = turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: makeFilter('f-rating', RecordFilterOperand.IS, '3'), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('rating.eq'); @@ -463,7 +466,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.GREATER_THAN_OR_EQUAL, '3', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('rating.in'); @@ -477,7 +480,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.LESS_THAN_OR_EQUAL, '3', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('rating.in'); @@ -489,7 +492,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { const result = turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'true'), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ isActive: { eq: true } }); @@ -499,7 +502,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { const result = turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'false'), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ isActive: { eq: false } }); @@ -515,7 +518,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS, '["ACTIVE","PENDING"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('status.in'); @@ -529,7 +532,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_NOT, '["ACTIVE"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('not'); @@ -545,7 +548,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, '["TAG1","TAG2"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('tags.containsAny'); @@ -559,7 +562,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.DOES_NOT_CONTAIN, '["TAG1"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('or'); @@ -575,7 +578,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS, '["550e8400-e29b-41d4-a716-446655440000"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('companyId.in'); @@ -589,7 +592,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS_NOT, '["550e8400-e29b-41d4-a716-446655440000"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('or'); @@ -605,7 +608,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, 'test', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ metadata: { like: '%test%' } }); @@ -619,7 +622,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.DOES_NOT_CONTAIN, 'test', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ not: { metadata: { like: '%test%' } } }); @@ -635,7 +638,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, 'doc', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ attachments: { like: '%doc%' } }); @@ -651,7 +654,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.VECTOR_SEARCH, 'hello world', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ search: { search: 'hello world' } }); @@ -669,7 +672,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { 'CURRENCY', 'amountMicros', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('revenue'); @@ -685,7 +688,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, 'John', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('or'); @@ -701,7 +704,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, 'Paris', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('or'); @@ -717,7 +720,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, 'api', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ @@ -744,7 +747,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, 'xyz123', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ @@ -769,7 +772,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.DOES_NOT_CONTAIN, 'api', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ @@ -800,7 +803,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.DOES_NOT_CONTAIN, 'xyz123', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ @@ -831,7 +834,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { 'PHONES', 'primaryPhoneNumber', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toBeDefined(); @@ -849,7 +852,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { 'EMAILS', 'primaryEmail', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toBeDefined(); @@ -867,7 +870,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { 'LINKS', 'primaryLinkUrl', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toBeDefined(); @@ -883,7 +886,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.CONTAINS, '["item1"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toBeDefined(); @@ -897,7 +900,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.DOES_NOT_CONTAIN, '["item1"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('not'); @@ -913,7 +916,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS, '["550e8400-e29b-41d4-a716-446655440000"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('recordId.in'); @@ -931,13 +934,13 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { ...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'), relationTargetFieldMetadataId: 'f-text', } as RecordFilter, - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ company: { name: { ilike: '%Acme%' } } }); }); - // If the target field is no longer in fieldMetadataItems (e.g. it was + // If the target field is no longer resolvable (e.g. it was // deleted from the workspace), dropping the filter is the safe path — // the alternative would silently interpret the text value as a UUID // list against the relation FK. @@ -948,7 +951,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { ...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'), relationTargetFieldMetadataId: 'nonexistent-target', } as RecordFilter, - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toBeUndefined(); @@ -963,7 +966,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { ...makeFilter('f-relation', RecordFilterOperand.IS_EMPTY, ''), relationTargetFieldMetadataId: 'f-text', } as RecordFilter, - fieldMetadataItems: fields, + findFieldMetadataItemById, }); // Without traversal, the RELATION case would have produced a filter @@ -984,7 +987,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { ...makeFilter('f-relation', RecordFilterOperand.IS, '["ACTIVE"]'), relationTargetFieldMetadataId: 'f-select', } as RecordFilter, - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toEqual({ company: { status: { in: ['ACTIVE'] } } }); @@ -1001,7 +1004,7 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => { RecordFilterOperand.IS, '["550e8400-e29b-41d4-a716-446655440000"]', ), - fieldMetadataItems: fields, + findFieldMetadataItemById, }); expect(result).toHaveProperty('companyId.in'); diff --git a/packages/twenty-shared/src/utils/filter/computeRecordGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/computeRecordGqlOperationFilter.ts index 63980d3779..d8ef975891 100644 --- a/packages/twenty-shared/src/utils/filter/computeRecordGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/computeRecordGqlOperationFilter.ts @@ -1,5 +1,4 @@ import { - type PartialFieldMetadataItem, type RecordFilterValueDependencies, type RecordGqlOperationFilter, } from '@/types'; @@ -8,17 +7,20 @@ import { type RecordFilter, type RecordFilterGroup, } from '@/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter'; -import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter'; +import { + type FindFieldMetadataItemById, + turnRecordFilterIntoRecordGqlOperationFilter, +} from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter'; import { isDefined } from '@/utils/validation/isDefined'; export const computeRecordGqlOperationFilter = ({ - fields, + findFieldMetadataItemById, recordFilters, recordFilterGroups, filterValueDependencies, }: { recordFilters: Omit[]; - fields: PartialFieldMetadataItem[]; + findFieldMetadataItemById: FindFieldMetadataItemById; recordFilterGroups: RecordFilterGroup[]; filterValueDependencies: RecordFilterValueDependencies; }): RecordGqlOperationFilter => { @@ -28,7 +30,7 @@ export const computeRecordGqlOperationFilter = ({ .map((regularFilter) => { return turnRecordFilterIntoRecordGqlOperationFilter({ recordFilter: regularFilter, - fieldMetadataItems: fields, + findFieldMetadataItemById, filterValueDependencies, }); }) @@ -42,7 +44,7 @@ export const computeRecordGqlOperationFilter = ({ turnRecordFilterGroupsIntoGqlOperationFilter({ filterValueDependencies, filters: recordFilters, - fields, + findFieldMetadataItemById, recordFilterGroups, currentRecordFilterGroupId: outermostFilterGroupId, }); diff --git a/packages/twenty-shared/src/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter.ts b/packages/twenty-shared/src/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter.ts index 2d89553267..2c08801b5b 100644 --- a/packages/twenty-shared/src/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter.ts @@ -221,11 +221,13 @@ export const turnAnyFieldFilterIntoRecordGqlFilter = ({ } } + const fieldById = new Map(fields.map((field) => [field.id, field])); + const baseRecordGqlOperationFilters = anyFieldRecordFilters .map((recordFilter) => turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies: {}, - fieldMetadataItems: fields, + findFieldMetadataItemById: (id) => fieldById.get(id), recordFilter, }), ) diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts index 8d7f0810ca..78732f20a3 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter.ts @@ -1,7 +1,6 @@ import { type CompositeFieldSubFieldName, type FilterableAndTSVectorFieldType, - type PartialFieldMetadataItem, RecordFilterGroupLogicalOperator, type RecordFilterValueDependencies, type RecordGqlOperationFilter, @@ -9,7 +8,10 @@ import { } from '@/types'; import { isDefined } from '@/utils'; -import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter'; +import { + type FindFieldMetadataItemById, + turnRecordFilterIntoRecordGqlOperationFilter, +} from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter'; export type RecordFilter = { id: string; @@ -31,13 +33,13 @@ export type RecordFilterGroup = { export const turnRecordFilterGroupsIntoGqlOperationFilter = ({ filterValueDependencies, filters, - fields, + findFieldMetadataItemById, recordFilterGroups, currentRecordFilterGroupId, }: { filterValueDependencies: RecordFilterValueDependencies; filters: Omit[]; - fields: PartialFieldMetadataItem[]; + findFieldMetadataItemById: FindFieldMetadataItemById; recordFilterGroups: RecordFilterGroup[]; currentRecordFilterGroupId?: string; }): RecordGqlOperationFilter | undefined => { @@ -58,7 +60,7 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({ turnRecordFilterIntoRecordGqlOperationFilter({ filterValueDependencies, recordFilter: recordFilter, - fieldMetadataItems: fields, + findFieldMetadataItemById, }), ) .filter(isDefined); @@ -73,7 +75,7 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({ turnRecordFilterGroupsIntoGqlOperationFilter({ filterValueDependencies, filters, - fields, + findFieldMetadataItemById, recordFilterGroups, currentRecordFilterGroupId: subRecordFilterGroup.id, }), diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts index 1889bcb0a8..0bd5026a1e 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -62,21 +62,23 @@ type FieldShared = { label: string; }; +export type FindFieldMetadataItemById = (id: string) => FieldShared | undefined; + type TurnRecordFilterIntoRecordGqlOperationFilterParams = { filterValueDependencies: RecordFilterValueDependencies; recordFilter: Omit; - fieldMetadataItems: FieldShared[]; + findFieldMetadataItemById: FindFieldMetadataItemById; }; export const turnRecordFilterIntoRecordGqlOperationFilter = ({ recordFilter, - fieldMetadataItems, + findFieldMetadataItemById, filterValueDependencies, }: TurnRecordFilterIntoRecordGqlOperationFilterParams): | RecordGqlOperationFilter | undefined => { - const sourceFieldMetadataItem = fieldMetadataItems.find( - (field) => field.id === recordFilter.fieldMetadataId, + const sourceFieldMetadataItem = findFieldMetadataItemById( + recordFilter.fieldMetadataId, ); if (!isDefined(sourceFieldMetadataItem)) { @@ -91,8 +93,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ sourceFieldMetadataItem.type === FieldMetadataType.RELATION && isDefined(recordFilter.relationTargetFieldMetadataId) ) { - const targetFieldMetadataItem = fieldMetadataItems.find( - (field) => field.id === recordFilter.relationTargetFieldMetadataId, + const targetFieldMetadataItem = findFieldMetadataItemById( + recordFilter.relationTargetFieldMetadataId, ); if (!isDefined(targetFieldMetadataItem)) { diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index cd16ebc16c..7255678a09 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -104,6 +104,7 @@ export type { RecordFilterGroup, } from './filter/turnRecordFilterGroupIntoGqlOperationFilter'; export { turnRecordFilterGroupsIntoGqlOperationFilter } from './filter/turnRecordFilterGroupIntoGqlOperationFilter'; +export type { FindFieldMetadataItemById } from './filter/turnRecordFilterIntoGqlOperationFilter'; export { turnRecordFilterIntoRecordGqlOperationFilter } from './filter/turnRecordFilterIntoGqlOperationFilter'; export { combineFilters } from './filter/utils/combineFilters'; export { convertViewFilterOperandToCoreOperand } from './filter/utils/convert-view-filter-operand-to-core-operand.util';