Fixed Apollo cache bug (#16523)
Fixes https://github.com/twentyhq/twenty/issues/16520 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+49
-15
@@ -2,13 +2,17 @@ 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 CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
|
||||
export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
cache,
|
||||
@@ -32,16 +36,50 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
fields: {
|
||||
[objectMetadataItem.namePlural]: (
|
||||
rootQueryCachedResponse,
|
||||
{ readField },
|
||||
{ readField, storeFieldName },
|
||||
) => {
|
||||
const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
|
||||
const { fieldVariables: rootQueryVariables } =
|
||||
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||
storeFieldName,
|
||||
);
|
||||
|
||||
if (
|
||||
!isObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
)
|
||||
) {
|
||||
return rootQueryCachedResponse;
|
||||
}
|
||||
|
||||
const totalCount = readField<number | undefined>(
|
||||
'totalCount',
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
const recordsMatchingRootQueryFilter = recordsToDestroy.filter(
|
||||
(record) =>
|
||||
isRecordMatchingFilter({
|
||||
record,
|
||||
filter: rootQueryVariables?.filter ?? {},
|
||||
objectMetadataItem,
|
||||
}),
|
||||
);
|
||||
|
||||
const newTotalCount = isDefined(totalCount)
|
||||
? Math.max(totalCount - recordsMatchingRootQueryFilter.length, 0)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
!isObjectRecordConnectionWithRefs(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) {
|
||||
return rootQueryCachedResponse;
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...rootQueryCachedResponse,
|
||||
totalCount: newTotalCount,
|
||||
};
|
||||
}
|
||||
|
||||
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
|
||||
@@ -52,11 +90,6 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
rootQueryCachedObjectRecordConnection,
|
||||
);
|
||||
|
||||
const totalCount = readField<number | undefined>(
|
||||
'totalCount',
|
||||
rootQueryCachedObjectRecordConnection,
|
||||
);
|
||||
|
||||
const nextCachedEdges =
|
||||
cachedEdges?.filter((cachedEdge) => {
|
||||
const nodeId = readField<string>('id', cachedEdge.node);
|
||||
@@ -65,14 +98,15 @@ export const triggerDestroyRecordsOptimisticEffect = ({
|
||||
}) || [];
|
||||
|
||||
if (nextCachedEdges.length === cachedEdges?.length)
|
||||
return rootQueryCachedObjectRecordConnection;
|
||||
return {
|
||||
...rootQueryCachedObjectRecordConnection,
|
||||
totalCount: newTotalCount,
|
||||
};
|
||||
|
||||
return {
|
||||
...rootQueryCachedObjectRecordConnection,
|
||||
edges: nextCachedEdges,
|
||||
totalCount: isDefined(totalCount)
|
||||
? totalCount - recordIdsToDestroy.length
|
||||
: undefined,
|
||||
totalCount: newTotalCount,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
+21
@@ -82,6 +82,26 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const currentRecordIndexInRootQueryEdges = isRecordMatchingFilter({
|
||||
record: currentRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const totalCount = readField<number | undefined>(
|
||||
'totalCount',
|
||||
rootQueryConnection,
|
||||
);
|
||||
|
||||
const newTotalCount = isDefined(totalCount)
|
||||
? Math.max(
|
||||
totalCount +
|
||||
(updatedRecordMatchesThisRootQueryFilter ? 1 : 0) +
|
||||
(currentRecordIndexInRootQueryEdges ? -1 : 0),
|
||||
0,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const updatedRecordIndexInRootQueryEdges =
|
||||
rootQueryCurrentEdges.findIndex(
|
||||
(cachedEdge) =>
|
||||
@@ -131,6 +151,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
return {
|
||||
...rootQueryConnection,
|
||||
edges: rootQueryNextEdges,
|
||||
totalCount: newTotalCount,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
+31
-15
@@ -7,15 +7,18 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
|
||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ApolloCache } from '@apollo/client';
|
||||
import { isArray } from '@sniptt/guards';
|
||||
import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationType,
|
||||
type ObjectPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
computeMorphRelationFieldName,
|
||||
CustomError,
|
||||
@@ -147,12 +150,12 @@ const triggerUpdateRelationOptimisticEffect = ({
|
||||
}
|
||||
|
||||
const currentFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlConnectionEdgesRequired
|
||||
| RecordGqlNode
|
||||
| null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
const updatedFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlConnectionEdgesRequired
|
||||
| RecordGqlNode
|
||||
| null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
@@ -179,6 +182,7 @@ const triggerUpdateRelationOptimisticEffect = ({
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
|
||||
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
|
||||
const gqlFieldNameOnTargetRecord =
|
||||
targetFieldMetadataFullObject.type === FieldMetadataType.RELATION
|
||||
? targetFieldMetadataFullObject.name
|
||||
@@ -189,7 +193,10 @@ const triggerUpdateRelationOptimisticEffect = ({
|
||||
sourceObjectMetadataItem.nameSingular,
|
||||
targetObjectMetadataNamePlural: sourceObjectMetadataItem.namePlural,
|
||||
});
|
||||
if (shouldCascadeDeleteTargetRecords) {
|
||||
if (
|
||||
shouldCascadeDeleteTargetRecords &&
|
||||
targetRecordsToDetachFrom.length > 0
|
||||
) {
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: fullTargetObjectMetadataItem,
|
||||
@@ -198,7 +205,10 @@ const triggerUpdateRelationOptimisticEffect = ({
|
||||
upsertRecordsInStore,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
} else if (isDefined(currentSourceRecord)) {
|
||||
} else if (
|
||||
isDefined(currentSourceRecord) &&
|
||||
targetRecordsToDetachFrom.length > 0
|
||||
) {
|
||||
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
|
||||
triggerDetachRelationOptimisticEffect({
|
||||
cache,
|
||||
@@ -306,12 +316,12 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
}
|
||||
|
||||
const currentFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlConnectionEdgesRequired
|
||||
| RecordGqlNode
|
||||
| null = currentSourceRecord?.[gqlFieldMorphRelation];
|
||||
|
||||
const updatedFieldValueOnSourceRecord:
|
||||
| RecordGqlConnection
|
||||
| RecordGqlConnectionEdgesRequired
|
||||
| RecordGqlNode
|
||||
| null = updatedSourceRecord?.[gqlFieldMorphRelation];
|
||||
|
||||
@@ -338,7 +348,10 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
|
||||
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
if (shouldCascadeDeleteTargetRecords) {
|
||||
if (
|
||||
shouldCascadeDeleteTargetRecords &&
|
||||
targetRecordsToDetachFrom.length > 0
|
||||
) {
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: fullTargetObjectMetadataItem,
|
||||
@@ -347,7 +360,10 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
upsertRecordsInStore,
|
||||
});
|
||||
} else if (isDefined(currentSourceRecord)) {
|
||||
} else if (
|
||||
isDefined(currentSourceRecord) &&
|
||||
targetRecordsToDetachFrom.length > 0
|
||||
) {
|
||||
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
|
||||
triggerDetachRelationOptimisticEffect({
|
||||
cache,
|
||||
@@ -387,7 +403,7 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
|
||||
};
|
||||
|
||||
const extractTargetRecordsFromRelation = (
|
||||
value: RecordGqlConnection | RecordGqlNode | null,
|
||||
value: RecordGqlConnectionEdgesRequired | RecordGqlNode | null,
|
||||
relation: FieldMetadataItemRelation,
|
||||
): RecordGqlNode[] => {
|
||||
// TODO investigate on the root cause of array injection here, should never occurs
|
||||
@@ -399,9 +415,9 @@ const extractTargetRecordsFromRelation = (
|
||||
if (!isDefined(relation)) {
|
||||
throw new Error('Relation found is undefined');
|
||||
}
|
||||
if (isObjectRecordConnection(relation, value)) {
|
||||
return value.edges.map(({ node }) => node);
|
||||
if (relation.type === RelationType.ONE_TO_MANY) {
|
||||
return value.edges.map(({ node }: { node: RecordGqlNode }) => node);
|
||||
}
|
||||
|
||||
return [value];
|
||||
return [value as RecordGqlNode];
|
||||
};
|
||||
|
||||
+5
-2
@@ -1,6 +1,9 @@
|
||||
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type RecordGqlRefConnection = Omit<RecordGqlConnection, 'edges'> & {
|
||||
export type RecordGqlRefConnection = Omit<
|
||||
RecordGqlConnectionEdgesRequired,
|
||||
'edges'
|
||||
> & {
|
||||
edges: RecordGqlRefEdge[];
|
||||
};
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getRecordFromRecordNode should convert a simple record node 1`] = `
|
||||
{
|
||||
"__typename": "Person",
|
||||
"email": "john@example.com",
|
||||
"id": "123",
|
||||
"name": "John Doe",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle array values 1`] = `
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "123",
|
||||
"scores": [
|
||||
100,
|
||||
200,
|
||||
300,
|
||||
],
|
||||
"tags": [
|
||||
"developer",
|
||||
"designer",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle connection fields with edges 1`] = `
|
||||
{
|
||||
"__typename": "Company",
|
||||
"id": "123",
|
||||
"name": "Acme Inc",
|
||||
"people": [
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "456",
|
||||
"name": "John Doe",
|
||||
},
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "789",
|
||||
"name": "Jane Smith",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle deeply nested structures 1`] = `
|
||||
{
|
||||
"__typename": "Workspace",
|
||||
"id": "123",
|
||||
"settings": {
|
||||
"__typename": "WorkspaceSettings",
|
||||
"display": {
|
||||
"__typename": "DisplaySettings",
|
||||
"layout": "compact",
|
||||
"theme": "dark",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle mixed nested objects and connections 1`] = `
|
||||
{
|
||||
"__typename": "Company",
|
||||
"address": {
|
||||
"__typename": "Address",
|
||||
"city": "New York",
|
||||
"country": "USA",
|
||||
},
|
||||
"employees": [
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "456",
|
||||
"name": {
|
||||
"__typename": "FullName",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": "123",
|
||||
"name": "Acme Inc",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle nested object fields 1`] = `
|
||||
{
|
||||
"__typename": "Person",
|
||||
"id": "123",
|
||||
"name": {
|
||||
"__typename": "FullName",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getRecordFromRecordNode should handle null and undefined values 1`] = `
|
||||
{
|
||||
"__typename": "Person",
|
||||
"email": null,
|
||||
"id": "123",
|
||||
"name": "John Doe",
|
||||
"phone": undefined,
|
||||
}
|
||||
`;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { getRecordFromRecordNode } from '../getRecordFromRecordNode';
|
||||
|
||||
describe('getRecordFromRecordNode', () => {
|
||||
it('should convert a simple record node', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Person',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle nested object fields', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Person',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle connection fields with edges', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Company',
|
||||
name: 'Acme Inc',
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '456',
|
||||
__typename: 'Person',
|
||||
name: 'John Doe',
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '789',
|
||||
__typename: 'Person',
|
||||
name: 'Jane Smith',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle null and undefined values', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Person',
|
||||
name: 'John Doe',
|
||||
email: null,
|
||||
phone: undefined,
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle array values', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Person',
|
||||
tags: ['developer', 'designer'],
|
||||
scores: [100, 200, 300],
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle deeply nested structures', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Workspace',
|
||||
settings: {
|
||||
__typename: 'WorkspaceSettings',
|
||||
display: {
|
||||
__typename: 'DisplaySettings',
|
||||
theme: 'dark',
|
||||
layout: 'compact',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle mixed nested objects and connections', () => {
|
||||
const recordNode = {
|
||||
id: '123',
|
||||
__typename: 'Company',
|
||||
name: 'Acme Inc',
|
||||
address: {
|
||||
__typename: 'Address',
|
||||
city: 'New York',
|
||||
country: 'USA',
|
||||
},
|
||||
employees: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '456',
|
||||
__typename: 'Person',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getRecordFromRecordNode({ recordNode });
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
+127
-31
@@ -1,36 +1,132 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { isObjectRecordConnection } from '../isObjectRecordConnection';
|
||||
|
||||
describe('isObjectRecordConnection', () => {
|
||||
const relationDefinitionMap: { [K in RelationType]: boolean } = {
|
||||
[RelationType.ONE_TO_MANY]: true,
|
||||
[RelationType.MANY_TO_ONE]: false,
|
||||
};
|
||||
|
||||
it.each(Object.entries(relationDefinitionMap))(
|
||||
'.$relation',
|
||||
(relation, expected) => {
|
||||
const emptyRecord = {};
|
||||
const result = isObjectRecordConnection(
|
||||
it('should return true for valid connection with edges', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
type: relation,
|
||||
} as NonNullable<FieldMetadataItem['relation']>,
|
||||
emptyRecord,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should throw on unknown relation direction', () => {
|
||||
const emptyRecord = {};
|
||||
expect(() =>
|
||||
isObjectRecordConnection(
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
{
|
||||
direction: 'UNKNOWN_TYPE',
|
||||
} as any,
|
||||
emptyRecord,
|
||||
),
|
||||
).toThrowError();
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
id: '456',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid connection with empty edges array', () => {
|
||||
const storeValue = {
|
||||
__typename: 'CompanyConnection',
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('company', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid connection without edges (optional)', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for incorrect __typename', () => {
|
||||
const storeValue = {
|
||||
__typename: 'WrongConnection',
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for incorrect edge __typename', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WrongEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true regardless of node content', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
name: 'John Doe',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('person', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for null value', () => {
|
||||
const result = isObjectRecordConnection('person', null);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined value', () => {
|
||||
const result = isObjectRecordConnection('person', undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for primitive value', () => {
|
||||
const result = isObjectRecordConnection('person', 'not an object');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle camelCase object names', () => {
|
||||
const storeValue = {
|
||||
__typename: 'CalendarEventConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'CalendarEventEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnection('calendarEvent', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { isObjectRecordConnectionWithRefs } from '../isObjectRecordConnectionWithRefs';
|
||||
|
||||
describe('isObjectRecordConnectionWithRefs', () => {
|
||||
it('should return true for valid connection with edges', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
__ref: 'Person:123',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
__ref: 'Person:456',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid connection with empty edges array', () => {
|
||||
const storeValue = {
|
||||
__typename: 'CompanyConnection',
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('company', storeValue);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for connection without edges (required)', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for incorrect __typename', () => {
|
||||
const storeValue = {
|
||||
__typename: 'WrongConnection',
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for incorrect edge __typename', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WrongEdge',
|
||||
node: {
|
||||
__ref: 'Person:123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for incorrect __ref prefix', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
__ref: 'Company:123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null value', () => {
|
||||
const result = isObjectRecordConnectionWithRefs('person', null);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined value', () => {
|
||||
const result = isObjectRecordConnectionWithRefs('person', undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for primitive value', () => {
|
||||
const result = isObjectRecordConnectionWithRefs('person', 'not an object');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle camelCase object names', () => {
|
||||
const storeValue = {
|
||||
__typename: 'CalendarEventConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'CalendarEventEdge',
|
||||
node: {
|
||||
__ref: 'CalendarEvent:123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs(
|
||||
'calendarEvent',
|
||||
storeValue,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when node is missing __ref', () => {
|
||||
const storeValue = {
|
||||
__typename: 'PersonConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PersonEdge',
|
||||
node: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = isObjectRecordConnectionWithRefs('person', storeValue);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
Vendored
+2
-2
@@ -2,7 +2,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
|
||||
import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename';
|
||||
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
|
||||
import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
@@ -40,5 +40,5 @@ export const getRecordConnectionFromRecords = <T extends ObjectRecord>({
|
||||
}),
|
||||
...(withPageInfo && { pageInfo: getEmptyPageInfo() }),
|
||||
...(withPageInfo && { totalCount: records.length }),
|
||||
} as RecordGqlConnection;
|
||||
} as RecordGqlConnectionEdgesRequired;
|
||||
};
|
||||
|
||||
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const getRecordsFromRecordConnection = <T extends ObjectRecord>({
|
||||
recordConnection,
|
||||
}: {
|
||||
recordConnection: RecordGqlConnection;
|
||||
recordConnection: RecordGqlConnectionEdgesRequired;
|
||||
}): T[] => {
|
||||
return recordConnection?.edges?.map((edge) =>
|
||||
getRecordFromRecordNode<T>({ recordNode: edge.node }),
|
||||
|
||||
+26
-16
@@ -1,20 +1,30 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type StoreValue } from '@apollo/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
export const isObjectRecordConnection = (
|
||||
relation: NonNullable<FieldMetadataItem['relation']>,
|
||||
value: unknown,
|
||||
): value is RecordGqlConnection => {
|
||||
switch (relation.type) {
|
||||
case RelationType.ONE_TO_MANY: {
|
||||
return true;
|
||||
}
|
||||
case RelationType.MANY_TO_ONE:
|
||||
return false;
|
||||
default: {
|
||||
return assertUnreachable(relation.type);
|
||||
}
|
||||
}
|
||||
objectNameSingular: string,
|
||||
storeValue: StoreValue,
|
||||
): storeValue is RecordGqlConnection => {
|
||||
const objectConnectionTypeName = `${capitalize(
|
||||
objectNameSingular,
|
||||
)}Connection`;
|
||||
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
|
||||
const cachedObjectConnectionSchema = z.object({
|
||||
__typename: z.literal(objectConnectionTypeName),
|
||||
edges: z
|
||||
.array(
|
||||
z.object({
|
||||
__typename: z.literal(objectEdgeTypeName),
|
||||
node: z.object(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
const cachedConnectionValidation =
|
||||
cachedObjectConnectionSchema.safeParse(storeValue);
|
||||
|
||||
return cachedConnectionValidation.success;
|
||||
};
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ import { type Nullable } from 'twenty-ui/utilities';
|
||||
|
||||
export type RecordGqlConnection = {
|
||||
__typename?: string;
|
||||
edges: RecordGqlEdge[];
|
||||
pageInfo: {
|
||||
edges?: RecordGqlEdge[];
|
||||
pageInfo?: {
|
||||
__typename?: Nullable<string>;
|
||||
hasNextPage?: Nullable<boolean>;
|
||||
hasPreviousPage?: Nullable<boolean>;
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
|
||||
export type RecordGqlConnectionEdgesRequired = Omit<
|
||||
RecordGqlConnection,
|
||||
'edges'
|
||||
> &
|
||||
Required<Pick<RecordGqlConnection, 'edges'>>;
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type RecordGqlOperationFindDuplicatesResult = {
|
||||
[objectNamePlural: string]: RecordGqlConnection[];
|
||||
[objectNamePlural: string]: RecordGqlConnectionEdgesRequired[];
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type RecordGqlOperationFindManyResult = {
|
||||
[objectNamePlural: string]: RecordGqlConnection;
|
||||
[objectNamePlural: string]: RecordGqlConnectionEdgesRequired;
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type RecordGqlOperationSearchResult = {
|
||||
[objectNamePlural: string]: RecordGqlConnection;
|
||||
[objectNamePlural: string]: RecordGqlConnectionEdgesRequired;
|
||||
};
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { peopleQueryResult } from '~/testing/mock-data/people';
|
||||
@@ -36,7 +36,7 @@ export const query = gql`
|
||||
|
||||
export const mockPageSize = 2;
|
||||
|
||||
export const peopleMockWithIdsOnly: RecordGqlConnection = {
|
||||
export const peopleMockWithIdsOnly: RecordGqlConnectionEdgesRequired = {
|
||||
...peopleQueryResult.people,
|
||||
edges: peopleQueryResult.people.edges.map((edge) => ({
|
||||
...edge,
|
||||
@@ -72,7 +72,7 @@ export const variablesThirdRequest = {
|
||||
};
|
||||
|
||||
const paginateRequestResponse = (
|
||||
response: RecordGqlConnection,
|
||||
response: RecordGqlConnectionEdgesRequired,
|
||||
start: number,
|
||||
end: number,
|
||||
hasNextPage: boolean,
|
||||
@@ -86,7 +86,7 @@ const paginateRequestResponse = (
|
||||
startCursor: response.edges[start].cursor,
|
||||
endCursor: response.edges[end].cursor,
|
||||
hasNextPage,
|
||||
} satisfies RecordGqlConnection['pageInfo'],
|
||||
} satisfies RecordGqlConnectionEdgesRequired['pageInfo'],
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type RecordGqlOperationFindDuplicatesResult } from '@/object-record/graphql/types/RecordGqlOperationFindDuplicatesResults';
|
||||
import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDuplicatesRecordsQuery';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@@ -20,7 +20,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
skip,
|
||||
}: ObjectMetadataItemIdentifier & {
|
||||
objectRecordIds: string[] | undefined;
|
||||
onCompleted?: (data: RecordGqlConnection[]) => void;
|
||||
onCompleted?: (data: RecordGqlConnectionEdgesRequired[]) => void;
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const findDuplicateQueryStateIdentifier = objectNameSingular;
|
||||
@@ -69,7 +69,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
|
||||
const results = useMemo(
|
||||
() =>
|
||||
objectResults?.map((result: RecordGqlConnection) => {
|
||||
objectResults?.map((result: RecordGqlConnectionEdgesRequired) => {
|
||||
return result
|
||||
? (getRecordsFromRecordConnection({
|
||||
recordConnection: result,
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
|
||||
export type CombinedFindManyRecordsQueryResult = {
|
||||
[namePlural: string]: RecordGqlConnection;
|
||||
[namePlural: string]: RecordGqlConnectionEdgesRequired;
|
||||
};
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { type SearchRecord } from '~/generated-metadata/graphql';
|
||||
import { sortMorphItems } from '../sortMorphItems';
|
||||
|
||||
const createMorphItem = (
|
||||
recordId: string,
|
||||
isSelected: boolean,
|
||||
): RecordPickerPickableMorphItem => ({
|
||||
recordId,
|
||||
objectMetadataId: 'object-1',
|
||||
isSelected,
|
||||
isMatchingSearchFilter: true,
|
||||
});
|
||||
|
||||
const createSearchRecord = (recordId: string): SearchRecord => ({
|
||||
recordId,
|
||||
label: `Record ${recordId}`,
|
||||
objectNameSingular: 'person',
|
||||
tsRank: 0,
|
||||
tsRankCD: 0,
|
||||
});
|
||||
|
||||
describe('sortMorphItems', () => {
|
||||
it('should sort selected items before non-selected items', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('1', false),
|
||||
createMorphItem('2', true),
|
||||
createMorphItem('3', false),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [
|
||||
createSearchRecord('1'),
|
||||
createSearchRecord('2'),
|
||||
createSearchRecord('3'),
|
||||
];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result[0].recordId).toBe('2');
|
||||
expect(result[0].isSelected).toBe(true);
|
||||
expect(result[1].isSelected).toBe(false);
|
||||
expect(result[2].isSelected).toBe(false);
|
||||
});
|
||||
|
||||
it('should sort by search record order within non-selected items', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('3', false),
|
||||
createMorphItem('1', false),
|
||||
createMorphItem('2', false),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [
|
||||
createSearchRecord('1'),
|
||||
createSearchRecord('2'),
|
||||
createSearchRecord('3'),
|
||||
];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result.map((item) => item.recordId)).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
it('should sort by search record order within selected items', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('3', true),
|
||||
createMorphItem('1', true),
|
||||
createMorphItem('2', true),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [
|
||||
createSearchRecord('1'),
|
||||
createSearchRecord('2'),
|
||||
createSearchRecord('3'),
|
||||
];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result.map((item) => item.recordId)).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
it('should handle mixed selected and non-selected items with correct ordering', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('4', false),
|
||||
createMorphItem('2', true),
|
||||
createMorphItem('1', false),
|
||||
createMorphItem('3', true),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [
|
||||
createSearchRecord('1'),
|
||||
createSearchRecord('2'),
|
||||
createSearchRecord('3'),
|
||||
createSearchRecord('4'),
|
||||
];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result.map((item) => item.recordId)).toEqual(['2', '3', '1', '4']);
|
||||
expect(result[0].isSelected).toBe(true);
|
||||
expect(result[1].isSelected).toBe(true);
|
||||
expect(result[2].isSelected).toBe(false);
|
||||
expect(result[3].isSelected).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty morphItems array', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [];
|
||||
const searchRecords: SearchRecord[] = [createSearchRecord('1')];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty searchRecords array', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('1', false),
|
||||
createMorphItem('2', true),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
expect(result[0].recordId).toBe('2');
|
||||
expect(result[0].isSelected).toBe(true);
|
||||
});
|
||||
|
||||
it('should place items not present in searchRecords before indexed items', () => {
|
||||
const morphItems: RecordPickerPickableMorphItem[] = [
|
||||
createMorphItem('unknown', false),
|
||||
createMorphItem('1', false),
|
||||
];
|
||||
const searchRecords: SearchRecord[] = [createSearchRecord('1')];
|
||||
|
||||
const result = sortMorphItems(morphItems, searchRecords);
|
||||
|
||||
// Items not in searchRecords get rank -1, so they come before items with rank >= 0
|
||||
expect(result[0].recordId).toBe('unknown');
|
||||
expect(result[1].recordId).toBe('1');
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -1377,7 +1377,7 @@ const baseMockToRecordConnection = {
|
||||
) as any,
|
||||
pageInfo: {},
|
||||
__typename: 'CompanyConnection',
|
||||
} as RecordGqlConnection;
|
||||
} as RecordGqlConnectionEdgesRequired;
|
||||
|
||||
// eslint-disable-next-line @nx/workspace-max-consts-per-file
|
||||
export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = getRecordsFromRecordConnection(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
@@ -1746,7 +1746,7 @@ export const peopleQueryResult = {
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies { people: RecordGqlConnection };
|
||||
} satisfies { people: RecordGqlConnectionEdgesRequired };
|
||||
|
||||
export const allMockPersonRecords = getRecordsFromRecordConnection({
|
||||
recordConnection: peopleQueryResult.people,
|
||||
|
||||
Reference in New Issue
Block a user