Browse Source

Merge pull request #8689 from weseek/support/commonize-upload-attachments-operation

support: Commonize upload-attachments operation
Yuki Takei 2 years ago
parent
commit
8d0bd848df

+ 1 - 0
apps/app/src/client/services/upload-attachments/index.ts

@@ -0,0 +1 @@
+export * from './upload-attachments';

+ 39 - 0
apps/app/src/client/services/upload-attachments/upload-attachments.ts

@@ -0,0 +1,39 @@
+import type { IAttachment } from '@growi/core';
+
+import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
+import type { IApiv3GetAttachmentLimitParams, IApiv3GetAttachmentLimitResponse, IApiv3PostAttachmentResponse } from '~/interfaces/apiv3/attachment';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:client:services:upload-attachment');
+
+
+type UploadOpts = {
+  onUploaded?: (attachment: IAttachment) => void,
+  onError?: (error: Error, file: File) => void,
+}
+
+export const uploadAttachments = async(pageId: string, files: File[], opts?: UploadOpts): Promise<void> => {
+  files.forEach(async(file) => {
+    try {
+      const params: IApiv3GetAttachmentLimitParams = { fileSize: file.size };
+      const { data: resLimit } = await apiv3Get<IApiv3GetAttachmentLimitResponse>('/attachment/limit', params);
+
+      if (!resLimit.isUploadable) {
+        throw new Error(resLimit.errorMessage);
+      }
+
+      const formData = new FormData();
+      formData.append('file', file);
+      formData.append('page_id', pageId);
+
+      const { data: resAdd } = await apiv3PostForm<IApiv3PostAttachmentResponse>('/attachment', formData);
+
+      opts?.onUploaded?.(resAdd.attachment);
+    }
+    catch (e) {
+      logger.error('failed to upload', e);
+      opts?.onError?.(e, file);
+    }
+  });
+};

+ 11 - 30
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -13,7 +13,7 @@ import {
   TabContent, TabPane,
 } from 'reactstrap';
 
-import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
+import { uploadAttachments } from '~/client/services/upload-attachments';
 import { toastError } from '~/client/util/toastr';
 import type { IEditorMethods } from '~/interfaces/editor-methods';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
@@ -201,40 +201,21 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
   // the upload event handler
   const uploadHandler = useCallback((files: File[]) => {
-    files.forEach(async(file) => {
-      try {
-        const { data: resLimit } = await apiv3Get('/attachment/limit', { fileSize: file.size });
-
-        if (!resLimit.isUploadable) {
-          throw new Error(resLimit.errorMessage);
-        }
-
-        const formData = new FormData();
-        formData.append('file', file);
-        if (pageId != null) {
-          formData.append('page_id', pageId);
-        }
-
-        const { data: resAdd } = await apiv3PostForm('/attachment', formData);
-
-        const attachment = resAdd.attachment;
+    uploadAttachments(pageId, files, {
+      onUploaded: (attachment) => {
         const fileName = attachment.originalName;
 
-        let insertText = `[${fileName}](${attachment.filePathProxied})\n`;
-        // when image
-        if (attachment.fileFormat.startsWith('image/')) {
-          // modify to "![fileName](url)" syntax
-          insertText = `!${insertText}`;
-        }
+        const prefix = attachment.fileFormat.startsWith('image/')
+          ? '!' // use "![fileName](url)" syntax when image
+          : '';
+        const insertText = `${prefix}[${fileName}](${attachment.filePathProxied})\n`;
 
         codeMirrorEditor?.insertText(insertText);
-      }
-      catch (e) {
-        logger.error('failed to upload', e);
-        toastError(e);
-      }
+      },
+      onError: (error) => {
+        toastError(error);
+      },
     });
-
   }, [codeMirrorEditor, pageId]);
 
   const getCommentHtml = useCallback(() => {

+ 17 - 29
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -21,7 +21,7 @@ import { throttle, debounce } from 'throttle-debounce';
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { updatePage, extractRemoteRevisionDataFromErrorObj } from '~/client/services/update-page';
-import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
+import { uploadAttachments } from '~/client/services/upload-attachments';
 import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import {
   useDefaultIndentSize, useCurrentUser,
@@ -237,40 +237,28 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   // the upload event handler
   const uploadHandler = useCallback((files: File[]) => {
-    files.forEach(async(file) => {
-      try {
-        const { data: resLimit } = await apiv3Get('/attachment/limit', { fileSize: file.size });
-
-        if (!resLimit.isUploadable) {
-          throw new Error(resLimit.errorMessage);
-        }
-
-        const formData = new FormData();
-        formData.append('file', file);
-        if (pageId != null) {
-          formData.append('page_id', pageId);
-        }
-
-        const { data: resAdd } = await apiv3PostForm('/attachment', formData);
+    if (pageId == null) {
+      logger.error('pageId is invalid', {
+        pageId,
+      });
+      throw new Error('pageId is invalid');
+    }
 
-        const attachment = resAdd.attachment;
+    uploadAttachments(pageId, files, {
+      onUploaded: (attachment) => {
         const fileName = attachment.originalName;
 
-        let insertText = `[${fileName}](${attachment.filePathProxied})\n`;
-        // when image
-        if (attachment.fileFormat.startsWith('image/')) {
-          // modify to "![fileName](url)" syntax
-          insertText = `!${insertText}`;
-        }
+        const prefix = attachment.fileFormat.startsWith('image/')
+          ? '!' // use "![fileName](url)" syntax when image
+          : '';
+        const insertText = `${prefix}[${fileName}](${attachment.filePathProxied})\n`;
 
         codeMirrorEditor?.insertText(insertText);
-      }
-      catch (e) {
-        logger.error('failed to upload', e);
-        toastError(e);
-      }
+      },
+      onError: (error) => {
+        toastError(error);
+      },
     });
-
   }, [codeMirrorEditor, pageId]);
 
   // set handler to save and return to View

+ 15 - 0
apps/app/src/interfaces/apiv3/attachment.ts

@@ -0,0 +1,15 @@
+import type { IAttachment, IPage, IRevision } from '@growi/core';
+
+import type { ICheckLimitResult } from '../attachment';
+
+export type IApiv3GetAttachmentLimitParams = {
+  fileSize: number,
+};
+
+export type IApiv3GetAttachmentLimitResponse = ICheckLimitResult;
+
+export type IApiv3PostAttachmentResponse = {
+  page: IPage,
+  revision: IRevision,
+  attachment: IAttachment,
+}

+ 5 - 0
apps/app/src/interfaces/attachment.ts

@@ -6,3 +6,8 @@ import type { PaginateResult } from './mongoose-utils';
 export type IResAttachmentList = {
   paginateResult: PaginateResult<IAttachmentHasId>
 };
+
+export type ICheckLimitResult = {
+  isUploadable: boolean,
+  errorMessage?: string,
+}

+ 3 - 7
apps/app/src/server/service/file-uploader/file-uploader.ts

@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto';
 
 import type { Response } from 'express';
 
+import type { ICheckLimitResult } from '~/interfaces/attachment';
 import { type RespondOptions, ResponseMode } from '~/server/interfaces/attachment';
 import { Attachment, type IAttachmentDocument } from '~/server/models';
 import loggerFactory from '~/utils/logger';
@@ -17,11 +18,6 @@ export type SaveFileParam = {
   data,
 }
 
-export type CheckLimitResult = {
-  isUploadable: boolean,
-  errorMessage?: string,
-}
-
 export type TemporaryUrl = {
   url: string,
   lifetimeSec: number,
@@ -38,7 +34,7 @@ export interface FileUploader {
   deleteFiles(): void,
   getFileUploadTotalLimit(): number,
   getTotalFileSize(): Promise<number>,
-  doCheckLimit(uploadFileSize: number, maxFileSize: number, totalLimit: number): Promise<CheckLimitResult>,
+  doCheckLimit(uploadFileSize: number, maxFileSize: number, totalLimit: number): Promise<ICheckLimitResult>,
   determineResponseMode(): ResponseMode,
   respond(res: Response, attachment: IAttachmentDocument, opts?: RespondOptions): void,
   findDeliveryFile(attachment: IAttachmentDocument): Promise<NodeJS.ReadableStream>,
@@ -135,7 +131,7 @@ export abstract class AbstractFileUploader implements FileUploader {
    * Check files size limits for all uploaders
    *
    */
-  async doCheckLimit(uploadFileSize: number, maxFileSize: number, totalLimit: number): Promise<CheckLimitResult> {
+  async doCheckLimit(uploadFileSize: number, maxFileSize: number, totalLimit: number): Promise<ICheckLimitResult> {
     if (uploadFileSize > maxFileSize) {
       return { isUploadable: false, errorMessage: 'File size exceeds the size limit per file' };
     }