From f499c728fdc0e91c84b2c33abb50b069c960bc88 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 20 Dec 2024 11:52:00 +0100 Subject: [PATCH] Fix activity editor (#9165) --- ...tEditor.tsx => ActivityRichTextEditor.tsx} | 144 +++--------------- ...RichTextEditorChangeOnActivityIdEffect.tsx | 27 ++++ .../useReplaceActivityBlockEditorContent.ts | 34 +++++ .../components/ShowPageActivityContainer.tsx | 5 +- .../components/ShowPageSubContainer.tsx | 4 +- 5 files changed, 88 insertions(+), 126 deletions(-) rename packages/twenty-front/src/modules/activities/components/{RichTextEditor.tsx => ActivityRichTextEditor.tsx} (68%) create mode 100644 packages/twenty-front/src/modules/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect.tsx create mode 100644 packages/twenty-front/src/modules/activities/hooks/useReplaceActivityBlockEditorContent.ts diff --git a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx similarity index 68% rename from packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx rename to packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index bff2bef3cb..f871566fff 100644 --- a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -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 ( - + <> + + + ); }; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect.tsx new file mode 100644 index 0000000000..5ca77b0cd1 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect.tsx @@ -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 <>; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useReplaceActivityBlockEditorContent.ts b/packages/twenty-front/src/modules/activities/hooks/useReplaceActivityBlockEditorContent.ts new file mode 100644 index 0000000000..7fbda840c3 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useReplaceActivityBlockEditorContent.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx index 93825d4252..2b17eebac2 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx @@ -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}`} > - { 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 = ({