From 9def3d5b57740acec10183e603019b02b558caee Mon Sep 17 00:00:00 2001 From: brendanlaschke Date: Fri, 5 Jan 2024 17:42:50 +0100 Subject: [PATCH] Activity editor add File block (#3146) * - added file block * fised auth useeffect * - add cmd v for file block * remove feature flag for attachment upload in blockeditor --------- Co-authored-by: Lucas Bordeau --- .../modules/activities/blocks/FileBlock.tsx | 137 ++++++++++++++++++ .../src/modules/activities/blocks/schema.ts | 8 + .../modules/activities/blocks/slashMenu.tsx | 49 +++++++ .../src/modules/activities/blocks/spec.ts | 8 + .../components/ActivityBodyEditor.tsx | 49 +++++-- .../components/ActivityComments.tsx | 2 - .../files/components/AttachmentIcon.tsx | 15 +- .../files/components/AttachmentRow.tsx | 2 +- 8 files changed, 245 insertions(+), 25 deletions(-) create mode 100644 packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx create mode 100644 packages/twenty-front/src/modules/activities/blocks/schema.ts create mode 100644 packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx create mode 100644 packages/twenty-front/src/modules/activities/blocks/spec.ts diff --git a/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx b/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx new file mode 100644 index 000000000..5e0c6675d --- /dev/null +++ b/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx @@ -0,0 +1,137 @@ +import { ChangeEvent, useRef } from 'react'; +import { + BlockFromConfig, + BlockNoteEditor, + InlineContentSchema, + PropSchema, + StyleSchema, +} from '@blocknote/core'; +import { createReactBlockSpec } from '@blocknote/react'; +import styled from '@emotion/styled'; + +import { Button } from '@/ui/input/button/components/Button'; +import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; + +import { AttachmentIcon } from '../files/components/AttachmentIcon'; +import { AttachmentType } from '../files/types/Attachment'; +import { getFileType } from '../files/utils/getFileType'; + +import { blockSpecs } from './spec'; + +export const filePropSchema = { + // File url + url: { + default: '' as string, + }, + name: { + default: '' as string, + }, + fileType: { + default: 'Other' as AttachmentType, + }, +} satisfies PropSchema; + +const StyledFileInput = styled.input` + display: none; +`; + +const FileBlockConfig = { + type: 'file' as const, + propSchema: filePropSchema, + content: 'none' as const, +}; + +const StyledFileLine = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledLink = styled.a` + align-items: center; + color: ${({ theme }) => theme.font.color.primary}; + display: flex; + text-decoration: none; + :hover { + color: ${({ theme }) => theme.font.color.secondary}; + } +`; + +const StyledUploadFileContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const FileBlockRenderer = ({ + block, + editor, +}: { + block: BlockFromConfig< + typeof FileBlockConfig, + InlineContentSchema, + StyleSchema + >; + editor: BlockNoteEditor; +}) => { + const inputFileRef = useRef(null); + + const handleUploadAttachment = async (file: File) => { + if (!file) { + return ''; + } + const fileUrl = await editor.uploadFile?.(file); + + editor.updateBlock(block.id, { + props: { + ...block.props, + ...{ url: fileUrl, fileType: getFileType(file.name), name: file.name }, + }, + }); + }; + const handleUploadFileClick = () => { + inputFileRef?.current?.click?.(); + }; + const handleFileChange = (e: ChangeEvent) => { + if (e.target.files) handleUploadAttachment?.(e.target.files[0]); + }; + + if (block.props.url) { + return ( + + + + + {block.props.name} + + + + ); + } + + return ( + + + + + + + ); +}; + +export const FileBlock = createReactBlockSpec(FileBlockConfig, { + render: (block) => { + return ( + + ); + }, +}); diff --git a/packages/twenty-front/src/modules/activities/blocks/schema.ts b/packages/twenty-front/src/modules/activities/blocks/schema.ts new file mode 100644 index 000000000..78e83e7d2 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/blocks/schema.ts @@ -0,0 +1,8 @@ +import { BlockSchema, defaultBlockSchema } from '@blocknote/core'; + +import { FileBlock } from './FileBlock'; + +export const blockSchema: BlockSchema = { + ...defaultBlockSchema, + file: FileBlock.config, +}; diff --git a/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx b/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx new file mode 100644 index 000000000..ced25f9c8 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx @@ -0,0 +1,49 @@ +import { + BlockNoteEditor, + InlineContentSchema, + StyleSchema, +} from '@blocknote/core'; +import { getDefaultReactSlashMenuItems } from '@blocknote/react'; + +import { IconFile } from '@/ui/display/icon'; + +import { blockSchema } from './schema'; + +export const getSlashMenu = (imagesActivated: boolean) => { + let items = [ + ...getDefaultReactSlashMenuItems(blockSchema), + { + name: 'File', + aliases: ['file', 'folder'], + group: 'Media', + icon: , + hint: 'Insert a file', + execute: ( + editor: BlockNoteEditor< + typeof blockSchema, + InlineContentSchema, + StyleSchema + >, + ) => { + editor.insertBlocks( + [ + { + type: 'file', + props: { + url: undefined, + }, + }, + ], + editor.getTextCursorPosition().block, + 'before', + ); + }, + }, + ]; + + if (!imagesActivated) { + items = items.filter((x) => x.name != 'Image'); + } + + return items; +}; diff --git a/packages/twenty-front/src/modules/activities/blocks/spec.ts b/packages/twenty-front/src/modules/activities/blocks/spec.ts new file mode 100644 index 000000000..fd95c52f2 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/blocks/spec.ts @@ -0,0 +1,8 @@ +import { defaultBlockSpecs } from '@blocknote/core'; + +import { FileBlock } from './FileBlock'; + +export const blockSpecs: any = { + ...defaultBlockSpecs, + file: FileBlock, +}; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx index 022ac3efb..9b97c76cc 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { BlockNoteEditor } from '@blocknote/core'; -import { getDefaultReactSlashMenuItems, useBlockNote } from '@blocknote/react'; +import { useBlockNote } from '@blocknote/react'; import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import debounce from 'lodash.debounce'; @@ -13,6 +13,10 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; +import { getSlashMenu } from '../blocks/slashMenu'; +import { blockSpecs } from '../blocks/spec'; +import { getFileType } from '../files/utils/getFileType'; + const StyledBlockNoteStyledContainer = styled.div` width: 100%; `; @@ -51,12 +55,8 @@ export const ActivityBodyEditor = ({ return debounce(onInternalChange, 200); }, [updateOneRecord, activity.id]); - let slashMenuItems = [...getDefaultReactSlashMenuItems()]; const imagesActivated = useIsFeatureEnabled('IS_NOTE_CREATE_IMAGES_ENABLED'); - - if (!imagesActivated) { - slashMenuItems = slashMenuItems.filter((x) => x.name !== 'Image'); - } + const slashMenuItems = getSlashMenu(imagesActivated); const [uploadFile] = useUploadFileMutation(); @@ -78,7 +78,7 @@ export const ActivityBodyEditor = ({ return imageUrl; }; - const editor: BlockNoteEditor | null = useBlockNote({ + const editor: BlockNoteEditor | null = useBlockNote({ initialContent: isNonEmptyString(activity.body) && activity.body !== '{}' ? JSON.parse(activity.body) @@ -88,7 +88,8 @@ export const ActivityBodyEditor = ({ debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? ''); }, slashMenuItems, - uploadFile: imagesActivated ? handleUploadAttachment : undefined, + blockSpecs: blockSpecs, + uploadFile: handleUploadAttachment, onEditorReady: (editor: BlockNoteEditor) => { editor.domElement.addEventListener('paste', handleImagePaste); }, @@ -99,23 +100,41 @@ export const ActivityBodyEditor = ({ if (clipboardItems) { for (let i = 0; i < clipboardItems.length; i++) { - if ( - clipboardItems[i].kind === 'file' && - clipboardItems[i].type.match('^image/') - ) { + if (clipboardItems[i].kind === 'file') { + const isImage = clipboardItems[i].type.match('^image/'); const pastedFile = clipboardItems[i].getAsFile(); if (!pastedFile) { return; } - const imageUrl = await handleUploadAttachment(pastedFile); - if (imageUrl) { + const attachmentUrl = await handleUploadAttachment(pastedFile); + + if (!attachmentUrl) { + return; + } + + if (isImage) { editor?.insertBlocks( [ { type: 'image', props: { - url: imageUrl, + url: attachmentUrl, + }, + }, + ], + editor?.getTextCursorPosition().block, + 'after', + ); + } else { + editor?.insertBlocks( + [ + { + type: 'file', + props: { + url: attachmentUrl, + fileType: getFileType(pastedFile.name), + name: pastedFile.name, }, }, ], diff --git a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx index 18324ad3b..001c402eb 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx @@ -36,10 +36,8 @@ const StyledThreadItemListContainer = styled.div` const StyledCommentActionBar = styled.div` background: ${({ theme }) => theme.background.primary}; border-top: 1px solid ${({ theme }) => theme.border.color.light}; - bottom: 0; display: flex; padding: 16px 24px 16px 48px; - position: absolute; width: calc( ${({ theme }) => (useIsMobile() ? '100%' : theme.rightDrawerWidth)} - 72px ); diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentIcon.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentIcon.tsx index 212bef9d3..aaf8232cb 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentIcon.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentIcon.tsx @@ -1,10 +1,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { - Attachment, - AttachmentType, -} from '@/activities/files/types/Attachment'; +import { AttachmentType } from '@/activities/files/types/Attachment'; import { IconFile, IconFileText, @@ -40,7 +37,11 @@ const IconMapping: { [key in AttachmentType]: IconComponent } = { Other: IconFile, }; -export const AttachmentIcon = ({ attachment }: { attachment: Attachment }) => { +export const AttachmentIcon = ({ + attachmentType, +}: { + attachmentType: AttachmentType; +}) => { const theme = useTheme(); const IconColors: { [key in AttachmentType]: string } = { @@ -54,10 +55,10 @@ export const AttachmentIcon = ({ attachment }: { attachment: Attachment }) => { Other: theme.color.gray, }; - const Icon = IconMapping[attachment.type]; + const Icon = IconMapping[attachmentType]; return ( - + {Icon && } ); diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx index db966ac33..afd543a8c 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -75,7 +75,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { - +