Feat/activity optimistic activities (#4009)

* Fix naming

* Fixed cache.evict bug for relation target deletion

* Fixed cascade delete activity targets

* Working version

* Fix

* fix

* WIP

* Fixed optimistic effect target inline cell

* Removed openCreateActivityDrawer v1

* Ok for timeline

* Removed console.log

* Fix update record optimistic effect

* Refactored activity queries into useActivities for everything

* Fixed bugs

* Cleaned

* Fix lint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2024-02-20 14:20:45 +01:00
committed by GitHub
parent 6fb0099eb3
commit 36a6558289
68 changed files with 1435 additions and 630 deletions

View File

@ -11,17 +11,19 @@ export const isObjectRecordConnection = (
objectNameSingular,
)}Connection`;
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
const objectConnectionSchema = z.object({
__typename: z.literal(objectConnectionTypeName),
__typename: z.literal(objectConnectionTypeName).optional(),
edges: z.array(
z.object({
__typename: z.literal(objectEdgeTypeName),
__typename: z.literal(objectEdgeTypeName).optional(),
node: z.object({
id: z.string().uuid(),
}),
}),
),
});
const connectionValidation = objectConnectionSchema.safeParse(value);
return connectionValidation.success;

View File

@ -32,29 +32,25 @@ export const triggerDetachRelationOptimisticEffect = ({
targetRecordFieldValue,
{ isReference, readField },
) => {
const isRelationTargetFieldAnObjectRecordConnection =
isCachedObjectRecordConnection(
sourceObjectNameSingular,
targetRecordFieldValue,
);
if (isRelationTargetFieldAnObjectRecordConnection) {
const relationTargetFieldEdgesWithoutRelationSourceRecordToDetach =
targetRecordFieldValue.edges.filter(
({ node }) => readField('id', node) !== sourceRecordId,
);
return {
...targetRecordFieldValue,
edges: relationTargetFieldEdgesWithoutRelationSourceRecordToDetach,
};
}
const isRelationTargetFieldASingleObjectRecord = isReference(
const isRecordConnection = isCachedObjectRecordConnection(
sourceObjectNameSingular,
targetRecordFieldValue,
);
if (isRelationTargetFieldASingleObjectRecord) {
if (isRecordConnection) {
const nextEdges = targetRecordFieldValue.edges.filter(
({ node }) => readField('id', node) !== sourceRecordId,
);
return {
...targetRecordFieldValue,
edges: nextEdges,
};
}
const isSingleReference = isReference(targetRecordFieldValue);
if (isSingleReference) {
return null;
}

View File

@ -45,41 +45,35 @@ export const triggerUpdateRecordOptimisticEffect = ({
rootQueryCachedResponse,
{ DELETE, readField, storeFieldName, toReference },
) => {
const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
!isCachedObjectRecordConnection(
objectMetadataItem.nameSingular,
rootQueryCachedResponse,
);
const shouldSkip = !isCachedObjectRecordConnection(
objectMetadataItem.nameSingular,
rootQueryCachedResponse,
);
if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) {
if (shouldSkip) {
return rootQueryCachedResponse;
}
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
const rootQueryConnection = rootQueryCachedResponse;
const { fieldArguments: rootQueryVariables } =
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
storeFieldName,
);
const rootQueryCurrentCachedRecordEdges =
readField<CachedObjectRecordEdge[]>(
'edges',
rootQueryCachedObjectRecordConnection,
) ?? [];
const rootQueryCurrentEdges =
readField<CachedObjectRecordEdge[]>('edges', rootQueryConnection) ??
[];
let rootQueryNextCachedRecordEdges = [
...rootQueryCurrentCachedRecordEdges,
];
let rootQueryNextEdges = [...rootQueryCurrentEdges];
const rootQueryFilter = rootQueryVariables?.filter;
const rootQueryOrderBy = rootQueryVariables?.orderBy;
const rootQueryLimit = rootQueryVariables?.first;
const shouldTestThatUpdatedRecordMatchesThisRootQueryFilter =
isDefined(rootQueryFilter);
const shouldTryToMatchFilter = isDefined(rootQueryFilter);
if (shouldTestThatUpdatedRecordMatchesThisRootQueryFilter) {
if (shouldTryToMatchFilter) {
const updatedRecordMatchesThisRootQueryFilter =
isRecordMatchingFilter({
record: updatedRecord,
@ -88,24 +82,27 @@ export const triggerUpdateRecordOptimisticEffect = ({
});
const updatedRecordIndexInRootQueryEdges =
rootQueryCurrentCachedRecordEdges.findIndex(
rootQueryCurrentEdges.findIndex(
(cachedEdge) =>
readField('id', cachedEdge.node) === updatedRecord.id,
);
const updatedRecordFoundInRootQueryEdges =
updatedRecordIndexInRootQueryEdges > -1;
const updatedRecordShouldBeAddedToRootQueryEdges =
updatedRecordMatchesThisRootQueryFilter &&
updatedRecordIndexInRootQueryEdges === -1;
!updatedRecordFoundInRootQueryEdges;
const updatedRecordShouldBeRemovedFromRootQueryEdges =
updatedRecordMatchesThisRootQueryFilter &&
updatedRecordIndexInRootQueryEdges === -1;
!updatedRecordMatchesThisRootQueryFilter &&
updatedRecordFoundInRootQueryEdges;
if (updatedRecordShouldBeAddedToRootQueryEdges) {
const updatedRecordNodeReference = toReference(updatedRecord);
if (isDefined(updatedRecordNodeReference)) {
rootQueryNextCachedRecordEdges.push({
rootQueryNextEdges.push({
__typename: objectEdgeTypeName,
node: updatedRecordNodeReference,
cursor: '',
@ -114,18 +111,15 @@ export const triggerUpdateRecordOptimisticEffect = ({
}
if (updatedRecordShouldBeRemovedFromRootQueryEdges) {
rootQueryNextCachedRecordEdges.splice(
updatedRecordIndexInRootQueryEdges,
1,
);
rootQueryNextEdges.splice(updatedRecordIndexInRootQueryEdges, 1);
}
}
const nextRootQueryEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
if (nextRootQueryEdgesShouldBeSorted) {
rootQueryNextCachedRecordEdges = sortCachedObjectEdges({
edges: rootQueryNextCachedRecordEdges,
if (rootQueryNextEdgesShouldBeSorted) {
rootQueryNextEdges = sortCachedObjectEdges({
edges: rootQueryNextEdges,
orderBy: rootQueryOrderBy,
readCacheField: readField,
});
@ -158,12 +152,12 @@ export const triggerUpdateRecordOptimisticEffect = ({
// the query's result.
// In this case, invalidate the cache entry so it can be re-fetched.
const rootQueryCurrentCachedRecordEdgesLengthIsAtLimit =
rootQueryCurrentCachedRecordEdges.length === rootQueryLimit;
rootQueryCurrentEdges.length === rootQueryLimit;
// If next edges length is under limit, then we can wait for the network response and merge the result
// then in the merge function we could implement this mechanism to limit the number of edges in the cache
const rootQueryNextCachedRecordEdgesLengthIsUnderLimit =
rootQueryNextCachedRecordEdges.length < rootQueryLimit;
rootQueryNextEdges.length < rootQueryLimit;
const shouldDeleteRootQuerySoItCanBeRefetched =
rootQueryCurrentCachedRecordEdgesLengthIsAtLimit &&
@ -174,16 +168,16 @@ export const triggerUpdateRecordOptimisticEffect = ({
}
const rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit =
rootQueryNextCachedRecordEdges.length > rootQueryLimit;
rootQueryNextEdges.length > rootQueryLimit;
if (rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit) {
rootQueryNextCachedRecordEdges.splice(rootQueryLimit);
rootQueryNextEdges.splice(rootQueryLimit);
}
}
return {
...rootQueryCachedObjectRecordConnection,
edges: rootQueryNextCachedRecordEdges,
...rootQueryConnection,
edges: rootQueryNextEdges,
};
},
},

View File

@ -6,7 +6,7 @@ import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effec
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH as CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
@ -74,6 +74,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
return;
}
// TODO: replace this by a relation type check, if it's one to many,
// it's an object record connection (we can still check it though as a safeguard)
const currentFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection(
targetObjectMetadataItem.nameSingular,
@ -104,12 +106,14 @@ export const triggerUpdateRelationsOptimisticEffect = ({
isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0;
if (shouldDetachSourceFromAllTargets) {
const shouldStartByDeletingRelationTargetRecordsFromCache =
CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH.includes(
// TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
// Instead of hardcoding it here
const shouldCascadeDeleteTargetRecords =
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
);
if (shouldStartByDeletingRelationTargetRecordsFromCache) {
if (shouldCascadeDeleteTargetRecords) {
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem: targetObjectMetadataItem,