From dc167b2d3d30988f7c4b7d5a1ec6a564e4c42d05 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:46:28 +0100 Subject: [PATCH] File - Disable file upload in standalone rich text widget (#17934) File's url are not signed, in standalone rich text. We choose to disable file upload to prevent unauthorised file upload links. --- .../DashboardBlockDragHandleMenu.tsx | 4 +- .../components/DashboardEditorSideMenu.tsx | 4 +- .../components/DashboardsBlockEditor.tsx | 16 +++---- .../StandaloneRichTextEditorContent.tsx | 34 ++++---------- .../components/StandaloneRichTextWidget.tsx | 1 - ...tandaloneRichTextWidgetAutoFocusEffect.tsx | 4 +- .../constants/DashboardBlockSchema.ts | 14 ++++++ .../utils/filterSupportedBlocks.ts | 39 +++++++++++++++ .../utils/getDashboardSlashMenu.ts | 47 +++++++++++++++++++ 9 files changed, 122 insertions(+), 41 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/utils/filterSupportedBlocks.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/utils/getDashboardSlashMenu.ts diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardBlockDragHandleMenu.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardBlockDragHandleMenu.tsx index 2cbfb8cd12..d2d6f822b6 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardBlockDragHandleMenu.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardBlockDragHandleMenu.tsx @@ -13,8 +13,8 @@ import { createPortal } from 'react-dom'; import { IconColorSwatch, IconPlus, IconTrash } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; -import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema'; import { DashboardBlockColorPicker } from '@/page-layout/widgets/standalone-rich-text/components/DashboardBlockColorPicker'; +import { type DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema'; import { DRAG_HANDLE_MENU_FLOATING_CONFIG } from '@/page-layout/widgets/standalone-rich-text/constants/DragHandleMenuFloatingConfig'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -23,7 +23,7 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis import { isDefined } from 'twenty-shared/utils'; type DashboardBlockDragHandleMenuProps = { - editor: typeof BLOCK_SCHEMA.BlockNoteEditor; + editor: typeof DASHBOARD_BLOCK_SCHEMA.BlockNoteEditor; block: Block; anchorElement: HTMLElement | null; boundaryElement?: HTMLElement | null; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardEditorSideMenu.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardEditorSideMenu.tsx index e61505d3e0..d5ef423bf6 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardEditorSideMenu.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardEditorSideMenu.tsx @@ -6,13 +6,13 @@ import { useState } from 'react'; import { createPortal } from 'react-dom'; import { IconGripVertical } from 'twenty-ui/display'; -import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema'; import { DashboardBlockDragHandleMenu } from '@/page-layout/widgets/standalone-rich-text/components/DashboardBlockDragHandleMenu'; +import { type DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema'; import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices'; import { isDefined } from 'twenty-shared/utils'; type DashboardEditorSideMenuProps = { - editor: typeof BLOCK_SCHEMA.BlockNoteEditor; + editor: typeof DASHBOARD_BLOCK_SCHEMA.BlockNoteEditor; boundaryElement?: HTMLElement | null; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor.tsx index b59c061190..f834c60160 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor.tsx @@ -5,24 +5,24 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { type ClipboardEvent } from 'react'; -import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema'; -import { getSlashMenu } from '@/blocknote-editor/utils/getSlashMenu'; -import { DashboardEditorSideMenu } from '@/page-layout/widgets/standalone-rich-text/components/DashboardEditorSideMenu'; -import { DashboardFormattingToolbar } from '@/page-layout/widgets/standalone-rich-text/components/DashboardFormattingToolbar'; import { CustomSlashMenu, type SuggestionItem, } from '@/blocknote-editor/components/CustomSlashMenu'; +import { DashboardEditorSideMenu } from '@/page-layout/widgets/standalone-rich-text/components/DashboardEditorSideMenu'; +import { DashboardFormattingToolbar } from '@/page-layout/widgets/standalone-rich-text/components/DashboardFormattingToolbar'; +import { type DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema'; +import { getDashboardSlashMenu } from '@/page-layout/widgets/standalone-rich-text/utils/getDashboardSlashMenu'; -interface DashboardsBlockEditorProps { - editor: typeof BLOCK_SCHEMA.BlockNoteEditor; +type DashboardsBlockEditorProps = { + editor: typeof DASHBOARD_BLOCK_SCHEMA.BlockNoteEditor; onFocus?: () => void; onBlur?: () => void; onPaste?: (event: ClipboardEvent) => void; onChange?: () => void; readonly?: boolean; boundaryElement?: HTMLElement | null; -} +}; // TODO: Refactor these BlockNote CSS overrides - some may be dead code now that we have custom components // (DashboardBlockDragHandleMenu, DashboardEditorSideMenu, DashboardColorSelectionMenu). @@ -190,7 +190,7 @@ export const DashboardsBlockEditor = ({ { - const items = getSlashMenu(editor); + const items = getDashboardSlashMenu(editor); return filterSuggestionItems(items, query); }} suggestionMenuComponent={CustomSlashMenu} diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx index 26d3b9d022..35a16ee033 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx @@ -1,35 +1,33 @@ import { useCallback, useMemo } from 'react'; -import { BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema'; -import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { type Attachment } from '@/activities/files/types/Attachment'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { DashboardsBlockEditor } from '@/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor'; import { StandaloneRichTextWidgetAutoFocusEffect } from '@/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidgetAutoFocusEffect'; +import { DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema'; +import { filterSupportedBlocks } from '@/page-layout/widgets/standalone-rich-text/utils/filterSupportedBlocks'; + import { BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG } from '@/blocknote-editor/constants/BlockEditorGlobalHotkeysConfig'; import { useAttachmentSync } from '@/blocknote-editor/hooks/useAttachmentSync'; import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote'; -import { prepareBodyWithSignedUrls } from '@/blocknote-editor/utils/prepareBodyWithSignedUrls'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { t } from '@lingui/core/macro'; import '@blocknote/core/fonts/inter.css'; import '@blocknote/mantine/style.css'; import { useCreateBlockNote } from '@blocknote/react'; import '@blocknote/react/style.css'; +import { t } from '@lingui/core/macro'; import { useRecoilCallback } from 'recoil'; import { useDebouncedCallback } from 'use-debounce'; import { WidgetConfigurationType } from '~/generated-metadata/graphql'; type StandaloneRichTextEditorContentProps = { widget: PageLayoutWidget; - dashboardId: string; currentBody: string; attachments: Attachment[]; isEditable: boolean; @@ -38,14 +36,12 @@ type StandaloneRichTextEditorContentProps = { export const StandaloneRichTextEditorContent = ({ widget, - dashboardId, currentBody, attachments, isEditable, containerElement, }: StandaloneRichTextEditorContentProps) => { const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(); - const { uploadAttachmentFile } = useUploadAttachmentFile(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const { removeFocusItemFromFocusStackById } = useRemoveFocusItemFromFocusStackById(); @@ -73,31 +69,18 @@ export const StandaloneRichTextEditorContent = ({ [isPageLayoutInEditModeState, pageLayoutEditingWidgetIdState, widget.id], ); - const handleUploadAttachment = async (file: File) => { - return await uploadAttachmentFile(file, { - id: dashboardId, - targetObjectNameSingular: CoreObjectNameSingular.Dashboard, - }); - }; - - const handleEditorBuiltInUploadFile = async (file: File) => { - const { attachmentAbsoluteURL } = await handleUploadAttachment(file); - return attachmentAbsoluteURL; - }; - const initialContent = useMemo( - () => parseInitialBlocknote(currentBody), + () => filterSupportedBlocks(parseInitialBlocknote(currentBody)), [currentBody], ); const editor = useCreateBlockNote({ initialContent, domAttributes: { editor: { class: 'editor' } }, - schema: BLOCK_SCHEMA, - uploadFile: handleEditorBuiltInUploadFile, + schema: DASHBOARD_BLOCK_SCHEMA, sideMenuDetection: 'editor', placeholders: { - default: t`Type '/' for commands, '@' for mentions`, + default: t`Enter text or type '/' for commands`, }, }); @@ -128,9 +111,8 @@ export const StandaloneRichTextEditorContent = ({ const handleEditorChange = () => { const newStringifiedBody = JSON.stringify(editor.document) ?? ''; - const preparedBody = prepareBodyWithSignedUrls(newStringifiedBody); - handlePersistBody(preparedBody); + handlePersistBody(newStringifiedBody); handleAttachmentSync(newStringifiedBody, currentBody); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx index e3bd8efab8..b6f4700870 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx @@ -91,7 +91,6 @@ export const StandaloneRichTextWidget = ({ + SUPPORTED_BLOCK_TYPES.includes(type as SupportedBlockType); + +const filterBlockRecursively = ( + block: PartialBlock, +): DashboardPartialBlock | undefined => { + if (!isDefined(block.type) || !isSupportedBlockType(block.type)) { + return undefined; + } + + return { + ...block, + children: block.children + ?.map(filterBlockRecursively) + .filter(isDefined) as DashboardPartialBlock[], + } as DashboardPartialBlock; +}; + +export const filterSupportedBlocks = ( + blocks: PartialBlock[] | undefined, +): DashboardPartialBlock[] | undefined => { + if (!isDefined(blocks)) { + return undefined; + } + + return blocks.map(filterBlockRecursively).filter(isDefined); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/utils/getDashboardSlashMenu.ts b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/utils/getDashboardSlashMenu.ts new file mode 100644 index 0000000000..affccfbc20 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/utils/getDashboardSlashMenu.ts @@ -0,0 +1,47 @@ +import { getDefaultReactSlashMenuItems } from '@blocknote/react'; + +import { type SuggestionItem } from '@/blocknote-editor/types/types'; +import { type DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema'; +import { + IconBlockquote, + IconCode, + type IconComponent, + IconH1, + IconH2, + IconH3, + IconList, + IconListCheck, + IconListNumbers, + IconMoodSmile, + IconPilcrow, + IconTable, +} from 'twenty-ui/display'; + +const Icons: Record = { + 'Heading 1': IconH1, + 'Heading 2': IconH2, + 'Heading 3': IconH3, + Quote: IconBlockquote, + 'Numbered List': IconListNumbers, + 'Bullet List': IconList, + 'Check List': IconListCheck, + 'Code Block': IconCode, + Paragraph: IconPilcrow, + Table: IconTable, + Emoji: IconMoodSmile, +}; + +const EXCLUDED_BLOCK_TYPES = ['Image', 'Video', 'Audio', 'File']; + +export const getDashboardSlashMenu = ( + editor: typeof DASHBOARD_BLOCK_SCHEMA.BlockNoteEditor, +) => { + const items: SuggestionItem[] = getDefaultReactSlashMenuItems(editor) + .filter((item) => !EXCLUDED_BLOCK_TYPES.includes(item.title)) + .map((item) => ({ + ...item, + Icon: Icons[item.title], + })); + + return items; +};