Activity injection into Apollo cache (#3665)

- Created addRecordInCache to inject a record in Apollo cache and inject single read query on this record
- Created createOneRecordInCache and createManyRecordsInCache that uses this addRecordInCache
- Created useOpenCreateActivityDrawerV2 hook to create an activity in cache and inject it into all other relevant requests in the app before opening activity drawer
- Refactored DEFAULT_SEARCH_REQUEST_LIMIT constant and hardcoded arbitrary request limits
- Added Apollo dev logs to see errors in the console when manipulating cache
This commit is contained in:
Lucas Bordeau
2024-01-29 16:12:52 +01:00
committed by GitHub
parent 64d0e15ada
commit 3b458d5207
57 changed files with 1160 additions and 190 deletions

View File

@ -1,4 +1,3 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
@ -15,41 +14,40 @@ export const useActivityTargetObjectRecords = ({
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { records: activityTargets } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: {
activityId: {
eq: activityId,
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: {
activityId: {
eq: activityId,
},
},
},
});
});
const activityTargetObjectRecords = useMemo(() => {
return activityTargets
.map<Nullable<ActivityTargetObjectRecord>>((activityTarget) => {
const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
!objectMetadataItem.isSystem,
);
const activityTargetObjectRecords = activityTargets
.map<Nullable<ActivityTargetObjectRecord>>((activityTarget) => {
const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
!objectMetadataItem.isSystem,
);
if (!correspondingObjectMetadataItem) {
return null;
}
if (!correspondingObjectMetadataItem) {
return null;
}
return {
activityTargetRecord: activityTarget,
targetObjectRecord:
activityTarget[correspondingObjectMetadataItem.nameSingular],
targetObjectMetadataItem: correspondingObjectMetadataItem,
targetObjectNameSingular:
correspondingObjectMetadataItem.nameSingular,
};
})
.filter(isDefined);
}, [activityTargets, objectMetadataItems]);
return {
activityTargetRecord: activityTarget,
targetObjectRecord:
activityTarget[correspondingObjectMetadataItem.nameSingular],
targetObjectMetadataItem: correspondingObjectMetadataItem,
targetObjectNameSingular: correspondingObjectMetadataItem.nameSingular,
};
})
.filter(isDefined);
return {
activityTargetObjectRecords,
loadingActivityTargets,
};
};

View File

@ -1,3 +1,5 @@
import { useState } from 'react';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
@ -13,17 +15,26 @@ export const useActivityTargets = ({
nameSingular: targetableObject.targetObjectNameSingular,
});
const { records: activityTargets } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
const [initialized, setInitialized] = useState(false);
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
},
},
},
skip: !targetableObject.id,
});
onCompleted: () => {
if (!initialized) {
setInitialized(true);
}
},
});
return {
activityTargets: activityTargets as ActivityTarget[],
loadingActivityTargets,
initialized,
};
};

View File

@ -1,76 +0,0 @@
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
export const useHandleCheckableActivityTargetChange = ({
activityId,
currentActivityTargets,
}: {
activityId: string;
currentActivityTargets: any[];
}) => {
const { createOneRecord: createOneActivityTarget } =
useCreateOneRecord<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
return async (
entityValues: Record<string, boolean>,
entitiesToSelect: any,
selectedEntities: any,
) => {
if (!activityId) {
return;
}
const currentActivityTargetRecordIds = currentActivityTargets.map(
({ companyId, personId }) => companyId ?? personId ?? '',
);
const idsToAdd = Object.entries(entityValues)
.filter(
([recordId, value]) =>
value && !currentActivityTargetRecordIds.includes(recordId),
)
.map(([id, _]) => id);
const idsToDelete = Object.entries(entityValues)
.filter(([_, value]) => !value)
.map(([id, _]) => id);
if (idsToAdd.length) {
idsToAdd.forEach((id) => {
const entityFromToSelect = entitiesToSelect.filter(
(entity: any) => entity.id === id,
).length
? entitiesToSelect.filter((entity: any) => entity.id === id)[0]
: null;
const entityFromSelected = selectedEntities.filter(
(entity: any) => entity.id === id,
).length
? selectedEntities.filter((entity: any) => entity.id === id)[0]
: null;
const entity = entityFromToSelect ?? entityFromSelected;
createOneActivityTarget?.({
activityId: activityId,
companyId: entity.record.__typename === 'Company' ? entity.id : null,
personId: entity.record.__typename === 'Person' ? entity.id : null,
});
});
}
if (idsToDelete.length) {
idsToDelete.forEach((id) => {
const currentActivityTargetId = currentActivityTargets.filter(
({ companyId, personId }) => companyId === id || personId === id,
)[0].id;
deleteOneActivityTarget?.(currentActivityTargetId);
});
}
};
};

View File

@ -0,0 +1,46 @@
import { useApolloClient } from '@apollo/client';
import { Activity } from '@/activities/types/Activity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
import { getCacheReferenceFromRecord } from '@/object-record/cache/utils/getCacheReferenceFromRecord';
export const useModifyActivityOnActivityTargetsCache = () => {
const { objectMetadataItem: objectMetadataItemActivityTarget } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const modifyActivityTargetFromCache = useModifyRecordFromCache({
objectMetadataItem: objectMetadataItemActivityTarget,
});
const apolloClient = useApolloClient();
const modifyActivityOnActivityTargetsCache = ({
activityTargetIds,
activity,
}: {
activityTargetIds: string[];
activity: Activity;
}) => {
for (const activityTargetId of activityTargetIds) {
modifyActivityTargetFromCache(activityTargetId, {
activity: () => {
const newActivityReference = getCacheReferenceFromRecord({
apolloClient,
objectNameSingular: CoreObjectNameSingular.Activity,
record: activity,
});
return newActivityReference;
},
});
}
};
return {
modifyActivityOnActivityTargetsCache,
};
};

View File

@ -0,0 +1,51 @@
import { useApolloClient } from '@apollo/client';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
import { getCachedRecordEdgesFromRecords } from '@/object-record/cache/utils/getCachedRecordEdgesFromRecords';
export const useModifyActivityTargetsOnActivityCache = () => {
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const modifyActivityFromCache = useModifyRecordFromCache({
objectMetadataItem: objectMetadataItemActivity,
});
const apolloClient = useApolloClient();
const modifyActivityTargetsOnActivityCache = ({
activityId,
activityTargets,
}: {
activityId: string;
activityTargets: ActivityTarget[];
}) => {
modifyActivityFromCache(activityId, {
activityTargets: (
activityTargetsCachedConnection: CachedObjectRecordConnection,
) => {
const newActivityTargetsCachedRecordEdges =
getCachedRecordEdgesFromRecords({
apolloClient,
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
records: activityTargets,
});
return {
...activityTargetsCachedConnection,
edges: newActivityTargetsCachedRecordEdges,
};
},
});
};
return {
modifyActivityTargetsOnActivityCache,
};
};

View File

@ -1,8 +1,10 @@
import { useRecoilCallback } from 'recoil';
import { ActivityType } from '@/activities/types/Activity';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { isDefined } from '~/utils/isDefined';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
@ -27,21 +29,35 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
getSelectedRowIdsSelector(),
);
let activityTargetableEntityArray: ActivityTargetableObject[] =
selectedRowIds.map((id: string) => ({
type: 'Custom',
targetObjectNameSingular: objectNameSingular,
id,
}));
let activityTargetableObjectArray: ActivityTargetableObject[] =
selectedRowIds
.map((recordId: string) => {
const targetObjectRecord = getSnapshotValue(
snapshot,
recordStoreFamilyState(recordId),
);
if (!targetObjectRecord) {
return null;
}
return {
type: 'Custom',
targetObjectNameSingular: objectNameSingular,
id: recordId,
targetObjectRecord,
};
})
.filter(isDefined);
if (relatedEntities) {
activityTargetableEntityArray =
activityTargetableEntityArray.concat(relatedEntities);
activityTargetableObjectArray =
activityTargetableObjectArray.concat(relatedEntities);
}
openCreateActivityDrawer({
type,
targetableObjects: activityTargetableEntityArray,
targetableObjects: activityTargetableObjectArray,
});
},
[openCreateActivityDrawer, getSelectedRowIdsSelector],

View File

@ -0,0 +1,163 @@
import { useCallback } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { useModifyActivityOnActivityTargetsCache } from '@/activities/hooks/useModifyActivityOnActivityTargetCache';
import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache';
import { useWriteActivityTargetsInCache } from '@/activities/hooks/useWriteActivityTargetsInCache';
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
import { useInjectIntoTimelineActivitiesQuery } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery';
import { Activity, ActivityType } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { getActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache';
import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { mapToRecordId } from '@/object-record/utils/mapToObjectId';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState';
import { viewableActivityIdState } from '../states/viewableActivityIdState';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
export const useOpenCreateActivityDrawerV2 = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
const { openRightDrawer } = useRightDrawer();
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
useCreateManyRecordsInCache<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const { createOneRecordInCache: createOneActivityInCache } =
useCreateOneRecordInCache<Activity>({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { record: workspaceMemberRecord } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
objectRecordId: currentWorkspaceMember?.id,
});
const setHotkeyScope = useSetHotkeyScope();
const [, setActivityTargetableEntityArray] = useRecoilState(
activityTargetableEntityArrayState,
);
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
const { activityTargets } = useActivityTargets({
targetableObject,
});
const { injectIntoTimelineActivitiesNextQuery } =
useInjectIntoTimelineActivitiesQuery();
const { injectIntoActivityTargetInlineCellCache } =
useInjectIntoActivityTargetInlineCellCache();
const { injectIntoUseActivityTargets } = useWriteActivityTargetsInCache();
const { modifyActivityTargetsOnActivityCache } =
useModifyActivityTargetsOnActivityCache();
const { modifyActivityOnActivityTargetsCache } =
useModifyActivityOnActivityTargetsCache();
return useCallback(
async ({
type,
targetableObjects,
assigneeId,
}: {
type: ActivityType;
targetableObjects: ActivityTargetableObject[];
assigneeId?: string;
}) => {
const activityId = v4();
const createdActivityInCache = await createOneActivityInCache({
id: activityId,
author: workspaceMemberRecord,
authorId: workspaceMemberRecord?.id,
assignee: !assigneeId ? workspaceMemberRecord : undefined,
assigneeId:
assigneeId ?? isNonEmptyString(workspaceMemberRecord?.id)
? workspaceMemberRecord?.id
: undefined,
type: type,
});
if (!createdActivityInCache) {
return;
}
const activityTargetsToCreate =
getActivityTargetsToCreateFromTargetableObjects({
activityId,
targetableObjects,
});
const createdActivityTargetsInCache =
await createManyActivityTargetsInCache(activityTargetsToCreate);
injectIntoUseActivityTargets({
targetableObject,
activityTargetsToInject: createdActivityTargetsInCache,
});
injectIntoTimelineActivitiesNextQuery({
activityTargets,
activityToInject: createdActivityInCache,
});
injectIntoActivityTargetInlineCellCache({
activityId,
activityTargetsToInject: createdActivityTargetsInCache,
});
modifyActivityTargetsOnActivityCache({
activityId,
activityTargets: createdActivityTargetsInCache,
});
modifyActivityOnActivityTargetsCache({
activityTargetIds: createdActivityTargetsInCache.map(mapToRecordId),
activity: createdActivityInCache,
});
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(activityId);
setActivityTargetableEntityArray(targetableObjects ?? []);
openRightDrawer(RightDrawerPages.CreateActivity);
},
[
openRightDrawer,
setActivityTargetableEntityArray,
createManyActivityTargetsInCache,
setHotkeyScope,
setViewableActivityId,
createOneActivityInCache,
workspaceMemberRecord,
activityTargets,
targetableObject,
injectIntoTimelineActivitiesNextQuery,
injectIntoActivityTargetInlineCellCache,
injectIntoUseActivityTargets,
modifyActivityTargetsOnActivityCache,
modifyActivityOnActivityTargetsCache,
],
);
};

View File

@ -0,0 +1,84 @@
import { useApolloClient } from '@apollo/client';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export const useWriteActivityTargetsInCache = () => {
const apolloClient = useApolloClient();
const {
objectMetadataItem: objectMetadataItemActivityTarget,
findManyRecordsQuery: findManyActivityTargetsQuery,
} = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const injectIntoUseActivityTargets = ({
targetableObject,
activityTargetsToInject,
}: {
targetableObject: Pick<
ActivityTargetableObject,
'id' | 'targetObjectNameSingular'
>;
activityTargetsToInject: ActivityTarget[];
}) => {
const targetObjectFieldName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
const existingActivityTargetsForTargetableObjectQueryResult =
apolloClient.readQuery({
query: findManyActivityTargetsQuery,
variables: {
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
},
},
},
});
const existingActivityTargetsForTargetableObject =
getRecordsFromRecordConnection({
recordConnection: existingActivityTargetsForTargetableObjectQueryResult[
objectMetadataItemActivityTarget.namePlural
] as ObjectRecordConnection<ActivityTarget>,
});
const newActivityTargetsForTargetableObject = [
...existingActivityTargetsForTargetableObject,
...activityTargetsToInject,
];
const newActivityTargetsConnection = getRecordConnectionFromRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
records: newActivityTargetsForTargetableObject,
});
apolloClient.writeQuery({
query: findManyActivityTargetsQuery,
variables: {
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
},
},
},
data: {
[objectMetadataItemActivityTarget.namePlural]:
newActivityTargetsConnection,
},
});
};
return {
injectIntoUseActivityTargets,
};
};

View File

@ -0,0 +1,58 @@
import { useApolloClient } from '@apollo/client';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordConnectionFromEdges } from '@/object-record/cache/utils/getRecordConnectionFromEdges';
import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord';
export const useInjectIntoActivityTargetInlineCellCache = () => {
const apolloClient = useApolloClient();
const {
objectMetadataItem: objectMetadataItemActivityTarget,
findManyRecordsQuery: findManyActivityTargetsQuery,
} = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const injectIntoActivityTargetInlineCellCache = ({
activityId,
activityTargetsToInject,
}: {
activityId: string;
activityTargetsToInject: ActivityTarget[];
}) => {
const newActivityTargetEdgesForCache = activityTargetsToInject.map(
(activityTargetToInject) =>
getRecordEdgeFromRecord({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
record: activityTargetToInject,
}),
);
const newActivityTargetConnectionForCache = getRecordConnectionFromEdges({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
edges: newActivityTargetEdgesForCache,
});
apolloClient.writeQuery({
query: findManyActivityTargetsQuery,
variables: {
filter: {
activityId: {
eq: activityId,
},
},
},
data: {
[objectMetadataItemActivityTarget.namePlural]:
newActivityTargetConnectionForCache,
},
});
};
return {
injectIntoActivityTargetInlineCellCache,
};
};

View File

@ -14,6 +14,7 @@ export const PageAddTaskButton = ({
const { selectedFilter } = useFilterDropdown({
filterDropdownId: filterDropdownId,
});
const openCreateActivity = useOpenCreateActivityDrawer();
const handleClick = () => {

View File

@ -1,14 +1,9 @@
import React from 'react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton';
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { Activity } from '@/activities/types/Activity';
import { useTimelineActivities } from '@/activities/timeline/hooks/useTimelineActivities';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { TimelineItemsContainer } from './TimelineItemsContainer';
@ -55,26 +50,22 @@ export const Timeline = ({
}: {
targetableObject: ActivityTargetableObject;
}) => {
const { activityTargets } = useActivityTargets({ targetableObject });
const { records: activities } = useFindManyRecords({
skip: !activityTargets?.length,
objectNameSingular: CoreObjectNameSingular.Activity,
filter: {
id: {
in: activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString),
},
},
orderBy: {
createdAt: 'AscNullsFirst',
},
const { activities, initialized } = useTimelineActivities({
targetableObject,
});
const openCreateActivity = useOpenCreateActivityDrawer();
if (!activities.length) {
const showEmptyState = initialized && activities.length === 0;
const showLoadingState = !initialized;
if (showLoadingState) {
// TODO: Display a beautiful loading page
return <></>;
}
if (showEmptyState) {
return (
<StyledTimelineEmptyContainer>
<StyledEmptyTimelineTitle>No activity yet</StyledEmptyTimelineTitle>
@ -99,7 +90,7 @@ export const Timeline = ({
return (
<StyledMainContainer>
<TimelineItemsContainer activities={activities as Activity[]} />
<TimelineItemsContainer activities={activities} />
</StyledMainContainer>
);
};

View File

@ -0,0 +1,89 @@
import { useApolloClient } from '@apollo/client';
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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export const useInjectIntoTimelineActivitiesQuery = () => {
const apolloClient = useApolloClient();
const {
objectMetadataItem: objectMetadataItemActivity,
findManyRecordsQuery: findManyActivitiesQuery,
} = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const injectIntoTimelineActivitiesQuery = ({
activityTargets,
activityToInject,
}: {
activityTargets: ActivityTarget[];
activityToInject: Activity;
}) => {
const activityIds = activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString);
const timelineActivitiesQueryVariables =
makeTimelineActivitiesQueryVariables({
activityIds,
});
const exitistingActivitiesQueryResult = apolloClient.readQuery({
query: findManyActivitiesQuery,
variables: timelineActivitiesQueryVariables,
});
const extistingActivities = exitistingActivitiesQueryResult
? getRecordsFromRecordConnection({
recordConnection: exitistingActivitiesQueryResult[
objectMetadataItemActivity.namePlural
] as ObjectRecordConnection<Activity>,
})
: [];
const newActivity = {
...activityToInject,
__typename: 'Activity',
};
const newActivitiesSortedAsActivitiesQuery = [
newActivity,
...extistingActivities,
];
const newActivityIdsSortedAsActivityTargetsQuery = [
...extistingActivities,
newActivity,
].map((activity) => activity.id);
const newTimelineActivitiesQueryVariables =
makeTimelineActivitiesQueryVariables({
activityIds: newActivityIdsSortedAsActivityTargetsQuery,
});
const newActivityConnectionForCache = getRecordConnectionFromRecords({
objectNameSingular: CoreObjectNameSingular.Activity,
records: newActivitiesSortedAsActivitiesQuery,
});
apolloClient.writeQuery({
query: findManyActivitiesQuery,
variables: newTimelineActivitiesQueryVariables,
data: {
[objectMetadataItemActivity.namePlural]: newActivityConnectionForCache,
},
});
};
return {
injectIntoTimelineActivitiesNextQuery: injectIntoTimelineActivitiesQuery,
};
};

View File

@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useTimelineActivities = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
const {
activityTargets,
loadingActivityTargets,
initialized: initializedActivityTargets,
} = useActivityTargets({
targetableObject,
});
const [initialized, setInitialized] = useState(false);
const [activities, setActivities] = useState<Activity[]>([]);
const activityIds = activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString);
const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables(
{
activityIds,
},
);
const { records: activitiesFromRequest, loading: loadingActivities } =
useFindManyRecords<Activity>({
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
objectNameSingular: CoreObjectNameSingular.Activity,
filter: timelineActivitiesQueryVariables.filter,
orderBy: timelineActivitiesQueryVariables.orderBy,
onCompleted: () => {
if (!initialized) {
setInitialized(true);
}
},
});
useEffect(() => {
if (!loadingActivities) {
setActivities(activitiesFromRequest);
}
}, [activitiesFromRequest, loadingActivities]);
const noActivityTargets =
initializedActivityTargets && !isNonEmptyArray(activityTargets);
useEffect(() => {
if (noActivityTargets) {
setInitialized(true);
}
}, [noActivityTargets]);
const loading = loadingActivities || loadingActivityTargets;
return {
activities,
loading,
initialized,
};
};

View File

@ -0,0 +1,18 @@
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
export const makeTimelineActivitiesQueryVariables = ({
activityIds,
}: {
activityIds: string[];
}): ObjectRecordQueryVariables => {
return {
filter: {
id: {
in: activityIds,
},
},
orderBy: {
createdAt: 'AscNullsFirst',
},
};
};

View File

@ -1,5 +1,8 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export type ActivityTargetableObject = {
id: string;
targetObjectNameSingular: string;
targetObjectRecord: ObjectRecord;
relatedTargetableObjects?: ActivityTargetableObject[];
};

View File

@ -0,0 +1,38 @@
import { v4 } from 'uuid';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
export const getActivityTargetsToCreateFromTargetableObjects = ({
targetableObjects,
activityId,
}: {
targetableObjects: ActivityTargetableObject[];
activityId: string;
}): Partial<ActivityTarget>[] => {
const activityTargetableObjects = targetableObjects
? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
targetableObjects,
)
: [];
const activityTargetsToCreate = activityTargetableObjects.map(
(targetableObject) => {
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
return {
[targetableObject.targetObjectNameSingular]:
targetableObject.targetObjectRecord,
[targetableObjectFieldIdName]: targetableObject.id,
activityId,
id: v4(),
};
},
);
return activityTargetsToCreate;
};