Feat/activity optimistic activities (#4009)
* Fix naming * Fixed cache.evict bug for relation target deletion * Fixed cascade delete activity targets * Working version * Fix * fix * WIP * Fixed optimistic effect target inline cell * Removed openCreateActivityDrawer v1 * Ok for timeline * Removed console.log * Fix update record optimistic effect * Refactored activity queries into useActivities for everything * Fixed bugs * Cleaned * Fix lint --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,20 +1,22 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useBlockNote } from '@blocknote/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
@ -42,10 +44,6 @@ export const ActivityBodyEditor = ({
|
||||
activity,
|
||||
fillTitleFromBody,
|
||||
}: ActivityBodyEditorProps) => {
|
||||
const [stringifiedBodyFromEditor, setStringifiedBodyFromEditor] = useState<
|
||||
string | null
|
||||
>(activity.body);
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activity.id,
|
||||
@ -96,19 +94,21 @@ export const ActivityBodyEditor = ({
|
||||
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],
|
||||
[persistTitleAndBodyDebounced],
|
||||
);
|
||||
|
||||
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
|
||||
canCreateActivityState,
|
||||
);
|
||||
|
||||
const handleBodyChange = useCallback(
|
||||
(activityBody: string) => {
|
||||
if (!canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
updateTitleAndBody(activityBody);
|
||||
} else {
|
||||
@ -120,18 +120,11 @@ export const ActivityBodyEditor = ({
|
||||
persistBodyDebounced,
|
||||
activityTitleHasBeenSet,
|
||||
updateTitleAndBody,
|
||||
setCanCreateActivity,
|
||||
canCreateActivity,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isNonEmptyString(stringifiedBodyFromEditor) &&
|
||||
activity.body !== stringifiedBodyFromEditor
|
||||
) {
|
||||
handleBodyChange(stringifiedBodyFromEditor);
|
||||
}
|
||||
}, [stringifiedBodyFromEditor, handleBodyChange, activity]);
|
||||
|
||||
const slashMenuItems = getSlashMenu();
|
||||
|
||||
const [uploadFile] = useUploadFileMutation();
|
||||
@ -160,9 +153,57 @@ export const ActivityBodyEditor = ({
|
||||
? JSON.parse(activity.body)
|
||||
: undefined,
|
||||
domAttributes: { editor: { class: 'editor' } },
|
||||
onEditorContentChange: (editor: BlockNoteEditor) => {
|
||||
setStringifiedBodyFromEditor(JSON.stringify(editor.topLevelBlocks) ?? '');
|
||||
},
|
||||
onEditorContentChange: useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(editor: BlockNoteEditor) => {
|
||||
const newStringifiedBody =
|
||||
JSON.stringify(editor.topLevelBlocks) ?? '';
|
||||
|
||||
set(recordStoreFamilyState(activity.id), (oldActivity) => {
|
||||
return {
|
||||
...oldActivity,
|
||||
id: activity.id,
|
||||
body: newStringifiedBody,
|
||||
};
|
||||
});
|
||||
|
||||
modifyActivityFromCache(activity.id, {
|
||||
body: () => {
|
||||
return newStringifiedBody;
|
||||
},
|
||||
});
|
||||
|
||||
const activityTitleHasBeenSet = snapshot
|
||||
.getLoadable(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activity.id,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const blockBody = JSON.parse(newStringifiedBody);
|
||||
const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string;
|
||||
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
set(recordStoreFamilyState(activity.id), (oldActivity) => {
|
||||
return {
|
||||
...oldActivity,
|
||||
id: activity.id,
|
||||
title: newTitleFromBody,
|
||||
};
|
||||
});
|
||||
|
||||
modifyActivityFromCache(activity.id, {
|
||||
title: () => {
|
||||
return newTitleFromBody;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleBodyChange(newStringifiedBody);
|
||||
},
|
||||
[activity, fillTitleFromBody, modifyActivityFromCache, handleBodyChange],
|
||||
),
|
||||
slashMenuItems,
|
||||
blockSpecs: blockSpecs,
|
||||
uploadFile: handleUploadAttachment,
|
||||
|
||||
@ -8,7 +8,9 @@ import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdo
|
||||
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 { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
@ -18,6 +20,7 @@ import {
|
||||
} 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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
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';
|
||||
@ -78,8 +81,10 @@ export const ActivityEditor = ({
|
||||
const { deleteActivityFromCache } = useDeleteActivityFromCache();
|
||||
|
||||
const useUpsertOneActivityMutation: RecordUpdateHook = () => {
|
||||
const upsertActivityMutation = ({ variables }: RecordUpdateHookParams) => {
|
||||
upsertActivity({ activity, input: variables.updateOneRecordInput });
|
||||
const upsertActivityMutation = async ({
|
||||
variables,
|
||||
}: RecordUpdateHookParams) => {
|
||||
await upsertActivity({ activity, input: variables.updateOneRecordInput });
|
||||
};
|
||||
|
||||
return [upsertActivityMutation, { loading: false }];
|
||||
@ -104,6 +109,20 @@ export const ActivityEditor = ({
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
|
||||
isActivityInCreateModeState,
|
||||
);
|
||||
|
||||
const [isUpsertingActivityInDB] = useRecoilState(
|
||||
isUpsertingActivityInDBState,
|
||||
);
|
||||
|
||||
const [canCreateActivity] = useRecoilState(canCreateActivityState);
|
||||
|
||||
const [activityFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(activity.id),
|
||||
);
|
||||
|
||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
@ -112,16 +131,40 @@ export const ActivityEditor = ({
|
||||
fieldPosition: 2,
|
||||
});
|
||||
|
||||
const [isCreatingActivity, setIsCreatingActivity] = useRecoilState(
|
||||
isCreatingActivityState,
|
||||
);
|
||||
|
||||
useRegisterClickOutsideListenerCallback({
|
||||
callbackId: 'activity-editor',
|
||||
callbackFunction: () => {
|
||||
if (isCreatingActivity) {
|
||||
setIsCreatingActivity(false);
|
||||
deleteActivityFromCache(activity);
|
||||
if (isUpsertingActivityInDB || !activityFromStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
if (canCreateActivity) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: activityFromStore.title,
|
||||
body: activityFromStore.body,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
deleteActivityFromCache(activity);
|
||||
}
|
||||
|
||||
setIsActivityInCreateMode(false);
|
||||
} else {
|
||||
if (
|
||||
activityFromStore.title !== activity.title ||
|
||||
activityFromStore.body !== activity.body
|
||||
) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: activityFromStore.title,
|
||||
body: activityFromStore.body,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -12,14 +12,14 @@ const StyledContainer = styled.div`
|
||||
export const ActivityTargetChips = ({
|
||||
activityTargetObjectRecords,
|
||||
}: {
|
||||
activityTargetObjectRecords: ActivityTargetObjectRecord[];
|
||||
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
|
||||
}) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{activityTargetObjectRecords?.map((activityTargetObjectRecord) => (
|
||||
<RecordChip
|
||||
key={activityTargetObjectRecord.targetObjectRecord.id}
|
||||
record={activityTargetObjectRecord.targetObjectRecord}
|
||||
key={activityTargetObjectRecord.targetObject.id}
|
||||
record={activityTargetObjectRecord.targetObject}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
|
||||
}
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import { useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxShape,
|
||||
@ -57,7 +59,13 @@ type ActivityTitleProps = {
|
||||
};
|
||||
|
||||
export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
const [internalTitle, setInternalTitle] = useState(activity.title);
|
||||
const [activityInStore, setActivityInStore] = useRecoilState(
|
||||
recordStoreFamilyState(activity.id),
|
||||
);
|
||||
|
||||
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
|
||||
canCreateActivityState,
|
||||
);
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
@ -115,7 +123,17 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
}, 500);
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setInternalTitle(newTitle);
|
||||
setActivityInStore((currentActivity) => {
|
||||
return {
|
||||
...currentActivity,
|
||||
id: activity.id,
|
||||
title: newTitle,
|
||||
};
|
||||
});
|
||||
|
||||
if (isNonEmptyString(newTitle) && !canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
modifyActivityFromCache(activity.id, {
|
||||
title: () => {
|
||||
@ -153,7 +171,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
ref={titleInputRef}
|
||||
placeholder={`${activity.type} title`}
|
||||
onChange={(event) => handleTitleChange(event.target.value)}
|
||||
value={internalTitle}
|
||||
value={activityInStore?.title ?? ''}
|
||||
completed={completed}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
|
||||
Reference in New Issue
Block a user