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
@@ -1,4 +1,4 @@
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -48,8 +48,8 @@ export const useRecordCalendarQueryDateRangeFilter = (
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const anyFieldFilterValue = useAtomComponentStateValue(
@@ -110,7 +110,7 @@ export const useRecordCalendarQueryDateRangeFilter = (
filterValueDependencies,
recordFilters: calendarRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
});
const { recordGqlOperationFilter: anyFieldFilter } =
@@ -22,14 +22,11 @@ const petMockObjectMetadataItem = getMockObjectMetadataItemOrThrow('pet');
const personMockObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
const findCompanyFieldById = (id: string) =>
companyMockObjectMetadataItem.fields.find((field) => field.id === id);
const companyFields = companyMockObjectMetadataItem.fields;
const findPersonFieldById = (id: string) =>
personMockObjectMetadataItem.fields.find((field) => field.id === id);
const personFields = personMockObjectMetadataItem.fields;
const findPetFieldById = (id: string) =>
petMockObjectMetadataItem.fields.find((field) => field.id === id);
const petFields = petMockObjectMetadataItem.fields;
const mockFilterValueDependencies: RecordFilterValueDependencies = {
currentWorkspaceMemberId: '32219445-f587-4c40-b2b1-6d3205ed96da',
@@ -63,7 +60,7 @@ describe('computeViewRecordGqlOperationFilter', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [nameFilter],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fieldMetadataItems: companyFields,
});
expect(result).toEqual({
@@ -116,7 +113,7 @@ describe('computeViewRecordGqlOperationFilter', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [nameFilter, employeesFilter],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fieldMetadataItems: companyFields,
});
expect(result).toEqual({
@@ -196,7 +193,7 @@ describe('should work as expected for the different field types', () => {
addressFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fieldMetadataItems: companyFields,
});
expect(result).toEqual({
@@ -660,7 +657,7 @@ describe('should work as expected for the different field types', () => {
phonesFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findPersonFieldById,
fieldMetadataItems: personFields,
});
expect(result).toEqual({
@@ -857,7 +854,7 @@ describe('should work as expected for the different field types', () => {
emailsFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findPersonFieldById,
fieldMetadataItems: personFields,
});
expect(result).toEqual({
@@ -1069,7 +1066,7 @@ describe('should work as expected for the different field types', () => {
dateFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fieldMetadataItems: companyFields,
});
expect(result).toEqual({
@@ -1171,7 +1168,7 @@ describe('should work as expected for the different field types', () => {
employeesFilterIsNotEmpty,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fieldMetadataItems: companyFields,
});
expect(result).toEqual({
@@ -1273,7 +1270,7 @@ describe('should work as expected for the different field types', () => {
ARRFilterIsNot,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fieldMetadataItems: companyFields,
});
expect(result).toEqual({
@@ -1350,7 +1347,7 @@ describe('should work as expected for the different field types', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [ARRFilterIn, ARRFilterNotIn],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fieldMetadataItems: companyFields,
});
expect(result).toEqual({
@@ -1410,7 +1407,7 @@ describe('should work as expected for the different field types', () => {
filterValueDependencies: mockFilterValueDependencies,
recordFilters: [selectFilterIs, selectFilterIsNot],
recordFilterGroups: [],
findFieldMetadataItemById: findPetFieldById,
fieldMetadataItems: petFields,
});
expect(result).toEqual({
@@ -1483,7 +1480,7 @@ describe('should work as expected for the different field types', () => {
multiSelectFilterDoesNotContain,
],
recordFilterGroups: [],
findFieldMetadataItemById: findCompanyFieldById,
fieldMetadataItems: companyFields,
});
expect(result).toEqual({
@@ -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 { 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 { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -54,8 +54,8 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const computedFilter = computeContextStoreFilters({
@@ -63,7 +63,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -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 { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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 fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const findManyRecordsParams = useFindManyRecordIndexTableParams(
@@ -107,7 +107,7 @@ export const useRecordIndexLazyFetchRecords = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -1,6 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -49,12 +49,12 @@ export const useFindManyRecordIndexTableParams = (
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const currentFilters = computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
recordFilterGroups: currentRecordFilterGroups,
recordFilters: currentRecordFilters,
filterValueDependencies,
@@ -1,5 +1,5 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -38,15 +38,15 @@ export const useRecordIndexGroupCommonQueryVariables = () => {
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const requestFilters = computeRecordGqlOperationFilter({
filterValueDependencies,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
});
const anyFieldFilterValue = useAtomComponentStateValue(
@@ -1,5 +1,5 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
@@ -48,15 +48,15 @@ export const useRecordIndexGroupsAggregatesGroupBy = ({
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const requestFilters = computeRecordGqlOperationFilter({
filterValueDependencies,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
});
const { recordAggregateGqlField } =
@@ -1,6 +1,6 @@
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -40,8 +40,8 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const currentRecordFilters = useAtomComponentStateValue(
@@ -63,7 +63,7 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
objectNameSingular: objectMetadataItem.nameSingular,
variables: {
filter: computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
filterValueDependencies,
@@ -77,7 +77,7 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
currentRecordFilterGroups,
filterValueDependencies,
currentRecordSorts,
fieldMetadataItemByIdMap,
flattenedFieldMetadataItems,
],
);
@@ -1,4 +1,4 @@
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -46,14 +46,14 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
const dateLocale = useAtomStateValue(dateLocaleState);
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const { filterValueDependencies } = useFilterValueDependencies();
const requestFilters = computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
filterValueDependencies,
recordFilterGroups: currentRecordFilterGroups,
recordFilters: currentRecordFilters,
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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';
@@ -16,8 +16,8 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { filterValueDependencies } = useFilterValueDependencies();
const fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const currentRecordFilters = useAtomComponentStateValue(
@@ -39,7 +39,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
objectNameSingular: objectMetadataItem.nameSingular,
variables: {
filter: computeRecordGqlOperationFilter({
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
filterValueDependencies,
@@ -53,7 +53,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
currentRecordFilterGroups,
filterValueDependencies,
currentRecordSorts,
fieldMetadataItemByIdMap,
flattenedFieldMetadataItems,
],
);
@@ -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 { fieldMetadataItemByIdMapSelector } from '@/object-metadata/states/fieldMetadataItemByIdMapSelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
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 fieldMetadataItemByIdMap = useAtomStateValue(
fieldMetadataItemByIdMapSelector,
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const graphqlFilter = computeContextStoreFilters({
@@ -54,7 +54,7 @@ export const useUpdateMultipleRecordsActions = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
findFieldMetadataItemById: (id) => fieldMetadataItemByIdMap.get(id),
fieldMetadataItems: flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});