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:
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+8
-8
@@ -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}
|
||||
|
||||
+8
-26
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
-1
@@ -91,7 +91,6 @@ export const StandaloneRichTextWidget = ({
|
||||
<StandaloneRichTextEditorContent
|
||||
key={isEditable ? 'editing' : 'readonly'}
|
||||
widget={widget}
|
||||
dashboardId={dashboardId}
|
||||
currentBody={currentBody}
|
||||
attachments={attachments}
|
||||
isEditable={isEditable}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+14
@@ -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,
|
||||
},
|
||||
});
|
||||
+39
@@ -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);
|
||||
};
|
||||
+47
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user