refactor(filters): pass fieldMetadataItems array to dispatcher (#20737)

## Summary

Alternative to #20717. Same goal (clean up the filter dispatcher API
after #20670) but smaller and follows the codebase's "pass data, not
behavior" style.

The dispatcher takes a `fieldMetadataItems: FieldShared[]` array
directly instead of a `findFieldMetadataItemById: (id) => FieldShared |
undefined` callback. The util builds the id lookup internally — once per
call, used for both source-field and relation-target-field lookups. No
new types, no separate hydration step.

## What changes

**`twenty-shared`**
- `computeRecordGqlOperationFilter` /
`turnRecordFilterIntoRecordGqlOperationFilter` /
`turnRecordFilterGroupsIntoGqlOperationFilter`: replace
`findFieldMetadataItemById` param with `fieldMetadataItems` /
`fieldMetadataItemById` (internal Map).
- Remove the exported `FindFieldMetadataItemById` type.
- `turnAnyFieldFilterIntoRecordGqlFilter`: rename its internal
`fieldById` Map for consistency.
- Tests updated to pass arrays.

**Frontend (15 call sites)**
- Switch from `fieldMetadataItemByIdMapSelector` to
`flattenedFieldMetadataItemsSelector`.
- Pass `fieldMetadataItems: flattenedFieldMetadataItems` to the
dispatcher.
- `useFindManyRecordsSelectedInContextStore` keeps the Map selector
because it still does a per-filter lookup for the soft-delete check.

**Server (5 call sites)**
- Pass
`Object.values(flatFieldMetadataMaps.byUniversalIdentifier).filter(isDefined)`.

## Why this over #20717

#20717 moves resolution into a separate hydration step + introduces a
`HydratedRecordFilter` type. The bug that #20717 originally surfaced was
Sentry catching 4 critical runtime errors during review
(`fieldMetadataItemByIdMap` declared but not passed). The added type and
the explicit hydration boundary are extra surface area for not much
benefit — the existing API was a callback wrapping a Map at every call
site, and the natural simplification is to just pass the Map (or its
array) directly.

Net diff: **196 insertions, 203 deletions** (~7 lines net removed). 32
files.

## Test plan
- [x] Shared filter unit tests pass (461 tests)
- [x] Frontend filter/context-store tests pass (13 tests)
- [x] Frontend typecheck passes
- [x] Server typecheck passes
- [x] Lint passes (frontend + server)
- [ ] Integration tests on #20670 still pass — workflow find-records +
chart-data with relation-traversal filter still work end-to-end through
the new array param
This commit is contained in:
Charles Bochet
2026-05-19 22:49:53 +02:00
committed by GitHub
parent 265d2edc83
commit 1a9f786e42
32 changed files with 194 additions and 203 deletions
@@ -6,6 +6,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -54,6 +55,10 @@ export const useFindManyRecordsSelectedInContextStore = ({
fieldMetadataItemByIdMapSelector,
);
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const isSoftDeleteFilterActive = contextStoreFilters.some(
(filter) =>
fieldMetadataItemByIdMap.get(filter.fieldMetadataId)?.name ===
@@ -65,7 +70,7 @@ export const useFindManyRecordsSelectedInContextStore = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -29,8 +29,7 @@ describe('computeContextStoreFilters', () => {
contextStoreFilters: [],
contextStoreFilterGroups: [],
objectMetadataItem: personObjectMetadataItem,
findFieldMetadataItemById: (id) =>
personObjectMetadataItem.fields.find((field) => field.id === id),
fieldMetadataItems: personObjectMetadataItem.fields,
filterValueDependencies: mockFilterValueDependencies,
contextStoreAnyFieldFilterValue: '',
});
@@ -75,8 +74,7 @@ describe('computeContextStoreFilters', () => {
contextStoreFilters,
contextStoreFilterGroups: [],
objectMetadataItem: personObjectMetadataItem,
findFieldMetadataItemById: (id) =>
personObjectMetadataItem.fields.find((field) => field.id === id),
fieldMetadataItems: personObjectMetadataItem.fields,
filterValueDependencies: mockFilterValueDependencies,
contextStoreAnyFieldFilterValue: '',
});
@@ -7,9 +7,9 @@ import {
type RecordFilterValueDependencies,
type RecordGqlOperationFilter,
} from 'twenty-shared/types';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import {
computeRecordGqlOperationFilter,
type FindFieldMetadataItemById,
turnAnyFieldFilterIntoRecordGqlFilter,
} from 'twenty-shared/utils';
@@ -18,7 +18,7 @@ type ComputeContextStoreFiltersProps = {
contextStoreFilters: RecordFilter[];
contextStoreFilterGroups: RecordFilterGroup[];
objectMetadataItem: EnrichedObjectMetadataItem;
findFieldMetadataItemById: FindFieldMetadataItemById;
fieldMetadataItems: FieldMetadataItem[];
filterValueDependencies: RecordFilterValueDependencies;
contextStoreAnyFieldFilterValue: string;
};
@@ -28,7 +28,7 @@ export const computeContextStoreFilters = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById,
fieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
}: ComputeContextStoreFiltersProps) => {
@@ -45,7 +45,7 @@ export const computeContextStoreFilters = ({
recordGqlFilterForAnyFieldFilter,
computeRecordGqlOperationFilter({
filterValueDependencies,
findFieldMetadataItemById,
fieldMetadataItems,
recordFilters: contextStoreFilters,
recordFilterGroups: contextStoreFilterGroups,
}),
@@ -74,7 +74,7 @@ export const computeContextStoreFilters = ({
},
computeRecordGqlOperationFilter({
filterValueDependencies,
findFieldMetadataItemById,
fieldMetadataItems,
recordFilters: contextStoreFilters,
recordFilterGroups: contextStoreFilterGroups,
}),