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.
This commit is contained in:
Etienne
2026-02-17 13:46:28 +01:00
committed by GitHub
parent 1e6c5b57b2
commit dc167b2d3d
9 changed files with 122 additions and 41 deletions
@@ -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;
@@ -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;
};
@@ -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 = ({
<SuggestionMenuController
triggerCharacter="/"
getItems={async (query) => {
const items = getSlashMenu(editor);
const items = getDashboardSlashMenu(editor);
return filterSuggestionItems<SuggestionItem>(items, query);
}}
suggestionMenuComponent={CustomSlashMenu}
@@ -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);
};
@@ -91,7 +91,6 @@ export const StandaloneRichTextWidget = ({
<StandaloneRichTextEditorContent
key={isEditable ? 'editing' : 'readonly'}
widget={widget}
dashboardId={dashboardId}
currentBody={currentBody}
attachments={attachments}
isEditable={isEditable}
@@ -1,9 +1,9 @@
import { type BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema';
import { type DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema';
import { useEffect } from 'react';
type StandaloneRichTextWidgetAutoFocusEffectProps = {
shouldFocus: boolean;
editor: typeof BLOCK_SCHEMA.BlockNoteEditor;
editor: typeof DASHBOARD_BLOCK_SCHEMA.BlockNoteEditor;
containerElement?: HTMLElement | null;
};
@@ -0,0 +1,14 @@
import { BlockNoteSchema, defaultBlockSpecs } from '@blocknote/core';
export const DASHBOARD_BLOCK_SCHEMA = BlockNoteSchema.create({
blockSpecs: {
paragraph: defaultBlockSpecs.paragraph,
heading: defaultBlockSpecs.heading,
bulletListItem: defaultBlockSpecs.bulletListItem,
numberedListItem: defaultBlockSpecs.numberedListItem,
checkListItem: defaultBlockSpecs.checkListItem,
codeBlock: defaultBlockSpecs.codeBlock,
table: defaultBlockSpecs.table,
quote: defaultBlockSpecs.quote,
},
});
@@ -0,0 +1,39 @@
import { type PartialBlock } from '@blocknote/core';
import { DASHBOARD_BLOCK_SCHEMA } from '@/page-layout/widgets/standalone-rich-text/constants/DashboardBlockSchema';
import { isDefined } from 'twenty-shared/utils';
type DashboardPartialBlock = (typeof DASHBOARD_BLOCK_SCHEMA)['PartialBlock'];
type SupportedBlockType = keyof (typeof DASHBOARD_BLOCK_SCHEMA)['blockSchema'];
const SUPPORTED_BLOCK_TYPES = Object.keys(
DASHBOARD_BLOCK_SCHEMA.blockSchema,
) as SupportedBlockType[];
const isSupportedBlockType = (type: string): type is SupportedBlockType =>
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);
};
@@ -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<string, IconComponent> = {
'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;
};