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

Merge branch 'master' into fix/140819-143352-editor-grant

Yuki Takei 2 лет назад
Родитель
Сommit
c86399765b
100 измененных файлов с 1033 добавлено и 3758 удалено
  1. 2 2
      .github/workflows/ci-app-prod.yml
  2. 16 16
      .github/workflows/ci-app.yml
  3. 15 15
      .github/workflows/ci-slackbot-proxy.yml
  4. 1 1
      .github/workflows/codeql-analysis.yml
  5. 2 2
      .github/workflows/draft-release.yml
  6. 2 2
      .github/workflows/list-unhealthy-branches.yml
  7. 5 6
      .github/workflows/release-rc-scheduled.yml
  8. 1 1
      .github/workflows/release-rc.yml
  9. 3 3
      .github/workflows/release-slackbot-proxy.yml
  10. 6 6
      .github/workflows/release.yml
  11. 2 2
      .github/workflows/reusable-app-build-image.yml
  12. 19 19
      .github/workflows/reusable-app-prod.yml
  13. 7 6
      .github/workflows/reusable-app-reg-suit.yml
  14. 29 1
      CHANGELOG.md
  15. 0 1
      apps/app/.eslintignore
  16. 0 1191
      apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.jsx
  17. 0 126
      apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.module.scss
  18. 0 65
      apps/app/_obsolete/src/components/PageEditor/CommentMentionHelper.ts
  19. 0 344
      apps/app/_obsolete/src/components/PageEditor/ConflictDiffModal.tsx
  20. 0 174
      apps/app/_obsolete/src/components/Sidebar/AppearanceModeDropdown.tsx
  21. 0 672
      apps/app/_obsolete/src/styles/theme/_apply-colors-dark.scss
  22. 0 534
      apps/app/_obsolete/src/styles/theme/_apply-colors-light.scss
  23. 0 22
      apps/app/_obsolete/src/styles/theme/_reboot-toastr-colors.scss
  24. 0 11
      apps/app/_obsolete/src/styles/theme/apply-colors.scss
  25. 1 1
      apps/app/docker/README.md
  26. 0 1
      apps/app/docker/codebuild/buildspec.yml
  27. 4 3
      apps/app/package.json
  28. 4 2
      apps/app/public/static/locales/en_US/commons.json
  29. 18 3
      apps/app/public/static/locales/en_US/translation.json
  30. 4 2
      apps/app/public/static/locales/ja_JP/commons.json
  31. 19 4
      apps/app/public/static/locales/ja_JP/translation.json
  32. 4 2
      apps/app/public/static/locales/zh_CN/commons.json
  33. 18 3
      apps/app/public/static/locales/zh_CN/translation.json
  34. 7 0
      apps/app/src/client/services/create-page/create-page.ts
  35. 1 0
      apps/app/src/client/services/create-page/index.ts
  36. 3 1
      apps/app/src/client/services/create-page/use-create-page-and-transit.tsx
  37. 5 12
      apps/app/src/client/services/page-operation.ts
  38. 1 0
      apps/app/src/client/services/upload-attachments/index.ts
  39. 39 0
      apps/app/src/client/services/upload-attachments/upload-attachments.ts
  40. 1 1
      apps/app/src/client/util/apiv3-client.ts
  41. 8 12
      apps/app/src/client/util/bookmark-utils.ts
  42. 2 1
      apps/app/src/client/util/toastr.ts
  43. 4 4
      apps/app/src/components/Admin/UserManagement.tsx
  44. 9 9
      apps/app/src/components/Admin/Users/UserTable.tsx
  45. 4 2
      apps/app/src/components/AuthorInfo/AuthorInfo.tsx
  46. 31 29
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  47. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx
  48. 4 4
      apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  49. 2 2
      apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx
  50. 3 3
      apps/app/src/components/Bookmarks/BookmarkItem.tsx
  51. 5 2
      apps/app/src/components/Comments.tsx
  52. 2 1
      apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx
  53. 18 9
      apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss
  54. 67 20
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  55. 2 1
      apps/app/src/components/CompleteUserRegistration.tsx
  56. 2 2
      apps/app/src/components/ContentLinkButtons.tsx
  57. 1 1
      apps/app/src/components/DescendantsPageListModal.tsx
  58. 8 0
      apps/app/src/components/ExpandOrContractButton.module.scss
  59. 9 2
      apps/app/src/components/ExpandOrContractButton.tsx
  60. 3 1
      apps/app/src/components/FontFamily/use-growi-custom-icons.tsx
  61. 1 1
      apps/app/src/components/FontFamily/use-lato.tsx
  62. 2 1
      apps/app/src/components/FontFamily/use-material-symbols-outlined.tsx
  63. 1 1
      apps/app/src/components/FontFamily/use-source-han-code-jp.tsx
  64. 2 2
      apps/app/src/components/Icons/FolderIcon.tsx
  65. 14 0
      apps/app/src/components/Layout/RawLayout.module.scss
  66. 8 3
      apps/app/src/components/Layout/RawLayout.tsx
  67. 44 0
      apps/app/src/components/LoginForm/ExternalAuthButton.tsx
  68. 9 6
      apps/app/src/components/LoginForm/LoginForm.module.scss
  69. 15 48
      apps/app/src/components/LoginForm/LoginForm.tsx
  70. 1 0
      apps/app/src/components/LoginForm/index.ts
  71. 9 0
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss
  72. 68 46
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  73. 8 2
      apps/app/src/components/Navbar/GrowiNavbarBottom.tsx
  74. 1 1
      apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx
  75. 15 6
      apps/app/src/components/PageAlert/FixPageGrantAlert.tsx
  76. 29 2
      apps/app/src/components/PageComment.module.scss
  77. 17 14
      apps/app/src/components/PageComment.tsx
  78. 32 23
      apps/app/src/components/PageComment/Comment.module.scss
  79. 16 20
      apps/app/src/components/PageComment/Comment.tsx
  80. 2 2
      apps/app/src/components/PageComment/CommentControl.tsx
  81. 21 20
      apps/app/src/components/PageComment/CommentEditor.module.scss
  82. 50 89
      apps/app/src/components/PageComment/CommentEditor.tsx
  83. 0 7
      apps/app/src/components/PageComment/CommentPreview.module.scss
  84. 3 1
      apps/app/src/components/PageComment/CommentPreview.tsx
  85. 9 5
      apps/app/src/components/PageComment/DeleteCommentModal.tsx
  86. 0 6
      apps/app/src/components/PageComment/ReplyComments.module.scss
  87. 1 1
      apps/app/src/components/PageComment/ReplyComments.tsx
  88. 46 0
      apps/app/src/components/PageComment/SwitchingButtonGroup.module.scss
  89. 73 0
      apps/app/src/components/PageComment/SwitchingButtonGroup.tsx
  90. 19 8
      apps/app/src/components/PageComment/_comment-inheritance.scss
  91. 1 1
      apps/app/src/components/PageControls/BookmarkButtons.tsx
  92. 1 1
      apps/app/src/components/PageControls/LikeButtons.tsx
  93. 68 55
      apps/app/src/components/PageControls/PageControls.tsx
  94. 1 1
      apps/app/src/components/PageControls/SearchButton.tsx
  95. 1 2
      apps/app/src/components/PageControls/SeenUserInfo.tsx
  96. 3 2
      apps/app/src/components/PageControls/SubscribeButton.tsx
  97. 1 1
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  98. 3 3
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  99. 11 11
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  100. 3 3
      apps/app/src/components/PageEditor/MarkdownTableDataImportForm.tsx

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

@@ -62,7 +62,7 @@ jobs:
     with:
     with:
       node-version: 20.x
       node-version: 20.x
       skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
-      cypress-report-artifact-name: Cypress report
+      cypress-report-artifact-name-prefix: cypress-report-
       cypress-config-video: ${{ inputs.cypress-config-video || false }}
       cypress-config-video: ${{ inputs.cypress-config-video || false }}
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -78,7 +78,7 @@ jobs:
     with:
     with:
       node-version: 20.x
       node-version: 20.x
       skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
-      cypress-report-artifact-name: Cypress report
+      cypress-report-artifact-name-pattern: cypress-report-*
     secrets:
     secrets:
       REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
       REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
       AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
       AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

+ 16 - 16
.github/workflows/ci-app.yml

@@ -30,16 +30,16 @@ jobs:
         node-version: [20.x]
         node-version: [20.x]
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
         with:
           node-version: ${{ matrix.node-version }}
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
           cache: 'yarn'
           cache-dependency-path: '**/yarn.lock'
           cache-dependency-path: '**/yarn.lock'
 
 
       - name: Cache/Restore node_modules
       - name: Cache/Restore node_modules
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
@@ -49,7 +49,7 @@ jobs:
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
       - name: Restore dist
       - name: Restore dist
-        uses: actions/cache/restore@v3
+        uses: actions/cache/restore@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -79,7 +79,7 @@ jobs:
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
       - name: Cache dist
       - name: Cache dist
-        uses: actions/cache/save@v3
+        uses: actions/cache/save@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -101,9 +101,9 @@ jobs:
           - 27017/tcp
           - 27017/tcp
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
         with:
           node-version: ${{ matrix.node-version }}
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
           cache: 'yarn'
@@ -111,7 +111,7 @@ jobs:
 
 
       - name: Cache/Restore node_modules
       - name: Cache/Restore node_modules
         id: cache-dependencies
         id: cache-dependencies
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
@@ -121,7 +121,7 @@ jobs:
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
       - name: Restore dist
       - name: Restore dist
-        uses: actions/cache/restore@v3
+        uses: actions/cache/restore@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -143,7 +143,7 @@ jobs:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
 
 
       - name: Upload coverage report as artifact
       - name: Upload coverage report as artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
         with:
           name: Coverage Report
           name: Coverage Report
           path: |
           path: |
@@ -161,7 +161,7 @@ jobs:
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
       - name: Cache dist
       - name: Cache dist
-        uses: actions/cache/save@v3
+        uses: actions/cache/save@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -183,9 +183,9 @@ jobs:
           - 27017/tcp
           - 27017/tcp
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
         with:
           node-version: ${{ matrix.node-version }}
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
           cache: 'yarn'
@@ -193,7 +193,7 @@ jobs:
 
 
       - name: Cache/Restore node_modules
       - name: Cache/Restore node_modules
         id: cache-dependencies
         id: cache-dependencies
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
@@ -203,7 +203,7 @@ jobs:
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
             node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
       - name: Restore dist
       - name: Restore dist
-        uses: actions/cache/restore@v3
+        uses: actions/cache/restore@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
@@ -238,7 +238,7 @@ jobs:
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
       - name: Cache dist
       - name: Cache dist
-        uses: actions/cache/save@v3
+        uses: actions/cache/save@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo

+ 15 - 15
.github/workflows/ci-slackbot-proxy.yml

@@ -32,16 +32,16 @@ jobs:
         node-version: [20.x]
         node-version: [20.x]
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -51,7 +51,7 @@ jobs:
           node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
           node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
     - name: Restore dist
     - name: Restore dist
-      uses: actions/cache/restore@v3
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -81,7 +81,7 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
     - name: Cache dist
     - name: Cache dist
-      uses: actions/cache/save@v3
+      uses: actions/cache/save@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -107,16 +107,16 @@ jobs:
           MYSQL_DATABASE: growi-slackbot-proxy
           MYSQL_DATABASE: growi-slackbot-proxy
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -126,7 +126,7 @@ jobs:
           node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
           node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
     - name: Restore dist
     - name: Restore dist
-      uses: actions/cache/restore@v3
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -166,7 +166,7 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
     - name: Cache dist
     - name: Cache dist
-      uses: actions/cache/save@v3
+      uses: actions/cache/save@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -192,9 +192,9 @@ jobs:
           MYSQL_DATABASE: growi-slackbot-proxy
           MYSQL_DATABASE: growi-slackbot-proxy
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -212,7 +212,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -226,7 +226,7 @@ jobs:
         yarn --frozen-lockfile
         yarn --frozen-lockfile
 
 
     - name: Restore dist
     - name: Restore dist
-      uses: actions/cache/restore@v3
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo
@@ -269,7 +269,7 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
     - name: Cache dist
     - name: Cache dist
-      uses: actions/cache/save@v3
+      uses: actions/cache/save@v4
       with:
       with:
         path: |
         path: |
           **/.turbo
           **/.turbo

+ 1 - 1
.github/workflows/codeql-analysis.yml

@@ -43,7 +43,7 @@ jobs:
 
 
     steps:
     steps:
     - name: Checkout repository
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
 
     # Initializes the CodeQL tools for scanning.
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
     - name: Initialize CodeQL

+ 2 - 2
.github/workflows/draft-release.yml

@@ -23,7 +23,7 @@ jobs:
       RELEASE_DRAFT_BODY: ${{ steps.release-drafter.outputs.body }}
       RELEASE_DRAFT_BODY: ${{ steps.release-drafter.outputs.body }}
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
       - name: Retrieve information from package.json
       - name: Retrieve information from package.json
         uses: myrotvorets/info-from-package-json-action@1.2.0
         uses: myrotvorets/info-from-package-json-action@1.2.0
@@ -47,7 +47,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
         with:
           fetch-depth: 0
           fetch-depth: 0
 
 

+ 2 - 2
.github/workflows/list-unhealthy-branches.yml

@@ -10,11 +10,11 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         fetch-depth: 0
         fetch-depth: 0
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: '18'
         node-version: '18'
 
 

+ 5 - 6
.github/workflows-archived/release-rc-v7.yml → .github/workflows/release-rc-scheduled.yml

@@ -1,10 +1,9 @@
-name: Release Docker Images for RC (for dev/7.0.x)
+name: Release Docker Images for RC (for master)
 
 
 on:
 on:
-  push:
-    branches:
-      - dev/7.0.x
-
+  schedule:
+    # Weekdays at 24:00hrs (JST) Executed
+    - cron: '0 15 * * 1-5'
 
 
 concurrency:
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -21,7 +20,7 @@ jobs:
       TAGS_GHCR: ${{ steps.meta-ghcr.outputs.tags }}
       TAGS_GHCR: ${{ steps.meta-ghcr.outputs.tags }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0
       uses: myrotvorets/info-from-package-json-action@1.2.0

+ 1 - 1
.github/workflows/release-rc.yml

@@ -20,7 +20,7 @@ jobs:
       TAGS: ${{ steps.meta.outputs.tags }}
       TAGS: ${{ steps.meta.outputs.tags }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0
       uses: myrotvorets/info-from-package-json-action@1.2.0

+ 3 - 3
.github/workflows/release-slackbot-proxy.yml

@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
@@ -89,11 +89,11 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: '18'
         node-version: '18'
         cache: 'yarn'
         cache: 'yarn'

+ 6 - 6
.github/workflows/release.yml

@@ -18,11 +18,11 @@ jobs:
       RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
       RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: '20'
         node-version: '20'
         cache: 'yarn'
         cache: 'yarn'
@@ -84,7 +84,7 @@ jobs:
       TAGS: ${{ steps.meta.outputs.tags }}
       TAGS: ${{ steps.meta.outputs.tags }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0
       uses: myrotvorets/info-from-package-json-action@1.2.0
@@ -133,7 +133,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
 
@@ -158,11 +158,11 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: '20'
         node-version: '20'
         cache: 'yarn'
         cache: 'yarn'

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

@@ -33,7 +33,7 @@ jobs:
         platform: [amd64, arm64]
         platform: [amd64, arm64]
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Configure AWS Credentials
     - name: Configure AWS Credentials
       uses: aws-actions/configure-aws-credentials@v4
       uses: aws-actions/configure-aws-credentials@v4
@@ -51,5 +51,5 @@ jobs:
         CODEBUILD__imageOverride: ${{ (matrix.platform == 'amd64' && 'aws/codebuild/amazonlinux2-x86_64-standard:4.0') || 'aws/codebuild/amazonlinux2-aarch64-standard:2.0' }}
         CODEBUILD__imageOverride: ${{ (matrix.platform == 'amd64' && 'aws/codebuild/amazonlinux2-x86_64-standard:4.0') || 'aws/codebuild/amazonlinux2-aarch64-standard:2.0' }}
         CODEBUILD__environmentTypeOverride: ${{ (matrix.platform == 'amd64' && 'LINUX_CONTAINER') || 'ARM_CONTAINER' }}
         CODEBUILD__environmentTypeOverride: ${{ (matrix.platform == 'amd64' && 'LINUX_CONTAINER') || 'ARM_CONTAINER' }}
         CODEBUILD__environmentVariablesOverride: '[
         CODEBUILD__environmentVariablesOverride: '[
-          { "name": "IMAGE_TAG", "type": "PLAINTEXT", "value": "docker.io/${{ inputs.image-name }}:${{ inputs.tag-temporary }}-${{ matrix.platform }}" },
+          { "name": "IMAGE_TAG", "type": "PLAINTEXT", "value": "docker.io/${{ inputs.image-name }}:${{ inputs.tag-temporary }}-${{ matrix.platform }}" }
         ]'
         ]'

+ 19 - 19
.github/workflows/reusable-app-prod.yml

@@ -8,7 +8,7 @@ on:
         type: string
         type: string
       skip-cypress:
       skip-cypress:
         type: boolean
         type: boolean
-      cypress-report-artifact-name:
+      cypress-report-artifact-name-prefix:
         type: string
         type: string
       cypress-config-video:
       cypress-config-video:
         type: boolean
         type: boolean
@@ -26,12 +26,12 @@ jobs:
       PROD_FILES: ${{ steps.archive-prod-files.outputs.file }}
       PROD_FILES: ${{ steps.archive-prod-files.outputs.file }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         # retrieve local font files
         # retrieve local font files
         lfs: true
         lfs: true
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -49,7 +49,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -63,7 +63,7 @@ jobs:
         yarn --frozen-lockfile
         yarn --frozen-lockfile
 
 
     - name: Restore dist
     - name: Restore dist
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           node_modules/.cache/turbo
           node_modules/.cache/turbo
@@ -100,15 +100,15 @@ jobs:
         echo "file=production.tar.gz" >> $GITHUB_OUTPUT
         echo "file=production.tar.gz" >> $GITHUB_OUTPUT
 
 
     - name: Upload production files as artifact
     - name: Upload production files as artifact
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
       with:
         name: Production Files (node${{ inputs.node-version }})
         name: Production Files (node${{ inputs.node-version }})
         path: ${{ steps.archive-prod-files.outputs.file }}
         path: ${{ steps.archive-prod-files.outputs.file }}
 
 
     - name: Upload report as artifact
     - name: Upload report as artifact
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
       with:
-        name: Bundle Analyzing Report
+        name: Bundle Analyzing Report (node${{ inputs.node-version }})
         path: |
         path: |
           apps/app/.next/analyze/client.html
           apps/app/.next/analyze/client.html
           apps/app/.next/analyze/server.html
           apps/app/.next/analyze/server.html
@@ -141,9 +141,9 @@ jobs:
           discovery.type: single-node
           discovery.type: single-node
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -161,7 +161,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -174,7 +174,7 @@ jobs:
         yarn --production
         yarn --production
 
 
     - name: Download production files artifact
     - name: Download production files artifact
-      uses: actions/download-artifact@v3
+      uses: actions/download-artifact@v4
       with:
       with:
         name: Production Files (node${{ inputs.node-version }})
         name: Production Files (node${{ inputs.node-version }})
 
 
@@ -229,12 +229,12 @@ jobs:
           discovery.type: single-node
           discovery.type: single-node
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
 
     - name: Install fonts
     - name: Install fonts
       run: sudo apt install fonts-noto
       run: sudo apt install fonts-noto
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -252,7 +252,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -261,7 +261,7 @@ jobs:
           node_modules-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
           node_modules-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
 
 
     - name: Cache/Restore Cypress files
     - name: Cache/Restore Cypress files
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           ~/.cache/Cypress
           ~/.cache/Cypress
@@ -276,7 +276,7 @@ jobs:
         yarn cypress install
         yarn cypress install
 
 
     - name: Download production files artifact
     - name: Download production files artifact
-      uses: actions/download-artifact@v3
+      uses: actions/download-artifact@v4
       with:
       with:
         name: Production Files (node${{ inputs.node-version }})
         name: Production Files (node${{ inputs.node-version }})
 
 
@@ -323,9 +323,9 @@ jobs:
 
 
     - name: Upload results
     - name: Upload results
       if: always()
       if: always()
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
       with:
-        name: ${{ inputs.cypress-report-artifact-name }}
+        name: ${{ inputs.cypress-report-artifact-name-prefix }}${{ matrix.spec-group }}
         path: |
         path: |
           apps/app/test/cypress/screenshots
           apps/app/test/cypress/screenshots
           apps/app/test/cypress/videos
           apps/app/test/cypress/videos

+ 7 - 6
.github/workflows/reusable-app-reg-suit.yml

@@ -11,7 +11,7 @@ on:
         default: ${{ github.head_ref }}
         default: ${{ github.head_ref }}
       skip-reg-suit:
       skip-reg-suit:
         type: boolean
         type: boolean
-      cypress-report-artifact-name:
+      cypress-report-artifact-name-pattern:
         required: true
         required: true
         type: string
         type: string
     secrets:
     secrets:
@@ -49,12 +49,12 @@ jobs:
       EXPECTED_IMAGES_EXIST: ${{ steps.check-expected-images.outputs.EXPECTED_IMAGES_EXIST }}
       EXPECTED_IMAGES_EXIST: ${{ steps.check-expected-images.outputs.EXPECTED_IMAGES_EXIST }}
 
 
     steps:
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
       with:
         ref: ${{ inputs.checkout-ref }}
         ref: ${{ inputs.checkout-ref }}
         fetch-depth: 0
         fetch-depth: 0
 
 
-    - uses: actions/setup-node@v3
+    - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
@@ -72,7 +72,7 @@ jobs:
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
       id: cache-dependencies
       id: cache-dependencies
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
@@ -86,10 +86,11 @@ jobs:
         yarn --frozen-lockfile
         yarn --frozen-lockfile
 
 
     - name: Download screenshots taken by cypress
     - name: Download screenshots taken by cypress
-      uses: actions/download-artifact@v3
+      uses: actions/download-artifact@v4
       with:
       with:
-        name: ${{ inputs.cypress-report-artifact-name }}
         path: apps/app/test/cypress
         path: apps/app/test/cypress
+        pattern: ${{ inputs.cypress-report-artifact-name-pattern }}
+        merge-multiple: true
 
 
     - name: Run reg-suit
     - name: Run reg-suit
       working-directory: ./apps/app
       working-directory: ./apps/app

+ 29 - 1
CHANGELOG.md

@@ -1,9 +1,37 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.0...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.1...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.0.1](https://github.com/weseek/growi/compare/v7.0.0...v7.0.1) - 2024-04-02
+
+### 🚀 Improvement
+
+* imprv: PagePathNav and PagePathHeader styles (#8643) @yuki-takei
+* imprv: Prevent tooltip flickering (#8642) @yuki-takei
+* imprv: PersonalDropdown style (#8641) @yuki-takei
+* imprv: Support color scheme in Page History (diff2html) (#8637) @yuki-takei
+* imprv: Disable RequestedAuthnContext in SAML authentication (v7.0.x) (#8635) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Admin users badge color in /admin/users (#8655) @satof3
+* fix: Custom logo style (#8656) @satof3
+* fix: Error when creating TTL index (#8653) @miya
+* fix: Login form style is broken (#8651) @yuki-takei
+* fix: Login buttons for external auth provider does not work (#8648) @yuki-takei
+* fix: Set `d-none` when the sidebar is closed in order to prevent scrollbars from appearing (#8640) @yuki-takei
+* fix: Style for Handsontable in dark mode (#8639) @yuki-takei
+* fix: Supress PageAccessoriesModal rerendering for keeping radio button selections of PageHistory (#8638) @yuki-takei
+* fix: Uncorrect update bookmark button clicked on page control (#8608) @jam411
+* fix: Counting comments when removing the thread (#8624) @yukendev
+
+### 🧰 Maintenance
+
+* support: Add light and dark badge color (#8652) @satof3
+* support: Omit configurations for publishing to GitHub Container Registry(ghcr.io) (#8628) @yuki-takei
+
 ## [v7.0.0](https://github.com/weseek/growi/compare/v6.3.2...v7.0.0) - 2024-03-27
 ## [v7.0.0](https://github.com/weseek/growi/compare/v6.3.2...v7.0.0) - 2024-03-27
 
 
 ### BREAKING CHANGES
 ### BREAKING CHANGES

+ 0 - 1
apps/app/.eslintignore

@@ -1,4 +1,3 @@
-/_obsolete/**
 /dist/**
 /dist/**
 /transpiled/**
 /transpiled/**
 /public/**
 /public/**

+ 0 - 1191
apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -1,1191 +0,0 @@
-import React, { useCallback, memo } from 'react';
-
-import { commands } from 'codemirror';
-import * as loadCssSync from 'load-css-file';
-import PropTypes from 'prop-types';
-import { Button } from 'reactstrap';
-import * as loadScript from 'simple-load-script';
-import { throttle, debounce } from 'throttle-debounce';
-import urljoin from 'url-join';
-
-import InterceptorManager from '~/services/interceptor-manager';
-import {
-  useHandsontableModal, useDrawioModal, useTemplateModal, useLinkEditModal,
-} from '~/stores/modal';
-import loggerFactory from '~/utils/logger';
-
-import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
-
-import AbstractEditor from './AbstractEditor';
-import CommentMentionHelper from './CommentMentionHelper';
-import EditorIcon from './EditorIcon';
-import EmojiPicker from './EmojiPicker';
-import EmojiPickerHelper from './EmojiPickerHelper';
-import GridEditModal from './GridEditModal';
-// TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-// import geu from './GridEditorUtil';
-import mdu from './MarkdownDrawioUtil';
-import markdownLinkUtil from './MarkdownLinkUtil';
-import markdownListUtil from './MarkdownListUtil';
-import MarkdownTableInterceptor from './MarkdownTableInterceptor';
-import mtu from './MarkdownTableUtil';
-import pasteHelper from './PasteHelper';
-import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
-import SimpleCheatsheet from './SimpleCheatsheet';
-
-import styles from './CodeMirrorEditor.module.scss';
-
-require('codemirror/addon/hint/show-hint.css'); // Import from CodeMirrorEditor.module.scss not working
-require('codemirror/addon/display/placeholder');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/matchtags');
-require('codemirror/addon/edit/closetag');
-require('codemirror/addon/edit/continuelist');
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/search/searchcursor');
-require('codemirror/addon/search/match-highlighter');
-require('codemirror/addon/selection/active-line');
-require('codemirror/addon/scroll/annotatescrollbar');
-require('codemirror/addon/scroll/scrollpastend');
-require('codemirror/addon/fold/foldcode');
-require('codemirror/addon/fold/foldgutter');
-require('codemirror/addon/fold/markdown-fold');
-require('codemirror/addon/fold/brace-fold');
-require('codemirror/addon/display/placeholder');
-require('~/client/util/codemirror/autorefresh.ext');
-require('~/client/util/codemirror/drawio-fold.ext');
-require('~/client/util/codemirror/gfm-growi.mode');
-// import modes to highlight
-require('codemirror/mode/clike/clike');
-require('codemirror/mode/css/css');
-require('codemirror/mode/django/django');
-require('codemirror/mode/erlang/erlang');
-require('codemirror/mode/gfm/gfm');
-require('codemirror/mode/go/go');
-require('codemirror/mode/javascript/javascript');
-require('codemirror/mode/jsx/jsx');
-require('codemirror/mode/mathematica/mathematica');
-require('codemirror/mode/nginx/nginx');
-require('codemirror/mode/perl/perl');
-require('codemirror/mode/php/php');
-require('codemirror/mode/python/python');
-require('codemirror/mode/r/r');
-require('codemirror/mode/ruby/ruby');
-require('codemirror/mode/rust/rust');
-require('codemirror/mode/sass/sass');
-require('codemirror/mode/shell/shell');
-require('codemirror/mode/sql/sql');
-require('codemirror/mode/stex/stex');
-require('codemirror/mode/stylus/stylus');
-require('codemirror/mode/swift/swift');
-require('codemirror/mode/toml/toml');
-require('codemirror/mode/vb/vb');
-require('codemirror/mode/vue/vue');
-require('codemirror/mode/xml/xml');
-require('codemirror/mode/yaml/yaml');
-
-
-const MARKDOWN_TABLE_ACTIVATED_CLASS = 'markdown-table-activated';
-const MARKDOWN_LINK_ACTIVATED_CLASS = 'markdown-link-activated';
-
-class CodeMirrorEditor extends AbstractEditor {
-
-  constructor(props) {
-    super(props);
-    this.logger = loggerFactory('growi:PageEditor:CodeMirrorEditor');
-
-    this.state = {
-      isGfmMode: this.props.isGfmMode,
-      isLoadingKeymap: false,
-      isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value?.length === 0,
-      isCheatsheetModalShown: false,
-      additionalClassSet: new Set(),
-      isEmojiPickerShown: false,
-      emojiSearchText: '',
-      startPosWithEmojiPickerModeTurnedOn: null,
-      isEmojiPickerMode: false,
-      isTemplateModalOpened: false,
-    };
-
-    this.cm = React.createRef();
-    this.gridEditModal = React.createRef();
-    this.linkEditModal = React.createRef();
-    this.drawioModal = React.createRef();
-
-    this.init();
-
-    this.getCodeMirror = this.getCodeMirror.bind(this);
-
-    this.getBol = this.getBol.bind(this);
-    this.getEol = this.getEol.bind(this);
-
-    this.loadTheme = this.loadTheme.bind(this);
-    this.loadKeymapMode = this.loadKeymapMode.bind(this);
-    this.setKeymapMode = this.setKeymapMode.bind(this);
-    this.handleEnterKey = this.handleEnterKey.bind(this);
-    this.handleCtrlEnterKey = this.handleCtrlEnterKey.bind(this);
-
-    this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this);
-    this.scrollCursorIntoViewHandlerThrottled = throttle(500, this.scrollCursorIntoViewHandler);
-    this.pasteHandler = this.pasteHandler.bind(this);
-    this.cursorHandler = this.cursorHandler.bind(this);
-    this.cursorHandlerDebounced = debounce(200, throttle(500, this.cursorHandler));
-    this.changeHandler = this.changeHandler.bind(this);
-    this.turnOnEmojiPickerMode = this.turnOnEmojiPickerMode.bind(this);
-    this.turnOffEmojiPickerMode = this.turnOffEmojiPickerMode.bind(this);
-    this.windowClickHandler = this.windowClickHandler.bind(this);
-    this.keyDownHandler = this.keyDownHandler.bind(this);
-    this.keyDownHandlerForEmojiPicker = this.keyDownHandlerForEmojiPicker.bind(this);
-    this.keyDownHandlerForEmojiPickerThrottled = throttle(400, this.keyDownHandlerForEmojiPicker);
-    this.showEmojiPicker = this.showEmojiPicker.bind(this);
-    this.keyPressHandlerForEmojiPicker = this.keyPressHandlerForEmojiPicker.bind(this);
-    this.keyPressHandlerForEmojiPickerThrottled = debounce(50, throttle(200, this.keyPressHandlerForEmojiPicker));
-    this.keyPressHandler = this.keyPressHandler.bind(this);
-
-    this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this);
-
-    this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
-    this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.bind(this);
-
-    this.makeHeaderHandler = this.makeHeaderHandler.bind(this);
-    // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-    // this.showGridEditorHandler = this.showGridEditorHandler.bind(this);
-
-    this.foldDrawioSection = this.foldDrawioSection.bind(this);
-    this.clickDrawioIconHandler = this.clickDrawioIconHandler.bind(this);
-    this.clickTableIconHandler = this.clickTableIconHandler.bind(this);
-
-    this.showTemplateModal = this.showTemplateModal.bind(this);
-    this.showLinkEditModal = this.showLinkEditModal.bind(this);
-
-  }
-
-  init() {
-    this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
-    this.cmNoCdnScriptRoot = '/static/js/cdn';
-    this.cmNoCdnStyleRoot = '/static/styles/cdn';
-    this.interceptorManager = new InterceptorManager();
-    this.interceptorManager.addInterceptors([
-      new PreventMarkdownListInterceptor(),
-      new MarkdownTableInterceptor(),
-    ]);
-
-    this.loadedThemeSet = new Set(['eclipse', 'elegant']); // themes imported in _vendor.scss
-    this.loadedKeymapSet = new Set();
-  }
-
-  componentDidMount() {
-    // ensure to be able to resolve 'this' to use 'codemirror.commands.save'
-    this.getCodeMirror().codeMirrorEditor = this;
-
-    // mark clean
-    this.getCodeMirror().getDoc().markClean();
-
-    // fold drawio section
-    this.foldDrawioSection();
-
-    // initialize commentMentionHelper if comment editor is opened
-    if (this.props.isComment) {
-      this.commentMentionHelper = new CommentMentionHelper(this.getCodeMirror());
-    }
-    this.emojiPickerHelper = new EmojiPickerHelper(this.getCodeMirror());
-
-    // HACKME: Find a better way to handle onClick for Editor
-    document.addEventListener('click', this.windowClickHandler);
-  }
-
-  componentWillUnmount() {
-    // HACKME: Find a better way to handle onClick for Editor
-    document.removeEventListener('click', this.windowClickHandler);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    this.initializeEditorSettings(nextProps.editorSettings);
-
-    // fold drawio section
-    this.foldDrawioSection();
-  }
-
-  initializeEditorSettings(editorSettings) {
-    if (editorSettings == null) {
-      return;
-    }
-
-    // load theme
-    const theme = editorSettings.theme;
-    if (theme != null) {
-      this.loadTheme(theme);
-    }
-
-    // set keymap
-    const keymapMode = editorSettings.keymapMode;
-    if (keymapMode != null) {
-      this.setKeymapMode(keymapMode);
-    }
-  }
-
-  getCodeMirror() {
-    return this.cm.current?.editor;
-  }
-
-  /**
-   * @inheritDoc
-   */
-  forceToFocus() {
-    // use setInterval with reluctance -- 2018.01.11 Yuki Takei
-    const intervalId = setInterval(() => {
-      const editor = this.getCodeMirror();
-      editor.focus();
-      if (editor.hasFocus()) {
-        clearInterval(intervalId);
-        // refresh
-        editor.refresh();
-      }
-    }, 100);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setValue(newValue) {
-    this.getCodeMirror().getDoc().setValue(newValue);
-
-    // mark clean
-    this.getCodeMirror().getDoc().markClean();
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setGfmMode(bool) {
-    // update state
-    this.setState({
-      isGfmMode: bool,
-    });
-
-    this.updateCheatsheetStates(bool, null);
-
-    // update CodeMirror option
-    const mode = bool ? 'gfm' : undefined;
-    this.getCodeMirror().setOption('mode', mode);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setCaretLine(line) {
-    if (Number.isNaN(line)) {
-      return;
-    }
-
-    const editor = this.getCodeMirror();
-    const linePosition = Math.max(0, line - 1);
-
-    editor.setCursor({ line: linePosition }); // leave 'ch' field as null/undefined to indicate the end of line
-
-    setTimeout(() => {
-      this.setScrollTopByLine(linePosition);
-    }, 100);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setScrollTopByLine(line) {
-    if (Number.isNaN(line)) {
-      return;
-    }
-
-    const editor = this.getCodeMirror();
-    // get top position of the line
-    const top = editor.charCoords({ line: line - 1, ch: 0 }, 'local').top;
-    editor.scrollTo(null, top);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrFromBol() {
-    const editor = this.getCodeMirror();
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(this.getBol(), curPos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrToEol() {
-    const editor = this.getCodeMirror();
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(curPos, this.getEol());
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrFromBolToSelectedUpperPos() {
-    const editor = this.getCodeMirror();
-    const pos = this.selectUpperPos(editor.getCursor('from'), editor.getCursor('to'));
-    return editor.getDoc().getRange(this.getBol(), pos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  replaceBolToCurrentPos(text) {
-    const editor = this.getCodeMirror();
-    const pos = this.selectLowerPos(editor.getCursor('from'), editor.getCursor('to'));
-    editor.getDoc().replaceRange(text, this.getBol(), pos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  replaceLine(text) {
-    const editor = this.getCodeMirror();
-    editor.getDoc().replaceRange(text, this.getBol(), this.getEol());
-  }
-
-  /**
-   * @inheritDoc
-   */
-  insertText(text) {
-    const editor = this.getCodeMirror();
-    editor.getDoc().replaceSelection(text);
-  }
-
-  /**
-   * return the postion of the BOL(beginning of line)
-   */
-  getBol() {
-    const editor = this.getCodeMirror();
-    const curPos = editor.getCursor();
-    return { line: curPos.line, ch: 0 };
-  }
-
-  /**
-   * return the postion of the EOL(end of line)
-   */
-  getEol() {
-    const editor = this.getCodeMirror();
-    const curPos = editor.getCursor();
-    const lineLength = editor.getDoc().getLine(curPos.line).length;
-    return { line: curPos.line, ch: lineLength };
-  }
-
-  /**
-   * select the upper position of pos1 and pos2
-   * @param {{line: number, ch: number}} pos1
-   * @param {{line: number, ch: number}} pos2
-   */
-  selectUpperPos(pos1, pos2) {
-    // if both is in same line
-    if (pos1.line === pos2.line) {
-      return (pos1.ch < pos2.ch) ? pos1 : pos2;
-    }
-    return (pos1.line < pos2.line) ? pos1 : pos2;
-  }
-
-  /**
-   * select the lower position of pos1 and pos2
-   * @param {{line: number, ch: number}} pos1
-   * @param {{line: number, ch: number}} pos2
-   */
-  selectLowerPos(pos1, pos2) {
-    // if both is in same line
-    if (pos1.line === pos2.line) {
-      return (pos1.ch < pos2.ch) ? pos2 : pos1;
-    }
-    return (pos1.line < pos2.line) ? pos2 : pos1;
-  }
-
-  loadCss(source) {
-    return new Promise((resolve) => {
-      loadCssSync(source);
-      resolve();
-    });
-  }
-
-  /**
-   * load Theme
-   * @see https://codemirror.net/doc/manual.html#config
-   *
-   * @param {string} theme
-   */
-  loadTheme(theme) {
-    if (!this.loadedThemeSet.has(theme)) {
-      const url = this.props.noCdn
-        ? urljoin(this.cmNoCdnStyleRoot, `codemirror-theme-${theme}.css`)
-        : urljoin(this.cmCdnRoot, `theme/${theme}.min.css`);
-
-      this.loadCss(url);
-
-      // update Set
-      this.loadedThemeSet.add(theme);
-    }
-  }
-
-  /**
-   * load assets for Key Maps
-   * @param {*} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
-   */
-  loadKeymapMode(keymapMode) {
-    const loadCss = this.loadCss;
-    const scriptList = [];
-    const cssList = [];
-
-    // add dependencies
-    if (this.loadedKeymapSet.size === 0) {
-      const dialogScriptUrl = this.props.noCdn
-        ? urljoin(this.cmNoCdnScriptRoot, 'codemirror-dialog.js')
-        : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js');
-      const dialogStyleUrl = this.props.noCdn
-        ? urljoin(this.cmNoCdnStyleRoot, 'codemirror-dialog.css')
-        : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css');
-
-      scriptList.push(loadScript(dialogScriptUrl));
-      cssList.push(loadCss(dialogStyleUrl));
-    }
-    // load keymap
-    if (!this.loadedKeymapSet.has(keymapMode)) {
-      const keymapScriptUrl = this.props.noCdn
-        ? urljoin(this.cmNoCdnScriptRoot, `codemirror-keymap-${keymapMode}.js`)
-        : urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`);
-      scriptList.push(loadScript(keymapScriptUrl));
-      // update Set
-      this.loadedKeymapSet.add(keymapMode);
-    }
-
-    // set loading state
-    this.setState({ isLoadingKeymap: true });
-
-    return Promise.all(scriptList.concat(cssList))
-      .then(() => {
-        this.setState({ isLoadingKeymap: false });
-      });
-  }
-
-  /**
-   * set Key Maps
-   * @see https://codemirror.net/doc/manual.html#keymaps
-   *
-   * @param {string} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
-   */
-  setKeymapMode(keymapMode) {
-    if (!keymapMode.match(/^(vim|emacs|sublime)$/)) {
-      // reset
-      this.getCodeMirror().setOption('keyMap', 'default');
-      return;
-    }
-
-    this.loadKeymapMode(keymapMode)
-      .then(() => {
-        let errorCount = 0;
-        const timer = setInterval(() => {
-          if (errorCount > 10) { // cancel over 3000ms
-            this.logger.error(`Timeout to load keyMap '${keymapMode}'`);
-            clearInterval(timer);
-          }
-
-          try {
-            this.getCodeMirror().setOption('keyMap', keymapMode);
-            clearInterval(timer);
-          }
-          catch (e) {
-            this.logger.info(`keyMap '${keymapMode}' has not been initialized. retry..`);
-
-            // continue if error occured
-            errorCount++;
-          }
-        }, 300);
-      });
-  }
-
-  /**
-   * handle ENTER key
-   */
-  handleEnterKey() {
-    if (!this.state.isGfmMode) {
-      commands.newlineAndIndent(this.getCodeMirror());
-      return;
-    }
-
-    const context = {
-      handlers: [], // list of handlers which process enter key
-      editor: this,
-      autoFormatMarkdownTable: this.props.editorSettings.autoFormatMarkdownTable,
-    };
-
-    const interceptorManager = this.interceptorManager;
-    interceptorManager.process('preHandleEnter', context)
-      .then(() => {
-        if (context.handlers.length === 0) {
-          markdownListUtil.newlineAndIndentContinueMarkdownList(this);
-        }
-      });
-  }
-
-  /**
-   * handle Ctrl+ENTER key
-   */
-  handleCtrlEnterKey() {
-    if (this.props.onCtrlEnter != null) {
-      this.props.onCtrlEnter();
-    }
-  }
-
-  scrollCursorIntoViewHandler(editor, event) {
-    if (this.props.onScrollCursorIntoView != null) {
-      const line = editor.getCursor().line;
-      this.props.onScrollCursorIntoView(line);
-    }
-  }
-
-  cursorHandler(editor, event) {
-    const { additionalClassSet } = this.state;
-    const hasCustomClass = additionalClassSet.has(MARKDOWN_TABLE_ACTIVATED_CLASS);
-    const hasLinkClass = additionalClassSet.has(MARKDOWN_LINK_ACTIVATED_CLASS);
-
-    const isInTable = mtu.isInTable(editor);
-    const isInLink = markdownLinkUtil.isInLink(editor);
-
-    if (!hasCustomClass && isInTable) {
-      additionalClassSet.add(MARKDOWN_TABLE_ACTIVATED_CLASS);
-      this.setState({ additionalClassSet });
-    }
-
-    if (hasCustomClass && !isInTable) {
-      additionalClassSet.delete(MARKDOWN_TABLE_ACTIVATED_CLASS);
-      this.setState({ additionalClassSet });
-    }
-
-    if (!hasLinkClass && isInLink) {
-      additionalClassSet.add(MARKDOWN_LINK_ACTIVATED_CLASS);
-      this.setState({ additionalClassSet });
-    }
-
-    if (hasLinkClass && !isInLink) {
-      additionalClassSet.delete(MARKDOWN_LINK_ACTIVATED_CLASS);
-      this.setState({ additionalClassSet });
-    }
-  }
-
-  changeHandler(editor, data, value) {
-    if (this.props.onChange != null) {
-      const isClean = data.origin == null || editor.isClean() || value === this.props.value;
-      this.props.onChange(value, isClean);
-    }
-
-    this.updateCheatsheetStates(null, value);
-
-    // Show username hint on comment editor
-    if (this.props.isComment) {
-      this.commentMentionHelper.showUsernameHint();
-    }
-
-  }
-
-  turnOnEmojiPickerMode(pos) {
-    this.setState({
-      isEmojiPickerMode: true,
-      startPosWithEmojiPickerModeTurnedOn: pos,
-    });
-  }
-
-  turnOffEmojiPickerMode() {
-    this.setState({
-      isEmojiPickerMode: false,
-    });
-  }
-
-  showEmojiPicker(initialSearchingText) {
-    // show emoji picker with a stored word
-    this.setState({
-      isEmojiPickerShown: true,
-      emojiSearchText: initialSearchingText ?? '',
-    });
-
-    const resetStartPos = initialSearchingText == null;
-    if (resetStartPos) {
-      this.setState({ startPosWithEmojiPickerModeTurnedOn: null });
-    }
-
-    this.turnOffEmojiPickerMode();
-  }
-
-  keyPressHandlerForEmojiPicker(editor, event) {
-    const char = event.key;
-    const isEmojiPickerMode = this.state.isEmojiPickerMode;
-
-    // evaluate whether emoji picker mode to be turned on
-    if (!isEmojiPickerMode) {
-      const startPos = this.emojiPickerHelper.shouldModeTurnOn(char);
-      if (startPos == null) {
-        return;
-      }
-
-      this.turnOnEmojiPickerMode(startPos);
-      return;
-    }
-
-    // evaluate whether EmojiPicker to be opened
-    const startPos = this.state.startPosWithEmojiPickerModeTurnedOn;
-    if (this.emojiPickerHelper.shouldOpen(startPos)) {
-      const initialSearchingText = this.emojiPickerHelper.getInitialSearchingText(startPos);
-      this.showEmojiPicker(initialSearchingText);
-      return;
-    }
-
-    this.turnOffEmojiPickerMode();
-  }
-
-  keyPressHandler(editor, event) {
-    this.keyPressHandlerForEmojiPickerThrottled(editor, event);
-  }
-
-  keyDownHandlerForEmojiPicker(editor, event) {
-    const key = event.key;
-
-    if (!this.state.isEmojiPickerMode) {
-      return;
-    }
-
-    if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'BackSpace'].includes(key)) {
-      this.turnOffEmojiPickerMode();
-    }
-  }
-
-  keyDownHandler(editor, event) {
-    this.keyDownHandlerForEmojiPickerThrottled(editor, event);
-  }
-
-  windowClickHandler() {
-    this.turnOffEmojiPickerMode();
-  }
-
-  /**
-   * CodeMirror paste event handler
-   * see: https://codemirror.net/doc/manual.html#events
-   * @param {any} editor An editor instance of CodeMirror
-   * @param {any} event
-   */
-  pasteHandler(editor, event) {
-    const types = event.clipboardData.types;
-
-    // files
-    if (types.includes('Files')) {
-      event.preventDefault();
-      this.dispatchPasteFiles(event);
-    }
-    // text
-    else if (types.includes('text/plain')) {
-      pasteHelper.pasteText(this, event);
-    }
-
-  }
-
-  /**
-   * update states which related to cheatsheet
-   * @param {boolean} isGfmModeTmp (use state.isGfmMode if null is set)
-   * @param {string} valueTmp (get value from codemirror if null is set)
-   */
-  updateCheatsheetStates(isGfmModeTmp, valueTmp) {
-    const isGfmMode = isGfmModeTmp || this.state.isGfmMode;
-    const value = valueTmp || this.getCodeMirror().getDoc().getValue();
-
-    // update isSimpleCheatsheetShown
-    const isSimpleCheatsheetShown = isGfmMode && value.length === 0;
-    this.setState({ isSimpleCheatsheetShown });
-  }
-
-  markdownHelpButtonClickedHandler() {
-    if (this.props.onMarkdownHelpButtonClicked != null) {
-      this.props.onMarkdownHelpButtonClicked();
-    }
-  }
-
-  renderLoadingKeymapOverlay() {
-    // centering
-    const style = {
-      top: 0,
-      right: 0,
-      bottom: 0,
-      left: 0,
-    };
-
-    return this.state.isLoadingKeymap
-      ? (
-        <div className="overlay overlay-loading-keymap">
-          <span style={style} className="overlay-content">
-            <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
-          </span>
-        </div>
-      )
-      : '';
-  }
-
-  renderCheatsheetModalButton() {
-    return (
-      <button type="button" className="btn-link gfm-cheatsheet-modal-link small" onClick={() => { this.markdownHelpButtonClickedHandler() }}>
-        <span className="material-symbols-outlined">help</span> Markdown
-      </button>
-    );
-  }
-
-  renderCheatsheetOverlay() {
-    const cheatsheetModalButton = this.renderCheatsheetModalButton();
-
-    return (
-      <div className="overlay overlay-gfm-cheatsheet mt-1 p-3">
-        { this.state.isSimpleCheatsheetShown
-          ? (
-            <div className="text-end">
-              {cheatsheetModalButton}
-              <div className="mb-2 d-none d-md-block">
-                <SimpleCheatsheet />
-              </div>
-            </div>
-          )
-          : (
-            <div className="me-4 mb-2">
-              {cheatsheetModalButton}
-            </div>
-          )
-        }
-      </div>
-    );
-  }
-
-  renderEmojiPicker() {
-    const { emojiSearchText } = this.state;
-    return this.state.isEmojiPickerShown
-      ? (
-        <div className="text-start">
-          <div className="mb-2 d-none d-md-block">
-            <EmojiPicker
-              onClose={() => this.setState({ isEmojiPickerShown: false })}
-              onSelected={emoji => this.emojiPickerHelper.addEmoji(emoji, this.state.startPosWithEmojiPickerModeTurnedOn)}
-              emojiSearchText={emojiSearchText}
-              emojiPickerHelper={this.emojiPickerHelper}
-              isOpen={this.state.isEmojiPickerShown}
-            />
-          </div>
-        </div>
-      )
-      : '';
-  }
-
-  /**
-   * return a function to replace a selected range with prefix + selection + suffix
-   *
-   * The cursor after replacing is inserted between the selection and the suffix.
-   */
-  createReplaceSelectionHandler(prefix, suffix) {
-    return () => {
-      const cm = this.getCodeMirror();
-      const selection = cm.getDoc().getSelection();
-      const curStartPos = cm.getCursor('from');
-      const curEndPos = cm.getCursor('to');
-
-      const curPosAfterReplacing = {};
-      curPosAfterReplacing.line = curEndPos.line;
-      if (curStartPos.line === curEndPos.line) {
-        curPosAfterReplacing.ch = curEndPos.ch + prefix.length;
-      }
-      else {
-        curPosAfterReplacing.ch = curEndPos.ch;
-      }
-
-      cm.getDoc().replaceSelection(prefix + selection + suffix);
-      cm.setCursor(curPosAfterReplacing);
-      cm.focus();
-    };
-  }
-
-  /**
-   * return a function to add prefix to selected each lines
-   *
-   * The cursor after editing is inserted between the end of the selection.
-   */
-  createAddPrefixToEachLinesHandler(prefix) {
-    return () => {
-      const cm = this.getCodeMirror();
-      const startLineNum = cm.getCursor('from').line;
-      const endLineNum = cm.getCursor('to').line;
-
-      const lines = [];
-      for (let i = startLineNum; i <= endLineNum; i++) {
-        lines.push(prefix + cm.getDoc().getLine(i));
-      }
-      const replacement = `${lines.join('\n')}\n`;
-      cm.getDoc().replaceRange(replacement, { line: startLineNum, ch: 0 }, { line: endLineNum + 1, ch: 0 });
-
-      cm.setCursor(endLineNum, cm.getDoc().getLine(endLineNum).length);
-      cm.focus();
-    };
-  }
-
-  /**
-   * make a selected line a header
-   *
-   * The cursor after editing is inserted between the end of the line.
-   */
-  makeHeaderHandler() {
-    const cm = this.getCodeMirror();
-    const lineNum = cm.getCursor('from').line;
-    const line = cm.getDoc().getLine(lineNum);
-    let prefix = '#';
-    if (!line.startsWith('#')) {
-      prefix += ' ';
-    }
-    cm.getDoc().replaceRange(prefix, { line: lineNum, ch: 0 }, { line: lineNum, ch: 0 });
-    cm.focus();
-  }
-
-  // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-  // showGridEditorHandler() {
-  //   this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror()));
-  // }
-
-  showTemplateModal() {
-    const onSubmit = templateText => this.insertText(templateText);
-    this.props.onClickTemplateBtn({ onSubmit });
-  }
-
-  showLinkEditModal() {
-    const onSubmit = (linkText) => {
-      return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText);
-    };
-
-    const defaultMarkdownLink = markdownLinkUtil.getMarkdownLink(this.getCodeMirror());
-
-    this.props.onClickLinkEditBtn(defaultMarkdownLink, onSubmit);
-  }
-
-  // fold draw.io section (``` drawio ~ ```)
-  foldDrawioSection() {
-    const editor = this.getCodeMirror();
-    const lineNumbers = mdu.findAllDrawioSection(editor);
-    lineNumbers.forEach((lineNumber) => {
-      editor.foldCode({ line: lineNumber, ch: 0 }, { scanUp: false }, 'fold');
-    });
-  }
-
-  clickDrawioIconHandler() {
-    const drawioMxFile = mdu.getMarkdownDrawioMxfile(this.getCodeMirror());
-
-    this.props.onClickDrawioBtn(
-      drawioMxFile,
-      // onSave
-      (drawioMxFile) => {
-        mdu.replaceFocusedDrawioWithEditor(this.getCodeMirror(), drawioMxFile);
-        // Fold the section after the drawio section (```drawio) has been updated.
-        this.foldDrawioSection();
-      },
-    );
-  }
-
-  clickTableIconHandler() {
-    const markdownTable = mtu.getMarkdownTable(this.getCodeMirror());
-
-    this.props.onClickTableBtn(
-      markdownTable,
-      this.getCodeMirror(),
-      this.props.editorSettings.autoFormatMarkdownTable,
-    );
-  }
-
-  getNavbarItems() {
-    return [
-      <Button
-        key="nav-item-bold"
-        color={null}
-        size="sm"
-        title="Bold"
-        onClick={this.createReplaceSelectionHandler('**', '**')}
-      >
-        <EditorIcon icon="Bold" />
-      </Button>,
-      <Button
-        key="nav-item-italic"
-        color={null}
-        size="sm"
-        title="Italic"
-        onClick={this.createReplaceSelectionHandler('*', '*')}
-      >
-        <EditorIcon icon="Italic" />
-      </Button>,
-      <Button
-        key="nav-item-strikethrough"
-        color={null}
-        size="sm"
-        title="Strikethrough"
-        onClick={this.createReplaceSelectionHandler('~~', '~~')}
-      >
-        <EditorIcon icon="Strikethrough" />
-      </Button>,
-      <Button
-        key="nav-item-header"
-        color={null}
-        size="sm"
-        title="Heading"
-        onClick={this.makeHeaderHandler}
-      >
-        <EditorIcon icon="Heading" />
-      </Button>,
-      <Button
-        key="nav-item-code"
-        color={null}
-        size="sm"
-        title="Inline Code"
-        onClick={this.createReplaceSelectionHandler('`', '`')}
-      >
-        <EditorIcon icon="InlineCode" />
-      </Button>,
-      <Button
-        key="nav-item-quote"
-        color={null}
-        size="sm"
-        title="Quote"
-        onClick={this.createAddPrefixToEachLinesHandler('> ')}
-      >
-        <EditorIcon icon="Quote" />
-      </Button>,
-      <Button
-        key="nav-item-ul"
-        color={null}
-        size="sm"
-        title="List"
-        onClick={this.createAddPrefixToEachLinesHandler('- ')}
-      >
-        <EditorIcon icon="List" />
-      </Button>,
-      <Button
-        key="nav-item-ol"
-        color={null}
-        size="sm"
-        title="Numbered List"
-        onClick={this.createAddPrefixToEachLinesHandler('1. ')}
-      >
-        <EditorIcon icon="NumberedList" />
-      </Button>,
-      <Button
-        key="nav-item-checkbox"
-        color={null}
-        size="sm"
-        title="Check List"
-        onClick={this.createAddPrefixToEachLinesHandler('- [ ] ')}
-      >
-        <EditorIcon icon="CheckList" />
-      </Button>,
-      <Button
-        key="nav-item-attachment"
-        color={null}
-        size="sm"
-        title="Attachment"
-        onClick={this.props.onAddAttachmentButtonClicked}
-      >
-        <EditorIcon icon="Attachment" />
-      </Button>,
-      <Button
-        key="nav-item-link"
-        color={null}
-        size="sm"
-        title="Link"
-        onClick={this.showLinkEditModal}
-      >
-        <EditorIcon icon="Link" />
-      </Button>,
-      <Button
-        key="nav-item-image"
-        color={null}
-        size="sm"
-        title="Image"
-        onClick={this.createReplaceSelectionHandler('![', ']()')}
-      >
-        <EditorIcon icon="Image" />
-      </Button>,
-      // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-      // <Button
-      //   key="nav-item-grid"
-      //   color={null}
-      //   size="sm"
-      //   title="Grid"
-      //   onClick={this.showGridEditorHandler}
-      // >
-      //   <EditorIcon icon="Grid" />
-      // </Button>,
-      <Button
-        key="nav-item-table"
-        color={null}
-        size="sm"
-        title="Table"
-        onClick={this.clickTableIconHandler}
-      >
-        <EditorIcon icon="Table" />
-      </Button>,
-      <Button
-        key="nav-item-drawio"
-        color={null}
-        bssize="small"
-        title="draw.io"
-        onClick={this.clickDrawioIconHandler}
-      >
-        <EditorIcon icon="Drawio" />
-      </Button>,
-      <Button
-        key="nav-item-emoji"
-        color={null}
-        bssize="small"
-        title="Emoji"
-        onClick={() => this.showEmojiPicker()}
-      >
-        <EditorIcon icon="Emoji" />
-      </Button>,
-      <Button
-        key="nav-item-template"
-        color={null}
-        bssize="small"
-        title="Template"
-        onClick={() => this.showTemplateModal()}
-      >
-        <EditorIcon icon="Template" />
-      </Button>,
-    ];
-  }
-
-
-  render() {
-    const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
-    const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plain Text..';
-
-    const gutters = [];
-    if (this.props.lineNumbers != null) {
-      gutters.push('CodeMirror-linenumbers', 'CodeMirror-foldgutter');
-    }
-
-    return (
-      <div className={`grw-codemirror-editor ${styles['grw-codemirror-editor']}`}>
-
-        <UncontrolledCodeMirror
-          ref={this.cm}
-          className={additionalClasses}
-          placeholder="search"
-          value={this.props.value}
-          options={{
-            indentUnit: this.props.indentSize,
-            theme: this.props.editorSettings.theme ?? 'elegant',
-            styleActiveLine: this.props.editorSettings.styleActiveLine,
-            lineWrapping: true,
-            scrollPastEnd: true,
-            autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-            autoCloseTags: true,
-            placeholder,
-            matchBrackets: true,
-            emoji: true,
-            matchTags: { bothTags: true },
-            // folding
-            foldGutter: this.props.lineNumbers,
-            gutters,
-            // match-highlighter, matchesonscrollbar, annotatescrollbar options
-            highlightSelectionMatches: { annotateScrollbar: true },
-            // continuelist, indentlist
-            extraKeys: {
-              Enter: this.handleEnterKey,
-              'Ctrl-Enter': this.handleCtrlEnterKey,
-              'Cmd-Enter': this.handleCtrlEnterKey,
-              Tab: 'indentMore',
-              'Shift-Tab': 'indentLess',
-              'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
-            },
-          }}
-          onCursor={this.cursorHandlerDebounced}
-          onScroll={(editor, data) => {
-            if (this.props.onScroll != null) {
-            // add line data
-              const line = editor.lineAtHeight(data.top, 'local');
-              data.line = line;
-              this.props.onScroll(data);
-            }
-          }}
-          onChange={this.changeHandler}
-          onDragEnter={(editor, event) => {
-            if (this.props.onDragEnter != null) {
-              this.props.onDragEnter(event);
-            }
-          }}
-          onKeyPress={this.keyPressHandler}
-          onKeyDown={this.keyDownHandler}
-          onPasteFiles={this.pasteHandler}
-          onScrollCursorIntoView={this.scrollCursorIntoViewHandlerThrottled}
-        />
-
-        { this.renderLoadingKeymapOverlay() }
-
-        { this.renderCheatsheetOverlay() }
-        { this.renderEmojiPicker() }
-
-        {/*
-        // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
-        <GridEditModal
-          ref={this.gridEditModal}
-          onSave={(grid) => { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }}
-        />
-         */}
-      </div>
-    );
-  }
-
-}
-
-CodeMirrorEditor.propTypes = Object.assign({
-  lineNumbers: PropTypes.bool,
-  editorSettings: PropTypes.object.isRequired,
-  onMarkdownHelpButtonClicked: PropTypes.func,
-  onAddAttachmentButtonClicked: PropTypes.func,
-}, AbstractEditor.propTypes);
-
-CodeMirrorEditor.defaultProps = {
-  lineNumbers: true,
-};
-
-const CodeMirrorEditorMemoized = memo(CodeMirrorEditor);
-
-
-const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
-  const { open: openDrawioModal } = useDrawioModal();
-  const { open: openHandsontableModal } = useHandsontableModal();
-  const { open: openTemplateModal } = useTemplateModal();
-  const { open: openLinkEditModal } = useLinkEditModal();
-
-  const openDrawioModalHandler = useCallback((drawioMxFile, onSave) => {
-    openDrawioModal(drawioMxFile, onSave);
-  }, [openDrawioModal]);
-
-  const openTableModalHandler = useCallback((markdownTable, editor, autoFormatMarkdownTable) => {
-    openHandsontableModal(markdownTable, editor, autoFormatMarkdownTable);
-  }, [openHandsontableModal]);
-
-  const openTemplateModalHandler = useCallback((onSubmit) => {
-    openTemplateModal(onSubmit);
-  }, [openTemplateModal]);
-
-  const openLinkEditModalHandler = useCallback((defaultMarkdownLink, onSubmit) => {
-    openLinkEditModal(defaultMarkdownLink, onSubmit);
-  }, [openLinkEditModal]);
-
-  return (
-    <CodeMirrorEditorMemoized
-      ref={ref}
-      onClickDrawioBtn={openDrawioModalHandler}
-      onClickTableBtn={openTableModalHandler}
-      onClickTemplateBtn={openTemplateModalHandler}
-      onClickLinkEditBtn={openLinkEditModalHandler}
-      {...props}
-    />
-  );
-});
-
-CodeMirrorEditorFc.displayName = 'CodeMirrorEditorFc';
-
-export default memo(CodeMirrorEditorFc);

+ 0 - 126
apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.module.scss

@@ -1,126 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-.grw-codemirror-editor :global {
-  @import '~codemirror/lib/codemirror';
-
-  // addons
-  @import '~codemirror/addon/fold/foldgutter';
-  @import '~codemirror/addon/lint/lint';
-
-  // themes
-  @import '~codemirror/theme/elegant';
-  @import '~codemirror/theme/eclipse';
-
-  .CodeMirror {
-    font-family: var(--font-family-monospace);
-    font-size: 15px;
-
-    pre.CodeMirror-line.grw-cm-header-line {
-      padding-top: 0.16em;
-      padding-bottom: 0.08em;
-      font-family: var(--font-family-monospace);
-
-      // '#'
-      .cm-formatting-header {
-        font-style: italic;
-        font-weight: bold;
-        opacity: 0.5;
-      }
-
-      .cm-header-1 {
-        font-size: 1.9em;
-      }
-      .cm-header-2 {
-        font-size: 1.6em;
-      }
-      .cm-header-3 {
-        font-size: 1.4em;
-      }
-      .cm-header-4 {
-        font-size: 1.35em;
-      }
-      .cm-header-5 {
-        font-size: 1.25em;
-      }
-      .cm-header-6 {
-        font-size: 1.2em;
-      }
-    }
-
-    .cm-matchhighlight {
-      color: bs.$gray-900 !important;
-      background-color: cyan;
-    }
-
-    .CodeMirror-selection-highlight-scrollbar {
-      background-color: darkcyan;
-    }
-
-    // overwrite .CodeMirror-placeholder
-    pre.CodeMirror-line-like.CodeMirror-placeholder {
-      color: bs.$text-muted;
-    }
-
-    // overwrite .CodeMirror-scroll
-    .CodeMirror-scroll {
-      box-sizing: border-box;
-    }
-  }
-
-  // patch to fix https://github.com/codemirror/CodeMirror/issues/4089
-  // see also https://github.com/codemirror/CodeMirror/commit/51a1e7da60a99e019f026a118dc7c98c2b1f9d62
-  .CodeMirror-wrap > div > textarea {
-    font-size: #{bs.$line-height-base}rem;
-  }
-
-  // overwrite .CodeMirror-hints
-  .CodeMirror-hints {
-    max-height: 30em !important;
-
-    // active line
-    .CodeMirror-hint-active.crowi-emoji-autocomplete {
-      .img-container {
-        padding-top: 0.3em;
-        padding-bottom: 0.3em;
-        font-size: 15px; // adjust to .wiki
-      }
-    }
-  }
-
-  // cheat sheat
-  .overlay.overlay-gfm-cheatsheet {
-    align-items: flex-end;
-    justify-content: flex-end;
-
-    pointer-events: none;
-
-    .card.gfm-cheatsheet {
-      box-shadow: unset;
-      opacity: 0.6;
-      .card-body {
-        min-width: 30em;
-        padding-bottom: 0;
-        font-family: var(--font-family-monospace);
-        color: bs.$text-muted;
-      }
-      ul > li {
-        list-style: none;
-      }
-    }
-
-    .gfm-cheatsheet-modal-link {
-      color: bs.$text-muted;
-      pointer-events: all;
-      cursor: pointer;
-      background-color: transparent;
-      border: none;
-
-      opacity: 0.6;
-
-      &:hover,
-      &:focus {
-        opacity: 1;
-      }
-    }
-  }
-}

+ 0 - 65
apps/app/_obsolete/src/components/PageEditor/CommentMentionHelper.ts

@@ -1,65 +0,0 @@
-import { Editor } from 'codemirror';
-import { i18n } from 'next-i18next';
-import { debounce } from 'throttle-debounce';
-
-import { apiv3Get } from '~/client/util/apiv3-client';
-
-type UsersListForHints = {
-  text: string
-  displayText: string
-}
-export default class CommentMentionHelper {
-
-  editor: Editor;
-
-  constructor(editor: Editor) {
-    this.editor = editor;
-  }
-
-  getUsenameHint = (): void => {
-    // Get word that contains `@` character at the begining
-    const currentPos = this.editor.getCursor();
-    const wordStart = this.editor.findWordAt(currentPos).anchor.ch - 1;
-    const wordEnd = this.editor.findWordAt(currentPos).head.ch;
-
-    const searchFrom = { line: currentPos.line, ch: wordStart };
-    const searchTo = { line: currentPos.line, ch: wordEnd };
-
-    const searchMention = this.editor.getRange(searchFrom, searchTo);
-    const isMentioning = searchMention.charAt(0) === '@';
-
-    // Return nothing if not mentioning
-    if (!isMentioning) {
-      return;
-    }
-
-    // Get username after `@` character and search username
-    const mention = searchMention.slice(1);
-    this.editor.showHint({
-      completeSingle: false,
-      hint: async() => {
-        if (mention.length > 0) {
-          const users = await this.getUsersList(mention);
-          return {
-            // Returns default value if i18n is null because it cannot do early return.
-            list: users.length > 0 ? users : [{ text: '', displayText: i18n != null ? i18n.t('page_comment.no_user_found') : 'No user found' }],
-            from: searchFrom,
-            to: searchTo,
-          };
-        }
-      },
-    });
-  };
-
-  getUsersList = async(q: string): Promise<UsersListForHints[]> => {
-    const limit = 20;
-    const { data } = await apiv3Get('/users/usernames', { q, limit });
-    return data.activeUser.usernames.map((username: string) => ({
-      text: `@${username} `,
-      displayText: username,
-    }));
-  };
-
-  showUsernameHint = debounce(800, () => this.getUsenameHint());
-
-}

+ 0 - 344
apps/app/_obsolete/src/components/PageEditor/ConflictDiffModal.tsx

@@ -1,344 +0,0 @@
-import React, {
-  useState, useEffect, useRef, useMemo, useCallback,
-} from 'react';
-
-import type { IRevisionOnConflict } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components';
-import CodeMirror from 'codemirror/lib/codemirror';
-import { format, parseISO } from 'date-fns';
-import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import { useSaveOrUpdate } from '~/client/services/page-operation';
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import { OptionsToSave } from '~/interfaces/page-operation';
-import { useCurrentPathname, useCurrentUser } from '~/stores/context';
-import { useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
-import {
-  useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, useSetRemoteLatestPageData,
-} from '~/stores/remote-latest-page';
-
-import ExpandOrContractButton from '../ExpandOrContractButton';
-import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
-
-require('codemirror/lib/codemirror.css');
-require('codemirror/addon/merge/merge');
-require('codemirror/addon/merge/merge.css');
-const DMP = require('diff_match_patch');
-
-Object.keys(DMP).forEach((key) => { window[key] = DMP[key] });
-
-type ConflictDiffModalProps = {
-  isOpen?: boolean;
-  onClose?: (() => void);
-  markdownOnEdit: string;
-  optionsToSave: OptionsToSave | undefined;
-  afterResolvedHandler: () => void,
-};
-
-type ConflictDiffModalCoreProps = {
-  isOpen?: boolean;
-  onClose?: (() => void);
-  optionsToSave: OptionsToSave | undefined;
-  request: IRevisionOnConflictWithStringDate,
-  origin: IRevisionOnConflictWithStringDate,
-  latest: IRevisionOnConflictWithStringDate,
-  afterResolvedHandler: () => void,
-};
-
-type IRevisionOnConflictWithStringDate = Omit<IRevisionOnConflict, 'createdAt'> & {
-  createdAt: string
-}
-
-const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element => {
-  const {
-    onClose, request, origin, latest, optionsToSave, afterResolvedHandler,
-  } = props;
-
-  const { t } = useTranslation('');
-  const [resolvedRevision, setResolvedRevision] = useState<string>('');
-  const [isRevisionselected, setIsRevisionSelected] = useState<boolean>(false);
-  const [isModalExpanded, setIsModalExpanded] = useState<boolean>(false);
-  const [codeMirrorRef, setCodeMirrorRef] = useState<HTMLDivElement | null>(null);
-
-  const { data: remoteRevisionId } = useRemoteRevisionId();
-  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
-  const { data: pageId } = useCurrentPageId();
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: currentPathname } = useCurrentPathname();
-
-  const saveOrUpdate = useSaveOrUpdate();
-
-  const uncontrolledRef = useRef<CodeMirror>(null);
-
-  useEffect(() => {
-    if (codeMirrorRef != null) {
-      CodeMirror.MergeView(codeMirrorRef, {
-        value: origin.revisionBody,
-        origLeft: request.revisionBody,
-        origRight: latest.revisionBody,
-        lineNumbers: true,
-        collapseIdentical: true,
-        showDifferences: true,
-        highlightDifferences: true,
-        connect: 'connect',
-        readOnly: true,
-        revertButtons: false,
-      });
-    }
-  }, [codeMirrorRef, origin.revisionBody, request.revisionBody, latest.revisionBody]);
-
-  const close = useCallback(() => {
-    if (onClose != null) {
-      onClose();
-    }
-  }, [onClose]);
-
-  const onResolveConflict = useCallback(async() => {
-    if (currentPathname == null) { return }
-    // disable button after clicked
-    setIsRevisionSelected(false);
-
-    const codeMirrorVal = uncontrolledRef.current?.editor.doc.getValue();
-
-    try {
-      const { page } = await saveOrUpdate(
-        codeMirrorVal,
-        { pageId, path: currentPagePath || currentPathname, revisionId: remoteRevisionId },
-        optionsToSave,
-      );
-      const remotePageData = {
-        remoteRevisionId: page.revision._id,
-        remoteRevisionBody: page.revision.body,
-        remoteRevisionLastUpdateUser: page.lastUpdateUser,
-        remoteRevisionLastUpdatedAt: page.updatedAt,
-        revisionIdHackmdSynced: page.revisionIdHackmdSynced,
-        hasDraftOnHackmd: page.hasDraftOnHackmd,
-      };
-      setRemoteLatestPageData(remotePageData);
-      afterResolvedHandler();
-
-      close();
-
-      toastSuccess('Saved successfully');
-    }
-    catch (error) {
-      toastError(`Error occured: ${error.message}`);
-    }
-
-  }, [afterResolvedHandler, close, currentPagePath, currentPathname, optionsToSave, pageId, remoteRevisionId, saveOrUpdate, setRemoteLatestPageData]);
-
-  // TODO: No longer support custom close icon in bootstrap v5
-  // const resizeAndCloseButtons = useMemo(() => (
-  //   <div className="d-flex flex-nowrap">
-  //     <ExpandOrContractButton
-  //       isWindowExpanded={isModalExpanded}
-  //       expandWindow={() => setIsModalExpanded(true)}
-  //       contractWindow={() => setIsModalExpanded(false)}
-  //     />
-  //     <button type="button" className="close text-white" onClick={close} aria-label="Close">
-  //       <span aria-hidden="true">&times;</span>
-  //     </button>
-  //   </div>
-  // ), [isModalExpanded, close]);
-
-  const isOpen = props.isOpen ?? false;
-
-  return (
-    <Modal
-      isOpen={isOpen}
-      toggle={close}
-      backdrop="static"
-      className={`${isModalExpanded ? ' grw-modal-expanded' : ''}`}
-      size="xl"
-    >
-      {/* <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light align-items-center py-3" close={resizeAndCloseButtons}> */}
-      <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light align-items-center py-3">
-        <span className="material-symbols-outlined">error</span>{t('modal_resolve_conflict.resolve_conflict')}
-      </ModalHeader>
-      <ModalBody className="mx-4 my-1">
-        { isOpen
-        && (
-          <div className="row">
-            <div className="col-12 text-center mt-2 mb-4">
-              <h2 className="fw-bold">{t('modal_resolve_conflict.resolve_conflict_message')}</h2>
-            </div>
-            <div className="col-4">
-              <h3 className="fw-bold my-2">{t('modal_resolve_conflict.requested_revision')}</h3>
-              <div className="d-flex align-items-center my-3">
-                <div>
-                  <UserPicture user={request.user} size="lg" noLink noTooltip />
-                </div>
-                <div className="ms-3 text-muted">
-                  <p className="my-0">updated by {request.user.username}</p>
-                  <p className="my-0">{request.createdAt}</p>
-                </div>
-              </div>
-            </div>
-            <div className="col-4">
-              <h3 className="fw-bold my-2">{t('modal_resolve_conflict.origin_revision')}</h3>
-              <div className="d-flex align-items-center my-3">
-                <div>
-                  <UserPicture user={origin.user} size="lg" noLink noTooltip />
-                </div>
-                <div className="ms-3 text-muted">
-                  <p className="my-0">updated by {origin.user.username}</p>
-                  <p className="my-0">{origin.createdAt}</p>
-                </div>
-              </div>
-            </div>
-            <div className="col-4">
-              <h3 className="fw-bold my-2">{t('modal_resolve_conflict.latest_revision')}</h3>
-              <div className="d-flex align-items-center my-3">
-                <div>
-                  <UserPicture user={latest.user} size="lg" noLink noTooltip />
-                </div>
-                <div className="ms-3 text-muted">
-                  <p className="my-0">updated by {latest.user.username}</p>
-                  <p className="my-0">{latest.createdAt}</p>
-                </div>
-              </div>
-            </div>
-            <div className="col-12" ref={(el) => { setCodeMirrorRef(el) }}></div>
-            <div className="col-4">
-              <div className="text-center my-4">
-                <button
-                  type="button"
-                  className="btn btn-outline-primary"
-                  onClick={() => {
-                    setIsRevisionSelected(true);
-                    setResolvedRevision(request.revisionBody);
-                  }}
-                >
-                  <span className="material-symbols-outlined">arrow_circle_down</span>
-                  {t('modal_resolve_conflict.select_revision', { revision: 'mine' })}
-                </button>
-              </div>
-            </div>
-            <div className="col-4">
-              <div className="text-center my-4">
-                <button
-                  type="button"
-                  className="btn btn-outline-primary"
-                  onClick={() => {
-                    setIsRevisionSelected(true);
-                    setResolvedRevision(origin.revisionBody);
-                  }}
-                >
-                  <span className="material-symbols-outlined">arrow_circle_down</span>
-                  {t('modal_resolve_conflict.select_revision', { revision: 'origin' })}
-                </button>
-              </div>
-            </div>
-            <div className="col-4">
-              <div className="text-center my-4">
-                <button
-                  type="button"
-                  className="btn btn-outline-primary"
-                  onClick={() => {
-                    setIsRevisionSelected(true);
-                    setResolvedRevision(latest.revisionBody);
-                  }}
-                >
-                  <span className="material-symbols-outlined">arrow_circle_down</span>
-                  {t('modal_resolve_conflict.select_revision', { revision: 'theirs' })}
-                </button>
-              </div>
-            </div>
-            <div className="col-12">
-              <div className="border border-dark">
-                <h3 className="fw-bold my-2 mx-2">{t('modal_resolve_conflict.selected_editable_revision')}</h3>
-                <UncontrolledCodeMirror
-                  ref={uncontrolledRef}
-                  value={resolvedRevision}
-                  options={{
-                    placeholder: t('modal_resolve_conflict.resolve_conflict_message'),
-                  }}
-                />
-              </div>
-            </div>
-          </div>
-        )}
-      </ModalBody>
-      <ModalFooter>
-        <button
-          type="button"
-          className="btn btn-outline-secondary"
-          onClick={onClose}
-        >
-          {t('Cancel')}
-        </button>
-        <button
-          type="button"
-          className="btn btn-primary ms-3"
-          onClick={onResolveConflict}
-          disabled={!isRevisionselected}
-        >
-          {t('modal_resolve_conflict.resolve_and_save')}
-        </button>
-      </ModalFooter>
-    </Modal>
-  );
-};
-
-
-export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element => {
-  const {
-    isOpen, onClose, optionsToSave, afterResolvedHandler,
-  } = props;
-  const { data: currentUser } = useCurrentUser();
-
-  // state for current page
-  const { data: currentPage } = useSWRxCurrentPage();
-
-  // state for latest page
-  const { data: remoteRevisionId } = useRemoteRevisionId();
-  const { data: remoteRevisionBody } = useRemoteRevisionBody();
-  const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
-  const { data: remoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
-
-  const currentTime: Date = new Date();
-
-  const isRemotePageDataInappropriate = remoteRevisionId == null || remoteRevisionBody == null || remoteRevisionLastUpdateUser == null;
-
-  if (!isOpen || currentUser == null || currentPage == null || isRemotePageDataInappropriate) {
-    return <></>;
-  }
-
-  const currentPageCreatedAtFixed = typeof currentPage.updatedAt === 'string'
-    ? parseISO(currentPage.updatedAt)
-    : currentPage.updatedAt;
-
-  const request: IRevisionOnConflictWithStringDate = {
-    revisionId: '',
-    revisionBody: props.markdownOnEdit,
-    createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
-    user: currentUser,
-  };
-  const origin: IRevisionOnConflictWithStringDate = {
-    revisionId: currentPage?.revision._id,
-    revisionBody: currentPage?.revision.body,
-    createdAt: format(currentPageCreatedAtFixed, 'yyyy/MM/dd HH:mm:ss'),
-    user: currentPage?.lastUpdateUser,
-  };
-  const latest: IRevisionOnConflictWithStringDate = {
-    revisionId: remoteRevisionId,
-    revisionBody: remoteRevisionBody,
-    createdAt: format(new Date(remoteRevisionLastUpdatedAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
-    user: remoteRevisionLastUpdateUser,
-  };
-
-  const propsForCore = {
-    isOpen,
-    onClose,
-    optionsToSave,
-    request,
-    origin,
-    latest,
-    afterResolvedHandler,
-  };
-
-  return <ConflictDiffModalCore {...propsForCore} />;
-};

+ 0 - 174
apps/app/_obsolete/src/components/Sidebar/AppearanceModeDropdown.tsx

@@ -1,174 +0,0 @@
-import React, {
-  FC, useCallback, useRef,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
-import { useRipple } from 'react-use-ripple';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { useUserUISettings } from '~/client/services/user-ui-settings';
-import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
-import { Themes, useNextThemes } from '~/stores/use-next-themes';
-
-import SidebarDockIcon from '../Icons/SidebarDockIcon';
-import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
-
-type AppearanceModeDropdownProps = {
-  isAuthenticated: boolean,
-}
-export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: AppearanceModeDropdownProps) => {
-
-  const { t } = useTranslation('commons');
-
-  const { isAuthenticated } = props;
-
-  const {
-    setTheme, resolvedTheme, useOsSettings, isDarkMode, isForcedByGrowiTheme,
-  } = useNextThemes();
-  const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
-  const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
-  const { scheduleToPut } = useUserUISettings();
-
-  // ripple
-  const buttonRef = useRef(null);
-  useRipple(buttonRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
-
-  const preferDrawerModeSwitchModifiedHandler = useCallback((preferDrawerMode: boolean, isEditMode: boolean) => {
-    if (isEditMode) {
-      mutatePreferDrawerModeOnEdit(preferDrawerMode);
-      scheduleToPut({ preferDrawerModeOnEditByUser: preferDrawerMode });
-    }
-    else {
-      updatePreferDrawerMode(preferDrawerMode);
-    }
-  }, [updatePreferDrawerMode, mutatePreferDrawerModeOnEdit, scheduleToPut]);
-
-  const followOsCheckboxModifiedHandler = useCallback((isChecked: boolean) => {
-    if (isChecked) {
-      setTheme(Themes.SYSTEM);
-    }
-    else {
-      setTheme(resolvedTheme ?? Themes.LIGHT);
-    }
-  }, [resolvedTheme, setTheme]);
-
-  const userPreferenceSwitchModifiedHandler = useCallback((isDarkMode: boolean) => {
-    setTheme(isDarkMode ? Themes.DARK : Themes.LIGHT);
-  }, [setTheme]);
-
-  /* eslint-disable react/prop-types */
-  const IconWithTooltip = ({
-    id, label, children, additionalClasses,
-  }) => (
-    <>
-      <div id={id} className={`px-2 grw-icon-container ${additionalClasses != null ? additionalClasses : ''}`}>{children}</div>
-      <UncontrolledTooltip placement="bottom" fade={false} target={id}>{label}</UncontrolledTooltip>
-    </>
-  );
-
-  const dropdownDivider = <div className="dropdown-divider"></div>;
-
-  const renderSidebarModeSwitch = useCallback((isEditMode: boolean) => {
-    return (
-      <>
-        <h6 className="dropdown-header">{t(isEditMode ? 'personal_dropdown.sidebar_mode_editor' : 'personal_dropdown.sidebar_mode')}</h6>
-        <form className="px-4">
-          <div className="justify-content-center">
-            <div className="col-auto mb-0 d-flex align-items-center">
-              <IconWithTooltip id={isEditMode ? 'iwt-sidebar-editor-drawer' : 'iwt-sidebar-drawer'} label="Drawer" additionalClasses="grw-sidebar-mode-icon">
-                <SidebarDrawerIcon />
-              </IconWithTooltip>
-              <div className="form-check form-switch form-check-secondary ms-2">
-                <input
-                  id={isEditMode ? 'swSidebarModeOnEditor' : 'swSidebarMode'}
-                  className="form-check-input"
-                  type="checkbox"
-                  checked={isEditMode ? !isPreferDrawerModeOnEdit : !isPreferDrawerMode}
-                  onChange={e => preferDrawerModeSwitchModifiedHandler(!e.target.checked, isEditMode)}
-                />
-                <label className="form-label form-check-label" htmlFor={isEditMode ? 'swSidebarModeOnEditor' : 'swSidebarMode'}></label>
-              </div>
-              <IconWithTooltip id={isEditMode ? 'iwt-sidebar-editor-dock' : 'iwt-sidebar-dock'} label="Dock" additionalClasses="grw-sidebar-mode-icon">
-                <SidebarDockIcon />
-              </IconWithTooltip>
-            </div>
-          </div>
-        </form>
-      </>
-    );
-  }, [isPreferDrawerMode, isPreferDrawerModeOnEdit, preferDrawerModeSwitchModifiedHandler, t]);
-
-  return (
-    <div className="dropend">
-      {/* setting button */}
-      {/* remove .dropdown-toggle for hide caret */}
-      {/* See https://stackoverflow.com/a/44577512/13183572 */}
-      <button className="btn btn-primary" type="button" data-bs-toggle="dropdown" ref={buttonRef} aria-haspopup="true">
-        <span className="material-symbols-outlined">settings</span>
-      </button>
-
-      {/* dropdown */}
-      <div className="dropdown-menu">
-
-        {/* sidebar mode */}
-        {renderSidebarModeSwitch(false)}
-
-        {/* side bar mode on editor */}
-        {isAuthenticated && (
-          <>
-            {dropdownDivider}
-            {renderSidebarModeSwitch(true)}
-          </>
-        )}
-
-        {/* color mode */}
-        { !isForcedByGrowiTheme && (
-          <>
-            {dropdownDivider}
-            <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
-            <form className="px-4">
-              <div className="justify-content-center">
-                <div className="col-auto d-flex align-items-center">
-                  <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                  <span className="material-symbols-outlined">light_mode</span>
-                  </IconWithTooltip>
-                  <div className="form-check form-switch form-check-secondary ms-2">
-                    <input
-                      id="swUserPreference"
-                      className="form-check-input"
-                      type="checkbox"
-                      checked={isDarkMode}
-                      disabled={useOsSettings}
-                      onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
-                    />
-                    <label className="form-label form-check-label" htmlFor="swUserPreference"></label>
-                  </div>
-                  <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                  <span className="material-symbols-outlined">dark_mode</span>
-                  </IconWithTooltip>
-                </div>
-              </div>
-              <div>
-                <div className="col-auto">
-                  <div className="form-check">
-                    <input
-                      id="cbFollowOs"
-                      className="form-check-input"
-                      type="checkbox"
-                      checked={useOsSettings}
-                      onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
-                    />
-                    <label className="form-label form-check-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
-                  </div>
-                </div>
-              </div>
-            </form>
-          </>
-        ) }
-
-      </div>
-
-    </div>
-  );
-
-};

+ 0 - 672
apps/app/_obsolete/src/styles/theme/_apply-colors-dark.scss

@@ -1,672 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as *;
-
-@use '../variables' as var;
-@use '../atoms/mixins/buttons' as mixins-buttons;
-@use './mixins/count-badge';
-@use './mixins/hsl-button';
-@use './hsl-functions' as hsl;
-
-// determine optional variables
-:root[data-bs-theme='dark'] {
-  $color-list: var(--color-list,var(--color-global));
-  $bgcolor-list: var(--bgcolor-list,var(--bgcolor-global));
-  $color-list-hover: var(--color-list-hover,var(--color-global));
-  $color-list-active: var(--color-list-active,var(--color-reversal));
-  $bgcolor-list-hover: var(--bgcolor-list-hover,var(--bgcolor-global));
-  $bgcolor-list-active: var(--bgcolor-list-active,var(--primary));
-  $color-table: var(--color-table,white);
-  $bgcolor-table: var(--bgcolor-table,#343a40);
-  $border-color-table: var(--border-color-table,lighten(#343a40, 7.5%));
-  $color-table-hover: var(--color-table-hover,rgba(white, 0.075));
-  $bgcolor-table-hover: var(--bgcolor-table-hover,lighten(#343a40, 7.5%));
-  $bgcolor-sidebar-list-group: var(--bgcolor-sidebar-list-group,var(--bgcolor-list));
-  $color-tags: var(--color-tags,#949494);
-  $bgcolor-tags: var(--bgcolor-tags,var(--dark));
-  $border-color-global: var(--border-color-global,#{$gray-500});
-  $border-color-toc: var(--border-color-toc,#{$border-color-global});
-  $color-dropdown: var(--color-dropdown,var(--color-global));
-  $bgcolor-dropdown: var(--bgcolor-dropdown,var(--bgcolor-global));
-  $color-dropdown-link: var(--color-dropdown-link,var(--color-global));
-  $color-dropdown-link-hover: var(--color-dropdown-link-hover,var(--light));
-  $bgcolor-dropdown-link-hover: var(--bgcolor-dropdown-link-hover,hsl.lighten(var(--bgcolor-global), 15%));
-  $color-dropdown-link-active: var(--color-dropdown-link-active,var(--light));
-  $bgcolor-dropdown-link-active: var(--bgcolor-dropdown-link-active,var(--primary));
-  $body-bg: var(--bgcolor-global);
-  $body-color: var(--color-global);
-
-  // override bootstrap variables
-  // $text-muted: $gray-550;
-  $table-dark-color: $color-table;
-  $table-dark-bg: $bgcolor-table;
-  $table-dark-border-color: $border-color-table;
-  $table-dark-hover-color: $color-table-hover;
-  $table-dark-hover-bg: $bgcolor-table-hover;
-  $border-color: $border-color-global;
-  $dropdown-color: $color-dropdown;
-  $dropdown-bg: $bgcolor-dropdown;
-  $dropdown-link-color: $color-dropdown-link;
-  $dropdown-link-hover-color: $color-dropdown-link-hover;
-  $dropdown-link-hover-bg: $bgcolor-dropdown-link-hover;
-  $dropdown-link-active-color: $color-dropdown-link-active;
-  $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
-
-  @import './mixins/list-group';
-  // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-  // @import './reboot-bootstrap-text';
-  // @import './reboot-bootstrap-border-colors';
-  // @import './reboot-bootstrap-tables';
-  // @import './reboot-bootstrap-theme-colors';
-  // @import 'hsl-reboot-bootstrap-theme-colors';
-  // @import './reboot-bootstrap-dropdown';
-
-
-  // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-
-  //   // List Group
-  //   @include override-list-group-item(
-  //     $color-list,
-  //     $bgcolor-sidebar-list-group,
-  //     $color-list-hover,
-  //     $bgcolor-list-hover,
-  //     $color-list-active,
-  //     $bgcolor-list-active
-  //   );
-  //   /*
-  //     * Form
-  //     */
-  //   input.form-control,
-  //   select.form-control,
-  //   select.form-select,
-  //   textarea.form-control {
-  //     color: var(--color-global);
-  //     background-color: hsl.darken(var(--bgcolor-global), 5%);
-  //     border-color: $border-color-global;
-  //     &:focus {
-  //       background-color: var(--bgcolor-global);
-  //     }
-  //     // FIXME: accent color
-  //     // border: 1px solid darken($border, 30%);
-  //   }
-
-  //   .form-control[disabled],
-  //   .form-control[readonly] {
-  //     color: hsl.lighten(var(--color-global),10%);
-  //     background-color: hsl.lighten(var(--bgcolor-global),5%);
-  //   }
-
-  // TODO: theme-color() dropped in bootstrap v5
-  // TODO: .input-group-prepend dropped in bootstrap v5
-  // https://redmine.weseek.co.jp/issues/128307
-  //   .input-group > .input-group-prepend > .input-group-text {
-  //     color: theme-color('light');
-  //     background-color: theme-color('secondary');
-  //     border: 1px solid theme-color('secondary');
-  //     border-right: none;
-  //     &.text-muted {
-  //       color: theme-color('light') !important;
-  //     }
-  //   }
-
-  //   .input-group input {
-  //     border-color: $border-color-global;
-  //   }
-
-  //   label.form-check-label::before {
-  //     background-color: hsl.darken(var(--bgcolor-global),5%);
-  //   }
-
-  //   .rbt-input-multi .rbt-input-main {
-  //     color: black;
-  //   }
-  //   /*
-  //   * Table
-  //   */
-  //   .table {
-  //     @extend .table-dark !optional;
-  //     thead th {
-  //       vertical-align: bottom;
-  //       border-bottom: 2px solid #d6dadf;
-  //     }
-  //   }
-
-  //   /*
-  //   * Card
-  //   */
-  //   .card:not([class*='bg-']):not(.custom-card):not(.card-disabled) {
-  //     @extend .bg-dark;
-  //   }
-
-  //   .card.custom-card {
-  //     border-color: var(--secondary);
-  //   }
-
-  //   .card.card-disabled {
-  //     background-color: lighten($dark, 10%);
-  //     border-color: var(--secondary);
-  //   }
-
-  //   /*
-  //   * Pagination
-  //   */
-  //   ul.pagination {
-  //     li.page-item.disabled {
-  //       button.page-link {
-  //         color: $gray-400;
-  //       }
-  //     }
-  //     li.page-item.active {
-  //       button.page-link {
-  //         color: hsl.contrast(var(--primary));
-  //         background-color: var(--primary);
-  //         &:hover,
-  //         &:focus {
-  //           color: hsl.contrast(var(--primary));
-  //           background-color: var(--primary);
-  //         }
-  //       }
-  //     }
-  //     li.page-item {
-  //       button.page-link {
-  //         @extend .btn-dark;
-  //         color: var(--primary);
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI Login form
-  //   */
-  //   .nologin {
-  //     // background color
-  //     $color-gradient: #3c465c;
-  //     background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
-  //       linear-gradient(135deg, darken(var.$growi-green, 30%) 10%, hsla(225, 95%, 50%, 0) 70%),
-  //       linear-gradient(225deg, darken(var.$growi-blue, 20%) 10%, hsla(140, 90%, 50%, 0) 80%),
-  //       linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
-
-  //     .nologin-header {
-  //       background-color: rgba(black, 0.5);
-
-  //       .logo {
-  //         background-color: rgba(white, 0);
-  //         fill: rgba(white, 0.5);
-  //       }
-
-  //       h1 {
-  //         color: rgba(white, 0.5);
-  //       }
-  //     }
-
-  //     .nologin-dialog {
-  //       background-color: rgba(black, 0.5);
-  //       .link-switch {
-  //         color: #7b9bd5;
-  //         &:hover {
-  //           color: lighten(#7b9bd5,10%);
-  //         }
-  //       }
-  //     }
-
-  //     .input-group {
-  //       .input-group-text {
-  //         color: darken(white, 30%);
-  //         background-color: rgba($gray-700, 0.7);
-  //       }
-
-  //       .form-control {
-  //         color: white;
-  //         background-color: rgba(#505050, 0.7);
-  //         box-shadow: unset;
-
-  //         &::placeholder {
-  //           color: darken(white, 30%);
-  //         }
-  //       }
-  //     }
-
-  //     .btn-fill {
-  //       .btn-label {
-  //         color: $gray-300;
-  //       }
-  //       .btn-label-text {
-  //         color: $gray-400;
-  //       }
-  //     }
-
-  //     .grw-external-auth-form {
-  //       border-color: gray !important;
-  //     }
-
-  //     .btn-external-auth-tab {
-  //       @extend .btn-dark;
-  //     }
-
-  //     // footer link text
-  //     .link-growi-org {
-  //       color: rgba(white, 0.4);
-
-  //       &:hover,
-  //       &.focus {
-  //         color: rgba(white, 0.7);
-
-  //         .growi {
-  //           color: darken(var.$growi-green, 5%);
-  //         }
-
-  //         .org {
-  //           color: darken(var.$growi-blue, 5%);
-  //         }
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI subnavigation
-  //   */
-  //   .grw-drawer-toggler {
-  //     @include button-variant($dark, $dark);
-  //     @include mixins-buttons.button-svg-icon-variant($dark, $dark);
-  //     color: $gray-400;
-  //     box-shadow: none !important;
-  //   }
-
-  //   /**
-  //    * GROWI PagePathHierarchicalLink
-  //    */
-  //   .grw-page-path-text-muted-container .grw-page-path-hierarchical-link a {
-  //     color: $gray-400;
-  //   }
-
-  //   /*
-  //   * GROWI page list
-  //   */
-  //   .page-list {
-  //     .page-list-ul {
-  //       > li {
-  //         > span.page-list-meta {
-  //           color: hsl.darken(var(--color-global),10%);
-  //         }
-  //       }
-  //     }
-
-  //     // List group
-  //     .list-group-item {
-  //       &.active {
-  //         background-color: hsl.lighten(var(--bgcolor-global),10%) !important;
-  //       }
-  //       &.list-group-item-action:hover {
-  //         background-color: hsl.lighten(var(--bgcolor-global),10%) !important;
-  //       }
-  //       .page-list-snippet {
-  //         color: hsl.darken(var(--color-global),10%);
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI ToC
-  //   */
-  //   .revision-toc-content {
-  //     ::marker {
-  //       color: hsl.lighten(var(--color-global),30%);
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI subnavigation
-  //   */
-  //   .grw-subnav {
-  //     background-color: var(--bgcolor-subnav);
-  //   }
-
-  //   .grw-subnav-fixed-container .grw-subnav {
-  //     background-color: hsl.alpha(var(--bgcolor-subnav),85%);
-  //   }
-
-  //   .grw-page-editor-mode-manager {
-  //     .btn-outline-primary {
-  //       &:hover {
-  //         color: var(--primary);
-  //         background-color: $gray-700;
-  //       }
-  //     }
-  //   }
-
-  //   // Search drop down
-  //   #search-typeahead-asynctypeahead {
-  //     background-color: var(--bgcolor-global);
-  //     .table {
-  //       background-color: transparent;
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI Sidebar
-  //   */
-  //   .grw-sidebar {
-  //     --gray-500: hsl(var(--gray-500-hs),var(--gray-500-l));
-  //     --gray-500-hs: 210,13%;
-  //     --gray-500-l: 61%;
-  //     // List
-  //     @include override-list-group-item(
-  //       $color-list,
-  //       $bgcolor-sidebar-list-group,
-  //       $color-list-hover,
-  //       $bgcolor-list-hover,
-  //       $color-list-active,
-  //       $bgcolor-list-active
-  //     );
-  //     // Pagetree
-  //     .grw-pagetree, .grw-foldertree {
-  //       @include override-list-group-item-for-pagetree(
-  //         var(--color-sidebar-context),
-  //         hsl.lighten(var(--bgcolor-sidebar-context),8%),
-  //         hsl.lighten(var(--bgcolor-sidebar-context),15%),
-  //         hsl.darken(var(--color-sidebar-context),15%),
-  //         hsl.darken(var(--color-sidebar-context),10%),
-  //         hsl.lighten(var(--bgcolor-sidebar-context),18%),
-  //         hsl.lighten(var(--bgcolor-sidebar-context),24%)
-  //       );
-  //       .grw-pagetree-triangle-btn, .grw-foldertree-triangle-btn {
-  //         @include mixins-buttons.button-outline-svg-icon-variant(var(--secondary), $gray-200);
-  //       }
-  //       .btn-page-item-control {
-  //         @include hsl-button.button-outline-variant(var(--gray-500), var(--gray-500), var(--secondary), transparent);
-  //         &:hover {
-  //           background-color: hsl.lighten(var(--bgcolor-sidebar-context),20%);
-  //         }
-  //         &:not(:disabled):not(.disabled):active,
-  //         &:not(:disabled):not(.disabled).active {
-  //           background-color: hsl.lighten(var(--bgcolor-sidebar-context),34%);
-  //         }
-  //         box-shadow: none !important;
-  //       }
-  //     }
-
-  //     // bookmarks
-  //     .grw-folder-tree-container {
-  //       .grw-drop-item-area , .grw-foldertree-item-container {
-  //         .grw-accept-drop-item {
-  //           border-color: hsl.lighten(var(--bgcolor-sidebar-context), 30%) !important;
-  //         }
-  //       }
-  //     }
-  //     .private-legacy-pages-link {
-  //       &:hover {
-  //         background: var(--bgcolor-list-hover);
-  //       }
-  //     }
-  //   }
-
-  //   .btn.btn-page-item-control {
-  //     @include hsl-button.button-outline-variant(var(--gray-500), var(--gray-500), var(--secondary), transparent);
-  //     &:hover {
-  //       background-color: $gray-700;
-  //     }
-  //     &:not(:disabled):not(.disabled):active,
-  //     &:not(:disabled):not(.disabled).active {
-  //       color: $gray-200;
-  //       background-color: $gray-600;
-  //     }
-  //     box-shadow: none !important;
-  //   }
-
-  //   // Bookmark item on user page
-  //   .grw-user-page-list-m {
-  //     @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
-  //     .grw-foldertree {
-  //       @include override-list-group-item-for-pagetree(
-  //         $body-color,
-  //         hsl.lighten($body-bg, 8%),
-  //         hsl.lighten($body-bg, 15%),
-  //         hsl.darken($body-color, 15%),
-  //         hsl.darken($body-color, 10%),
-  //         hsl.lighten($body-bg, 18%),
-  //         hsl.lighten($body-bg, 24%)
-  //       );
-  //       .grw-foldertree-triangle-btn {
-  //         @include mixins-buttons.button-outline-svg-icon-variant($secondary, $gray-200);
-  //       }
-  //     }
-  //     .grw-folder-tree-container {
-  //       .grw-drop-item-area , .grw-foldertree-item-container {
-  //         .grw-accept-drop-item {
-  //           border-color: hsl.lighten(var($body-bg), 30%) !important;
-  //         }
-  //       }
-  //     }
-  //   }
-
-  //   // Bookmark dropdown menu
-  //   .grw-bookmark-folder-dropdown  {
-  //     .grw-bookmark-folder-menu {
-  //       .form-control{
-  //         &:focus {
-  //           color: $body-color
-  //         }
-  //       }
-  //       .grw-bookmark-folder-menu-item  {
-  //         @include mixins-buttons.button-outline-svg-icon-variant($secondary, $gray-200);
-  //         .grw-bookmark-folder-menu-item-title {
-  //           color: $body-color
-  //         }
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * Popover
-  //   */
-  //   .popover {
-  //     background-color: var(--bgcolor-global);
-  //     border-color: var(--secondary);
-  //     .popover-header {
-  //       color: white;
-  //       background-color: var(--secondary);
-  //       border-color: var(--secondary);
-  //     }
-  //     .popover-body {
-  //       color: inherit;
-  //     }
-
-  // TODO: Check renamed .arrow to .popover-arrow
-  // see: https://getbootstrap.com/docs/5.2/migration/#popovers
-
-  //     &.bs-popover-top .arrow {
-  //       &::before {
-  //         border-top-color: var(--secondary);
-  //       }
-
-  //       &::after {
-  //         border-top-color: var(--bgcolor-global);
-  //       }
-  //     }
-  //     &.bs-popover-bottom .arrow {
-  //       &::before {
-  //         border-bottom-color: var(--secondary);
-  //       }
-
-  //       &::after {
-  //         border-bottom-color: var(--bgcolor-global);
-  //       }
-  //     }
-  //     &.bs-popover-right .arrow {
-  //       &::before {
-  //         border-right-color: var(--secondary);
-  //       }
-
-  //       &::after {
-  //         border-right-color: var(--bgcolor-global);
-  //       }
-  //     }
-  //     &.bs-popover-left .arrow {
-  //       &::before {
-  //         border-left-color: var(--secondary);
-  //       }
-
-  //       &::after {
-  //         border-left-color: var(--bgcolor-global);
-  //       }
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI Grid Edit Modal
-  //   */
-  //   .grw-grid-edit-preview {
-  //     background: $gray-900;
-  //   }
-
-  //   /*
-  //   * Slack
-  //   */
-  //   .grw-slack-notification {
-  //     background-color: transparent;
-  //     $color-slack: #4b144c;
-
-  //     .form-control {
-  //       background: var(--bgcolor-global);
-  //     }
-
-  //     .form-check-label {
-  //       &::before {
-  //         background-color: var(--secondary);
-  //         border-color: transparent;
-  //       }
-  //       &::after {
-  //         background-color: darken($color-slack, 5%);
-  //         background-image: url(/images/icons/slack/slack-logo-dark-off.svg);
-  //       }
-  //     }
-
-  //     .form-check-input:checked ~ .form-check-label {
-  //       &::before {
-  //         background-color: lighten($color-slack, 10%);
-  //       }
-  //       &::after {
-  //         background-color: darken($color-slack, 5%);
-  //         background-image: url(/images/icons/slack/slack-logo-dark-on.svg);
-  //       }
-  //     }
-  //     .grw-slack-logo svg {
-  //       fill: #dd80de;
-  //     }
-
-  //     .grw-btn-slack {
-  //       background-color: black;
-  //       &:focus,
-  //       &:hover {
-  //         background-color: black;
-  //       }
-  //     }
-
-  //     .grw-btn-slack-triangle {
-  //       color: var(--secondary);
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI HandsontableModal
-  //   */
-
-  //   .handsontable td {
-  //     color: black;
-  //   }
-
-  //   .grw-hot-modal-navbar {
-  //     background-color: var(--dark);
-  //   }
-
-  //   .wiki {
-  //     h1 {
-  //       border-color: hsl.lighten(var(--border-color-theme),10%);
-  //     }
-  //     h2 {
-  //       border-color: var(--border-color-theme);
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI comment form
-  //   */
-  //   .comment-form {
-  //     #slack-mark-black {
-  //       display: none;
-  //     }
-  //   }
-
-  //   .page-comment-form .comment-form-main {
-  //     &:before {
-  //       border-right-color: var(--bgcolor-global);
-  //     }
-  //   }
-
-  //   /*
-  //   * GROWI tags
-  //   */
-  //   .grw-tag-labels {
-  //     .grw-tag-label {
-  //       color: $color-tags;
-  //       background-color: $bgcolor-tags;
-  //     }
-  //   }
-
-  //   mark.rbt-highlight-text {
-  //     color: var(--color-global);
-  //   }
-
-  //   /*
-  //   * GROWI popular tags
-  //   */
-  //   .grw-popular-tag-labels {
-  //     .grw-tag-label {
-  //       color: $color-tags;
-  //       background-color: $bgcolor-tags;
-  //     }
-  //   }
-
-  //   /*
-  //   * admin settings
-  //   */
-  //   .admin-setting-header {
-  //     border-color: $border-color-global;
-  //   }
-
-  //   /*
-  //   * grw-side-contents
-  //   */
-  //   .grw-side-contents-sticky-container {
-  //     .grw-count-badge {
-  //       @include count-badge.count-badge($gray-400, $gray-700);
-  //     }
-
-  //     .grw-border-vr {
-  //       border-color: $border-color-toc;
-  //     }
-
-  //     .revision-toc {
-  //       border-color: $border-color-toc;
-  //     }
-  //   }
-
-  //   /*
-  //   * drawio
-  //   */
-  //   .drawio-viewer {
-  //     border-color: $border-color-global;
-  //   }
-
-  //   /*
-  //   * modal
-  //   */
-  //   .grw-modal-head {
-  //     border-color: $border-color-global;
-  //   }
-
-  //   /*
-  //   * skeleton
-  //   */
-  //   .grw-skeleton {
-  //     background-color: hsl.lighten(var(--bgcolor-subnav),10%);
-  //   }
-}

+ 0 - 534
apps/app/_obsolete/src/styles/theme/_apply-colors-light.scss

@@ -1,534 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as *;
-
-@use '../variables' as var;
-@use '../atoms/mixins/buttons' as mixins-buttons;
-@use './mixins/count-badge';
-@use './mixins/hsl-button';
-@use './hsl-functions' as hsl;
-
-// determine optional variables
-:root[data-bs-theme='light'] {
-  $color-list: var(--color-list,var(--color-global));
-  $bgcolor-list: var(--bgcolor-list,var(--bgcolor-global));
-  $color-list-hover: var(--color-list-hover,var(--color-global));
-  $bgcolor-list-hover: var(--bgcolor-list-hover, var(--bgcolor-global));
-  $bgcolor-list-active: var(--bgcolor-list-active, hsl.lighten(var(--primary),65%));
-  $color-list-active: var(--color-list-active,hsl(var(--primary-hs), clamp(10%, (100% - var(--primary-l)  - 65% - 51%) * 1000, 95%)));
-  $color-table: var(--color-table,var(--color-global));
-  $bgcolor-table: var(--bgcolor-table,null);
-  $border-color-table: var(--border-color-table,#{$gray-200});
-  $color-table-hover: var(--color-table-hover,var(--color-table));
-  $bgcolor-table-hover: var(--bgcolor-table-hover,rgba(black, 0.075));
-  $bgcolor-sidebar-list-group: var(--bgcolor-sidebar-list-group,var(--bgcolor-list));
-  $color-tags: var(--color-tags,var(--secondary));
-  $bgcolor-tags: var(--bgcolor-tags,#{$gray-200});
-  $border-color-global: var(--border-color-global,#{$gray-300});
-  $border-color-toc: var(--border-color-toc,#{$gray-300});
-  $color-dropdown: var(--color-dropdown,var(--color-global));
-  $bgcolor-dropdown: var(--color-dropdown,var(--bgcolor-global));
-  $color-dropdown-link: var(--color-dropdown-link,var(--color-global));
-  $color-dropdown-link-hover: var(--color-dropdown-link-hover,var(--color-global));
-  $color-dropdown-link-active: var(--color-dropdown-link-active,var(--color-reversal));
-  $bgcolor-dropdown-link-hover: hsl.darken(var(--bgcolor-global),15%);
-  $bgcolor-dropdown-link-active: var(--bgcolor-dropdown-link-active,var(--primary));
-  $body-bg: var(--bgcolor-global);
-  $body-color: var(--color-global);
-
-  // override bootstrap variables
-  $text-muted: $gray-500;
-  $table-color: $color-table;
-  $table-bg: $bgcolor-table;
-  $table-border-color: $border-color-table;
-  $table-hover-color: $color-table-hover;
-  $table-hover-bg: $bgcolor-table-hover;
-  $border-color: $border-color-global;
-  $dropdown-color: $color-dropdown;
-  $dropdown-link-color: $color-dropdown-link;
-  $dropdown-link-hover-color: $color-dropdown-link-hover;
-  $dropdown-link-hover-bg: $bgcolor-dropdown-link-hover;
-  $dropdown-link-active-color: $color-dropdown-link-active;
-  $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
-
-  @import './mixins/list-group';
-  // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-  // @import './reboot-bootstrap-text';
-  // @import './reboot-bootstrap-border-colors';
-  // @import './reboot-bootstrap-tables';
-  // @import './reboot-bootstrap-theme-colors';
-  // @import 'hsl-reboot-bootstrap-theme-colors';
-  // @import './reboot-bootstrap-dropdown';
-
-  // List Group
-  @include override-list-group-item(
-    $color-list,
-    $bgcolor-sidebar-list-group,
-    $color-list-hover,
-    $bgcolor-list-hover,
-    $color-list-active,
-    $bgcolor-list-active
-  );
-  /*
-  * Form
-  */
-  .form-control {
-    background-color: var(--bgcolor-global);
-  }
-
-  .form-control::placeholder {
-    color: hsl.darken(var(--bgcolor-global), 20%);
-  }
-
-  .form-control[disabled],
-  .form-control[readonly] {
-    color: hsl.lighten(var(--color-global),10%);
-    background-color: hsl.darken(var(--bgcolor-global),5%);
-  }
-
-  /*
-  * card
-  */
-  .card.card-disabled {
-    background-color: var(--bgcolor-global);
-    border-color: $gray-200;
-  }
-
-  /*
-  * GROWI Login form
-  */
-  .nologin {
-    // background color
-    $color-gradient: #3c465c;
-    background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
-      linear-gradient(135deg, var.$growi-green 10%, hsla(225, 95%, 50%, 0) 70%), linear-gradient(225deg, var.$growi-blue 10%, hsla(140, 90%, 50%, 0) 80%),
-      linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
-
-    .nologin-header {
-      background-color: rgba(white, 0.5);
-
-      .logo {
-        background-color: rgba(black, 0);
-        fill: rgba(black, 0.5);
-      }
-
-      h1 {
-        color: rgba(black, 0.5);
-      }
-    }
-
-    .nologin-dialog {
-      background-color: rgba(white, 0.5);
-      .link-switch {
-        color: #1939b8;
-        &:hover {
-          color: lighten(#1939b8,20%);
-        }
-      }
-    }
-
-    .dropdown-with-icon {
-      .dropdown-toggle {
-        color: white;
-        background-color: rgba($gray-600, 0.7);
-        box-shadow: unset;
-        &:focus {
-          color: white;
-          background-color: rgba($gray-600, 0.7);
-        }
-      }
-      i {
-        color: darken(white, 30%);
-        background-color: rgba($gray-700, 0.7);
-      }
-    }
-
-    .input-group {
-      .input-group-text {
-        color: darken(white, 30%);
-        background-color: rgba($gray-700, 0.7);
-      }
-
-      .form-control {
-        color: white;
-        background-color: rgba($gray-600, 0.7);
-        box-shadow: unset;
-
-        &::placeholder {
-          color: darken(white, 30%);
-        }
-      }
-    }
-
-    // footer link text
-    .link-growi-org {
-      color: rgba(black, 0.4);
-
-      &:hover,
-      &.focus {
-        color: black;
-
-        .growi {
-          color: darken(var.$growi-green, 20%);
-        }
-
-        .org {
-          color: darken(var.$growi-blue, 15%);
-        }
-      }
-    }
-  }
-
-  /*
-  * GROWI subnavigation
-  */
-  // .grw-subnav {
-  //   background-color: var(--bgcolor-subnav);
-  // }
-
-  .grw-subnav-fixed-container .grw-subnav {
-    background-color: hsl.alpha(var(--bgcolor-subnav),85%);
-  }
-
-  .grw-page-editor-mode-manager {
-    .btn-outline-primary {
-      &:hover {
-        color: var(--primary);
-        background-color: $gray-200;
-      }
-    }
-  }
-
-  .grw-drawer-toggler {
-    @include button-variant($light, $light);
-    @include mixins-buttons.button-svg-icon-variant($light, $light);
-    color: $gray-500;
-    box-shadow: none !important;
-  }
-
-  /**
-   * GROWI PagePathHierarchicalLink
-   */
-  // .grw-page-path-text-muted-container .grw-page-path-hierarchical-link a {
-  //   color: $gray-600;
-  // }
-  /*
-  * GROWI Sidebar
-  */
-  .grw-sidebar {
-    // List
-    @include override-list-group-item(
-      $color-list,
-      $bgcolor-sidebar-list-group,
-      $color-list-hover,
-      $bgcolor-list-hover,
-      $color-list-active,
-      $bgcolor-list-active
-    );
-    // sidebar-centent-bg
-    .grw-navigation-wrap {
-      // Drop a shadow on the light theme. The dark theme makes '$ bgcolor-sidebar-context' brighter than the body.
-      box-shadow: 0px 0px 3px rgba(black, 0.24);
-    }
-    // Pagetree
-    .grw-pagetree, .grw-foldertree {
-      @include override-list-group-item-for-pagetree(
-        var(--color-sidebar-context),
-        hsl.darken(var(--bgcolor-sidebar-context),5%),
-        hsl.darken(var(--bgcolor-sidebar-context),12%),
-        hsl.lighten(var(--color-sidebar-context),10%),
-        hsl.lighten(var(--color-sidebar-context),8%),
-        hsl.darken(var(--bgcolor-sidebar-context),15%),
-        hsl.darken(var(--bgcolor-sidebar-context),24%)
-      );
-
-      .grw-pagetree-triangle-btn, .grw-foldertree-triangle-btn {
-        @include mixins-buttons.button-outline-svg-icon-variant($gray-400, var(--primary));
-      }
-    }
-
-    // bookmark
-    .grw-folder-tree-container {
-      .grw-drop-item-area, .grw-foldertree-item-container  {
-        .grw-accept-drop-item {
-          border-color: hsl.darken(var(--bgcolor-sidebar-context), 30%) !important;
-        }
-      }
-    }
-
-    .private-legacy-pages-link {
-      &:hover {
-        background: $bgcolor-list-hover;
-      }
-    }
-  }
-
-  .btn.btn-page-item-control {
-    --gray-500: hsl(var(--gray-500-hs),var(--gray-500-l));
-    --gray-500-hs: 210,13%;
-    --gray-500-l: 61%;
-    @include hsl-button.button-outline-variant(var(--gray-500), var(--primary), #{hsl.lighten(var(--primary), 52%)}, transparent);
-    &:hover {
-      background-color: hsl.lighten(var(--primary), 58%);
-    }
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: var(--primary);
-    }
-    box-shadow: none !important;
-  }
-
-
-  // Bookmark item on user page
-  .grw-user-page-list-m {
-    @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
-    .grw-foldertree {
-      @include override-list-group-item-for-pagetree(
-        $body-color,
-        hsl.darken($body-bg, 5%),
-        hsl.darken($body-bg, 12%),
-        hsl.lighten($body-color, 10%),
-        hsl.lighten($body-color, 8%),
-        hsl.darken($body-bg, 15%),
-        hsl.darken($body-bg, 24%)
-      );
-      .grw-foldertree-triangle-btn {
-        @include mixins-buttons.button-outline-svg-icon-variant($gray-400, $primary);
-      }
-    }
-    .grw-folder-tree-container {
-      .grw-drop-item-area, .grw-foldertree-item-container  {
-        .grw-accept-drop-item {
-          border-color: hsl.darken(var($body-bg), 30%) !important;
-        }
-      }
-    }
-  }
-
-  // Bookmark dropdown menu
-  .grw-bookmark-folder-menu {
-    .form-control{
-      &:focus {
-        color: $body-color
-      }
-    }
-    .grw-bookmark-folder-menu-item {
-      @include mixins-buttons.button-outline-svg-icon-variant($gray-400, $primary);
-      .grw-bookmark-folder-menu-item-title {
-        color: $body-color
-      }
-    }
-  }
-
-  /*
-  * GROWI page list
-  */
-  .page-list {
-    .page-list-ul {
-      > li {
-        > span.page-list-meta {
-          color: hsl.lighten(var(--color-global),10%);
-        }
-      }
-    }
-    // List group
-    .list-group-item {
-      &.active {
-        background-color: hsl.lighten(var(--primary),77%) !important;
-      }
-      &.list-group-item-action:hover {
-        background-color: hsl.lighten(var(--primary),72%) !important;
-      }
-      .page-list-snippet {
-        color: hsl.lighten(var(--color-global),10%);
-      }
-    }
-  }
-
-  /*
-  * GROWI ToC
-  */
-  .revision-toc-content {
-    ::marker {
-      color: hsl.darken(var(--bgcolor-global),20%);
-    }
-  }
-
-  /*
-  * GROWI Editor
-  */
-  .grw-editor-navbar-bottom {
-    background-color: $gray-100;
-
-    #slack-mark-white {
-      display: none;
-    }
-
-    .input-group-text {
-      margin-right: 1px;
-      color: var(--secondary);
-      border-color: var(--light);
-    }
-
-    .btn.btn-outline-secondary {
-      border-color: $border-color;
-    }
-  }
-
-  /*
-  * GROWI Link Edit Modal
-  */
-  .link-edit-modal {
-    span i {
-      color: $gray-400;
-    }
-  }
-
-  /*
-  * GROWI Grid Edit Modal
-  */
-
-  .grw-grid-edit-preview {
-    background: $gray-100;
-  }
-
-  /*
-  * Slack
-  */
-  .grw-slack-notification {
-    background-color: white;
-    $color-slack: #4b144c;
-
-    .form-control {
-      background: white;
-    }
-
-    .form-check-label {
-      &::before {
-        background-color: $gray-200;
-        border-color: transparent;
-      }
-      &::after {
-        background-color: white;
-        background-image: url(/images/icons/slack/slack-logo-off.svg);
-      }
-    }
-    .form-check-input:checked ~ .form-check-label {
-      &::before {
-        background-color: lighten($color-slack, 60%);
-      }
-      &::after {
-        background-image: url(/images/icons/slack/slack-logo-on.svg);
-      }
-    }
-    .grw-slack-logo svg {
-      fill: #af30b0;
-    }
-
-    .grw-btn-slack {
-      background-color: white;
-
-      &:hover,
-      &:focus {
-        background-color: white;
-      }
-    }
-
-    .grw-btn-slack-triangle {
-      color: var(--secondary);
-    }
-  }
-
-  /*
-  * GROWI HandsontableModal
-  */
-  .grw-hot-modal-navbar {
-    background-color: var(--light);
-  }
-
-  .wiki {
-    h1 {
-      border-color: var(--border-color-theme);
-    }
-    h2 {
-      border-color: var(--border-color-theme);
-    }
-  }
-
-  /*
-  * GROWI comment form
-  */
-  .comment-form {
-    #slack-mark-white {
-      display: none;
-    }
-  }
-
-  .page-comment-form .comment-form-main {
-    &:before {
-      border-right-color: var(--bgcolor-global);
-    }
-  }
-
-  /*
-  * GROWI tags
-  */
-  .grw-tag-labels {
-    .grw-tag-label {
-      color: $color-tags;
-      background-color: $bgcolor-tags;
-    }
-  }
-
-  /*
-  * GROWI popular tags
-  */
-  .grw-popular-tag-labels {
-    .grw-tag-label {
-      color: $color-tags;
-      background-color: $bgcolor-tags;
-    }
-  }
-
-  /*
-  * grw-side-contents
-  */
-  .grw-side-contents-sticky-container {
-    .grw-count-badge {
-      @include count-badge.count-badge($gray-600, $gray-200);
-    }
-
-    .grw-border-vr {
-      border-color: $border-color-toc;
-    }
-    .revision-toc {
-      border-color: $border-color-toc;
-    }
-  }
-
-  /*
-  * drawio
-  */
-  .drawio-viewer {
-    border-color: $border-color-global;
-  }
-
-  /*
-  * admin settings
-  */
-  .admin-setting-header {
-    border-color: $border-color;
-  }
-
-  /*
-  * modal
-  */
-  .grw-modal-head {
-    border-color: $border-color-global;
-  }
-
-  /*
-  * skeleton
-  */
-  .grw-skeleton {
-    background-color: hsl.darken(var(--bgcolor-subnav),10%);
-  }
-}

+ 0 - 22
apps/app/_obsolete/src/styles/theme/_reboot-toastr-colors.scss

@@ -1,22 +0,0 @@
-.toast-success {
-  background-color: var(--success) !important;
-}
-
-.toast-error {
-  background-color: var(--danger) !important;
-}
-
-.toast-info {
-  background-color: var(--info) !important;
-}
-
-.toast-warning {
-  background-color: var(--warning) !important;
-}
-
-:root {
-  --toastify-color-info: var(--info);
-  --toastify-color-success: var(--success);
-  --toastify-color-warning: var(--warning);
-  --toastify-color-error: var(--danger);
-}

+ 0 - 11
apps/app/_obsolete/src/styles/theme/apply-colors.scss

@@ -561,17 +561,6 @@ ul.pagination {
   background: var.$growi-green;
   background: var.$growi-green;
 }
 }
 
 
-/*
- * GROWI comment
- */
-.page-comment-meta .page-comment-revision svg {
-  fill: var(--color-link);
-
-  &:hover {
-    fill: var(--color-link-hover);
-  }
-}
-
 /*
 /*
  * GROWI comment form
  * GROWI comment form
  */
  */

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

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 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)
+* [`7.0.1`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.1/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)

+ 0 - 1
apps/app/docker/codebuild/buildspec.yml

@@ -20,7 +20,6 @@ phases:
   build:
   build:
     commands:
     commands:
       - docker build -t ${IMAGE_TAG} -f ./apps/app/docker/Dockerfile .
       - docker build -t ${IMAGE_TAG} -f ./apps/app/docker/Dockerfile .
-      - docker tag ${IMAGE_TAG}
 
 
   post_build:
   post_build:
     commands:
     commands:

+ 4 - 3
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.0.1-RC.0",
+  "version": "7.0.2-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -177,7 +177,7 @@
     "react-markdown": "^8.0.7",
     "react-markdown": "^8.0.7",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-scroll": "^1.8.7",
-    "react-stickynode": "^4.1.0",
+    "react-stickynode": "^4.1.1",
     "react-syntax-highlighter": "^15.5.0",
     "react-syntax-highlighter": "^15.5.0",
     "react-toastify": "^9.1.3",
     "react-toastify": "^9.1.3",
     "react-use-ripple": "^1.5.2",
     "react-use-ripple": "^1.5.2",
@@ -234,6 +234,7 @@
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
     "@types/jest": "^29.5.2",
     "@types/jest": "^29.5.2",
     "@types/react-scroll": "^1.8.4",
     "@types/react-scroll": "^1.8.4",
+    "@types/react-stickynode": "^4.0.3",
     "@types/throttle-debounce": "^5.0.1",
     "@types/throttle-debounce": "^5.0.1",
     "@types/unzip-stream": "^0.3.4",
     "@types/unzip-stream": "^0.3.4",
     "@types/url-join": "^4.0.2",
     "@types/url-join": "^4.0.2",
@@ -243,7 +244,7 @@
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
     "bootstrap": "^5.3.1",
     "bootstrap": "^5.3.1",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
-    "diff2html": "^3.4.35",
+    "diff2html": "^3.4.47",
     "downshift": "^8.2.3",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
     "eazy-logger": "^3.1.0",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-cypress": "^2.12.1",

+ 4 - 2
apps/app/public/static/locales/en_US/commons.json

@@ -61,7 +61,8 @@
     "no_notification": "You don't have any notificatios.",
     "no_notification": "You don't have any notificatios.",
     "all": "All",
     "all": "All",
     "unopend": "Unread",
     "unopend": "Unread",
-    "mark_all_as_read": "Mark all as read"
+    "mark_all_as_read": "Mark all as read",
+    "only_unread": "Only unread"
   },
   },
 
 
   "personal_dropdown": {
   "personal_dropdown": {
@@ -93,7 +94,8 @@
     "Page URL": "Page URL",
     "Page URL": "Page URL",
     "Permanent link": "Permanent link",
     "Permanent link": "Permanent link",
     "Page path and permanent link": "Page path and permanent link",
     "Page path and permanent link": "Page path and permanent link",
-    "Markdown link": "Markdown link"
+    "Markdown link": "Markdown link",
+    "Append params": "Append params"
   },
   },
 
 
   "crop_image_modal": {
   "crop_image_modal": {

+ 18 - 3
apps/app/public/static/locales/en_US/translation.json

@@ -87,6 +87,7 @@
   "Go to this version": "View this version",
   "Go to this version": "View this version",
   "View diff": "View diff",
   "View diff": "View diff",
   "No diff": "No diff",
   "No diff": "No diff",
+  "Latest": "Latest",
   "User ID": "User ID",
   "User ID": "User ID",
   "User Information": "User information",
   "User Information": "User information",
   "User Activation": "User Activation",
   "User Activation": "User Activation",
@@ -126,6 +127,7 @@
   "Only me": "Only me",
   "Only me": "Only me",
   "Only inside the group": "Only inside the group",
   "Only inside the group": "Only inside the group",
   "page_list": "Page List",
   "page_list": "Page List",
+  "comments": "Comments",
   "Reselect the group": "Reselect the group",
   "Reselect the group": "Reselect the group",
   "Shareable link": "Shareable link",
   "Shareable link": "Shareable link",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
@@ -147,6 +149,7 @@
   "wide_view": "Wide View",
   "wide_view": "Wide View",
   "Recent Changes": "Recent Changes",
   "Recent Changes": "Recent Changes",
   "Page Tree": "Page Tree",
   "Page Tree": "Page Tree",
+  "Bookmarks": "Bookmarks",
   "In-App Notification": "Notifications",
   "In-App Notification": "Notifications",
   "original_path": "Original path",
   "original_path": "Original path",
   "new_path": "New path",
   "new_path": "New path",
@@ -173,6 +176,10 @@
   "custom_navigation": {
   "custom_navigation": {
     "no_pages_under_this_page": "There are no pages under this page."
     "no_pages_under_this_page": "There are no pages under this page."
   },
   },
+  "author_info": {
+    "created_at": "Created at",
+    "last_revision_posted_at": "Last revision posted at"
+  },
   "installer": {
   "installer": {
     "tab": "Create account",
     "tab": "Create account",
     "title": "Installer",
     "title": "Installer",
@@ -328,8 +335,15 @@
     "changes_not_saved": "Changes you made may not be saved. Are you sure you want to move?"
     "changes_not_saved": "Changes you made may not be saved. Are you sure you want to move?"
   },
   },
   "page_comment": {
   "page_comment": {
+    "comments": "Commments",
+    "comment": "Commment",
+    "preview": "Preview",
+    "write": "Write",
+    "add_a_comment": "Add a comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
-    "no_user_found": "No user found"
+    "no_user_found": "No user found",
+    "reply": "Reply",
+    "delete_comment": "Delete comment?"
   },
   },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "notfound_or_forbidden": "Original page is not found or forbidden.",
@@ -529,7 +543,8 @@
   },
   },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack Notification",
     "popover_title": "Slack Notification",
-    "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
+    "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list.",
+    "input_channels": "Input channels"
   },
   },
   "search_result": {
   "search_result": {
     "title": "Search",
     "title": "Search",
@@ -777,7 +792,7 @@
     "paths_recovered": "Paths recovered successfully",
     "paths_recovered": "Paths recovered successfully",
     "path_recovery_failed": "Path recovery failed"
     "path_recovery_failed": "Path recovery failed"
   },
   },
-  "footer": {
+  "user_home_page": {
     "bookmarks": "Bookmarks",
     "bookmarks": "Bookmarks",
     "recently_created": "Recently Created"
     "recently_created": "Recently Created"
   },
   },

+ 4 - 2
apps/app/public/static/locales/ja_JP/commons.json

@@ -63,7 +63,8 @@
     "no_notification": "通知はありません",
     "no_notification": "通知はありません",
     "all": "全て",
     "all": "全て",
     "unopend": "未読",
     "unopend": "未読",
-    "mark_all_as_read": "全て既読にする"
+    "mark_all_as_read": "全て既読にする",
+    "only_unread": "未読のみ"
   },
   },
 
 
   "personal_dropdown": {
   "personal_dropdown": {
@@ -95,7 +96,8 @@
     "Page URL": "ページURL",
     "Page URL": "ページURL",
     "Permanent link": "パーマリンク",
     "Permanent link": "パーマリンク",
     "Page path and permanent link": "ページ名とパーマリンク",
     "Page path and permanent link": "ページ名とパーマリンク",
-    "Markdown link": "マークダウン形式のリンク"
+    "Markdown link": "マークダウン形式のリンク",
+    "Append params": "パラメータの追加"
   },
   },
 
 
   "crop_image_modal": {
   "crop_image_modal": {

+ 19 - 4
apps/app/public/static/locales/ja_JP/translation.json

@@ -57,7 +57,7 @@
   "Timeline View": "タイムライン",
   "Timeline View": "タイムライン",
   "History": "更新履歴",
   "History": "更新履歴",
   "attachment_data": "添付データ",
   "attachment_data": "添付データ",
-  "No_attachments_yet": "No attachments yet.",
+  "No_attachments_yet": "添付データはありません",
   "Presentation Mode": "プレゼンテーション",
   "Presentation Mode": "プレゼンテーション",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Not available in this version": "このバージョンでは利用できません",
   "Not available in this version": "このバージョンでは利用できません",
@@ -83,6 +83,7 @@
   "Go to this version": "このバージョンを見る",
   "Go to this version": "このバージョンを見る",
   "View diff": "差分を表示",
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "No diff": "差分なし",
+  "Latest": "最新",
   "User ID": "ユーザーID",
   "User ID": "ユーザーID",
   "User Settings": "ユーザー設定",
   "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "User Information": "ユーザー情報",
@@ -125,6 +126,7 @@
   "Only me": "自分のみ",
   "Only me": "自分のみ",
   "Only inside the group": "特定グループのみ",
   "Only inside the group": "特定グループのみ",
   "page_list": "ページリスト",
   "page_list": "ページリスト",
+  "comments": "コメント",
   "Reselect the group": "グループの再選択",
   "Reselect the group": "グループの再選択",
   "Shareable link": "このページの共有用URL",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
@@ -148,6 +150,7 @@
   "wide_view": "ワイドビュー",
   "wide_view": "ワイドビュー",
   "Recent Changes": "最新の変更",
   "Recent Changes": "最新の変更",
   "Page Tree": "ページツリー",
   "Page Tree": "ページツリー",
+  "Bookmarks": "ブックマーク",
   "In-App Notification": "通知",
   "In-App Notification": "通知",
   "original_path": "元のパス",
   "original_path": "元のパス",
   "new_path": "新しいパス",
   "new_path": "新しいパス",
@@ -174,6 +177,10 @@
   "custom_navigation": {
   "custom_navigation": {
     "no_pages_under_this_page": "このページの配下にはページが存在しません。"
     "no_pages_under_this_page": "このページの配下にはページが存在しません。"
   },
   },
+  "author_info": {
+    "created_at": "作成日",
+    "last_revision_posted_at": "最終更新日"
+  },
   "installer": {
   "installer": {
     "tab": "アカウント作成",
     "tab": "アカウント作成",
     "title": "インストーラー",
     "title": "インストーラー",
@@ -361,8 +368,15 @@
     "changes_not_saved": "変更が保存されていない可能性があります。本当に移動しますか?"
     "changes_not_saved": "変更が保存されていない可能性があります。本当に移動しますか?"
   },
   },
   "page_comment": {
   "page_comment": {
+    "comments": "コメント",
+    "comment": "コメント",
+    "preview": "プレビュー",
+    "write": "入力",
+    "add_a_comment": "コメントを追加",
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
-    "no_user_found": "ユーザー名が見つかりません"
+    "no_user_found": "ユーザー名が見つかりません",
+    "reply": "返信",
+    "delete_comment": "コメントを削除しますか?"
   },
   },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
@@ -562,7 +576,8 @@
   },
   },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack 通知",
     "popover_title": "Slack 通知",
-    "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
+    "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。",
+    "input_channels": "チャンネル名"
   },
   },
   "search_result": {
   "search_result": {
     "title": "検索",
     "title": "検索",
@@ -810,7 +825,7 @@
     "paths_recovered": "パスを修復しました",
     "paths_recovered": "パスを修復しました",
     "path_recovery_failed": "パスを修復できませんでした"
     "path_recovery_failed": "パスを修復できませんでした"
   },
   },
-  "footer": {
+  "user_home_page": {
     "bookmarks": "ブックマーク",
     "bookmarks": "ブックマーク",
     "recently_created": "最近作成したページ"
     "recently_created": "最近作成したページ"
   },
   },

+ 4 - 2
apps/app/public/static/locales/zh_CN/commons.json

@@ -64,7 +64,8 @@
     "no_notification": "您没有任何通知",
     "no_notification": "您没有任何通知",
     "all": "全部",
     "all": "全部",
     "unopend": "未读",
     "unopend": "未读",
-    "mark_all_as_read" : "标记为已读"
+    "mark_all_as_read" : "标记为已读",
+    "only_unread": "Only unread"
   },
   },
 
 
   "personal_dropdown": {
   "personal_dropdown": {
@@ -96,7 +97,8 @@
 		"Page URL": "页面Url",
 		"Page URL": "页面Url",
 		"Parmanent link": "参数化链接",
 		"Parmanent link": "参数化链接",
 		"Page path and parmanent link": "页面路径及参数化链接",
 		"Page path and parmanent link": "页面路径及参数化链接",
-		"Markdown link": "Markdown链接"
+		"Markdown link": "Markdown链接",
+    "Append params": "Append params"
 	},
 	},
 
 
   "crop_image_modal": {
   "crop_image_modal": {

+ 18 - 3
apps/app/public/static/locales/zh_CN/translation.json

@@ -85,6 +85,7 @@
   "Go to this version": "查看此版本",
   "Go to this version": "查看此版本",
   "View diff": "查看差异",
   "View diff": "查看差异",
   "No diff": "无差异",
   "No diff": "无差异",
+  "Latest": "最新",
   "User ID": "用户ID",
   "User ID": "用户ID",
   "Home": "首页",
   "Home": "首页",
   "My Drafts": "My Drafts",
   "My Drafts": "My Drafts",
@@ -133,6 +134,7 @@
   "Only me": "只有我",
   "Only me": "只有我",
   "Only inside the group": "仅组内",
   "Only inside the group": "仅组内",
   "page_list": "Page List",
   "page_list": "Page List",
+  "comments": "Comments",
   "Reselect the group": "重新选择组",
   "Reselect the group": "重新选择组",
   "Shareable link": "可分享链接",
   "Shareable link": "可分享链接",
   "The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
   "The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
@@ -153,6 +155,7 @@
   "wide_view": "视野开阔",
   "wide_view": "视野开阔",
   "Recent Changes": "最新修改",
   "Recent Changes": "最新修改",
   "Page Tree": "页面树",
   "Page Tree": "页面树",
+  "Bookmarks": "书签",
   "In-App Notification": "通知",
   "In-App Notification": "通知",
   "original_path": "Original path",
   "original_path": "Original path",
   "new_path": "New path",
   "new_path": "New path",
@@ -180,6 +183,10 @@
   "custom_navigation": {
   "custom_navigation": {
     "no_pages_under_this_page": "There are no pages under this page."
     "no_pages_under_this_page": "There are no pages under this page."
   },
   },
+  "author_info": {
+    "created_at": "Created at",
+    "last_revision_posted_at": "Last revision posted at"
+  },
   "installer": {
   "installer": {
     "tab": "创建账户",
     "tab": "创建账户",
     "title": "安装",
     "title": "安装",
@@ -318,8 +325,15 @@
     "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
     "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
   },
   },
   "page_comment": {
   "page_comment": {
+    "comments": "评论",
+    "comment": "评论",
+    "preview": "预览",
+    "write": "输入",
+    "add_a_comment": "Add a comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
-    "no_user_found": "未找到用户名"
+    "no_user_found": "未找到用户名",
+    "reply": "Reply",
+    "delete_comment": "Delete comment?"
   },
   },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "未找到或禁止原始页。",
     "notfound_or_forbidden": "未找到或禁止原始页。",
@@ -516,7 +530,8 @@
   },
   },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack Notification",
     "popover_title": "Slack Notification",
-    "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
+    "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list.",
+    "input_channels": "Input channels"
   },
   },
   "share_links": {
   "share_links": {
     "Shere this page link to public": "Shere this page link to public",
     "Shere this page link to public": "Shere this page link to public",
@@ -780,7 +795,7 @@
     "paths_recovered": "成功恢复了页面路径",
     "paths_recovered": "成功恢复了页面路径",
     "path_recovery_failed": "路径恢复失败"
     "path_recovery_failed": "路径恢复失败"
   },
   },
-  "footer": {
+  "user_home_page": {
     "bookmarks": "书签",
     "bookmarks": "书签",
     "recently_created": "最近创建页面"
     "recently_created": "最近创建页面"
   },
   },

+ 7 - 0
apps/app/src/client/services/create-page/create-page.ts

@@ -0,0 +1,7 @@
+import { apiv3Post } from '~/client/util/apiv3-client';
+import type { IApiv3PageCreateParams, IApiv3PageCreateResponse } from '~/interfaces/apiv3';
+
+export const createPage = async(params: IApiv3PageCreateParams): Promise<IApiv3PageCreateResponse> => {
+  const res = await apiv3Post<IApiv3PageCreateResponse>('/page', params);
+  return res.data;
+};

+ 1 - 0
apps/app/src/client/services/create-page/index.ts

@@ -1,2 +1,3 @@
+export * from './create-page';
 export * from './use-create-page-and-transit';
 export * from './use-create-page-and-transit';
 export * from './use-create-template-page';
 export * from './use-create-template-page';

+ 3 - 1
apps/app/src/client/services/create-page/use-create-page-and-transit.tsx

@@ -2,12 +2,14 @@ import { useCallback, useState } from 'react';
 
 
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
-import { createPage, exist } from '~/client/services/page-operation';
+import { exist } from '~/client/services/page-operation';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { createPage } from './create-page';
+
 const logger = loggerFactory('growi:Navbar:GrowiContextualSubNavigation');
 const logger = loggerFactory('growi:Navbar:GrowiContextualSubNavigation');
 
 
 /**
 /**

+ 5 - 12
apps/app/src/client/services/page-operation.ts

@@ -4,12 +4,10 @@ import type { IPageHasId } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import type {
-  IApiv3PageCreateParams, IApiv3PageCreateResponse,
-} from '~/interfaces/apiv3';
 import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
 import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
 import {
 import {
-  useCurrentPageId, useSWRMUTxCurrentPage, useSWRxIsGrantNormalized, useSWRxApplicableGrant, useSWRxTagsInfo,
+  useCurrentPageId, useSWRMUTxCurrentPage, useSWRxApplicableGrant, useSWRxTagsInfo,
+  useSWRxCurrentGrantData,
 } from '~/stores/page';
 } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -93,11 +91,6 @@ export const resumeRenameOperation = async(pageId: string): Promise<void> => {
   await apiv3Post('/pages/resume-rename', { pageId });
   await apiv3Post('/pages/resume-rename', { pageId });
 };
 };
 
 
-export const createPage = async(params: IApiv3PageCreateParams): Promise<IApiv3PageCreateResponse> => {
-  const res = await apiv3Post<IApiv3PageCreateResponse>('/page', params);
-  return res.data;
-};
-
 export type UpdateStateAfterSaveOption = {
 export type UpdateStateAfterSaveOption = {
   supressEditingMarkdownMutation: boolean,
   supressEditingMarkdownMutation: boolean,
 }
 }
@@ -109,7 +102,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const { mutate: mutateGrant } = useSWRxIsGrantNormalized(pageId);
+  const { mutate: mutateCurrentGrantData } = useSWRxCurrentGrantData(pageId);
   const { mutate: mutateApplicableGrant } = useSWRxApplicableGrant(pageId);
   const { mutate: mutateApplicableGrant } = useSWRxApplicableGrant(pageId);
 
 
   // update swr 'currentPageId', 'currentPage', remote states
   // update swr 'currentPageId', 'currentPage', remote states
@@ -133,7 +126,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
       mutateEditingMarkdown(updatedPage.revision.body);
       mutateEditingMarkdown(updatedPage.revision.body);
     }
     }
 
 
-    await mutateGrant();
+    await mutateCurrentGrantData();
     await mutateApplicableGrant();
     await mutateApplicableGrant();
 
 
     const remoterevisionData = {
     const remoterevisionData = {
@@ -146,7 +139,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
     setRemoteLatestPageData(remoterevisionData);
     setRemoteLatestPageData(remoterevisionData);
   },
   },
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  [pageId, mutateTagsInfo, syncTagsInfoForEditor, mutateCurrentPageId, mutateCurrentPage, opts?.supressEditingMarkdownMutation, mutateGrant, mutateApplicableGrant, setRemoteLatestPageData, mutateEditingMarkdown]);
+  [pageId, mutateTagsInfo, syncTagsInfoForEditor, mutateCurrentPageId, mutateCurrentPage, opts?.supressEditingMarkdownMutation, mutateCurrentGrantData, mutateApplicableGrant, setRemoteLatestPageData, mutateEditingMarkdown]);
 };
 };
 
 
 export const unlink = async(path: string): Promise<void> => {
 export const unlink = async(path: string): Promise<void> => {

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

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

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

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

+ 1 - 1
apps/app/src/client/util/apiv3-client.ts

@@ -1,5 +1,5 @@
 // eslint-disable-next-line no-restricted-imports
 // eslint-disable-next-line no-restricted-imports
-import { AxiosResponse } from 'axios';
+import type { AxiosResponse } from 'axios';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
 // eslint-disable-next-line no-restricted-imports
 // eslint-disable-next-line no-restricted-imports

+ 8 - 12
apps/app/src/client/util/bookmark-utils.ts

@@ -1,18 +1,12 @@
 import type { IRevision, Ref } from '@growi/core';
 import type { IRevision, Ref } from '@growi/core';
 
 
-import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import type { BookmarkFolderItems, BookmarkedPage } from '~/interfaces/bookmark-info';
 
 
 import { apiv3Delete, apiv3Post, apiv3Put } from './apiv3-client';
 import { apiv3Delete, apiv3Post, apiv3Put } from './apiv3-client';
 
 
-// Check if bookmark folder item has children
-export const hasChildren = (item: BookmarkFolderItems | BookmarkFolderItems[]): boolean => {
-  if (item === null) {
-    return false;
-  }
-  if (Array.isArray(item)) {
-    return item.length > 0;
-  }
-  return item.children && item.children.length > 0;
+// Check if bookmark folder item has childFolder or bookmarks
+export const hasChildren = ({ childFolder, bookmarks }: { childFolder?: BookmarkFolderItems[], bookmarks?: BookmarkedPage[] }): boolean => {
+  return !!((childFolder && childFolder.length > 0) || (bookmarks && bookmarks.length > 0));
 };
 };
 
 
 // Add new folder helper
 // Add new folder helper
@@ -41,8 +35,10 @@ export const toggleBookmark = async(pageId: string, status: boolean): Promise<vo
 };
 };
 
 
 // Update Bookmark folder
 // Update Bookmark folder
-export const updateBookmarkFolder = async(bookmarkFolderId: string, name: string, parent: string | null, children: BookmarkFolderItems[]): Promise<void> => {
+export const updateBookmarkFolder = async(
+    bookmarkFolderId: string, name: string, parent: string | null, childFolder: BookmarkFolderItems[],
+): Promise<void> => {
   await apiv3Put('/bookmark-folder', {
   await apiv3Put('/bookmark-folder', {
-    bookmarkFolderId, name, parent, children,
+    bookmarkFolderId, name, parent, childFolder,
   });
   });
 };
 };

+ 2 - 1
apps/app/src/client/util/toastr.ts

@@ -1,4 +1,5 @@
-import { toast, ToastContent, ToastOptions } from 'react-toastify';
+import type { ToastContent, ToastOptions } from 'react-toastify';
+import { toast } from 'react-toastify';
 
 
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 

+ 4 - 4
apps/app/src/components/Admin/UserManagement.tsx

@@ -90,7 +90,7 @@ const UserManagement = (props: UserManagementProps) => {
           onChange={() => clickHandler(status)}
           onChange={() => clickHandler(status)}
         />
         />
         <label className="form-label form-check-label" htmlFor={`c_${status}`}>
         <label className="form-label form-check-label" htmlFor={`c_${status}`}>
-          <span className={`badge rounded-pill bg-${statusColor} d-inline-block vt mt-1`}>
+          <span className={`badge text-bg-${statusColor} d-inline-block vt mt-1`}>
             {statusLabel}
             {statusLabel}
           </span>
           </span>
         </label>
         </label>
@@ -167,11 +167,11 @@ const UserManagement = (props: UserManagementProps) => {
 
 
           <div className="offset-md-1 col-md-6 my-2">
           <div className="offset-md-1 col-md-6 my-2">
             <div>
             <div>
-              {renderCheckbox('all', 'All', 'secondary')}
+              {renderCheckbox('all', 'All', 'primary')}
               {renderCheckbox('registered', 'Approval Pending', 'info')}
               {renderCheckbox('registered', 'Approval Pending', 'info')}
               {renderCheckbox('active', 'Active', 'success')}
               {renderCheckbox('active', 'Active', 'success')}
-              {renderCheckbox('suspended', 'Suspended', 'warning text-dark')}
-              {renderCheckbox('invited', 'Invited', 'pink')}
+              {renderCheckbox('suspended', 'Suspended', 'warning')}
+              {renderCheckbox('invited', 'Invited', 'secondary')}
             </div>
             </div>
             <div>
             <div>
               { isNotifyCommentShow && <span className="text-warning">{t('admin:user_management.click_twice_same_checkbox')}</span> }
               { isNotifyCommentShow && <span className="text-warning">{t('admin:user_management.click_twice_same_checkbox')}</span> }

+ 9 - 9
apps/app/src/components/Admin/Users/UserTable.tsx

@@ -22,34 +22,34 @@ const UserTable = (props: UserTableProps) => {
   const { adminUsersContainer } = props;
   const { adminUsersContainer } = props;
 
 
   const getUserStatusLabel = (userStatus: number) => {
   const getUserStatusLabel = (userStatus: number) => {
-    let additionalClassName = 'bg-info';
+    let additionalClassName = 'text-bg-info';
     let text = 'Approval Pending';
     let text = 'Approval Pending';
 
 
     switch (userStatus) {
     switch (userStatus) {
       case 1:
       case 1:
-        additionalClassName = 'bg-info';
+        additionalClassName = 'text-bg-info';
         text = 'Approval Pending';
         text = 'Approval Pending';
         break;
         break;
       case 2:
       case 2:
-        additionalClassName = 'bg-success';
+        additionalClassName = 'text-bg-success';
         text = 'Active';
         text = 'Active';
         break;
         break;
       case 3:
       case 3:
-        additionalClassName = 'bg-warning text-dark';
+        additionalClassName = 'text-bg-warning';
         text = 'Suspended';
         text = 'Suspended';
         break;
         break;
       case 4:
       case 4:
-        additionalClassName = 'bg-danger';
+        additionalClassName = 'text-bg-danger';
         text = 'Deleted';
         text = 'Deleted';
         break;
         break;
       case 5:
       case 5:
-        additionalClassName = 'bg-pink';
+        additionalClassName = 'text-bg-secondary';
         text = 'Invited';
         text = 'Invited';
         break;
         break;
     }
     }
 
 
     return (
     return (
-      <span className={`badge rounded-pill ${additionalClassName}`}>
+      <span className={`badge ${additionalClassName}`}>
         {text}
         {text}
       </span>
       </span>
     );
     );
@@ -153,12 +153,12 @@ const UserTable = (props: UserTableProps) => {
                 <td>
                 <td>
                   {getUserStatusLabel(user.status)}
                   {getUserStatusLabel(user.status)}
                   {(user.admin) && (
                   {(user.admin) && (
-                    <span className="badge bg-indigo rounded-pill ms-2">
+                    <span className="badge text-bg-secondary ms-2">
                       {t('admin:user_management.user_table.administrator')}
                       {t('admin:user_management.user_table.administrator')}
                     </span>
                     </span>
                   )}
                   )}
                   {(user.readOnly) && (
                   {(user.readOnly) && (
-                    <span className="badge bg-light text-dark rounded-pill ms-2">
+                    <span className="badge text-bg-light ms-2">
                       {t('admin:user_management.user_table.read_only')}
                       {t('admin:user_management.user_table.read_only')}
                     </span>
                     </span>
                   )}
                   )}

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

@@ -4,6 +4,7 @@ import type { IUser } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
+import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
 
 
 
 
@@ -18,6 +19,7 @@ export type AuthorInfoProps = {
 }
 }
 
 
 export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
 export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
+  const { t } = useTranslation();
   const {
   const {
     date, user, mode = 'create', locate = 'subnav',
     date, user, mode = 'create', locate = 'subnav',
   } = props;
   } = props;
@@ -31,8 +33,8 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
     ? 'Created by'
     ? 'Created by'
     : 'Updated by';
     : 'Updated by';
   const infoLabelForFooter = mode === 'create'
   const infoLabelForFooter = mode === 'create'
-    ? 'Created at'
-    : 'Last revision posted at';
+    ? t('author_info.created_at')
+    : t('author_info.last_revision_posted_at');
   const userLabel = user != null
   const userLabel = user != null
     ? (
     ? (
       <Link href={pagePathUtils.userHomepagePath(user)} prefetch={false}>
       <Link href={pagePathUtils.userHomepagePath(user)} prefetch={false}>

+ 31 - 29
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -1,6 +1,5 @@
-import {
-  FC, useCallback, useState,
-} from 'react';
+import type { FC } from 'react';
+import { useCallback, useState } from 'react';
 
 
 import type { IPageToDeleteWithMeta } from '@growi/core';
 import type { IPageToDeleteWithMeta } from '@growi/core';
 import { DropdownToggle } from 'reactstrap';
 import { DropdownToggle } from 'reactstrap';
@@ -10,10 +9,9 @@ import {
 } from '~/client/util/bookmark-utils';
 } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { FolderIcon } from '~/components/Icons/FolderIcon';
 import { FolderIcon } from '~/components/Icons/FolderIcon';
-import {
-  BookmarkFolderItems, DragItemDataType, DragItemType, DRAG_ITEM_TYPE,
-} from '~/interfaces/bookmark-info';
-import { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
+import type { BookmarkFolderItems, DragItemDataType, DragItemType } from '~/interfaces/bookmark-info';
+import { DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
+import type { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
 
 
 import { BookmarkFolderItemControl } from './BookmarkFolderItemControl';
 import { BookmarkFolderItemControl } from './BookmarkFolderItemControl';
@@ -42,7 +40,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   } = props;
   } = props;
 
 
   const {
   const {
-    name, _id: folderId, children, parent, bookmarks,
+    name, _id: folderId, childFolder, parent, bookmarks,
   } = bookmarkFolder;
   } = bookmarkFolder;
 
 
   const [targetFolder, setTargetFolder] = useState<string | null>(folderId);
   const [targetFolder, setTargetFolder] = useState<string | null>(folderId);
@@ -52,7 +50,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
 
 
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
 
 
-  const childrenExists = hasChildren(children);
+  const childrenExists = hasChildren({ childFolder, bookmarks });
 
 
   const paddingLeft = BASE_FOLDER_PADDING * level;
   const paddingLeft = BASE_FOLDER_PADDING * level;
 
 
@@ -65,14 +63,14 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
     try {
     try {
       // TODO: do not use any type
       // TODO: do not use any type
-      await updateBookmarkFolder(folderId, folderName, parent as any, children);
+      await updateBookmarkFolder(folderId, folderName, parent as any, childFolder);
       bookmarkFolderTreeMutation();
       bookmarkFolderTreeMutation();
       setIsRenameAction(false);
       setIsRenameAction(false);
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkFolderTreeMutation, children, folderId, parent]);
+  }, [bookmarkFolderTreeMutation, childFolder, folderId, parent]);
 
 
   // Create new folder / subfolder handler
   // Create new folder / subfolder handler
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
@@ -99,7 +97,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
     if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
       try {
       try {
         if (item.bookmarkFolder != null) {
         if (item.bookmarkFolder != null) {
-          await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, bookmarkFolder._id, item.bookmarkFolder.children);
+          await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, bookmarkFolder._id, item.bookmarkFolder.childFolder);
           bookmarkFolderTreeMutation();
           bookmarkFolderTreeMutation();
         }
         }
       }
       }
@@ -129,7 +127,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
       // Maximum folder hierarchy of 2 levels
       // Maximum folder hierarchy of 2 levels
       // If the drop source folder has child folders, the drop source folder cannot be moved because the drop source folder hierarchy is already 2.
       // If the drop source folder has child folders, the drop source folder cannot be moved because the drop source folder hierarchy is already 2.
       // If the destination folder has a parent, the source folder cannot be moved because the destination folder hierarchy is already 2.
       // If the destination folder has a parent, the source folder cannot be moved because the destination folder hierarchy is already 2.
-      if (item.bookmarkFolder.children.length !== 0 || bookmarkFolder.parent != null) {
+      if (item.bookmarkFolder.childFolder.length !== 0 || bookmarkFolder.parent != null) {
         return false;
         return false;
       }
       }
 
 
@@ -142,9 +140,15 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     return true;
     return true;
   };
   };
 
 
+  const triangleBtnClassName = (isOpen: boolean, childrenExists: boolean): string => {
+    if (!childrenExists) {
+      return 'grw-foldertree-triangle-btn btn px-0 opacity-25';
+    }
+    return `grw-foldertree-triangle-btn btn px-0 ${isOpen ? 'grw-foldertree-open' : ''}`;
+  };
 
 
   const renderChildFolder = () => {
   const renderChildFolder = () => {
-    return isOpen && children?.map((childFolder) => {
+    return isOpen && childFolder?.map((childFolder) => {
       return (
       return (
         <div key={childFolder._id} className="grw-foldertree-item-children">
         <div key={childFolder._id} className="grw-foldertree-item-children">
           <BookmarkFolderItem
           <BookmarkFolderItem
@@ -201,13 +205,13 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
 
 
   const onClickMoveToRootHandlerForBookmarkFolderItemControl = useCallback(async() => {
   const onClickMoveToRootHandlerForBookmarkFolderItemControl = useCallback(async() => {
     try {
     try {
-      await updateBookmarkFolder(bookmarkFolder._id, bookmarkFolder.name, null, bookmarkFolder.children);
+      await updateBookmarkFolder(bookmarkFolder._id, bookmarkFolder.name, null, bookmarkFolder.childFolder);
       bookmarkFolderTreeMutation();
       bookmarkFolderTreeMutation();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkFolder._id, bookmarkFolder.children, bookmarkFolder.name, bookmarkFolderTreeMutation]);
+  }, [bookmarkFolder._id, bookmarkFolder.childFolder, bookmarkFolder.name, bookmarkFolderTreeMutation]);
 
 
   return (
   return (
     <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
     <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
@@ -221,22 +225,20 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
         isDropable={isDropable}
         isDropable={isDropable}
       >
       >
         <li
         <li
-          className="list-group-item list-group-item-action border-0 py-0 pe-3 d-flex align-items-center"
+          className="list-group-item list-group-item-action border-0 py-2 d-flex align-items-center rounded"
           onClick={loadChildFolder}
           onClick={loadChildFolder}
           style={{ paddingLeft }}
           style={{ paddingLeft }}
         >
         >
           <div className="grw-triangle-container d-flex justify-content-center">
           <div className="grw-triangle-container d-flex justify-content-center">
-            {childrenExists && (
-              <button
-                type="button"
-                className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
-                onClick={loadChildFolder}
-              >
-                <div className="d-flex justify-content-center">
-                  <span className="material-symbols-outlined">arrow_right</span>
-                </div>
-              </button>
-            )}
+            <button
+              type="button"
+              className={triangleBtnClassName(isOpen, childrenExists)}
+              onClick={loadChildFolder}
+            >
+              <div className="d-flex justify-content-center">
+                <span className="material-symbols-outlined">arrow_right</span>
+              </div>
+            </button>
           </div>
           </div>
           <div>
           <div>
             <FolderIcon isOpen={isOpen} />
             <FolderIcon isOpen={isOpen} />
@@ -249,7 +251,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
             />
             />
           ) : (
           ) : (
             <>
             <>
-              <div className="grw-foldertree-title-anchor ps-2">
+              <div className="grw-foldertree-title-anchor ps-1">
                 <p className="text-truncate m-auto ">{name}</p>
                 <p className="text-truncate m-auto ">{name}</p>
               </div>
               </div>
             </>
             </>

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -156,7 +156,7 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
                     isSelected={selectedItem === folder._id}
                     isSelected={selectedItem === folder._id}
                   />
                   />
                 </div>
                 </div>
-                {folder.children?.map(child => (
+                {folder.childFolder?.map(child => (
                   <div key={child._id}>
                   <div key={child._id}>
                     <div
                     <div
                       className="dropdown-item grw-bookmark-folder-menu-item grw-bookmark-folder-menu-item-folder-second list-group-item list-group-item-action"
                       className="dropdown-item grw-bookmark-folder-menu-item grw-bookmark-folder-menu-item-folder-second list-group-item list-group-item-action"

+ 4 - 4
apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -33,6 +33,9 @@ $grw-bookmark-item-padding-left: 35px;
       .grw-visible-on-hover {
       .grw-visible-on-hover {
         display: block;
         display: block;
       }
       }
+      .page-list-meta {
+        display: none;
+      }
     }
     }
 
 
     .grw-foldertree-triangle-btn {
     .grw-foldertree-triangle-btn {
@@ -54,10 +57,7 @@ $grw-bookmark-item-padding-left: 35px;
 
 
   .grw-foldertree-item-container {
   .grw-foldertree-item-container {
     .grw-triangle-container {
     .grw-triangle-container {
-      // TODO: ignore width frickering
-      // https://redmine.weseek.co.jp/issues/130828
-      // min-width: 35px;
-      height: 40px;
+      height:30px;
     }
     }
 
 
     .grw-bookmark-item-list{
     .grw-bookmark-item-list{

+ 2 - 2
apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
-import { OnDeletedFunction } from '~/interfaces/ui';
+import type { OnDeletedFunction } from '~/interfaces/ui';
 import {
 import {
   useSWRxUserBookmarks, useSWRMUTxCurrentUserBookmarks,
   useSWRxUserBookmarks, useSWRMUTxCurrentUserBookmarks,
 } from '~/stores/bookmark';
 } from '~/stores/bookmark';
@@ -103,7 +103,7 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <div className={`grw-folder-tree-container ${styles['grw-folder-tree-container']}`}>
     <div className={`grw-folder-tree-container ${styles['grw-folder-tree-container']}`}>
-      <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group px-2 py-2`}>
+      <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group py-2`}>
         {bookmarkFolders?.map((bookmarkFolder) => {
         {bookmarkFolders?.map((bookmarkFolder) => {
           return (
           return (
             <BookmarkFolderItem
             <BookmarkFolderItem

+ 3 - 3
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -39,7 +39,7 @@ type Props = {
 
 
 export const BookmarkItem = (props: Props): JSX.Element => {
 export const BookmarkItem = (props: Props): JSX.Element => {
   const BASE_FOLDER_PADDING = 15;
   const BASE_FOLDER_PADDING = 15;
-  const BASE_BOOKMARK_PADDING = 20;
+  const BASE_BOOKMARK_PADDING = 16;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const router = useRouter();
   const router = useRouter();
@@ -56,7 +56,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
   const dPagePath = new DevidedPagePath(bookmarkedPage.path, false, true);
   const dPagePath = new DevidedPagePath(bookmarkedPage.path, false, true);
   const { latter: pageTitle, former: formerPagePath } = dPagePath;
   const { latter: pageTitle, former: formerPagePath } = dPagePath;
   const bookmarkItemId = `bookmark-item-${bookmarkedPage._id}`;
   const bookmarkItemId = `bookmark-item-${bookmarkedPage._id}`;
-  const paddingLeft = BASE_BOOKMARK_PADDING + (BASE_FOLDER_PADDING * (level + 1));
+  const paddingLeft = BASE_BOOKMARK_PADDING + (BASE_FOLDER_PADDING * (level));
   const dragItem: Partial<DragItemDataType> = {
   const dragItem: Partial<DragItemDataType> = {
     ...bookmarkedPage, parentFolder,
     ...bookmarkedPage, parentFolder,
   };
   };
@@ -148,7 +148,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
       useDragMode={isOperable}
       useDragMode={isOperable}
     >
     >
       <li
       <li
-        className="grw-bookmark-item-list list-group-item list-group-item-action border-0 py-0 me-auto d-flex align-items-center"
+        className="grw-bookmark-item-list list-group-item list-group-item-action border-0 py-0 pe-1 me-auto d-flex align-items-center rounded"
         key={bookmarkedPage._id}
         key={bookmarkedPage._id}
         id={bookmarkItemId}
         id={bookmarkItemId}
         style={{ paddingLeft }}
         style={{ paddingLeft }}

+ 5 - 2
apps/app/src/components/Comments.tsx

@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef } from 'react';
 
 
 import type { IRevisionHasId } from '@growi/core';
 import type { IRevisionHasId } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
@@ -13,7 +14,6 @@ import { useCurrentUser } from '../stores/context';
 
 
 import type { CommentEditorProps } from './PageComment/CommentEditor';
 import type { CommentEditorProps } from './PageComment/CommentEditor';
 
 
-
 const { isTopPage } = pagePathUtils;
 const { isTopPage } = pagePathUtils;
 
 
 
 
@@ -33,6 +33,8 @@ export const Comments = (props: CommentsProps): JSX.Element => {
     pageId, pagePath, revision, onLoaded,
     pageId, pagePath, revision, onLoaded,
   } = props;
   } = props;
 
 
+  const { t } = useTranslation('');
+
   const { mutate } = useSWRxPageComment(pageId);
   const { mutate } = useSWRxPageComment(pageId);
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
   const { data: isDeleted } = useIsTrashPage();
   const { data: isDeleted } = useIsTrashPage();
@@ -69,7 +71,8 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   };
   };
 
 
   return (
   return (
-    <div className="page-comments-row mt-5 py-4 border-top border-3 d-edit-none d-print-none">
+    <div className="page-comments-row mt-5 py-4 border-top d-edit-none d-print-none">
+      <h4 className="mb-3">{t('page_comment.comments')}</h4>
       <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
       <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
         <PageComment
         <PageComment
           pageId={pageId}
           pageId={pageId}

+ 2 - 1
apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx

@@ -120,6 +120,7 @@ export const CopyDropdown = (props) => {
 
 
         <DropdownMenu
         <DropdownMenu
           strategy="fixed"
           strategy="fixed"
+          container="body"
         >
         >
           <div className="d-flex align-items-center justify-content-between">
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
             <DropdownItem header className="px-3">
@@ -134,7 +135,7 @@ export const CopyDropdown = (props) => {
                   checked={isParamsAppended}
                   checked={isParamsAppended}
                   onChange={toggleAppendParams}
                   onChange={toggleAppendParams}
                 />
                 />
-                <label className="form-label form-check-label small" htmlFor={customSwitchForParamsId}>Append params</label>
+                <label className="form-label form-check-label small" htmlFor={customSwitchForParamsId}>{ t('copy_to_clipboard.Append params') }</label>
               </div>
               </div>
             ) }
             ) }
           </div>
           </div>

+ 18 - 9
apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss

@@ -14,21 +14,13 @@
     z-index: bs.$zindex-sticky;
     z-index: bs.$zindex-sticky;
   }
   }
 
 
+  // TODO:Responsive font size
   // set smaller font-size when sticky
   // set smaller font-size when sticky
   .sticky-inner-wrapper.active {
   .sticky-inner-wrapper.active {
     h1 {
     h1 {
       font-size: 1.75rem !important;
       font-size: 1.75rem !important;
     }
     }
   }
   }
-  // avoid sticky-top nav to turnate page path
-  .is-collapse-with-top {
-    @include bs.media-breakpoint-down(md) {
-      max-width: calc(100% - 350px);
-    }
-    @include bs.media-breakpoint-up(md) {
-      max-width: calc(100% - 500px);
-    }
-  }
 }
 }
 
 
 .grw-page-path-nav :global {
 .grw-page-path-nav :global {
@@ -36,3 +28,20 @@
     @include btn-muted.colorize(bs.$orange);
     @include btn-muted.colorize(bs.$orange);
   }
   }
 }
 }
+
+// == Colors
+.grw-former-link :global {
+  .separator {
+    opacity: 0.75;
+  }
+}
+
+.grw-former-link a {
+  --bs-link-opacity: 0.75;
+
+  &:global {
+    &:hover {
+      --bs-link-opacity: 1;
+    }
+  }
+}

+ 67 - 20
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -1,5 +1,8 @@
-import type { FC } from 'react';
-import React from 'react';
+import React, {
+  useEffect,
+  useRef,
+  useState,
+} from 'react';
 
 
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
@@ -7,6 +10,9 @@ import dynamic from 'next/dynamic';
 import Sticky from 'react-stickynode';
 import Sticky from 'react-stickynode';
 
 
 import { useIsNotFound } from '~/stores/page';
 import { useIsNotFound } from '~/stores/page';
+import {
+  usePageControlsX, useCurrentProductNavWidth, useSidebarMode,
+} from '~/stores/ui';
 
 
 import LinkedPagePath from '../../../models/linked-page-path';
 import LinkedPagePath from '../../../models/linked-page-path';
 import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
 import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
@@ -25,18 +31,19 @@ type Props = {
   isCollapseParents?: boolean,
   isCollapseParents?: boolean,
   formerLinkClassName?: string,
   formerLinkClassName?: string,
   latterLinkClassName?: string,
   latterLinkClassName?: string,
+  maxWidth?: number,
 }
 }
 
 
 const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
 const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
 
 
-const Separator = (): JSX.Element => {
-  return <span className={styles['grw-mx-02em']}>/</span>;
+const Separator = ({ className }: {className?: string}): JSX.Element => {
+  return <span className={`separator ${className ?? ''} ${styles['grw-mx-02em']}`}>/</span>;
 };
 };
 
 
-export const PagePathNav: FC<Props> = (props: Props) => {
+export const PagePathNav = (props: Props): JSX.Element => {
   const {
   const {
     pageId, pagePath, isWipPage, isSingleLineMode, isCollapseParents,
     pageId, pagePath, isWipPage, isSingleLineMode, isCollapseParents,
-    formerLinkClassName, latterLinkClassName,
+    formerLinkClassName, latterLinkClassName, maxWidth,
   } = props;
   } = props;
   const dPagePath = new DevidedPagePath(pagePath, false, true);
   const dPagePath = new DevidedPagePath(pagePath, false, true);
 
 
@@ -69,10 +76,10 @@ export const PagePathNav: FC<Props> = (props: Props) => {
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     formerLink = (
     formerLink = (
-      <div className="fs-5">
+      <>
         <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
         <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
         <Separator />
         <Separator />
-      </div>
+      </>
     );
     );
     latterLink = (
     latterLink = (
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
       <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
@@ -82,8 +89,8 @@ export const PagePathNav: FC<Props> = (props: Props) => {
   const copyDropdownId = `copydropdown-${pageId}`;
   const copyDropdownId = `copydropdown-${pageId}`;
 
 
   return (
   return (
-    <div>
-      <span className={formerLinkClassName}>{formerLink}</span>
+    <div style={{ maxWidth }}>
+      <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']}`}>{formerLink}</span>
       <div className="d-flex align-items-center">
       <div className="d-flex align-items-center">
         <h1 className={`m-0 ${latterLinkClassName}`}>
         <h1 className={`m-0 ${latterLinkClassName}`}>
           {latterLink}
           {latterLink}
@@ -103,25 +110,65 @@ export const PagePathNav: FC<Props> = (props: Props) => {
   );
   );
 };
 };
 
 
+PagePathNav.displayName = 'PagePathNav';
+
 
 
 type PagePathNavStickyProps = Omit<Props, 'isCollapseParents'>;
 type PagePathNavStickyProps = Omit<Props, 'isCollapseParents'>;
 
 
 export const PagePathNavSticky = (props: PagePathNavStickyProps): JSX.Element => {
 export const PagePathNavSticky = (props: PagePathNavStickyProps): JSX.Element => {
+
+  const { data: pageControlsX } = usePageControlsX();
+  const { data: sidebarWidth } = useCurrentProductNavWidth();
+  const { data: sidebarMode } = useSidebarMode();
+  const pagePathNavRef = useRef<HTMLDivElement>(null);
+
+  const [navMaxWidth, setNavMaxWidth] = useState<number | undefined>();
+
+  useEffect(() => {
+    if (pageControlsX == null || pagePathNavRef.current == null || sidebarWidth == null) {
+      return;
+    }
+    setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
+  }, [pageControlsX, pagePathNavRef, sidebarWidth]);
+
+  useEffect(() => {
+    // wait for the end of the animation of the opening and closing of the sidebar
+    const timeout = setTimeout(() => {
+      if (pageControlsX == null || pagePathNavRef.current == null || sidebarMode == null) {
+        return;
+      }
+      setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
+    }, 200);
+    return () => {
+      clearTimeout(timeout);
+    };
+  }, [pageControlsX, pagePathNavRef, sidebarMode]);
+
   return (
   return (
     // Controlling pointer-events
     // Controlling pointer-events
     //  1. disable pointer-events with 'pe-none'
     //  1. disable pointer-events with 'pe-none'
-    <Sticky className={`${styles['grw-page-path-nav-sticky']} mb-4`} innerClass="mt-1 pe-none" innerActiveClass="active">
-      {({ status }: { status: boolean }) => {
-        const isCollapseParents = status === Sticky.STATUS_FIXED;
-        return (
+    <div ref={pagePathNavRef}>
+      <Sticky className={`${styles['grw-page-path-nav-sticky']} mb-4`} innerClass="mt-1 pe-none" innerActiveClass="active">
+        {({ status }) => {
+          const isCollapseParents = status === Sticky.STATUS_FIXED;
+          return (
           // Controlling pointer-events
           // Controlling pointer-events
           //  2. enable pointer-events with 'pe-auto' only against the children
           //  2. enable pointer-events with 'pe-auto' only against the children
           //      which width is minimized by 'd-inline-block'
           //      which width is minimized by 'd-inline-block'
-          <div className={`d-inline-block pe-auto ${isCollapseParents ? 'is-collapse-with-top' : ''}`}>
-            <PagePathNav {...props} isCollapseParents={isCollapseParents} latterLinkClassName={isCollapseParents ? 'fs-3  text-truncate' : 'fs-2'} />
-          </div>
-        );
-      }}
-    </Sticky>
+          //
+            <div className="d-inline-block pe-auto">
+              <PagePathNav
+                {...props}
+                isCollapseParents={isCollapseParents}
+                latterLinkClassName={isCollapseParents ? 'fs-3  text-truncate' : 'fs-2'}
+                maxWidth={isCollapseParents ? navMaxWidth : undefined}
+              />
+            </div>
+          );
+        }}
+      </Sticky>
+    </div>
   );
   );
 };
 };
+
+PagePathNavSticky.displayName = 'PagePathNavSticky';

+ 2 - 1
apps/app/src/components/CompleteUserRegistration.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 

+ 2 - 2
apps/app/src/components/ContentLinkButtons.tsx

@@ -13,7 +13,7 @@ const BookMarkLinkButton = React.memo(() => {
         className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100"
         className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100"
       >
       >
         <span className="material-symbols-outlined p-0">bookmark</span>
         <span className="material-symbols-outlined p-0">bookmark</span>
-        <span>{t('footer.bookmarks')}</span>
+        <span>{t('user_home_page.bookmarks')}</span>
       </button>
       </button>
     </ScrollLink>
     </ScrollLink>
   );
   );
@@ -30,7 +30,7 @@ const RecentlyCreatedLinkButton = React.memo(() => {
         className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100"
         className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100"
       >
       >
         <span className="growi-custom-icons mx-1">recently_created</span>
         <span className="growi-custom-icons mx-1">recently_created</span>
-        <span>{t('footer.recently_created')}</span>
+        <span>{t('user_home_page.recently_created')}</span>
       </button>
       </button>
     </ScrollLink>
     </ScrollLink>
   );
   );

+ 1 - 1
apps/app/src/components/DescendantsPageListModal.tsx

@@ -75,7 +75,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
         expandWindow={() => setIsWindowExpanded(true)}
         expandWindow={() => setIsWindowExpanded(true)}
         contractWindow={() => setIsWindowExpanded(false)}
         contractWindow={() => setIsWindowExpanded(false)}
       />
       />
-      <button type="button" className="btn btn-close" onClick={close} aria-label="Close"></button>
+      <button type="button" className="btn btn-close ms-2" onClick={close} aria-label="Close"></button>
     </span>
     </span>
   ), [close, isWindowExpanded]);
   ), [close, isWindowExpanded]);
 
 

+ 8 - 0
apps/app/src/components/ExpandOrContractButton.module.scss

@@ -0,0 +1,8 @@
+.btn-expand-or-contract {
+  padding: 3px;
+  opacity: .5;
+
+  &:hover {
+    opacity: .75;
+  }
+}

+ 9 - 2
apps/app/src/components/ExpandOrContractButton.tsx

@@ -1,12 +1,17 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React from 'react';
 import React from 'react';
 
 
+import styles from './ExpandOrContractButton.module.scss';
+
 type Props = {
 type Props = {
   isWindowExpanded: boolean,
   isWindowExpanded: boolean,
   contractWindow?: () => void,
   contractWindow?: () => void,
   expandWindow?: () => void,
   expandWindow?: () => void,
 };
 };
 
 
+const moduleClass = styles['btn-expand-or-contract'] ?? '';
+
+
 const ExpandOrContractButton: FC<Props> = (props: Props) => {
 const ExpandOrContractButton: FC<Props> = (props: Props) => {
   const { isWindowExpanded, contractWindow, expandWindow } = props;
   const { isWindowExpanded, contractWindow, expandWindow } = props;
 
 
@@ -25,10 +30,12 @@ const ExpandOrContractButton: FC<Props> = (props: Props) => {
   return (
   return (
     <button
     <button
       type="button"
       type="button"
-      className="btn material-symbols-outlined"
+      className={`btn ${moduleClass}`}
       onClick={isWindowExpanded ? clickContractButtonHandler : clickExpandButtonHandler}
       onClick={isWindowExpanded ? clickContractButtonHandler : clickExpandButtonHandler}
     >
     >
-      {isWindowExpanded ? 'close_fullscreen' : 'open_in_full'}
+      <span className="material-symbols-outlined fw-bold">
+        {isWindowExpanded ? 'close_fullscreen' : 'open_in_full'}
+      </span>
     </button>
     </button>
   );
   );
 };
 };

+ 3 - 1
apps/app/src/components/FontFamily/use-growi-custom-icons.tsx

@@ -1,9 +1,11 @@
 import localFont from 'next/font/local';
 import localFont from 'next/font/local';
 
 
-import { DefineStyle } from './types';
+import type { DefineStyle } from './types';
 
 
 const growiCustomIconFont = localFont({
 const growiCustomIconFont = localFont({
   src: '../../../../../packages/custom-icons/dist/growi-custom-icons.woff2',
   src: '../../../../../packages/custom-icons/dist/growi-custom-icons.woff2',
+  adjustFontFallback: false,
+  display: 'block',
 });
 });
 
 
 export const useGrowiCustomIcon: DefineStyle = () => (
 export const useGrowiCustomIcon: DefineStyle = () => (

+ 1 - 1
apps/app/src/components/FontFamily/use-lato.tsx

@@ -1,6 +1,6 @@
 import { Lato } from 'next/font/google';
 import { Lato } from 'next/font/google';
 
 
-import { DefineStyle } from './types';
+import type { DefineStyle } from './types';
 
 
 const lato = Lato({
 const lato = Lato({
   weight: ['400', '700'],
   weight: ['400', '700'],

+ 2 - 1
apps/app/src/components/FontFamily/use-material-symbols-outlined.tsx

@@ -1,10 +1,11 @@
 import localFont from 'next/font/local';
 import localFont from 'next/font/local';
 
 
-import { DefineStyle } from './types';
+import type { DefineStyle } from './types';
 
 
 const materialSymbolsOutlined = localFont({
 const materialSymbolsOutlined = localFont({
   src: '../../../resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2',
   src: '../../../resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2',
   adjustFontFallback: false,
   adjustFontFallback: false,
+  display: 'block',
 });
 });
 
 
 export const useMaterialSymbolsOutlined: DefineStyle = () => (
 export const useMaterialSymbolsOutlined: DefineStyle = () => (

+ 1 - 1
apps/app/src/components/FontFamily/use-source-han-code-jp.tsx

@@ -1,6 +1,6 @@
 import localFont from 'next/font/local';
 import localFont from 'next/font/local';
 
 
-import { DefineStyle } from './types';
+import type { DefineStyle } from './types';
 
 
 const sourceHanCodeJPSubsetMain = localFont({
 const sourceHanCodeJPSubsetMain = localFont({
   src: '../../../resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2',
   src: '../../../resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2',

+ 2 - 2
apps/app/src/components/Icons/FolderIcon.tsx

@@ -9,10 +9,10 @@ export const FolderIcon = (props: Props): JSX.Element => {
   return (
   return (
     <>
     <>
       {!isOpen ? (
       {!isOpen ? (
-        <span className="material-symbols-outlined">folder_open</span>
+        <span className="material-symbols-outlined">folder</span>
 
 
       ) : (
       ) : (
-        <span className="material-symbols-outlined">folder</span>
+        <span className="material-symbols-outlined">folder_open</span>
       )
       )
       }
       }
     </>
     </>

+ 14 - 0
apps/app/src/components/Layout/RawLayout.module.scss

@@ -0,0 +1,14 @@
+.grw-toast-container {
+  --toastify-color-info: var(--bs-info);
+  --toastify-color-success: var(--bs-success);
+  --toastify-color-warning: var(--bs-warning);
+  --toastify-color-error: var(--bs-danger);
+  --toastify-icon-color-info: var(--toastify-color-success);
+  --toastify-icon-color-success: var(--toastify-color-success);
+  --toastify-icon-color-warning: var(--toastify-color-warning);
+  --toastify-icon-color-error: var(--toastify-color-error);
+  --toastify-color-progress-info: var(--toastify-color-success);
+  --toastify-color-progress-success: var(--toastify-color-success);
+  --toastify-color-progress-warning: var(--toastify-color-warning);
+  --toastify-color-progress-error: var(--toastify-color-error);
+}

+ 8 - 3
apps/app/src/components/Layout/RawLayout.tsx

@@ -1,6 +1,7 @@
-import React, { ReactNode, useState } from 'react';
+import type { ReactNode } from 'react';
+import React, { useState } from 'react';
 
 
-import { ColorScheme } from '@growi/core';
+import type { ColorScheme } from '@growi/core';
 import Head from 'next/head';
 import Head from 'next/head';
 import { ToastContainer } from 'react-toastify';
 import { ToastContainer } from 'react-toastify';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
@@ -9,6 +10,10 @@ import { useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
+import styles from './RawLayout.module.scss';
+
+const toastContainerClass = styles['grw-toast-container'] ?? '';
+
 const logger = loggerFactory('growi:cli:RawLayout');
 const logger = loggerFactory('growi:cli:RawLayout');
 
 
 
 
@@ -41,7 +46,7 @@ export const RawLayout = ({ children, className }: Props): JSX.Element => {
       <NextThemesProvider>
       <NextThemesProvider>
         <div className={classNames.join(' ')}>
         <div className={classNames.join(' ')}>
           {children}
           {children}
-          <ToastContainer theme={colorScheme} />
+          <ToastContainer className={toastContainerClass} theme={colorScheme} />
         </div>
         </div>
       </NextThemesProvider>
       </NextThemesProvider>
     </>
     </>

+ 44 - 0
apps/app/src/components/LoginForm/ExternalAuthButton.tsx

@@ -0,0 +1,44 @@
+import { useCallback } from 'react';
+
+import { IExternalAuthProviderType } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+
+const authIcon = {
+  [IExternalAuthProviderType.google]: <span className="growi-custom-icons align-bottom">google</span>,
+  [IExternalAuthProviderType.github]: <span className="growi-custom-icons align-bottom">github</span>,
+  [IExternalAuthProviderType.facebook]: <span className="growi-custom-icons align-bottom">facebook</span>,
+  [IExternalAuthProviderType.oidc]: <span className="growi-custom-icons align-bottom">openid</span>,
+  [IExternalAuthProviderType.saml]: <span className="material-symbols-outlined align-bottom">key</span>,
+};
+
+const authLabel = {
+  [IExternalAuthProviderType.google]: 'Google',
+  [IExternalAuthProviderType.github]: 'GitHub',
+  [IExternalAuthProviderType.facebook]: 'Facebook',
+  [IExternalAuthProviderType.oidc]: 'OIDC',
+  [IExternalAuthProviderType.saml]: 'SAML',
+};
+
+
+export const ExternalAuthButton = ({ authType }: {authType: IExternalAuthProviderType}): JSX.Element => {
+  const { t } = useTranslation();
+
+  const key = `btn-auth-${authType.toString()}`;
+  const btnClass = `btn-auth-${authType.toString()}`;
+
+  const handleLoginWithExternalAuth = useCallback(() => {
+    window.location.href = `/passport/${authType.toString()}`;
+  }, [authType]);
+
+  return (
+    <button
+      key={key}
+      type="button"
+      className={`btn btn-secondary ${btnClass} my-2 col-10 col-sm-7 mx-auto d-flex`}
+      onClick={handleLoginWithExternalAuth}
+    >
+      <span>{authIcon[authType]}</span>
+      <span className="flex-grow-1">{t('Sign in with External auth', { signin: authLabel[authType] })}</span>
+    </button>
+  );
+};

+ 9 - 6
apps/app/src/components/LoginForm.module.scss → apps/app/src/components/LoginForm/LoginForm.module.scss

@@ -3,12 +3,15 @@
 @use '~/styles/atoms/placeholders/buttons';
 @use '~/styles/atoms/placeholders/buttons';
 
 
 .login-form :global {
 .login-form :global {
-  // To adjust the behavior, this problem is not solved.
-  // See https://github.com/AaronCCWong/react-card-flip/issues/56
-  .react-card-front,
-  .react-card-back {
-    height: 0% !important;
-  }
+  //
+  // deactivated in order to fix https://redmine.weseek.co.jp/issues/143531 -- 2024.04.02 Yuki Takei
+  //
+  // // To adjust the behavior, this problem is not solved.
+  // // See https://github.com/AaronCCWong/react-card-flip/issues/56
+  // .react-card-front,
+  // .react-card-back {
+  //   height: 0% !important;
+  // }
 
 
   .collapse-external-auth {
   .collapse-external-auth {
     overflow: hidden;
     overflow: hidden;

+ 15 - 48
apps/app/src/components/LoginForm.tsx → apps/app/src/components/LoginForm/LoginForm.tsx

@@ -2,6 +2,7 @@ import React, {
   useState, useEffect, useCallback,
   useState, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
+import type { IExternalAuthProviderType } from '@growi/core';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
@@ -14,12 +15,15 @@ import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
-import { CompleteUserRegistration } from './CompleteUserRegistration';
+import { CompleteUserRegistration } from '../CompleteUserRegistration';
+
+import { ExternalAuthButton } from './ExternalAuthButton';
 
 
 import styles from './LoginForm.module.scss';
 import styles from './LoginForm.module.scss';
 
 
 const moduleClass = styles['login-form'];
 const moduleClass = styles['login-form'];
 
 
+
 type LoginFormProps = {
 type LoginFormProps = {
   username?: string,
   username?: string,
   name?: string,
   name?: string,
@@ -31,7 +35,7 @@ type LoginFormProps = {
   isLocalStrategySetup: boolean,
   isLocalStrategySetup: boolean,
   isLdapStrategySetup: boolean,
   isLdapStrategySetup: boolean,
   isLdapSetupFailed: boolean,
   isLdapSetupFailed: boolean,
-  objOfIsExternalAuthEnableds?: any,
+  enabledExternalAuthType?: IExternalAuthProviderType[],
   isMailerSetup?: boolean,
   isMailerSetup?: boolean,
   externalAccountLoginError?: IExternalAccountLoginError,
   externalAccountLoginError?: IExternalAccountLoginError,
 }
 }
@@ -42,10 +46,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
 
   const {
   const {
     isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
     isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
-    isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, objOfIsExternalAuthEnableds,
+    isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, enabledExternalAuthType,
   } = props;
   } = props;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
-  const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
+  const isSomeExternalAuthEnabled = enabledExternalAuthType != null && enabledExternalAuthType.length > 0;
 
 
   // states
   // states
   const [isRegistering, setIsRegistering] = useState(false);
   const [isRegistering, setIsRegistering] = useState(false);
@@ -81,12 +85,6 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     return t(key);
     return t(key);
   }, [t]);
   }, [t]);
 
 
-  const handleLoginWithExternalAuth = useCallback((e) => {
-    const auth = e.currentTarget.id;
-
-    window.location.href = `/passport/${auth}`;
-  }, []);
-
   const resetLoginErrors = useCallback(() => {
   const resetLoginErrors = useCallback(() => {
     if (loginErrors.length === 0) return;
     if (loginErrors.length === 0) return;
     setLoginErrors([]);
     setLoginErrors([]);
@@ -265,38 +263,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   ]);
   ]);
 
 
 
 
-  const renderExternalAuthInput = useCallback((auth) => {
-    const authIcon = {
-      google: <span className="growi-custom-icons align-bottom">google</span>,
-      github: <span className="growi-custom-icons align-bottom">github</span>,
-      facebook: <span className="growi-custom-icons align-bottom">facebook</span>,
-      oidc: <span className="growi-custom-icons align-bottom">openid</span>,
-      saml: <span className="material-symbols-outlined align-bottom">key</span>,
-    };
-    const authBtn = `btn-auth-${auth}`;
-    const signin = {
-      google: 'Google',
-      github: 'GitHub',
-      facebook: 'Facebook',
-      oidc: 'OIDC',
-      saml: 'SAML',
-    };
-
-    return (
-      <button
-        key={`btn-auth-${auth}`}
-        type="button"
-        className={`btn btn-secondary ${authBtn} my-2 col-10 col-sm-7 mx-auto d-flex`}
-        onClick={handleLoginWithExternalAuth}
-      >
-        <span>{authIcon[auth]}</span>
-        <span className="flex-grow-1">{t('Sign in with External auth', { signin: signin[auth] })}</span>
-      </button>
-    );
-  }, [handleLoginWithExternalAuth, t]);
-
   const renderExternalAuthLoginForm = useCallback(() => {
   const renderExternalAuthLoginForm = useCallback(() => {
-    const { objOfIsExternalAuthEnableds } = props;
+    const { enabledExternalAuthType } = props;
+
+    if (enabledExternalAuthType == null) {
+      return <></>;
+    }
 
 
     return (
     return (
       <>
       <>
@@ -304,16 +276,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
           <p className="text-white mb-0">{t('or')}</p>
           <p className="text-white mb-0">{t('or')}</p>
         </div>
         </div>
         <div className="mt-2">
         <div className="mt-2">
-          {Object.keys(objOfIsExternalAuthEnableds).map((auth) => {
-            if (!objOfIsExternalAuthEnableds[auth]) {
-              return;
-            }
-            return renderExternalAuthInput(auth);
-          })}
+          { enabledExternalAuthType.map(authType => <ExternalAuthButton authType={authType} />) }
         </div>
         </div>
       </>
       </>
     );
     );
-  }, [props, t, renderExternalAuthInput]);
+  }, [props, t]);
 
 
   const resetRegisterErrors = useCallback(() => {
   const resetRegisterErrors = useCallback(() => {
     if (registerErrors.length === 0) return;
     if (registerErrors.length === 0) return;

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

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

+ 9 - 0
apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss

@@ -1,4 +1,13 @@
 @use '~/styles/mixins';
 @use '~/styles/mixins';
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+.grw-contextual-sub-navigation {
+  @include bs.media-breakpoint-down(md) {
+    // set min-height to keep the height
+    //  even if the item is empty on the share link page
+    min-height: 46px;
+  }
+}
 
 
 @include mixins.editing() {
 @include mixins.editing() {
   .grw-contextual-sub-navigation {
   .grw-contextual-sub-navigation {

+ 68 - 46
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,4 +1,5 @@
-import React, { useState, useCallback } from 'react';
+import React, { useState, useCallback, useMemo } from 'react';
+
 
 
 import { isPopulated } from '@growi/core';
 import { isPopulated } from '@growi/core';
 import type {
 import type {
@@ -10,6 +11,7 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import Sticky from 'react-stickynode';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useShouldExpandContent } from '~/client/services/layout';
@@ -30,6 +32,7 @@ import { mutatePageTree } from '~/stores/page-listing';
 import {
 import {
   useEditorMode, useIsAbleToShowPageManagement,
   useEditorMode, useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode,
   useIsAbleToChangeEditorMode,
+  useIsDeviceLargerThanMd,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 import { CreateTemplateModal } from '../CreateTemplateModal';
 import { CreateTemplateModal } from '../CreateTemplateModal';
@@ -194,12 +197,16 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
   const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
+  const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
 
 
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId);
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId);
 
 
+  const [isStickyActive, setStickyActive] = useState(false);
+
+
   const path = currentPage?.path ?? currentPathname;
   const path = currentPage?.path ?? currentPathname;
   // const grant = currentPage?.grant ?? grantData?.grant;
   // const grant = currentPage?.grant ?? grantData?.grant;
   // const grantUserGroupId = currentPage?.grantedGroup?._id ?? grantData?.grantedGroup?.id;
   // const grantUserGroupId = currentPage?.grantedGroup?._id ?? grantData?.grantedGroup?.id;
@@ -288,53 +295,68 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     );
     );
   }, [isLinkSharingDisabled, isReadOnlyUser, pageId, revisionId]);
   }, [isLinkSharingDisabled, isReadOnlyUser, pageId, revisionId]);
 
 
+  // hide sub controls when sticky on mobile device
+  const hideSubControls = useMemo(() => {
+    return !isDeviceLargerThanMd && isStickyActive;
+  }, [isDeviceLargerThanMd, isStickyActive]);
+
   return (
   return (
     <>
     <>
-      <GroundGlassBar
-        className={`${styles['grw-contextual-sub-navigation']}
-          d-flex align-items-center justify-content-end px-2 px-sm-3 px-md-4 py-1 gap-2 gap-md-4 d-print-none
-        `}
-        data-testid="grw-contextual-sub-nav"
-        id="grw-contextual-sub-nav"
-      >
-        {pageId != null && (
-          <PageControls
-            pageId={pageId}
-            revisionId={revisionId}
-            shareLinkId={shareLinkId}
-            path={path ?? currentPathname} // If the page is empty, "path" is undefined
-            expandContentWidth={shouldExpandContent}
-            disableSeenUserInfoPopover={isSharedUser}
-            showPageControlDropdown={isAbleToShowPageManagement}
-            additionalMenuItemRenderer={additionalMenuItemsRenderer}
-            onClickDuplicateMenuItem={duplicateItemClickedHandler}
-            onClickRenameMenuItem={renameItemClickedHandler}
-            onClickDeleteMenuItem={deleteItemClickedHandler}
-            onClickSwitchContentWidth={switchContentWidthHandler}
-          />
-        )}
-
-        {isAbleToChangeEditorMode && (
-          <PageEditorModeManager
-            editorMode={editorMode}
-            isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
-            path={path}
-            // grant={grant}
-            // grantUserGroupId={grantUserGroupId}
-          />
-        )}
-
-        { isGuestUser && (
-          <div className="mt-2">
-            <Link href="/login#register" className="btn me-2" prefetch={false}>
-              <span className="material-symbols-outlined me-1">person_add</span>{t('Sign up')}
-            </Link>
-            <Link href="/login#login" className="btn btn-primary" prefetch={false}>
-              <span className="material-symbols-outlined me-1">login</span>{t('Sign in')}
-            </Link>
-          </div>
-        ) }
-      </GroundGlassBar>
+      <GroundGlassBar className="py-4 d-block d-md-none d-print-none border-bottom" />
+
+      <Sticky className="z-1" onStateChange={status => setStickyActive(status.status === Sticky.STATUS_FIXED)}>
+        <GroundGlassBar>
+
+          <nav
+            className={`${styles['grw-contextual-sub-navigation']}
+              d-flex align-items-center justify-content-end px-2 px-sm-3 px-md-4 py-1 gap-2 gap-md-4 d-print-none
+            `}
+            data-testid="grw-contextual-sub-nav"
+            id="grw-contextual-sub-nav"
+          >
+
+            {pageId != null && (
+              <PageControls
+                pageId={pageId}
+                revisionId={revisionId}
+                shareLinkId={shareLinkId}
+                path={path ?? currentPathname} // If the page is empty, "path" is undefined
+                expandContentWidth={shouldExpandContent}
+                disableSeenUserInfoPopover={isSharedUser}
+                hideSubControls={hideSubControls}
+                showPageControlDropdown={isAbleToShowPageManagement}
+                additionalMenuItemRenderer={additionalMenuItemsRenderer}
+                onClickDuplicateMenuItem={duplicateItemClickedHandler}
+                onClickRenameMenuItem={renameItemClickedHandler}
+                onClickDeleteMenuItem={deleteItemClickedHandler}
+                onClickSwitchContentWidth={switchContentWidthHandler}
+              />
+            )}
+
+            {isAbleToChangeEditorMode && (
+              <PageEditorModeManager
+                editorMode={editorMode}
+                isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
+                path={path}
+                // grant={grant}
+                // grantUserGroupId={grantUserGroupId}
+              />
+            )}
+
+            { isGuestUser && (
+              <div className="mt-2">
+                <Link href="/login#register" className="btn me-2" prefetch={false}>
+                  <span className="material-symbols-outlined me-1">person_add</span>{t('Sign up')}
+                </Link>
+                <Link href="/login#login" className="btn btn-primary" prefetch={false}>
+                  <span className="material-symbols-outlined me-1">login</span>{t('Sign in')}
+                </Link>
+              </div>
+            ) }
+          </nav>
+
+        </GroundGlassBar>
+      </Sticky>
 
 
       {path != null && currentUser != null && !isReadOnlyUser && (
       {path != null && currentUser != null && !isReadOnlyUser && (
         <CreateTemplateModal
         <CreateTemplateModal

+ 8 - 2
apps/app/src/components/Navbar/GrowiNavbarBottom.tsx

@@ -1,4 +1,6 @@
-import React from 'react';
+import React, {
+  useCallback,
+} from 'react';
 
 
 import { useSearchModal } from '~/features/search/client/stores/search';
 import { useSearchModal } from '~/features/search/client/stores/search';
 import { useIsSearchPage } from '~/stores/context';
 import { useIsSearchPage } from '~/stores/context';
@@ -19,6 +21,10 @@ export const GrowiNavbarBottom = (): JSX.Element => {
   const { data: isSearchPage } = useIsSearchPage();
   const { data: isSearchPage } = useIsSearchPage();
   const { open: openSearchModal } = useSearchModal();
   const { open: openSearchModal } = useSearchModal();
 
 
+  const searchButtonClickHandler = useCallback(() => {
+    openSearchModal();
+  }, [openSearchModal]);
+
   return (
   return (
     <GroundGlassBar className={`
     <GroundGlassBar className={`
       ${styles['grw-navbar-bottom']}
       ${styles['grw-navbar-bottom']}
@@ -54,7 +60,7 @@ export const GrowiNavbarBottom = (): JSX.Element => {
                 <a
                 <a
                   role="button"
                   role="button"
                   className="nav-link btn-lg"
                   className="nav-link btn-lg"
-                  onClick={openSearchModal}
+                  onClick={searchButtonClickHandler}
                 >
                 >
                   <span className="material-symbols-outlined fs-2">search</span>
                   <span className="material-symbols-outlined fs-2">search</span>
                 </a>
                 </a>

+ 1 - 1
apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx

@@ -75,7 +75,7 @@ export const PageAccessoriesModal = (): JSX.Element => {
         expandWindow={() => setIsWindowExpanded(true)}
         expandWindow={() => setIsWindowExpanded(true)}
         contractWindow={() => setIsWindowExpanded(false)}
         contractWindow={() => setIsWindowExpanded(false)}
       />
       />
-      <button type="button" className="btn btn-close" onClick={close} aria-label="Close"></button>
+      <button type="button" className="btn btn-close ms-2" onClick={close} aria-label="Close"></button>
     </span>
     </span>
   ), [close, isWindowExpanded]);
   ), [close, isWindowExpanded]);
 
 

+ 15 - 6
apps/app/src/components/PageAlert/FixPageGrantAlert.tsx

@@ -8,10 +8,10 @@ import {
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import type { IPageGrantData } from '~/interfaces/page';
+import { UserGroupPageGrantStatus, type IPageGrantData } from '~/interfaces/page';
 import type { PopulatedGrantedGroup, IRecordApplicableGrant, IResIsGrantNormalizedGrantData } from '~/interfaces/page-grant';
 import type { PopulatedGrantedGroup, IRecordApplicableGrant, IResIsGrantNormalizedGrantData } from '~/interfaces/page-grant';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
-import { useSWRxApplicableGrant, useSWRxIsGrantNormalized, useSWRxCurrentPage } from '~/stores/page';
+import { useSWRxApplicableGrant, useSWRxCurrentGrantData, useSWRxCurrentPage } from '~/stores/page';
 
 
 type ModalProps = {
 type ModalProps = {
   isOpen: boolean
   isOpen: boolean
@@ -99,10 +99,19 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
     }
     }
 
 
     if (grantData.grant === 5) {
     if (grantData.grant === 5) {
-      if (grantData.userRelatedGrantedGroups == null || grantData.userRelatedGrantedGroups.length === 0) {
-        return t('fix_page_grant.modal.grant_label.isForbidden');
+      const groupGrantData = grantData.groupGrantData;
+      if (groupGrantData != null) {
+        const userRelatedGrantedGroups = groupGrantData.userRelatedGroups.filter(group => group.status === UserGroupPageGrantStatus.isGranted);
+        if (userRelatedGrantedGroups.length > 0) {
+          const grantedGroupNames = [
+            ...userRelatedGrantedGroups.map(group => group.name),
+            ...groupGrantData.nonUserRelatedGrantedGroups.map(group => group.name),
+          ];
+          return `${t('fix_page_grant.modal.radio_btn.grant_group')} (${grantedGroupNames.join(', ')})`;
+        }
       }
       }
-      return `${t('fix_page_grant.modal.radio_btn.grant_group')} (${grantData.userRelatedGrantedGroups.map(g => g.name).join(', ')})`;
+
+      return t('fix_page_grant.modal.grant_label.isForbidden');
     }
     }
 
 
     throw Error('cannot get grant label'); // this error can't be throwed
     throw Error('cannot get grant label'); // this error can't be throwed
@@ -278,7 +287,7 @@ export const FixPageGrantAlert = (): JSX.Element => {
 
 
   const [isOpen, setOpen] = useState<boolean>(false);
   const [isOpen, setOpen] = useState<boolean>(false);
 
 
-  const { data: dataIsGrantNormalized } = useSWRxIsGrantNormalized(currentUser != null ? pageId : null);
+  const { data: dataIsGrantNormalized } = useSWRxCurrentGrantData(currentUser != null ? pageId : null);
   const { data: dataApplicableGrant } = useSWRxApplicableGrant(currentUser != null ? pageId : null);
   const { data: dataApplicableGrant } = useSWRxApplicableGrant(currentUser != null ? pageId : null);
 
 
   // Dependencies
   // Dependencies

+ 29 - 2
apps/app/src/components/PageComment.module.scss

@@ -9,8 +9,7 @@
 
 
   // reply button
   // reply button
   .btn-comment-reply {
   .btn-comment-reply {
-    margin-top: 0.5em;
-    border: none;
+    backdrop-filter: blur(10px);
   }
   }
 
 
   // TODO: Refacotr Soft-coding
   // TODO: Refacotr Soft-coding
@@ -21,3 +20,31 @@
     border: none;
     border: none;
   }
   }
 }
 }
+
+// // Light mode color
+@include bs.color-mode(light) {
+  .page-comment-styles :global {
+    .btn-comment-reply {
+      --bs-btn-color: #{( bs.$gray-600 )};
+      --bs-btn-hover-color:  #{( bs.$gray-700 )};
+      --bs-btn-bg: #{rgba( bs.$gray-200, 0.3 )};
+      --bs-btn-hover-bg: #{( bs.$gray-200 )};
+      --bs-btn-border-color: #{( bs.$gray-200 )};
+      --bs-btn-hover-border-color: #{( bs.$gray-300 )};
+    }
+  }
+}
+
+// dark mode color
+@include bs.color-mode(dark) {
+  .page-comment-styles :global {
+    .btn-comment-reply {
+      --bs-btn-color: #{( bs.$gray-500 )};
+      --bs-btn-hover-color:  #{( bs.$gray-400 )};
+      --bs-btn-bg: #{rgba( bs.$gray-800, 0.3 )};
+      --bs-btn-hover-bg: #{( bs.$gray-800 )};
+      --bs-btn-border-color: #{( bs.$gray-700 )};
+      --bs-btn-hover-border-color: #{( bs.$gray-600 )};
+    }
+  }
+}

+ 17 - 14
apps/app/src/components/PageComment.tsx

@@ -1,17 +1,19 @@
+import type { FC } from 'react';
 import React, {
 import React, {
-  FC, useState, useMemo, memo, useCallback,
+  useState, useMemo, memo, useCallback,
 } from 'react';
 } from 'react';
 
 
 import { isPopulated, getIdForRef, type IRevisionHasId } from '@growi/core';
 import { isPopulated, getIdForRef, type IRevisionHasId } from '@growi/core';
-import { Button } from 'reactstrap';
+import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
 
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
-import { RendererOptions } from '~/interfaces/renderer-options';
+import type { RendererOptions } from '~/interfaces/renderer-options';
 import { useSWRMUTxPageInfo } from '~/stores/page';
 import { useSWRMUTxPageInfo } from '~/stores/page';
 import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 
 
-import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
+import type { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
 import { useSWRxPageComment } from '../stores/comment';
 
 
 import { NotAvailableForGuest } from './NotAvailableForGuest';
 import { NotAvailableForGuest } from './NotAvailableForGuest';
@@ -49,6 +51,8 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   const [errorMessageOnDelete, setErrorMessageOnDelete] = useState<string>('');
   const [errorMessageOnDelete, setErrorMessageOnDelete] = useState<string>('');
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
 
 
+  const { t } = useTranslation('');
+
   const commentsFromOldest = useMemo(() => (comments != null ? [...comments].reverse() : null), [comments]);
   const commentsFromOldest = useMemo(() => (comments != null ? [...comments].reverse() : null), [comments]);
   const commentsExceptReply: ICommentHasIdList | undefined = useMemo(
   const commentsExceptReply: ICommentHasIdList | undefined = useMemo(
     () => commentsFromOldest?.filter(comment => comment.replyTo == null), [commentsFromOldest],
     () => commentsFromOldest?.filter(comment => comment.replyTo == null), [commentsFromOldest],
@@ -153,10 +157,10 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   return (
   return (
     <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
     <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
       <div className="page-comments">
       <div className="page-comments">
-        <div className="page-comments-list" id="page-comments-list">
+        <div className="page-comments-list mb-3" id="page-comments-list">
           {commentsExceptReply.map((comment) => {
           {commentsExceptReply.map((comment) => {
 
 
-            const defaultCommentThreadClasses = 'page-comment-thread pb-5';
+            const defaultCommentThreadClasses = 'page-comment-thread mb-2';
             const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
             const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
 
 
             let commentThreadClasses = '';
             let commentThreadClasses = '';
@@ -170,16 +174,15 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
                   <div className="d-flex flex-row-reverse">
                   <div className="d-flex flex-row-reverse">
                     <NotAvailableForGuest>
                     <NotAvailableForGuest>
                       <NotAvailableForReadOnlyUser>
                       <NotAvailableForReadOnlyUser>
-                        <Button
-                          data-testid="comment-reply-button"
-                          outline
-                          color="secondary"
-                          size="sm"
-                          className="btn-comment-reply"
+                        <button
+                          type="button"
+                          id="comment-reply-button"
+                          className="btn btn-secondary btn-comment-reply text-start w-100 ms-5"
                           onClick={() => onReplyButtonClickHandler(comment._id)}
                           onClick={() => onReplyButtonClickHandler(comment._id)}
                         >
                         >
-                          <span className="material-symbols-outlined">replay</span> Reply
-                        </Button>
+                          <UserPicture user={currentUser} noLink noTooltip additionalClassName="me-2" />
+                          <span className="material-symbols-outlined me-1 fs-5 pb-1">reply</span><small>{t('page_comment.reply')}...</small>
+                        </button>
                       </NotAvailableForReadOnlyUser>
                       </NotAvailableForReadOnlyUser>
                     </NotAvailableForGuest>
                     </NotAvailableForGuest>
                   </div>
                   </div>

+ 32 - 23
apps/app/src/components/PageComment/Comment.module.scss

@@ -4,11 +4,6 @@
 @use './_comment-inheritance';
 @use './_comment-inheritance';
 
 
 .comment-styles :global {
 .comment-styles :global {
-  .page-comment-writer {
-    @include bs.media-breakpoint-down(xs) {
-      height: 3.5em;
-    }
-  }
 
 
   .page-comment {
   .page-comment {
     position: relative;
     position: relative;
@@ -16,11 +11,9 @@
 
 
     scroll-margin-top: var.$grw-scroll-margin-top-in-view;
     scroll-margin-top: var.$grw-scroll-margin-top-in-view;
 
 
-    // user name
-    .page-comment-creator {
-      margin-top: -0.5em;
-      margin-bottom: 0.5em;
-      font-weight: bold;
+    // background
+    .bg-comment {
+      @extend %bg-comment;
     }
     }
 
 
     // user icon
     // user icon
@@ -31,14 +24,6 @@
     // comment section
     // comment section
     .page-comment-main {
     .page-comment-main {
       @extend %comment-section;
       @extend %comment-section;
-      @include bs.media-breakpoint-up(sm) {
-        margin-left: 4.5em;
-      }
-      @include bs.media-breakpoint-down(xs) {
-        &:before {
-          content: none;
-        }
-      }
 
 
       pointer-events: auto;
       pointer-events: auto;
 
 
@@ -57,8 +42,10 @@
 
 
     // comment body
     // comment body
     .page-comment-body {
     .page-comment-body {
-      margin-bottom: 0.5em;
       word-wrap: break-word;
       word-wrap: break-word;
+      .wiki p {
+        margin: 8px 0;
+      }
     }
     }
 
 
     // older comments
     // older comments
@@ -73,6 +60,14 @@
       }
       }
     }
     }
 
 
+    .page-comment-revision {
+      .material-symbols-outlined {
+        font-size: 16px;
+        vertical-align: middle;
+      }
+    }
+
+
     .page-comment-meta {
     .page-comment-meta {
       display: flex;
       display: flex;
       justify-content: flex-end;
       justify-content: flex-end;
@@ -81,10 +76,6 @@
       color: bs.$gray-400;
       color: bs.$gray-400;
     }
     }
 
 
-    .page-comment-revision svg {
-      width: 16px;
-      height: 16px;
-    }
   }
   }
 
 
   // TODO: Refacotr Soft-coding
   // TODO: Refacotr Soft-coding
@@ -98,3 +89,21 @@
     }
     }
   }
   }
 }
 }
+
+// // Light mode color
+@include bs.color-mode(light) {
+  .comment-styles :global {
+    .page-comment-revision {
+      color: bs.$gray-500;
+    }
+  }
+}
+
+// // Dark mode color
+@include bs.color-mode(dark) {
+  .comment-styles :global {
+    .page-comment-revision {
+      color: bs.$gray-600;
+    }
+  }
+}

+ 16 - 20
apps/app/src/components/PageComment/Comment.tsx

@@ -111,10 +111,6 @@ export const Comment = (props: CommentProps): JSX.Element => {
     deleteBtnClicked(comment);
     deleteBtnClicked(comment);
   };
   };
 
 
-  const renderText = (comment: string) => {
-    return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
-  };
-
   const commentBody = useMemo(() => {
   const commentBody = useMemo(() => {
     if (rendererOptions == null) {
     if (rendererOptions == null) {
       return <></>;
       return <></>;
@@ -151,24 +147,15 @@ export const Comment = (props: CommentProps): JSX.Element => {
         />
         />
       ) : (
       ) : (
         <div id={commentId} className={rootClassName}>
         <div id={commentId} className={rootClassName}>
-          <div className="page-comment-writer">
-            <UserPicture user={creator} />
-          </div>
-          <div className="page-comment-main">
-            <div className="page-comment-creator">
-              <Username user={creator} />
-            </div>
-            <div className="page-comment-body">{commentBody}</div>
-            <div className="page-comment-meta">
-              <Link href={`#${commentId}`} prefetch={false}>
+          <div className="page-comment-main bg-comment rounded mb-2">
+            <div className="d-flex align-items-center">
+              <UserPicture user={creator} additionalClassName="me-2" />
+              <div className="small fw-bold me-3">
+                <Username user={creator} />
+              </div>
+              <Link href={`#${commentId}`} prefetch={false} className="small page-comment-revision">
                 <FormattedDistanceDate id={commentId} date={comment.createdAt} />
                 <FormattedDistanceDate id={commentId} date={comment.createdAt} />
               </Link>
               </Link>
-              { isEdited && (
-                <>
-                  <span id={editedDateId}>&nbsp;(edited)</span>
-                  <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
-                </>
-              ) }
               <span className="ms-2">
               <span className="ms-2">
                 <Link
                 <Link
                   id={`page-comment-revision-${commentId}`}
                   id={`page-comment-revision-${commentId}`}
@@ -183,6 +170,15 @@ export const Comment = (props: CommentProps): JSX.Element => {
                 </UncontrolledTooltip>
                 </UncontrolledTooltip>
               </span>
               </span>
             </div>
             </div>
+            <div className="page-comment-body">{commentBody}</div>
+            <div className="page-comment-meta">
+              { isEdited && (
+                <>
+                  <span id={editedDateId}>&nbsp;(edited)</span>
+                  <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
+                </>
+              ) }
+            </div>
             { (isCurrentUserEqualsToAuthor() && !isReadOnly) && (
             { (isCurrentUserEqualsToAuthor() && !isReadOnly) && (
               <CommentControl
               <CommentControl
                 onClickDeleteBtn={deleteBtnClickedHandler}
                 onClickDeleteBtn={deleteBtnClickedHandler}

+ 2 - 2
apps/app/src/components/PageComment/CommentControl.tsx

@@ -13,13 +13,13 @@ export const CommentControl = (props: CommentControlProps): JSX.Element => {
   return (
   return (
     // The page-comment-control class is imported from Comment.module.scss
     // The page-comment-control class is imported from Comment.module.scss
     <div className="page-comment-control">
     <div className="page-comment-control">
-      <button type="button" className="btn btn-link p-2" onClick={onClickEditBtn}>
+      <button type="button" className="btn btn-link p-2 opacity-50" onClick={onClickEditBtn}>
         <span className="material-symbols-outlined">edit</span>
         <span className="material-symbols-outlined">edit</span>
       </button>
       </button>
       <button
       <button
         data-testid="comment-delete-button"
         data-testid="comment-delete-button"
         type="button"
         type="button"
-        className="btn btn-link p-2 me-2"
+        className="btn btn-link p-2 me-2 opacity-50"
         onClick={onClickDeleteBtn}
         onClick={onClickDeleteBtn}
       >
       >
         <span className="material-symbols-outlined">close</span>
         <span className="material-symbols-outlined">close</span>

+ 21 - 20
apps/app/src/components/PageComment/CommentEditor.module.scss

@@ -4,34 +4,35 @@
 
 
 // display cheatsheet for comment form only
 // display cheatsheet for comment form only
 .comment-editor-styles :global {
 .comment-editor-styles :global {
-  .cm-editor {
-    height: 300px !important;
-  }
-
   .comment-form {
   .comment-form {
     position: relative;
     position: relative;
-    margin-top: 1em;
+
+    // background
+    .bg-comment {
+      @extend %bg-comment
+    }
 
 
     // user icon
     // user icon
     .picture {
     .picture {
       @extend %picture;
       @extend %picture;
     }
     }
 
 
-    // seciton
-    .comment-form-main {
-      @extend %comment-section;
-      margin-left: 4.5em;
-      @include bs.media-breakpoint-down(xs) {
-        margin-left: 3.5em;
-      }
-    }
+  }
+}
 
 
-    // textarea
-    .comment-write {
-      margin-bottom: 0.5em;
-    }
-    .comment-form-preview {
-      padding-top: 0.5em;
-    }
+
+// adjust height
+.comment-editor-styles :global {
+  .cm-editor {
+    min-height: comment-inheritance.$codemirror-default-height !important;
+    max-height: #{2 * comment-inheritance.$codemirror-default-height};
+  }
+  .cm-gutters {
+    min-height: comment-inheritance.$codemirror-default-height !important;
+  }
+  .comment-preview-container {
+    min-height: page-editor-inheritance.$navbar-editor-height
+      + comment-inheritance.$codemirror-default-height;
+    padding-top: 0.5em;
   }
   }
 }
 }

+ 50 - 89
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -6,13 +6,14 @@ import {
   CodeMirrorEditorComment, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
   CodeMirrorEditorComment, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
 } from '@growi/editor';
 } from '@growi/editor';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import {
 import {
-  Button, TabContent, TabPane,
+  TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
+import { uploadAttachments } from '~/client/services/upload-attachments';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import type { IEditorMethods } from '~/interfaces/editor-methods';
 import type { IEditorMethods } from '~/interfaces/editor-methods';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
@@ -26,11 +27,12 @@ import { useCurrentPagePath } from '~/stores/page';
 import { useNextThemes } from '~/stores/use-next-themes';
 import { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 
 
 import { CommentPreview } from './CommentPreview';
 import { CommentPreview } from './CommentPreview';
+import { SwitchingButtonGroup } from './SwitchingButtonGroup';
+
 
 
 import '@growi/editor/dist/style.css';
 import '@growi/editor/dist/style.css';
 import styles from './CommentEditor.module.scss';
 import styles from './CommentEditor.module.scss';
@@ -41,18 +43,6 @@ const logger = loggerFactory('growi:components:CommentEditor');
 
 
 const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 
 
-
-const navTabMapping = {
-  comment_editor: {
-    Icon: () => <span className="material-symbols-outlined">edit_square</span>,
-    i18n: 'Write',
-  },
-  comment_preview: {
-    Icon: () => <span className="material-symbols-outlined">play_arrow</span>,
-    i18n: 'Preview',
-  },
-};
-
 export type CommentEditorProps = {
 export type CommentEditorProps = {
   pageId: string,
   pageId: string,
   isForNewComment?: boolean,
   isForNewComment?: boolean,
@@ -92,11 +82,13 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [comment, setComment] = useState(commentBody ?? '');
   const [comment, setComment] = useState(commentBody ?? '');
-  const [activeTab, setActiveTab] = useState('comment_editor');
+  const [showPreview, setShowPreview] = useState(false);
   const [error, setError] = useState();
   const [error, setError] = useState();
   const [slackChannels, setSlackChannels] = useState<string>('');
   const [slackChannels, setSlackChannels] = useState<string>('');
   const [incremented, setIncremented] = useState(false);
   const [incremented, setIncremented] = useState(false);
 
 
+  const { t } = useTranslation('');
+
   const editorRef = useRef<IEditorMethods>(null);
   const editorRef = useRef<IEditorMethods>(null);
 
 
   const router = useRouter();
   const router = useRouter();
@@ -113,8 +105,8 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     };
     };
   }, [onRouterChangeComplete, router.events]);
   }, [onRouterChangeComplete, router.events]);
 
 
-  const handleSelect = useCallback((activeTab: string) => {
-    setActiveTab(activeTab);
+  const handleSelect = useCallback((showPreview: boolean) => {
+    setShowPreview(showPreview);
   }, []);
   }, []);
 
 
   // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
   // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
@@ -140,7 +132,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     const editingCommentsNum = comment !== '' ? await decrementEditingCommentsNum() : undefined;
     const editingCommentsNum = comment !== '' ? await decrementEditingCommentsNum() : undefined;
 
 
     setComment('');
     setComment('');
-    setActiveTab('comment_editor');
+    setShowPreview(false);
     setError(undefined);
     setError(undefined);
     initializeSlackEnabled();
     initializeSlackEnabled();
     // reset value
     // reset value
@@ -209,40 +201,21 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
   // the upload event handler
   // the upload event handler
   const uploadHandler = useCallback((files: File[]) => {
   const uploadHandler = useCallback((files: File[]) => {
-    files.forEach(async(file) => {
-      try {
-        const { data: resLimit } = await apiv3Get('/attachment/limit', { fileSize: file.size });
-
-        if (!resLimit.isUploadable) {
-          throw new Error(resLimit.errorMessage);
-        }
-
-        const formData = new FormData();
-        formData.append('file', file);
-        if (pageId != null) {
-          formData.append('page_id', pageId);
-        }
-
-        const { data: resAdd } = await apiv3PostForm('/attachment', formData);
-
-        const attachment = resAdd.attachment;
+    uploadAttachments(pageId, files, {
+      onUploaded: (attachment) => {
         const fileName = attachment.originalName;
         const fileName = attachment.originalName;
 
 
-        let insertText = `[${fileName}](${attachment.filePathProxied})\n`;
-        // when image
-        if (attachment.fileFormat.startsWith('image/')) {
-          // modify to "![fileName](url)" syntax
-          insertText = `!${insertText}`;
-        }
+        const prefix = attachment.fileFormat.startsWith('image/')
+          ? '!' // use "![fileName](url)" syntax when image
+          : '';
+        const insertText = `${prefix}[${fileName}](${attachment.filePathProxied})\n`;
 
 
         codeMirrorEditor?.insertText(insertText);
         codeMirrorEditor?.insertText(insertText);
-      }
-      catch (e) {
-        logger.error('failed to upload', e);
-        toastError(e);
-      }
+      },
+      onError: (error) => {
+        toastError(error);
+      },
     });
     });
-
   }, [codeMirrorEditor, pageId]);
   }, [codeMirrorEditor, pageId]);
 
 
   const getCommentHtml = useCallback(() => {
   const getCommentHtml = useCallback(() => {
@@ -255,22 +228,24 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
   const renderBeforeReady = useCallback((): JSX.Element => {
   const renderBeforeReady = useCallback((): JSX.Element => {
     return (
     return (
-      <div className="text-center">
+      <div>
         <NotAvailableForGuest>
         <NotAvailableForGuest>
           <NotAvailableForReadOnlyUser>
           <NotAvailableForReadOnlyUser>
             <button
             <button
               type="button"
               type="button"
-              className="btn btn-lg btn-link"
+              className="btn btn-outline-primary w-100 text-start py-3"
               onClick={() => setIsReadyToUse(true)}
               onClick={() => setIsReadyToUse(true)}
               data-testid="open-comment-editor-button"
               data-testid="open-comment-editor-button"
             >
             >
-              <span className="material-symbols-outlined">comment</span> Add Comment
+              <UserPicture user={currentUser} noLink noTooltip additionalClassName="me-3" />
+              <span className="material-symbols-outlined me-1 fs-5">add_comment</span>
+              <small>{t('page_comment.add_a_comment')}...</small>
             </button>
             </button>
           </NotAvailableForReadOnlyUser>
           </NotAvailableForReadOnlyUser>
         </NotAvailableForGuest>
         </NotAvailableForGuest>
       </div>
       </div>
     );
     );
-  }, []);
+  }, [currentUser]);
 
 
   // const onChangeHandler = useCallback((newValue: string, isClean: boolean) => {
   // const onChangeHandler = useCallback((newValue: string, isClean: boolean) => {
   //   setComment(newValue);
   //   setComment(newValue);
@@ -304,33 +279,36 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
     const errorMessage = <span className="text-danger text-end me-2">{error}</span>;
     const errorMessage = <span className="text-danger text-end me-2">{error}</span>;
     const cancelButton = (
     const cancelButton = (
-      <Button
-        outline
-        color="danger"
-        size="xs"
-        className="btn btn-outline-danger rounded-pill"
+      <button
+        type="button"
+        className="btn btn-outline-neutral-secondary"
         onClick={cancelButtonClickedHandler}
         onClick={cancelButtonClickedHandler}
       >
       >
-        Cancel
-      </Button>
+        {t('Cancel')}
+      </button>
     );
     );
     const submitButton = (
     const submitButton = (
-      <Button
+      <button
+        type="button"
         data-testid="comment-submit-button"
         data-testid="comment-submit-button"
-        outline
-        color="primary"
-        className="btn btn-outline-primary rounded-pill"
+        className="btn btn-primary"
         onClick={postCommentHandler}
         onClick={postCommentHandler}
       >
       >
-        Comment
-      </Button>
+        {t('page_comment.comment')}
+      </button>
     );
     );
 
 
     return (
     return (
       <>
       <>
-        <div className="comment-write">
-          <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={handleSelect} hideBorderBottom />
-          <TabContent activeTab={activeTab}>
+        <div className="px-4 pt-3 pb-1">
+          <div className="d-flex justify-content-between align-items-center mb-2">
+            <div className="d-flex">
+              <UserPicture user={currentUser} noLink noTooltip />
+              <p className="ms-2 mb-0">{t('page_comment.add_a_comment')}</p>
+            </div>
+            <SwitchingButtonGroup showPreview={showPreview} onSelected={handleSelect} />
+          </div>
+          <TabContent activeTab={showPreview ? 'comment_preview' : 'comment_editor'}>
             <TabPane tabId="comment_editor">
             <TabPane tabId="comment_editor">
               <CodeMirrorEditorComment
               <CodeMirrorEditorComment
                 acceptedUploadFileType={acceptedUploadFileType}
                 acceptedUploadFileType={acceptedUploadFileType}
@@ -339,37 +317,23 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
                 onUpload={uploadHandler}
                 onUpload={uploadHandler}
                 editorSettings={editorSettings}
                 editorSettings={editorSettings}
               />
               />
-              {/* <Editor
-                ref={editorRef}
-                value={commentBody ?? ''} // DO NOT use state
-                isUploadable={isUploadable}
-                isUploadAllFileAllowed={isUploadAllFileAllowed}
-                onChange={onChangeHandler}
-                onUpload={uploadHandler}
-                onCtrlEnter={ctrlEnterHandler}
-                isComment
-              /> */}
-              {/*
-                Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
-                See a review comment in https://github.com/weseek/growi/pull/3473
-              */}
             </TabPane>
             </TabPane>
             <TabPane tabId="comment_preview">
             <TabPane tabId="comment_preview">
-              <div className="comment-form-preview">
+              <div className="comment-preview-container">
                 {commentPreview}
                 {commentPreview}
               </div>
               </div>
             </TabPane>
             </TabPane>
           </TabContent>
           </TabContent>
         </div>
         </div>
 
 
-        <div className="comment-submit">
+        <div className="comment-submit px-4 pb-3 mb-2">
           <div className="d-flex">
           <div className="d-flex">
             <span className="flex-grow-1" />
             <span className="flex-grow-1" />
             <span className="d-none d-sm-inline">{errorMessage && errorMessage}</span>
             <span className="d-none d-sm-inline">{errorMessage && errorMessage}</span>
 
 
             {isSlackConfigured && isSlackEnabled != null
             {isSlackConfigured && isSlackEnabled != null
               && (
               && (
-                <div className="align-self-center me-md-2">
+                <div className="align-self-center me-md-3">
                   <SlackNotification
                   <SlackNotification
                     isSlackEnabled={isSlackEnabled}
                     isSlackEnabled={isSlackEnabled}
                     slackChannels={slackChannels}
                     slackChannels={slackChannels}
@@ -398,10 +362,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   return (
   return (
     <div className={`${styles['comment-editor-styles']} form page-comment-form`}>
     <div className={`${styles['comment-editor-styles']} form page-comment-form`}>
       <div className="comment-form">
       <div className="comment-form">
-        <div className="comment-form-user">
-          <UserPicture user={currentUser} noLink noTooltip />
-        </div>
-        <div className="comment-form-main">
+        <div className="comment-form-main bg-comment rounded">
           {isReadyToUse
           {isReadyToUse
             ? renderReady()
             ? renderReady()
             : renderBeforeReady()
             : renderBeforeReady()

+ 0 - 7
apps/app/src/components/PageComment/CommentPreview.module.scss

@@ -1,9 +1,2 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-@use './comment-inheritance';
-@use '../PageEditor/page-editor-inheritance';
-
 .grw-comment-preview {
 .grw-comment-preview {
-  min-height: page-editor-inheritance.$navbar-editor-height
-    + comment-inheritance.$codemirror-default-height
-    + bs.$line-height-base;
 }
 }

+ 3 - 1
apps/app/src/components/PageComment/CommentPreview.tsx

@@ -5,6 +5,8 @@ import RevisionRenderer from '../Page/RevisionRenderer';
 
 
 import styles from './CommentPreview.module.scss';
 import styles from './CommentPreview.module.scss';
 
 
+const moduleClass = styles['grw-comment-preview'] ?? '';
+
 
 
 type CommentPreviewPorps = {
 type CommentPreviewPorps = {
   markdown: string,
   markdown: string,
@@ -21,7 +23,7 @@ export const CommentPreview = (props: CommentPreviewPorps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <div className={`grw-comment-preview ${styles['grw-comment-preview']}`}>
+    <div className={moduleClass}>
       <RevisionRenderer
       <RevisionRenderer
         rendererOptions={rendererOptions}
         rendererOptions={rendererOptions}
         markdown={markdown}
         markdown={markdown}

+ 9 - 5
apps/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -3,11 +3,13 @@ import React from 'react';
 import { isPopulated } from '@growi/core';
 import { isPopulated } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
+import { t } from 'i18next';
+import { useTranslation } from 'next-i18next';
 import {
 import {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { ICommentHasId } from '../../interfaces/comment';
+import type { ICommentHasId } from '../../interfaces/comment';
 import { Username } from '../User/Username';
 import { Username } from '../User/Username';
 
 
 import styles from './DeleteCommentModal.module.scss';
 import styles from './DeleteCommentModal.module.scss';
@@ -26,6 +28,8 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
     isShown, comment, errorMessage, cancelToDelete, confirmToDelete,
     isShown, comment, errorMessage, cancelToDelete, confirmToDelete,
   } = props;
   } = props;
 
 
+  const { t } = useTranslation();
+
   const headerContent = () => {
   const headerContent = () => {
     if (comment == null || isShown === false) {
     if (comment == null || isShown === false) {
       return <></>;
       return <></>;
@@ -33,7 +37,7 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
     return (
     return (
       <span>
       <span>
         <span className="material-symbols-outlined">delete_forever</span>
         <span className="material-symbols-outlined">delete_forever</span>
-        Delete comment?
+        {t('page_comment.delete_comment')}
       </span>
       </span>
     );
     );
   };
   };
@@ -58,7 +62,7 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
 
 
     return (
     return (
       <>
       <>
-        <UserPicture user={creator} size="xs" /> <strong><Username user={creator}></Username></strong> wrote on {commentDate}:
+        <UserPicture user={creator} size="xs" /> <strong className="me-2"><Username user={creator}></Username></strong>{commentDate}:
         <p className="card custom-card comment-body mt-2 p-2">{commentBodyElement}</p>
         <p className="card custom-card comment-body mt-2 p-2">{commentBodyElement}</p>
       </>
       </>
     );
     );
@@ -71,10 +75,10 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
     return (
     return (
       <>
       <>
         <span className="text-danger">{errorMessage}</span>&nbsp;
         <span className="text-danger">{errorMessage}</span>&nbsp;
-        <Button onClick={cancelToDelete}>Cancel</Button>
+        <Button onClick={cancelToDelete}>{t('Cancel')}</Button>
         <Button color="danger" onClick={confirmToDelete}>
         <Button color="danger" onClick={confirmToDelete}>
           <span className="material-symbols-outlined">delete_forever</span>
           <span className="material-symbols-outlined">delete_forever</span>
-          Delete
+          {t('Delete')}
         </Button>
         </Button>
       </>
       </>
     );
     );

+ 0 - 6
apps/app/src/components/PageComment/ReplyComments.module.scss

@@ -1,9 +1,3 @@
-/*
-* reply
-*/
-.page-comment-reply :global {
-  margin-top: 1em;
-}
 
 
 // remove margin after hidden replies
 // remove margin after hidden replies
 .page-comments-hidden-replies + .page-comment-reply :global {
 .page-comments-hidden-replies + .page-comment-reply :global {

+ 1 - 1
apps/app/src/components/PageComment/ReplyComments.tsx

@@ -40,7 +40,7 @@ export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
 
 
   const renderReply = (reply: ICommentHasId) => {
   const renderReply = (reply: ICommentHasId) => {
     return (
     return (
-      <div key={reply._id} className={`${styles['page-comment-reply']} ms-4 ms-sm-5 me-3`}>
+      <div key={reply._id} className={`${styles['page-comment-reply']} mt-2 ms-4 ms-sm-5`}>
         <Comment
         <Comment
           rendererOptions={rendererOptions}
           rendererOptions={rendererOptions}
           comment={reply}
           comment={reply}

+ 46 - 0
apps/app/src/components/PageComment/SwitchingButtonGroup.module.scss

@@ -0,0 +1,46 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+
+.btn-group-switching :global {
+  .btn {
+    --bs-btn-border-width: 2px;
+
+    width: 60px;
+    height: 38px;
+
+    @include bs.media-breakpoint-up(sm) {
+      width: auto;
+      height: 30px;
+    }
+  }
+}
+
+
+// == Colors
+@include bs.color-mode(light) {
+  .btn-group-switching :global {
+    .btn {
+      $bg: var(--bs-gray-500);
+
+      --bs-btn-border-color: #{$bg};
+      --bs-btn-hover-bg: var(--bs-gray-100);
+      --bs-btn-hover-border-color: #{$bg};
+      --bs-btn-active-color: white;
+      --bs-btn-active-bg: #{$bg};
+      --bs-btn-active-border-color: #{$bg};
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .btn-group-switching :global {
+    .btn {
+      $bg: var(--bs-gray-800);
+
+      --bs-btn-border-color: #{$bg};
+      --bs-btn-hover-bg: #{rgba(bs.$gray-600, 0.1)};
+      --bs-btn-hover-border-color: #{$bg};
+      --bs-btn-active-bg: #{$bg};
+      --bs-btn-active-border-color: #{$bg};
+    }
+  }
+}

+ 73 - 0
apps/app/src/components/PageComment/SwitchingButtonGroup.tsx

@@ -0,0 +1,73 @@
+import type { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
+import { memo } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import styles from './SwitchingButtonGroup.module.scss';
+
+const moduleClass = styles['btn-group-switching'] ?? '';
+
+
+type SwitchingButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
+    active?: boolean,
+}
+
+const SwitchingButton = memo((props: SwitchingButtonProps) => {
+  const {
+    active, className, children, onClick, ...rest
+  } = props;
+
+  return (
+    <button
+      type="button"
+      className={`btn btn-sm py-1 d-flex align-items-center justify-content-center
+        ${className}
+        ${active ? 'active' : ''}
+      `}
+      onClick={onClick}
+      {...rest}
+    >
+      {children}
+    </button>
+  );
+});
+
+
+type Props = {
+  showPreview: boolean,
+  onSelected?: (showPreview: boolean) => void,
+};
+
+export const SwitchingButtonGroup = (props: Props): JSX.Element => {
+
+  const { t } = useTranslation();
+
+  const {
+    showPreview, onSelected,
+  } = props;
+
+  return (
+    <div
+      className={`btn-group ${moduleClass}`}
+      role="group"
+    >
+      <SwitchingButton
+        active={showPreview}
+        className="ps-2 pe-3"
+        onClick={() => onSelected?.(true)}
+      >
+        <span className="material-symbols-outlined me-0">play_arrow</span>
+        <span className="d-none d-sm-inline">{t('page_comment.preview')}</span>
+      </SwitchingButton>
+      <SwitchingButton
+        active={!showPreview}
+        className="px-2"
+        onClick={() => onSelected?.(false)}
+      >
+        <span className="material-symbols-outlined me-1">edit_square</span>
+        <span className="d-none d-sm-inline">{t('page_comment.write')}</span>
+      </SwitchingButton>
+    </div>
+  );
+
+};

+ 19 - 8
apps/app/src/components/PageComment/_comment-inheritance.scss

@@ -22,15 +22,26 @@
 }
 }
 
 
 %picture {
 %picture {
-  float: left;
-  width: 3em;
-  height: 3em;
-  margin-top: 0.8em;
+  width: 1.2em;
+  height: 1.2em;
+}
+
+$codemirror-default-height: 300px;
 
 
-  @include bs.media-breakpoint-down(xs) {
-    width: 2em;
-    height: 2em;
+// // Light mode color
+@include bs.color-mode(light) {
+  %bg-comment {
+    background-color: rgba( bs.$gray-200, 0.5 );
+    border: 1px solid bs.$gray-200;
+    backdrop-filter: blur(10px);
   }
   }
 }
 }
 
 
-$codemirror-default-height: 300px;
+// // Dark mode color
+@include bs.color-mode(dark) {
+  %bg-comment {
+    background-color: rgba( bs.$gray-800, 0.3 );
+    border: 1px solid bs.$gray-700;
+    backdrop-filter: blur(10px);
+  }
+}

+ 1 - 1
apps/app/src/components/PageControls/BookmarkButtons.tsx

@@ -81,7 +81,7 @@ export const BookmarkButtons: FC<Props> = (props: Props) => {
           </span>
           </span>
         </DropdownToggle>
         </DropdownToggle>
       </BookmarkFolderMenu>
       </BookmarkFolderMenu>
-      <UncontrolledTooltip placement="top" data-testid="bookmark-button-tooltip" target="bookmark-dropdown-btn" fade={false}>
+      <UncontrolledTooltip data-testid="bookmark-button-tooltip" target="bookmark-dropdown-btn" fade={false}>
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
 
 

+ 1 - 1
apps/app/src/components/PageControls/LikeButtons.tsx

@@ -54,7 +54,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         <span className={`material-symbols-outlined ${isLiked ? 'fill' : ''}`}>favorite</span>
         <span className={`material-symbols-outlined ${isLiked ? 'fill' : ''}`}>favorite</span>
       </button>
       </button>
 
 
-      <UncontrolledTooltip data-testid="like-button-tooltip" placement="top" target="like-button" autohide={false} fade={false}>
+      <UncontrolledTooltip data-testid="like-button-tooltip" target="like-button" autohide={false} fade={false}>
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
 
 

+ 68 - 55
apps/app/src/components/PageControls/PageControls.tsx

@@ -1,4 +1,6 @@
-import React, { memo, useCallback, useMemo } from 'react';
+import React, {
+  memo, useCallback, useEffect, useMemo, useRef,
+} from 'react';
 
 
 import type {
 import type {
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta,
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta,
@@ -6,6 +8,7 @@ import type {
 import {
 import {
   isIPageInfoForEntity, isIPageInfoForOperation,
   isIPageInfoForEntity, isIPageInfoForOperation,
 } from '@growi/core';
 } from '@growi/core';
+import { useRect } from '@growi/ui/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
@@ -15,7 +18,9 @@ import {
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useTagEditModal, type IPageForPageDuplicateModal } from '~/stores/modal';
 import { useTagEditModal, type IPageForPageDuplicateModal } from '~/stores/modal';
-import { EditorMode, useEditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
+import {
+  EditorMode, useEditorMode, useIsDeviceLargerThanMd, usePageControlsX,
+} from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
 import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
@@ -49,10 +54,10 @@ const Tags = (props: TagsProps): JSX.Element => {
     <div className="grw-tag-labels-container d-flex align-items-center">
     <div className="grw-tag-labels-container d-flex align-items-center">
       <button
       <button
         type="button"
         type="button"
-        className="btn btn-link btn-edit-tags text-muted border border-secondary p-1 d-flex align-items-center"
+        className="btn btn-sm btn-outline-neutral-secondary"
         onClick={onClickEditTagsButton}
         onClick={onClickEditTagsButton}
       >
       >
-        <span className="material-symbols-outlined me-2">local_offer</span>
+        <span className="material-symbols-outlined me-1">local_offer</span>
         Tags
         Tags
       </button>
       </button>
     </div>
     </div>
@@ -94,7 +99,13 @@ const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
 
 
 
 
 type CommonProps = {
 type CommonProps = {
+  pageId: string,
+  shareLinkId?: string | null,
+  revisionId?: string | null,
+  path?: string | null,
+  expandContentWidth?: boolean,
   disableSeenUserInfoPopover?: boolean,
   disableSeenUserInfoPopover?: boolean,
+  hideSubControls?: boolean,
   showPageControlDropdown?: boolean,
   showPageControlDropdown?: boolean,
   forceHideMenuItems?: ForceHideMenuItems,
   forceHideMenuItems?: ForceHideMenuItems,
   additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
@@ -105,12 +116,7 @@ type CommonProps = {
 }
 }
 
 
 type PageControlsSubstanceProps = CommonProps & {
 type PageControlsSubstanceProps = CommonProps & {
-  pageId: string,
-  shareLinkId?: string | null,
-  revisionId?: string | null,
-  path?: string | null,
   pageInfo: IPageInfoForOperation,
   pageInfo: IPageInfoForOperation,
-  expandContentWidth?: boolean,
   onClickEditTagsButton: () => void,
   onClickEditTagsButton: () => void,
 }
 }
 
 
@@ -118,7 +124,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   const {
   const {
     pageInfo,
     pageInfo,
     pageId, revisionId, path, shareLinkId, expandContentWidth,
     pageId, revisionId, path, shareLinkId, expandContentWidth,
-    disableSeenUserInfoPopover, showPageControlDropdown, forceHideMenuItems, additionalMenuItemRenderer,
+    disableSeenUserInfoPopover, hideSubControls, showPageControlDropdown, forceHideMenuItems, additionalMenuItemRenderer,
     onClickEditTagsButton, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
     onClickEditTagsButton, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
   } = props;
   } = props;
 
 
@@ -132,6 +138,19 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
 
 
+  const { mutateAndSave: mutatePageControlsX } = usePageControlsX();
+
+  const pageControlsRef = useRef<HTMLDivElement>(null);
+  const [pageControlsRect] = useRect(pageControlsRef);
+
+  useEffect(() => {
+    if (pageControlsRect?.x == null) {
+      return;
+    }
+    mutatePageControlsX(pageControlsRect.x);
+  }, [pageControlsRect?.x, mutatePageControlsX]);
+
+
   // Put in a mixture of seenUserIds and likerIds data to make the cache work
   // Put in a mixture of seenUserIds and likerIds data to make the cache work
   const { data: usersList } = useSWRxUsersList([...likerIds, ...seenUserIds]);
   const { data: usersList } = useSWRxUsersList([...likerIds, ...seenUserIds]);
   const likers = usersList != null ? usersList.filter(({ _id }) => likerIds.includes(_id)).slice(0, 15) : [];
   const likers = usersList != null ? usersList.filter(({ _id }) => likerIds.includes(_id)).slice(0, 15) : [];
@@ -253,46 +272,52 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   const isViewMode = editorMode === EditorMode.View;
   const isViewMode = editorMode === EditorMode.View;
 
 
   return (
   return (
-    <div className={`grw-page-controls ${styles['grw-page-controls']} d-flex`} style={{ gap: '2px' }}>
+    <div className={`${styles['grw-page-controls']} hstack gap-2`} ref={pageControlsRef}>
       { isDeviceLargerThanMd && (
       { isDeviceLargerThanMd && (
         <SearchButton />
         <SearchButton />
       )}
       )}
+
       {revisionId != null && !isViewMode && _isIPageInfoForOperation && (
       {revisionId != null && !isViewMode && _isIPageInfoForOperation && (
         <Tags
         <Tags
           onClickEditTagsButton={onClickEditTagsButton}
           onClickEditTagsButton={onClickEditTagsButton}
         />
         />
       )}
       )}
-      {revisionId != null && _isIPageInfoForOperation && (
-        <SubscribeButton
-          status={pageInfo.subscriptionStatus}
-          onClick={subscribeClickhandler}
-        />
-      )}
-      {revisionId != null && _isIPageInfoForOperation && (
-        <LikeButtons
-          onLikeClicked={likeClickhandler}
-          sumOfLikers={sumOfLikers}
-          isLiked={isLiked}
-          likers={likers}
-        />
-      )}
-      {revisionId != null && _isIPageInfoForOperation && (
-        <BookmarkButtons
-          pageId={pageId}
-          isBookmarked={pageInfo.isBookmarked}
-          bookmarkCount={pageInfo.bookmarkCount}
-        />
-      )}
-      {revisionId != null && (
-        <SeenUserInfo
-          seenUsers={seenUsers}
-          sumOfSeenUsers={sumOfSeenUsers}
-          disabled={disableSeenUserInfoPopover}
-        />
+
+      { !hideSubControls && (
+        <div className="hstack gap-1">
+          {revisionId != null && _isIPageInfoForOperation && (
+            <SubscribeButton
+              status={pageInfo.subscriptionStatus}
+              onClick={subscribeClickhandler}
+            />
+          )}
+          {revisionId != null && _isIPageInfoForOperation && (
+            <LikeButtons
+              onLikeClicked={likeClickhandler}
+              sumOfLikers={sumOfLikers}
+              isLiked={isLiked}
+              likers={likers}
+            />
+          )}
+          {revisionId != null && _isIPageInfoForOperation && (
+            <BookmarkButtons
+              pageId={pageId}
+              isBookmarked={pageInfo.isBookmarked}
+              bookmarkCount={pageInfo.bookmarkCount}
+            />
+          )}
+          {revisionId != null && (
+            <SeenUserInfo
+              seenUsers={seenUsers}
+              sumOfSeenUsers={sumOfSeenUsers}
+              disabled={disableSeenUserInfoPopover}
+            />
+          ) }
+        </div>
       ) }
       ) }
+
       { showPageControlDropdown && _isIPageInfoForOperation && (
       { showPageControlDropdown && _isIPageInfoForOperation && (
         <PageItemControl
         <PageItemControl
-          alignEnd
           pageId={pageId}
           pageId={pageId}
           pageInfo={pageInfo}
           pageInfo={pageInfo}
           isEnableActions={!isGuestUser}
           isEnableActions={!isGuestUser}
@@ -309,18 +334,12 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   );
   );
 };
 };
 
 
-type PageControlsProps = CommonProps & {
-  pageId: string,
-  shareLinkId?: string | null,
-  revisionId?: string | null,
-  path?: string | null,
-  expandContentWidth?: boolean,
-};
+type PageControlsProps = CommonProps;
 
 
 export const PageControls = memo((props: PageControlsProps): JSX.Element => {
 export const PageControls = memo((props: PageControlsProps): JSX.Element => {
   const {
   const {
-    pageId, revisionId, path, shareLinkId, expandContentWidth,
-    onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
+    pageId, revisionId, shareLinkId,
+    ...rest
   } = props;
   } = props;
 
 
   const { data: pageInfo, error } = useSWRxPageInfo(pageId ?? null, shareLinkId);
   const { data: pageInfo, error } = useSWRxPageInfo(pageId ?? null, shareLinkId);
@@ -344,17 +363,11 @@ export const PageControls = memo((props: PageControlsProps): JSX.Element => {
 
 
   return (
   return (
     <PageControlsSubstance
     <PageControlsSubstance
-      {...props}
       pageInfo={pageInfo}
       pageInfo={pageInfo}
       pageId={pageId}
       pageId={pageId}
       revisionId={revisionId}
       revisionId={revisionId}
-      path={path}
       onClickEditTagsButton={onClickEditTagsButton}
       onClickEditTagsButton={onClickEditTagsButton}
-      onClickDuplicateMenuItem={onClickDuplicateMenuItem}
-      onClickRenameMenuItem={onClickRenameMenuItem}
-      onClickDeleteMenuItem={onClickDeleteMenuItem}
-      onClickSwitchContentWidth={onClickSwitchContentWidth}
-      expandContentWidth={expandContentWidth}
+      {...rest}
     />
     />
   );
   );
 });
 });

+ 1 - 1
apps/app/src/components/PageControls/SearchButton.tsx

@@ -17,7 +17,7 @@ const SearchButton = (): JSX.Element => {
   return (
   return (
     <button
     <button
       type="button"
       type="button"
-      className={`me-3 btn btn-search ${styles['btn-search']}`}
+      className={`btn btn-search ${styles['btn-search']}`}
       onClick={searchButtonClickHandler}
       onClick={searchButtonClickHandler}
       data-testid="open-search-modal-button"
       data-testid="open-search-modal-button"
     >
     >

+ 1 - 2
apps/app/src/components/PageControls/SeenUserInfo.tsx

@@ -2,7 +2,6 @@ import type { FC } from 'react';
 import React, { useState } from 'react';
 import React, { useState } from 'react';
 
 
 import type { IUser } from '@growi/core';
 import type { IUser } from '@growi/core';
-import { FootstampIcon } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 
 
@@ -40,7 +39,7 @@ const SeenUserInfo: FC<Props> = (props: Props) => {
           </div>
           </div>
         </PopoverBody>
         </PopoverBody>
       </Popover>
       </Popover>
-      <UncontrolledTooltip data-testid="seen-user-info-tooltip" placement="top" target="btn-seen-user" fade={false}>
+      <UncontrolledTooltip data-testid="seen-user-info-tooltip" target="btn-seen-user" fade={false}>
         {t('tooltip.footprints')}
         {t('tooltip.footprints')}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
     </div>
     </div>

+ 3 - 2
apps/app/src/components/PageControls/SubscribeButton.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useCallback } from 'react';
 
 
 import { SubscriptionStatusType } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
@@ -41,7 +42,7 @@ const SubscribeButton: FC<Props> = (props: Props) => {
         </span>
         </span>
       </button>
       </button>
 
 
-      <UncontrolledTooltip data-testid="subscribe-button-tooltip" placement="top" target="subscribe-button" fade={false}>
+      <UncontrolledTooltip data-testid="subscribe-button-tooltip" target="subscribe-button" fade={false}>
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
     </>
     </>

+ 1 - 1
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx

@@ -12,7 +12,7 @@ export const EditorNavbar = (): JSX.Element => {
   const { data: editingUsers } = useEditingUsers();
   const { data: editingUsers } = useEditingUsers();
 
 
   return (
   return (
-    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1 ms-3`}>
+    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1`}>
       <PageHeader />
       <PageHeader />
       <EditingUserList
       <EditingUserList
         userList={editingUsers?.userList ?? []}
         userList={editingUsers?.userList ?? []}

+ 3 - 3
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -10,11 +10,11 @@ const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSel
 const EditorNavbarBottom = (): JSX.Element => {
 const EditorNavbarBottom = (): JSX.Element => {
   return (
   return (
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
-      <div className={`flex-expand-horiz align-items-center px-2 py-1 py-md-2 px-md-3 ${moduleClass}`}>
-        <form className="m-2 me-auto">
+      <div className={`flex-expand-horiz align-items-center p-2 ps-md-3 pe-md-4 ${moduleClass}`}>
+        <form className="me-auto">
           <OptionsSelector />
           <OptionsSelector />
         </form>
         </form>
-        <form className="m-2">
+        <form>
           <SavePageControls />
           <SavePageControls />
         </form>
         </form>
       </div>
       </div>

+ 11 - 11
apps/app/src/components/PageEditor/HandsontableModal.tsx

@@ -442,7 +442,7 @@ export const HandsontableModal = (): JSX.Element => {
         contractWindow={contractWindow}
         contractWindow={contractWindow}
         expandWindow={expandWindow}
         expandWindow={expandWindow}
       />
       />
-      <button type="button" className="btn btn-close" onClick={cancel} aria-label="Close"></button>
+      <button type="button" className="btn btn-close ms-2" onClick={cancel} aria-label="Close"></button>
     </span>
     </span>
   );
   );
 
 
@@ -457,14 +457,14 @@ export const HandsontableModal = (): JSX.Element => {
       className={`handsontable-modal ${isWindowExpanded && 'grw-modal-expanded'}`}
       className={`handsontable-modal ${isWindowExpanded && 'grw-modal-expanded'}`}
       onOpened={handleModalOpen}
       onOpened={handleModalOpen}
     >
     >
-      <ModalHeader tag="h4" toggle={cancel} close={closeButton} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={cancel} close={closeButton}>
         {t('handsontable_modal.title')}
         {t('handsontable_modal.title')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody className="p-0 d-flex flex-column">
       <ModalBody className="p-0 d-flex flex-column">
-        <div className="grw-hot-modal-navbar px-4 py-3 border-bottom">
+        <div className="grw-hot-modal-navbar p-3">
           <button
           <button
             type="button"
             type="button"
-            className="me-4 data-import-button btn btn-secondary"
+            className="me-4 data-import-button btn btn-outline-neutral-secondary"
             data-bs-toggle="collapse"
             data-bs-toggle="collapse"
             data-bs-target="#collapseDataImport"
             data-bs-target="#collapseDataImport"
             aria-expanded={isDataImportAreaExpanded}
             aria-expanded={isDataImportAreaExpanded}
@@ -474,23 +474,23 @@ export const HandsontableModal = (): JSX.Element => {
             <span className="material-symbols-outlined">{isDataImportAreaExpanded ? 'expand_less' : 'expand_more'}</span>
             <span className="material-symbols-outlined">{isDataImportAreaExpanded ? 'expand_less' : 'expand_more'}</span>
           </button>
           </button>
           <div role="group" className="btn-group">
           <div role="group" className="btn-group">
-            <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('l') }}>
+            <button type="button" className="btn btn-outline-neutral-secondary" onClick={() => { alignButtonHandler('l') }}>
               <span className="material-symbols-outlined">format_align_left</span>
               <span className="material-symbols-outlined">format_align_left</span>
             </button>
             </button>
-            <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('c') }}>
+            <button type="button" className="btn btn-outline-neutral-secondary" onClick={() => { alignButtonHandler('c') }}>
               <span className="material-symbols-outlined">format_align_center</span>
               <span className="material-symbols-outlined">format_align_center</span>
             </button>
             </button>
-            <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('r') }}>
+            <button type="button" className="btn btn-outline-neutral-secondary" onClick={() => { alignButtonHandler('r') }}>
               <span className="material-symbols-outlined">format_align_right</span>
               <span className="material-symbols-outlined">format_align_right</span>
             </button>
             </button>
           </div>
           </div>
           <Collapse isOpen={isDataImportAreaExpanded}>
           <Collapse isOpen={isDataImportAreaExpanded}>
-            <div className="mt-4">
+            <div className="py-3 border-bottom">
               <MarkdownTableDataImportForm onCancel={toggleDataImportArea} onImport={importData} />
               <MarkdownTableDataImportForm onCancel={toggleDataImportArea} onImport={importData} />
             </div>
             </div>
           </Collapse>
           </Collapse>
         </div>
         </div>
-        <div ref={c => setHotTableContainer(c)} className="m-4 hot-table-container">
+        <div ref={c => setHotTableContainer(c)} className="hot-table-container px-3">
           <HotTable
           <HotTable
             ref={c => setHotTable(c)}
             ref={c => setHotTable(c)}
             data={markdownTable.table}
             data={markdownTable.table}
@@ -507,9 +507,9 @@ export const HandsontableModal = (): JSX.Element => {
         </div>
         </div>
       </ModalBody>
       </ModalBody>
       <ModalFooter className="grw-modal-footer">
       <ModalFooter className="grw-modal-footer">
-        <button type="button" className="btn btn-danger" onClick={reset}>{t('commons:Reset')}</button>
+        <button type="button" className="btn btn-outline-danger" onClick={reset}>{t('commons:Reset')}</button>
         <div className="ms-auto">
         <div className="ms-auto">
-          <button type="button" className="me-2 btn btn-secondary" onClick={cancel}>{t('handsontable_modal.cancel')}</button>
+          <button type="button" className="me-2 btn btn-outline-neutral-secondary" onClick={cancel}>{t('handsontable_modal.cancel')}</button>
           <button type="button" className="btn btn-primary" onClick={save}>{t('handsontable_modal.done')}</button>
           <button type="button" className="btn btn-primary" onClick={save}>{t('handsontable_modal.done')}</button>
         </div>
         </div>
       </ModalFooter>
       </ModalFooter>

+ 3 - 3
apps/app/src/components/PageEditor/MarkdownTableDataImportForm.tsx

@@ -66,7 +66,7 @@ export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormPr
           <option value="html">HTML</option>
           <option value="html">HTML</option>
         </select>
         </select>
       </div>
       </div>
-      <div>
+      <div className="mt-2">
         <label htmlFor="data-import-form-type-textarea" className="form-label">{t('import_data')}</label>
         <label htmlFor="data-import-form-type-textarea" className="form-label">{t('import_data')}</label>
         <textarea
         <textarea
           id="data-import-form-type-textarea"
           id="data-import-form-type-textarea"
@@ -88,8 +88,8 @@ export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormPr
           />
           />
         </div>
         </div>
       </Collapse>
       </Collapse>
-      <div className="d-flex justify-content-end">
-        <Button color="secondary me-2" onClick={onCancel}>{t('cancel')}</Button>
+      <div className="mt-3 d-flex justify-content-end">
+        <Button color="outline-neutral-secondary me-2" onClick={onCancel}>{t('cancel')}</Button>
         <Button color="primary" onClick={importButtonHandler}>{t('import')}</Button>
         <Button color="primary" onClick={importButtonHandler}>{t('import')}</Button>
       </div>
       </div>
     </form>
     </form>

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