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

@ -0,0 +1,32 @@
import { useInjectIntoActivitiesQuery } from '@/activities/hooks/useInjectIntoActivitiesQuery';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
export const useInjectIntoTimelineActivitiesQueries = () => {
const { injectActivitiesQueries } = useInjectIntoActivitiesQuery();
const injectIntoTimelineActivitiesQueries = ({
activityToInject,
activityTargetsToInject,
timelineTargetableObject,
}: {
activityToInject: Activity;
activityTargetsToInject: ActivityTarget[];
timelineTargetableObject: ActivityTargetableObject;
}) => {
injectActivitiesQueries({
activitiesFilters: {},
activitiesOrderByVariables: {
createdAt: 'DescNullsFirst',
},
activityTargetsToInject,
activityToInject,
targetableObjects: [timelineTargetableObject],
});
};
return {
injectIntoTimelineActivitiesQueries,
};
};

View File

@ -1,124 +0,0 @@
import { isNonEmptyString } from '@sniptt/guards';
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
export const useInjectIntoTimelineActivitiesQueryAfterDrawerMount = () => {
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const {
upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache,
} = useUpsertFindManyRecordsQueryInCache({
objectMetadataItem: objectMetadataItemActivity,
});
const { objectMetadataItem: objectMetadataItemActivityTarget } =
useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const {
readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache,
} = useReadFindManyRecordsQueryInCache({
objectMetadataItem: objectMetadataItemActivityTarget,
});
const {
readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache,
} = useReadFindManyRecordsQueryInCache({
objectMetadataItem: objectMetadataItemActivity,
});
const {
upsertFindManyRecordsQueryInCache:
overwriteFindManyActivityTargetsQueryInCache,
} = useUpsertFindManyRecordsQueryInCache({
objectMetadataItem: objectMetadataItemActivityTarget,
});
const injectIntoTimelineActivitiesQueryAfterDrawerMount = ({
activityToInject,
activityTargetsToInject,
timelineTargetableObject,
}: {
activityToInject: Activity;
activityTargetsToInject: ActivityTarget[];
timelineTargetableObject: ActivityTargetableObject;
}) => {
const newActivity = {
...activityToInject,
__typename: 'Activity',
};
const targetObjectFieldName = getActivityTargetObjectFieldIdName({
nameSingular: timelineTargetableObject.targetObjectNameSingular,
});
const activitiyTargetsForTargetableObjectQueryVariables = {
filter: {
[targetObjectFieldName]: {
eq: timelineTargetableObject.id,
},
},
};
const existingActivityTargetsForTargetableObject =
readFindManyActivityTargetsQueryInCache({
queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
});
const newActivityTargetsForTargetableObject = [
...existingActivityTargetsForTargetableObject,
...activityTargetsToInject,
];
const existingActivityIds = existingActivityTargetsForTargetableObject
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString);
const timelineActivitiesQueryVariablesBeforeDrawerMount =
makeTimelineActivitiesQueryVariables({
activityIds: existingActivityIds,
});
const existingActivities = readFindManyActivitiesQueryInCache({
queryVariables: timelineActivitiesQueryVariablesBeforeDrawerMount,
});
const activityIdsAfterDrawerMount = [
...existingActivityIds,
newActivity.id,
];
const timelineActivitiesQueryVariablesAfterDrawerMount =
makeTimelineActivitiesQueryVariables({
activityIds: activityIdsAfterDrawerMount,
});
overwriteFindManyActivityTargetsQueryInCache({
objectRecordsToOverwrite: newActivityTargetsForTargetableObject,
queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
});
const newActivities = [newActivity, ...existingActivities];
overwriteFindManyActivitiesInCache({
objectRecordsToOverwrite: newActivities,
queryVariables: timelineActivitiesQueryVariablesAfterDrawerMount,
});
};
return {
injectIntoTimelineActivitiesQueryAfterDrawerMount,
};
};

View File

@ -0,0 +1,138 @@
import { useRecoilValue } from 'recoil';
import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries';
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
export const useRemoveFromTimelineActivitiesQueries = () => {
const timelineTargetableObject = useRecoilValue(
timelineTargetableObjectState,
);
// const { objectMetadataItem: objectMetadataItemActivity } =
// useObjectMetadataItemOnly({
// objectNameSingular: CoreObjectNameSingular.Activity,
// });
// const {
// upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache,
// } = useUpsertFindManyRecordsQueryInCache({
// objectMetadataItem: objectMetadataItemActivity,
// });
// const { objectMetadataItem: objectMetadataItemActivityTarget } =
// useObjectMetadataItemOnly({
// objectNameSingular: CoreObjectNameSingular.ActivityTarget,
// });
// const {
// readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache,
// } = useReadFindManyRecordsQueryInCache({
// objectMetadataItem: objectMetadataItemActivityTarget,
// });
// const {
// readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache,
// } = useReadFindManyRecordsQueryInCache({
// objectMetadataItem: objectMetadataItemActivity,
// });
// const {
// upsertFindManyRecordsQueryInCache:
// overwriteFindManyActivityTargetsQueryInCache,
// } = useUpsertFindManyRecordsQueryInCache({
// objectMetadataItem: objectMetadataItemActivityTarget,
// });
const { removeFromActivitiesQueries } = useRemoveFromActivitiesQueries();
const removeFromTimelineActivitiesQueries = ({
activityIdToRemove,
activityTargetsToRemove,
}: {
activityIdToRemove: string;
activityTargetsToRemove: ActivityTarget[];
}) => {
if (!timelineTargetableObject) {
throw new Error('Timeline targetable object is not defined');
}
removeFromActivitiesQueries({
activityIdToRemove,
activityTargetsToRemove,
targetableObjects: [timelineTargetableObject],
activitiesFilters: {},
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
});
// const targetObjectFieldName = getActivityTargetObjectFieldIdName({
// nameSingular: timelineTargetableObject.targetObjectNameSingular,
// });
// const activitiyTargetsForTargetableObjectQueryVariables = {
// filter: {
// [targetObjectFieldName]: {
// eq: timelineTargetableObject.id,
// },
// },
// };
// const existingActivityTargetsForTargetableObject =
// readFindManyActivityTargetsQueryInCache({
// queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
// });
// const newActivityTargetsForTargetableObject = isNonEmptyArray(
// activityTargetsToRemove,
// )
// ? existingActivityTargetsForTargetableObject.filter(
// (existingActivityTarget) =>
// activityTargetsToRemove.some(
// (activityTargetToRemove) =>
// activityTargetToRemove.id !== existingActivityTarget.id,
// ),
// )
// : existingActivityTargetsForTargetableObject;
// overwriteFindManyActivityTargetsQueryInCache({
// objectRecordsToOverwrite: newActivityTargetsForTargetableObject,
// queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
// });
// const existingActivityIds = existingActivityTargetsForTargetableObject
// ?.map((activityTarget) => activityTarget.activityId)
// .filter(isNonEmptyString);
// const timelineActivitiesQueryVariablesBeforeDrawerMount =
// makeTimelineActivitiesQueryVariables({
// activityIds: existingActivityIds,
// });
// const existingActivities = readFindManyActivitiesQueryInCache({
// queryVariables: timelineActivitiesQueryVariablesBeforeDrawerMount,
// });
// const activityIdsAfterRemoval = existingActivityIds.filter(
// (existingActivityId) => existingActivityId !== activityIdToRemove,
// );
// const timelineActivitiesQueryVariablesAfterRemoval =
// makeTimelineActivitiesQueryVariables({
// activityIds: activityIdsAfterRemoval,
// });
// const newActivities = existingActivities
// .filter((existingActivity) => existingActivity.id !== activityIdToRemove)
// .toSorted(sortObjectRecordByDateField('createdAt', 'DescNullsFirst'));
// overwriteFindManyActivitiesInCache({
// objectRecordsToOverwrite: newActivities,
// queryVariables: timelineActivitiesQueryVariablesAfterRemoval,
// });
};
return {
removeFromTimelineActivitiesQueries,
};
};

View File

@ -1,18 +1,37 @@
import { useEffect, useState } from 'react';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { sortByAscString } from '~/utils/array/sortByAscString';
import { isDefined } from '~/utils/isDefined';
export const useTimelineActivities = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
const [, setTimelineTargetableObject] = useRecoilState(
timelineTargetableObjectState,
);
useEffect(() => {
if (isDefined(targetableObject)) {
setTimelineTargetableObject(targetableObject);
}
}, [targetableObject, setTimelineTargetableObject]);
const {
activityTargets,
loadingActivityTargets,
@ -23,9 +42,14 @@ export const useTimelineActivities = ({
const [initialized, setInitialized] = useState(false);
const activityIds = activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString);
const activityIds = Array.from(
new Set(
activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString)
.toSorted(sortByAscString),
),
);
const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables(
{
@ -33,17 +57,30 @@ export const useTimelineActivities = ({
},
);
const { records: activities, loading: loadingActivities } =
const { records: activitiesWithConnection, loading: loadingActivities } =
useFindManyRecords<Activity>({
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
objectNameSingular: CoreObjectNameSingular.Activity,
filter: timelineActivitiesQueryVariables.filter,
orderBy: timelineActivitiesQueryVariables.orderBy,
onCompleted: () => {
if (!initialized) {
setInitialized(true);
}
},
onCompleted: useRecoilCallback(
({ set }) =>
(data) => {
if (!initialized) {
setInitialized(true);
}
const activities = getRecordsFromRecordConnection({
recordConnection: data,
});
for (const activity of activities) {
set(recordStoreFamilyState(activity.id), activity);
}
},
[initialized],
),
depth: 3,
});
const noActivityTargets =
@ -57,6 +94,11 @@ export const useTimelineActivities = ({
const loading = loadingActivities || loadingActivityTargets;
const activities = activitiesWithConnection
?.map(makeActivityWithoutConnection as any)
.map(({ activity }: any) => activity as any)
.filter(isDefined);
return {
activities,
loading,