mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 23:37:21 +03:00
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:
parent
9ceff84bbf
commit
cca72da708
@ -68,6 +68,7 @@
|
||||
"danger-plugin-todos": "^1.3.1",
|
||||
"dataloader": "^2.2.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"debounce": "^2.0.0",
|
||||
"deep-equal": "^2.2.2",
|
||||
"docusaurus-node-polyfills": "^1.0.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
@ -149,6 +150,7 @@
|
||||
"tsup": "^8.0.1",
|
||||
"type-fest": "^4.1.0",
|
||||
"typeorm": "^0.3.17",
|
||||
"use-debounce": "^10.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"xlsx-ugnis": "^0.19.3",
|
||||
|
@ -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,
|
||||
|
@ -67,6 +67,7 @@ export const ActivityComments = ({
|
||||
|
||||
const { records: comments } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Comment,
|
||||
skip: !isNonEmptyString(activity?.id),
|
||||
filter: {
|
||||
activityId: {
|
||||
eq: activity?.id ?? '',
|
||||
|
@ -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 && (
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
|
@ -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,
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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} />
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -13,7 +13,7 @@ export const RightDrawerCreateActivity = () => {
|
||||
<RightDrawerActivity
|
||||
activityId={viewableActivityId}
|
||||
showComment={false}
|
||||
autoFillTitle={true}
|
||||
fillTitleFromBody={true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const activityTitleHasBeenSetFamilyState = atomFamily<
|
||||
boolean,
|
||||
{ activityId: string }
|
||||
>({
|
||||
key: 'activityTitleHasBeenSetFamilyState',
|
||||
default: false,
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isCreatingActivityState = atom<boolean>({
|
||||
key: 'isCreatingActivityState',
|
||||
default: false,
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
|
||||
|
||||
export const temporaryActivityForEditorState = atom<ActivityForEditor | null>({
|
||||
key: 'temporaryActivityForEditorState',
|
||||
default: null,
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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);
|
||||
|
||||
|
@ -12,7 +12,7 @@ export const makeTimelineActivitiesQueryVariables = ({
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'AscNullsFirst',
|
||||
createdAt: 'DescNullsFirst',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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'
|
||||
>
|
||||
>;
|
||||
};
|
@ -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;
|
||||
|
@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -22,7 +22,7 @@ export const useCachedRootQuery = ({
|
||||
const buildRecordFieldsFragment = () => {
|
||||
return objectMetadataItem.fields
|
||||
.filter((field) => field.type !== 'RELATION')
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery({ field }))
|
||||
.join(' \n');
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RelationType } from '@/settings/data-model/types/RelationType';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const getRelationDefinition = ({
|
||||
objectMetadataItems,
|
||||
fieldMetadataItemOnSourceRecord,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
fieldMetadataItemOnSourceRecord: FieldMetadataItem;
|
||||
}) => {
|
||||
if (fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.Relation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relationMetadataItem =
|
||||
fieldMetadataItemOnSourceRecord.fromRelationMetadata ||
|
||||
fieldMetadataItemOnSourceRecord.toRelationMetadata;
|
||||
|
||||
if (!relationMetadataItem) return null;
|
||||
|
||||
const relationSourceFieldMetadataItemId =
|
||||
'toFieldMetadataId' in relationMetadataItem
|
||||
? relationMetadataItem.toFieldMetadataId
|
||||
: relationMetadataItem.fromFieldMetadataId;
|
||||
|
||||
if (!relationSourceFieldMetadataItemId) return null;
|
||||
|
||||
// TODO: precise naming, is it relationTypeFromTargetPointOfView or relationTypeFromSourcePointOfView ?
|
||||
const relationType =
|
||||
relationMetadataItem.relationType === RelationMetadataType.OneToMany &&
|
||||
fieldMetadataItemOnSourceRecord.toRelationMetadata
|
||||
? ('MANY_TO_ONE' satisfies RelationType)
|
||||
: (relationMetadataItem.relationType as RelationType);
|
||||
|
||||
const targetObjectMetadataNameSingular =
|
||||
'toObjectMetadata' in relationMetadataItem
|
||||
? relationMetadataItem.toObjectMetadata.nameSingular
|
||||
: relationMetadataItem.fromObjectMetadata.nameSingular;
|
||||
|
||||
const targetObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === targetObjectMetadataNameSingular,
|
||||
);
|
||||
|
||||
if (!targetObjectMetadataItem) return null;
|
||||
|
||||
const fieldMetadataItemOnTargetRecord = targetObjectMetadataItem.fields.find(
|
||||
(field) => field.id === relationSourceFieldMetadataItemId,
|
||||
);
|
||||
|
||||
if (!fieldMetadataItemOnTargetRecord) return null;
|
||||
|
||||
return {
|
||||
fieldMetadataItemOnTargetRecord,
|
||||
targetObjectMetadataItem,
|
||||
relationType,
|
||||
};
|
||||
};
|
@ -2,57 +2,69 @@ import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const triggerAttachRelationOptimisticEffect = ({
|
||||
cache,
|
||||
objectNameSingular,
|
||||
recordId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
relationRecordId,
|
||||
sourceObjectNameSingular,
|
||||
sourceRecordId,
|
||||
targetObjectNameSingular,
|
||||
fieldNameOnTargetRecord,
|
||||
targetRecordId,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
relationObjectMetadataNameSingular: string;
|
||||
relationFieldName: string;
|
||||
relationRecordId: string;
|
||||
sourceObjectNameSingular: string;
|
||||
sourceRecordId: string;
|
||||
targetObjectNameSingular: string;
|
||||
fieldNameOnTargetRecord: string;
|
||||
targetRecordId: string;
|
||||
}) => {
|
||||
const recordTypeName = capitalize(objectNameSingular);
|
||||
const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular);
|
||||
const sourceRecordTypeName = capitalize(sourceObjectNameSingular);
|
||||
const targetRecordTypeName = capitalize(targetObjectNameSingular);
|
||||
|
||||
const targetRecordCacheId = cache.identify({
|
||||
id: targetRecordId,
|
||||
__typename: targetRecordTypeName,
|
||||
});
|
||||
|
||||
cache.modify<StoreObject>({
|
||||
id: cache.identify({
|
||||
id: relationRecordId,
|
||||
__typename: relationRecordTypeName,
|
||||
}),
|
||||
id: targetRecordCacheId,
|
||||
fields: {
|
||||
[relationFieldName]: (cachedFieldValue, { toReference }) => {
|
||||
const nodeReference = toReference({
|
||||
id: recordId,
|
||||
__typename: recordTypeName,
|
||||
[fieldNameOnTargetRecord]: (targetRecordFieldValue, { toReference }) => {
|
||||
const fieldValueIsCachedObjectRecordConnection =
|
||||
isCachedObjectRecordConnection(
|
||||
sourceObjectNameSingular,
|
||||
targetRecordFieldValue,
|
||||
);
|
||||
|
||||
const sourceRecordReference = toReference({
|
||||
id: sourceRecordId,
|
||||
__typename: sourceRecordTypeName,
|
||||
});
|
||||
|
||||
if (!nodeReference) return cachedFieldValue;
|
||||
if (!isDefined(sourceRecordReference)) {
|
||||
return targetRecordFieldValue;
|
||||
}
|
||||
|
||||
if (
|
||||
isCachedObjectRecordConnection(objectNameSingular, cachedFieldValue)
|
||||
) {
|
||||
// To many objects => add record to next relation field list
|
||||
if (fieldValueIsCachedObjectRecordConnection) {
|
||||
const nextEdges: CachedObjectRecordEdge[] = [
|
||||
...cachedFieldValue.edges,
|
||||
...targetRecordFieldValue.edges,
|
||||
{
|
||||
__typename: `${recordTypeName}Edge`,
|
||||
node: nodeReference,
|
||||
__typename: `${sourceRecordTypeName}Edge`,
|
||||
node: sourceRecordReference,
|
||||
cursor: '',
|
||||
},
|
||||
];
|
||||
return { ...cachedFieldValue, edges: nextEdges };
|
||||
}
|
||||
|
||||
// To one object => attach next relation record
|
||||
return nodeReference;
|
||||
return {
|
||||
...targetRecordFieldValue,
|
||||
edges: nextEdges,
|
||||
};
|
||||
} else {
|
||||
// To one object => attach next relation record
|
||||
return sourceRecordReference;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -4,9 +4,8 @@ import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
|
||||
|
||||
/*
|
||||
TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are.
|
||||
@ -16,32 +15,32 @@ import { capitalize } from '~/utils/string/capitalize';
|
||||
export const triggerCreateRecordsOptimisticEffect = ({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records,
|
||||
getRelationMetadata,
|
||||
recordsToCreate,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
records: CachedObjectRecord[];
|
||||
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
|
||||
recordsToCreate: CachedObjectRecord[];
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
}) => {
|
||||
const objectEdgeTypeName = `${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}Edge`;
|
||||
const objectEdgeTypeName = getEdgeTypename({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
records.forEach((record) =>
|
||||
recordsToCreate.forEach((record) =>
|
||||
triggerUpdateRelationsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
previousRecord: null,
|
||||
nextRecord: record,
|
||||
getRelationMetadata,
|
||||
sourceObjectMetadataItem: objectMetadataItem,
|
||||
currentSourceRecord: null,
|
||||
updatedSourceRecord: record,
|
||||
objectMetadataItems,
|
||||
}),
|
||||
);
|
||||
|
||||
cache.modify<StoreObject>({
|
||||
fields: {
|
||||
[objectMetadataItem.namePlural]: (
|
||||
cachedConnection,
|
||||
rootQueryCachedResponse,
|
||||
{
|
||||
DELETE: _DELETE,
|
||||
readField,
|
||||
@ -49,42 +48,49 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
||||
toReference,
|
||||
},
|
||||
) => {
|
||||
if (
|
||||
!isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
cachedConnection,
|
||||
)
|
||||
)
|
||||
return cachedConnection;
|
||||
|
||||
/* const { variables } =
|
||||
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||
storeFieldName,
|
||||
); */
|
||||
|
||||
const cachedEdges = readField<CachedObjectRecordEdge[]>(
|
||||
'edges',
|
||||
cachedConnection,
|
||||
const shouldSkip = !isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
const nextCachedEdges = cachedEdges ? [...cachedEdges] : [];
|
||||
|
||||
const hasAddedRecords = records
|
||||
.map((record) => {
|
||||
/* const matchesFilter =
|
||||
!variables?.filter ||
|
||||
isRecordMatchingFilter({
|
||||
record,
|
||||
filter: variables.filter,
|
||||
objectMetadataItem,
|
||||
}); */
|
||||
if (shouldSkip) {
|
||||
return rootQueryCachedResponse;
|
||||
}
|
||||
|
||||
if (/* matchesFilter && */ record.id) {
|
||||
const nodeReference = toReference(record);
|
||||
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
|
||||
|
||||
if (nodeReference) {
|
||||
nextCachedEdges.unshift({
|
||||
const rootQueryCachedRecordEdges = readField<CachedObjectRecordEdge[]>(
|
||||
'edges',
|
||||
rootQueryCachedObjectRecordConnection,
|
||||
);
|
||||
const nextRootQueryCachedRecordEdges = rootQueryCachedRecordEdges
|
||||
? [...rootQueryCachedRecordEdges]
|
||||
: [];
|
||||
|
||||
const hasAddedRecords = recordsToCreate
|
||||
.map((recordToCreate) => {
|
||||
if (recordToCreate.id) {
|
||||
const recordToCreateReference = toReference(recordToCreate);
|
||||
|
||||
if (!recordToCreateReference) {
|
||||
throw new Error(
|
||||
`Failed to create reference for record with id: ${recordToCreate.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const recordAlreadyInCache = rootQueryCachedRecordEdges?.some(
|
||||
(cachedEdge) => {
|
||||
return (
|
||||
cache.identify(recordToCreateReference) ===
|
||||
cache.identify(cachedEdge.node)
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (recordToCreateReference && !recordAlreadyInCache) {
|
||||
nextRootQueryCachedRecordEdges.unshift({
|
||||
__typename: objectEdgeTypeName,
|
||||
node: nodeReference,
|
||||
node: recordToCreateReference,
|
||||
cursor: '',
|
||||
});
|
||||
|
||||
@ -96,30 +102,14 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
||||
})
|
||||
.some((hasAddedRecord) => hasAddedRecord);
|
||||
|
||||
if (!hasAddedRecords) return cachedConnection;
|
||||
|
||||
/* if (variables?.orderBy) {
|
||||
nextCachedEdges = sortCachedObjectEdges({
|
||||
edges: nextCachedEdges,
|
||||
orderBy: variables.orderBy,
|
||||
readCacheField: readField,
|
||||
});
|
||||
if (!hasAddedRecords) {
|
||||
return rootQueryCachedObjectRecordConnection;
|
||||
}
|
||||
|
||||
if (isDefined(variables?.first)) {
|
||||
if (
|
||||
cachedEdges?.length === variables.first &&
|
||||
nextCachedEdges.length < variables.first
|
||||
) {
|
||||
return DELETE;
|
||||
}
|
||||
|
||||
if (nextCachedEdges.length > variables.first) {
|
||||
nextCachedEdges.splice(variables.first);
|
||||
}
|
||||
} */
|
||||
|
||||
return { ...cachedConnection, edges: nextCachedEdges };
|
||||
return {
|
||||
...rootQueryCachedObjectRecordConnection,
|
||||
edges: nextRootQueryCachedRecordEdges,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -5,7 +5,6 @@ import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effe
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
@ -13,70 +12,79 @@ import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
export const triggerDeleteRecordsOptimisticEffect = ({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records,
|
||||
getRelationMetadata,
|
||||
recordsToDelete,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
records: CachedObjectRecord[];
|
||||
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
|
||||
recordsToDelete: CachedObjectRecord[];
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
}) => {
|
||||
cache.modify<StoreObject>({
|
||||
fields: {
|
||||
[objectMetadataItem.namePlural]: (
|
||||
cachedConnection,
|
||||
rootQueryCachedResponse,
|
||||
{ DELETE, readField, storeFieldName },
|
||||
) => {
|
||||
if (
|
||||
const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
|
||||
!isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
cachedConnection,
|
||||
)
|
||||
) {
|
||||
return cachedConnection;
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) {
|
||||
return rootQueryCachedResponse;
|
||||
}
|
||||
|
||||
const { variables } =
|
||||
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
|
||||
|
||||
const { fieldArguments: rootQueryVariables } =
|
||||
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||
storeFieldName,
|
||||
);
|
||||
|
||||
const recordIds = records.map(({ id }) => id);
|
||||
const recordIdsToDelete = recordsToDelete.map(({ id }) => id);
|
||||
|
||||
const cachedEdges = readField<CachedObjectRecordEdge[]>(
|
||||
'edges',
|
||||
cachedConnection,
|
||||
rootQueryCachedObjectRecordConnection,
|
||||
);
|
||||
|
||||
const nextCachedEdges =
|
||||
cachedEdges?.filter((cachedEdge) => {
|
||||
const nodeId = readField<string>('id', cachedEdge.node);
|
||||
return nodeId && !recordIds.includes(nodeId);
|
||||
|
||||
return nodeId && !recordIdsToDelete.includes(nodeId);
|
||||
}) || [];
|
||||
|
||||
if (nextCachedEdges.length === cachedEdges?.length)
|
||||
return cachedConnection;
|
||||
return rootQueryCachedObjectRecordConnection;
|
||||
|
||||
// TODO: same as in update, should we trigger DELETE ?
|
||||
if (
|
||||
isDefined(variables?.first) &&
|
||||
cachedEdges?.length === variables.first
|
||||
isDefined(rootQueryVariables?.first) &&
|
||||
cachedEdges?.length === rootQueryVariables.first
|
||||
) {
|
||||
return DELETE;
|
||||
}
|
||||
|
||||
return { ...cachedConnection, edges: nextCachedEdges };
|
||||
return {
|
||||
...rootQueryCachedObjectRecordConnection,
|
||||
edges: nextCachedEdges,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
records.forEach((record) => {
|
||||
recordsToDelete.forEach((recordToDelete) => {
|
||||
triggerUpdateRelationsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
previousRecord: record,
|
||||
nextRecord: null,
|
||||
getRelationMetadata,
|
||||
sourceObjectMetadataItem: objectMetadataItem,
|
||||
currentSourceRecord: recordToDelete,
|
||||
updatedSourceRecord: null,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
cache.evict({ id: cache.identify(record) });
|
||||
cache.evict({ id: cache.identify(recordToDelete) });
|
||||
});
|
||||
};
|
||||
|
@ -5,44 +5,60 @@ import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const triggerDetachRelationOptimisticEffect = ({
|
||||
cache,
|
||||
objectNameSingular,
|
||||
recordId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
relationRecordId,
|
||||
sourceObjectNameSingular,
|
||||
sourceRecordId,
|
||||
targetObjectNameSingular,
|
||||
fieldNameOnTargetRecord,
|
||||
targetRecordId,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
relationObjectMetadataNameSingular: string;
|
||||
relationFieldName: string;
|
||||
relationRecordId: string;
|
||||
sourceObjectNameSingular: string;
|
||||
sourceRecordId: string;
|
||||
targetObjectNameSingular: string;
|
||||
fieldNameOnTargetRecord: string;
|
||||
targetRecordId: string;
|
||||
}) => {
|
||||
const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular);
|
||||
const targetRecordTypeName = capitalize(targetObjectNameSingular);
|
||||
|
||||
const targetRecordCacheId = cache.identify({
|
||||
id: targetRecordId,
|
||||
__typename: targetRecordTypeName,
|
||||
});
|
||||
|
||||
cache.modify<StoreObject>({
|
||||
id: cache.identify({
|
||||
id: relationRecordId,
|
||||
__typename: relationRecordTypeName,
|
||||
}),
|
||||
id: targetRecordCacheId,
|
||||
fields: {
|
||||
[relationFieldName]: (cachedFieldValue, { isReference, readField }) => {
|
||||
// To many objects => remove record from previous relation field list
|
||||
if (
|
||||
isCachedObjectRecordConnection(objectNameSingular, cachedFieldValue)
|
||||
) {
|
||||
const nextEdges = cachedFieldValue.edges.filter(
|
||||
({ node }) => readField('id', node) !== recordId,
|
||||
[fieldNameOnTargetRecord]: (
|
||||
targetRecordFieldValue,
|
||||
{ isReference, readField },
|
||||
) => {
|
||||
const isRelationTargetFieldAnObjectRecordConnection =
|
||||
isCachedObjectRecordConnection(
|
||||
sourceObjectNameSingular,
|
||||
targetRecordFieldValue,
|
||||
);
|
||||
return { ...cachedFieldValue, edges: nextEdges };
|
||||
|
||||
if (isRelationTargetFieldAnObjectRecordConnection) {
|
||||
const relationTargetFieldEdgesWithoutRelationSourceRecordToDetach =
|
||||
targetRecordFieldValue.edges.filter(
|
||||
({ node }) => readField('id', node) !== sourceRecordId,
|
||||
);
|
||||
|
||||
return {
|
||||
...targetRecordFieldValue,
|
||||
edges: relationTargetFieldEdgesWithoutRelationSourceRecordToDetach,
|
||||
};
|
||||
}
|
||||
|
||||
// To one object => detach previous relation record
|
||||
if (isReference(cachedFieldValue)) {
|
||||
const isRelationTargetFieldASingleObjectRecord = isReference(
|
||||
targetRecordFieldValue,
|
||||
);
|
||||
|
||||
if (isRelationTargetFieldASingleObjectRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cachedFieldValue;
|
||||
return targetRecordFieldValue;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -6,124 +6,185 @@ import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effe
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
// TODO: add extensive unit tests for this function
|
||||
// That will also serve as documentation
|
||||
export const triggerUpdateRecordOptimisticEffect = ({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
previousRecord,
|
||||
nextRecord,
|
||||
getRelationMetadata,
|
||||
currentRecord,
|
||||
updatedRecord,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
previousRecord: CachedObjectRecord;
|
||||
nextRecord: CachedObjectRecord;
|
||||
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
|
||||
currentRecord: CachedObjectRecord;
|
||||
updatedRecord: CachedObjectRecord;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
}) => {
|
||||
const objectEdgeTypeName = `${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}Edge`;
|
||||
const objectEdgeTypeName = getEdgeTypename({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
triggerUpdateRelationsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
previousRecord,
|
||||
nextRecord,
|
||||
getRelationMetadata,
|
||||
sourceObjectMetadataItem: objectMetadataItem,
|
||||
currentSourceRecord: currentRecord,
|
||||
updatedSourceRecord: updatedRecord,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
// Optimistically update record lists
|
||||
cache.modify<StoreObject>({
|
||||
fields: {
|
||||
[objectMetadataItem.namePlural]: (
|
||||
cachedConnection,
|
||||
rootQueryCachedResponse,
|
||||
{ DELETE, readField, storeFieldName, toReference },
|
||||
) => {
|
||||
if (
|
||||
const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
|
||||
!isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
cachedConnection,
|
||||
)
|
||||
)
|
||||
return cachedConnection;
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
const { variables } =
|
||||
if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) {
|
||||
return rootQueryCachedResponse;
|
||||
}
|
||||
|
||||
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
|
||||
|
||||
const { fieldArguments: rootQueryVariables } =
|
||||
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||
storeFieldName,
|
||||
);
|
||||
|
||||
const cachedEdges = readField<CachedObjectRecordEdge[]>(
|
||||
'edges',
|
||||
cachedConnection,
|
||||
);
|
||||
let nextCachedEdges = cachedEdges ? [...cachedEdges] : [];
|
||||
const rootQueryCurrentCachedRecordEdges =
|
||||
readField<CachedObjectRecordEdge[]>(
|
||||
'edges',
|
||||
rootQueryCachedObjectRecordConnection,
|
||||
) ?? [];
|
||||
|
||||
// Test if the record matches this list's filters
|
||||
if (variables?.filter) {
|
||||
const matchesFilter = isRecordMatchingFilter({
|
||||
record: nextRecord,
|
||||
filter: variables.filter,
|
||||
objectMetadataItem,
|
||||
});
|
||||
const recordIndex = nextCachedEdges.findIndex(
|
||||
(cachedEdge) => readField('id', cachedEdge.node) === nextRecord.id,
|
||||
);
|
||||
let rootQueryNextCachedRecordEdges = [
|
||||
...rootQueryCurrentCachedRecordEdges,
|
||||
];
|
||||
|
||||
// If after update, the record matches this list's filters, then add it to the list
|
||||
if (matchesFilter && recordIndex === -1) {
|
||||
const nodeReference = toReference(nextRecord);
|
||||
nodeReference &&
|
||||
nextCachedEdges.push({
|
||||
const rootQueryFilter = rootQueryVariables?.filter;
|
||||
const rootQueryOrderBy = rootQueryVariables?.orderBy;
|
||||
const rootQueryLimit = rootQueryVariables?.first;
|
||||
|
||||
const shouldTestThatUpdatedRecordMatchesThisRootQueryFilter =
|
||||
isDefined(rootQueryFilter);
|
||||
|
||||
if (shouldTestThatUpdatedRecordMatchesThisRootQueryFilter) {
|
||||
const updatedRecordMatchesThisRootQueryFilter =
|
||||
isRecordMatchingFilter({
|
||||
record: updatedRecord,
|
||||
filter: rootQueryFilter,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const updatedRecordIndexInRootQueryEdges =
|
||||
rootQueryCurrentCachedRecordEdges.findIndex(
|
||||
(cachedEdge) =>
|
||||
readField('id', cachedEdge.node) === updatedRecord.id,
|
||||
);
|
||||
|
||||
const updatedRecordShouldBeAddedToRootQueryEdges =
|
||||
updatedRecordMatchesThisRootQueryFilter &&
|
||||
updatedRecordIndexInRootQueryEdges === -1;
|
||||
|
||||
const updatedRecordShouldBeRemovedFromRootQueryEdges =
|
||||
updatedRecordMatchesThisRootQueryFilter &&
|
||||
updatedRecordIndexInRootQueryEdges === -1;
|
||||
|
||||
if (updatedRecordShouldBeAddedToRootQueryEdges) {
|
||||
const updatedRecordNodeReference = toReference(updatedRecord);
|
||||
|
||||
if (isDefined(updatedRecordNodeReference)) {
|
||||
rootQueryNextCachedRecordEdges.push({
|
||||
__typename: objectEdgeTypeName,
|
||||
node: nodeReference,
|
||||
node: updatedRecordNodeReference,
|
||||
cursor: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If after update, the record does not match this list's filters anymore, then remove it from the list
|
||||
if (!matchesFilter && recordIndex > -1) {
|
||||
nextCachedEdges.splice(recordIndex, 1);
|
||||
if (updatedRecordShouldBeRemovedFromRootQueryEdges) {
|
||||
rootQueryNextCachedRecordEdges.splice(
|
||||
updatedRecordIndexInRootQueryEdges,
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort updated list
|
||||
if (variables?.orderBy) {
|
||||
nextCachedEdges = sortCachedObjectEdges({
|
||||
edges: nextCachedEdges,
|
||||
orderBy: variables.orderBy,
|
||||
const nextRootQueryEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
|
||||
|
||||
if (nextRootQueryEdgesShouldBeSorted) {
|
||||
rootQueryNextCachedRecordEdges = sortCachedObjectEdges({
|
||||
edges: rootQueryNextCachedRecordEdges,
|
||||
orderBy: rootQueryOrderBy,
|
||||
readCacheField: readField,
|
||||
});
|
||||
}
|
||||
|
||||
// Limit the updated list to the required size
|
||||
if (isDefined(variables?.first)) {
|
||||
const shouldLimitNextRootQueryEdges = isDefined(rootQueryLimit);
|
||||
|
||||
// TODO: not sure that we should trigger a DELETE here, as it will trigger a network request
|
||||
// Is it the responsibility of this optimistic effect function to delete a root query that will trigger a network request ?
|
||||
// Shouldn't we let the response from the network overwrite the cache and keep this util purely about cache updates ?
|
||||
//
|
||||
// Shoud we even apply the limit at all since with pagination we cannot really do optimistic rendering and should
|
||||
// wait for the network response to update the cache
|
||||
//
|
||||
// Maybe we could apply a merging function instead and exclude limit from the caching field arguments ?
|
||||
// Also we have a problem that is not yet present with this but may arise if we start
|
||||
// to use limit arguments, as for now we rely on the hard coded limit of 60 in pg_graphql.
|
||||
// This is as if we had a { limit: 60 } argument in every query but we don't.
|
||||
// so Apollo correctly merges the return of fetchMore for now, because of this,
|
||||
// but wouldn't do it well like Thomas had the problem with mail threads
|
||||
// because he applied a limit of 2 and Apollo created one root query in the cache for each.
|
||||
// In Thomas' case we should implement this because he use a hack to overwrite the first request with the return of the other.
|
||||
// See: https://www.apollographql.com/docs/react/pagination/cursor-based/#relay-style-cursor-pagination
|
||||
// See: https://www.apollographql.com/docs/react/pagination/core-api/#merging-queries
|
||||
if (shouldLimitNextRootQueryEdges) {
|
||||
// If previous edges length was exactly at the required limit,
|
||||
// but after update next edges length is under the limit,
|
||||
// we cannot for sure know if re-fetching the query
|
||||
// would return more edges, so we cannot optimistically deduce
|
||||
// the query's result.
|
||||
// In this case, invalidate the cache entry so it can be re-fetched.
|
||||
if (
|
||||
cachedEdges?.length === variables.first &&
|
||||
nextCachedEdges.length < variables.first
|
||||
) {
|
||||
const rootQueryCurrentCachedRecordEdgesLengthIsAtLimit =
|
||||
rootQueryCurrentCachedRecordEdges.length === rootQueryLimit;
|
||||
|
||||
// If next edges length is under limit, then we can wait for the network response and merge the result
|
||||
// then in the merge function we could implement this mechanism to limit the number of edges in the cache
|
||||
const rootQueryNextCachedRecordEdgesLengthIsUnderLimit =
|
||||
rootQueryNextCachedRecordEdges.length < rootQueryLimit;
|
||||
|
||||
const shouldDeleteRootQuerySoItCanBeRefetched =
|
||||
rootQueryCurrentCachedRecordEdgesLengthIsAtLimit &&
|
||||
rootQueryNextCachedRecordEdgesLengthIsUnderLimit;
|
||||
|
||||
if (shouldDeleteRootQuerySoItCanBeRefetched) {
|
||||
return DELETE;
|
||||
}
|
||||
|
||||
// If next edges length exceeds the required limit,
|
||||
// trim the next edges array to the correct length.
|
||||
if (nextCachedEdges.length > variables.first) {
|
||||
nextCachedEdges.splice(variables.first);
|
||||
const rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit =
|
||||
rootQueryNextCachedRecordEdges.length > rootQueryLimit;
|
||||
|
||||
if (rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit) {
|
||||
rootQueryNextCachedRecordEdges.splice(rootQueryLimit);
|
||||
}
|
||||
}
|
||||
|
||||
return { ...cachedConnection, edges: nextCachedEdges };
|
||||
return {
|
||||
...rootQueryCachedObjectRecordConnection,
|
||||
edges: rootQueryNextCachedRecordEdges,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,111 +1,147 @@
|
||||
import { ApolloCache } from '@apollo/client';
|
||||
|
||||
import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition';
|
||||
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { coreObjectNamesToDeleteOnRelationDetach } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH as CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const triggerUpdateRelationsOptimisticEffect = ({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
previousRecord,
|
||||
nextRecord,
|
||||
getRelationMetadata,
|
||||
sourceObjectMetadataItem,
|
||||
currentSourceRecord,
|
||||
updatedSourceRecord,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
previousRecord: CachedObjectRecord | null;
|
||||
nextRecord: CachedObjectRecord | null;
|
||||
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
|
||||
sourceObjectMetadataItem: ObjectMetadataItem;
|
||||
currentSourceRecord: CachedObjectRecord | null;
|
||||
updatedSourceRecord: CachedObjectRecord | null;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
}) =>
|
||||
// Optimistically update relation records
|
||||
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
||||
if (nextRecord && !(fieldMetadataItem.name in nextRecord)) return;
|
||||
sourceObjectMetadataItem.fields.forEach((fieldMetadataItemOnSourceRecord) => {
|
||||
const notARelationField =
|
||||
fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.Relation;
|
||||
|
||||
const relationMetadata = getRelationMetadata({
|
||||
fieldMetadataItem,
|
||||
if (notARelationField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldDoesNotExist =
|
||||
isDefined(updatedSourceRecord) &&
|
||||
!(fieldMetadataItemOnSourceRecord.name in updatedSourceRecord);
|
||||
|
||||
if (fieldDoesNotExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relationDefinition = getRelationDefinition({
|
||||
fieldMetadataItemOnSourceRecord,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
if (!relationMetadata) return;
|
||||
if (!relationDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
// Object metadata for the related record
|
||||
relationObjectMetadataItem,
|
||||
// Field on the related record
|
||||
relationFieldMetadataItem,
|
||||
} = relationMetadata;
|
||||
const { targetObjectMetadataItem, fieldMetadataItemOnTargetRecord } =
|
||||
relationDefinition;
|
||||
|
||||
const previousFieldValue:
|
||||
const currentFieldValueOnSourceRecord:
|
||||
| ObjectRecordConnection
|
||||
| CachedObjectRecord
|
||||
| null = previousRecord?.[fieldMetadataItem.name];
|
||||
const nextFieldValue: ObjectRecordConnection | CachedObjectRecord | null =
|
||||
nextRecord?.[fieldMetadataItem.name];
|
||||
| null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
if (isDeeplyEqual(previousFieldValue, nextFieldValue)) return;
|
||||
const updatedFieldValueOnSourceRecord:
|
||||
| ObjectRecordConnection
|
||||
| CachedObjectRecord
|
||||
| null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||
|
||||
const isPreviousFieldValueRecordConnection = isObjectRecordConnection(
|
||||
relationObjectMetadataItem.nameSingular,
|
||||
previousFieldValue,
|
||||
);
|
||||
const relationRecordsToDetach = isPreviousFieldValueRecordConnection
|
||||
? previousFieldValue.edges.map(({ node }) => node as CachedObjectRecord)
|
||||
: [previousFieldValue].filter(isDefined);
|
||||
if (
|
||||
isDeeplyEqual(
|
||||
currentFieldValueOnSourceRecord,
|
||||
updatedFieldValueOnSourceRecord,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isNextFieldValueRecordConnection = isObjectRecordConnection(
|
||||
relationObjectMetadataItem.nameSingular,
|
||||
nextFieldValue,
|
||||
);
|
||||
const relationRecordsToAttach = isNextFieldValueRecordConnection
|
||||
? nextFieldValue.edges.map(({ node }) => node as CachedObjectRecord)
|
||||
: [nextFieldValue].filter(isDefined);
|
||||
const currentFieldValueOnSourceRecordIsARecordConnection =
|
||||
isObjectRecordConnection(
|
||||
targetObjectMetadataItem.nameSingular,
|
||||
currentFieldValueOnSourceRecord,
|
||||
);
|
||||
|
||||
if (previousRecord && relationRecordsToDetach.length) {
|
||||
const shouldDeleteRelationRecord =
|
||||
coreObjectNamesToDeleteOnRelationDetach.includes(
|
||||
relationObjectMetadataItem.nameSingular as CoreObjectNameSingular,
|
||||
const targetRecordsToDetachFrom =
|
||||
currentFieldValueOnSourceRecordIsARecordConnection
|
||||
? currentFieldValueOnSourceRecord.edges.map(
|
||||
({ node }) => node as CachedObjectRecord,
|
||||
)
|
||||
: [currentFieldValueOnSourceRecord].filter(isDefined);
|
||||
|
||||
const updatedFieldValueOnSourceRecordIsARecordConnection =
|
||||
isObjectRecordConnection(
|
||||
targetObjectMetadataItem.nameSingular,
|
||||
updatedFieldValueOnSourceRecord,
|
||||
);
|
||||
|
||||
const targetRecordsToAttachTo =
|
||||
updatedFieldValueOnSourceRecordIsARecordConnection
|
||||
? updatedFieldValueOnSourceRecord.edges.map(
|
||||
({ node }) => node as CachedObjectRecord,
|
||||
)
|
||||
: [updatedFieldValueOnSourceRecord].filter(isDefined);
|
||||
|
||||
const shouldDetachSourceFromAllTargets =
|
||||
isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0;
|
||||
|
||||
if (shouldDetachSourceFromAllTargets) {
|
||||
const shouldStartByDeletingRelationTargetRecordsFromCache =
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH.includes(
|
||||
targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
|
||||
if (shouldDeleteRelationRecord) {
|
||||
if (shouldStartByDeletingRelationTargetRecordsFromCache) {
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: relationObjectMetadataItem,
|
||||
records: relationRecordsToDetach,
|
||||
getRelationMetadata,
|
||||
objectMetadataItem: targetObjectMetadataItem,
|
||||
recordsToDelete: targetRecordsToDetachFrom,
|
||||
objectMetadataItems,
|
||||
});
|
||||
} else {
|
||||
relationRecordsToDetach.forEach((relationRecordToDetach) => {
|
||||
targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
|
||||
triggerDetachRelationOptimisticEffect({
|
||||
cache,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
recordId: previousRecord.id,
|
||||
relationFieldName: relationFieldMetadataItem.name,
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem.nameSingular,
|
||||
relationRecordId: relationRecordToDetach.id,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: currentSourceRecord.id,
|
||||
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
|
||||
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
|
||||
targetRecordId: targetRecordToDetachFrom.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (nextRecord && relationRecordsToAttach.length) {
|
||||
relationRecordsToAttach.forEach((relationRecordToAttach) =>
|
||||
const shouldAttachSourceToAllTargets =
|
||||
updatedSourceRecord && targetRecordsToAttachTo.length;
|
||||
|
||||
if (shouldAttachSourceToAllTargets) {
|
||||
targetRecordsToAttachTo.forEach((targetRecordToAttachTo) =>
|
||||
triggerAttachRelationOptimisticEffect({
|
||||
cache,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
recordId: nextRecord.id,
|
||||
relationFieldName: relationFieldMetadataItem.name,
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem.nameSingular,
|
||||
relationRecordId: relationRecordToAttach.id,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: updatedSourceRecord.id,
|
||||
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
|
||||
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
|
||||
targetRecordId: targetRecordToAttachTo.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
|
||||
export const coreObjectNamesToDeleteOnRelationDetach = [
|
||||
export const CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH = [
|
||||
CoreObjectNameSingular.Favorite,
|
||||
];
|
||||
|
@ -8,10 +8,15 @@ import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
export const useMapFieldMetadataToGraphQLQuery = () => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const mapFieldMetadataToGraphQLQuery = (
|
||||
field: FieldMetadataItem,
|
||||
maxDepthForRelations: number = 2,
|
||||
): any => {
|
||||
const mapFieldMetadataToGraphQLQuery = ({
|
||||
field,
|
||||
maxDepthForRelations = 2,
|
||||
onlyTypenameAndIdOnDeepestRelationFields = false,
|
||||
}: {
|
||||
field: FieldMetadataItem;
|
||||
maxDepthForRelations?: number;
|
||||
onlyTypenameAndIdOnDeepestRelationFields?: boolean;
|
||||
}): any => {
|
||||
if (maxDepthForRelations <= 0) {
|
||||
return '';
|
||||
}
|
||||
@ -45,14 +50,25 @@ export const useMapFieldMetadataToGraphQLQuery = () => {
|
||||
(field.toRelationMetadata as any)?.fromObjectMetadata?.id,
|
||||
);
|
||||
|
||||
let subfieldQuery = '';
|
||||
|
||||
if (maxDepthForRelations > 0) {
|
||||
subfieldQuery = `${(relationMetadataItem?.fields ?? [])
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
maxDepthForRelations: maxDepthForRelations - 1,
|
||||
onlyTypenameAndIdOnDeepestRelationFields,
|
||||
}),
|
||||
)
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
return `${field.name}
|
||||
{
|
||||
__typename
|
||||
id
|
||||
${(relationMetadataItem?.fields ?? [])
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
||||
)
|
||||
.join('\n')}
|
||||
${subfieldQuery}
|
||||
}`;
|
||||
} else if (
|
||||
fieldType === 'RELATION' &&
|
||||
@ -64,14 +80,25 @@ export const useMapFieldMetadataToGraphQLQuery = () => {
|
||||
(field.toRelationMetadata as any)?.fromObjectMetadata?.id,
|
||||
);
|
||||
|
||||
let subfieldQuery = '';
|
||||
|
||||
if (maxDepthForRelations > 0) {
|
||||
subfieldQuery = `${(relationMetadataItem?.fields ?? [])
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
maxDepthForRelations: maxDepthForRelations - 1,
|
||||
onlyTypenameAndIdOnDeepestRelationFields,
|
||||
}),
|
||||
)
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
return `${field.name}
|
||||
{
|
||||
__typename
|
||||
id
|
||||
${(relationMetadataItem?.fields ?? [])
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
||||
)
|
||||
.join('\n')}
|
||||
${subfieldQuery}
|
||||
}`;
|
||||
} else if (
|
||||
fieldType === 'RELATION' &&
|
||||
@ -83,16 +110,27 @@ export const useMapFieldMetadataToGraphQLQuery = () => {
|
||||
(field.fromRelationMetadata as any)?.toObjectMetadata?.id,
|
||||
);
|
||||
|
||||
let subfieldQuery = '';
|
||||
|
||||
if (maxDepthForRelations > 0) {
|
||||
subfieldQuery = `${(relationMetadataItem?.fields ?? [])
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
maxDepthForRelations: maxDepthForRelations - 1,
|
||||
onlyTypenameAndIdOnDeepestRelationFields,
|
||||
}),
|
||||
)
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
return `${field.name}
|
||||
{
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
id
|
||||
${(relationMetadataItem?.fields ?? [])
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
||||
)
|
||||
.join('\n')}
|
||||
${subfieldQuery}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
|
||||
export const useObjectMetadataItems = () => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
return {
|
||||
objectMetadataItems,
|
||||
};
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const getObjectMetadataItemByNameSingular = ({
|
||||
objectMetadataItems,
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const foundObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
if (!foundObjectMetadataItem) {
|
||||
throw new Error(
|
||||
`Could not find object metadata item with singular name ${objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
return foundObjectMetadataItem;
|
||||
};
|
@ -17,12 +17,19 @@ export const getObjectRecordIdentifier = ({
|
||||
}): ObjectRecordIdentifier => {
|
||||
switch (objectMetadataItem.nameSingular) {
|
||||
case CoreObjectNameSingular.WorkspaceMember: {
|
||||
const workspaceMember = record as WorkspaceMember;
|
||||
const workspaceMember = record as Partial<WorkspaceMember> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const name = workspaceMember.name
|
||||
? `${workspaceMember.name?.firstName ?? ''} ${
|
||||
workspaceMember.name?.lastName ?? ''
|
||||
}`
|
||||
: '';
|
||||
|
||||
return {
|
||||
id: workspaceMember.id,
|
||||
name:
|
||||
workspaceMember.name.firstName + ' ' + workspaceMember.name.lastName,
|
||||
name,
|
||||
avatarUrl: workspaceMember.avatarUrl ?? undefined,
|
||||
avatarType: 'rounded',
|
||||
};
|
||||
|
@ -0,0 +1 @@
|
||||
export const MAX_QUERY_DEPTH_FOR_CACHE_INJECTION = 1;
|
@ -4,7 +4,8 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
|
||||
import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection';
|
||||
import { useInjectIntoFindOneRecordQueryCache } from '@/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
@ -17,47 +18,44 @@ export const useAddRecordInCache = ({
|
||||
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const generateFindOneRecordQuery = useGenerateFindOneRecordQuery();
|
||||
|
||||
const findOneRecordQuery = generateFindOneRecordQuery({
|
||||
objectMetadataItem,
|
||||
});
|
||||
const { injectIntoFindOneRecordQueryCache } =
|
||||
useInjectIntoFindOneRecordQueryCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
(record: ObjectRecord) => {
|
||||
const fragment = gql`
|
||||
fragment Create${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}InCache on ${capitalize(objectMetadataItem.nameSingular)} {
|
||||
__typename
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
maxDepthForRelations: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
|
||||
const cachedObjectRecord = {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...record,
|
||||
};
|
||||
|
||||
apolloClient.writeFragment({
|
||||
id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`,
|
||||
fragment: gql`
|
||||
fragment Create${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}InCache on ${capitalize(objectMetadataItem.nameSingular)} {
|
||||
__typename
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
.join('\n')}
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...record,
|
||||
},
|
||||
fragment,
|
||||
data: cachedObjectRecord,
|
||||
});
|
||||
|
||||
// TODO: Turn into injectIntoFindOneRecordQueryCache
|
||||
apolloClient.writeQuery({
|
||||
query: findOneRecordQuery,
|
||||
variables: {
|
||||
objectRecordId: record.id,
|
||||
},
|
||||
data: {
|
||||
[objectMetadataItem.nameSingular]: {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...record,
|
||||
},
|
||||
},
|
||||
});
|
||||
// TODO: should we keep this here ? Or should the caller of createOneRecordInCache/createManyRecordsInCache be responsible for this ?
|
||||
injectIntoFindOneRecordQueryCache(cachedObjectRecord);
|
||||
|
||||
// TODO: remove this once we get rid of entityFieldsFamilyState
|
||||
set(recordStoreFamilyState(record.id), record);
|
||||
@ -66,7 +64,7 @@ export const useAddRecordInCache = ({
|
||||
objectMetadataItem,
|
||||
apolloClient,
|
||||
mapFieldMetadataToGraphQLQuery,
|
||||
findOneRecordQuery,
|
||||
injectIntoFindOneRecordQueryCache,
|
||||
],
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,50 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
|
||||
export const useAppendToFindManyRecordsQueryInCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { readFindManyRecordsQueryInCache } =
|
||||
useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const {
|
||||
upsertFindManyRecordsQueryInCache: overwriteFindManyRecordsQueryInCache,
|
||||
} = useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const appendToFindManyRecordsQueryInCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
queryVariables,
|
||||
objectRecordsToAppend,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
objectRecordsToAppend: T[];
|
||||
}) => {
|
||||
const existingObjectRecords = readFindManyRecordsQueryInCache({
|
||||
queryVariables,
|
||||
});
|
||||
|
||||
const newObjectRecordList = [
|
||||
...existingObjectRecords,
|
||||
...objectRecordsToAppend,
|
||||
];
|
||||
|
||||
overwriteFindManyRecordsQueryInCache({
|
||||
objectRecordsToOverwrite: newObjectRecordList,
|
||||
queryVariables,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
appendToFindManyRecordsQueryInCache,
|
||||
};
|
||||
};
|
@ -27,7 +27,7 @@ export const useGetRecordFromCache = ({
|
||||
fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery({ field }))
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useInjectIntoFindOneRecordQueryCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const generateFindOneRecordQuery = useGenerateFindOneRecordQuery();
|
||||
|
||||
const injectIntoFindOneRecordQueryCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>(
|
||||
record: T,
|
||||
) => {
|
||||
const findOneRecordQueryForCacheInjection = generateFindOneRecordQuery({
|
||||
objectMetadataItem,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
apolloClient.writeQuery({
|
||||
query: findOneRecordQueryForCacheInjection,
|
||||
variables: {
|
||||
objectRecordId: record.id,
|
||||
},
|
||||
data: {
|
||||
[objectMetadataItem.nameSingular]: {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...record,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
injectIntoFindOneRecordQueryCache,
|
||||
};
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordQueryResult } from '@/object-record/types/ObjectRecordQueryResult';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useReadFindManyRecordsQueryInCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery();
|
||||
|
||||
const readFindManyRecordsQueryInCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
queryVariables,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
}) => {
|
||||
const findManyRecordsQueryForCacheRead = generateFindManyRecordsQuery({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const existingRecordsQueryResult = apolloClient.readQuery<
|
||||
ObjectRecordQueryResult<T>
|
||||
>({
|
||||
query: findManyRecordsQueryForCacheRead,
|
||||
variables: queryVariables,
|
||||
});
|
||||
|
||||
const existingRecordConnection =
|
||||
existingRecordsQueryResult?.[objectMetadataItem.namePlural];
|
||||
|
||||
const existingObjectRecords = isDefined(existingRecordConnection)
|
||||
? getRecordsFromRecordConnection({
|
||||
recordConnection: existingRecordConnection,
|
||||
})
|
||||
: [];
|
||||
|
||||
return existingObjectRecords;
|
||||
};
|
||||
|
||||
return {
|
||||
readFindManyRecordsQueryInCache,
|
||||
};
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection';
|
||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||
import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
|
||||
export const useUpsertFindManyRecordsQueryInCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery();
|
||||
|
||||
const upsertFindManyRecordsQueryInCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
queryVariables,
|
||||
objectRecordsToOverwrite,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
objectRecordsToOverwrite: T[];
|
||||
}) => {
|
||||
const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({
|
||||
objectMetadataItem,
|
||||
depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION,
|
||||
});
|
||||
|
||||
const newObjectRecordConnection = getRecordConnectionFromRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
records: objectRecordsToOverwrite,
|
||||
});
|
||||
|
||||
apolloClient.writeQuery({
|
||||
query: findManyRecordsQueryForCacheOverwrite,
|
||||
variables: queryVariables,
|
||||
data: {
|
||||
[objectMetadataItem.namePlural]: newObjectRecordConnection,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
upsertFindManyRecordsQueryInCache,
|
||||
};
|
||||
};
|
@ -2,14 +2,18 @@ import { useApolloClient } from '@apollo/client';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
|
||||
type CreateManyRecordsOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
};
|
||||
|
||||
export const useCreateManyRecords = <
|
||||
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
@ -27,15 +31,22 @@ export const useCreateManyRecords = <
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const createManyRecords = async (data: Partial<CreatedObjectRecord>[]) => {
|
||||
const sanitizedCreateManyRecordsInput = data.map((input) =>
|
||||
sanitizeRecordInput({
|
||||
const createManyRecords = async (
|
||||
data: Partial<CreatedObjectRecord>[],
|
||||
options?: CreateManyRecordsOptions,
|
||||
) => {
|
||||
const sanitizedCreateManyRecordsInput = data.map((input) => {
|
||||
const idForCreation = input.id ?? v4();
|
||||
|
||||
const sanitizedRecordInput = sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: { ...input, id: v4() },
|
||||
}),
|
||||
);
|
||||
recordInput: { ...input, id: idForCreation },
|
||||
});
|
||||
|
||||
return sanitizedRecordInput;
|
||||
});
|
||||
|
||||
const optimisticallyCreatedRecords = sanitizedCreateManyRecordsInput.map(
|
||||
(record) =>
|
||||
@ -51,21 +62,25 @@ export const useCreateManyRecords = <
|
||||
variables: {
|
||||
data: sanitizedCreateManyRecordsInput,
|
||||
},
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: optimisticallyCreatedRecords,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: optimisticallyCreatedRecords,
|
||||
},
|
||||
update: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
|
||||
if (!records?.length) return;
|
||||
if (!records?.length) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records,
|
||||
getRelationMetadata,
|
||||
});
|
||||
},
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: records,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return createdObjects.data?.[mutationResponseField] ?? [];
|
||||
|
@ -22,7 +22,7 @@ export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const createManyRecordsInCache = async (data: Partial<T>[]) => {
|
||||
const createManyRecordsInCache = (data: Partial<T>[]) => {
|
||||
const recordsWithId = data.map((record) => ({
|
||||
...record,
|
||||
id: (record.id as string) ?? v4(),
|
||||
|
@ -2,8 +2,8 @@ import { useApolloClient } from '@apollo/client';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@ -13,6 +13,10 @@ type useCreateOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
type CreateOneRecordOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
};
|
||||
|
||||
export const useCreateOneRecord = <
|
||||
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
@ -29,12 +33,17 @@ export const useCreateOneRecord = <
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const createOneRecord = async (
|
||||
input: Partial<CreatedObjectRecord>,
|
||||
options?: CreateOneRecordOptions,
|
||||
) => {
|
||||
const idForCreation = input.id ?? v4();
|
||||
|
||||
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
||||
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: { ...input, id: v4() },
|
||||
recordInput: { ...input, id: idForCreation },
|
||||
});
|
||||
|
||||
const optimisticallyCreatedRecord =
|
||||
@ -51,21 +60,25 @@ export const useCreateOneRecord = <
|
||||
variables: {
|
||||
input: sanitizedCreateOneRecordInput,
|
||||
},
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: optimisticallyCreatedRecord,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: optimisticallyCreatedRecord,
|
||||
},
|
||||
update: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
|
||||
if (!record) return;
|
||||
if (!record) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records: [record],
|
||||
getRelationMetadata,
|
||||
});
|
||||
},
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [record],
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return createdObject.data?.[mutationResponseField] ?? null;
|
||||
|
@ -23,7 +23,7 @@ export const useCreateOneRecordInCache = <T>({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const createOneRecordInCache = async (input: ObjectRecord) => {
|
||||
const createOneRecordInCache = (input: ObjectRecord) => {
|
||||
const generatedCachedObjectRecord =
|
||||
generateObjectRecordOptimisticResponse(input);
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
@ -20,7 +20,7 @@ export const useDeleteManyRecords = ({
|
||||
const { objectMetadataItem, deleteManyRecordsMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
@ -50,8 +50,8 @@ export const useDeleteManyRecords = ({
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records: cachedRecords,
|
||||
getRelationMetadata,
|
||||
recordsToDelete: cachedRecords,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -2,8 +2,8 @@ import { useCallback } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
@ -20,7 +20,7 @@ export const useDeleteOneRecord = ({
|
||||
const { objectMetadataItem, deleteOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const mutationResponseField =
|
||||
getDeleteOneRecordMutationResponseField(objectNameSingular);
|
||||
@ -48,8 +48,8 @@ export const useDeleteOneRecord = ({
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records: [cachedRecord],
|
||||
getRelationMetadata,
|
||||
recordsToDelete: [cachedRecord],
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -60,10 +60,10 @@ export const useDeleteOneRecord = ({
|
||||
apolloClient,
|
||||
deleteOneRecordMutation,
|
||||
getRecordFromCache,
|
||||
getRelationMetadata,
|
||||
mutationResponseField,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -17,6 +17,7 @@ export const useFieldContext = ({
|
||||
isLabelIdentifier = false,
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
customUseUpdateOneObjectHook,
|
||||
}: {
|
||||
clearable?: boolean;
|
||||
fieldMetadataName: string;
|
||||
@ -24,6 +25,7 @@ export const useFieldContext = ({
|
||||
isLabelIdentifier?: boolean;
|
||||
objectNameSingular: string;
|
||||
objectRecordId: string;
|
||||
customUseUpdateOneObjectHook?: RecordUpdateHook;
|
||||
}) => {
|
||||
const { basePathToShowPage, objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -65,7 +67,8 @@ export const useFieldContext = ({
|
||||
position: fieldPosition,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectMutation,
|
||||
useUpdateRecord:
|
||||
customUseUpdateOneObjectHook ?? useUpdateOneObjectMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
clearable,
|
||||
}}
|
||||
|
@ -2,10 +2,9 @@ import { useQuery } from '@apollo/client';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const useFindOneRecord = <
|
||||
ObjectType extends { id: string } & Record<string, any>,
|
||||
>({
|
||||
export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
||||
objectNameSingular,
|
||||
objectRecordId = '',
|
||||
onCompleted,
|
||||
@ -13,7 +12,7 @@ export const useFindOneRecord = <
|
||||
skip,
|
||||
}: ObjectMetadataItemIdentifier & {
|
||||
objectRecordId: string | undefined;
|
||||
onCompleted?: (data: ObjectType) => void;
|
||||
onCompleted?: (data: T) => void;
|
||||
skip?: boolean;
|
||||
depth?: number;
|
||||
}) => {
|
||||
@ -23,7 +22,7 @@ export const useFindOneRecord = <
|
||||
);
|
||||
|
||||
const { data, loading, error } = useQuery<
|
||||
{ [nameSingular: string]: ObjectType },
|
||||
{ [nameSingular: string]: T },
|
||||
{ objectRecordId: string }
|
||||
>(findOneRecordQuery, {
|
||||
skip: !objectMetadataItem || !objectRecordId || skip,
|
||||
|
@ -31,7 +31,11 @@ export const useGenerateCreateManyRecordMutation = ({
|
||||
${mutationResponseField}(data: $data) {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}`;
|
||||
|
@ -31,7 +31,11 @@ export const useGenerateCreateOneRecordMutation = ({
|
||||
${mutationResponseField}(data: $input) {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,11 @@ export const useGenerateExecuteQuickActionOnOneRecordMutation = ({
|
||||
${graphQLFieldForExecuteQuickActionOnOneRecordMutation}(id: $idToExecuteQuickActionOn) {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,12 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({
|
||||
node {
|
||||
id
|
||||
${fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field, depth))
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
maxDepthForRelations: depth,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
cursor
|
||||
|
@ -28,7 +28,12 @@ export const useGenerateFindManyRecordsQuery = () => {
|
||||
node {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field, depth))
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
maxDepthForRelations: depth,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
cursor
|
||||
|
@ -12,8 +12,8 @@ export const useGenerateFindOneRecordQuery = () => {
|
||||
}: {
|
||||
objectMetadataItem: Pick<ObjectMetadataItem, 'nameSingular' | 'fields'>;
|
||||
depth?: number;
|
||||
}) =>
|
||||
gql`
|
||||
}) => {
|
||||
return gql`
|
||||
query FindOne${objectMetadataItem.nameSingular}($objectRecordId: UUID!) {
|
||||
${objectMetadataItem.nameSingular}(filter: {
|
||||
id: {
|
||||
@ -22,9 +22,15 @@ export const useGenerateFindOneRecordQuery = () => {
|
||||
}){
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field, depth))
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
field,
|
||||
maxDepthForRelations: depth,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
`;
|
||||
`;
|
||||
};
|
||||
};
|
||||
|
@ -31,7 +31,7 @@ export const useGenerateUpdateOneRecordMutation = ({
|
||||
${mutationResponseField}(id: $idToUpdate, data: $input) {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery({ field }))
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@ -27,7 +27,7 @@ export const useUpdateOneRecord = <
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const updateOneRecord = async ({
|
||||
idToUpdate,
|
||||
@ -69,9 +69,9 @@ export const useUpdateOneRecord = <
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
previousRecord: cachedRecord,
|
||||
nextRecord: record,
|
||||
getRelationMetadata,
|
||||
currentRecord: cachedRecord,
|
||||
updatedRecord: record,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ChangeEvent, useCallback, useContext, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import { IconTrash } from '@/ui/display/icon';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -8,7 +9,6 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor';
|
||||
import { mainColorNames, ThemeColor } from '@/ui/theme/constants/colors';
|
||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
||||
import { debounce } from '~/utils/debounce';
|
||||
|
||||
import { BoardColumnContext } from '../contexts/BoardColumnContext';
|
||||
import { useRecordBoardDeprecated } from '../hooks/useRecordBoardDeprecated';
|
||||
|
@ -36,7 +36,7 @@ export const generateEmptyFieldValue = (
|
||||
return null;
|
||||
}
|
||||
case FieldMetadataType.Uuid: {
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
case FieldMetadataType.Boolean: {
|
||||
return true;
|
||||
|
@ -0,0 +1,8 @@
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||
|
||||
export const mapEdgeToObjectRecord = <T extends ObjectRecord>(
|
||||
objectRecordEdge: ObjectRecordEdge<T>,
|
||||
) => {
|
||||
return objectRecordEdge.node as T;
|
||||
};
|
@ -58,7 +58,9 @@ export const RightDrawer = () => {
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [rightDrawerRef],
|
||||
callback: () => closeRightDrawer(),
|
||||
callback: () => {
|
||||
closeRightDrawer();
|
||||
},
|
||||
mode: ClickOutsideMode.comparePixels,
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2';
|
||||
import { ActivityType } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
@ -19,17 +19,19 @@ const StyledContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const ShowPageAddButton = ({
|
||||
entity,
|
||||
activityTargetObject,
|
||||
}: {
|
||||
entity: ActivityTargetableObject;
|
||||
activityTargetObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { closeDropdown, toggleDropdown } = useDropdown(
|
||||
SHOW_PAGE_ADD_BUTTON_DROPDOWN_ID,
|
||||
);
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
const { closeDropdown, toggleDropdown } = useDropdown('add-show-page');
|
||||
const openCreateActivity = useOpenCreateActivityDrawerV2();
|
||||
|
||||
const handleSelect = (type: ActivityType) => {
|
||||
openCreateActivity({ type, targetableObjects: [entity] });
|
||||
openCreateActivity({
|
||||
type,
|
||||
targetableObjects: [activityTargetObject],
|
||||
timelineTargetableObject: activityTargetObject,
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { clickOutsideListenerCallbacksStateScopeMap } from '@/ui/utilities/pointer-event/states/clickOutsideListenerCallbacksStateScopeMap';
|
||||
import { clickOutsideListenerIsActivatedStateScopeMap } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedStateScopeMap';
|
||||
import { clickOutsideListenerIsMouseDownInsideStateScopeMap } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsMouseDownInsideStateScopeMap';
|
||||
import { lockedListenerIdState } from '@/ui/utilities/pointer-event/states/lockedListenerIdState';
|
||||
@ -9,6 +10,10 @@ export const useClickOustideListenerStates = (componentId: string) => {
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
getClickOutsideListenerCallbacksState: getState(
|
||||
clickOutsideListenerCallbacksStateScopeMap,
|
||||
scopeId,
|
||||
),
|
||||
getClickOutsideListenerIsMouseDownInsideState: getState(
|
||||
clickOutsideListenerIsMouseDownInsideStateScopeMap,
|
||||
scopeId,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||
@ -5,14 +6,18 @@ import {
|
||||
ClickOutsideListenerProps,
|
||||
useListenClickOutsideV2,
|
||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
|
||||
import { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback';
|
||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useClickOutsideListener = (componentId: string) => {
|
||||
// TODO: improve typing
|
||||
const scopeId = getScopeIdFromComponentId(componentId) ?? '';
|
||||
|
||||
const { getClickOutsideListenerIsActivatedState } =
|
||||
useClickOustideListenerStates(componentId);
|
||||
const {
|
||||
getClickOutsideListenerIsActivatedState,
|
||||
getClickOutsideListenerCallbacksState,
|
||||
} = useClickOustideListenerStates(componentId);
|
||||
|
||||
const useListenClickOutside = <T extends Element>({
|
||||
callback,
|
||||
@ -23,7 +28,21 @@ export const useClickOutsideListener = (componentId: string) => {
|
||||
return useListenClickOutsideV2({
|
||||
listenerId: componentId,
|
||||
refs,
|
||||
callback,
|
||||
callback: useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(event) => {
|
||||
callback(event);
|
||||
|
||||
const additionalCallbacks = snapshot
|
||||
.getLoadable(getClickOutsideListenerCallbacksState())
|
||||
.getValue();
|
||||
|
||||
additionalCallbacks.forEach((additionalCallback) => {
|
||||
additionalCallback.callbackFunction(event);
|
||||
});
|
||||
},
|
||||
[callback],
|
||||
),
|
||||
enabled,
|
||||
mode,
|
||||
});
|
||||
@ -37,9 +56,97 @@ export const useClickOutsideListener = (componentId: string) => {
|
||||
[getClickOutsideListenerIsActivatedState],
|
||||
);
|
||||
|
||||
const registerOnClickOutsideCallback = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({ callbackFunction, callbackId }: ClickOutsideListenerCallback) => {
|
||||
const existingCallbacks = snapshot
|
||||
.getLoadable(getClickOutsideListenerCallbacksState())
|
||||
.getValue();
|
||||
|
||||
const existingCallbackWithSameId = existingCallbacks.find(
|
||||
(callback) => callback.callbackId === callbackId,
|
||||
);
|
||||
|
||||
if (!isDefined(existingCallbackWithSameId)) {
|
||||
const existingCallbacksWithNewCallback = existingCallbacks.concat({
|
||||
callbackId,
|
||||
callbackFunction,
|
||||
});
|
||||
|
||||
set(
|
||||
getClickOutsideListenerCallbacksState(),
|
||||
existingCallbacksWithNewCallback,
|
||||
);
|
||||
} else {
|
||||
const existingCallbacksWithOverwrittenCallback = [
|
||||
...existingCallbacks,
|
||||
];
|
||||
|
||||
const indexOfExistingCallbackWithSameId =
|
||||
existingCallbacksWithOverwrittenCallback.findIndex(
|
||||
(callback) => callback.callbackId === callbackId,
|
||||
);
|
||||
|
||||
existingCallbacksWithOverwrittenCallback[
|
||||
indexOfExistingCallbackWithSameId
|
||||
] = {
|
||||
callbackId,
|
||||
callbackFunction,
|
||||
};
|
||||
|
||||
set(
|
||||
getClickOutsideListenerCallbacksState(),
|
||||
existingCallbacksWithOverwrittenCallback,
|
||||
);
|
||||
}
|
||||
},
|
||||
[getClickOutsideListenerCallbacksState],
|
||||
);
|
||||
|
||||
const unregisterOnClickOutsideCallback = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({ callbackId }: { callbackId: string }) => {
|
||||
const existingCallbacks = snapshot
|
||||
.getLoadable(getClickOutsideListenerCallbacksState())
|
||||
.getValue();
|
||||
|
||||
const indexOfCallbackToUnsubscribe = existingCallbacks.findIndex(
|
||||
(callback) => callback.callbackId === callbackId,
|
||||
);
|
||||
|
||||
const callbackToUnsubscribeIsFound = indexOfCallbackToUnsubscribe > -1;
|
||||
|
||||
if (callbackToUnsubscribeIsFound) {
|
||||
const newCallbacksWithoutCallbackToUnsubscribe =
|
||||
existingCallbacks.toSpliced(indexOfCallbackToUnsubscribe, 1);
|
||||
|
||||
set(
|
||||
getClickOutsideListenerCallbacksState(),
|
||||
newCallbacksWithoutCallbackToUnsubscribe,
|
||||
);
|
||||
}
|
||||
},
|
||||
[getClickOutsideListenerCallbacksState],
|
||||
);
|
||||
|
||||
const useRegisterClickOutsideListenerCallback = (
|
||||
callback: ClickOutsideListenerCallback,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
registerOnClickOutsideCallback(callback);
|
||||
|
||||
return () => {
|
||||
unregisterOnClickOutsideCallback({
|
||||
callbackId: callback.callbackId,
|
||||
});
|
||||
};
|
||||
}, [callback]);
|
||||
};
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
useListenClickOutside,
|
||||
toggleClickOutsideListener,
|
||||
useRegisterClickOutsideListenerCallback,
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback';
|
||||
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
|
||||
|
||||
export const clickOutsideListenerCallbacksStateScopeMap = createStateScopeMap<
|
||||
ClickOutsideListenerCallback[]
|
||||
>({
|
||||
key: 'clickOutsideListenerCallbacksStateScopeMap',
|
||||
defaultValue: [],
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
import { ClickOutsideListenerCallbackFunction } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallbackFunction';
|
||||
|
||||
export type ClickOutsideListenerCallback = {
|
||||
callbackId: string;
|
||||
callbackFunction: ClickOutsideListenerCallbackFunction;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export type ClickOutsideListenerCallbackFunction = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
) => void;
|
@ -94,7 +94,7 @@ export const RecordShowPage = () => {
|
||||
/>
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
activityTargetObject={{
|
||||
id: record.id,
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
targetObjectRecord: record,
|
||||
|
1
packages/twenty-front/src/types/StringKeyOf.ts
Normal file
1
packages/twenty-front/src/types/StringKeyOf.ts
Normal file
@ -0,0 +1 @@
|
||||
export type StringKeyOf<T> = Extract<keyof T, string>;
|
@ -1,12 +0,0 @@
|
||||
export const debounce = <FuncArgs extends any[]>(
|
||||
func: (...args: FuncArgs) => void,
|
||||
delay: number,
|
||||
) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return (...args: FuncArgs) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
@ -16,11 +16,11 @@ export const parseApolloStoreFieldName = <
|
||||
const fieldName = matches[1] as string;
|
||||
|
||||
try {
|
||||
const variables = stringifiedVariables
|
||||
const fieldArguments = stringifiedVariables
|
||||
? (JSON.parse(stringifiedVariables) as Variables)
|
||||
: undefined;
|
||||
|
||||
return { fieldName, variables };
|
||||
return { fieldName, fieldArguments };
|
||||
} catch {
|
||||
return { fieldName };
|
||||
}
|
||||
|
18
yarn.lock
18
yarn.lock
@ -22307,6 +22307,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debounce@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "debounce@npm:2.0.0"
|
||||
checksum: 35a492f7f4f346f0539d486d7e68a68bd2eaa0c7fb159df8e59f89a20afd9b0ed024d0dc5a6f283b6963f5f032dacd66d805a325e9d8338511e77b505b76f26e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.8, debug@npm:^2.6.9":
|
||||
version: 2.6.9
|
||||
resolution: "debug@npm:2.6.9"
|
||||
@ -43447,6 +43454,7 @@ __metadata:
|
||||
danger-plugin-todos: "npm:^1.3.1"
|
||||
dataloader: "npm:^2.2.2"
|
||||
date-fns: "npm:^2.30.0"
|
||||
debounce: "npm:^2.0.0"
|
||||
deep-equal: "npm:^2.2.2"
|
||||
docusaurus-node-polyfills: "npm:^1.0.0"
|
||||
dotenv-cli: "npm:^7.2.1"
|
||||
@ -43560,6 +43568,7 @@ __metadata:
|
||||
type-fest: "npm:^4.1.0"
|
||||
typeorm: "npm:^0.3.17"
|
||||
typescript: "npm:^5.3.3"
|
||||
use-debounce: "npm:^10.0.0"
|
||||
uuid: "npm:^9.0.0"
|
||||
vite: "npm:^5.0.0"
|
||||
vite-plugin-checker: "npm:^0.6.2"
|
||||
@ -44676,6 +44685,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-debounce@npm:^10.0.0":
|
||||
version: 10.0.0
|
||||
resolution: "use-debounce@npm:10.0.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: c1166cba52dedeab17e3e29275af89c57a3e8981b75f6e38ae2896ac36ecd4ed7d8fff5f882ba4b2f91eac7510d5ae0dd89fa4f7d081622ed436c3c89eda5cd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-isomorphic-layout-effect@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "use-isomorphic-layout-effect@npm:1.1.2"
|
||||
|
Loading…
Reference in New Issue
Block a user