From 73268535dc5b2d2be556585df812bc928666c58c Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 6 Mar 2026 17:33:09 +0100 Subject: [PATCH] Added record filter hidden fields in query (#18149) Fixes https://github.com/twentyhq/twenty/issues/17506 Hidden fields are now queried when they are in record filters, to avoid optimistic and filtering bugs with hidden fields. --- .../hooks/useLazyFindManyRecordsWithOffset.ts | 4 +- .../object-record/hooks/useUpdateOneRecord.ts | 2 +- .../hooks/useRecordCalendarGroupByRecords.ts | 6 +- ...elds.ts => useRelevantRecordsGqlFields.ts} | 47 ++++++++--- ...useRecordIndexGroupCommonQueryVariables.ts | 4 +- .../useRecordIndexGroupsRecordsLazyGroupBy.ts | 2 + .../hooks/useRecordIndexTableFetchMore.ts | 4 +- .../hooks/useRecordIndexTableQuery.ts | 4 +- ...seResetVirtualizationBecauseDataChanged.ts | 7 +- .../__tests__/filterDuplicatesById.test.ts | 77 +++++++++++++++++++ .../src/utils/array/filterDuplicatesById.ts | 7 ++ packages/twenty-shared/src/utils/index.ts | 1 + 12 files changed, 138 insertions(+), 27 deletions(-) rename packages/twenty-front/src/modules/object-record/record-field/hooks/{useRecordsFieldVisibleGqlFields.ts => useRelevantRecordsGqlFields.ts} (74%) create mode 100644 packages/twenty-shared/src/utils/array/__tests__/filterDuplicatesById.test.ts create mode 100644 packages/twenty-shared/src/utils/array/filterDuplicatesById.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecordsWithOffset.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecordsWithOffset.ts index 13f365b521..c21a408cb4 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecordsWithOffset.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecordsWithOffset.ts @@ -10,7 +10,7 @@ import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecord import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; -import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; +import { useRelevantRecordsGqlFields } from '@/object-record/record-field/hooks/useRelevantRecordsGqlFields'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; @@ -28,7 +28,7 @@ export const useLazyFindManyRecordsWithOffset = ({ const params = useFindManyRecordIndexTableParams(objectNameSingular); - const recordGqlFields = useRecordsFieldVisibleGqlFields({ + const recordGqlFields = useRelevantRecordsGqlFields({ objectMetadataItem, }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index 585a51615e..f236df43e9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -1,5 +1,6 @@ import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation'; @@ -16,7 +17,6 @@ import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggr import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput'; -import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent'; import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/hooks/useRecordCalendarGroupByRecords.ts b/packages/twenty-front/src/modules/object-record/record-calendar/hooks/useRecordCalendarGroupByRecords.ts index 263675dc7c..c3addbd5c7 100644 --- a/packages/twenty-front/src/modules/object-record/record-calendar/hooks/useRecordCalendarGroupByRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-calendar/hooks/useRecordCalendarGroupByRecords.ts @@ -4,13 +4,13 @@ import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getR import { useGroupByRecordsQuery } from '@/object-record/hooks/useGroupByRecordsQuery'; import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext'; import { useRecordCalendarQueryDateRangeFilter } from '@/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter'; -import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; +import { useRelevantRecordsGqlFields } from '@/object-record/record-field/hooks/useRelevantRecordsGqlFields'; import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject'; import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone'; -import { useQuery } from '@apollo/client'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useQuery } from '@apollo/client'; import { useMemo } from 'react'; import { type Temporal } from 'temporal-polyfill'; import { @@ -30,7 +30,7 @@ export const useRecordCalendarGroupByRecords = ( recordIndexCalendarFieldMetadataIdState, ); - const recordGqlFields = useRecordsFieldVisibleGqlFields({ + const recordGqlFields = useRelevantRecordsGqlFields({ objectMetadataItem, additionalFieldMetadataId: recordIndexCalendarFieldMetadataId, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRelevantRecordsGqlFields.ts similarity index 74% rename from packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts rename to packages/twenty-front/src/modules/object-record/record-field/hooks/useRelevantRecordsGqlFields.ts index efb6ac26f9..0fd55cbe25 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRelevantRecordsGqlFields.ts @@ -1,42 +1,65 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getImageIdentifierFieldMetadataItem } from '@/object-metadata/utils/getImageIdentifierFieldMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { hasObjectMetadataItemPositionField } from '@/object-metadata/utils/hasObjectMetadataItemPositionField'; import { generateActivityTargetGqlFields } from '@/object-record/graphql/record-gql-fields/utils/generateActivityTargetGqlFields'; +import { CoreObjectNameSingular } from 'twenty-shared/types'; + import { generateDepthRecordGqlFieldsFromFields } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromFields'; import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; -import { isDefined } from 'twenty-shared/utils'; -type UseRecordsFieldVisibleGqlFields = { +import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { filterDuplicatesById, isDefined } from 'twenty-shared/utils'; + +type UseRecordsUsefulGqlFields = { objectMetadataItem: ObjectMetadataItem; additionalFieldMetadataId?: string | null; }; -export const useRecordsFieldVisibleGqlFields = ({ +export const useRelevantRecordsGqlFields = ({ objectMetadataItem, additionalFieldMetadataId, -}: UseRecordsFieldVisibleGqlFields) => { +}: UseRecordsUsefulGqlFields) => { const visibleRecordFields = useAtomComponentSelectorValue( visibleRecordFieldsComponentSelector, ); + const currentRecordFilters = useAtomComponentStateValue( + currentRecordFiltersComponentState, + ); + const { fieldMetadataItemByFieldMetadataItemId } = useRecordIndexContextOrThrow(); const { objectMetadataItems } = useObjectMetadataItems(); + const visibleRecordFieldMetadataItems = visibleRecordFields + .map( + (field) => + fieldMetadataItemByFieldMetadataItemId[field.fieldMetadataItemId], + ) + .filter(isDefined); + + const recordFilterFields = currentRecordFilters + .map((recordFilter) => + objectMetadataItem.fields.find( + (field) => field.id === recordFilter.fieldMetadataId, + ), + ) + .filter(isDefined); + + const fieldMetadataItemsToUse = [ + ...visibleRecordFieldMetadataItems, + ...(recordFilterFields ?? []), + ].filter(filterDuplicatesById); + const allDepthOneGqlFields = generateDepthRecordGqlFieldsFromFields({ objectMetadataItems, - fields: visibleRecordFields - .map( - (field) => - fieldMetadataItemByFieldMetadataItemId[field.fieldMetadataItemId], - ) - .filter(isDefined), + fields: fieldMetadataItemsToUse, depth: 1, }); 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 351cd15929..2fbd33b509 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,6 +1,6 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; +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'; @@ -63,7 +63,7 @@ export const useRecordIndexGroupCommonQueryVariables = () => { recordIndexGroupFieldMetadataItemComponentState, ); - const recordGqlFields = useRecordsFieldVisibleGqlFields({ + const recordGqlFields = useRelevantRecordsGqlFields({ objectMetadataItem, additionalFieldMetadataId: recordIndexGroupFieldMetadataItem?.id, }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy.ts index 2335a0dbd0..c898ed6ac9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy.ts @@ -5,6 +5,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI import { type RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { generateGroupsRecordsGroupByQuery } from '@/object-record/record-aggregate/utils/generateGroupsRecordsGroupByQuery'; + import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables'; import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject'; import { useLazyQuery } from '@apollo/client'; @@ -63,6 +64,7 @@ export const useRecordIndexGroupsRecordsLazyGroupBy = ({ limit: recordGroupsLimit, }, client: apolloCoreClient, + fetchPolicy: 'no-cache', }, ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableFetchMore.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableFetchMore.ts index 242ee8c13f..73c99ab104 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableFetchMore.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableFetchMore.ts @@ -1,6 +1,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; -import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; +import { useRelevantRecordsGqlFields } from '@/object-record/record-field/hooks/useRelevantRecordsGqlFields'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; export const useRecordIndexTableFetchMore = (objectNameSingular: string) => { @@ -10,7 +10,7 @@ export const useRecordIndexTableFetchMore = (objectNameSingular: string) => { objectNameSingular, }); - const recordGqlFields = useRecordsFieldVisibleGqlFields({ + const recordGqlFields = useRelevantRecordsGqlFields({ objectMetadataItem, }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableQuery.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableQuery.ts index 4ed2b22b54..884d8b0b99 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableQuery.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableQuery.ts @@ -1,6 +1,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; +import { useRelevantRecordsGqlFields } from '@/object-record/record-field/hooks/useRelevantRecordsGqlFields'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies'; import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; @@ -14,7 +14,7 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => { objectNameSingular, }); - const recordGqlFields = useRecordsFieldVisibleGqlFields({ + const recordGqlFields = useRelevantRecordsGqlFields({ objectMetadataItem, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged.ts index d69c098d0b..0a2fdca584 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged.ts @@ -1,9 +1,9 @@ -import { useCallback } from 'react'; import { useStore } from 'jotai'; +import { useCallback } from 'react'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; -import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; +import { useRelevantRecordsGqlFields } from '@/object-record/record-field/hooks/useRelevantRecordsGqlFields'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { useTriggerFetchPages } from '@/object-record/record-table/virtualization/hooks/useTriggerFetchPages'; @@ -29,7 +29,8 @@ export const useResetVirtualizationBecauseDataChanged = ( const params = useFindManyRecordIndexTableParams(objectNameSingular); - const recordGqlFields = useRecordsFieldVisibleGqlFields({ + // TODO: we could optimize this by using an aggregate or using only id: true in recordGqlFields + const recordGqlFields = useRelevantRecordsGqlFields({ objectMetadataItem, }); diff --git a/packages/twenty-shared/src/utils/array/__tests__/filterDuplicatesById.test.ts b/packages/twenty-shared/src/utils/array/__tests__/filterDuplicatesById.test.ts new file mode 100644 index 0000000000..7ccc1be4cc --- /dev/null +++ b/packages/twenty-shared/src/utils/array/__tests__/filterDuplicatesById.test.ts @@ -0,0 +1,77 @@ +import { filterDuplicatesById } from '@/utils/array/filterDuplicatesById'; + +type ObjectRecordTest = { id: string }; + +describe('filterDuplicatesById', () => { + it('should work with empty array', () => { + const array: ObjectRecordTest[] = []; + + const filteredArray = array.filter(filterDuplicatesById); + + expect(filteredArray).toEqual([]); + }); + + it('should work with no duplicates', () => { + const array: ObjectRecordTest[] = [ + { + id: '1', + }, + { + id: '2', + }, + ]; + + const filteredArray = array.filter(filterDuplicatesById); + + expect(filteredArray).toEqual(array); + }); + + it('should work with one duplicate', () => { + const array: ObjectRecordTest[] = [ + { + id: '1', + }, + { + id: '1', + }, + ]; + + const filteredArray = array.filter(filterDuplicatesById); + + expect(filteredArray).toEqual([{ id: '1' }]); + }); + + it('should work with multiple duplicates', () => { + const array: ObjectRecordTest[] = [ + { + id: '1', + }, + { + id: '1', + }, + { + id: '2', + }, + { + id: '3', + }, + { + id: '3', + }, + ]; + + const filteredArray = array.filter(filterDuplicatesById); + + expect(filteredArray).toEqual([ + { + id: '1', + }, + { + id: '2', + }, + { + id: '3', + }, + ]); + }); +}); diff --git a/packages/twenty-shared/src/utils/array/filterDuplicatesById.ts b/packages/twenty-shared/src/utils/array/filterDuplicatesById.ts new file mode 100644 index 0000000000..2d53f09b57 --- /dev/null +++ b/packages/twenty-shared/src/utils/array/filterDuplicatesById.ts @@ -0,0 +1,7 @@ +export const filterDuplicatesById = ( + item: T, + index: number, + self: T[], +) => { + return self.findIndex((i) => i.id === item.id) === index; +}; diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 60fecf353f..72e3d22975 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -9,6 +9,7 @@ export { applyDiff } from './applyDiff'; export { compareArraysOfObjectsByProperty } from './array/compareArraysOfObjectsByProperty'; +export { filterDuplicatesById } from './array/filterDuplicatesById'; export { filterOutByProperty } from './array/filterOutByProperty'; export { findById } from './array/findById'; export { findByProperty } from './array/findByProperty';