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 { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
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 { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||||
import { encodeCursor } from '@/apollo/utils/encodeCursor';
|
import { encodeCursor } from '@/apollo/utils/encodeCursor';
|
||||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
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 { 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 { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||||
@@ -90,4 +91,11 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
|||||||
|
|
||||||
cache.evict({ id: cache.identify(recordToDestroy) });
|
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 { 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 { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||||
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
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 { 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 { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||||
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||||
@@ -141,4 +142,12 @@ export const triggerUpdateRecordOptimisticEffectByBatch = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
triggerUpdateGroupByQueriesOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
operation: 'update',
|
||||||
|
records: updatedRecords,
|
||||||
|
shouldMatchRootQueryFilter: true,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
import { type Reference } from '@apollo/client';
|
||||||
|
|
||||||
|
import { encodeCursor } from '@/apollo/utils/encodeCursor';
|
||||||
|
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||||
|
import { type ToReferenceFunction } from '@apollo/client/cache/core/types/common';
|
||||||
|
|
||||||
|
import { createCacheEdgeWithRecordRef } from '../createCacheEdgeWithRecordRef';
|
||||||
|
|
||||||
|
describe('createCacheEdgeWithRecordRef', () => {
|
||||||
|
it('should create an edge with reference when toReference returns a reference', () => {
|
||||||
|
// Given
|
||||||
|
const record: RecordGqlNode = {
|
||||||
|
__typename: 'Person',
|
||||||
|
id: '123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectMetadataItem = {
|
||||||
|
nameSingular: 'person',
|
||||||
|
} as ObjectMetadataItem;
|
||||||
|
|
||||||
|
const mockReference: Reference = {
|
||||||
|
__ref: 'Person:123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const toReference: ToReferenceFunction = jest.fn(() => mockReference);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = createCacheEdgeWithRecordRef({
|
||||||
|
record,
|
||||||
|
objectMetadataItem,
|
||||||
|
toReference,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result).toEqual({
|
||||||
|
__typename: 'PersonEdge',
|
||||||
|
node: mockReference,
|
||||||
|
cursor: encodeCursor(record),
|
||||||
|
});
|
||||||
|
expect(toReference).toHaveBeenCalledWith(record);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when toReference returns undefined', () => {
|
||||||
|
// Given
|
||||||
|
const record: RecordGqlNode = {
|
||||||
|
__typename: 'Person',
|
||||||
|
id: '123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectMetadataItem = {
|
||||||
|
nameSingular: 'person',
|
||||||
|
} as ObjectMetadataItem;
|
||||||
|
|
||||||
|
const toReference: ToReferenceFunction = jest.fn(() => undefined);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = createCacheEdgeWithRecordRef({
|
||||||
|
record,
|
||||||
|
objectMetadataItem,
|
||||||
|
toReference,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBeNull();
|
||||||
|
expect(toReference).toHaveBeenCalledWith(record);
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import { encodeCursor } from '@/apollo/utils/encodeCursor';
|
||||||
|
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||||
|
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
|
||||||
|
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||||
|
import { type ToReferenceFunction } from '@apollo/client/cache/core/types/common';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
type CreateCacheEdgeWithRecordRefParams = {
|
||||||
|
record: RecordGqlNode;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
toReference: ToReferenceFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCacheEdgeWithRecordRef = ({
|
||||||
|
record,
|
||||||
|
objectMetadataItem,
|
||||||
|
toReference,
|
||||||
|
}: CreateCacheEdgeWithRecordRefParams): RecordGqlRefEdge | null => {
|
||||||
|
const reference = toReference(record);
|
||||||
|
if (!isDefined(reference)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
|
||||||
|
node: reference,
|
||||||
|
cursor: encodeCursor(record),
|
||||||
|
};
|
||||||
|
};
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||||
|
|
||||||
|
export type RecordGqlGroupByConnection = RecordGqlConnection & {
|
||||||
|
groupByDimensionValues: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecordGqlOperationGroupByResult = {
|
||||||
|
[objectNamePlural: string]: RecordGqlGroupByConnection[];
|
||||||
|
};
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { type RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
|
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export type RecordGqlOperationGroupByVariables = {
|
||||||
|
groupBy: Record<string, any>[];
|
||||||
|
filter?: RecordGqlOperationFilter;
|
||||||
|
orderBy?: Record<string, any>[];
|
||||||
|
orderByForRecords?: RecordGqlOperationOrderBy;
|
||||||
|
viewId?: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
|
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||||
|
import { generateGroupByRecordsQuery } from '@/object-record/utils/generateGroupByRecordsQuery';
|
||||||
|
|
||||||
|
export const useGroupByRecordsQuery = ({
|
||||||
|
objectNameSingular,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences,
|
||||||
|
}: {
|
||||||
|
objectNameSingular: string;
|
||||||
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
computeReferences?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
|
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||||
|
|
||||||
|
const groupByRecordsQuery = generateGroupByRecordsQuery({
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences,
|
||||||
|
objectPermissionsByObjectMetadataId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupByRecordsQuery,
|
||||||
|
};
|
||||||
|
};
|
||||||
+7
-24
@@ -1,18 +1,14 @@
|
|||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useRecordCalendarGroupByRecords } from '@/object-record/record-calendar/hooks/useRecordCalendarGroupByRecords';
|
||||||
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
|
|
||||||
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
|
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
|
||||||
|
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
|
||||||
import { recordCalendarSelectedRecordIdsComponentSelector } from '@/object-record/record-calendar/states/selectors/recordCalendarSelectedRecordIdsComponentSelector';
|
import { recordCalendarSelectedRecordIdsComponentSelector } from '@/object-record/record-calendar/states/selectors/recordCalendarSelectedRecordIdsComponentSelector';
|
||||||
import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields';
|
|
||||||
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
|
|
||||||
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
|
|
||||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
export const RecordIndexCalendarDataLoaderEffect = () => {
|
export const RecordIndexCalendarDataLoaderEffect = () => {
|
||||||
const recordCalendarId = useAvailableComponentInstanceIdOrThrow(
|
const recordCalendarId = useAvailableComponentInstanceIdOrThrow(
|
||||||
@@ -24,7 +20,9 @@ export const RecordIndexCalendarDataLoaderEffect = () => {
|
|||||||
recordCalendarId,
|
recordCalendarId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
|
const recordCalendarSelectedDate = useRecoilComponentValue(
|
||||||
|
recordCalendarSelectedDateComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
@@ -36,25 +34,10 @@ export const RecordIndexCalendarDataLoaderEffect = () => {
|
|||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordIndexCalendarFieldMetadataId = useRecoilValue(
|
const { records } = useRecordCalendarGroupByRecords(
|
||||||
recordIndexCalendarFieldMetadataIdState,
|
recordCalendarSelectedDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
const objectNameSingular = objectMetadataItem.nameSingular;
|
|
||||||
|
|
||||||
const params = useFindManyRecordIndexTableParams(objectNameSingular);
|
|
||||||
|
|
||||||
const recordGqlFields = useRecordsFieldVisibleGqlFields({
|
|
||||||
objectMetadataItem,
|
|
||||||
additionalFieldMetadataId: recordIndexCalendarFieldMetadataId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { records } = useFindManyRecords({
|
|
||||||
...params,
|
|
||||||
limit: 100,
|
|
||||||
recordGqlFields,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
upsertRecordsInStore(records);
|
upsertRecordsInStore(records);
|
||||||
setRecordIndexAllRecordIdsSelector(records.map((record) => record.id));
|
setRecordIndexAllRecordIdsSelector(records.map((record) => record.id));
|
||||||
|
|||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||||
|
import { hasObjectMetadataItemPositionField } from '@/object-metadata/utils/hasObjectMetadataItemPositionField';
|
||||||
|
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||||
|
import { type RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
|
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 { 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 { useQuery } from '@apollo/client';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const useRecordCalendarGroupByRecords = (selectedDate: Date) => {
|
||||||
|
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
|
||||||
|
|
||||||
|
const recordIndexCalendarFieldMetadataId = useRecoilValue(
|
||||||
|
recordIndexCalendarFieldMetadataIdState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const recordGqlFields = useRecordsFieldVisibleGqlFields({
|
||||||
|
objectMetadataItem,
|
||||||
|
additionalFieldMetadataId: recordIndexCalendarFieldMetadataId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dateRangeFilter } =
|
||||||
|
useRecordCalendarQueryDateRangeFilter(selectedDate);
|
||||||
|
|
||||||
|
const calendarFieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
|
(field) => field.id === recordIndexCalendarFieldMetadataId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupBy = !isDefined(calendarFieldMetadataItem)
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
buildGroupByFieldObject({
|
||||||
|
field: calendarFieldMetadataItem,
|
||||||
|
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const orderByForRecords: RecordGqlOperationOrderBy | undefined =
|
||||||
|
!objectMetadataItem.isRemote &&
|
||||||
|
hasObjectMetadataItemPositionField(objectMetadataItem)
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
position: 'AscNullsFirst',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const { groupByRecordsQuery } = useGroupByRecordsQuery({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const apolloCoreClient = useApolloCoreClient();
|
||||||
|
|
||||||
|
const { data, loading, error } = useQuery(groupByRecordsQuery, {
|
||||||
|
client: apolloCoreClient,
|
||||||
|
skip:
|
||||||
|
!isDefined(calendarFieldMetadataItem) ||
|
||||||
|
groupBy.length === 0 ||
|
||||||
|
Object.keys(dateRangeFilter).length === 0,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
variables: {
|
||||||
|
groupBy,
|
||||||
|
filter: dateRangeFilter,
|
||||||
|
orderByForRecords,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupByResults = data?.[`${objectMetadataItem.namePlural}GroupBy`];
|
||||||
|
|
||||||
|
const records: ObjectRecord[] = useMemo(() => {
|
||||||
|
if (!isDefined(groupByResults)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const allRecords = groupByResults.flatMap((group: any) =>
|
||||||
|
getRecordsFromRecordConnection<ObjectRecord>({
|
||||||
|
recordConnection: group,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deduplicate records by ID to prevent duplicate keys in React
|
||||||
|
const uniqueRecordsMap = new Map<string, ObjectRecord>();
|
||||||
|
allRecords.forEach((record: ObjectRecord) => {
|
||||||
|
if (!uniqueRecordsMap.has(record.id)) {
|
||||||
|
uniqueRecordsMap.set(record.id, record);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(uniqueRecordsMap.values());
|
||||||
|
}, [groupByResults]);
|
||||||
|
|
||||||
|
const groupByDimensionValues = useMemo(
|
||||||
|
() =>
|
||||||
|
!isDefined(groupByResults)
|
||||||
|
? []
|
||||||
|
: groupByResults.flatMap(
|
||||||
|
(group: any) => group.groupByDimensionValues || [],
|
||||||
|
),
|
||||||
|
[groupByResults],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
records,
|
||||||
|
groupByDimensionValues,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
calendarFieldMetadataItem,
|
||||||
|
};
|
||||||
|
};
|
||||||
+37
-4
@@ -1,11 +1,18 @@
|
|||||||
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
|
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
|
||||||
import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange';
|
import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange';
|
||||||
|
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';
|
||||||
|
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||||
|
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||||
import {
|
import {
|
||||||
|
combineFilters,
|
||||||
computeRecordGqlOperationFilter,
|
computeRecordGqlOperationFilter,
|
||||||
isDefined,
|
isDefined,
|
||||||
|
turnAnyFieldFilterIntoRecordGqlFilter,
|
||||||
} from 'twenty-shared/utils';
|
} from 'twenty-shared/utils';
|
||||||
|
|
||||||
const DATE_RANGE_FILTER_AFTER_ID = 'DATE_RANGE_FILTER_AFTER_ID';
|
const DATE_RANGE_FILTER_AFTER_ID = 'DATE_RANGE_FILTER_AFTER_ID';
|
||||||
@@ -18,6 +25,20 @@ export const useRecordCalendarQueryDateRangeFilter = (selectedDate: Date) => {
|
|||||||
|
|
||||||
const { currentView } = useGetCurrentViewOnly();
|
const { currentView } = useGetCurrentViewOnly();
|
||||||
|
|
||||||
|
const currentRecordFilterGroups = useRecoilComponentValue(
|
||||||
|
currentRecordFilterGroupsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentRecordFilters = useRecoilComponentValue(
|
||||||
|
currentRecordFiltersComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { filterValueDependencies } = useFilterValueDependencies();
|
||||||
|
|
||||||
|
const anyFieldFilterValue = useRecoilComponentValue(
|
||||||
|
anyFieldFilterValueComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isDefined(currentView) ||
|
!isDefined(currentView) ||
|
||||||
!isDefined(currentView.calendarFieldMetadataId)
|
!isDefined(currentView.calendarFieldMetadataId)
|
||||||
@@ -50,13 +71,25 @@ export const useRecordCalendarQueryDateRangeFilter = (selectedDate: Date) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dateRangeFilter = computeRecordGqlOperationFilter({
|
const dateRangeFilter = computeRecordGqlOperationFilter({
|
||||||
filterValueDependencies: {},
|
filterValueDependencies,
|
||||||
recordFilters: [dateRangeFilterAfter, dateRangeFilterBefore],
|
recordFilters: [
|
||||||
recordFilterGroups: [],
|
...currentRecordFilters,
|
||||||
|
dateRangeFilterAfter,
|
||||||
|
dateRangeFilterBefore,
|
||||||
|
],
|
||||||
|
recordFilterGroups: currentRecordFilterGroups,
|
||||||
fields: objectMetadataItem.fields,
|
fields: objectMetadataItem.fields,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { recordGqlOperationFilter: anyFieldFilter } =
|
||||||
|
turnAnyFieldFilterIntoRecordGqlFilter({
|
||||||
|
fields: objectMetadataItem.fields,
|
||||||
|
filterValue: anyFieldFilterValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const combinedFilter = combineFilters([dateRangeFilter, anyFieldFilter]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dateRangeFilter,
|
dateRangeFilter: combinedFilter,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+20
@@ -1,4 +1,5 @@
|
|||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { hasObjectMetadataItemPositionField } from '@/object-metadata/utils/hasObjectMetadataItemPositionField';
|
||||||
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
|
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
|
||||||
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
|
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
|
||||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||||
@@ -60,6 +61,25 @@ export const calendarDayRecordIdsComponentFamilySelector =
|
|||||||
return isSameDay(recordDateObj, dayDate);
|
return isSameDay(recordDateObj, dayDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!objectMetadataItem.isRemote &&
|
||||||
|
hasObjectMetadataItemPositionField(objectMetadataItem)
|
||||||
|
) {
|
||||||
|
return recordIds.sort((a, b) => {
|
||||||
|
const recordA = get(recordStoreFamilyState(a));
|
||||||
|
const recordB = get(recordStoreFamilyState(b));
|
||||||
|
|
||||||
|
const positionA = recordA?.position;
|
||||||
|
const positionB = recordB?.position;
|
||||||
|
|
||||||
|
if (!isDefined(positionA) && !isDefined(positionB)) return 0;
|
||||||
|
if (!isDefined(positionA)) return -1;
|
||||||
|
if (!isDefined(positionB)) return 1;
|
||||||
|
|
||||||
|
return positionA - positionB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return recordIds;
|
return recordIds;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+29
-10
@@ -58,18 +58,37 @@ export const useHandleDragOneCalendarCard = () => {
|
|||||||
.getLoadable(calendarDayRecordIdsSelector(destinationDate))
|
.getLoadable(calendarDayRecordIdsSelector(destinationDate))
|
||||||
.getValue() as string[];
|
.getValue() as string[];
|
||||||
|
|
||||||
const recordsWithPosition = extractRecordPositions(
|
const targetDayIsEmpty = destinationRecordIds.length === 0;
|
||||||
destinationRecordIds,
|
|
||||||
snapshot,
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetRecordId = destinationRecordIds[destinationIndex];
|
let newPosition: number;
|
||||||
|
|
||||||
const newPosition = computeNewPositionOfDraggedRecord({
|
if (targetDayIsEmpty) {
|
||||||
arrayOfRecordsWithPosition: recordsWithPosition,
|
newPosition = 1;
|
||||||
idOfItemToMove: recordId,
|
} else {
|
||||||
idOfTargetItem: targetRecordId,
|
const recordsWithPosition = extractRecordPositions(
|
||||||
});
|
destinationRecordIds,
|
||||||
|
snapshot,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDroppedAfterList =
|
||||||
|
destinationIndex >= recordsWithPosition.length;
|
||||||
|
|
||||||
|
const targetRecord = isDroppedAfterList
|
||||||
|
? recordsWithPosition.at(-1)
|
||||||
|
: recordsWithPosition.at(destinationIndex);
|
||||||
|
|
||||||
|
if (!isDefined(targetRecord)) {
|
||||||
|
throw new Error(
|
||||||
|
`targetRecord cannot be found in passed recordsWithPosition, this should not happen.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
newPosition = computeNewPositionOfDraggedRecord({
|
||||||
|
arrayOfRecordsWithPosition: recordsWithPosition,
|
||||||
|
idOfItemToMove: recordId,
|
||||||
|
idOfTargetItem: targetRecord.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const targetDate = parse(destinationDate, 'yyyy-MM-dd', new Date());
|
const targetDate = parse(destinationDate, 'yyyy-MM-dd', new Date());
|
||||||
const currentFieldValue = record[calendarFieldMetadata.name];
|
const currentFieldValue = record[calendarFieldMetadata.name];
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||||
|
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
|
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||||
|
import { capitalize } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const generateGroupByRecordsQuery = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences,
|
||||||
|
objectPermissionsByObjectMetadataId,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
computeReferences?: boolean;
|
||||||
|
objectPermissionsByObjectMetadataId: Record<
|
||||||
|
string,
|
||||||
|
ObjectPermissions & { objectMetadataId: string }
|
||||||
|
>;
|
||||||
|
}) => gql`
|
||||||
|
query GroupBy${capitalize(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
)}($groupBy: [${capitalize(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
)}GroupByInput!]!, $filter: ${capitalize(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
)}FilterInput, $orderBy: [${capitalize(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
)}OrderByWithGroupByInput!], $orderByForRecords: [${capitalize(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
)}OrderByInput], $viewId: UUID) {
|
||||||
|
${objectMetadataItem.namePlural}GroupBy(
|
||||||
|
groupBy: $groupBy
|
||||||
|
filter: $filter
|
||||||
|
orderBy: $orderBy
|
||||||
|
orderByForRecords: $orderByForRecords
|
||||||
|
viewId: $viewId
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node ${mapObjectMetadataToGraphQLQuery({
|
||||||
|
objectMetadataItems,
|
||||||
|
objectMetadataItem,
|
||||||
|
recordGqlFields,
|
||||||
|
computeReferences,
|
||||||
|
objectPermissionsByObjectMetadataId,
|
||||||
|
})}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
groupByDimensionValues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -6,11 +6,14 @@ export const getQueryIdentifier = ({
|
|||||||
orderBy,
|
orderBy,
|
||||||
limit,
|
limit,
|
||||||
cursorFilter,
|
cursorFilter,
|
||||||
|
groupBy,
|
||||||
}: RecordGqlOperationVariables & {
|
}: RecordGqlOperationVariables & {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
|
groupBy?: Record<string, any>[];
|
||||||
}) =>
|
}) =>
|
||||||
objectNameSingular +
|
objectNameSingular +
|
||||||
JSON.stringify(filter) +
|
JSON.stringify(filter) +
|
||||||
JSON.stringify(orderBy) +
|
JSON.stringify(orderBy) +
|
||||||
limit +
|
limit +
|
||||||
(cursorFilter ? JSON.stringify(cursorFilter) : undefined);
|
(cursorFilter ? JSON.stringify(cursorFilter) : undefined) +
|
||||||
|
(groupBy ? JSON.stringify(groupBy) : '');
|
||||||
|
|||||||
Reference in New Issue
Block a user