Преглед изворни кода

Merge branch 'dev/7.0.x' into support/133781-refactor-where-to-get-users-to-be-notified

WNomunomu пре 2 година
родитељ
комит
bcc7b47d46
33 измењених фајлова са 333 додато и 342 уклоњено
  1. 3 1
      .devcontainer/Dockerfile
  2. 2 1
      .devcontainer/devcontainer.json
  3. 2 36
      .devcontainer/docker-compose.yml
  4. 2 4
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  5. 2 4
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  6. 11 32
      apps/app/src/components/InAppNotification/InAppNotificationElm.tsx
  7. 1 1
      apps/app/src/components/InAppNotification/InAppNotificationList.tsx
  8. 3 1
      apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx
  9. 30 7
      apps/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  10. 11 8
      apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx
  11. 3 3
      apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts
  12. 2 4
      apps/app/src/components/InstallerForm.tsx
  13. 0 1
      apps/app/src/components/Navbar/hooks.tsx
  14. 5 5
      apps/app/src/components/PageComment/CommentEditor.tsx
  15. 0 106
      apps/app/src/components/PageEditor/Editor.module.scss
  16. 6 6
      apps/app/src/components/PageEditor/Editor.tsx
  17. 11 12
      apps/app/src/components/PageEditor/PageEditor.tsx
  18. 1 5
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  19. 2 2
      apps/app/src/interfaces/editor-settings.ts
  20. 2 2
      apps/app/src/interfaces/in-app-notification.ts
  21. 0 1
      apps/app/src/interfaces/page-operation.ts
  22. 5 5
      apps/app/src/pages/[[...path]].page.tsx
  23. 1 0
      apps/app/src/server/routes/apiv3/in-app-notification.ts
  24. 0 17
      apps/app/src/server/routes/page.js
  25. 4 4
      apps/app/src/stores/context.tsx
  26. 3 10
      apps/app/src/stores/in-app-notification.ts
  27. 0 20
      apps/app/src/styles/_mixins.scss
  28. 101 0
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.module.scss
  29. 47 5
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  30. 2 1
      packages/editor/src/services/file-dropzone/index.ts
  31. 0 38
      packages/editor/src/services/file-dropzone/use-file-dropzone.ts
  32. 19 0
      packages/editor/src/services/file-dropzone/use-file-dropzone/FileDropzoneOverlay.tsx
  33. 52 0
      packages/editor/src/services/file-dropzone/use-file-dropzone/use-file-dropzone.ts

+ 3 - 1
.devcontainer/Dockerfile

@@ -37,7 +37,9 @@ RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable
 RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
 
 RUN apt-get update \
-    && apt-get -y install --no-install-recommends git-lfs \
+    && apt-get -y install --no-install-recommends \
+      git-lfs \
+      iputils-ping net-tools dnsutils \
 
     # Uncomment below lines to install Chromium
     # --- works only on AMD64 ---

+ 2 - 1
.devcontainer/devcontainer.json

@@ -19,6 +19,7 @@
     "eamodio.gitlens",
     "github.vscode-pull-request-github",
     "cschleiden.vscode-github-actions",
+    "mongodb.mongodb-vscode",
     "msjsdiag.debugger-for-chrome",
     "firefox-devtools.vscode-firefox-debug",
     "editorconfig.editorconfig",
@@ -34,7 +35,7 @@
   // "shutdownAction": "none",
 
   // Use 'postCreateCommand' to run commands after the container is created.
-  "postCreateCommand": "git-lfs pull & yarn install",
+  "postCreateCommand": "git-lfs pull & turbo run bootstrap",
 
   // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
   "remoteUser": "node"

+ 2 - 36
.devcontainer/docker-compose.yml

@@ -31,17 +31,10 @@ services:
     image: mongo:6.0
     restart: unless-stopped
     ports:
-      - 27018:27017
+      - 27017
     volumes:
       - /data/db
 
-  # ogp:
-  #   image: ghcr.io/weseek/growi-unique-ogp:latest
-  #   ports:
-  #     - 8088:8088
-  #   restart: unless-stopped
-  #   tty: true
-
   # This container requires '../../growi-docker-compose' repository
   #   cloned from https://github.com/weseek/growi-docker-compose.git
   elasticsearch:
@@ -52,7 +45,7 @@ services:
         - version=8.7.0
     restart: unless-stopped
     ports:
-      - 9201:9200
+      - 9200
     environment:
       - bootstrap.memory_lock=true
       - "ES_JAVA_OPTS=-Xms256m -Xmx256m"
@@ -65,33 +58,6 @@ services:
       - /usr/share/elasticsearch/data
       - ../../growi-docker-compose/elasticsearch/v8/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
 
-  #need to adjust kibana version based on elasticsearch version (use same version as elasticsearch version)
-  # kibana:
-  #   image: docker.elastic.co/kibana/kibana:8.7.0
-  #   restart: unless-stopped
-  #   environment:
-  #     ELASTICSEARCH_HOSTS: 'http://elasticsearch:9200'
-  #   ports:
-  #     - 5601:5601
-  #   depends_on:
-  #     - elasticsearch
-
-  # This container requires '../../growi-docker-compose' repository
-  #   cloned from https://github.com/weseek/growi-docker-compose.git
-  hackmd:
-    build:
-      context: ../../growi-docker-compose/hackmd
-    restart: unless-stopped
-    environment:
-      - GROWI_URI=http://localhost:3000
-      # define 'storage' option value
-      # see https://github.com/sequelize/cli/blob/7160d0/src/helpers/config-helper.js#L192
-      - CMD_DB_URL=sqlite://dummyhost/hackmd/sqlite/codimd.db
-      - CMD_CSP_ENABLE=false
-    ports:
-      - 3011:3000
-    volumes:
-      - /files/sqlite
 volumes:
   node_modules:
   node_modules_app:

+ 2 - 4
apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -30,14 +30,13 @@ export const useDrawioModalLauncherForView = (opts?: {
   const { data: shareLinkId } = useShareLinkId();
 
   const { data: currentPage } = useSWRxCurrentPage();
-  const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
 
   const { open: openDrawioModal } = useDrawioModal();
 
   const saveOrUpdate = useSaveOrUpdate();
 
   const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
-    if (currentPage == null || tagsInfo == null || shareLinkId != null) {
+    if (currentPage == null || shareLinkId != null) {
       return;
     }
 
@@ -50,7 +49,6 @@ export const useDrawioModalLauncherForView = (opts?: {
       grant: currentPage.grant,
       grantUserGroupId: currentPage.grantedGroup?._id,
       grantUserGroupName: currentPage.grantedGroup?.name,
-      pageTags: tagsInfo.tags,
     };
 
     try {
@@ -67,7 +65,7 @@ export const useDrawioModalLauncherForView = (opts?: {
       logger.error('failed to save', error);
       opts?.onSaveError?.(error);
     }
-  }, [currentPage, opts, saveOrUpdate, shareLinkId, tagsInfo]);
+  }, [currentPage, opts, saveOrUpdate, shareLinkId]);
 
 
   // set handler to open DrawioModal

+ 2 - 4
apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -29,14 +29,13 @@ export const useHandsontableModalLauncherForView = (opts?: {
   const { data: shareLinkId } = useShareLinkId();
 
   const { data: currentPage } = useSWRxCurrentPage();
-  const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
 
   const { open: openHandsontableModal } = useHandsontableModal();
 
   const saveOrUpdate = useSaveOrUpdate();
 
   const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
-    if (currentPage == null || tagsInfo == null || shareLinkId != null) {
+    if (currentPage == null || shareLinkId != null) {
       return;
     }
 
@@ -49,7 +48,6 @@ export const useHandsontableModalLauncherForView = (opts?: {
       grant: currentPage.grant,
       grantUserGroupId: currentPage.grantedGroup?._id,
       grantUserGroupName: currentPage.grantedGroup?.name,
-      pageTags: tagsInfo.tags,
     };
 
     try {
@@ -66,7 +64,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
       logger.error('failed to save', error);
       opts?.onSaveError?.(error);
     }
-  }, [currentPage, opts, saveOrUpdate, shareLinkId, tagsInfo]);
+  }, [currentPage, opts, saveOrUpdate, shareLinkId]);
 
 
   // set handler to open HandsonTableModal

+ 11 - 32
apps/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -2,7 +2,7 @@ import React, {
   FC, useRef,
 } from 'react';
 
-import type { HasObjectId } from '@growi/core';
+import type { IUser, IPage, HasObjectId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 import { DropdownItem } from 'reactstrap';
 
@@ -40,31 +40,6 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
     }
   };
 
-  const getActionUsers = () => {
-    if (notification.targetModel === SupportedTargetModel.MODEL_USER) {
-      return notification.target.username;
-    }
-
-    const latestActionUsers = notification.actionUsers.slice(0, 3);
-    const latestUsers = latestActionUsers.map((user) => {
-      return `@${user.name}`;
-    });
-
-    let actionedUsers = '';
-    const latestUsersCount = latestUsers.length;
-    if (latestUsersCount === 1) {
-      actionedUsers = latestUsers[0];
-    }
-    else if (notification.actionUsers.length >= 4) {
-      actionedUsers = `${latestUsers.slice(0, 2).join(', ')} and ${notification.actionUsers.length - 2} others`;
-    }
-    else {
-      actionedUsers = latestUsers.join(', ');
-    }
-
-    return actionedUsers;
-  };
-
   const renderActionUserPictures = (): JSX.Element => {
     const actionUsers = notification.actionUsers;
 
@@ -84,10 +59,16 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
     );
   };
 
-  const actionUsers = getActionUsers();
-
   const isDropdownItem = props.type === 'dropdown-item';
 
+  const isPageNotification = (notification: IInAppNotification): notification is IInAppNotification<IPage> => {
+    return notification.targetModel === SupportedTargetModel.MODEL_PAGE;
+  };
+
+  const isUserNotification = (notification: IInAppNotification): notification is IInAppNotification<IUser> => {
+    return notification.targetModel === SupportedTargetModel.MODEL_USER;
+  };
+
   // determine tag
   const TagElem = isDropdownItem
     ? DropdownItem
@@ -105,18 +86,16 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
         >
         </span>
         {renderActionUserPictures()}
-        {notification.targetModel === SupportedTargetModel.MODEL_PAGE && (
+        {isPageNotification(notification) && (
           <PageModelNotification
             ref={notificationRef}
             notification={notification}
-            actionUsers={actionUsers}
           />
         )}
-        {notification.targetModel === SupportedTargetModel.MODEL_USER && (
+        {isUserNotification(notification) && (
           <UserModelNotification
             ref={notificationRef}
             notification={notification}
-            actionUsers={actionUsers}
           />
         )}
       </div>

+ 1 - 1
apps/app/src/components/InAppNotification/InAppNotificationList.tsx

@@ -26,7 +26,7 @@ const InAppNotificationList: FC<Props> = (props: Props) => {
     );
   }
 
-  const notifications = inAppNotificationData.docs.filter((notification) => { return notification.parsedSnapshot != null });
+  const notifications = inAppNotificationData.docs;
 
   return (
     <>

+ 3 - 1
apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx

@@ -31,7 +31,9 @@ export const ModelNotification: FC<Props> = (props) => {
   return (
     <div className="p-2 overflow-hidden">
       <div className="text-truncate">
-        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
+        <b>{actionUsers}</b>
+        {actionMsg}
+        <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
       </div>
       <i className={`${actionIcon} me-2`} />
       <FormattedDistanceDate

+ 30 - 7
apps/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -1,32 +1,53 @@
 import React, {
-  forwardRef, ForwardRefRenderFunction,
+  forwardRef, ForwardRefRenderFunction, useCallback,
 } from 'react';
 
-import type { HasObjectId } from '@growi/core';
+import type { IPage, HasObjectId } from '@growi/core';
 import { useRouter } from 'next/router';
 
 import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
+import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
 
 import { ModelNotification } from './ModelNotification';
 import { useActionMsgAndIconForPageModelNotification } from './useActionAndMsg';
 
 
 interface Props {
-  notification: IInAppNotification & HasObjectId
-  actionUsers: string
+  notification: IInAppNotification<IPage> & HasObjectId
 }
 
 const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable, Props> = (props: Props, ref) => {
 
-  const {
-    notification, actionUsers,
-  } = props;
+  const { notification } = props;
 
   const { actionMsg, actionIcon } = useActionMsgAndIconForPageModelNotification(notification);
 
   const router = useRouter();
 
+  const getActionUsers = useCallback(() => {
+    const latestActionUsers = notification.actionUsers.slice(0, 3);
+    const latestUsers = latestActionUsers.map((user) => {
+      return `@${user.name}`;
+    });
+
+    let actionedUsers = '';
+    const latestUsersCount = latestUsers.length;
+    if (latestUsersCount === 1) {
+      actionedUsers = latestUsers[0];
+    }
+    else if (notification.actionUsers.length >= 4) {
+      actionedUsers = `${latestUsers.slice(0, 2).join(', ')} and ${notification.actionUsers.length - 2} others`;
+    }
+    else {
+      actionedUsers = latestUsers.join(', ');
+    }
+
+    return actionedUsers;
+  }, [notification.actionUsers]);
+
+  const actionUsers = getActionUsers();
+
   // publish open()
   const publishOpen = () => {
     if (notification.target != null) {
@@ -38,6 +59,8 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
     }
   };
 
+  notification.parsedSnapshot = pageSerializers.parseSnapshot(notification.snapshot);
+
   return (
     <ModelNotification
       notification={notification}

+ 11 - 8
apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx

@@ -2,7 +2,7 @@ import React, {
   forwardRef, ForwardRefRenderFunction,
 } from 'react';
 
-import type { HasObjectId } from '@growi/core';
+import type { IUser, HasObjectId } from '@growi/core';
 import { useRouter } from 'next/router';
 
 import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
@@ -11,22 +11,25 @@ import type { IInAppNotification } from '~/interfaces/in-app-notification';
 import { ModelNotification } from './ModelNotification';
 import { useActionMsgAndIconForUserModelNotification } from './useActionAndMsg';
 
+interface Props {
+  notification: IInAppNotification<IUser> & HasObjectId
+}
 
-const UserModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable, {
-  notification: IInAppNotification & HasObjectId
-  actionUsers: string
-}> = ({
-  notification, actionUsers,
-}, ref) => {
-  const router = useRouter();
+const UserModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable, Props> = (props: Props, ref) => {
+
+  const { notification } = props;
 
   const { actionMsg, actionIcon } = useActionMsgAndIconForUserModelNotification(notification);
 
+  const router = useRouter();
+
   // publish open()
   const publishOpen = () => {
     router.push('/admin/users');
   };
 
+  const actionUsers = notification.target.username;
+
   return (
     <ModelNotification
       notification={notification}

+ 3 - 3
apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts

@@ -1,4 +1,4 @@
-import type { HasObjectId } from '@growi/core';
+import type { IUser, IPage, HasObjectId } from '@growi/core';
 
 import { SupportedAction } from '~/interfaces/activity';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
@@ -8,7 +8,7 @@ export type ActionMsgAndIconType = {
   actionIcon: string
 }
 
-export const useActionMsgAndIconForPageModelNotification = (notification: IInAppNotification & HasObjectId): ActionMsgAndIconType => {
+export const useActionMsgAndIconForPageModelNotification = (notification: IInAppNotification<IPage> & HasObjectId): ActionMsgAndIconType => {
   const actionType: string = notification.action;
   let actionMsg: string;
   let actionIcon: string;
@@ -77,7 +77,7 @@ export const useActionMsgAndIconForPageModelNotification = (notification: IInApp
   };
 };
 
-export const useActionMsgAndIconForUserModelNotification = (notification: IInAppNotification & HasObjectId): ActionMsgAndIconType => {
+export const useActionMsgAndIconForUserModelNotification = (notification: IInAppNotification<IUser> & HasObjectId): ActionMsgAndIconType => {
   const actionType: string = notification.action;
   let actionMsg: string;
   let actionIcon: string;

+ 2 - 4
apps/app/src/components/InstallerForm.tsx

@@ -99,10 +99,8 @@ const InstallerForm = memo((): JSX.Element => {
       <div className="row">
         <form role="form" id="register-form" className="col-md-12" onSubmit={submitHandler}>
           <div className="dropdown mb-3">
-            <div className="input-group">
-              <div className=" dropdown-with-icon">
-                <i className="input-group-text icon-bubbles border-0 rounded-0" />
-              </div>
+            <div className="input-group dropdown-with-icon">
+              <span className="input-group-text"><i className="icon-bubbles" /></span>
               <button
                 type="button"
                 className="btn btn-secondary dropdown-toggle form-control text-end rounded-end"

+ 0 - 1
apps/app/src/components/Navbar/hooks.tsx

@@ -35,7 +35,6 @@ export const useOnPageEditorModeButtonClicked = (
           isSlackEnabled: false,
           slackChannels: '',
           grant,
-          pageTags: [],
           grantUserGroupId,
         };
 

+ 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 (

+ 11 - 12
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,
@@ -98,7 +98,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: currentPage } = useSWRxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { data: grantData } = useSelectedGrant();
-  const { data: pageTags, sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
+  const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { data: editingMarkdown, mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
@@ -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();
@@ -218,12 +218,11 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       isSlackEnabled: isSlackEnabled ?? false,
       slackChannels: '', // set in save method by opts in SavePageControlls.tsx
       grant: grantData.grant,
-      pageTags: pageTags ?? [],
       grantUserGroupId: grantData.grantedGroup?.id,
       grantUserGroupName: grantData.grantedGroup?.name,
     };
     return optionsToSave;
-  }, [grantData, isSlackEnabled, pageTags]);
+  }, [grantData, isSlackEnabled]);
 
 
   const save = useCallback(async(opts?: {slackChannels: string, overwriteScopesOfDescendants?: boolean}): Promise<IPageHasId | null> => {
@@ -357,14 +356,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 +572,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}

+ 1 - 5
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -10,8 +10,8 @@ import { useCurrentUser } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
-import { DropendMenu } from './DropendMenu';
 import { CreateButton } from './CreateButton';
+import { DropendMenu } from './DropendMenu';
 import { DropendToggle } from './DropendToggle';
 
 const logger = loggerFactory('growi:cli:PageCreateButton');
@@ -50,7 +50,6 @@ export const PageCreateButton = React.memo((): JSX.Element => {
         isSlackEnabled: false,
         slackChannels: '',
         grant: currentPage?.grant || 1,
-        pageTags: [],
         grantUserGroupId: currentPage?.grantedGroup?._id,
         shouldGeneratePath: true,
       };
@@ -82,7 +81,6 @@ export const PageCreateButton = React.memo((): JSX.Element => {
         isSlackEnabled: false,
         slackChannels: '',
         grant: 1,
-        pageTags: [],
       };
 
       const res = await exist(JSON.stringify([todaysPath]));
@@ -115,7 +113,6 @@ export const PageCreateButton = React.memo((): JSX.Element => {
         isSlackEnabled: false,
         slackChannels: '',
         grant: currentPage?.grant || 1,
-        pageTags: [],
         grantUserGroupId: currentPage?.grantedGroup?._id,
       };
 
@@ -149,7 +146,6 @@ export const PageCreateButton = React.memo((): JSX.Element => {
         isSlackEnabled: false,
         slackChannels: '',
         grant: currentPage?.grant || 1,
-        pageTags: [],
         grantUserGroupId: currentPage?.grantedGroup?._id,
       };
 

+ 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,
   }
 }

+ 2 - 2
apps/app/src/interfaces/in-app-notification.ts

@@ -10,10 +10,10 @@ export enum InAppNotificationStatuses {
 
 // TODO: do not use any type
 // https://redmine.weseek.co.jp/issues/120632
-export interface IInAppNotification {
+export interface IInAppNotification<T = unknown> {
   user: IUser
   targetModel: SupportedTargetModelType
-  target: any
+  target: T
   action: SupportedActionType
   status: InAppNotificationStatuses
   actionUsers: IUser[]

+ 0 - 1
apps/app/src/interfaces/page-operation.ts

@@ -31,7 +31,6 @@ export type OptionsToSave = {
   isSlackEnabled: boolean;
   slackChannels: string;
   grant: number;
-  pageTags: string[] | null;
   grantUserGroupId?: string | null;
   grantUserGroupName?: string | null;
   shouldGeneratePath?: boolean | null;

+ 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');

+ 1 - 0
apps/app/src/server/routes/apiv3/in-app-notification.ts

@@ -1,6 +1,7 @@
 import { SupportedAction } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 
+
 import { IInAppNotification } from '../../../interfaces/in-app-notification';
 
 const express = require('express');

+ 0 - 17
apps/app/src/server/routes/page.js

@@ -329,7 +329,6 @@ module.exports = function(crowi, app) {
     const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
     const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
     const slackChannels = req.body.slackChannels || null;
-    const pageTags = req.body.pageTags || undefined;
 
     if (body === null || pagePath === null) {
       return res.json(ApiResponse.error('Parameters body and path are required.'));
@@ -352,16 +351,9 @@ module.exports = function(crowi, app) {
 
     const createdPage = await crowi.pageService.create(pagePath, body, req.user, options);
 
-    let savedTags;
-    if (pageTags != null) {
-      await PageTagRelation.updatePageTags(createdPage.id, pageTags);
-      savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
-    }
-
     const result = {
       page: serializePageSecurely(createdPage),
       revision: serializeRevisionSecurely(createdPage.revision),
-      tags: savedTags,
     };
     res.json(ApiResponse.success(result));
 
@@ -456,7 +448,6 @@ module.exports = function(crowi, app) {
     const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
     const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
     const slackChannels = req.body.slackChannels || null;
-    const pageTags = req.body.pageTags || undefined;
 
     if (pageId === null || pageBody === null || revisionId === null) {
       return res.json(ApiResponse.error('page_id, body and revision_id are required.'));
@@ -497,18 +488,10 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error(err));
     }
 
-    let savedTags;
-    if (pageTags != null) {
-      const tagEvent = crowi.event('tag');
-      await PageTagRelation.updatePageTags(pageId, pageTags);
-      savedTags = await PageTagRelation.listTagNamesByPage(pageId);
-      tagEvent.emit('update', page, savedTags);
-    }
 
     const result = {
       page: serializePageSecurely(page),
       revision: serializeRevisionSecurely(page.revision),
-      tags: savedTags,
     };
     res.json(ApiResponse.success(result));
 

+ 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> => {

+ 3 - 10
apps/app/src/stores/in-app-notification.ts

@@ -1,8 +1,8 @@
 import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
+
 import { SupportedTargetModel } from '~/interfaces/activity';
 import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
-import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
 import * as userSerializers from '~/models/serializers/in-app-notification-snapshot/user';
 import loggerFactory from '~/utils/logger';
 
@@ -24,15 +24,8 @@ export const useSWRxInAppNotifications = (
       const inAppNotificationPaginateResult = response.data as inAppNotificationPaginateResult;
       inAppNotificationPaginateResult.docs.forEach((doc) => {
         try {
-          switch (doc.targetModel) {
-            case SupportedTargetModel.MODEL_PAGE:
-              doc.parsedSnapshot = pageSerializers.parseSnapshot(doc.snapshot);
-              break;
-            case SupportedTargetModel.MODEL_USER:
-              doc.parsedSnapshot = userSerializers.parseSnapshot(doc.snapshot);
-              break;
-            default:
-              throw new Error(`No serializer found for targetModel: ${doc.targetModel}`);
+          if (doc.targetModel === SupportedTargetModel.MODEL_USER) {
+            doc.parsedSnapshot = userSerializers.parseSnapshot(doc.snapshot);
           }
         }
         catch (err) {

+ 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,
+  };
+};