Fix activity editor (#9165)
This commit is contained in:
@ -8,14 +8,11 @@ import { useDebouncedCallback } from 'use-debounce';
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
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 { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
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 { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
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 { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
|
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
|
||||||
|
import { ActivityRichTextEditorChangeOnActivityIdEffect } from '@/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect';
|
||||||
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
|
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
|
||||||
import { Note } from '@/activities/types/Note';
|
import { Note } from '@/activities/types/Note';
|
||||||
import { Task } from '@/activities/types/Task';
|
import { Task } from '@/activities/types/Task';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||||
import '@blocknote/core/fonts/inter.css';
|
import '@blocknote/core/fonts/inter.css';
|
||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
import '@blocknote/react/style.css';
|
import '@blocknote/react/style.css';
|
||||||
|
|
||||||
type RichTextEditorProps = {
|
type ActivityRichTextEditorProps = {
|
||||||
activityId: string;
|
activityId: string;
|
||||||
fillTitleFromBody: boolean;
|
|
||||||
activityObjectNameSingular:
|
activityObjectNameSingular:
|
||||||
| CoreObjectNameSingular.Task
|
| CoreObjectNameSingular.Task
|
||||||
| CoreObjectNameSingular.Note;
|
| CoreObjectNameSingular.Note;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RichTextEditor = ({
|
export const ActivityRichTextEditor = ({
|
||||||
activityId,
|
activityId,
|
||||||
fillTitleFromBody,
|
|
||||||
activityObjectNameSingular,
|
activityObjectNameSingular,
|
||||||
}: RichTextEditorProps) => {
|
}: ActivityRichTextEditorProps) => {
|
||||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||||
|
|
||||||
const cache = useApolloClient().cache;
|
const cache = useApolloClient().cache;
|
||||||
const activity = activityInStore as Task | Note | null;
|
const activity = activityInStore as Task | Note | null;
|
||||||
|
|
||||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
|
||||||
activityTitleHasBeenSetFamilyState({
|
|
||||||
activityId: activityId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [activityBody, setActivityBody] = useRecoilState(
|
|
||||||
activityBodyFamilyState({
|
|
||||||
activityId: activityId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular: activityObjectNameSingular,
|
objectNameSingular: activityObjectNameSingular,
|
||||||
@ -86,33 +71,6 @@ export const RichTextEditor = ({
|
|||||||
}
|
}
|
||||||
}, 300);
|
}, 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(
|
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
|
||||||
canCreateActivityState,
|
canCreateActivityState,
|
||||||
);
|
);
|
||||||
@ -156,24 +114,13 @@ export const RichTextEditor = ({
|
|||||||
setCanCreateActivity(true);
|
setCanCreateActivity(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
persistBodyDebounced(prepareBody(activityBody));
|
||||||
updateTitleAndBody(activityBody);
|
|
||||||
} else {
|
|
||||||
persistBodyDebounced(prepareBody(activityBody));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[
|
[persistBodyDebounced, setCanCreateActivity, canCreateActivity],
|
||||||
fillTitleFromBody,
|
|
||||||
persistBodyDebounced,
|
|
||||||
activityTitleHasBeenSet,
|
|
||||||
updateTitleAndBody,
|
|
||||||
setCanCreateActivity,
|
|
||||||
canCreateActivity,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBodyChange = useRecoilCallback(
|
const handleBodyChange = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ set }) =>
|
||||||
(newStringifiedBody: string) => {
|
(newStringifiedBody: string) => {
|
||||||
set(recordStoreFamilyState(activityId), (oldActivity) => {
|
set(recordStoreFamilyState(activityId), (oldActivity) => {
|
||||||
return {
|
return {
|
||||||
@ -195,79 +142,28 @@ export const RichTextEditor = ({
|
|||||||
objectMetadataItem: objectMetadataItemActivity,
|
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);
|
handlePersistBody(newStringifiedBody);
|
||||||
},
|
},
|
||||||
[
|
[activityId, cache, objectMetadataItemActivity, handlePersistBody],
|
||||||
activityId,
|
|
||||||
cache,
|
|
||||||
objectMetadataItemActivity,
|
|
||||||
fillTitleFromBody,
|
|
||||||
handlePersistBody,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500);
|
const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500);
|
||||||
|
|
||||||
// See https://github.com/twentyhq/twenty/issues/6724 for explanation
|
|
||||||
const setActivityBodyDebouncedToAvoidDragBug = useDebouncedCallback(
|
|
||||||
setActivityBody,
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleEditorChange = () => {
|
const handleEditorChange = () => {
|
||||||
const newStringifiedBody = JSON.stringify(editor.document) ?? '';
|
const newStringifiedBody = JSON.stringify(editor.document) ?? '';
|
||||||
|
|
||||||
setActivityBodyDebouncedToAvoidDragBug(newStringifiedBody);
|
|
||||||
|
|
||||||
handleBodyChangeDebounced(newStringifiedBody);
|
handleBodyChangeDebounced(newStringifiedBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialBody = useMemo(() => {
|
const initialBody = useMemo(() => {
|
||||||
if (isNonEmptyString(activityBody) && activityBody !== '{}') {
|
if (
|
||||||
return JSON.parse(activityBody);
|
|
||||||
} else if (
|
|
||||||
isDefined(activity) &&
|
isDefined(activity) &&
|
||||||
isNonEmptyString(activity.body) &&
|
isNonEmptyString(activity.body) &&
|
||||||
activity?.body !== '{}'
|
activity?.body !== '{}'
|
||||||
) {
|
) {
|
||||||
return JSON.parse(activity.body);
|
return JSON.parse(activity.body);
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}, [activity, activityBody]);
|
}, [activity]);
|
||||||
|
|
||||||
const handleEditorBuiltInUploadFile = async (file: File) => {
|
const handleEditorBuiltInUploadFile = async (file: File) => {
|
||||||
const { attachementAbsoluteURL } = await handleUploadAttachment(file);
|
const { attachementAbsoluteURL } = await handleUploadAttachment(file);
|
||||||
@ -367,11 +263,17 @@ export const RichTextEditor = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockEditor
|
<>
|
||||||
onFocus={handleBlockEditorFocus}
|
<ActivityRichTextEditorChangeOnActivityIdEffect
|
||||||
onBlur={handlerBlockEditorBlur}
|
editor={editor}
|
||||||
onChange={handleEditorChange}
|
activityId={activityId}
|
||||||
editor={editor}
|
/>
|
||||||
/>
|
<BlockEditor
|
||||||
|
onFocus={handleBlockEditorFocus}
|
||||||
|
onBlur={handlerBlockEditorBlur}
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
editor={editor}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -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 <></>;
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { RichTextEditor } from '@/activities/components/RichTextEditor';
|
import { ActivityRichTextEditor } from '@/activities/components/ActivityRichTextEditor';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
|
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
|
||||||
@ -28,9 +28,8 @@ export const ShowPageActivityContainer = ({
|
|||||||
componentInstanceId={`scroll-wrapper-tab-list-${targetableObject.id}`}
|
componentInstanceId={`scroll-wrapper-tab-list-${targetableObject.id}`}
|
||||||
>
|
>
|
||||||
<StyledShowPageActivityContainer>
|
<StyledShowPageActivityContainer>
|
||||||
<RichTextEditor
|
<ActivityRichTextEditor
|
||||||
activityId={targetableObject.id}
|
activityId={targetableObject.id}
|
||||||
fillTitleFromBody={false}
|
|
||||||
activityObjectNameSingular={
|
activityObjectNameSingular={
|
||||||
targetableObject.targetObjectNameSingular as
|
targetableObject.targetObjectNameSingular as
|
||||||
| CoreObjectNameSingular.Note
|
| CoreObjectNameSingular.Note
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export const ShowPageSubContainer = ({
|
|||||||
isNewRightDrawerItemLoading = false,
|
isNewRightDrawerItemLoading = false,
|
||||||
}: ShowPageSubContainerProps) => {
|
}: ShowPageSubContainerProps) => {
|
||||||
const { activeTabId } = useTabList(
|
const { activeTabId } = useTabList(
|
||||||
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
|
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}-${targetableObject.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
@ -128,7 +128,7 @@ export const ShowPageSubContainer = ({
|
|||||||
<TabList
|
<TabList
|
||||||
behaveAsLinks={!isInRightDrawer}
|
behaveAsLinks={!isInRightDrawer}
|
||||||
loading={loading || isNewViewableRecordLoading}
|
loading={loading || isNewViewableRecordLoading}
|
||||||
tabListInstanceId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`}
|
tabListInstanceId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}-${targetableObject.id}`}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
/>
|
/>
|
||||||
</StyledTabListContainer>
|
</StyledTabListContainer>
|
||||||
|
|||||||
Reference in New Issue
Block a user