Просмотр исходного кода

Merge pull request #8191 from weseek/imprv/131554-131771-file-drop-overlay

imprv:  Enable file uploading overlay
Yuki Takei 2 лет назад
Родитель
Сommit
b4b4e737d3

+ 5 - 5
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -16,7 +16,7 @@ import { IEditorMethods } from '~/interfaces/editor-methods';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
 import {
   useCurrentUser, useIsSlackConfigured,
-  useIsUploadableFile, useIsUploadableImage,
+  useIsUploadAllFileAllowed, useIsUploadEnabled,
 } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
@@ -71,8 +71,8 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: isSlackConfigured } = useIsSlackConfigured();
-  const { data: isUploadableFile } = useIsUploadableFile();
-  const { data: isUploadableImage } = useIsUploadableImage();
+  const { data: isUploadAllFileAllowed } = useIsUploadAllFileAllowed();
+  const { data: isUploadEnabled } = useIsUploadEnabled();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const {
     increment: incrementEditingCommentsNum,
@@ -303,7 +303,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
       </Button>
     );
 
-    const isUploadable = isUploadableImage || isUploadableFile;
+    const isUploadable = isUploadEnabled || isUploadAllFileAllowed;
 
     return (
       <>
@@ -315,7 +315,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
                 ref={editorRef}
                 value={commentBody ?? ''} // DO NOT use state
                 isUploadable={isUploadable}
-                isUploadableFile={isUploadableFile}
+                isUploadAllFileAllowed={isUploadAllFileAllowed}
                 onChange={onChangeHandler}
                 onUpload={uploadHandler}
                 onCtrlEnter={ctrlEnterHandler}

+ 0 - 106
apps/app/src/components/PageEditor/Editor.module.scss

@@ -4,112 +4,6 @@
 
 
 .editor-container :global {
-  // overlay in .editor-container
-  .overlay {
-    position: absolute;
-    top: 0;
-    right: 0;
-    bottom: 0;
-    left: 0;
-    z-index: 7; // forward than .CodeMirror-vscrollbar
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-
-  // loading keymap
-  @include ms.overlay-processing-style(overlay-loading-keymap, 2.5em, 0.3em);
-
-  // for Dropzone
-  .dropzone {
-    position: relative; // against .overlay position: absolute
-
-    @include ms.overlay-processing-style(overlay-dropzone-active, 2.5em, 0.5em);
-
-    // unuploadable or rejected
-    &.dropzone-unuploadable,
-    &.dropzone-rejected {
-      .overlay.overlay-dropzone-active {
-        background: rgba(200, 200, 200, 0.8);
-
-        .overlay-content {
-          color: bs.$gray-300;
-        }
-      }
-    }
-
-    // uploading
-    &.dropzone-uploading {
-      @include ms.overlay-processing-style(overlay-dropzone-active, 2.5em, 0.5em);
-    }
-
-    // unuploadable
-    &.dropzone-unuploadable {
-      .overlay.overlay-dropzone-active {
-        .overlay-content {
-          // insert content
-          @include ms.insertSimpleLineIcons('\e617'); // icon-exclamation
-
-          &:after {
-            content: 'File uploading is disabled';
-          }
-        }
-      }
-    }
-
-    // uploadable
-    &.dropzone-uploadable {
-      // accepted
-      &.dropzone-accepted:not(.dropzone-rejected) {
-        .overlay.overlay-dropzone-active {
-          border: 4px dashed bs.$gray-300;
-
-          .overlay-content {
-            // insert content
-            @include ms.insertSimpleLineIcons('\e084'); // icon-cloud-upload
-
-            &:after {
-              content: 'Drop here to upload';
-            }
-
-            // style
-            color: bs.$secondary;
-            background: rgba(200, 200, 200, 0.8);
-          }
-        }
-      }
-
-      // file type mismatch
-      &.dropzone-rejected:not(.dropzone-uploadablefile) {
-        .overlay.overlay-dropzone-active {
-          .overlay-content {
-            // insert content
-            @include ms.insertSimpleLineIcons('\e032'); // icon-picture
-
-            &:after {
-              content: 'Only an image file is allowed';
-            }
-          }
-        }
-      }
-
-      // multiple files
-      &.dropzone-accepted.dropzone-rejected {
-        .overlay.overlay-dropzone-active {
-          .overlay-content {
-            // insert content
-            @include ms.insertSimpleLineIcons('\e617'); // icon-exclamation
-
-            &:after {
-              content: 'Only 1 file is allowed';
-            }
-          }
-        }
-      }
-    }
-
-    /* end of.dropzone */
-  }
 
   .btn.btn-open-dropzone {
     z-index: 2;

+ 6 - 6
apps/app/src/components/PageEditor/Editor.tsx

@@ -32,7 +32,7 @@ export type EditorPropsType = {
   isGfmMode?: boolean,
   noCdn?: boolean,
   isUploadable?: boolean,
-  isUploadableFile?: boolean,
+  isUploadAllFileAllowed?: boolean,
   onChange?: (newValue: string, isClean?: boolean) => void,
   onUpload?: (file) => void,
   editorSettings?: IEditorSettings,
@@ -54,7 +54,7 @@ type DropzoneRef = {
 
 const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props, ref): JSX.Element => {
   const {
-    onUpload, isUploadable, isUploadableFile, indentSize, isGfmMode = true,
+    onUpload, isUploadable, isUploadAllFileAllowed, indentSize, isGfmMode = true,
   } = props;
 
   const [dropzoneActive, setDropzoneActive] = useState(false);
@@ -121,7 +121,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
   const getAcceptableType = useCallback(() => {
     let accept = 'null'; // reject all
     if (isUploadable) {
-      if (!isUploadableFile) {
+      if (!isUploadAllFileAllowed) {
         accept = 'image/*'; // image only
       }
       else {
@@ -130,7 +130,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
     }
 
     return accept;
-  }, [isUploadable, isUploadableFile]);
+  }, [isUploadable, isUploadAllFileAllowed]);
 
   const pasteFilesHandler = useCallback((event) => {
     const items = event.clipboardData.items || event.clipboardData.files || [];
@@ -191,7 +191,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
     else {
       className += ' dropzone-uploadable';
 
-      if (isUploadableFile) {
+      if (isUploadAllFileAllowed) {
         className += ' dropzone-uploadablefile';
       }
     }
@@ -210,7 +210,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
     }
 
     return className;
-  }, [isUploadable, isUploading, isUploadableFile]);
+  }, [isUploadable, isUploading, isUploadAllFileAllowed]);
 
   const renderDropzoneOverlay = useCallback(() => {
     return (

+ 9 - 9
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -24,7 +24,7 @@ import { SocketEventName } from '~/interfaces/websocket';
 import {
   useDefaultIndentSize,
   useCurrentPathname, useIsEnabledAttachTitleHeader,
-  useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsIndentSizeForced,
+  useIsEditable, useIsUploadAllFileAllowed, useIsUploadEnabled, useIsIndentSizeForced,
 } from '~/stores/context';
 import {
   useCurrentIndentSize, useIsSlackEnabled, usePageTagsForEditors,
@@ -110,8 +110,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: defaultIndentSize } = useDefaultIndentSize();
-  const { data: isUploadableFile } = useIsUploadableFile();
-  const { data: isUploadableImage } = useIsUploadableImage();
+  const { data: isUploadAllFileAllowed } = useIsUploadAllFileAllowed();
+  const { data: isUploadEnabled } = useIsUploadEnabled();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
   const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
   const { mutate: mutateRemotePageId } = useRemoteRevisionId();
@@ -357,14 +357,14 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   }, [codeMirrorEditor, currentPagePath, pageId]);
 
   const acceptedFileType = useMemo(() => {
-    if (!isUploadableFile) {
+    if (!isUploadEnabled) {
       return AcceptedUploadFileType.NONE;
     }
-    if (isUploadableImage) {
-      return AcceptedUploadFileType.IMAGE;
+    if (isUploadAllFileAllowed) {
+      return AcceptedUploadFileType.ALL;
     }
-    return AcceptedUploadFileType.ALL;
-  }, [isUploadableFile, isUploadableImage]);
+    return AcceptedUploadFileType.IMAGE;
+  }, [isUploadAllFileAllowed, isUploadEnabled]);
 
   const scrollPreviewByEditorLine = useCallback((line: number) => {
     if (previewRef.current == null) {
@@ -573,7 +573,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             ref={editorRef}
             value={initialValue}
             isUploadable={isUploadable}
-            isUploadableFile={isUploadableFile}
+            isUploadAllFileAllowed={isUploadAllFileAllowed}
             indentSize={currentIndentSize}
             onScroll={editorScrolledHandler}
             onScrollCursorIntoView={editorScrollCursorIntoViewHandler}

+ 2 - 2
apps/app/src/interfaces/editor-settings.ts

@@ -18,7 +18,7 @@ export interface IEditorSettings {
 
 export type EditorConfig = {
   upload: {
-    isUploadableFile: boolean,
-    isUploadableImage: boolean,
+    isUploadAllFileAllowed: boolean,
+    isUploadEnabled: boolean,
   }
 }

+ 5 - 5
apps/app/src/pages/[[...path]].page.tsx

@@ -37,7 +37,7 @@ import {
   useIsAclEnabled, useIsSearchPage, useIsEnabledAttachTitleHeader,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useIsEnabledMarp, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useGrowiCloudUri,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
+  useEditorConfig, useIsAllReplyShown, useIsUploadAllFileAllowed, useIsUploadEnabled, useIsContainerFluid, useIsNotCreatable,
 } from '~/stores/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import {
@@ -217,8 +217,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   // useGrowiRendererConfig(props.growiRendererConfigStr != null ? JSON.parse(props.growiRendererConfigStr) : undefined);
   useIsAllReplyShown(props.isAllReplyShown);
 
-  useIsUploadableFile(props.editorConfig.upload.isUploadableFile);
-  useIsUploadableImage(props.editorConfig.upload.isUploadableImage);
+  useIsUploadAllFileAllowed(props.editorConfig.upload.isUploadAllFileAllowed);
+  useIsUploadEnabled(props.editorConfig.upload.isUploadEnabled);
 
   const { pageWithMeta } = props;
 
@@ -561,8 +561,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
   props.editorConfig = {
     upload: {
-      isUploadableFile: crowi.fileUploadService.getFileUploadEnabled(),
-      isUploadableImage: crowi.fileUploadService.getIsUploadable(),
+      isUploadAllFileAllowed: crowi.fileUploadService.getFileUploadEnabled(),
+      isUploadEnabled: crowi.fileUploadService.getIsUploadable(),
     },
   };
   props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');

+ 4 - 4
apps/app/src/stores/context.tsx

@@ -157,12 +157,12 @@ export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boo
   return useContextSWR('isBlinkedAtBoot', initialData, { fallbackData: false });
 };
 
-export const useIsUploadableImage = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useContextSWR('isUploadableImage', initialData);
+export const useIsUploadEnabled = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useContextSWR('isUploadEnabled', initialData);
 };
 
-export const useIsUploadableFile = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useContextSWR('isUploadableFile', initialData);
+export const useIsUploadAllFileAllowed = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useContextSWR('isUploadAllFileAllowed', initialData);
 };
 
 export const useShowPageLimitationL = (initialData?: number): SWRResponse<number, Error> => {

+ 0 - 20
apps/app/src/styles/_mixins.scss

@@ -59,26 +59,6 @@
   }
 }
 
-@mixin overlay-processing-style($additionalSelector, $contentFontSize: inherit, $contentPadding: inherit) {
-  .overlay.#{$additionalSelector} {
-    background: rgba(255, 255, 255, 0.5);
-    .overlay-content {
-      padding: $contentPadding;
-      font-size: $contentFontSize;
-      color: bs.$gray-700;
-      background: rgba(200, 200, 200, 0.5);
-    }
-  }
-}
-
-@mixin insertSimpleLineIcons($code) {
-  &:before {
-    margin-right: 0.2em;
-    font-family: 'simple-line-icons';
-    content: $code;
-  }
-}
-
 @mixin grw-skeleton-text($font-size, $line-height) {
   height: $line-height;
   padding: (($line-height - $font-size)  / 2) 0;

+ 101 - 0
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.module.scss

@@ -1,3 +1,6 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+
 .codemirror-editor-container :global {
 
   .cm-editor {
@@ -35,3 +38,101 @@
   }
 
 }
+
+
+@mixin insertMaterialSymbolAndMessage($code, $message) {
+  .overlay-icon:before {
+    margin-right: 0.2em;
+    font-size:1.4em;
+    content: $code;
+  }
+  &:after {
+    content: $message;
+  }
+}
+
+.codemirror-editor :global {
+
+  // overlay in .codemirror-editor
+  .overlay {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 7; // forward than .CodeMirror-vscrollbar
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  // for Dropzone
+  .dropzone {
+    position: relative; // against .overlay position: absolute
+
+    .overlay.overlay-dropzone-active {
+      background: rgba(255, 255, 255, 0.5);
+      .overlay-content {
+        padding: 0.5em;
+        font-size: 2.5em;
+        color: bs.$gray-700;
+        background: rgba(200, 200, 200, 0.5);
+      }
+    }
+
+    // uploading
+    &.dropzone-uploading {
+      .overlay.overlay-dropzone-active {
+        .overlay-content {
+          @include insertMaterialSymbolAndMessage('upload_file', 'Uploading...');
+        }
+      }
+    }
+
+    // diabled
+    &.dropzone-disabled {
+      .overlay.overlay-dropzone-active {
+        .overlay-content {
+          @include insertMaterialSymbolAndMessage('error', 'File uploading is disabled');
+        }
+      }
+    }
+
+    // accepted
+    &.dropzone-accepted {
+      .overlay.overlay-dropzone-active {
+        // style
+        color: bs.$secondary;
+        background: rgba(200, 200, 200, 0.8);
+        border: 4px dashed bs.$gray-300;
+
+        .overlay-content {
+          @include insertMaterialSymbolAndMessage('cloud_upload', 'Drop here to upload');
+        }
+      }
+    }
+
+    // file type mismatch
+    &.dropzone-mismatch-picture {
+      .overlay.overlay-dropzone-active {
+        .overlay-content {
+          @include insertMaterialSymbolAndMessage('photo', 'Only an image file is allowed');
+        }
+      }
+    }
+
+    // rejected
+    &.dropzone-rejected {
+      .overlay.overlay-dropzone-active {
+        background: rgba(200, 200, 200, 0.8);
+
+        .overlay-content {
+          @include insertMaterialSymbolAndMessage('error', 'This file is not allowed');
+        }
+
+      }
+    }
+    /* end of.dropzone */
+  }
+}
+

+ 47 - 5
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -7,7 +7,7 @@ import { EditorView } from '@codemirror/view';
 import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 
 import { GlobalCodeMirrorEditorKey, AcceptedUploadFileType } from '../../consts';
-import { useFileDropzone } from '../../services';
+import { useFileDropzone, FileDropzoneOverlay } from '../../services';
 import { useCodeMirrorEditorIsolated } from '../../stores';
 
 import { Toolbar } from './Toolbar';
@@ -100,12 +100,54 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
 
   }, [codeMirrorEditor]);
 
-  const { getRootProps, open } = useFileDropzone({ onUpload, acceptedFileType });
+  const {
+    getRootProps,
+    isDragActive,
+    isDragAccept,
+    isDragReject,
+    isUploading,
+    open,
+  } = useFileDropzone({ onUpload, acceptedFileType });
+
+  const fileUploadState = useMemo(() => {
+
+    if (isUploading) {
+      return 'dropzone-uploading';
+    }
+
+    switch (acceptedFileType) {
+      case AcceptedUploadFileType.NONE:
+        return 'dropzone-disabled';
+
+      case AcceptedUploadFileType.IMAGE:
+        if (isDragAccept) {
+          return 'dropzone-accepted';
+        }
+        if (isDragReject) {
+          return 'dropzone-mismatch-picture';
+        }
+        break;
+
+      case AcceptedUploadFileType.ALL:
+        if (isDragAccept) {
+          return 'dropzone-accepted';
+        }
+        if (isDragReject) {
+          return 'dropzone-rejected';
+        }
+        break;
+    }
+
+    return '';
+  }, [isUploading, isDragAccept,isDragReject, acceptedFileType]);
 
   return (
-    <div {...getRootProps()} className="flex-expand-vert">
-      <CodeMirrorEditorContainer ref={containerRef} />
-      <Toolbar editorKey={editorKey} onFileOpen={open} acceptedFileType={acceptedFileType} />
+    <div className={`${style['codemirror-editor']} flex-expand-vert`}>
+      <div {...getRootProps()} className={`dropzone ${fileUploadState} flex-expand-vert`}>
+        <FileDropzoneOverlay isEnabled={isDragActive}/>
+        <CodeMirrorEditorContainer ref={containerRef} />
+        <Toolbar editorKey={editorKey} onFileOpen={open} acceptedFileType={acceptedFileType} />
+      </div>
     </div>
   );
 };

+ 2 - 1
packages/editor/src/services/file-dropzone/index.ts

@@ -1 +1,2 @@
-export * from './use-file-dropzone';
+export * from './use-file-dropzone/use-file-dropzone';
+export * from './use-file-dropzone/FileDropzoneOverlay';

+ 0 - 38
packages/editor/src/services/file-dropzone/use-file-dropzone.ts

@@ -1,38 +0,0 @@
-import { useCallback } from 'react';
-
-import { useDropzone, Accept } from 'react-dropzone';
-import type { DropzoneState } from 'react-dropzone';
-
-import { AcceptedUploadFileType } from '../../consts';
-
-type DropzoneEditor = {
-  onUpload?: (files: File[]) => void,
-  acceptedFileType: AcceptedUploadFileType,
-}
-
-export const useFileDropzone = (props: DropzoneEditor): DropzoneState => {
-
-  const { onUpload, acceptedFileType } = props;
-
-  const dropHandler = useCallback((acceptedFiles: File[]) => {
-    if (onUpload == null) {
-      return;
-    }
-    onUpload(acceptedFiles);
-  }, [onUpload]);
-
-  const accept: Accept = {
-    acceptedFileType: [],
-  };
-
-  const disabled = acceptedFileType === AcceptedUploadFileType.NONE;
-
-  return useDropzone({
-    noKeyboard: true,
-    noClick: true,
-    disabled,
-    onDrop: dropHandler,
-    accept,
-  });
-
-};

+ 19 - 0
packages/editor/src/services/file-dropzone/use-file-dropzone/FileDropzoneOverlay.tsx

@@ -0,0 +1,19 @@
+type Props = {
+  isEnabled: boolean,
+}
+
+export const FileDropzoneOverlay = (props: Props) => {
+  const { isEnabled } = props;
+
+    if (isEnabled) {
+      return (
+        <div className="overlay overlay-dropzone-active">
+          <span className="overlay-content">
+            <span className="overlay-icon material-symbols-outlined">
+            </span>
+          </span>
+        </div>
+      );
+    }
+    return <></>;
+}

+ 52 - 0
packages/editor/src/services/file-dropzone/use-file-dropzone/use-file-dropzone.ts

@@ -0,0 +1,52 @@
+import { useCallback, useState } from 'react';
+
+import { useDropzone, Accept } from 'react-dropzone';
+import type { DropzoneState } from 'react-dropzone';
+
+import { AcceptedUploadFileType } from '../../../consts';
+
+type FileDropzoneState = DropzoneState & {
+  isUploading: boolean,
+}
+
+type DropzoneEditor = {
+  onUpload?: (files: File[]) => void,
+  acceptedFileType: AcceptedUploadFileType,
+}
+
+export const useFileDropzone = (props: DropzoneEditor): FileDropzoneState => {
+
+  const { onUpload, acceptedFileType } = props;
+
+  const [isUploading, setIsUploading] = useState(false);
+
+  const dropHandler = useCallback((acceptedFiles: File[]) => {
+    if (onUpload == null) {
+      return;
+    }
+    if (acceptedFileType === AcceptedUploadFileType.NONE) {
+      return;
+    }
+
+    setIsUploading(true);
+    onUpload(acceptedFiles);
+    setIsUploading(false);
+
+  }, [onUpload, setIsUploading, acceptedFileType]);
+
+  const accept: Accept = {
+  };
+  accept[acceptedFileType] = [];
+
+  const dzState = useDropzone({
+    noKeyboard: true,
+    noClick: true,
+    onDrop: dropHandler,
+    accept,
+  });
+
+  return {
+    ...dzState,
+    isUploading,
+  };
+};