From c4fecb0a1ac72b6a6ca86449367c58fbe0b08b86 Mon Sep 17 00:00:00 2001 From: brendanlaschke Date: Wed, 20 Dec 2023 15:16:19 +0100 Subject: [PATCH] Upload image for use in blocknote editor (#3044) * - upload image to use in blocknote editor - fix local-storage not in gitignore * fix lint * fix runtime config add tests for body parsing notes and tasks * lint --- .../components/ActivityBodyEditor.tsx | 23 ++++ .../files/components/AttachmentList.tsx | 2 +- .../files/components/AttachmentRow.tsx | 7 +- .../activities/files/utils/downloadFile.ts | 4 +- .../activities/notes/components/NoteCard.tsx | 15 +-- .../activities/tasks/components/TaskRow.tsx | 5 +- .../__tests__/getActivityPreview.test.ts | 112 ++++++++++++++++++ .../__tests__/getActivitySummary.test.ts | 112 ++++++++++++++++++ .../activities/utils/getActivityPreview.ts | 14 +++ .../activities/utils/getActivitySummary.ts | 9 ++ packages/twenty-server/.gitignore | 3 +- 11 files changed, 282 insertions(+), 24 deletions(-) create mode 100644 packages/twenty-front/src/modules/activities/utils/__tests__/getActivityPreview.test.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/getActivityPreview.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts diff --git a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx index 741603cf8..6e00c5cb7 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -9,6 +9,8 @@ import { Activity } from '@/activities/types/Activity'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; +import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; const StyledBlockNoteStyledContainer = styled.div` width: 100%; @@ -56,6 +58,26 @@ export const ActivityBodyEditor = ({ slashMenuItems = slashMenuItems.filter((x) => x.name != 'Image'); } + const [uploadFile] = useUploadFileMutation(); + + const handleUploadAttachment = async (file: File): Promise => { + if (!file) { + return ''; + } + const result = await uploadFile({ + variables: { + file, + fileFolder: FileFolder.Attachment, + }, + }); + if (!result?.data?.uploadFile) { + throw new Error("Couldn't upload Image"); + } + const imageUrl = + REACT_APP_SERVER_BASE_URL + '/files/' + result?.data?.uploadFile; + return imageUrl; + }; + const editor: BlockNoteEditor | null = useBlockNote({ initialContent: isNonEmptyString(activity.body) && activity.body !== '{}' @@ -66,6 +88,7 @@ export const ActivityBodyEditor = ({ debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? ''); }, slashMenuItems, + uploadFile: imagesActivated ? handleUploadAttachment : undefined, }); return ( diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx index f82827d7d..3aa4a5dac 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx @@ -45,7 +45,7 @@ const StyledAttachmentContainer = styled.div` background: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.md}; - disply: flex; + display: flex; flex-flow: column nowrap; justify-content: center; width: 100%; 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 ee2f73529..de533fc1d 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -12,6 +12,7 @@ import { } from '@/object-record/field/contexts/FieldContext'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { IconCalendar } from '@/ui/display/icon'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { formatToHumanReadableDate } from '~/utils'; const StyledRow = styled.div` @@ -75,11 +76,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { {attachment.name} diff --git a/packages/twenty-front/src/modules/activities/files/utils/downloadFile.ts b/packages/twenty-front/src/modules/activities/files/utils/downloadFile.ts index a8f981ca0..dc354264e 100644 --- a/packages/twenty-front/src/modules/activities/files/utils/downloadFile.ts +++ b/packages/twenty-front/src/modules/activities/files/utils/downloadFile.ts @@ -1,5 +1,7 @@ +import { REACT_APP_SERVER_BASE_URL } from '~/config'; + export const downloadFile = (fullPath: string, fileName: string) => { - fetch(process.env.REACT_APP_SERVER_BASE_URL + '/files/' + fullPath) + fetch(REACT_APP_SERVER_BASE_URL + '/files/' + fullPath) .then((resp) => resp.status === 200 ? resp.blob() diff --git a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx index 15fb1bfcb..3b63e6b12 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx @@ -6,6 +6,7 @@ import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRi import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { GraphQLActivity } from '@/activities/types/GraphQLActivity'; import { Note } from '@/activities/types/Note'; +import { getActivityPreview } from '@/activities/utils/getActivityPreview'; import { FieldContext, GenericFieldContextType, @@ -83,19 +84,7 @@ export const NoteCard = ({ }) => { const theme = useTheme(); const openActivityRightDrawer = useOpenActivityRightDrawer(); - - const noteBody = note.body ? JSON.parse(note.body) : []; - - const body = noteBody.length - ? noteBody - .map((x: any) => - Array.isArray(x.content) - ? x.content?.map((content: any) => content?.text).join(' ') - : x.content?.text, - ) - .filter((x: string) => x) - .join('\n') - : ''; + const body = getActivityPreview(note.body); const fieldContext = useMemo( () => ({ recoilScopeId: note?.id ?? '' }), diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index e7246cf5d..866c2c269 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -1,11 +1,11 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { GraphQLActivity } from '@/activities/types/GraphQLActivity'; +import { getActivitySummary } from '@/activities/utils/getActivitySummary'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { IconCalendar, IconComment } from '@/ui/display/icon'; import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip'; @@ -72,8 +72,7 @@ export const TaskRow = ({ const theme = useTheme(); const openActivityRightDrawer = useOpenActivityRightDrawer(); - const body = JSON.parse(isNonEmptyString(task.body) ? task.body : '{}')[0] - ?.content[0]?.text; + const body = getActivitySummary(task.body); const { completeTask } = useCompleteTask(task); const activityTargetIds = diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityPreview.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityPreview.test.ts new file mode 100644 index 000000000..b64767583 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityPreview.test.ts @@ -0,0 +1,112 @@ +import { getActivityPreview } from '../getActivityPreview'; + +describe('getActivityPreview', () => { + it('should work for empty body', () => { + const activityBody = {}; + + const res = getActivityPreview(JSON.stringify(activityBody)); + + expect(res).toEqual(''); + }); + it('should work for empty body', () => { + const activityBody = ''; + + const res = getActivityPreview(JSON.stringify(activityBody)); + + expect(res).toEqual(''); + }); + it('should work for text body', () => { + const activityBody = [ + { + id: '103003f3-60de-4713-8779-a694ee7a22c9', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [{ type: 'text', text: 'test 1', styles: {} }], + children: [], + }, + { + id: 'bec5b84b-e9e7-49f6-854a-c2a9811f16e5', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [{ type: 'text', text: 'test text', styles: {} }], + children: [], + }, + { + id: 'a347c1e7-65cb-4829-af9e-52fba407c3c8', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [{ type: 'text', text: 'test 2', styles: {} }], + children: [], + }, + { + id: '7b999cbb-f248-4ead-a64f-94840191dc86', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [], + children: [], + }, + ]; + + const res = getActivityPreview(JSON.stringify(activityBody)); + + expect(res).toEqual('test 1\ntest text\ntest 2'); + }); + it('should work for image as first block', () => { + const activityBody = [ + { + id: '7c7779f3-9c60-4504-ab3b-230fe390d430', + type: 'image', + props: { + backgroundColor: 'default', + textAlignment: 'left', + url: 'https://favicon.twenty.com/qonto.com', + caption: '', + width: 230, + }, + children: [], + }, + { + id: 'ab470e73-4564-493d-a1ad-bbe2c86c4481', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [{ type: 'text', text: 'TEST', styles: {} }], + children: [], + }, + { + id: 'd4720499-2a45-4f3b-96cf-a8415c295678', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [], + children: [], + }, + ]; + + const res = getActivityPreview(JSON.stringify(activityBody)); + + expect(res).toEqual('TEST'); + }); +}); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts new file mode 100644 index 000000000..e4c16d239 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts @@ -0,0 +1,112 @@ +import { getActivitySummary } from '../getActivitySummary'; + +describe('getActivitySummary', () => { + it('should work for empty body', () => { + const activityBody = {}; + + const res = getActivitySummary(JSON.stringify(activityBody)); + + expect(res).toEqual(''); + }); + it('should work for empty body', () => { + const activityBody = ''; + + const res = getActivitySummary(JSON.stringify(activityBody)); + + expect(res).toEqual(''); + }); + it('should work for paragraph as first block', () => { + const activityBody = [ + { + id: '103003f3-60de-4713-8779-a694ee7a22c9', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [{ type: 'text', text: 'test 1', styles: {} }], + children: [], + }, + { + id: 'bec5b84b-e9e7-49f6-854a-c2a9811f16e5', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [{ type: 'text', text: 'test text', styles: {} }], + children: [], + }, + { + id: 'a347c1e7-65cb-4829-af9e-52fba407c3c8', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [{ type: 'text', text: 'test 2', styles: {} }], + children: [], + }, + { + id: '7b999cbb-f248-4ead-a64f-94840191dc86', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [], + children: [], + }, + ]; + + const res = getActivitySummary(JSON.stringify(activityBody)); + + expect(res).toEqual('test 1'); + }); + it('should work for image as first block', () => { + const activityBody = [ + { + id: '7c7779f3-9c60-4504-ab3b-230fe390d430', + type: 'image', + props: { + backgroundColor: 'default', + textAlignment: 'left', + url: 'https://favicon.twenty.com/qonto.com', + caption: '', + width: 230, + }, + children: [], + }, + { + id: 'ab470e73-4564-493d-a1ad-bbe2c86c4481', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [{ type: 'text', text: 'TEST', styles: {} }], + children: [], + }, + { + id: 'd4720499-2a45-4f3b-96cf-a8415c295678', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [], + children: [], + }, + ]; + + const res = getActivitySummary(JSON.stringify(activityBody)); + + expect(res).toEqual(''); + }); +}); diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityPreview.ts b/packages/twenty-front/src/modules/activities/utils/getActivityPreview.ts new file mode 100644 index 000000000..5587a6680 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/getActivityPreview.ts @@ -0,0 +1,14 @@ +export const getActivityPreview = (activityBody: string) => { + const noteBody = activityBody ? JSON.parse(activityBody) : []; + + return noteBody.length + ? noteBody + .map((x: any) => + Array.isArray(x.content) + ? x.content?.map((content: any) => content?.text).join(' ') + : x.content?.text, + ) + .filter((x: string) => x) + .join('\n') + : ''; +}; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts b/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts new file mode 100644 index 000000000..7032ffac1 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts @@ -0,0 +1,9 @@ +export const getActivitySummary = (activityBody: string) => { + const noteBody = activityBody ? JSON.parse(activityBody) : []; + + return ( + noteBody[0]?.content?.text || + noteBody[0]?.content?.map((content: any) => content?.text).join(' ') || + '' + ); +}; diff --git a/packages/twenty-server/.gitignore b/packages/twenty-server/.gitignore index 4be6e160a..6d66634c8 100644 --- a/packages/twenty-server/.gitignore +++ b/packages/twenty-server/.gitignore @@ -1 +1,2 @@ -dist/* \ No newline at end of file +dist/* +.local-storage \ No newline at end of file