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.
This commit is contained in:
Lucas Bordeau
2026-03-06 17:33:09 +01:00
committed by GitHub
parent 02bd71052b
commit 73268535dc
12 changed files with 138 additions and 27 deletions
@@ -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,
});
@@ -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';
@@ -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,
});
@@ -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,
});
@@ -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,
});
@@ -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',
},
);
@@ -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,
});
@@ -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,
});
@@ -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,
});
@@ -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',
},
]);
});
});
@@ -0,0 +1,7 @@
export const filterDuplicatesById = <T extends { id: string }>(
item: T,
index: number,
self: T[],
) => {
return self.findIndex((i) => i.id === item.id) === index;
};
@@ -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';