Skip to content

Commit 44f0cfd

Browse files
authored
[Dashboards] Rich text editor frontend (#16437)
closes twentyhq/core-team-issues#1894
1 parent afcca28 commit 44f0cfd

File tree

42 files changed

+2054
-143
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2054
-143
lines changed

packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx

Lines changed: 16 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,27 @@ import { ActivityRichTextEditorChangeOnActivityIdEffect } from '@/activities/com
1818
import { type Attachment } from '@/activities/files/types/Attachment';
1919
import { type Note } from '@/activities/types/Note';
2020
import { type Task } from '@/activities/types/Task';
21-
import { filterAttachmentsToRestore } from '@/activities/utils/filterAttachmentsToRestore';
22-
import { getActivityAttachmentIdsAndNameToUpdate } from '@/activities/utils/getActivityAttachmentIdsAndNameToUpdate';
23-
import { getActivityAttachmentIdsToDelete } from '@/activities/utils/getActivityAttachmentIdsToDelete';
24-
import { getActivityAttachmentPathsToRestore } from '@/activities/utils/getActivityAttachmentPathsToRestore';
2521
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
2622
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
2723
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
28-
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
2924
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
30-
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
31-
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
32-
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
3325
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
3426
import { isTitleCellInEditModeComponentState } from '@/object-record/record-title-cell/states/isTitleCellInEditModeComponentState';
3527
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
3628
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
3729
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
30+
import { BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG } from '@/ui/input/editor/constants/BlockEditorGlobalHotkeysConfig';
31+
import { useAttachmentSync } from '@/ui/input/editor/hooks/useAttachmentSync';
32+
import { parseInitialBlocknote } from '@/ui/input/editor/utils/parseInitialBlocknote';
33+
import { prepareBodyWithSignedUrls } from '@/ui/input/editor/utils/prepareBodyWithSignedUrls';
3834
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
3935
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
4036
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
4137
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
42-
import type { PartialBlock } from '@blocknote/core';
4338
import '@blocknote/core/fonts/inter.css';
4439
import '@blocknote/mantine/style.css';
4540
import { useCreateBlockNote } from '@blocknote/react';
4641
import '@blocknote/react/style.css';
47-
import { isArray, isNonEmptyString } from '@sniptt/guards';
4842
import { isDefined } from 'twenty-shared/utils';
4943

5044
type ActivityRichTextEditorProps = {
@@ -72,14 +66,6 @@ export const ActivityRichTextEditor = ({
7266
(field) => field.name === 'bodyV2',
7367
);
7468

75-
const { deleteManyRecords: deleteAttachments } = useDeleteManyRecords({
76-
objectNameSingular: CoreObjectNameSingular.Attachment,
77-
});
78-
79-
const { restoreManyRecords: restoreAttachments } = useRestoreManyRecords({
80-
objectNameSingular: CoreObjectNameSingular.Attachment,
81-
});
82-
8369
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
8470
const { removeFocusItemFromFocusStackById } =
8571
useRemoveFocusItemFromFocusStackById();
@@ -102,18 +88,8 @@ export const ActivityRichTextEditor = ({
10288
},
10389
});
10490

105-
const { fetchAllRecords: findSoftDeletedAttachments } =
106-
useLazyFetchAllRecords({
107-
objectNameSingular: CoreObjectNameSingular.Attachment,
108-
filter: {
109-
deletedAt: {
110-
is: 'NOT_NULL',
111-
},
112-
},
113-
});
114-
const { updateOneRecord: updateOneAttachment } = useUpdateOneRecord({
115-
objectNameSingular: CoreObjectNameSingular.Attachment,
116-
});
91+
const { syncAttachments } = useAttachmentSync(attachments);
92+
11793
const { upsertActivity } = useUpsertActivity({
11894
activityObjectNameSingular: activityObjectNameSingular,
11995
});
@@ -155,37 +131,13 @@ export const ActivityRichTextEditor = ({
155131
});
156132
};
157133

158-
const prepareBody = (newStringifiedBody: string) => {
159-
if (!newStringifiedBody) return newStringifiedBody;
160-
161-
const body = JSON.parse(newStringifiedBody);
162-
163-
const bodyWithSignedPayload = body.map((block: any) => {
164-
if (block.type !== 'image' || !block.props.url) {
165-
return block;
166-
}
167-
168-
const imageProps = block.props;
169-
const imageUrl = new URL(imageProps.url);
170-
171-
return {
172-
...block,
173-
props: {
174-
...imageProps,
175-
url: `${imageUrl.toString()}`,
176-
},
177-
};
178-
});
179-
return JSON.stringify(bodyWithSignedPayload);
180-
};
181-
182134
const handlePersistBody = useCallback(
183135
(activityBody: string) => {
184136
if (!canCreateActivity) {
185137
setCanCreateActivity(true);
186138
}
187139

188-
persistBodyDebounced(prepareBody(activityBody));
140+
persistBodyDebounced(prepareBodyWithSignedUrls(activityBody));
189141
},
190142
[persistBodyDebounced, setCanCreateActivity, canCreateActivity],
191143
);
@@ -225,60 +177,17 @@ export const ActivityRichTextEditor = ({
225177

226178
handlePersistBody(newStringifiedBody);
227179

228-
const attachmentIdsToDelete = getActivityAttachmentIdsToDelete(
180+
await syncAttachments(
229181
newStringifiedBody,
230-
attachments,
231182
oldActivity?.bodyV2.blocknote,
232183
);
233-
234-
if (attachmentIdsToDelete.length > 0) {
235-
await deleteAttachments({
236-
recordIdsToDelete: attachmentIdsToDelete,
237-
});
238-
}
239-
240-
const attachmentPathsToRestore = getActivityAttachmentPathsToRestore(
241-
newStringifiedBody,
242-
attachments,
243-
);
244-
245-
if (attachmentPathsToRestore.length > 0) {
246-
const softDeletedAttachments =
247-
(await findSoftDeletedAttachments()) as Attachment[];
248-
249-
const attachmentIdsToRestore = filterAttachmentsToRestore(
250-
attachmentPathsToRestore,
251-
softDeletedAttachments,
252-
);
253-
254-
await restoreAttachments({
255-
idsToRestore: attachmentIdsToRestore,
256-
});
257-
}
258-
const attachmentsToUpdate = getActivityAttachmentIdsAndNameToUpdate(
259-
newStringifiedBody,
260-
attachments,
261-
);
262-
if (attachmentsToUpdate.length > 0) {
263-
for (const attachmentToUpdate of attachmentsToUpdate) {
264-
if (!attachmentToUpdate.id) continue;
265-
await updateOneAttachment({
266-
idToUpdate: attachmentToUpdate.id,
267-
updateOneRecordInput: { name: attachmentToUpdate.name },
268-
});
269-
}
270-
}
271184
},
272185
[
273-
attachments,
274186
activityId,
275187
cache,
276188
objectMetadataItemActivity,
277189
handlePersistBody,
278-
deleteAttachments,
279-
restoreAttachments,
280-
findSoftDeletedAttachments,
281-
updateOneAttachment,
190+
syncAttachments,
282191
],
283192
);
284193

@@ -291,35 +200,14 @@ export const ActivityRichTextEditor = ({
291200
};
292201

293202
const initialBody = useMemo(() => {
294-
const blocknote = activity?.bodyV2?.blocknote;
295-
296-
if (
297-
isDefined(activity) &&
298-
isNonEmptyString(blocknote) &&
299-
blocknote !== '{}'
300-
) {
301-
let parsedBody: PartialBlock[] | undefined = undefined;
302-
303-
// TODO: Remove this once we have removed the old rich text
304-
try {
305-
parsedBody = JSON.parse(blocknote);
306-
} catch {
307-
// eslint-disable-next-line no-console
308-
console.warn(
309-
`Failed to parse body for activity ${activityId}, for rich text version 'v2'`,
310-
);
311-
// eslint-disable-next-line no-console
312-
console.warn(blocknote);
313-
}
314-
315-
if (isArray(parsedBody) && parsedBody.length === 0) {
316-
return undefined;
317-
}
318-
319-
return parsedBody;
203+
if (!isDefined(activity)) {
204+
return undefined;
320205
}
321206

322-
return undefined;
207+
return parseInitialBlocknote(
208+
activity?.bodyV2?.blocknote,
209+
`Failed to parse body for activity ${activityId}, for rich text version 'v2'`,
210+
);
323211
}, [activity, activityId]);
324212

325213
const handleEditorBuiltInUploadFile = async (file: File) => {
@@ -431,10 +319,7 @@ export const ActivityRichTextEditor = ({
431319
type: FocusComponentType.ACTIVITY_RICH_TEXT_EDITOR,
432320
},
433321
focusId: activityId,
434-
globalHotkeysConfig: {
435-
enableGlobalHotkeysConflictingWithKeyboard: false,
436-
enableGlobalHotkeysWithModifiers: true,
437-
},
322+
globalHotkeysConfig: BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG,
438323
});
439324
},
440325
[recordTitleCellId, activityId, editor, pushFocusItemToFocusStack],

packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ export const usePageLayoutHeaderInfo = ({
157157
widgetInEditMode: undefined,
158158
};
159159
}
160-
161160
default:
162161
return null;
163162
}

packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ export const CommandMenuPageLayoutIframeSettings = () => {
2222

2323
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
2424

25-
if (!isDefined(widgetInEditMode)) {
26-
throw new Error('Widget ID must be present while editing the widget');
27-
}
28-
29-
const widgetConfiguration = widgetInEditMode.configuration;
25+
const widgetConfiguration = widgetInEditMode?.configuration;
3026

3127
const configUrl =
3228
widgetConfiguration && 'url' in widgetConfiguration
@@ -38,6 +34,10 @@ export const CommandMenuPageLayoutIframeSettings = () => {
3834
);
3935
const [urlError, setUrlError] = useState('');
4036

37+
if (!isDefined(widgetInEditMode)) {
38+
return null;
39+
}
40+
4141
const validateUrl = (urlString: string): boolean => {
4242
const trimmedUrl = urlString.trim();
4343

packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
import { CommandGroup } from '@/command-menu/components/CommandGroup';
22
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
33
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
4+
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
45
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
56
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
67
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
78
import { useCompanyDefaultChartConfig } from '@/page-layout/hooks/useCompanyDefaultChartConfig';
89
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
910
import { useCreatePageLayoutIframeWidget } from '@/page-layout/hooks/useCreatePageLayoutIframeWidget';
11+
import { useCreatePageLayoutStandaloneRichTextWidget } from '@/page-layout/hooks/useCreatePageLayoutStandaloneRichTextWidget';
1012
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
1113
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
1214
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
1315
import { t } from '@lingui/core/macro';
1416
import { isDefined } from 'twenty-shared/utils';
15-
import { IconChartPie, IconFrame } from 'twenty-ui/display';
17+
import {
18+
IconAlignBoxLeftTop,
19+
IconChartPie,
20+
IconFrame,
21+
} from 'twenty-ui/display';
1622
import { GraphType } from '~/generated-metadata/graphql';
1723

1824
export const CommandMenuPageLayoutWidgetTypeSelect = () => {
1925
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
2026

27+
const { closeCommandMenu } = useCommandMenu();
28+
2129
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
2230

2331
const { buildBarChartFieldSelection } = useCompanyDefaultChartConfig();
@@ -28,6 +36,9 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
2836
const { createPageLayoutIframeWidget } =
2937
useCreatePageLayoutIframeWidget(pageLayoutId);
3038

39+
const { createPageLayoutStandaloneRichTextWidget } =
40+
useCreatePageLayoutStandaloneRichTextWidget(pageLayoutId);
41+
3142
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
3243
useRecoilComponentState(
3344
pageLayoutEditingWidgetIdComponentState,
@@ -64,8 +75,23 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
6475
});
6576
};
6677

78+
const handleNavigateToRichTextSettings = () => {
79+
if (!isDefined(pageLayoutEditingWidgetId)) {
80+
const newWidget = createPageLayoutStandaloneRichTextWidget({
81+
blocknote: '',
82+
markdown: null,
83+
});
84+
setPageLayoutEditingWidgetId(newWidget.id);
85+
}
86+
87+
closeCommandMenu();
88+
};
89+
6790
return (
68-
<CommandMenuList commandGroups={[]} selectableItemIds={['chart', 'iframe']}>
91+
<CommandMenuList
92+
commandGroups={[]}
93+
selectableItemIds={['chart', 'iframe', 'rich-text']}
94+
>
6995
<CommandGroup heading={t`Widget type`}>
7096
<SelectableListItem
7197
itemId="chart"
@@ -89,6 +115,18 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
89115
onClick={handleNavigateToIframeSettings}
90116
/>
91117
</SelectableListItem>
118+
119+
<SelectableListItem
120+
itemId="rich-text"
121+
onEnter={handleNavigateToRichTextSettings}
122+
>
123+
<CommandMenuItem
124+
Icon={IconAlignBoxLeftTop}
125+
label={t`Rich Text`}
126+
id="rich-text"
127+
onClick={handleNavigateToRichTextSettings}
128+
/>
129+
</SelectableListItem>
92130
</CommandGroup>
93131
</CommandMenuList>
94132
);

packages/twenty-front/src/modules/page-layout/constants/WidgetSizes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ export const WIDGET_SIZES: Partial<Record<WidgetType, WidgetSizeConfig>> = {
66
default: { w: 6, h: 6 },
77
minimum: { w: 4, h: 5 },
88
},
9+
[WidgetType.STANDALONE_RICH_TEXT]: {
10+
default: { w: 4, h: 4 },
11+
minimum: { w: 1, h: 1 },
12+
},
913
};

0 commit comments

Comments
 (0)