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

Merge branch 'imprv/126522-change-font-size-in-editor-header-level' into feat/130149-able-to-change-font-size-in-header-level

soumaeda 2 лет назад
Родитель
Сommit
71539d6f33
52 измененных файлов с 409 добавлено и 739 удалено
  1. 1 0
      .devcontainer/Dockerfile
  2. 1 1
      .devcontainer/devcontainer.json
  3. 1 1
      .github/workflows/auto-labeling.yml
  4. 5 2
      .github/workflows/ci-app.yml
  5. 3 0
      .github/workflows/ci-slackbot-proxy.yml
  6. 1 0
      .github/workflows/release-slackbot-proxy.yml
  7. 4 8
      .github/workflows/release.yml
  8. 1 1
      .github/workflows/reusable-app-build-image.yml
  9. 5 3
      .github/workflows/reusable-app-prod.yml
  10. 1 0
      .github/workflows/reusable-app-reg-suit.yml
  11. 25 1
      CHANGELOG.md
  12. 2 2
      README.md
  13. 2 2
      README_JP.md
  14. 1 1
      apps/app/bin/github-actions/update-readme.sh
  15. 1 0
      apps/app/docker/Dockerfile
  16. 3 3
      apps/app/docker/README.md
  17. 1 0
      apps/app/package.json
  18. 3 2
      apps/app/public/static/locales/en_US/admin.json
  19. 3 2
      apps/app/public/static/locales/ja_JP/admin.json
  20. 3 2
      apps/app/public/static/locales/zh_CN/admin.json
  21. 2 2
      apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
  22. 32 55
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  23. 0 4
      apps/app/src/components/Navbar/GrowiSubNavigation.module.scss
  24. 2 22
      apps/app/src/components/Navbar/GrowiSubNavigation.tsx
  25. 58 57
      apps/app/src/components/PageEditor/PageEditor.tsx
  26. 0 0
      apps/app/src/components/PageSideContents/PageSideContents.module.scss
  27. 71 7
      apps/app/src/components/PageSideContents/PageSideContents.tsx
  28. 1 0
      apps/app/src/components/PageSideContents/index.ts
  29. 3 4
      apps/app/src/components/PageTags/PageTags.tsx
  30. 0 0
      apps/app/src/components/PageTags/RenderTagLabels.tsx
  31. 1 1
      apps/app/src/components/PageTags/TagEditModal.tsx
  32. 0 0
      apps/app/src/components/PageTags/TagLabels.module.scss
  33. 1 3
      apps/app/src/components/PageTags/TagsInput.tsx
  34. 2 0
      apps/app/src/components/PageTags/index.ts
  35. 4 4
      apps/app/src/components/Sidebar/PageTree/Item.module.scss
  36. 0 4
      apps/app/src/styles/_editor.scss
  37. 3 3
      apps/app/src/styles/molecules/_list-group-item.scss
  38. 1 0
      apps/slackbot-proxy/docker/Dockerfile
  39. 2 3
      package.json
  40. 1 0
      packages/editor/package.json
  41. 7 3
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  42. 9 3
      packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropup.tsx
  43. 8 2
      packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx
  44. 3 1
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  45. 14 0
      packages/editor/src/components/playground/Playground.tsx
  46. 8 0
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts
  47. 24 0
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/insert-text.ts
  48. 15 0
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/replace-text.ts
  49. 1 0
      packages/editor/src/services/file-dropzone/index.ts
  50. 27 0
      packages/editor/src/services/file-dropzone/use-file-dropzone.ts
  51. 1 0
      packages/editor/src/services/index.ts
  52. 41 530
      yarn.lock

+ 1 - 0
.devcontainer/Dockerfile

@@ -51,6 +51,7 @@ RUN apt-get update \
 ENV DEBIAN_FRONTEND=dialog
 
 RUN yarn global add turbo
+RUN yarn global add node-gyp
 
 # Uncomment to default to non-root user
 # USER $USER_UID

+ 1 - 1
.devcontainer/devcontainer.json

@@ -34,7 +34,7 @@
   // "shutdownAction": "none",
 
   // Use 'postCreateCommand' to run commands after the container is created.
-  "postCreateCommand": "yarn global add turbo && yarn install",
+  "postCreateCommand": "yarn global add turbo node-gyp && yarn install",
 
   // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
   "remoteUser": "node"

+ 1 - 1
.github/workflows/auto-labeling.yml

@@ -37,7 +37,7 @@ jobs:
         !startsWith( github.head_ref, 'dependabot/' ))
 
     steps:
-      - uses: amannn/action-semantic-pull-request@v5.0.2
+      - uses: amannn/action-semantic-pull-request@v5
         with:
           types: |
             feat

+ 5 - 2
.github/workflows/ci-app.yml

@@ -61,6 +61,7 @@ jobs:
       - name: Install dependencies
         run: |
           yarn global add turbo
+          yarn global add node-gyp
           yarn --frozen-lockfile
 
       - name: Lint
@@ -95,7 +96,7 @@ jobs:
 
     services:
       mongodb:
-        image: mongo:4.4
+        image: mongo:6.0
         ports:
           - 27017/tcp
 
@@ -132,6 +133,7 @@ jobs:
       - name: Install dependencies
         run: |
           yarn global add turbo
+          yarn global add node-gyp
           yarn --frozen-lockfile
 
       - name: Test
@@ -176,7 +178,7 @@ jobs:
 
     services:
       mongodb:
-        image: mongo:4.4
+        image: mongo:6.0
         ports:
           - 27017/tcp
 
@@ -214,6 +216,7 @@ jobs:
       - name: Install dependencies
         run: |
           yarn global add turbo
+          yarn global add node-gyp
           yarn --frozen-lockfile
 
       - name: turbo run dev:ci

+ 3 - 0
.github/workflows/ci-slackbot-proxy.yml

@@ -63,6 +63,7 @@ jobs:
     - name: Install dependencies
       run: |
         yarn global add turbo
+        yarn global add node-gyp
         yarn --frozen-lockfile
 
     - name: Lint
@@ -137,6 +138,7 @@ jobs:
     - name: Install dependencies
       run: |
         yarn global add turbo
+        yarn global add node-gyp
         yarn --frozen-lockfile
 
     - name: yarn dev:ci
@@ -220,6 +222,7 @@ jobs:
 
     - name: Install dependencies
       run: |
+        yarn global add node-gyp
         yarn --frozen-lockfile
 
     - name: Restore dist

+ 1 - 0
.github/workflows/release-slackbot-proxy.yml

@@ -109,6 +109,7 @@ jobs:
     - name: Install dependencies
       run: |
         yarn global add turbo
+        yarn global add node-gyp
         yarn --frozen-lockfile
 
     - name: Bump versions for next RC

+ 4 - 8
.github/workflows/release.yml

@@ -31,6 +31,7 @@ jobs:
     - name: Install dependencies
       run: |
         yarn global add turbo
+        yarn global add node-gyp
         yarn --frozen-lockfile
 
     - name: Bump versions
@@ -153,7 +154,7 @@ jobs:
 
 
   post-publish:
-    needs: [publish-image, publish-image-ghcr]
+    needs: [create-github-release, publish-image, publish-image-ghcr]
     runs-on: ubuntu-latest
 
     steps:
@@ -176,15 +177,9 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         created_tag: 'v${{ needs.create-github-release.outputs.RELEASED_VERSION }}'
 
-    - name: Check whether workspace is clean
-      run: |
-        STATUS=`git status --porcelain`
-        if [ -z "$STATUS" ]; then exit 0; else exit 1; fi
-
-
 
   create-pr-for-next-rc:
-    needs: [publish-image, publish-image-ghcr]
+    needs: [create-github-release, publish-image, publish-image-ghcr]
     runs-on: ubuntu-latest
 
     steps:
@@ -201,6 +196,7 @@ jobs:
     - name: Install dependencies
       run: |
         yarn global add turbo
+        yarn global add node-gyp
         yarn --frozen-lockfile
 
     - name: Bump versions for next RC

+ 1 - 1
.github/workflows/reusable-app-build-image.yml

@@ -36,7 +36,7 @@ jobs:
     - uses: actions/checkout@v3
 
     - name: Configure AWS Credentials
-      uses: aws-actions/configure-aws-credentials@v2
+      uses: aws-actions/configure-aws-credentials@v4
       with:
         aws-region: ap-northeast-1
         role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}

+ 5 - 3
.github/workflows/reusable-app-prod.yml

@@ -56,6 +56,7 @@ jobs:
 
     - name: Install dependencies
       run: |
+        yarn global add node-gyp
         yarn --frozen-lockfile
 
     - name: Restore dist
@@ -126,7 +127,7 @@ jobs:
 
     services:
       mongodb:
-        image: mongo:4.4
+        image: mongo:6.0
         ports:
         - 27017/tcp
       elasticsearch:
@@ -214,7 +215,7 @@ jobs:
 
     services:
       mongodb:
-        image: mongo:4.4
+        image: mongo:6.0
         ports:
         - 27017/tcp
       elasticsearch:
@@ -267,6 +268,7 @@ jobs:
 
     - name: Install dependencies
       run: |
+        yarn global add node-gyp
         yarn --frozen-lockfile
         yarn cypress install
 
@@ -303,7 +305,7 @@ jobs:
         cat config/ci/.env.local.for-auto-install-with-allowing-guest >> .env.production.local
 
     - name: Cypress Run
-      uses: cypress-io/github-action@v5
+      uses: cypress-io/github-action@v6
       with:
         browser: chromium
         working-directory: ./apps/app

+ 1 - 0
.github/workflows/reusable-app-reg-suit.yml

@@ -82,6 +82,7 @@ jobs:
 
     - name: Install dependencies
       run: |
+        yarn global add node-gyp
         yarn --frozen-lockfile
 
     - name: Download screenshots taken by cypress

+ 25 - 1
CHANGELOG.md

@@ -1,9 +1,33 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.2.0...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.2.1...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.2.1](https://github.com/weseek/growi/compare/v6.2.0...v6.2.1) - 2023-10-03
+
+### BREAKING CHANGES
+
+- support: Omit promster (#8105) @yuki-takei
+
+### 🚀 Improvement
+
+- imprv: Download a markdown file using the page name as the file name (#8061) @soumaeda
+- imprv: Admin customize presentation form (#8083) @meiri-k
+- imprv: i18n for marp settings (#8110) @moekumasaka
+- imprv: i18n "Create /Sidebar page" label (#8085) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Marp is enabled incorrectly problem (#8100) @reiji-h
+- fix: Do not work img tag if use style property 62x (#8092) @jam411
+
+### 🧰 Maintenance
+
+- support: Internationalization USER_REGISTRATION_APPROVAL_REQUEST label for v62x (#8130) @jam411
+- ci(deps): bump get-func-name from 2.0.0 to 2.0.2 (#8119) @dependabot
+- support: Omit promster (#8105) @yuki-takei
+
 ## [v6.2.0](https://github.com/weseek/growi/compare/v6.1.12...v6.2.0) - 2023-09-14
 
 ### 💎 Features

+ 2 - 2
README.md

@@ -83,12 +83,12 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 - npm 6.x
 - yarn
 - [Turborepo](https://turbo.build/repo)
-- MongoDB 4.x
+- MongoDB 4.4 or above
 
 ### Optional Dependencies
 
 - Redis 3.x
-- ElasticSearch 6.x or 7.x (needed when using Full-text search)
+- ElasticSearch 7.x or 8.x (needed when using Full-text search)
   - **CAUTION: Following plugins are required**
     - [Japanese (kuromoji) Analysis plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)
     - [ICU Analysis Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html)

+ 2 - 2
README_JP.md

@@ -82,12 +82,12 @@ Crowi からの移行は **[こちら](https://docs.growi.org/en/admin-guide/mig
 - npm 6.x
 - yarn
 - [Turborepo](https://turbo.build/repo)
-- MongoDB 4.x
+- MongoDB 4.4 以上
 
 ### オプションの依存関係
 
 - Redis 3.x
-- ElasticSearch 6.x or 7.x (needed when using Full-text search)
+- ElasticSearch 7.x or 8.x (needed when using Full-text search)
   - **注意: 次のプラグインが必要です**
     - [Japanese (kuromoji) Analysis plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)
     - [ICU Analysis Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html)

+ 1 - 1
apps/app/bin/github-actions/update-readme.sh

@@ -2,4 +2,4 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`7\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/packages\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`7\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/apps\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md

+ 1 - 0
apps/app/docker/Dockerfile

@@ -34,6 +34,7 @@ COPY --from=base ${optDir}/out/yarn.lock ./yarn.lock
 
 # setup (with network-timeout = 1 hour)
 RUN yarn config set network-timeout 3600000
+RUN yarn global add node-gyp
 RUN yarn --frozen-lockfile
 
 # make artifacts

+ 3 - 3
apps/app/docker/README.md

@@ -11,8 +11,8 @@ Supported tags and respective Dockerfile links
 ------------------------------------------------
 
 * [`7.0.0`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.0/apps/app/docker/Dockerfile)
-* [`6.2.0`, `6.2`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.0/apps/app/docker/Dockerfile)
-* [`6.1.0`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.8/apps/app/docker/Dockerfile)
+* [`6.2.1`, `6.2`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.1/apps/app/docker/Dockerfile)
+* [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 
 
 What is GROWI?
@@ -30,7 +30,7 @@ Requirements
 
 ### Optional Dependencies
 
-* ElasticSearch (>= 6.6)
+* ElasticSearch (>= 7.17)
     * Japanese (kuromoji) Analysis plugin
     * ICU Analysis Plugin
 

+ 1 - 0
apps/app/package.json

@@ -242,6 +242,7 @@
     "null-loader": "^4.0.1",
     "plantuml-encoder": "^1.2.5",
     "prettier": "^1.19.1",
+    "pretty-bytes": "^6.1.1",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",

+ 3 - 2
apps/app/public/static/locales/en_US/admin.json

@@ -488,8 +488,8 @@
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "marp_official_site": "The Marp Official Site",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "GROWI Docs - Create slide using Marp",
-      "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
+      "presentation_docs" : "GROWI Docs - Create slides for a presentation",
+      "presentation_docs_link": "https://docs.growi.org/en/guide/features/presentation.html"
     },
     "custom_title": "Custom title",
     "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag. Following placeholders will be automatically replaced:",
@@ -912,6 +912,7 @@
     "USER_API_TOKEN_UPDATE": "API Token update",
     "USER_EDITOR_SETTINGS_UPDATE": "Editor settings update",
     "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "In-App Notification settings update",
+    "USER_REGISTRATION_APPROVAL_REQUEST": "User registration request for ID/Password authentication",
     "PAGE_VIEW": "Page view",
     "PAGE_USER_HOME_VIEW": "Page view (User home)",
     "PAGE_FORBIDDEN": "Page view (Fobidden page)",

+ 3 - 2
apps/app/public/static/locales/ja_JP/admin.json

@@ -497,8 +497,8 @@
       "enable_marp_desc": "プレゼンテーション表示に Marp を利用できるようになります。ただし、XSS に対して脆弱になる恐れがあります。",
       "marp_official_site": "参考:Marp 公式サイト",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "参考:GROWI Docs - Marp でスライドを作成する",
-      "marp_in_growi_link": "https://docs.growi.org/ja/guide/features/marp.html"
+      "presentation_docs" : "参考:GROWI Docs - プレゼンテーション機能を使う",
+      "presentation_docs_link": "https://docs.growi.org/ja/guide/features/presentation.html"
     },
     "custom_title": "カスタム Title",
     "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。以下のプレースホルダーは自動的に置換されます:",
@@ -921,6 +921,7 @@
     "USER_API_TOKEN_UPDATE": "API トークンの更新",
     "USER_EDITOR_SETTINGS_UPDATE": "エディター設定の更新",
     "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "アプリ内通知設定の更新",
+    "USER_REGISTRATION_APPROVAL_REQUEST": "ID/Password 認証のユーザー登録リクエスト",
     "PAGE_VIEW": "ページ閲覧",
     "PAGE_USER_HOME_VIEW": "ページ閲覧(ユーザーホーム)",
     "PAGE_FORBIDDEN": "ページ閲覧(fobiddenページ)",

+ 3 - 2
apps/app/public/static/locales/zh_CN/admin.json

@@ -496,8 +496,8 @@
       "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
       "marp_official_site": "参考资料:Marp 官方网站",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "参考资料:GROWI Docs - Create slide using Marp",
-      "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
+      "presentation_docs" : "参考资料:GROWI Docs - Create slides for a presentation",
+      "presentation_docs_link": "https://docs.growi.org/en/guide/features/presentation.html"
     },
     "custom_title": "自定义标题",
     "custom_title_detail": "您可以自定义<code>&lt;title&gt;</code>标记。<br><code>&123;&123;sitename&&125;&125;</code>将自动替换为应用程序名称,并且<code>&123;&123;page&&125;&125;</code>将替换为页面名称/路径。",
@@ -920,6 +920,7 @@
     "USER_API_TOKEN_UPDATE": "API 令牌更新",
     "USER_EDITOR_SETTINGS_UPDATE": "编辑器设置更新",
     "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "应用内通知设置更新",
+    "USER_REGISTRATION_APPROVAL_REQUEST": "用户注册 ID/密码验证请求",
     "PAGE_VIEW": "页面浏览量",
     "PAGE_USER_HOME_VIEW": "页面浏览量(用户主页)",
     "PAGE_FORBIDDEN": "页面浏览量(禁止页面)",

+ 2 - 2
apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx

@@ -51,10 +51,10 @@ const CustomizePresentationSetting = (props: Props): JSX.Element => {
               </a>
               <br></br>
               <a
-                href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
+                href={`${t('admin:customize_settings.presentation_options.presentation_docs_link')}`}
                 target="_blank"
                 rel="noopener noreferrer"
-              >{`${t('admin:customize_settings.presenattion_options.marp_in_growi')}`}
+              >{`${t('admin:customize_settings.presentation_options.presentation_docs')}`}
               </a>
             </p>
           </CustomizePresentationOption>

+ 32 - 55
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import { isPopulated } from '@growi/core';
 import type {
@@ -11,25 +11,22 @@ import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { DropdownItem } from 'reactstrap';
 
-import { exportAsMarkdown, updateContentWidth, useUpdateStateAfterSave } from '~/client/services/page-operation';
-import { apiPost } from '~/client/util/apiv1-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
   useCurrentPathname,
   useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId, useIsContainerFluid, useIsIdenticalPath,
 } from '~/stores/context';
-import { usePageTagsForEditors } from '~/stores/editor';
 import {
   usePageAccessoriesModal, PageAccessoriesModalContents, IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
 import {
-  useSWRMUTxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useTemplateTagData, useSWRxPageInfo,
+  useSWRMUTxCurrentPage, useCurrentPageId, useIsNotFound, useSWRxPageInfo,
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import {
-  EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
+  EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
 } from '~/stores/ui';
 
@@ -217,38 +214,39 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isContainerFluid } = useIsContainerFluid();
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
-  const { data: isAbleToShowTagLabel } = useIsAbleToShowTagLabel();
   const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
   const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
 
-  const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRxTagsInfo(currentPage?._id);
-
+  // TODO: implement tags for editor
+  // refs: https://redmine.weseek.co.jp/issues/132125
   // eslint-disable-next-line max-len
-  const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(!isSharedPage ? currentPage?._id : undefined);
+  // const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(!isSharedPage ? currentPage?._id : undefined);
+  // const { data: templateTagData } = useTemplateTagData();
 
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
-  const { data: templateTagData } = useTemplateTagData();
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId);
 
-  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
-
   const path = currentPage?.path ?? currentPathname;
 
-  useEffect(() => {
-    // Run only when tagsInfoData has been updated
-    if (templateTagData == null) {
-      syncPageTagsForEditors();
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [tagsInfoData?.tags]);
-
-  useEffect(() => {
-    if (pageId === null && templateTagData != null) {
-      mutatePageTagsForEditors(templateTagData);
-    }
-  }, [pageId, mutatePageTagsForEditors, templateTagData, mutateSWRTagsInfo]);
+  // TODO: implement tags for editor
+  // refs: https://redmine.weseek.co.jp/issues/132125
+  // useEffect(() => {
+  //   // Run only when tagsInfoData has been updated
+  //   if (templateTagData == null) {
+  //     syncPageTagsForEditors();
+  //   }
+  //   // eslint-disable-next-line react-hooks/exhaustive-deps
+  // }, [tagsInfoData?.tags]);
+
+  // TODO: implement tags for editor
+  // refs: https://redmine.weseek.co.jp/issues/132125
+  // useEffect(() => {
+  //   if (pageId === null && templateTagData != null) {
+  //     mutatePageTagsForEditors(templateTagData);
+  //   }
+  // }, [pageId, mutatePageTagsForEditors, templateTagData, mutateSWRTagsInfo]);
 
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
 
@@ -257,30 +255,13 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const isViewMode = editorMode === EditorMode.View;
 
 
-  const tagsUpdatedHandlerForViewMode = useCallback(async(newTags: string[]) => {
-    if (currentPage == null) {
-      return;
-    }
-
-    const { _id: pageId, revision: revisionId } = currentPage;
-    try {
-      await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
-
-      updateStateAfterSave?.();
-
-      toastSuccess('updated tags successfully');
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-  }, [currentPage, updateStateAfterSave]);
-
-  const tagsUpdatedHandlerForEditMode = useCallback((newTags: string[]): void => {
-    // It will not be reflected in the DB until the page is refreshed
-    mutatePageTagsForEditors(newTags);
-    return;
-  }, [mutatePageTagsForEditors]);
+  // TODO: implement tags for editor
+  // refs: https://redmine.weseek.co.jp/issues/132125
+  // const tagsUpdatedHandlerForEditMode = useCallback((newTags: string[]): void => {
+  //   // It will not be reflected in the DB until the page is refreshed
+  //   mutatePageTagsForEditors(newTags);
+  //   return;
+  // }, [mutatePageTagsForEditors]);
 
   const duplicateItemClickedHandler = useCallback(async(page: IPageForPageDuplicateModal) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
@@ -440,12 +421,8 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       pagePath={pagePath}
       pageId={currentPage?._id}
       showDrawerToggler={isDrawerMode}
-      showTagLabel={isAbleToShowTagLabel}
-      isTagLabelsDisabled={!!isGuestUser || !!isReadOnlyUser}
       isDrawerMode={isDrawerMode}
       isCompactMode={isCompactMode}
-      tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
-      tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
       rightComponent={RightComponent}
       additionalClasses={['container-fluid']}
     />

+ 0 - 4
apps/app/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -33,10 +33,6 @@
       line-height: 1.4em;
     }
 
-    .grw-taglabels-container {
-      margin-bottom: 0.5rem;
-    }
-
     .grw-page-path-nav {
       .separator {
         margin-right: 0.2em;

+ 2 - 22
apps/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -1,12 +1,9 @@
 import React from 'react';
 
-import dynamic from 'next/dynamic';
-
 import {
   EditorMode, useEditorMode,
 } from '~/stores/ui';
 
-import { TagLabelsSkeleton } from '../Page/TagLabels';
 import PagePathNav from '../PagePathNav';
 
 import DrawerToggler from './DrawerToggler';
@@ -15,23 +12,15 @@ import DrawerToggler from './DrawerToggler';
 import styles from './GrowiSubNavigation.module.scss';
 
 
-const TagLabels = dynamic(() => import('../Page/TagLabels').then(mod => mod.TagLabels), {
-  ssr: false,
-  loading: TagLabelsSkeleton,
-});
-
-
 export type GrowiSubNavigationProps = {
   pagePath?: string,
   pageId?: string,
   isNotFound?: boolean,
   showDrawerToggler?: boolean,
-  showTagLabel?: boolean,
   isTagLabelsDisabled?: boolean,
   isDrawerMode?: boolean,
   isCompactMode?: boolean,
   tags?: string[],
-  tagsUpdatedHandler?: (newTags: string[]) => Promise<void> | void,
   rightComponent?: React.FunctionComponent,
   additionalClasses?: string[],
 }
@@ -42,9 +31,8 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
 
   const {
     pageId, pagePath,
-    showDrawerToggler, showTagLabel,
-    isTagLabelsDisabled, isDrawerMode, isCompactMode,
-    tags, tagsUpdatedHandler,
+    showDrawerToggler,
+    isDrawerMode, isCompactMode,
     rightComponent: RightComponent,
     additionalClasses = [],
   } = props;
@@ -67,14 +55,6 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
           </div>
         ) }
         <div className="grw-path-nav-container">
-          { (showTagLabel && !isCompactMode) && (
-            <div className="grw-taglabels-container">
-              { tags != null
-                ? <TagLabels tags={tags} isTagLabelsDisabled={isTagLabelsDisabled ?? false} tagsUpdateInvoked={tagsUpdatedHandler} />
-                : <TagLabelsSkeleton />
-              }
-            </div>
-          ) }
           { pagePath != null && (
             <PagePathNav pageId={pageId} pagePath={pagePath} isSingleLineMode={isEditorMode} isCompactMode={isCompactMode} />
           ) }

+ 58 - 57
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -294,67 +294,67 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   }, [isNotFound, mutateEditorMode, router, save, t, updateStateAfterSave]);
 
 
-  /**
-   * the upload event handler
-   * @param {any} file
-   */
-  const uploadHandler = useCallback(async(file) => {
-    try {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      let res: any = await apiGet('/attachments.limit', {
-        fileSize: file.size,
-      });
-
-      if (!res.isUploadable) {
-        throw new Error(res.errorMessage);
+  // the upload event handler
+  const uploadHandler = useCallback((files: File[]) => {
+    files.forEach(async(file) => {
+      try {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const resLimit: any = await apiGet('/attachments.limit', {
+          fileSize: file.size,
+        });
+
+        if (!resLimit.isUploadable) {
+          throw new Error(resLimit.errorMessage);
+        }
+
+        const formData = new FormData();
+        formData.append('file', file);
+        if (currentPagePath != null) {
+          formData.append('path', currentPagePath);
+        }
+        if (pageId != null) {
+          formData.append('page_id', pageId);
+        }
+        if (pageId == null) {
+          formData.append('page_body', codeMirrorEditor?.getDoc() ?? '');
+        }
+
+        const resAdd: any = await apiPostForm('/attachments.add', formData);
+        const attachment = resAdd.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}`;
+        }
+        // TODO: implement
+        // refs: https://redmine.weseek.co.jp/issues/126528
+        // editorRef.current.insertText(insertText);
+        codeMirrorEditor?.insertText(insertText);
+
+        // when if created newly
+        // Not using 'mutateGrant' to inherit the grant of the parent page
+        if (resAdd.pageCreated) {
+          logger.info('Page is created', resAdd.page._id);
+          mutateIsLatestRevision(true);
+          setCreatedPageRevisionIdWithAttachment(resAdd.page.revision);
+          await mutateCurrentPageId(resAdd.page._id);
+          await mutateCurrentPage();
+        }
       }
-
-      const formData = new FormData();
-      // const { pageId, path } = pageContainer.state;
-      formData.append('file', file);
-      if (currentPagePath != null) {
-        formData.append('path', currentPagePath);
-      }
-      if (pageId != null) {
-        formData.append('page_id', pageId);
+      catch (e) {
+        logger.error('failed to upload', e);
+        toastError(e);
       }
-      if (pageId == null) {
-        formData.append('page_body', codeMirrorEditor?.getDoc() ?? '');
+      finally {
+        // TODO: implement
+        // refs: https://redmine.weseek.co.jp/issues/126528
+        // editorRef.current.terminateUploadingState();
       }
+    });
 
-      res = await apiPostForm('/attachments.add', formData);
-      const attachment = res.attachment;
-      const fileName = attachment.originalName;
-
-      let insertText = `[${fileName}](${attachment.filePathProxied})`;
-      // when image
-      if (attachment.fileFormat.startsWith('image/')) {
-        // modify to "![fileName](url)" syntax
-        insertText = `!${insertText}`;
-      }
-      // TODO: implement
-      // refs: https://redmine.weseek.co.jp/issues/126528
-      // editorRef.current.insertText(insertText);
-
-      // when if created newly
-      // Not using 'mutateGrant' to inherit the grant of the parent page
-      if (res.pageCreated) {
-        logger.info('Page is created', res.page._id);
-        mutateIsLatestRevision(true);
-        setCreatedPageRevisionIdWithAttachment(res.page.revision);
-        await mutateCurrentPageId(res.page._id);
-        await mutateCurrentPage();
-      }
-    }
-    catch (e) {
-      logger.error('failed to upload', e);
-      toastError(e);
-    }
-    finally {
-      // TODO: implement
-      // refs: https://redmine.weseek.co.jp/issues/126528
-      // editorRef.current.terminateUploadingState();
-    }
   }, [codeMirrorEditor, currentPagePath, mutateCurrentPage, mutateCurrentPageId, mutateIsLatestRevision, pageId]);
 
 
@@ -574,6 +574,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
         <CodeMirrorEditorMain
           onChange={markdownChangedHandler}
           onSave={saveWithShortcut}
+          onUpload={uploadHandler}
           indentSize={currentIndentSize ?? defaultIndentSize}
         />
       </div>

+ 0 - 0
apps/app/src/components/PageSideContents.module.scss → apps/app/src/components/PageSideContents/PageSideContents.module.scss


+ 71 - 7
apps/app/src/components/PageSideContents.tsx → apps/app/src/components/PageSideContents/PageSideContents.tsx

@@ -1,17 +1,24 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
-import type { IPageHasId, IPageInfoForOperation } from '@growi/core';
+import { getIdForRef, type IPageHasId, type IPageInfoForOperation } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import { Link } from 'react-scroll';
 
+import { useUpdateStateAfterSave } from '~/client/services/page-operation';
+import { apiPost } from '~/client/util/apiv1-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
-import { useSWRxPageInfo } from '~/stores/page';
+import { useSWRxPageInfo, useSWRxTagsInfo } from '~/stores/page';
+import { useIsAbleToShowTagLabel } from '~/stores/ui';
 
-import CountBadge from './Common/CountBadge';
-import { ContentLinkButtons } from './ContentLinkButtons';
-import PageListIcon from './Icons/PageListIcon';
-import TableOfContents from './TableOfContents';
+import CountBadge from '../Common/CountBadge';
+import { ContentLinkButtons } from '../ContentLinkButtons';
+import PageListIcon from '../Icons/PageListIcon';
+import { PageTagsSkeleton } from '../PageTags';
+import TableOfContents from '../TableOfContents';
 
 import styles from './PageSideContents.module.scss';
 
@@ -19,6 +26,59 @@ import styles from './PageSideContents.module.scss';
 const { isTopPage, isUsersHomepage, isTrashPage } = pagePathUtils;
 
 
+const PageTags = dynamic(() => import('../PageTags').then(mod => mod.PageTags), {
+  ssr: false,
+  loading: PageTagsSkeleton,
+});
+
+
+type TagsProps = {
+  pageId: string,
+  revisionId: string,
+}
+
+const Tags = (props: TagsProps): JSX.Element => {
+  const { pageId, revisionId } = props;
+
+  const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
+
+  const { data: showTagLabel } = useIsAbleToShowTagLabel();
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
+
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
+
+  const tagsUpdatedHandler = useCallback(async(newTags: string[]) => {
+    try {
+      await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
+
+      updateStateAfterSave?.();
+
+      toastSuccess('updated tags successfully');
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+  }, [pageId, revisionId, updateStateAfterSave]);
+
+  if (!showTagLabel) {
+    return <></>;
+  }
+
+  const isTagLabelsDisabled = !!isGuestUser || !!isReadOnlyUser;
+
+  return (
+    <div className="grw-taglabels-container">
+      { tagsInfoData?.tags != null
+        ? <PageTags tags={tagsInfoData.tags} isTagLabelsDisabled={isTagLabelsDisabled ?? false} tagsUpdateInvoked={tagsUpdatedHandler} />
+        : <PageTagsSkeleton />
+      }
+    </div>
+  );
+};
+
+
 export type PageSideContentsProps = {
   page: IPageHasId,
   isSharedUser?: boolean,
@@ -38,8 +98,12 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
   const isUsersHomepagePath = isUsersHomepage(pagePath);
   const isTrash = isTrashPage(pagePath);
 
+
   return (
     <>
+      {/* Tags */}
+      <Tags pageId={page._id} revisionId={getIdForRef(page.revision)} />
+
       {/* Page list */}
       <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
         {!isSharedUser && (

+ 1 - 0
apps/app/src/components/PageSideContents/index.ts

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

+ 3 - 4
apps/app/src/components/Page/TagLabels.tsx → apps/app/src/components/PageTags/PageTags.tsx

@@ -13,11 +13,11 @@ type Props = {
   tagsUpdateInvoked?: (tags: string[]) => Promise<void> | void,
 }
 
-export const TagLabelsSkeleton = (): JSX.Element => {
+export const PageTagsSkeleton = (): JSX.Element => {
   return <Skeleton additionalClass={`${styles['grw-tag-labels-skeleton']} py-1`} />;
 };
 
-export const TagLabels:FC<Props> = (props: Props) => {
+export const PageTags:FC<Props> = (props: Props) => {
   const { tags, isTagLabelsDisabled, tagsUpdateInvoked } = props;
 
   const [isTagEditModalShown, setIsTagEditModalShown] = useState(false);
@@ -31,13 +31,12 @@ export const TagLabels:FC<Props> = (props: Props) => {
   };
 
   if (tags == null) {
-    return <TagLabelsSkeleton />;
+    return <PageTagsSkeleton />;
   }
 
   return (
     <>
       <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
-        <i className="tag-icon icon-tag me-2" />
         <RenderTagLabels
           tags={tags}
           openEditorModal={openEditorModal}

+ 0 - 0
apps/app/src/components/Page/RenderTagLabels.tsx → apps/app/src/components/PageTags/RenderTagLabels.tsx


+ 1 - 1
apps/app/src/components/Page/TagEditModal.tsx → apps/app/src/components/PageTags/TagEditModal.tsx

@@ -5,7 +5,7 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import TagsInput from './TagsInput';
+import { TagsInput } from './TagsInput';
 
 type Props = {
   tags: string[],

+ 0 - 0
apps/app/src/components/Page/TagLabels.module.scss → apps/app/src/components/PageTags/TagLabels.module.scss


+ 1 - 3
apps/app/src/components/Page/TagsInput.tsx → apps/app/src/components/PageTags/TagsInput.tsx

@@ -20,7 +20,7 @@ type Props = {
   onTagsUpdated: (tags: string[]) => void,
 }
 
-const TagsInput: FC<Props> = (props: Props) => {
+export const TagsInput: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const tagsInputRef = useRef<TypeaheadInstance>(null);
 
@@ -79,5 +79,3 @@ const TagsInput: FC<Props> = (props: Props) => {
     </div>
   );
 };
-
-export default TagsInput;

+ 2 - 0
apps/app/src/components/PageTags/index.ts

@@ -0,0 +1,2 @@
+export * from './PageTags';
+export * from './TagsInput';

+ 4 - 4
apps/app/src/components/Sidebar/PageTree/Item.module.scss

@@ -7,8 +7,8 @@
     .list-group-item-action {
       .btn-page-item-control {
         --bs-btn-bg: transparent;
-        --bs-btn-hover-bg: var(--grw-primary-300);
-        --bs-btn-active-bg: var(--grw-primary-400);
+        --bs-btn-hover-bg: var(--grw-primary-200);
+        --bs-btn-active-bg: var(--grw-primary-300);
       }
     }
   }
@@ -18,8 +18,8 @@
     .list-group-item-action {
       .btn-page-item-control {
         --bs-btn-bg: transparent;
-        --bs-btn-hover-bg: var(--grw-primary-700);
-        --bs-btn-active-bg: var(--grw-primary-800);
+        --bs-btn-hover-bg: var(--grw-primary-600);
+        --bs-btn-active-bg: var(--grw-primary-700);
       }
     }
   }

+ 0 - 4
apps/app/src/styles/_editor.scss

@@ -96,10 +96,6 @@
       height: 38px;
       font-size: 18px;
     }
-
-    .grw-taglabels-container {
-      margin-bottom: 0;
-    }
   }
 
   // ellipsis .grw-page-path-hierarchical-link

+ 3 - 3
apps/app/src/styles/molecules/_list-group-item.scss

@@ -2,13 +2,13 @@
 
 @include bs.color-mode(light) {
   .list-group-item-action {
-    --bs-list-group-action-hover-bg: var(--grw-primary-200);
-    --bs-list-group-action-active-bg: var(--grw-primary-400);
+    --bs-list-group-action-hover-bg: var(--grw-primary-100);
+    --bs-list-group-action-active-bg: var(--grw-primary-200);
   }
 }
 @include bs.color-mode(dark) {
   .list-group-item-action {
     --bs-list-group-action-hover-bg: var(--grw-primary-800);
-    --bs-list-group-action-active-bg: var(--grw-primary-800);
+    --bs-list-group-action-active-bg: var(--grw-primary-700);
   }
 }

+ 1 - 0
apps/slackbot-proxy/docker/Dockerfile

@@ -29,6 +29,7 @@ COPY --from=base ${optDir}/out/yarn.lock ./yarn.lock
 
 # setup (with network-timeout = 1 hour)
 RUN yarn config set network-timeout 3600000
+RUN yarn global add node-gyp
 RUN yarn --frozen-lockfile
 
 # make artifacts

+ 2 - 3
package.json

@@ -51,7 +51,6 @@
     "@swc-node/register": "^1.6.2",
     "@swc/core": "^1.3.36",
     "@swc/helpers": "^0.4.14",
-    "@testing-library/cypress": "^9.0.0",
     "@types/css-modules": "^1.0.2",
     "@types/eslint": "^8.37.0",
     "@types/estree": "^1.0.1",
@@ -62,8 +61,8 @@
     "@vitejs/plugin-react": "^4.0.3",
     "@vitest/coverage-c8": "^0.31.1",
     "@vitest/ui": "^0.31.1",
-    "cypress": "^12.17.2",
-    "cypress-wait-until": "^1.7.2",
+    "cypress": "^13.3.0",
+    "cypress-wait-until": "^2.0.1",
     "eslint": "^8.41.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.1",

+ 1 - 0
packages/editor/package.json

@@ -34,6 +34,7 @@
     "codemirror": "^6.0.1",
     "eslint-plugin-react-refresh": "^0.4.1",
     "material-icons": "^1.13.10",
+    "react-dropzone": "^14.2.3",
     "react-hook-form": "^7.45.4",
     "react-toastify": "^9.1.3",
     "reactstrap": "^9.2.0",

+ 7 - 3
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -6,6 +6,7 @@ import { indentUnit } from '@codemirror/language';
 import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 
 import { GlobalCodeMirrorEditorKey } from '../../consts';
+import { useFileDropzone } from '../../services';
 import { useCodeMirrorEditorIsolated } from '../../stores';
 
 import { Toolbar } from './Toolbar';
@@ -18,10 +19,10 @@ const CodeMirrorEditorContainer = forwardRef<HTMLDivElement>((props, ref) => {
   );
 });
 
-
 type Props = {
   editorKey: string | GlobalCodeMirrorEditorKey,
   onChange?: (value: string) => void,
+  onUpload?: (files: File[]) => void,
   indentSize?: number,
 }
 
@@ -29,6 +30,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
   const {
     editorKey,
     onChange,
+    onUpload,
     indentSize,
   } = props;
 
@@ -52,10 +54,12 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
 
   }, [codeMirrorEditor, indentSize]);
 
+  const { getRootProps, open } = useFileDropzone({ onUpload });
+
   return (
-    <div className="flex-expand-vert">
+    <div {...getRootProps()} className="flex-expand-vert">
       <CodeMirrorEditorContainer ref={containerRef} />
-      <Toolbar />
+      <Toolbar onFileOpen={open} />
     </div>
   );
 };

+ 9 - 3
packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropup.tsx

@@ -5,7 +5,13 @@ import {
   DropdownItem,
 } from 'reactstrap';
 
-export const AttachmentsDropup = (): JSX.Element => {
+type Props = {
+  onFileOpen: () => void,
+}
+
+export const AttachmentsDropup = (props: Props): JSX.Element => {
+
+  const { onFileOpen } = props;
   return (
     <>
       <UncontrolledDropdown direction="up" className="lh-1">
@@ -18,11 +24,11 @@ export const AttachmentsDropup = (): JSX.Element => {
             Attachments
           </DropdownItem>
           <DropdownItem divider />
-          <DropdownItem className="d-flex gap-1 align-items-center">
+          <DropdownItem className="d-flex gap-1 align-items-center" onClick={onFileOpen}>
             <span className="material-icons-outlined fs-5">attach_file</span>
             Files
           </DropdownItem>
-          <DropdownItem className="d-flex gap-1 align-items-center">
+          <DropdownItem className="d-flex gap-1 align-items-center" onClick={onFileOpen}>
             <span className="material-icons-outlined fs-5">image</span>
             Images
           </DropdownItem>

+ 8 - 2
packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx

@@ -9,10 +9,16 @@ import { TextFormatTools } from './TextFormatTools';
 
 import styles from './Toolbar.module.scss';
 
-export const Toolbar = memo((): JSX.Element => {
+type Props = {
+  onFileOpen: () => void,
+}
+
+export const Toolbar = memo((props: Props): JSX.Element => {
+
+  const { onFileOpen } = props;
   return (
     <div className={`d-flex gap-2 p-2 codemirror-editor-toolbar ${styles['codemirror-editor-toolbar']}`}>
-      <AttachmentsDropup />
+      <AttachmentsDropup onFileOpen={onFileOpen} />
       <TextFormatTools />
       <EmojiButton />
       <TableButton />

+ 3 - 1
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -17,12 +17,13 @@ const additionalExtensions: Extension[] = [
 type Props = {
   onChange?: (value: string) => void,
   onSave?: () => void,
+  onUpload?: (files: File[]) => void,
   indentSize?: number,
 }
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
-    onSave, onChange, indentSize,
+    onSave, onChange, onUpload, indentSize,
   } = props;
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
@@ -61,6 +62,7 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
     <CodeMirrorEditor
       editorKey={GlobalCodeMirrorEditorKey.MAIN}
       onChange={onChange}
+      onUpload={onUpload}
       indentSize={indentSize}
     />
   );

+ 14 - 0
packages/editor/src/components/playground/Playground.tsx

@@ -37,6 +37,18 @@ export const Playground = (): JSX.Element => {
     toast.success('Saved.', { autoClose: 2000 });
   }, [codeMirrorEditor]);
 
+  // the upload event handler
+  // demo of uploading a file.
+  const uploadHandler = useCallback((files: File[]) => {
+    files.forEach((file) => {
+      // set dummy file name.
+      const insertText = `[${file.name}](/attachment/aaaabbbbccccdddd)\n`;
+      codeMirrorEditor?.insertText(insertText);
+    });
+
+  }, [codeMirrorEditor]);
+
+
   return (
     <>
       <div className="flex-expand-vert justify-content-center align-items-center bg-dark" style={{ minHeight: '83px' }}>
@@ -47,6 +59,8 @@ export const Playground = (): JSX.Element => {
           <CodeMirrorEditorMain
             onSave={saveHandler}
             onChange={setMarkdownToPreview}
+            onUpload={uploadHandler}
+            indentSize={4}
           />
         </div>
         <div className="flex-expand-vert d-none d-lg-flex bg-light text-dark border-start border-dark-subtle p-3">

+ 8 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -14,6 +14,8 @@ import { useAppendExtensions, type AppendExtensions } from './utils/append-exten
 import { useFocus, type Focus } from './utils/focus';
 import { useGetDoc, type GetDoc } from './utils/get-doc';
 import { useInitDoc, type InitDoc } from './utils/init-doc';
+import { useInsertText, type InsertText } from './utils/insert-text';
+import { useReplaceText, type ReplaceText } from './utils/replace-text';
 import { useSetCaretLine, type SetCaretLine } from './utils/set-caret-line';
 
 const markdownHighlighting = HighlightStyle.define([
@@ -31,6 +33,8 @@ type UseCodeMirrorEditorUtils = {
   getDoc: GetDoc,
   focus: Focus,
   setCaretLine: SetCaretLine,
+  insertText: InsertText,
+  replaceText: ReplaceText,
 }
 export type UseCodeMirrorEditor = {
   state: EditorState | undefined;
@@ -70,6 +74,8 @@ export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor
   const getDoc = useGetDoc(view);
   const focus = useFocus(view);
   const setCaretLine = useSetCaretLine(view);
+  const insertText = useInsertText(view);
+  const replaceText = useReplaceText(view);
 
   return {
     state,
@@ -79,5 +85,7 @@ export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor
     getDoc,
     focus,
     setCaretLine,
+    insertText,
+    replaceText,
   };
 };

+ 24 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/insert-text.ts

@@ -0,0 +1,24 @@
+import { useCallback } from 'react';
+
+import { EditorView } from '@codemirror/view';
+
+export type InsertText = (text: string) => void;
+
+export const useInsertText = (view?: EditorView): InsertText => {
+
+  return useCallback((text) => {
+    if (view == null) {
+      return;
+    }
+    const insertPos = view.state.selection.main.head;
+    view.dispatch({
+      changes: {
+        from: insertPos,
+        to: insertPos,
+        insert: text,
+      },
+      selection: { anchor: insertPos },
+    });
+  }, [view]);
+
+};

+ 15 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/replace-text.ts

@@ -0,0 +1,15 @@
+import { useCallback } from 'react';
+
+import { EditorView } from '@codemirror/view';
+
+export type ReplaceText = (text: string) => void;
+
+export const useReplaceText = (view?: EditorView): ReplaceText => {
+
+  return useCallback((text) => {
+    view?.dispatch(
+      view?.state.replaceSelection(text),
+    );
+  }, [view]);
+
+};

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

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

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

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

+ 1 - 0
packages/editor/src/services/index.ts

@@ -1 +1,2 @@
 export * from './codemirror-editor';
+export * from './file-dropzone';

Разница между файлами не показана из-за своего большого размера
+ 41 - 530
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов