Add useUploadAttachment hook (#7617) (#7690)

Reusing the useUploadAttachment Hook

In the implementation of the feature to ensure the attachment table is
updated whenever new images are added to a RICH_TEXT field #7617 , it is
likely that the useUploadAttachment hook is reused.

The useUploadAttachment hook is responsible for handling the upload of
attachments, including images, and returning the uploaded file URL. By
reusing this hook, you can leverage its existing functionality to handle
image uploads within the RICH_TEXT field.

In this case, the modified image handling logic would utilize the
useUploadAttachment hook to upload new images added to the RICH_TEXT
content. The hook would then return the uploaded file URL, which would
be used to update the attachment table with the details of the newly
added images.

By reusing the useUploadAttachment hook, you can avoid duplicating code
and ensure consistency in the way attachments are handled throughout the
application.

Fixes #6565

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
shubham yadav 2024-11-06 02:48:31 +05:30 committed by GitHub
parent 9e051d7900
commit ca91dc2dc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 22 additions and 78 deletions

View File

@ -1,7 +1,7 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useCreateBlockNote } from '@blocknote/react'; import { useCreateBlockNote } from '@blocknote/react';
import { isArray, isNonEmptyString } from '@sniptt/guards'; import { isArray, isNonEmptyString } from '@sniptt/guards';
import { ClipboardEvent, useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil'; import { useRecoilCallback, useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
@ -20,20 +20,16 @@ import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDraw
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey'; import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { getFileType } from '../files/utils/getFileType';
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { Note } from '@/activities/types/Note'; import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task'; import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import '@blocknote/core/fonts/inter.css'; import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css'; import '@blocknote/mantine/style.css';
import '@blocknote/react/style.css'; import '@blocknote/react/style.css';
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
type RichTextEditorProps = { type RichTextEditorProps = {
activityId: string; activityId: string;
@ -121,22 +117,13 @@ export const RichTextEditor = ({
canCreateActivityState, canCreateActivityState,
); );
const [uploadFile] = useUploadFileMutation(); const { uploadAttachmentFile } = useUploadAttachmentFile();
const handleUploadAttachment = async (file: File): Promise<string> => { const handleUploadAttachment = async (file: File) => {
if (isUndefinedOrNull(file)) { return await uploadAttachmentFile(file, {
return ''; id: activityId,
} targetObjectNameSingular: activityObjectNameSingular,
const result = await uploadFile({
variables: {
file,
fileFolder: FileFolder.Attachment,
},
}); });
if (!result?.data?.uploadFile) {
throw new Error("Couldn't upload Image");
}
return getFileAbsoluteURI(result.data.uploadFile);
}; };
const prepareBody = (newStringifiedBody: string) => { const prepareBody = (newStringifiedBody: string) => {
@ -152,8 +139,6 @@ export const RichTextEditor = ({
const imageProps = block.props; const imageProps = block.props;
const imageUrl = new URL(imageProps.url); const imageUrl = new URL(imageProps.url);
imageUrl.searchParams.delete('token');
return { return {
...block, ...block,
props: { props: {
@ -284,65 +269,19 @@ export const RichTextEditor = ({
} }
}, [activity, activityBody]); }, [activity, activityBody]);
const handleEditorBuiltInUploadFile = async (file: File) => {
const { attachementAbsoluteURL } = await handleUploadAttachment(file);
return attachementAbsoluteURL;
};
const editor = useCreateBlockNote({ const editor = useCreateBlockNote({
initialContent: initialBody, initialContent: initialBody,
domAttributes: { editor: { class: 'editor' } }, domAttributes: { editor: { class: 'editor' } },
schema: BLOCK_SCHEMA, schema: BLOCK_SCHEMA,
uploadFile: handleUploadAttachment, uploadFile: handleEditorBuiltInUploadFile,
}); });
const handleImagePaste = async (event: ClipboardEvent) => {
const clipboardItems = event.clipboardData?.items;
if (isDefined(clipboardItems)) {
for (let i = 0; i < clipboardItems.length; i++) {
if (clipboardItems[i].kind === 'file') {
const isImage = clipboardItems[i].type.match('^image/');
const pastedFile = clipboardItems[i].getAsFile();
if (!pastedFile) {
return;
}
const attachmentUrl = await handleUploadAttachment(pastedFile);
if (!attachmentUrl) {
return;
}
if (isDefined(isImage)) {
editor?.insertBlocks(
[
{
type: 'image',
props: {
url: attachmentUrl,
},
},
],
editor?.getTextCursorPosition().block,
'after',
);
} else {
editor?.insertBlocks(
[
{
type: 'file',
props: {
url: attachmentUrl,
fileType: getFileType(pastedFile.name),
name: pastedFile.name,
},
},
],
editor?.getTextCursorPosition().block,
'after',
);
}
}
}
}
};
useScopedHotkeys( useScopedHotkeys(
Key.Escape, Key.Escape,
() => { () => {
@ -427,7 +366,6 @@ export const RichTextEditor = ({
<BlockEditor <BlockEditor
onFocus={handleBlockEditorFocus} onFocus={handleBlockEditorFocus}
onBlur={handlerBlockEditorBlur} onBlur={handlerBlockEditorBlur}
onPaste={handleImagePaste}
onChange={handleEditorChange} onChange={handleEditorChange}
editor={editor} editor={editor}
/> />

View File

@ -7,7 +7,9 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { isNonEmptyString } from '@sniptt/guards';
import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
// Note: This is probably not the right way to do this. // Note: This is probably not the right way to do this.
export const computePathWithoutToken = (attachmentPath: string): string => { export const computePathWithoutToken = (attachmentPath: string): string => {
@ -36,8 +38,8 @@ export const useUploadAttachmentFile = () => {
const attachmentPath = result?.data?.uploadFile; const attachmentPath = result?.data?.uploadFile;
if (!attachmentPath) { if (!isNonEmptyString(attachmentPath)) {
return; throw new Error("Couldn't upload the attachment.");
} }
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
@ -55,6 +57,10 @@ export const useUploadAttachmentFile = () => {
} as Partial<Attachment>; } as Partial<Attachment>;
await createOneAttachment(attachmentToCreate); await createOneAttachment(attachmentToCreate);
const attachementAbsoluteURL = getFileAbsoluteURI(attachmentPath);
return { attachementAbsoluteURL };
}; };
return { uploadAttachmentFile }; return { uploadAttachmentFile };