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
This commit is contained in:
@ -9,6 +9,8 @@ import { Activity } from '@/activities/types/Activity';
|
|||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
|
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
const StyledBlockNoteStyledContainer = styled.div`
|
const StyledBlockNoteStyledContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -56,6 +58,26 @@ export const ActivityBodyEditor = ({
|
|||||||
slashMenuItems = slashMenuItems.filter((x) => x.name != 'Image');
|
slashMenuItems = slashMenuItems.filter((x) => x.name != 'Image');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [uploadFile] = useUploadFileMutation();
|
||||||
|
|
||||||
|
const handleUploadAttachment = async (file: File): Promise<string> => {
|
||||||
|
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({
|
const editor: BlockNoteEditor | null = useBlockNote({
|
||||||
initialContent:
|
initialContent:
|
||||||
isNonEmptyString(activity.body) && activity.body !== '{}'
|
isNonEmptyString(activity.body) && activity.body !== '{}'
|
||||||
@ -66,6 +88,7 @@ export const ActivityBodyEditor = ({
|
|||||||
debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? '');
|
debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? '');
|
||||||
},
|
},
|
||||||
slashMenuItems,
|
slashMenuItems,
|
||||||
|
uploadFile: imagesActivated ? handleUploadAttachment : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const StyledAttachmentContainer = styled.div`
|
|||||||
background: ${({ theme }) => theme.background.secondary};
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
disply: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
} from '@/object-record/field/contexts/FieldContext';
|
} from '@/object-record/field/contexts/FieldContext';
|
||||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||||
import { IconCalendar } from '@/ui/display/icon';
|
import { IconCalendar } from '@/ui/display/icon';
|
||||||
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
import { formatToHumanReadableDate } from '~/utils';
|
import { formatToHumanReadableDate } from '~/utils';
|
||||||
|
|
||||||
const StyledRow = styled.div`
|
const StyledRow = styled.div`
|
||||||
@ -75,11 +76,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => {
|
|||||||
<StyledLeftContent>
|
<StyledLeftContent>
|
||||||
<AttachmentIcon attachment={attachment} />
|
<AttachmentIcon attachment={attachment} />
|
||||||
<StyledLink
|
<StyledLink
|
||||||
href={
|
href={REACT_APP_SERVER_BASE_URL + '/files/' + attachment.fullPath}
|
||||||
process.env.REACT_APP_SERVER_BASE_URL +
|
|
||||||
'/files/' +
|
|
||||||
attachment.fullPath
|
|
||||||
}
|
|
||||||
target="__blank"
|
target="__blank"
|
||||||
>
|
>
|
||||||
{attachment.name}
|
{attachment.name}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
|
|
||||||
export const downloadFile = (fullPath: string, fileName: string) => {
|
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) =>
|
.then((resp) =>
|
||||||
resp.status === 200
|
resp.status === 200
|
||||||
? resp.blob()
|
? resp.blob()
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRi
|
|||||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||||
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
||||||
import { Note } from '@/activities/types/Note';
|
import { Note } from '@/activities/types/Note';
|
||||||
|
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
|
||||||
import {
|
import {
|
||||||
FieldContext,
|
FieldContext,
|
||||||
GenericFieldContextType,
|
GenericFieldContextType,
|
||||||
@ -83,19 +84,7 @@ export const NoteCard = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||||
|
const body = getActivityPreview(note.body);
|
||||||
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 fieldContext = useMemo(
|
const fieldContext = useMemo(
|
||||||
() => ({ recoilScopeId: note?.id ?? '' }),
|
() => ({ recoilScopeId: note?.id ?? '' }),
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
|
|
||||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||||
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
||||||
|
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { IconCalendar, IconComment } from '@/ui/display/icon';
|
import { IconCalendar, IconComment } from '@/ui/display/icon';
|
||||||
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
||||||
@ -72,8 +72,7 @@ export const TaskRow = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||||
|
|
||||||
const body = JSON.parse(isNonEmptyString(task.body) ? task.body : '{}')[0]
|
const body = getActivitySummary(task.body);
|
||||||
?.content[0]?.text;
|
|
||||||
const { completeTask } = useCompleteTask(task);
|
const { completeTask } = useCompleteTask(task);
|
||||||
|
|
||||||
const activityTargetIds =
|
const activityTargetIds =
|
||||||
|
|||||||
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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('');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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')
|
||||||
|
: '';
|
||||||
|
};
|
||||||
@ -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(' ') ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
};
|
||||||
3
packages/twenty-server/.gitignore
vendored
3
packages/twenty-server/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
dist/*
|
dist/*
|
||||||
|
.local-storage
|
||||||
Reference in New Issue
Block a user