Fix activity editor (#9165)

This commit is contained in:
Charles Bochet 2024-12-20 11:52:00 +01:00 committed by GitHub
parent 0d2bfad3c9
commit f499c728fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 88 additions and 126 deletions

View File

@ -8,14 +8,11 @@ import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
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';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -23,44 +20,32 @@ import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritin
import { isDefined } from '~/utils/isDefined';
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { ActivityRichTextEditorChangeOnActivityIdEffect } from '@/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css';
import '@blocknote/react/style.css';
type RichTextEditorProps = {
type ActivityRichTextEditorProps = {
activityId: string;
fillTitleFromBody: boolean;
activityObjectNameSingular:
| CoreObjectNameSingular.Task
| CoreObjectNameSingular.Note;
};
export const RichTextEditor = ({
export const ActivityRichTextEditor = ({
activityId,
fillTitleFromBody,
activityObjectNameSingular,
}: RichTextEditorProps) => {
}: ActivityRichTextEditorProps) => {
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
const cache = useApolloClient().cache;
const activity = activityInStore as Task | Note | null;
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
activityTitleHasBeenSetFamilyState({
activityId: activityId,
}),
);
const [activityBody, setActivityBody] = useRecoilState(
activityBodyFamilyState({
activityId: activityId,
}),
);
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({
objectNameSingular: activityObjectNameSingular,
@ -86,33 +71,6 @@ export const RichTextEditor = ({
}
}, 300);
const persistTitleAndBodyDebounced = useDebouncedCallback(
(newTitle: string, newBody: string) => {
if (isDefined(activity)) {
upsertActivity({
activity,
input: {
title: newTitle,
body: newBody,
},
});
setActivityTitleHasBeenSet(true);
}
},
200,
);
const updateTitleAndBody = useCallback(
(newStringifiedBody: string) => {
const blockBody = JSON.parse(newStringifiedBody);
const newTitleFromBody = blockBody[0]?.content?.[0]?.text;
persistTitleAndBodyDebounced(newTitleFromBody, newStringifiedBody);
},
[persistTitleAndBodyDebounced],
);
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
canCreateActivityState,
);
@ -156,24 +114,13 @@ export const RichTextEditor = ({
setCanCreateActivity(true);
}
if (!activityTitleHasBeenSet && fillTitleFromBody) {
updateTitleAndBody(activityBody);
} else {
persistBodyDebounced(prepareBody(activityBody));
}
persistBodyDebounced(prepareBody(activityBody));
},
[
fillTitleFromBody,
persistBodyDebounced,
activityTitleHasBeenSet,
updateTitleAndBody,
setCanCreateActivity,
canCreateActivity,
],
[persistBodyDebounced, setCanCreateActivity, canCreateActivity],
);
const handleBodyChange = useRecoilCallback(
({ snapshot, set }) =>
({ set }) =>
(newStringifiedBody: string) => {
set(recordStoreFamilyState(activityId), (oldActivity) => {
return {
@ -195,79 +142,28 @@ export const RichTextEditor = ({
objectMetadataItem: objectMetadataItemActivity,
});
const activityTitleHasBeenSet = snapshot
.getLoadable(
activityTitleHasBeenSetFamilyState({
activityId: activityId,
}),
)
.getValue();
const blockBody = JSON.parse(newStringifiedBody);
const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string;
if (!activityTitleHasBeenSet && fillTitleFromBody) {
set(recordStoreFamilyState(activityId), (oldActivity) => {
return {
...oldActivity,
id: activityId,
title: newTitleFromBody,
__typename: 'Activity',
};
});
modifyRecordFromCache({
recordId: activityId,
fieldModifiers: {
title: () => {
return newTitleFromBody;
},
},
cache,
objectMetadataItem: objectMetadataItemActivity,
});
}
handlePersistBody(newStringifiedBody);
},
[
activityId,
cache,
objectMetadataItemActivity,
fillTitleFromBody,
handlePersistBody,
],
[activityId, cache, objectMetadataItemActivity, handlePersistBody],
);
const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500);
// See https://github.com/twentyhq/twenty/issues/6724 for explanation
const setActivityBodyDebouncedToAvoidDragBug = useDebouncedCallback(
setActivityBody,
100,
);
const handleEditorChange = () => {
const newStringifiedBody = JSON.stringify(editor.document) ?? '';
setActivityBodyDebouncedToAvoidDragBug(newStringifiedBody);
handleBodyChangeDebounced(newStringifiedBody);
};
const initialBody = useMemo(() => {
if (isNonEmptyString(activityBody) && activityBody !== '{}') {
return JSON.parse(activityBody);
} else if (
if (
isDefined(activity) &&
isNonEmptyString(activity.body) &&
activity?.body !== '{}'
) {
return JSON.parse(activity.body);
} else {
return undefined;
}
}, [activity, activityBody]);
}, [activity]);
const handleEditorBuiltInUploadFile = async (file: File) => {
const { attachementAbsoluteURL } = await handleUploadAttachment(file);
@ -367,11 +263,17 @@ export const RichTextEditor = ({
};
return (
<BlockEditor
onFocus={handleBlockEditorFocus}
onBlur={handlerBlockEditorBlur}
onChange={handleEditorChange}
editor={editor}
/>
<>
<ActivityRichTextEditorChangeOnActivityIdEffect
editor={editor}
activityId={activityId}
/>
<BlockEditor
onFocus={handleBlockEditorFocus}
onBlur={handlerBlockEditorBlur}
onChange={handleEditorChange}
editor={editor}
/>
</>
);
};

View File

@ -0,0 +1,27 @@
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { useReplaceActivityBlockEditorContent } from '@/activities/hooks/useReplaceActivityBlockEditorContent';
import { useEffect, useState } from 'react';
type ActivityRichTextEditorChangeOnActivityIdEffectProps = {
activityId: string;
editor: typeof BLOCK_SCHEMA.BlockNoteEditor;
};
export const ActivityRichTextEditorChangeOnActivityIdEffect = ({
activityId,
editor,
}: ActivityRichTextEditorChangeOnActivityIdEffectProps) => {
const { replaceBlockEditorContent } =
useReplaceActivityBlockEditorContent(editor);
const [currentActivityId, setCurrentActivityId] = useState(activityId);
useEffect(() => {
if (currentActivityId !== activityId) {
replaceBlockEditorContent(activityId);
setCurrentActivityId(activityId);
}
}, [activityId, currentActivityId, replaceBlockEditorContent]);
return <></>;
};

View File

@ -0,0 +1,34 @@
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useReplaceActivityBlockEditorContent = (
editor: typeof BLOCK_SCHEMA.BlockNoteEditor,
) => {
const replaceBlockEditorContent = useRecoilCallback(
({ snapshot }) =>
(activityId: string) => {
if (isDefined(editor)) {
const activityInStore = snapshot
.getLoadable(recordStoreFamilyState(activityId))
.getValue();
const content = isNonEmptyString(activityInStore?.body)
? JSON.parse(activityInStore?.body)
: [{ type: 'paragraph', content: '' }];
if (!isDeeplyEqual(editor.document, content)) {
editor.replaceBlocks(editor.document, content);
}
}
},
[editor],
);
return {
replaceBlockEditorContent,
};
};

View File

@ -1,4 +1,4 @@
import { RichTextEditor } from '@/activities/components/RichTextEditor';
import { ActivityRichTextEditor } from '@/activities/components/ActivityRichTextEditor';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
@ -28,9 +28,8 @@ export const ShowPageActivityContainer = ({
componentInstanceId={`scroll-wrapper-tab-list-${targetableObject.id}`}
>
<StyledShowPageActivityContainer>
<RichTextEditor
<ActivityRichTextEditor
activityId={targetableObject.id}
fillTitleFromBody={false}
activityObjectNameSingular={
targetableObject.targetObjectNameSingular as
| CoreObjectNameSingular.Note

View File

@ -65,7 +65,7 @@ export const ShowPageSubContainer = ({
isNewRightDrawerItemLoading = false,
}: ShowPageSubContainerProps) => {
const { activeTabId } = useTabList(
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}-${targetableObject.id}`,
);
const isMobile = useIsMobile();
@ -128,7 +128,7 @@ export const ShowPageSubContainer = ({
<TabList
behaveAsLinks={!isInRightDrawer}
loading={loading || isNewViewableRecordLoading}
tabListInstanceId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`}
tabListInstanceId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}-${targetableObject.id}`}
tabs={tabs}
/>
</StyledTabListContainer>