Only fetch steps and trigger for the current workflow version (#15003)

Avoid fetching full steps and trigger for versions that are not the
current version. Because those won't be used anyway. Better for
performances.

Only difficulty was for the `createDraft` mutation. I needed to return
the full created version so I can store it in cache and use it as new
`currentVersion`. Otherwise the current version is considered as
incomplete for a short time, since workflow is fetched separately from
the current version.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas Trompette
2025-10-12 18:06:12 +02:00
committed by GitHub
parent 0fb2d66808
commit 7b84db4708
54 changed files with 485 additions and 259 deletions
@@ -1,26 +1,45 @@
import { type ApolloCache, type StoreObject } from '@apollo/client';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { type ObjectPermissions } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
export const triggerAttachRelationOptimisticEffect = ({
cache,
sourceObjectNameSingular,
sourceRecordId,
targetObjectNameSingular,
targetObjectMetadataItem,
fieldNameOnTargetRecord,
targetRecordId,
upsertRecordsInStore,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
}: {
cache: ApolloCache<unknown>;
sourceObjectNameSingular: string;
sourceRecordId: string;
targetObjectNameSingular: string;
targetObjectMetadataItem: Pick<
ObjectMetadataItem,
'fields' | 'nameSingular' | 'id' | 'readableFields'
>;
fieldNameOnTargetRecord: string;
targetRecordId: string;
objectMetadataItems: ObjectMetadataItem[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
upsertRecordsInStore: (records: ObjectRecord[]) => void;
}) => {
const sourceRecordTypeName = capitalize(sourceObjectNameSingular);
const targetRecordTypeName = capitalize(targetObjectNameSingular);
const targetRecordTypeName = capitalize(
targetObjectMetadataItem.nameSingular,
);
const targetRecordCacheId = cache.identify({
id: targetRecordId,
@@ -65,6 +84,19 @@ export const triggerAttachRelationOptimisticEffect = ({
},
];
upsertRecordsInStore([
getRecordFromRecordNode({
recordNode: {
id: targetRecordId,
[fieldNameOnTargetRecord]: {
...targetRecordFieldValue,
edges: nextEdges,
},
__typename: targetRecordTypeName,
},
}),
]);
return {
...targetRecordFieldValue,
edges: nextEdges,
@@ -76,4 +108,18 @@ export const triggerAttachRelationOptimisticEffect = ({
},
},
});
const newCachedRecord = getRecordFromCache({
cache,
objectMetadataItem: targetObjectMetadataItem,
objectMetadataItems,
recordId: targetRecordId,
objectPermissionsByObjectMetadataId,
});
if (!isDefined(newCachedRecord)) {
return;
}
upsertRecordsInStore([newCachedRecord]);
};
@@ -13,6 +13,7 @@ import { type CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObje
import { encodeCursor } from '@/apollo/utils/encodeCursor';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
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';
@@ -33,7 +34,9 @@ type TriggerCreateRecordsOptimisticEffectArgs = {
string,
ObjectPermissions & { objectMetadataId: string }
>;
upsertRecordsInStore: (records: ObjectRecord[]) => void;
};
export const triggerCreateRecordsOptimisticEffect = ({
cache,
objectMetadataItem,
@@ -42,6 +45,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
shouldMatchRootQueryFilter,
checkForRecordInCache = false,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: TriggerCreateRecordsOptimisticEffectArgs) => {
const getRecordNodeFromCache = (recordId: string): RecordGqlNode | null => {
const cachedRecord = getRecordFromCache({
@@ -69,6 +73,8 @@ export const triggerCreateRecordsOptimisticEffect = ({
currentSourceRecord,
updatedSourceRecord: record,
objectMetadataItems,
upsertRecordsInStore,
objectPermissionsByObjectMetadataId,
});
});
@@ -5,6 +5,8 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const triggerDestroyRecordsOptimisticEffect = ({
@@ -12,11 +14,18 @@ export const triggerDestroyRecordsOptimisticEffect = ({
objectMetadataItem,
recordsToDestroy,
objectMetadataItems,
upsertRecordsInStore,
objectPermissionsByObjectMetadataId,
}: {
cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem;
recordsToDestroy: RecordGqlNode[];
objectMetadataItems: ObjectMetadataItem[];
upsertRecordsInStore: (records: ObjectRecord[]) => void;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
}) => {
cache.modify<StoreObject>({
fields: {
@@ -75,6 +84,8 @@ export const triggerDestroyRecordsOptimisticEffect = ({
currentSourceRecord: recordToDestroy,
updatedSourceRecord: null,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
});
cache.evict({ id: cache.identify(recordToDestroy) });
@@ -1,24 +1,42 @@
import { type ApolloCache, type StoreObject } from '@apollo/client';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
import { capitalize } from 'twenty-shared/utils';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { type ObjectPermissions } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
export const triggerDetachRelationOptimisticEffect = ({
cache,
sourceObjectNameSingular,
sourceRecordId,
targetObjectNameSingular,
targetObjectMetadataItem,
fieldNameOnTargetRecord,
targetRecordId,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: {
cache: ApolloCache<unknown>;
sourceObjectNameSingular: string;
sourceRecordId: string;
targetObjectNameSingular: string;
targetObjectMetadataItem: Pick<
ObjectMetadataItem,
'fields' | 'nameSingular' | 'id' | 'readableFields'
>;
fieldNameOnTargetRecord: string;
targetRecordId: string;
objectMetadataItems: ObjectMetadataItem[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
upsertRecordsInStore: (records: ObjectRecord[]) => void;
}) => {
const targetRecordTypeName = capitalize(targetObjectNameSingular);
const targetRecordTypeName = capitalize(
targetObjectMetadataItem.nameSingular,
);
const targetRecordCacheId = cache.identify({
id: targetRecordId,
@@ -58,4 +76,18 @@ export const triggerDetachRelationOptimisticEffect = ({
},
},
});
const newCachedRecord = getRecordFromCache({
cache,
objectMetadataItem: targetObjectMetadataItem,
objectMetadataItems,
recordId: targetRecordId,
objectPermissionsByObjectMetadataId,
});
if (!isDefined(newCachedRecord)) {
return;
}
upsertRecordsInStore([newCachedRecord]);
};
@@ -9,9 +9,10 @@ import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
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 { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
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';
// TODO: add extensive unit tests for this function
// That will also serve as documentation
export const triggerUpdateRecordOptimisticEffect = ({
@@ -20,12 +21,19 @@ export const triggerUpdateRecordOptimisticEffect = ({
currentRecord,
updatedRecord,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: {
cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem;
currentRecord: RecordGqlNode;
updatedRecord: RecordGqlNode;
objectMetadataItems: ObjectMetadataItem[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
upsertRecordsInStore: (records: ObjectRecord[]) => void;
}) => {
triggerUpdateRelationsOptimisticEffect({
cache,
@@ -33,6 +41,8 @@ export const triggerUpdateRecordOptimisticEffect = ({
currentSourceRecord: currentRecord,
updatedSourceRecord: updatedRecord,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
});
cache.modify<StoreObject>({
@@ -9,8 +9,10 @@ import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
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 { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
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';
// TODO: add extensive unit tests for this function
// That will also serve as documentation
@@ -20,12 +22,19 @@ export const triggerUpdateRecordOptimisticEffectByBatch = ({
currentRecords,
updatedRecords,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: {
cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem;
currentRecords: RecordGqlNode[];
updatedRecords: RecordGqlNode[];
objectMetadataItems: ObjectMetadataItem[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
upsertRecordsInStore: (records: ObjectRecord[]) => void;
}) => {
for (const [index, currentRecord] of currentRecords.entries()) {
triggerUpdateRelationsOptimisticEffect({
@@ -34,6 +43,8 @@ export const triggerUpdateRecordOptimisticEffectByBatch = ({
currentSourceRecord: currentRecord,
updatedSourceRecord: updatedRecords[index],
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
});
}
@@ -12,9 +12,10 @@ import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGq
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 } from 'twenty-shared/types';
import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types';
import {
computeMorphRelationFieldName,
CustomError,
@@ -28,13 +29,21 @@ type TriggerUpdateRelationsOptimisticEffectArgs = {
currentSourceRecord: RecordGqlNode | null;
updatedSourceRecord: RecordGqlNode | null;
objectMetadataItems: ObjectMetadataItem[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
upsertRecordsInStore: (records: ObjectRecord[]) => void;
};
export const triggerUpdateRelationsOptimisticEffect = ({
cache,
sourceObjectMetadataItem,
currentSourceRecord,
updatedSourceRecord,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: TriggerUpdateRelationsOptimisticEffectArgs) => {
const isDeletion =
isDefined(updatedSourceRecord) &&
@@ -57,6 +66,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
sourceObjectMetadataItem,
cache,
isDeletion,
upsertRecordsInStore,
objectPermissionsByObjectMetadataId,
});
}
@@ -69,6 +80,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
sourceObjectMetadataItem,
cache,
isDeletion,
upsertRecordsInStore,
objectPermissionsByObjectMetadataId,
});
}
});
@@ -82,6 +95,8 @@ const triggerUpdateRelationOptimisticEffect = ({
sourceObjectMetadataItem,
cache,
isDeletion,
upsertRecordsInStore,
objectPermissionsByObjectMetadataId,
}: {
fieldMetadataItemOnSourceRecord: FieldMetadataItem;
updatedSourceRecord: RecordGqlNode | null;
@@ -90,6 +105,11 @@ const triggerUpdateRelationOptimisticEffect = ({
sourceObjectMetadataItem: ObjectMetadataItem;
cache: ApolloCache<unknown>;
isDeletion: boolean;
upsertRecordsInStore: (records: ObjectRecord[]) => void;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
}) => {
const fieldDoesNotExist =
isDefined(updatedSourceRecord) &&
@@ -175,6 +195,8 @@ const triggerUpdateRelationOptimisticEffect = ({
objectMetadataItem: fullTargetObjectMetadataItem,
recordsToDestroy: targetRecordsToDetachFrom,
objectMetadataItems,
upsertRecordsInStore,
objectPermissionsByObjectMetadataId,
});
} else if (isDefined(currentSourceRecord)) {
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
@@ -183,8 +205,11 @@ const triggerUpdateRelationOptimisticEffect = ({
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: currentSourceRecord.id,
fieldNameOnTargetRecord: gqlFieldNameOnTargetRecord,
targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetObjectMetadataItem: fullTargetObjectMetadataItem,
targetRecordId: targetRecordToDetachFrom.id,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
});
});
}
@@ -201,8 +226,11 @@ const triggerUpdateRelationOptimisticEffect = ({
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: updatedSourceRecord.id,
fieldNameOnTargetRecord: gqlFieldNameOnTargetRecord,
targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetObjectMetadataItem: fullTargetObjectMetadataItem,
targetRecordId: targetRecordToAttachTo.id,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}),
);
}
@@ -216,6 +244,8 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
sourceObjectMetadataItem,
cache,
isDeletion,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: {
fieldMetadataItemOnSourceRecord: FieldMetadataItem;
updatedSourceRecord: RecordGqlNode | null;
@@ -224,6 +254,11 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
sourceObjectMetadataItem: ObjectMetadataItem;
cache: ApolloCache<unknown>;
isDeletion: boolean;
upsertRecordsInStore: (records: ObjectRecord[]) => void;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
}) => {
const morphRelations = fieldMetadataItemOnSourceRecord.morphRelations;
if (!morphRelations) {
@@ -309,6 +344,8 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
objectMetadataItem: fullTargetObjectMetadataItem,
recordsToDestroy: targetRecordsToDetachFrom,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
});
} else if (isDefined(currentSourceRecord)) {
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
@@ -317,8 +354,11 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: currentSourceRecord.id,
fieldNameOnTargetRecord: targetFieldMetadata.name,
targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetObjectMetadataItem: fullTargetObjectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
targetRecordId: targetRecordToDetachFrom.id,
upsertRecordsInStore,
});
});
}
@@ -335,8 +375,11 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: updatedSourceRecord.id,
fieldNameOnTargetRecord: targetFieldMetadata.name,
targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetObjectMetadataItem: fullTargetObjectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
targetRecordId: targetRecordToAttachTo.id,
upsertRecordsInStore,
}),
);
}