Activity cache injection (#3791)

* WIP

* Minor fixes

* Added TODO

* Fix post merge

* Fix

* Fixed warnings

* Fixed comments

* Fixed comments

* Fixed naming

* Removed comment

* WIP

* WIP 2

* Finished working version

* Fixes

* Fixed typing

* Fixes

* Fixes

* Fixes

* Naming fixes

* WIP

* Fix import

* WIP

* Working version on title

* Fixed create record id overwrite

* Removed unecessary callback

* Masterpiece

* Fixed delete on click outside drawer or delete

* Cleaned

* Cleaned

* Cleaned

* Minor fixes

* Fixes

* Fixed naming

* WIP

* Fix

* Fixed create from target inline cell

* Removed console.log

* Fixed delete activity optimistic effect

* Fixed no title

* Fixed debounce and title body creation

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2024-02-09 14:51:30 +01:00
committed by GitHub
parent 9ceff84bbf
commit cca72da708
87 changed files with 2195 additions and 1058 deletions

View File

@ -1,13 +1,17 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { BlockNoteEditor } from '@blocknote/core';
import { useBlockNote } from '@blocknote/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import debounce from 'lodash.debounce';
import { useRecoilState } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
import { Activity } from '@/activities/types/Activity';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
@ -23,38 +27,99 @@ const StyledBlockNoteStyledContainer = styled.div`
`;
type ActivityBodyEditorProps = {
activity: Pick<Activity, 'id' | 'body'>;
onChange?: (activityBody: string) => void;
activity: Activity;
fillTitleFromBody: boolean;
};
export const ActivityBodyEditor = ({
activity,
onChange,
fillTitleFromBody,
}: ActivityBodyEditorProps) => {
const [body, setBody] = useState<string | null>(null);
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.Activity,
const [stringifiedBodyFromEditor, setStringifiedBodyFromEditor] = useState<
string | null
>(activity.body);
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
activityTitleHasBeenSetFamilyState({
activityId: activity.id,
}),
);
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const modifyActivityFromCache = useModifyRecordFromCache({
objectMetadataItem: objectMetadataItemActivity,
});
useEffect(() => {
if (body) {
onChange?.(body);
}
}, [body, onChange]);
const { upsertActivity } = useUpsertActivity();
const debounceOnChange = useMemo(() => {
const onInternalChange = (activityBody: string) => {
setBody(activityBody);
updateOneRecord?.({
idToUpdate: activity.id,
updateOneRecordInput: {
body: activityBody,
const persistBodyDebounced = useDebouncedCallback((newBody: string) => {
upsertActivity({
activity,
input: {
body: newBody,
},
});
}, 500);
const persistTitleAndBodyDebounced = useDebouncedCallback(
(newTitle: string, newBody: string) => {
upsertActivity({
activity,
input: {
title: newTitle,
body: newBody,
},
});
};
return debounce(onInternalChange, 200);
}, [updateOneRecord, activity.id]);
setActivityTitleHasBeenSet(true);
},
500,
);
const updateTitleAndBody = useCallback(
(newStringifiedBody: string) => {
const blockBody = JSON.parse(newStringifiedBody);
const newTitleFromBody = blockBody[0]?.content?.[0]?.text;
modifyActivityFromCache(activity.id, {
title: () => {
return newTitleFromBody;
},
});
persistTitleAndBodyDebounced(newTitleFromBody, newStringifiedBody);
},
[activity.id, modifyActivityFromCache, persistTitleAndBodyDebounced],
);
const handleBodyChange = useCallback(
(activityBody: string) => {
if (!activityTitleHasBeenSet && fillTitleFromBody) {
updateTitleAndBody(activityBody);
} else {
persistBodyDebounced(activityBody);
}
},
[
fillTitleFromBody,
persistBodyDebounced,
activityTitleHasBeenSet,
updateTitleAndBody,
],
);
useEffect(() => {
if (
isNonEmptyString(stringifiedBodyFromEditor) &&
activity.body !== stringifiedBodyFromEditor
) {
handleBodyChange(stringifiedBodyFromEditor);
}
}, [stringifiedBodyFromEditor, handleBodyChange, activity]);
const slashMenuItems = getSlashMenu();
@ -85,7 +150,7 @@ export const ActivityBodyEditor = ({
: undefined,
domAttributes: { editor: { class: 'editor' } },
onEditorContentChange: (editor: BlockNoteEditor) => {
debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? '');
setStringifiedBodyFromEditor(JSON.stringify(editor.topLevelBlocks) ?? '');
},
slashMenuItems,
blockSpecs: blockSpecs,

View File

@ -67,6 +67,7 @@ export const ActivityComments = ({
const { records: comments } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.Comment,
skip: !isNonEmptyString(activity?.id),
filter: {
activityId: {
eq: activity?.id ?? '',

View File

@ -1,22 +1,26 @@
import React, { useCallback, useRef, useState } from 'react';
import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { Comment } from '@/activities/types/Comment';
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import {
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { debounce } from '~/utils/debounce';
import { ActivityTitle } from './ActivityTitle';
@ -54,36 +58,32 @@ const StyledTopContainer = styled.div`
`;
type ActivityEditorProps = {
activity: Pick<
Activity,
'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt'
> & {
comments?: Array<Comment> | null;
} & {
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
} & {
activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'companyId' | 'personId'>
> | null;
};
activity: Activity;
showComment?: boolean;
autoFillTitle?: boolean;
fillTitleFromBody?: boolean;
};
export const ActivityEditor = ({
activity,
showComment = true,
autoFillTitle = false,
fillTitleFromBody = false,
}: ActivityEditorProps) => {
const [hasUserManuallySetTitle, setHasUserManuallySetTitle] =
useState<boolean>(false);
const [title, setTitle] = useState<string | null>(activity.title ?? '');
const containerRef = useRef<HTMLDivElement>(null);
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
);
const { upsertActivity } = useUpsertActivity();
const { deleteActivityFromCache } = useDeleteActivityFromCache();
const useUpsertOneActivityMutation: RecordUpdateHook = () => {
const upsertActivityMutation = ({ variables }: RecordUpdateHookParams) => {
upsertActivity({ activity, input: variables.updateOneRecordInput });
};
return [upsertActivityMutation, { loading: false }];
};
const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({
objectNameSingular: CoreObjectNameSingular.Activity,
@ -91,6 +91,7 @@ export const ActivityEditor = ({
fieldMetadataName: 'dueAt',
fieldPosition: 0,
clearable: true,
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
});
const { FieldContextProvider: AssigneeFieldContextProvider } =
@ -100,41 +101,24 @@ export const ActivityEditor = ({
fieldMetadataName: 'assignee',
fieldPosition: 1,
clearable: true,
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
});
const updateTitle = useCallback(
(newTitle: string) => {
updateOneActivity?.({
idToUpdate: activity.id,
updateOneRecordInput: {
title: newTitle ?? '',
},
});
},
[activity.id, updateOneActivity],
);
const handleActivityCompletionChange = useCallback(
(value: boolean) => {
updateOneActivity?.({
idToUpdate: activity.id,
updateOneRecordInput: {
completedAt: value ? new Date().toISOString() : null,
},
});
},
[activity.id, updateOneActivity],
const [isCreatingActivity, setIsCreatingActivity] = useRecoilState(
isCreatingActivityState,
);
const debouncedUpdateTitle = debounce(updateTitle, 200);
// TODO: remove
const updateTitleFromBody = (body: string) => {
const blockBody = JSON.parse(body);
const parsedTitle = blockBody[0]?.content?.[0]?.text;
if (!hasUserManuallySetTitle && autoFillTitle) {
setTitle(parsedTitle);
debouncedUpdateTitle(parsedTitle);
}
};
useRegisterClickOutsideListenerCallback({
callbackId: 'activity-editor',
callbackFunction: () => {
if (isCreatingActivity) {
setIsCreatingActivity(false);
deleteActivityFromCache(activity);
}
},
});
if (!activity) {
return <></>;
@ -145,17 +129,7 @@ export const ActivityEditor = ({
<StyledUpperPartContainer>
<StyledTopContainer>
<ActivityTypeDropdown activity={activity} />
<ActivityTitle
title={title ?? ''}
completed={!!activity.completedAt}
type={activity.type}
onTitleChange={(newTitle) => {
setTitle(newTitle);
setHasUserManuallySetTitle(true);
debouncedUpdateTitle(newTitle);
}}
onCompletionChange={handleActivityCompletionChange}
/>
<ActivityTitle activity={activity} />
<PropertyBox>
{activity.type === 'Task' &&
DueAtFieldContextProvider &&
@ -169,14 +143,12 @@ export const ActivityEditor = ({
</AssigneeFieldContextProvider>
</>
)}
<ActivityTargetsInlineCell
activity={activity as unknown as GraphQLActivity}
/>
<ActivityTargetsInlineCell activity={activity} />
</PropertyBox>
</StyledTopContainer>
<ActivityBodyEditor
activity={activity}
onChange={updateTitleFromBody}
fillTitleFromBody={fillTitleFromBody}
/>
</StyledUpperPartContainer>
{showComment && (

View File

@ -1,11 +1,20 @@
import { useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
import { ActivityType } from '@/activities/types/Activity';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
import { Activity } from '@/activities/types/Activity';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
import {
Checkbox,
CheckboxShape,
CheckboxSize,
} from '@/ui/input/components/Checkbox';
import { isDefined } from '~/utils/isDefined';
const StyledEditableTitleInput = styled.input<{
completed: boolean;
@ -39,36 +48,83 @@ const StyledContainer = styled.div`
`;
type ActivityTitleProps = {
title: string;
type: ActivityType;
completed: boolean;
onTitleChange: (title: string) => void;
onCompletionChange: (value: boolean) => void;
activity: Activity;
};
export const ActivityTitle = ({
title,
completed,
type,
onTitleChange,
onCompletionChange,
}: ActivityTitleProps) => (
<StyledContainer>
{type === 'Task' && (
<Checkbox
size={CheckboxSize.Large}
shape={CheckboxShape.Rounded}
checked={completed}
onCheckedChange={(value) => onCompletionChange(value)}
export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
const [internalTitle, setInternalTitle] = useState(activity.title);
const { upsertActivity } = useUpsertActivity();
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
activityTitleHasBeenSetFamilyState({
activityId: activity.id,
}),
);
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const modifyActivityFromCache = useModifyRecordFromCache({
objectMetadataItem: objectMetadataItemActivity,
});
const persistTitleDebounced = useDebouncedCallback((newTitle: string) => {
upsertActivity({
activity,
input: {
title: newTitle,
},
});
if (!activityTitleHasBeenSet) {
setActivityTitleHasBeenSet(true);
}
}, 500);
const handleTitleChange = (newTitle: string) => {
setInternalTitle(newTitle);
modifyActivityFromCache(activity.id, {
title: () => {
return newTitle;
},
});
persistTitleDebounced(newTitle);
};
const handleActivityCompletionChange = (value: boolean) => {
upsertActivity({
activity,
input: {
completedAt: value ? new Date().toISOString() : null,
},
});
};
const completed = isDefined(activity.completedAt);
return (
<StyledContainer>
{activity.type === 'Task' && (
<Checkbox
size={CheckboxSize.Large}
shape={CheckboxShape.Rounded}
checked={completed}
onCheckedChange={(value) => handleActivityCompletionChange(value)}
/>
)}
<StyledEditableTitleInput
autoComplete="off"
autoFocus
placeholder={`${activity.type} title`}
onChange={(event) => handleTitleChange(event.target.value)}
value={internalTitle}
completed={completed}
/>
)}
<StyledEditableTitleInput
autoComplete="off"
autoFocus
placeholder={`${type} title`}
onChange={(event) => onTitleChange(event.target.value)}
value={title}
completed={completed}
/>
</StyledContainer>
);
</StyledContainer>
);
};

View File

@ -0,0 +1,34 @@
import { useSetRecoilState } from 'recoil';
import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
const QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS = 3;
export const useActivityById = ({ activityId }: { activityId: string }) => {
const setEntityFields = useSetRecoilState(recordStoreFamilyState(activityId));
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
const { record: activityWithConnections } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.Activity,
objectRecordId: activityId,
skip: !activityId,
onCompleted: (activityWithConnections: any) => {
const { activity } = makeActivityWithoutConnection(
activityWithConnections,
);
setEntityFields(activity);
},
depth: QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS,
});
const { activity } = makeActivityWithoutConnection(activityWithConnections);
return {
activity,
};
};

View File

@ -1,3 +1,4 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
@ -17,6 +18,7 @@ export const useActivityTargetObjectRecords = ({
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
skip: !isNonEmptyString(activityId),
filter: {
activityId: {
eq: activityId,

View File

@ -1,4 +1,5 @@
import { useState } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
@ -6,7 +7,7 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTarget
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useActivityTargets = ({
export const useActivityTargetsForTargetableObject = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
@ -17,9 +18,14 @@ export const useActivityTargets = ({
const [initialized, setInitialized] = useState(false);
const targetableObjectId = targetableObject.id;
const skipRequest = !isNonEmptyString(targetableObjectId);
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
skip: skipRequest,
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,

View File

@ -0,0 +1,91 @@
import { useApolloClient } from '@apollo/client';
import { StringKeyOf } from 'type-fest';
import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition';
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from '~/utils/isDefined';
export const useAttachRelationInBothDirections = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const apolloClient = useApolloClient();
const attachRelationInBothDirections = <
Source extends ObjectRecord = ObjectRecord,
Target extends ObjectRecord = ObjectRecord,
>({
sourceRecord,
targetRecords,
sourceObjectNameSingular,
targetObjectNameSingular,
fieldNameOnSourceRecord,
fieldNameOnTargetRecord,
}: {
sourceRecord: Source;
targetRecords: Target[];
sourceObjectNameSingular: string;
targetObjectNameSingular: string;
fieldNameOnSourceRecord: StringKeyOf<Source>;
fieldNameOnTargetRecord: StringKeyOf<Target>;
}) => {
const sourceObjectMetadataItem = getObjectMetadataItemByNameSingular({
objectMetadataItems,
objectNameSingular: sourceObjectNameSingular,
});
const targetObjectMetadataItem = getObjectMetadataItemByNameSingular({
objectMetadataItems,
objectNameSingular: targetObjectNameSingular,
});
const fieldMetadataItemOnSourceRecord =
sourceObjectMetadataItem.fields.find(
(field) => field.name === fieldNameOnSourceRecord,
);
if (!isDefined(fieldMetadataItemOnSourceRecord)) {
throw new Error(
`Field ${fieldNameOnSourceRecord} not found on object ${sourceObjectNameSingular}`,
);
}
const relationDefinition = getRelationDefinition({
fieldMetadataItemOnSourceRecord: fieldMetadataItemOnSourceRecord,
objectMetadataItems,
});
if (!isDefined(relationDefinition)) {
throw new Error(
`Relation metadata not found for field ${fieldNameOnSourceRecord} on object ${sourceObjectNameSingular}`,
);
}
// TODO: could we use triggerUpdateRelationsOptimisticEffect here?
targetRecords.forEach((relationTargetRecord) => {
triggerAttachRelationOptimisticEffect({
cache: apolloClient.cache,
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: sourceRecord.id,
fieldNameOnTargetRecord: fieldNameOnTargetRecord,
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
targetRecordId: relationTargetRecord.id,
});
triggerAttachRelationOptimisticEffect({
cache: apolloClient.cache,
sourceObjectNameSingular: targetObjectMetadataItem.nameSingular,
sourceRecordId: relationTargetRecord.id,
fieldNameOnTargetRecord: fieldNameOnSourceRecord,
targetObjectNameSingular: sourceObjectMetadataItem.nameSingular,
targetRecordId: sourceRecord.id,
});
});
};
return {
attachRelationInBothDirections,
};
};

View File

@ -0,0 +1,115 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections';
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
import { useInjectIntoTimelineActivitiesQueryAfterDrawerMount } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueryAfterDrawerMount';
import { Activity, ActivityType } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
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';
export const useCreateActivityInCache = () => {
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,
depth: 3,
});
const { injectIntoTimelineActivitiesQueryAfterDrawerMount } =
useInjectIntoTimelineActivitiesQueryAfterDrawerMount();
const { injectIntoActivityTargetInlineCellCache } =
useInjectIntoActivityTargetInlineCellCache();
const {
attachRelationInBothDirections:
attachRelationSourceRecordToItsRelationTargetRecordsAndViceVersaInCache,
} = useAttachRelationInBothDirections();
const createActivityInCache = ({
type,
targetableObjects,
timelineTargetableObject,
assigneeId,
}: {
type: ActivityType;
targetableObjects: ActivityTargetableObject[];
timelineTargetableObject: ActivityTargetableObject;
assigneeId?: string;
}) => {
const activityId = v4();
const createdActivityInCache = createOneActivityInCache({
id: activityId,
author: workspaceMemberRecord,
authorId: workspaceMemberRecord?.id,
assignee: !assigneeId ? workspaceMemberRecord : undefined,
assigneeId:
assigneeId ?? isNonEmptyString(workspaceMemberRecord?.id)
? workspaceMemberRecord?.id
: undefined,
type: type,
});
const activityTargetsToCreate =
getActivityTargetsToCreateFromTargetableObjects({
activityId,
targetableObjects,
});
const createdActivityTargetsInCache = createManyActivityTargetsInCache(
activityTargetsToCreate,
);
injectIntoTimelineActivitiesQueryAfterDrawerMount({
activityToInject: createdActivityInCache,
activityTargetsToInject: createdActivityTargetsInCache,
timelineTargetableObject,
});
injectIntoActivityTargetInlineCellCache({
activityId,
activityTargetsToInject: createdActivityTargetsInCache,
});
attachRelationSourceRecordToItsRelationTargetRecordsAndViceVersaInCache({
sourceRecord: createdActivityInCache,
fieldNameOnSourceRecord: 'activityTargets',
sourceObjectNameSingular: CoreObjectNameSingular.Activity,
fieldNameOnTargetRecord: 'activity',
targetObjectNameSingular: CoreObjectNameSingular.ActivityTarget,
targetRecords: createdActivityTargetsInCache,
});
return {
createdActivityInCache: {
...createdActivityInCache,
activityTargets: createdActivityTargetsInCache,
},
createdActivityTargetsInCache,
};
};
return {
createActivityInCache,
};
};

View File

@ -0,0 +1,59 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache';
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
export const useCreateActivityInDB = () => {
const { createOneRecord: createOneActivity } = useCreateOneRecord({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const { createManyRecords: createManyActivityTargets } =
useCreateManyRecords<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const { makeActivityWithConnection } = useActivityConnectionUtils();
const { modifyActivityTargetsOnActivityCache } =
useModifyActivityTargetsOnActivityCache();
const createActivityInDB = async (activityToCreate: ActivityForEditor) => {
const { activityWithConnection } = makeActivityWithConnection(
activityToCreate as any, // TODO: fix type
);
await createOneActivity?.(
{
...activityWithConnection,
updatedAt: new Date().toISOString(),
},
{
skipOptimisticEffect: true,
},
);
const activityTargetsToCreate = activityToCreate.activityTargets ?? [];
if (isNonEmptyArray(activityTargetsToCreate)) {
await createManyActivityTargets(activityTargetsToCreate, {
skipOptimisticEffect: true,
});
}
// TODO: replace by trigger optimistic effect
modifyActivityTargetsOnActivityCache({
activityId: activityToCreate.id,
activityTargets: activityTargetsToCreate,
});
};
return {
createActivityInDB,
};
};

View File

@ -0,0 +1,39 @@
import { useApolloClient } from '@apollo/client';
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
// TODO: this should be useDeleteRecordFromCache
export const useDeleteActivityFromCache = () => {
const { makeActivityWithConnection } = useActivityConnectionUtils();
const apolloClient = useApolloClient();
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const { objectMetadataItems } = useObjectMetadataItems();
const deleteActivityFromCache = (activityToDelete: ActivityForEditor) => {
const { activityWithConnection } = makeActivityWithConnection(
activityToDelete as any, // TODO: fix type
);
triggerDeleteRecordsOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem: objectMetadataItemActivity,
objectMetadataItems,
recordsToDelete: [activityWithConnection],
});
};
return {
deleteActivityFromCache,
};
};

View File

@ -1,163 +1,61 @@
import { useCallback } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { useRecoilState, useSetRecoilState } from 'recoil';
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 { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState';
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { ActivityType } from '@/activities/types/Activity';
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;
}) => {
export const useOpenCreateActivityDrawerV2 = () => {
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 { createActivityInCache } = useCreateActivityInCache();
const [, setActivityTargetableEntityArray] = useRecoilState(
activityTargetableEntityArrayState,
);
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
const { activityTargets } = useActivityTargets({
targetableObject,
});
const setIsCreatingActivity = useSetRecoilState(isCreatingActivityState);
const { injectIntoTimelineActivitiesNextQuery } =
useInjectIntoTimelineActivitiesQuery();
const setTemporaryActivityForEditor = useSetRecoilState(
temporaryActivityForEditorState,
);
const { injectIntoActivityTargetInlineCellCache } =
useInjectIntoActivityTargetInlineCellCache();
const { injectIntoUseActivityTargets } = useWriteActivityTargetsInCache();
const { modifyActivityTargetsOnActivityCache } =
useModifyActivityTargetsOnActivityCache();
const { modifyActivityOnActivityTargetsCache } =
useModifyActivityOnActivityTargetsCache();
return useCallback(
async ({
const openCreateActivityDrawer = async ({
type,
targetableObjects,
timelineTargetableObject,
assigneeId,
}: {
type: ActivityType;
targetableObjects: ActivityTargetableObject[];
timelineTargetableObject: ActivityTargetableObject;
assigneeId?: string;
}) => {
const { createdActivityInCache } = createActivityInCache({
type,
targetableObjects,
timelineTargetableObject,
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,
});
setTemporaryActivityForEditor(createdActivityInCache);
setIsCreatingActivity(true);
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(createdActivityInCache.id);
setActivityTargetableEntityArray(targetableObjects ?? []);
openRightDrawer(RightDrawerPages.CreateActivity);
};
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,
],
);
return openCreateActivityDrawer;
};

View File

@ -0,0 +1,45 @@
import { useRecoilState } from 'recoil';
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState';
import { Activity } from '@/activities/types/Activity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
export const useUpsertActivity = () => {
const [isCreatingActivity, setIsCreatingActivity] = useRecoilState(
isCreatingActivityState,
);
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const { createActivityInDB } = useCreateActivityInDB();
const upsertActivity = ({
activity,
input,
}: {
activity: Activity;
input: Partial<Activity>;
}) => {
if (isCreatingActivity) {
createActivityInDB({
...activity,
...input,
});
setIsCreatingActivity(false);
} else {
updateOneActivity?.({
idToUpdate: activity.id,
updateOneRecordInput: input,
});
}
};
return {
upsertActivity,
};
};

View File

@ -1,84 +0,0 @@
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

@ -1,10 +1,18 @@
import styled from '@emotion/styled';
import { isNonEmptyArray } from '@sniptt/guards';
import { useRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
@ -18,14 +26,16 @@ const StyledSelectContainer = styled.div`
`;
type ActivityTargetInlineCellEditModeProps = {
activityId: string;
activity: Activity;
activityTargetObjectRecords: ActivityTargetObjectRecord[];
};
export const ActivityTargetInlineCellEditMode = ({
activityId,
activity,
activityTargetObjectRecords,
}: ActivityTargetInlineCellEditModeProps) => {
const [isCreatingActivity] = useRecoilState(isCreatingActivityState);
const selectedObjectRecordIds = activityTargetObjectRecords.map(
(activityTarget) => ({
objectNameSingular: activityTarget.targetObjectNameSingular,
@ -46,6 +56,21 @@ export const ActivityTargetInlineCellEditMode = ({
const { closeInlineCell: closeEditableField } = useInlineCell();
const { upsertActivity } = useUpsertActivity();
const { objectMetadataItem: objectMetadataItemActivityTarget } =
useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const { injectIntoActivityTargetInlineCellCache } =
useInjectIntoActivityTargetInlineCellCache();
const { generateObjectRecordOptimisticResponse } =
useGenerateObjectRecordOptimisticResponse({
objectMetadataItem: objectMetadataItemActivityTarget,
});
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
closeEditableField();
@ -67,25 +92,74 @@ export const ActivityTargetInlineCellEditMode = ({
),
);
if (activityTargetRecordsToCreate.length > 0) {
await createManyActivityTargets(
activityTargetRecordsToCreate.map((selectedRecord) => ({
id: v4(),
activityId,
[getActivityTargetObjectFieldIdName({
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
})]: selectedRecord.recordIdentifier.id,
})),
);
}
if (isCreatingActivity) {
let activityTargetsForCreation = activity.activityTargets;
if (activityTargetRecordsToDelete.length > 0) {
await deleteManyActivityTargets(
activityTargetRecordsToDelete.map(
(activityTargetObjectRecord) =>
activityTargetObjectRecord.activityTargetRecord.id,
),
);
if (isNonEmptyArray(activityTargetsForCreation)) {
const generatedActivityTargets = activityTargetRecordsToCreate.map(
(selectedRecord) => {
const emptyActivityTarget =
generateObjectRecordOptimisticResponse<ActivityTarget>({
id: v4(),
activityId: activity.id,
activity,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
[getActivityTargetObjectFieldIdName({
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
})]: selectedRecord.recordIdentifier.id,
});
return emptyActivityTarget;
},
);
activityTargetsForCreation.push(...generatedActivityTargets);
}
if (isNonEmptyArray(activityTargetRecordsToDelete)) {
activityTargetsForCreation = activityTargetsForCreation.filter(
(activityTarget) =>
!activityTargetRecordsToDelete.some(
(activityTargetObjectRecord) =>
activityTargetObjectRecord.targetObjectRecord.id ===
activityTarget.id,
),
);
}
injectIntoActivityTargetInlineCellCache({
activityId: activity.id,
activityTargetsToInject: activityTargetsForCreation,
});
upsertActivity({
activity,
input: {
activityTargets: activityTargetsForCreation,
},
});
} else {
if (activityTargetRecordsToCreate.length > 0) {
await createManyActivityTargets(
activityTargetRecordsToCreate.map((selectedRecord) => ({
id: v4(),
activityId: activity.id,
[getActivityTargetObjectFieldIdName({
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
})]: selectedRecord.recordIdentifier.id,
})),
);
}
if (activityTargetRecordsToDelete.length > 0) {
await deleteManyActivityTargets(
activityTargetRecordsToDelete.map(
(activityTargetObjectRecord) =>
activityTargetObjectRecord.activityTargetRecord.id,
),
);
}
}
};

View File

@ -1,8 +1,7 @@
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
import { Activity } from '@/activities/types/Activity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
@ -11,13 +10,7 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types
import { IconArrowUpRight, IconPencil } from '@/ui/display/icon';
type ActivityTargetsInlineCellProps = {
activity?: Pick<GraphQLActivity, 'id'> & {
activityTargets?: {
edges: Array<{
node: Pick<ActivityTarget, 'id'>;
}> | null;
};
};
activity: Activity;
};
export const ActivityTargetsInlineCell = ({
@ -47,8 +40,8 @@ export const ActivityTargetsInlineCell = ({
IconLabel={IconArrowUpRight}
editModeContent={
<ActivityTargetInlineCellEditMode
activityId={activity?.id ?? ''}
activityTargetObjectRecords={activityTargetObjectRecords as any}
activity={activity}
activityTargetObjectRecords={activityTargetObjectRecords}
/>
}
label="Relations"

View File

@ -1,19 +1,19 @@
import { useApolloClient } from '@apollo/client';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordConnectionFromEdges } from '@/object-record/cache/utils/getRecordConnectionFromEdges';
import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord';
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
export const useInjectIntoActivityTargetInlineCellCache = () => {
const apolloClient = useApolloClient();
const { objectMetadataItem: objectMetadataItemActivityTarget } =
useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const {
upsertFindManyRecordsQueryInCache:
overwriteFindManyActivityTargetsQueryInCache,
} = useUpsertFindManyRecordsQueryInCache({
objectMetadataItem: objectMetadataItemActivityTarget,
findManyRecordsQuery: findManyActivityTargetsQuery,
} = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const injectIntoActivityTargetInlineCellCache = ({
@ -23,32 +23,17 @@ export const useInjectIntoActivityTargetInlineCellCache = () => {
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,
},
const activityTargetInlineCellQueryVariables = {
filter: {
activityId: {
eq: activityId,
},
},
data: {
[objectMetadataItemActivityTarget.namePlural]:
newActivityTargetConnectionForCache,
},
};
overwriteFindManyActivityTargetsQueryInCache({
queryVariables: activityTargetInlineCellQueryVariables,
objectRecordsToOverwrite: activityTargetsToInject,
});
};

View File

@ -4,7 +4,6 @@ import styled from '@emotion/styled';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
import { Note } from '@/activities/types/Note';
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
import {
@ -101,9 +100,7 @@ export const NoteCard = ({
<StyledCardContent>{body}</StyledCardContent>
</StyledCardDetailsContainer>
<StyledFooter>
<ActivityTargetsInlineCell
activity={note as unknown as GraphQLActivity}
/>
<ActivityTargetsInlineCell activity={note} />
{note.comments && note.comments.length > 0 && (
<StyledCommentIcon>
<IconComment size={theme.icon.size.md} />

View File

@ -1,4 +1,4 @@
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
import { Note } from '@/activities/types/Note';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { OrderByField } from '@/object-metadata/types/OrderByField';
@ -7,7 +7,9 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity';
export const useNotes = (targetableObject: ActivityTargetableObject) => {
const { activityTargets } = useActivityTargets({ targetableObject });
const { activityTargets } = useActivityTargetsForTargetableObject({
targetableObject,
});
const filter = {
id: {

View File

@ -1,11 +1,16 @@
import { useApolloClient } from '@apollo/client';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState';
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { IconTrash } from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
import { isDefined } from '~/utils/isDefined';
export const ActivityActionBar = () => {
const viewableActivityId = useRecoilValue(viewableActivityIdState);
@ -15,9 +20,27 @@ export const ActivityActionBar = () => {
refetchFindManyQuery: true,
});
const [temporaryActivityForEditor, setTemporaryActivityForEditor] =
useRecoilState(temporaryActivityForEditorState);
const { deleteActivityFromCache } = useDeleteActivityFromCache();
const [isCreatingActivity] = useRecoilState(isCreatingActivityState);
const apolloClient = useApolloClient();
const deleteActivity = () => {
if (viewableActivityId) {
deleteOneActivity?.(viewableActivityId);
if (isCreatingActivity && isDefined(temporaryActivityForEditor)) {
deleteActivityFromCache(temporaryActivityForEditor);
setTemporaryActivityForEditor(null);
} else {
deleteOneActivity?.(viewableActivityId);
// TODO: find a better way to do this with custom optimistic rendering for activities
apolloClient.refetchQueries({
include: ['FindManyActivities'],
});
}
}
setIsRightDrawerOpen(false);

View File

@ -1,12 +1,7 @@
import React from 'react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { ActivityEditor } from '@/activities/components/ActivityEditor';
import { Activity } from '@/activities/types/Activity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useActivityById } from '@/activities/hooks/useActivityById';
const StyledContainer = styled.div`
box-sizing: border-box;
@ -21,23 +16,16 @@ const StyledContainer = styled.div`
type RightDrawerActivityProps = {
activityId: string;
showComment?: boolean;
autoFillTitle?: boolean;
fillTitleFromBody?: boolean;
};
export const RightDrawerActivity = ({
activityId,
showComment = true,
autoFillTitle = false,
fillTitleFromBody = false,
}: RightDrawerActivityProps) => {
const setEntityFields = useSetRecoilState(recordStoreFamilyState(activityId));
const { record: activity } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.Activity,
objectRecordId: activityId,
skip: !activityId,
onCompleted: (activity: Activity) => {
setEntityFields(activity ?? {});
},
const { activity } = useActivityById({
activityId,
});
if (!activity) {
@ -49,7 +37,7 @@ export const RightDrawerActivity = ({
<ActivityEditor
activity={activity}
showComment={showComment}
autoFillTitle={autoFillTitle}
fillTitleFromBody={fillTitleFromBody}
/>
</StyledContainer>
);

View File

@ -13,7 +13,7 @@ export const RightDrawerCreateActivity = () => {
<RightDrawerActivity
activityId={viewableActivityId}
showComment={false}
autoFillTitle={true}
fillTitleFromBody={true}
/>
)}
</>

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const activityTitleHasBeenSetFamilyState = atomFamily<
boolean,
{ activityId: string }
>({
key: 'activityTitleHasBeenSetFamilyState',
default: false,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isCreatingActivityState = atom<boolean>({
key: 'isCreatingActivityState',
default: false,
});

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
export const temporaryActivityForEditorState = atom<ActivityForEditor | null>({
key: 'temporaryActivityForEditorState',
default: null,
});

View File

@ -1,71 +0,0 @@
import { isNonEmptyArray } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import CommentCounter from '@/activities/comment/CommentCounter';
import { Activity } from '@/activities/types/Activity';
import { UserChip } from '@/users/components/UserChip';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { beautifyExactDate } from '~/utils/date-utils';
type TimelineActivityCardFooterProps = {
activity: Pick<Activity, 'id' | 'dueAt' | 'comments'> & {
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
};
};
const StyledContainer = styled.div`
align-items: center;
border-top: 1px solid ${({ theme }) => theme.border.color.medium};
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
width: calc(100% - ${({ theme }) => theme.spacing(4)});
`;
const StyledVerticalSeparator = styled.div`
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
height: 24px;
`;
const StyledComment = styled.div`
margin-left: auto;
`;
export const TimelineActivityCardFooter = ({
activity,
}: TimelineActivityCardFooterProps) => {
const hasComments = isNonEmptyArray(activity.comments || []);
return (
<>
{(activity.assignee || activity.dueAt || hasComments) && (
<StyledContainer>
{activity.assignee && (
<UserChip
id={activity.assignee.id}
name={
activity.assignee.name.firstName +
' ' +
activity.assignee.name.lastName ?? ''
}
avatarUrl={activity.assignee.avatarUrl ?? ''}
/>
)}
{activity.dueAt && (
<>
{activity.assignee && <StyledVerticalSeparator />}
{beautifyExactDate(activity.dueAt)}
</>
)}
<StyledComment>
{hasComments && (
<CommentCounter commentCount={activity.comments?.length || 0} />
)}
</StyledComment>
</StyledContainer>
)}
</>
);
};

View File

@ -1,7 +1,7 @@
import { useSetRecoilState } from 'recoil';
import { Button, ButtonGroup } from 'tsup.ui.index';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import {
IconCheckbox,
@ -19,7 +19,7 @@ export const TimelineCreateButtonGroup = ({
const { getActiveTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
const setActiveTabId = useSetRecoilState(getActiveTabIdState());
const openCreateActivity = useOpenCreateActivityDrawer();
const openCreateActivity = useOpenCreateActivityDrawerV2();
return (
<ButtonGroup variant={'secondary'}>
@ -30,6 +30,7 @@ export const TimelineCreateButtonGroup = ({
openCreateActivity({
type: 'Note',
targetableObjects: [targetableObject],
timelineTargetableObject: targetableObject,
})
}
/>
@ -40,6 +41,7 @@ export const TimelineCreateButtonGroup = ({
openCreateActivity({
type: 'Task',
targetableObjects: [targetableObject],
timelineTargetableObject: targetableObject,
})
}
/>

View File

@ -1,89 +0,0 @@
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,124 @@
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

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
@ -17,14 +17,12 @@ export const useTimelineActivities = ({
activityTargets,
loadingActivityTargets,
initialized: initializedActivityTargets,
} = useActivityTargets({
} = useActivityTargetsForTargetableObject({
targetableObject,
});
const [initialized, setInitialized] = useState(false);
const [activities, setActivities] = useState<Activity[]>([]);
const activityIds = activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString);
@ -35,7 +33,7 @@ export const useTimelineActivities = ({
},
);
const { records: activitiesFromRequest, loading: loadingActivities } =
const { records: activities, loading: loadingActivities } =
useFindManyRecords<Activity>({
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
objectNameSingular: CoreObjectNameSingular.Activity,
@ -48,12 +46,6 @@ export const useTimelineActivities = ({
},
});
useEffect(() => {
if (!loadingActivities) {
setActivities(activitiesFromRequest);
}
}, [activitiesFromRequest, loadingActivities]);
const noActivityTargets =
initializedActivityTargets && !isNonEmptyArray(activityTargets);

View File

@ -12,7 +12,7 @@ export const makeTimelineActivitiesQueryVariables = ({
},
},
orderBy: {
createdAt: 'AscNullsFirst',
createdAt: 'DescNullsFirst',
},
};
};

View File

@ -0,0 +1,20 @@
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { Comment } from '@/activities/types/Comment';
import { WorkspaceMember } from '~/generated-metadata/graphql';
export type ActivityForEditor = Pick<
Activity,
'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt' | 'updatedAt'
> & {
comments?: Comment[];
} & {
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
} & {
activityTargets?: Array<
Pick<
ActivityTarget,
'id' | 'companyId' | 'personId' | 'createdAt' | 'updatedAt' | 'activity'
>
>;
};

View File

@ -6,8 +6,8 @@ export type ActivityTarget = {
id: string;
createdAt: string;
updatedAt: string;
companyId: string | null;
personId: string | null;
companyId?: string | null;
personId?: string | null;
activity: Pick<Activity, 'id' | 'createdAt' | 'updatedAt'>;
person?: Pick<Person, 'id' | 'name' | 'avatarUrl'> | null;
company?: Pick<Company, 'id' | 'name' | 'domainName'> | null;

View File

@ -24,13 +24,17 @@ export const getActivityTargetsToCreateFromTargetableObjects = ({
nameSingular: targetableObject.targetObjectNameSingular,
});
return {
const activityTarget = {
[targetableObject.targetObjectNameSingular]:
targetableObject.targetObjectRecord,
[targetableObjectFieldIdName]: targetableObject.id,
activityId,
id: v4(),
};
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
} as Partial<ActivityTarget>;
return activityTarget;
},
);

View File

@ -0,0 +1,102 @@
import { isNonEmptyArray } from '@apollo/client/utilities';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { Comment } from '@/activities/types/Comment';
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { isDefined } from '~/utils/isDefined';
export const useActivityConnectionUtils = () => {
const mapConnectionToRecords = useMapConnectionToRecords();
const makeActivityWithoutConnection = (activityWithConnections: any) => {
if (!isDefined(activityWithConnections)) {
return { activity: null };
}
const hasActivityTargetsConnection = isObjectRecordConnection(
CoreObjectNameSingular.ActivityTarget,
activityWithConnections?.activityTargets,
);
const activityTargets: ActivityTarget[] = [];
if (hasActivityTargetsConnection) {
const newActivityTargets = mapConnectionToRecords({
objectRecordConnection: activityWithConnections?.activityTargets,
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
depth: 5,
}) as ActivityTarget[];
activityTargets.push(...newActivityTargets);
}
const hasCommentsConnection = isObjectRecordConnection(
CoreObjectNameSingular.Comment,
activityWithConnections?.comments,
);
const comments: Comment[] = [];
if (hasCommentsConnection) {
const newComments = mapConnectionToRecords({
objectRecordConnection: activityWithConnections?.comments,
objectNameSingular: CoreObjectNameSingular.Comment,
depth: 5,
}) as Comment[];
comments.push(...newComments);
}
const activity: Activity = {
...activityWithConnections,
activityTargets,
comments,
};
return { activity };
};
const makeActivityWithConnection = (activity: Activity) => {
const activityTargetEdges = isNonEmptyArray(activity?.activityTargets)
? activity.activityTargets.map((activityTarget) => ({
node: activityTarget,
cursor: '',
}))
: [];
const commentEdges = isNonEmptyArray(activity?.comments)
? activity.comments.map((comment) => ({
node: comment,
cursor: '',
}))
: [];
const activityTargets = {
edges: activityTargetEdges,
pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection<ActivityTarget>;
const comments = {
edges: commentEdges,
pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection<Comment>;
const activityWithConnection = {
...activity,
activityTargets,
comments,
};
return { activityWithConnection };
};
return {
makeActivityWithoutConnection,
makeActivityWithConnection,
};
};