Add useGroupBy hook + update calendar view to use groupBy (#15439)
## Context Took inspiration from findMany + kanban => Implemented a hook for the new groupBy API endpoints Fixed an issue when DnD to an empty day
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
|
||||
import { doesRecordBelongToGroup } from '../doesRecordBelongToGroup';
|
||||
|
||||
describe('doesRecordBelongToGroup', () => {
|
||||
it('should return true when groupByConfig is undefined', () => {
|
||||
const record: RecordGqlNode = {
|
||||
__typename: 'Person',
|
||||
id: '123',
|
||||
name: 'John',
|
||||
};
|
||||
|
||||
const result = doesRecordBelongToGroup(record, ['value1'], undefined);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when groupByConfig is empty', () => {
|
||||
const record: RecordGqlNode = {
|
||||
__typename: 'Person',
|
||||
id: '123',
|
||||
name: 'John',
|
||||
};
|
||||
|
||||
const result = doesRecordBelongToGroup(record, ['value1'], []);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when record matches group dimension values', () => {
|
||||
const record: RecordGqlNode = {
|
||||
__typename: 'Person',
|
||||
id: '123',
|
||||
name: 'John',
|
||||
};
|
||||
|
||||
const result = doesRecordBelongToGroup(record, ['John'], [{ name: true }]);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when record does not match group dimension values', () => {
|
||||
const record: RecordGqlNode = {
|
||||
__typename: 'Person',
|
||||
id: '123',
|
||||
name: 'John',
|
||||
};
|
||||
|
||||
const result = doesRecordBelongToGroup(record, ['Jane'], [{ name: true }]);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when record field is undefined', () => {
|
||||
const record: RecordGqlNode = {
|
||||
__typename: 'Person',
|
||||
id: '123',
|
||||
};
|
||||
|
||||
const result = doesRecordBelongToGroup(record, ['John'], [{ name: true }]);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { normalizeGroupByDimensionValue } from '../normalizeGroupByDimensionValue';
|
||||
|
||||
describe('normalizeGroupByDimensionValue', () => {
|
||||
it('should convert string to string', () => {
|
||||
const result = normalizeGroupByDimensionValue('test', undefined);
|
||||
expect(result).toBe('test');
|
||||
});
|
||||
|
||||
it('should convert number to string', () => {
|
||||
const result = normalizeGroupByDimensionValue(123, undefined);
|
||||
expect(result).toBe('123');
|
||||
});
|
||||
|
||||
it('should handle date with DAY granularity', () => {
|
||||
const date = new Date('2024-01-15T10:30:00Z');
|
||||
const result = normalizeGroupByDimensionValue(date, { granularity: 'DAY' });
|
||||
expect(result).toBe('2024-01-15');
|
||||
});
|
||||
|
||||
it('should handle date with MONTH granularity', () => {
|
||||
const date = new Date('2024-01-15T10:30:00Z');
|
||||
const result = normalizeGroupByDimensionValue(date, {
|
||||
granularity: 'MONTH',
|
||||
});
|
||||
expect(result).toBe('2024-01');
|
||||
});
|
||||
|
||||
it('should handle date with YEAR granularity', () => {
|
||||
const date = new Date('2024-01-15T10:30:00Z');
|
||||
const result = normalizeGroupByDimensionValue(date, {
|
||||
granularity: 'YEAR',
|
||||
});
|
||||
expect(result).toBe('2024');
|
||||
});
|
||||
|
||||
it('should handle object with id', () => {
|
||||
const obj = { id: '123', name: 'test' };
|
||||
const result = normalizeGroupByDimensionValue(obj, undefined);
|
||||
expect(result).toBe('123');
|
||||
});
|
||||
|
||||
it('should handle object without id', () => {
|
||||
const obj = { name: 'test' };
|
||||
const result = normalizeGroupByDimensionValue(obj, undefined);
|
||||
expect(result).toBe(JSON.stringify(obj));
|
||||
});
|
||||
});
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { type Reference } from '@apollo/client';
|
||||
import {
|
||||
type ReadFieldFunction,
|
||||
type ToReferenceFunction,
|
||||
} from '@apollo/client/cache/core/types/common';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
|
||||
import { processGroupByConnectionWithRecords } from '../processGroupByConnectionWithRecords';
|
||||
|
||||
describe('processGroupByConnectionWithRecords', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockRecord: RecordGqlNode = {
|
||||
__typename: 'Person',
|
||||
id: '123',
|
||||
name: 'John',
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
const mockReference: Reference = {
|
||||
__ref: 'Person:123',
|
||||
};
|
||||
|
||||
const mockReadField = jest.fn(
|
||||
(fieldName: any, from: any, ..._args: any[]) => {
|
||||
if (fieldName === 'id' && from === mockReference) {
|
||||
return '123';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
) as unknown as ReadFieldFunction;
|
||||
|
||||
const mockToReference: ToReferenceFunction = jest.fn(() => mockReference);
|
||||
|
||||
it('should return cached data when no records match', () => {
|
||||
const cachedEdges: RecordGqlRefEdge[] = [];
|
||||
const cachedPageInfo = {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
};
|
||||
|
||||
const result = processGroupByConnectionWithRecords({
|
||||
cachedEdges,
|
||||
cachedPageInfo,
|
||||
records: [],
|
||||
operation: 'create',
|
||||
queryFilter: {},
|
||||
shouldMatchRootQueryFilter: false,
|
||||
groupByDimensionValues: [],
|
||||
groupByConfig: undefined,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
readField: mockReadField,
|
||||
toReference: mockToReference,
|
||||
});
|
||||
|
||||
expect(result.nextEdges).toEqual([]);
|
||||
expect(result.totalCountDelta).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle create operation', () => {
|
||||
const cachedEdges: RecordGqlRefEdge[] = [];
|
||||
const cachedPageInfo = {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
};
|
||||
|
||||
const result = processGroupByConnectionWithRecords({
|
||||
cachedEdges,
|
||||
cachedPageInfo,
|
||||
records: [mockRecord],
|
||||
operation: 'create',
|
||||
queryFilter: {},
|
||||
shouldMatchRootQueryFilter: false,
|
||||
groupByDimensionValues: [],
|
||||
groupByConfig: undefined,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
readField: mockReadField,
|
||||
toReference: mockToReference,
|
||||
});
|
||||
|
||||
expect(result.totalCountDelta).toBe(1);
|
||||
expect(result.nextEdges.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle delete operation', () => {
|
||||
const mockEdge: RecordGqlRefEdge = {
|
||||
__typename: 'PersonEdge',
|
||||
node: mockReference,
|
||||
cursor: 'cursor123',
|
||||
};
|
||||
|
||||
const cachedEdges: RecordGqlRefEdge[] = [mockEdge];
|
||||
const cachedPageInfo = {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
};
|
||||
|
||||
const result = processGroupByConnectionWithRecords({
|
||||
cachedEdges,
|
||||
cachedPageInfo,
|
||||
records: [mockRecord],
|
||||
operation: 'delete',
|
||||
queryFilter: {},
|
||||
shouldMatchRootQueryFilter: false,
|
||||
groupByDimensionValues: [],
|
||||
groupByConfig: undefined,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
readField: mockReadField,
|
||||
toReference: mockToReference,
|
||||
});
|
||||
|
||||
expect(result.totalCountDelta).toBe(-1);
|
||||
expect(result.nextEdges.length).toBe(0);
|
||||
});
|
||||
});
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { type ApolloCache } from '@apollo/client';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
|
||||
import { triggerUpdateGroupByQueriesOptimisticEffect } from '../triggerUpdateGroupByQueriesOptimisticEffect';
|
||||
|
||||
describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockRecord: RecordGqlNode = {
|
||||
__typename: 'Person',
|
||||
id: '123',
|
||||
name: 'John',
|
||||
};
|
||||
|
||||
it('should call cache.modify with correct field name', () => {
|
||||
const mockModify = jest.fn();
|
||||
const mockCache = {
|
||||
modify: mockModify,
|
||||
} as unknown as ApolloCache<unknown>;
|
||||
|
||||
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||
cache: mockCache,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
operation: 'create',
|
||||
records: [mockRecord],
|
||||
shouldMatchRootQueryFilter: false,
|
||||
});
|
||||
|
||||
expect(mockModify).toHaveBeenCalledWith({
|
||||
broadcast: false,
|
||||
fields: expect.objectContaining({
|
||||
peopleGroupBy: expect.any(Function),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle update operation', () => {
|
||||
const mockModify = jest.fn();
|
||||
const mockCache = {
|
||||
modify: mockModify,
|
||||
} as unknown as ApolloCache<unknown>;
|
||||
|
||||
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||
cache: mockCache,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
operation: 'update',
|
||||
records: [mockRecord],
|
||||
shouldMatchRootQueryFilter: false,
|
||||
});
|
||||
|
||||
expect(mockModify).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle delete operation', () => {
|
||||
const mockModify = jest.fn();
|
||||
const mockCache = {
|
||||
modify: mockModify,
|
||||
} as unknown as ApolloCache<unknown>;
|
||||
|
||||
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||
cache: mockCache,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
operation: 'delete',
|
||||
records: [mockRecord],
|
||||
shouldMatchRootQueryFilter: false,
|
||||
});
|
||||
|
||||
expect(mockModify).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty records array', () => {
|
||||
const mockModify = jest.fn();
|
||||
const mockCache = {
|
||||
modify: mockModify,
|
||||
} as unknown as ApolloCache<unknown>;
|
||||
|
||||
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||
cache: mockCache,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
operation: 'create',
|
||||
records: [],
|
||||
shouldMatchRootQueryFilter: false,
|
||||
});
|
||||
|
||||
expect(mockModify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { normalizeGroupByDimensionValue } from '@/apollo/optimistic-effect/group-by/utils/normalizeGroupByDimensionValue';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const doesRecordBelongToGroup = (
|
||||
record: RecordGqlNode,
|
||||
groupByDimensionValues: readonly string[],
|
||||
groupByConfig?: Array<Record<string, boolean | Record<string, string>>>,
|
||||
): boolean => {
|
||||
if (!isDefined(groupByConfig) || groupByConfig.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const groupByFieldNames = groupByConfig.map(
|
||||
(groupByField) => Object.keys(groupByField)[0],
|
||||
);
|
||||
|
||||
for (let i = 0; i < groupByFieldNames.length; i++) {
|
||||
const fieldName = groupByFieldNames[i];
|
||||
const expectedValue = groupByDimensionValues[i];
|
||||
|
||||
if (!isDefined(expectedValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let recordValue = record[fieldName];
|
||||
|
||||
if (!isDefined(recordValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fieldConfig = groupByConfig[i][fieldName];
|
||||
const recordValueStr = normalizeGroupByDimensionValue(
|
||||
recordValue,
|
||||
fieldConfig,
|
||||
);
|
||||
const expectedValueStr = normalizeGroupByDimensionValue(
|
||||
expectedValue,
|
||||
fieldConfig,
|
||||
);
|
||||
|
||||
if (recordValueStr !== expectedValueStr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const normalizeGroupByDimensionValue = (
|
||||
value: any,
|
||||
fieldConfig: boolean | Record<string, string> | undefined,
|
||||
): string => {
|
||||
if (typeof fieldConfig === 'object' && isDefined(fieldConfig.granularity)) {
|
||||
const dateValue = new Date(value);
|
||||
const granularity = fieldConfig.granularity;
|
||||
|
||||
// TODO: to remove once backend properly returns DATE without time
|
||||
switch (granularity) {
|
||||
case 'DAY':
|
||||
return dateValue.toISOString().split('T')[0];
|
||||
case 'MONTH':
|
||||
return dateValue.toISOString().substring(0, 7);
|
||||
case 'YEAR':
|
||||
return dateValue.getFullYear().toString();
|
||||
case 'DAY_OF_THE_WEEK':
|
||||
return dateValue.getDay().toString();
|
||||
case 'MONTH_OF_THE_YEAR':
|
||||
return (dateValue.getMonth() + 1).toString();
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return value.id ? String(value.id) : JSON.stringify(value);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { doesRecordBelongToGroup } from '@/apollo/optimistic-effect/group-by/utils/doesRecordBelongToGroup';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
import { createCacheEdgeWithRecordRef } from '@/object-record/cache/utils/createCacheEdgeWithRecordRef';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import {
|
||||
type ReadFieldFunction,
|
||||
type ToReferenceFunction,
|
||||
} from '@apollo/client/cache/core/types/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type ProcessGroupByConnectionWithRecordsArgs = {
|
||||
cachedEdges: readonly RecordGqlRefEdge[];
|
||||
cachedPageInfo: {
|
||||
startCursor?: string;
|
||||
endCursor?: string;
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
};
|
||||
records: RecordGqlNode[];
|
||||
operation: 'create' | 'update' | 'delete';
|
||||
queryFilter: any;
|
||||
shouldMatchRootQueryFilter: boolean;
|
||||
groupByDimensionValues: readonly string[];
|
||||
groupByConfig?: Array<Record<string, boolean | Record<string, string>>>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
readField: ReadFieldFunction;
|
||||
toReference: ToReferenceFunction;
|
||||
};
|
||||
|
||||
export const processGroupByConnectionWithRecords = ({
|
||||
cachedEdges,
|
||||
cachedPageInfo,
|
||||
records,
|
||||
operation,
|
||||
queryFilter,
|
||||
shouldMatchRootQueryFilter,
|
||||
groupByDimensionValues,
|
||||
groupByConfig,
|
||||
objectMetadataItem,
|
||||
readField,
|
||||
toReference,
|
||||
}: ProcessGroupByConnectionWithRecordsArgs): {
|
||||
nextEdges: RecordGqlRefEdge[];
|
||||
nextPageInfo: {
|
||||
startCursor?: string;
|
||||
endCursor?: string;
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
};
|
||||
totalCountDelta: number;
|
||||
} => {
|
||||
const nextEdges = [...cachedEdges];
|
||||
const nextPageInfo = isDefined(cachedPageInfo) ? { ...cachedPageInfo } : {};
|
||||
let totalCountDelta = 0;
|
||||
|
||||
for (const record of records) {
|
||||
const recordMatchesFilter = isRecordMatchingFilter({
|
||||
record,
|
||||
filter: queryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const belongsToGroup = doesRecordBelongToGroup(
|
||||
record,
|
||||
groupByDimensionValues,
|
||||
groupByConfig,
|
||||
);
|
||||
|
||||
const recordReference = toReference(record);
|
||||
|
||||
if (!recordReference) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordIndexInEdges = cachedEdges.findIndex(
|
||||
(cachedEdge) => readField('id', cachedEdge.node) === record.id,
|
||||
);
|
||||
const recordExistsInEdges = recordIndexInEdges !== -1;
|
||||
|
||||
if (operation === 'create') {
|
||||
const shouldAdd =
|
||||
(!shouldMatchRootQueryFilter || recordMatchesFilter) &&
|
||||
belongsToGroup &&
|
||||
!recordExistsInEdges;
|
||||
|
||||
if (shouldAdd) {
|
||||
const edge = createCacheEdgeWithRecordRef({
|
||||
record,
|
||||
objectMetadataItem,
|
||||
toReference,
|
||||
});
|
||||
|
||||
if (isDefined(edge)) {
|
||||
nextEdges.unshift(edge);
|
||||
nextPageInfo.startCursor = edge.cursor;
|
||||
totalCountDelta++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'update') {
|
||||
const shouldBeInGroup = recordMatchesFilter && belongsToGroup;
|
||||
|
||||
if (shouldBeInGroup && !recordExistsInEdges) {
|
||||
const edge = createCacheEdgeWithRecordRef({
|
||||
record,
|
||||
objectMetadataItem,
|
||||
toReference,
|
||||
});
|
||||
|
||||
if (isDefined(edge)) {
|
||||
nextEdges.push(edge);
|
||||
totalCountDelta++;
|
||||
}
|
||||
} else if (!shouldBeInGroup && recordExistsInEdges) {
|
||||
nextEdges.splice(recordIndexInEdges, 1);
|
||||
totalCountDelta--;
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'delete' && recordExistsInEdges) {
|
||||
nextEdges.splice(recordIndexInEdges, 1);
|
||||
totalCountDelta--;
|
||||
}
|
||||
}
|
||||
|
||||
return { nextEdges, nextPageInfo, totalCountDelta };
|
||||
};
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
import { type ApolloCache, type StoreObject } from '@apollo/client';
|
||||
|
||||
import { normalizeGroupByDimensionValue } from '@/apollo/optimistic-effect/group-by/utils/normalizeGroupByDimensionValue';
|
||||
import { processGroupByConnectionWithRecords } from '@/apollo/optimistic-effect/group-by/utils/processGroupByConnectionWithRecords';
|
||||
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
import { createCacheEdgeWithRecordRef } from '@/object-record/cache/utils/createCacheEdgeWithRecordRef';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { type RecordGqlGroupByConnection } from '@/object-record/graphql/types/RecordGqlOperationGroupByResult';
|
||||
import { type RecordGqlOperationGroupByVariables } from '@/object-record/graphql/types/RecordGqlOperationGroupByVariables';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
|
||||
type TriggerUpdateGroupByQueriesOptimisticEffectArgs = {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
operation: 'create' | 'update' | 'delete';
|
||||
records: RecordGqlNode[];
|
||||
shouldMatchRootQueryFilter?: boolean;
|
||||
};
|
||||
|
||||
export const triggerUpdateGroupByQueriesOptimisticEffect = ({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
operation,
|
||||
records,
|
||||
shouldMatchRootQueryFilter = false,
|
||||
}: TriggerUpdateGroupByQueriesOptimisticEffectArgs) => {
|
||||
const groupByQueryFieldName = `${objectMetadataItem.namePlural}GroupBy`;
|
||||
|
||||
cache.modify<StoreObject>({
|
||||
broadcast: false,
|
||||
fields: {
|
||||
[groupByQueryFieldName]: (
|
||||
cachedGroupByQueryResult,
|
||||
{ readField, toReference, storeFieldName },
|
||||
) => {
|
||||
const cachedGroupByConnections = cachedGroupByQueryResult as
|
||||
| RecordGqlGroupByConnection[]
|
||||
| undefined;
|
||||
|
||||
if (!Array.isArray(cachedGroupByConnections)) {
|
||||
return cachedGroupByQueryResult;
|
||||
}
|
||||
|
||||
const { fieldVariables: queryVariables } = parseApolloStoreFieldName<
|
||||
CachedObjectRecordQueryVariables & RecordGqlOperationGroupByVariables
|
||||
>(storeFieldName);
|
||||
|
||||
const queryFilter = queryVariables?.filter;
|
||||
const groupByConfig = queryVariables?.groupBy;
|
||||
|
||||
const updatedGroupByConnections = cachedGroupByConnections.map(
|
||||
(groupConnection) => {
|
||||
const groupByDimensionValues =
|
||||
readField('groupByDimensionValues', groupConnection) || [];
|
||||
const cachedEdges =
|
||||
readField<RecordGqlRefEdge[]>('edges', groupConnection) || [];
|
||||
const cachedTotalCount = readField<number | undefined>(
|
||||
'totalCount',
|
||||
groupConnection,
|
||||
);
|
||||
const cachedPageInfo = readField<{
|
||||
startCursor?: string;
|
||||
endCursor?: string;
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
}>('pageInfo', groupConnection);
|
||||
|
||||
const { nextEdges, nextPageInfo, totalCountDelta } =
|
||||
processGroupByConnectionWithRecords({
|
||||
cachedEdges,
|
||||
cachedPageInfo: cachedPageInfo || {},
|
||||
records,
|
||||
operation,
|
||||
queryFilter,
|
||||
shouldMatchRootQueryFilter,
|
||||
groupByDimensionValues: Array.isArray(groupByDimensionValues)
|
||||
? groupByDimensionValues
|
||||
: [],
|
||||
groupByConfig,
|
||||
objectMetadataItem,
|
||||
readField,
|
||||
toReference,
|
||||
});
|
||||
|
||||
if (
|
||||
totalCountDelta === 0 &&
|
||||
nextEdges.length === cachedEdges.length
|
||||
) {
|
||||
return groupConnection;
|
||||
}
|
||||
|
||||
return {
|
||||
...groupConnection,
|
||||
edges: nextEdges,
|
||||
totalCount: isDefined(cachedTotalCount)
|
||||
? cachedTotalCount + totalCountDelta
|
||||
: undefined,
|
||||
pageInfo: nextPageInfo,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
if (operation === 'create' || operation === 'update') {
|
||||
const recordsToAddToNewGroups: Map<
|
||||
string,
|
||||
{
|
||||
dimensionValues: string[];
|
||||
edges: RecordGqlRefEdge[];
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
for (const record of records) {
|
||||
const recordMatchesFilter = isRecordMatchingFilter({
|
||||
record,
|
||||
filter: queryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (
|
||||
shouldMatchRootQueryFilter &&
|
||||
!recordMatchesFilter &&
|
||||
operation === 'create'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDefined(groupByConfig) || groupByConfig.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupByFieldNames = groupByConfig.map(
|
||||
(groupByField) => Object.keys(groupByField)[0],
|
||||
);
|
||||
|
||||
const recordDimensionValues: string[] = [];
|
||||
|
||||
for (let i = 0; i < groupByFieldNames.length; i++) {
|
||||
const fieldName = groupByFieldNames[i];
|
||||
let recordValue = record[fieldName];
|
||||
|
||||
if (!isDefined(recordValue)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const fieldConfig = groupByConfig[i][fieldName];
|
||||
const normalizedValue = normalizeGroupByDimensionValue(
|
||||
recordValue,
|
||||
fieldConfig,
|
||||
);
|
||||
recordDimensionValues.push(normalizedValue);
|
||||
}
|
||||
|
||||
const dimensionKey = recordDimensionValues.join('|');
|
||||
const dimensionExists = updatedGroupByConnections.some((conn) => {
|
||||
const connDimensionValues =
|
||||
readField('groupByDimensionValues', conn) || [];
|
||||
return (
|
||||
Array.isArray(connDimensionValues) &&
|
||||
connDimensionValues.join('|') === dimensionKey
|
||||
);
|
||||
});
|
||||
|
||||
if (
|
||||
!dimensionExists &&
|
||||
recordDimensionValues.length === groupByFieldNames.length
|
||||
) {
|
||||
const edge = createCacheEdgeWithRecordRef({
|
||||
record,
|
||||
objectMetadataItem,
|
||||
toReference,
|
||||
});
|
||||
|
||||
if (isDefined(edge)) {
|
||||
if (!recordsToAddToNewGroups.has(dimensionKey)) {
|
||||
recordsToAddToNewGroups.set(dimensionKey, {
|
||||
dimensionValues: recordDimensionValues,
|
||||
edges: [],
|
||||
});
|
||||
}
|
||||
|
||||
recordsToAddToNewGroups.get(dimensionKey)!.edges.push(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [_, groupData] of recordsToAddToNewGroups) {
|
||||
if (groupData.edges.length > 0) {
|
||||
const newGroupConnection = {
|
||||
__typename: `${objectMetadataItem.nameSingular}Connection`,
|
||||
edges: groupData.edges,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: groupData.edges[0].cursor,
|
||||
endCursor: groupData.edges[groupData.edges.length - 1].cursor,
|
||||
},
|
||||
totalCount: groupData.edges.length,
|
||||
groupByDimensionValues: groupData.dimensionValues,
|
||||
};
|
||||
|
||||
updatedGroupByConnections.push(newGroupConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedGroupByConnections;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
+9
@@ -9,6 +9,7 @@ import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/is
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
|
||||
import { triggerUpdateGroupByQueriesOptimisticEffect } from '@/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect';
|
||||
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
import { encodeCursor } from '@/apollo/utils/encodeCursor';
|
||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||
@@ -238,4 +239,12 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
operation: 'create',
|
||||
records: recordsToCreate,
|
||||
shouldMatchRootQueryFilter,
|
||||
});
|
||||
};
|
||||
|
||||
+8
@@ -1,5 +1,6 @@
|
||||
import { type ApolloCache, type StoreObject } from '@apollo/client';
|
||||
|
||||
import { triggerUpdateGroupByQueriesOptimisticEffect } from '@/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect';
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
@@ -90,4 +91,11 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
|
||||
cache.evict({ id: cache.identify(recordToDestroy) });
|
||||
});
|
||||
|
||||
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
operation: 'delete',
|
||||
records: recordsToDestroy,
|
||||
});
|
||||
};
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
import { type ApolloCache, type StoreObject } from '@apollo/client';
|
||||
|
||||
import { triggerUpdateGroupByQueriesOptimisticEffect } from '@/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect';
|
||||
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
@@ -134,4 +135,12 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
operation: 'update',
|
||||
records: [updatedRecord],
|
||||
shouldMatchRootQueryFilter: true,
|
||||
});
|
||||
};
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
import { type ApolloCache, type StoreObject } from '@apollo/client';
|
||||
|
||||
import { triggerUpdateGroupByQueriesOptimisticEffect } from '@/apollo/optimistic-effect/group-by/utils/triggerUpdateGroupByQueriesOptimisticEffect';
|
||||
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
@@ -141,4 +142,12 @@ export const triggerUpdateRecordOptimisticEffectByBatch = ({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
operation: 'update',
|
||||
records: updatedRecords,
|
||||
shouldMatchRootQueryFilter: true,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user