Explorar el Código

Merge branch 'feat/115673-marp' into imprv/115673-115675-enable-from-adminsetting

reiji-h hace 2 años
padre
commit
c2e3faa14a
Se han modificado 100 ficheros con 565 adiciones y 471 borrados
  1. 7 0
      .github/release-drafter-dev-6.2.x.yml
  2. 7 0
      .github/release-drafter-master.yml
  3. 2 1
      .github/workflows/auto-labeling.yml
  4. 2 0
      .github/workflows/ci-app-prod.yml
  5. 3 3
      .github/workflows/ci-app.yml
  6. 3 3
      .github/workflows/ci-slackbot-proxy.yml
  7. 11 2
      .github/workflows/draft-release.yml
  8. 1 1
      .github/workflows/release-slackbot-proxy.yml
  9. 2 2
      .github/workflows/release.yml
  10. 8 15
      .github/workflows/reusable-app-prod.yml
  11. 1 1
      .github/workflows/reusable-app-reg-suit.yml
  12. 4 4
      .mergify.yml
  13. 46 1
      CHANGELOG.md
  14. 3 2
      apps/app/bin/download-cdn-resources.ts
  15. 1 1
      apps/app/bin/github-actions/update-readme.sh
  16. 2 1
      apps/app/config/next-i18next.config.js
  17. 1 0
      apps/app/cypress.config.ts
  18. 6 5
      apps/app/docker/README.md
  19. 1 0
      apps/app/next.config.js
  20. 6 8
      apps/app/package.json
  21. 1 1
      apps/app/public/static/locales/en_US/translation.json
  22. 1 1
      apps/app/public/static/locales/ja_JP/translation.json
  23. 1 1
      apps/app/public/static/locales/zh_CN/translation.json
  24. 1 3
      apps/app/src/client/models/Linker.js
  25. 1 1
      apps/app/src/client/services/AdminAppContainer.js
  26. 1 1
      apps/app/src/client/services/AdminCustomizeContainer.js
  27. 1 1
      apps/app/src/client/services/AdminExternalAccountsContainer.js
  28. 1 1
      apps/app/src/client/services/AdminGeneralSecurityContainer.js
  29. 1 2
      apps/app/src/client/services/AdminGitHubSecurityContainer.js
  30. 1 2
      apps/app/src/client/services/AdminGoogleSecurityContainer.js
  31. 1 1
      apps/app/src/client/services/AdminHomeContainer.js
  32. 1 1
      apps/app/src/client/services/AdminImportContainer.js
  33. 1 1
      apps/app/src/client/services/AdminLdapSecurityContainer.js
  34. 1 1
      apps/app/src/client/services/AdminLocalSecurityContainer.js
  35. 1 1
      apps/app/src/client/services/AdminMarkDownContainer.js
  36. 1 1
      apps/app/src/client/services/AdminNotificationContainer.js
  37. 1 2
      apps/app/src/client/services/AdminOidcSecurityContainer.js
  38. 1 2
      apps/app/src/client/services/AdminSamlSecurityContainer.js
  39. 1 1
      apps/app/src/client/services/AdminSlackIntegrationLegacyContainer.js
  40. 1 1
      apps/app/src/client/services/AdminUsersContainer.js
  41. 0 34
      apps/app/src/client/services/ShowPageAccessoriesModal.tsx
  42. 2 1
      apps/app/src/client/services/layout.ts
  43. 2 4
      apps/app/src/client/services/page-operation.ts
  44. 8 3
      apps/app/src/client/services/renderer/renderer.tsx
  45. 1 1
      apps/app/src/client/services/user-ui-settings.ts
  46. 1 1
      apps/app/src/client/util/bookmark-utils.ts
  47. 2 2
      apps/app/src/components/Admin/AuditLog/ActivityTable.tsx
  48. 0 166
      apps/app/src/components/Admin/Common/AdminNavigation.jsx
  49. 167 0
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  50. 1 1
      apps/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx
  51. 3 2
      apps/app/src/components/Admin/Notification/ManageGlobalNotification.tsx
  52. 1 1
      apps/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  53. 1 1
      apps/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  54. 1 1
      apps/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  55. 1 1
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  56. 1 1
      apps/app/src/components/Admin/Security/ShareLinkSetting.tsx
  57. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  58. 1 2
      apps/app/src/components/Admin/UserGroup/UserGroupDropdown.tsx
  59. 1 2
      apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  60. 1 3
      apps/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  61. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  62. 3 2
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  63. 4 4
      apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  64. 7 3
      apps/app/src/components/Admin/UserGroupDetail/UserGroupPageList.tsx
  65. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  66. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.tsx
  67. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  68. 1 1
      apps/app/src/components/Admin/Users/UserMenu.tsx
  69. 1 1
      apps/app/src/components/Admin/Users/UserTable.tsx
  70. 4 3
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  71. 3 3
      apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx
  72. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  73. 11 7
      apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx
  74. 45 18
      apps/app/src/components/Bookmarks/BookmarkItem.tsx
  75. 13 6
      apps/app/src/components/Comments.tsx
  76. 3 3
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  77. 1 1
      apps/app/src/components/ContentLinkButtons.tsx
  78. 1 1
      apps/app/src/components/CreateTemplateModal.jsx
  79. 1 1
      apps/app/src/components/CustomNavigation/CustomNav.tsx
  80. 6 11
      apps/app/src/components/DescendantsPageList.tsx
  81. 1 1
      apps/app/src/components/DuplicatedPathsTable.tsx
  82. 1 1
      apps/app/src/components/IdenticalPathPage.tsx
  83. 2 2
      apps/app/src/components/InAppNotification/InAppNotificationElm.tsx
  84. 1 1
      apps/app/src/components/InAppNotification/InAppNotificationList.tsx
  85. 2 2
      apps/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  86. 1 1
      apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx
  87. 6 16
      apps/app/src/components/InstallerForm.tsx
  88. 6 13
      apps/app/src/components/InvitedForm.tsx
  89. 1 1
      apps/app/src/components/Layout/AdminLayout.tsx
  90. 4 2
      apps/app/src/components/Layout/BasicLayout.tsx
  91. 1 1
      apps/app/src/components/LikeButtons.tsx
  92. 46 9
      apps/app/src/components/LoginForm.tsx
  93. 2 2
      apps/app/src/components/Me/DisassociateModal.tsx
  94. 2 2
      apps/app/src/components/Navbar/AuthorInfo.tsx
  95. 1 1
      apps/app/src/components/Navbar/GlobalSearch.tsx
  96. 30 31
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  97. 2 3
      apps/app/src/components/Navbar/PageEditorModeManager.jsx
  98. 1 1
      apps/app/src/components/Navbar/PersonalDropdown.jsx
  99. 12 6
      apps/app/src/components/Navbar/SubNavButtons.tsx
  100. 1 1
      apps/app/src/components/NotCreatablePage.tsx

+ 7 - 0
.github/release-drafter-dev-6.2.x.yml

@@ -0,0 +1,7 @@
+_extends: growi:.github/release-drafter.yml
+
+prerelease: true
+
+# Filter previous releases to consider only those with the tags starts with 'v6.2'
+include-pre-releases: true
+tag-prefix: v6.2

+ 7 - 0
.github/release-drafter-master.yml

@@ -0,0 +1,7 @@
+_extends: growi:.github/release-drafter.yml
+
+prerelease: true
+
+# Filter previous releases to consider only those with the master branch
+include-pre-releases: true
+filter-by-commitish: true

+ 2 - 1
.github/workflows/pr-to-master.yml → .github/workflows/auto-labeling.yml

@@ -1,9 +1,10 @@
-name: PR to master
+name: Auto-labeling
 
 on:
   pull_request:
     branches:
       - master
+      - dev/*.*.*
     # Only following types are handled by the action, but one can default to all as well
     types: [opened, reopened, edited, synchronize]
 

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

@@ -4,6 +4,7 @@ on:
   push:
     branches:
       - master
+      - dev/6.*.x
     paths:
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
@@ -18,6 +19,7 @@ on:
   pull_request:
     branches:
       - master
+      - dev/6.*.x
     types: [opened, reopened, synchronize]
     paths:
       - .github/workflows/ci-app-prod.yml

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

@@ -60,7 +60,7 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
+          yarn global add turbo@1.10.9
           yarn --frozen-lockfile
 
       - name: Lint
@@ -131,7 +131,7 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
+          yarn global add turbo@1.10.9
           yarn --frozen-lockfile
 
       - name: Test
@@ -213,7 +213,7 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
+          yarn global add turbo@1.10.9
           yarn --frozen-lockfile
 
       - name: turbo run dev:ci

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

@@ -62,7 +62,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: Lint
@@ -136,7 +136,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: yarn dev:ci
@@ -200,7 +200,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |

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

@@ -4,6 +4,8 @@ on:
   push:
     branches:
       - master
+      - dev/*.*.*
+
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -27,10 +29,17 @@ jobs:
         uses: myrotvorets/info-from-package-json-action@1.2.0
         id: package-json
 
-      # Drafts your next Release notes as Pull Requests are merged into "master"
+      - name: Determine config file
+        id: determine-config-name
+        run: |
+          BRANCH_NAME="${{ github.ref_name }}"
+          BRANCH_NAME_REPLACED=${BRANCH_NAME/\//-}
+          echo "value=release-drafter-$BRANCH_NAME_REPLACED.yml" >> $GITHUB_OUTPUT
+
       - uses: release-drafter/release-drafter@v5
         id: release-drafter
         with:
+          config-name: ${{ steps.determine-config-name.outputs.value }}
           name: v${{ steps.package-json.outputs.packageVersion }}
           tag: v${{ steps.package-json.outputs.packageVersion }}
           version: ${{ steps.package-json.outputs.packageVersion }}
@@ -60,7 +69,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GIT_PR_RELEASE_BRANCH_PRODUCTION: release/current
-          GIT_PR_RELEASE_BRANCH_STAGING: master
+          GIT_PR_RELEASE_BRANCH_STAGING: ${{ github.ref_name }}
           GIT_PR_RELEASE_TEMPLATE: .github/git-pr-release-template.erb
           GIT_PR_RELEASE_TITLE: Release v${{ steps.release-version.outputs.RELEASE_VERSION }}
           GIT_PR_RELEASE_BODY: ${{ needs.update-release-draft.outputs.RELEASE_DRAFT_BODY }}

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

@@ -108,7 +108,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: Bump versions for next RC

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

@@ -30,7 +30,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: Bump versions
@@ -93,7 +93,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: Bump versions for next RC

+ 8 - 15
.github/workflows/reusable-app-prod.yml

@@ -36,7 +36,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |
@@ -59,15 +59,17 @@ jobs:
         yarn --frozen-lockfile
 
     - name: Restore dist
-      uses: actions/cache/restore@v3
+      uses: actions/cache@v3
       with:
         path: |
+          node_modules/.cache/turbo
           **/.turbo
           **/dist
           ${{ github.workspace }}/apps/app/.next
-        key: dist-app-prod-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
+        key: dist-app-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ github.ref_name }}-${{ github.sha }}
         restore-keys: |
-          dist-app-prod-${{ runner.OS }}-node${{ matrix.node-version }}-
+          dist-app-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ github.ref_name }}-
+          dist-app-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
 
     - name: Build
       working-directory: ./apps/app
@@ -117,15 +119,6 @@ jobs:
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
-    - name: Cache dist
-      uses: actions/cache/save@v3
-      with:
-        path: |
-          **/.turbo
-          **/dist
-          ${{ github.workspace }}/apps/app/.next
-        key: dist-app-prod-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
-
 
   launch-prod:
     needs: [build-prod]
@@ -154,7 +147,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |
@@ -245,7 +238,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |

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

@@ -62,7 +62,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |

+ 4 - 4
.mergify.yml

@@ -3,11 +3,11 @@ pull_request_rules:
     conditions:
       - author = dependabot[bot]
       - '#approved-reviews-by >= 1'
-      - check-success = "lint (16.x)"
-      - check-success = "test (16.x)"
-      - check-success = "launch-dev (16.x)"
-      - check-success = "test-prod-node14 / launch-prod"
+      - check-success = "lint (18.x)"
+      - check-success = "test (18.x)"
+      - check-success = "launch-dev (18.x)"
       - check-success = "test-prod-node16 / launch-prod"
+      - check-success = "test-prod-node18 / launch-prod"
     actions:
       merge:
         method: merge

+ 46 - 1
CHANGELOG.md

@@ -1,9 +1,54 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.1.6...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.1.8...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.1.8](https://github.com/weseek/growi/compare/v6.1.7...v6.1.8) - 2023-07-24
+
+### 💎 Features
+
+- feat: Add plugin badge to TemplateModal's list of templates (#7897) @TatsuyaIse
+
+### 🚀 Improvement
+
+- imprv: Replace isAdmin with usersAdminHooks (#7840) @WNomunomu
+- imprv: Show alert for trashed pages only when the page is not empty (#7903) @TatsuyaIse
+- imprv: Template name truncation (#7898) @TatsuyaIse
+
+### 🐛 Bug Fixes
+
+- fix: Cancel a comment will cancel all comments (#7804) @mudana-grune
+
+### 🧰 Maintenance
+
+- ci(deps-dev): bump vite from 4.3.8 to 4.4.5 (#7901) @dependabot
+- ci(deps): bump semver from 5.7.1 to 5.7.2 (#7867) @dependabot
+- ci(deps): bump mongoose from 6.11.1 to 6.11.3 (#7891) @dependabot
+- ci(deps): bump word-wrap from 1.2.3 to 1.2.4 (#7892) @dependabot
+
+## [v6.1.7](https://github.com/weseek/growi/compare/v6.1.6...v6.1.7) - 2023-07-19
+
+### 💎 Features
+
+- feat: Authentication settings cannot be disabled if there will be no administrator user available to log in (#7761) @mudana-grune
+
+### 🚀 Improvement
+
+- imprv: Show spinner while installing and logging-in (#7823) @soumaeda
+- imprv: Routing with next link (#7880) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Auto popup PageAccessoriesModal and show page history (#7888) @yuki-takei
+- fix: Auto-scroll does not work when accessing the page when the header string is CJK (#7882) @yuki-takei
+- fix: Avoid unnecessary next routing (#7863) @miya
+- fix: Work put back page on bookmark sidebar (#7698) @mudana-grune
+
+### 🧰 Maintenance
+
+- support: Render SearchPageBase in CSR (#7889) @yuki-takei
+
 ## [v6.1.6](https://github.com/weseek/growi/compare/v6.1.5...v6.1.6) - 2023-07-12
 
 ### 🐛 Bug Fixes

+ 3 - 2
apps/app/bin/download-cdn-resources.ts

@@ -3,11 +3,12 @@
  *
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
-import { envUtils } from '@growi/core';
+import { envUtils } from '@growi/core/dist/utils';
 
-import CdnResourcesDownloader from './cdn/cdn-resources-downloader';
 import loggerFactory from '../src/utils/logger';
 
+import CdnResourcesDownloader from './cdn/cdn-resources-downloader';
+
 const logger = loggerFactory('growi:bin:download-cdn-resources');
 
 // check env var

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

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

+ 2 - 1
apps/app/config/next-i18next.config.js

@@ -1,6 +1,7 @@
 const path = require('path');
 
-const { isServer, AllLang, Lang } = require('@growi/core');
+const { AllLang, Lang } = require('@growi/core');
+const { isServer } = require('@growi/core/dist/utils');
 const I18nextChainedBackend = require('i18next-chained-backend').default;
 const I18NextHttpBackend = require('i18next-http-backend');
 const I18NextLocalStorageBackend = require('i18next-localstorage-backend').default;

+ 1 - 0
apps/app/cypress.config.ts

@@ -22,6 +22,7 @@ export default defineConfig({
   fixturesFolder: 'test/cypress/fixtures',
   screenshotsFolder: 'test/cypress/screenshots',
   videosFolder: 'test/cypress/videos',
+  video: false,
 
   viewportWidth: 1400,
   viewportHeight: 1024,

+ 6 - 5
apps/app/docker/README.md

@@ -10,11 +10,12 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`6.1.0`, `6.1`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.0/apps/app/docker/Dockerfile)
-* [`6.0.15`, `6.0`](https://github.com/weseek/growi/blob/v6.0.15/packages/app/docker/Dockerfile)
-* [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
-* [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
-* [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
+* [`6.2.0`, `6.2`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.0/apps/app/docker/Dockerfile)
+* [`6.1.0`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.8/apps/app/docker/Dockerfile)
+* [`6.0.15`, `6.0` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.15/packages/app/docker/Dockerfile)
+* [`5.1.7`, `5.1`, `5` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
+* [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
+* [`4.5.23`, `4.5`, `4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 * [`4.5.23-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 
 

+ 1 - 0
apps/app/next.config.js

@@ -27,6 +27,7 @@ const getTranspilePackages = () => {
     'comma-separated-tokens',
     'decode-named-character-reference',
     'devlop',
+    'fault',
     'escape-string-regexp',
     'hastscript',
     'html-void-elements',

+ 6 - 8
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.1.7-RC.0",
+  "version": "6.2.0-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -115,7 +115,7 @@
     "extensible-custom-error": "^0.0.7",
     "form-data": "^4.0.0",
     "graceful-fs": "^4.1.11",
-    "hast-util-select": "^5.0.2",
+    "hast-util-select": "^5.0.5",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "i18next": "^22.4.10",
@@ -127,14 +127,11 @@
     "lucene-query-parser": "^1.2.0",
     "markdown-table": "^1.1.1",
     "md5": "^2.2.1",
-    "mdast-util-frontmatter": "1.0.1",
-    "mdast-util-gfm": "^3.0.0",
-    "mdast-util-to-markdown": "^1.5.0",
     "mermaid": "^10.1.0",
     "method-override": "^3.0.0",
     "migrate-mongo": "^8.2.3",
     "mkdirp": "^1.0.3",
-    "mongoose": "^6.5.0",
+    "mongoose": "^6.11.3",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
@@ -199,7 +196,7 @@
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
     "swr": "^2.0.3",
-    "throttle-debounce": "^3.0.1",
+    "throttle-debounce": "^5.0.0",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
@@ -267,6 +264,7 @@
     "socket.io-client": "^4.2.0",
     "source-map-loader": "^4.0.1",
     "swagger2openapi": "^5.3.1",
-    "tsc-alias": "^1.2.9"
+    "tsc-alias": "^1.2.9",
+    "fslightbox-react": "^1.7.6"
   }
 }

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

@@ -165,7 +165,7 @@
   "folder_name": "Folder name",
   "field": "field",
   "not_creatable_page": {
-    "could_not_creata_path": "Couldn't create path."
+    "message": "Page contents cannot be created in this path."
   },
   "custom_navigation": {
     "no_pages_under_this_page": "There are no pages under this page."

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

@@ -166,7 +166,7 @@
   "folder_name": "フォルダ名",
   "field": "フィールド",
   "not_creatable_page": {
-    "could_not_creata_path": "パスを作成できませんでした。"
+    "message": "このパスではページ コンテンツを作成できません。"
   },
   "custom_navigation": {
     "no_pages_under_this_page": "このページの配下にはページが存在しません。"

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

@@ -172,7 +172,7 @@
   "folder_name": "文件夹名称",
   "field": "字段",
   "not_creatable_page": {
-    "could_not_creata_path": "无法创建路径"
+    "message": "无法在此路径中创建页面内容。"
   },
   "custom_navigation": {
     "no_pages_under_this_page": "There are no pages under this page."

+ 1 - 3
apps/app/src/client/models/Linker.js

@@ -1,7 +1,5 @@
 
-import { pagePathUtils } from '@growi/core';
-
-const { encodeSpaces } = pagePathUtils;
+import { encodeSpaces } from '@growi/core/dist/utils/page-path-utils';
 
 export default class Linker {
 

+ 1 - 1
apps/app/src/client/services/AdminAppContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import { apiv3Get, apiv3Post, apiv3Put } from '../util/apiv3-client';

+ 1 - 1
apps/app/src/client/services/AdminCustomizeContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';

+ 1 - 1
apps/app/src/client/services/AdminExternalAccountsContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';

+ 1 - 1
apps/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import {

+ 1 - 2
apps/app/src/client/services/AdminGitHubSecurityContainer.js

@@ -1,6 +1,5 @@
-import { isServer, pathUtils } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
-import urljoin from 'url-join';
 
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 2
apps/app/src/client/services/AdminGoogleSecurityContainer.js

@@ -1,6 +1,5 @@
-import { isServer, pathUtils } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
-import urljoin from 'url-join';
 
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
apps/app/src/client/services/AdminHomeContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';

+ 1 - 1
apps/app/src/client/services/AdminImportContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';

+ 1 - 1
apps/app/src/client/services/AdminLdapSecurityContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';

+ 1 - 1
apps/app/src/client/services/AdminLocalSecurityContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';

+ 1 - 1
apps/app/src/client/services/AdminMarkDownContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import { apiv3Get, apiv3Put } from '../util/apiv3-client';

+ 1 - 1
apps/app/src/client/services/AdminNotificationContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import {

+ 1 - 2
apps/app/src/client/services/AdminOidcSecurityContainer.js

@@ -1,6 +1,5 @@
-import { isServer, pathUtils } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
-import urljoin from 'url-join';
 
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 2
apps/app/src/client/services/AdminSamlSecurityContainer.js

@@ -1,6 +1,5 @@
-import { isServer, pathUtils } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
-import urljoin from 'url-join';
 
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
apps/app/src/client/services/AdminSlackIntegrationLegacyContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import { apiv3Get, apiv3Put } from '../util/apiv3-client';

+ 1 - 1
apps/app/src/client/services/AdminUsersContainer.js

@@ -1,4 +1,4 @@
-import { isServer } from '@growi/core';
+import { isServer } from '@growi/core/dist/utils';
 import { debounce } from 'throttle-debounce';
 import { Container } from 'unstated';
 

+ 0 - 34
apps/app/src/client/services/ShowPageAccessoriesModal.tsx

@@ -1,34 +0,0 @@
-import React, { useEffect, useState } from 'react';
-
-import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
-
-function getURLQueryParamValue(key: string) {
-// window.location.href is page URL;
-  const queryStr: URLSearchParams = new URL(window.location.href).searchParams;
-  return queryStr.get(key);
-}
-
-const queryCompareFormat = new RegExp(/([a-z0-9]){24}...([a-z0-9]){24}/);
-
-const ShowPageAccessoriesModal = (): JSX.Element => {
-  const { data: status, open: openPageAccessories } = usePageAccessoriesModal();
-  const [isArleadyMounted, setIsArleadyMounted] = useState(false);
-  useEffect(() => {
-    const pageIdParams = getURLQueryParamValue('compare');
-    if (status == null || status.isOpened === true) {
-      return;
-    }
-    if (isArleadyMounted === true) {
-      return;
-    }
-    if (pageIdParams != null) {
-      if (queryCompareFormat.test(pageIdParams)) {
-        openPageAccessories(PageAccessoriesModalContents.PageHistory);
-      }
-    }
-    setIsArleadyMounted(true);
-  }, [openPageAccessories, status, isArleadyMounted]);
-  return <></>;
-};
-
-export default ShowPageAccessoriesModal;

+ 2 - 1
apps/app/src/client/services/layout.ts

@@ -1,4 +1,5 @@
-import type { IPage } from '~/interfaces/page';
+import type { IPage } from '@growi/core';
+
 import { useIsContainerFluid } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useEditorMode } from '~/stores/ui';

+ 2 - 4
apps/app/src/client/services/page-operation.ts

@@ -1,6 +1,6 @@
 import { useCallback } from 'react';
 
-import { SubscriptionStatusType, Nullable } from '@growi/core';
+import { SubscriptionStatusType, type Nullable } from '@growi/core';
 import urljoin from 'url-join';
 
 import { OptionsToSave } from '~/interfaces/page-operation';
@@ -153,10 +153,8 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
     // markdown = pageEditor.getMarkdown();
     // }
 
-    const isNoRevisionPage = pageId != null && revisionId == null;
-
     let res;
-    if (pageId == null || isNoRevisionPage) {
+    if (pageId == null || revisionId == null) {
       res = await createPage(path, markdown, options);
     }
     else {

+ 8 - 3
apps/app/src/client/services/renderer/renderer.tsx

@@ -1,10 +1,11 @@
 import assert from 'assert';
 
 import { isClient } from '@growi/core/dist/utils/browser-utils';
-import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client/index.mjs';
+import * as slides from '@growi/presentation';
+import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
 import * as drawio from '@growi/remark-drawio';
 // eslint-disable-next-line import/extensions
-import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client/index.mjs';
+import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client';
 import katex from 'rehype-katex';
 import sanitize from 'rehype-sanitize';
 import slug from 'rehype-slug';
@@ -16,7 +17,9 @@ import type { Pluggable } from 'unified';
 
 import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
+import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
+// eslint-disable-next-line import/no-cycle
 import { SlideViewer } from '~/components/ReactMarkdownComponents/SlideViewer';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
@@ -28,7 +31,6 @@ import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-
 import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
 import * as attachment from '~/services/renderer/remark-plugins/attachment';
 import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
-import * as slides from '~/services/renderer/remark-plugins/slides';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import {
   commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
@@ -117,6 +119,7 @@ export const generateViewOptions = (
     components.table = TableWithEditButton;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
     components.slide = SlideViewer;
   }
 
@@ -223,6 +226,7 @@ export const generateSimpleViewOptions = (
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
   }
 
   if (config.isEnabledXssPrevention) {
@@ -302,6 +306,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
     components.slide = SlideViewer;
   }
 

+ 1 - 1
apps/app/src/client/services/user-ui-settings.ts

@@ -15,7 +15,7 @@ const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> =>
   return result;
 };
 
-const _putUserUISettingsInBulkDebounced = debounce(1500, false, _putUserUISettingsInBulk);
+const _putUserUISettingsInBulkDebounced = debounce(1500, _putUserUISettingsInBulk);
 
 type ScheduleToPutFunction = (settings: Partial<IUserUISettings>) => Promise<AxiosResponse<IUserUISettings>>;
 const scheduleToPut: ScheduleToPutFunction = (settings: Partial<IUserUISettings>): Promise<AxiosResponse<IUserUISettings>> => {

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

@@ -1,4 +1,4 @@
-import { IRevision, Ref } from '@growi/core';
+import type { IRevision, Ref } from '@growi/core';
 
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 

+ 2 - 2
apps/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,7 +1,7 @@
 import React, { FC, useState, useCallback } from 'react';
 
-import { pagePathUtils } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
+import { pagePathUtils } from '@growi/core/dist/utils';
+import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { useTranslation } from 'react-i18next';

+ 0 - 166
apps/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -1,166 +0,0 @@
-import React from 'react';
-
-import { pathUtils } from '@growi/core';
-import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
-import PropTypes from 'prop-types';
-import urljoin from 'url-join';
-
-import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../../stores/context';
-// import AppContainer from '~/client/services/AppContainer';
-
-// import { withUnstatedContainers } from '../../UnstatedUtils';
-
-const AdminNavigation = (props) => {
-  const { t } = useTranslation(['admin', 'commons']);
-  // const { appContainer } = props;
-  const pathname = window.location.pathname;
-
-  const { data: growiCloudUri } = useGrowiCloudUri();
-  const { data: growiAppIdForGrowiCloud } = useGrowiAppIdForGrowiCloud();
-
-  // eslint-disable-next-line react/prop-types
-  const MenuLabel = ({ menu }) => {
-    switch (menu) {
-      /* eslint-disable no-multi-spaces, max-len */
-      case 'app':                      return <><i className="mr-1 icon-fw icon-settings"></i>{        t('headers.app_settings', { ns: 'commons' }) }</>;
-      case 'security':                 return <><i className="mr-1 icon-fw icon-shield"></i>{          t('security_settings.security_settings') }</>;
-      case 'markdown':                 return <><i className="mr-1 icon-fw icon-note"></i>{            t('markdown_settings.markdown_settings') }</>;
-      case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
-      case 'importer':                 return <><i className="mr-1 icon-fw icon-cloud-upload"></i>{    t('importer_management.import_data') }</>;
-      case 'export':                   return <><i className="mr-1 icon-fw icon-cloud-download"></i>{  t('export_management.export_archive_data') }</>;
-      case 'data-transfer':            return <><i className="mr-1 icon-fw icon-plane"></i>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
-      case 'notification':             return <><i className="mr-1 icon-fw icon-bell"></i>{            t('external_notification.external_notification')}</>;
-      case 'slack-integration':        return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration.slack_integration') }</>;
-      case 'slack-integration-legacy': return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
-      case 'users':                    return <><i className="mr-1 icon-fw icon-user"></i>{            t('user_management.user_management') }</>;
-      case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
-      case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-      case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{          t('plugins.plugins')}</>;
-      case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
-      case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
-      default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
-      /* eslint-enable no-multi-spaces, max-len */
-    }
-  };
-
-  const MenuLink = ({
-    // eslint-disable-next-line react/prop-types
-    menu, isRoot, isListGroupItems, isActive,
-  }) => {
-    const pageTransitionClassName = isListGroupItems
-      ? 'list-group-item list-group-item-action border-0 round-corner'
-      : 'dropdown-item px-3 py-2';
-
-    const href = isRoot ? '/admin' : urljoin('/admin', menu);
-
-    return (
-      <Link
-        href={href}
-        className={`${pageTransitionClassName} ${isActive ? 'active' : ''}`}
-      >
-        <MenuLabel menu={menu} />
-      </Link>
-    );
-  };
-
-  const isActiveMenu = (path) => {
-    const basisPath = pathUtils.normalizePath(urljoin('/admin', path));
-    const basisParentPath = pathUtils.addTrailingSlash(basisPath);
-
-    return (
-      pathname === basisPath
-      || pathname.startsWith(basisParentPath)
-    );
-  };
-
-  const getListGroupItemOrDropdownItemList = (isListGroupItems) => {
-    return (
-      <>
-        {/* eslint-disable no-multi-spaces */}
-        <MenuLink menu="home"         isListGroupItems isActive={pathname === '/admin'} isRoot />
-        <MenuLink menu="app"          isListGroupItems isActive={isActiveMenu('/app')} />
-        <MenuLink menu="security"     isListGroupItems isActive={isActiveMenu('/security')} />
-        <MenuLink menu="markdown"     isListGroupItems isActive={isActiveMenu('/markdown')} />
-        <MenuLink menu="customize"    isListGroupItems isActive={isActiveMenu('/customize')} />
-        <MenuLink menu="importer"     isListGroupItems isActive={isActiveMenu('/importer')} />
-        <MenuLink menu="export"       isListGroupItems isActive={isActiveMenu('/export')} />
-        <MenuLink menu="data-transfer" isListGroupItems isActive={isActiveMenu('/data-transfer')} />
-        <MenuLink menu="notification" isListGroupItems isActive={isActiveMenu('/notification') || isActiveMenu('/global-notification')} />
-        <MenuLink menu="slack-integration" isListGroupItems isActive={isActiveMenu('/slack-integration')} />
-        <MenuLink menu="slack-integration-legacy" isListGroupItems isActive={isActiveMenu('/slack-integration-legacy')} />
-        <MenuLink menu="users"        isListGroupItems isActive={isActiveMenu('/users')} />
-        <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
-        <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
-        <MenuLink menu="plugins"      isListGroupItems isActive={isActiveMenu('/plugins')} />
-        <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
-        {growiCloudUri != null && growiAppIdForGrowiCloud != null
-          && (
-            <a
-              href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
-              className="list-group-item list-group-item-action border-0 round-corner"
-            >
-              <MenuLabel menu="cloud" />
-            </a>
-          )
-        }
-        {/* eslint-enable no-multi-spaces */}
-      </>
-    );
-  };
-
-  return (
-    <React.Fragment>
-      {/* List group */}
-      <div className="list-group admin-navigation sticky-top d-none d-lg-block">
-        {getListGroupItemOrDropdownItemList(true)}
-      </div>
-
-      {/* Dropdown */}
-      <div className="dropdown d-block d-lg-none mb-5">
-        <button
-          className="btn btn-outline-primary btn-lg dropdown-toggle col-12 text-right"
-          type="button"
-          id="dropdown-admin-navigation"
-          data-display="static"
-          data-toggle="dropdown"
-          aria-haspopup="true"
-          aria-expanded="false"
-        >
-          <span className="float-left">
-            {/* eslint-disable no-multi-spaces */}
-            {pathname === '/admin' &&              <MenuLabel menu="home" />}
-            {isActiveMenu('/app') &&               <MenuLabel menu="app" />}
-            {isActiveMenu('/security') &&          <MenuLabel menu="security" />}
-            {isActiveMenu('/markdown') &&          <MenuLabel menu="markdown" />}
-            {isActiveMenu('/customize') &&         <MenuLabel menu="customize" />}
-            {isActiveMenu('/importer') &&          <MenuLabel menu="importer" />}
-            {isActiveMenu('/export') &&            <MenuLabel menu="export" />}
-            {(isActiveMenu('/notification') || isActiveMenu('/global-notification')) && <MenuLabel menu="notification" />}
-            {isActiveMenu('/slack-integration') && <MenuLabel menu="slack-integration" />}
-            {isActiveMenu('/users') &&             <MenuLabel menu="users" />}
-            {isActiveMenu('/user-groups') &&       <MenuLabel menu="user-groups" />}
-            {isActiveMenu('/search') &&            <MenuLabel menu="search" />}
-            {isActiveMenu('/audit-log') &&         <MenuLabel menu="audit-log" />}
-            {isActiveMenu('/plugins') &&           <MenuLabel menu="plugins" />}
-            {isActiveMenu('/data-transfer') &&     <MenuLabel menu="data-transfer" />}
-            {/* eslint-enable no-multi-spaces */}
-          </span>
-        </button>
-        <div className="dropdown-menu" aria-labelledby="dropdown-admin-navigation">
-          {getListGroupItemOrDropdownItemList(false)}
-        </div>
-      </div>
-
-    </React.Fragment>
-  );
-};
-
-// const AdminNavigationWrapper = withUnstatedContainers(AdminNavigation, [AppContainer]);
-
-AdminNavigation.propTypes = {
-  // appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-// export default AdminNavigationWrapper;
-export default AdminNavigation;

+ 167 - 0
apps/app/src/components/Admin/Common/AdminNavigation.tsx

@@ -0,0 +1,167 @@
+import React, { useCallback } from 'react';
+
+import { pathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
+import urljoin from 'url-join';
+
+import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../../stores/context';
+
+// eslint-disable-next-line react/prop-types
+const MenuLabel = ({ menu }: { menu: string }) => {
+  const { t } = useTranslation(['admin', 'commons']);
+
+  switch (menu) {
+    /* eslint-disable no-multi-spaces, max-len */
+    case 'app':                      return <><i className="mr-1 icon-fw icon-settings"></i>{        t('headers.app_settings', { ns: 'commons' }) }</>;
+    case 'security':                 return <><i className="mr-1 icon-fw icon-shield"></i>{          t('security_settings.security_settings') }</>;
+    case 'markdown':                 return <><i className="mr-1 icon-fw icon-note"></i>{            t('markdown_settings.markdown_settings') }</>;
+    case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
+    case 'importer':                 return <><i className="mr-1 icon-fw icon-cloud-upload"></i>{    t('importer_management.import_data') }</>;
+    case 'export':                   return <><i className="mr-1 icon-fw icon-cloud-download"></i>{  t('export_management.export_archive_data') }</>;
+    case 'data-transfer':            return <><i className="mr-1 icon-fw icon-plane"></i>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
+    case 'notification':             return <><i className="mr-1 icon-fw icon-bell"></i>{            t('external_notification.external_notification')}</>;
+    case 'slack-integration':        return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration.slack_integration') }</>;
+    case 'slack-integration-legacy': return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
+    case 'users':                    return <><i className="mr-1 icon-fw icon-user"></i>{            t('user_management.user_management') }</>;
+    case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
+    case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
+    case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{          t('plugins.plugins')}</>;
+    case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
+    case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
+    default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
+      /* eslint-enable no-multi-spaces, max-len */
+  }
+};
+
+type MenuLinkProps = {
+  menu: string,
+  isListGroupItems: boolean,
+  isRoot?: boolean,
+  isActive?: boolean,
+}
+
+const MenuLink = ({
+  menu, isRoot, isListGroupItems, isActive,
+}: MenuLinkProps) => {
+
+  const pageTransitionClassName = isListGroupItems
+    ? 'list-group-item list-group-item-action border-0 round-corner'
+    : 'dropdown-item px-3 py-2';
+
+  const href = isRoot ? '/admin' : urljoin('/admin', menu);
+
+  return (
+    <Link
+      href={href}
+      className={`${pageTransitionClassName} ${isActive ? 'active' : ''}`}
+    >
+      <MenuLabel menu={menu} />
+    </Link>
+  );
+};
+
+export const AdminNavigation = (): JSX.Element => {
+  const pathname = window.location.pathname;
+
+  const { data: growiCloudUri } = useGrowiCloudUri();
+  const { data: growiAppIdForGrowiCloud } = useGrowiAppIdForGrowiCloud();
+
+  const isActiveMenu = useCallback((path: string | string[]) => {
+    const paths = Array.isArray(path) ? path : [path];
+
+    return paths.some((path) => {
+      const basisPath = pathUtils.normalizePath(urljoin('/admin', path));
+      const basisParentPath = pathUtils.addTrailingSlash(basisPath);
+
+      return (
+        pathname === basisPath
+        || pathname.startsWith(basisParentPath)
+      );
+    });
+
+  }, [pathname]);
+
+  const getListGroupItemOrDropdownItemList = (isListGroupItems: boolean) => {
+    return (
+      <>
+        {/* eslint-disable no-multi-spaces */}
+        <MenuLink menu="home"                       isListGroupItems={isListGroupItems} isActive={pathname === '/admin'} isRoot />
+        <MenuLink menu="app"                        isListGroupItems={isListGroupItems} isActive={isActiveMenu('/app')} />
+        <MenuLink menu="security"                   isListGroupItems={isListGroupItems} isActive={isActiveMenu('/security')} />
+        <MenuLink menu="markdown"                   isListGroupItems={isListGroupItems} isActive={isActiveMenu('/markdown')} />
+        <MenuLink menu="customize"                  isListGroupItems={isListGroupItems} isActive={isActiveMenu('/customize')} />
+        <MenuLink menu="importer"                   isListGroupItems={isListGroupItems} isActive={isActiveMenu('/importer')} />
+        <MenuLink menu="export"                     isListGroupItems={isListGroupItems} isActive={isActiveMenu('/export')} />
+        <MenuLink menu="data-transfer"              isListGroupItems={isListGroupItems} isActive={isActiveMenu('/data-transfer')} />
+        <MenuLink menu="notification"               isListGroupItems={isListGroupItems} isActive={isActiveMenu(['/notification', '/global-notification'])} />
+        <MenuLink menu="slack-integration"          isListGroupItems={isListGroupItems} isActive={isActiveMenu('/slack-integration')} />
+        <MenuLink menu="slack-integration-legacy"   isListGroupItems={isListGroupItems} isActive={isActiveMenu('/slack-integration-legacy')} />
+        <MenuLink menu="users"                      isListGroupItems={isListGroupItems} isActive={isActiveMenu('/users')} />
+        <MenuLink menu="user-groups"                isListGroupItems={isListGroupItems} isActive={isActiveMenu(['/user-groups', 'user-group-detail'])} />
+        <MenuLink menu="audit-log"                  isListGroupItems={isListGroupItems} isActive={isActiveMenu('/audit-log')} />
+        <MenuLink menu="plugins"                    isListGroupItems={isListGroupItems} isActive={isActiveMenu('/plugins')} />
+        <MenuLink menu="search"                     isListGroupItems={isListGroupItems} isActive={isActiveMenu('/search')} />
+        {growiCloudUri != null && growiAppIdForGrowiCloud != null
+          && (
+            <a
+              href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
+              className="list-group-item list-group-item-action border-0 round-corner"
+            >
+              <MenuLabel menu="cloud" />
+            </a>
+          )
+        }
+        {/* eslint-enable no-multi-spaces */}
+      </>
+    );
+  };
+
+  return (
+    <React.Fragment>
+      {/* List group */}
+      <div className="list-group admin-navigation sticky-top d-none d-lg-block">
+        {getListGroupItemOrDropdownItemList(true)}
+      </div>
+
+      {/* Dropdown */}
+      <div className="dropdown d-block d-lg-none mb-5">
+        <button
+          className="btn btn-outline-primary btn-lg dropdown-toggle col-12 text-right"
+          type="button"
+          id="dropdown-admin-navigation"
+          data-display="static"
+          data-toggle="dropdown"
+          aria-haspopup="true"
+          aria-expanded="false"
+        >
+          <span className="float-left">
+            {/* eslint-disable no-multi-spaces */}
+            {pathname === '/admin'                  && <MenuLabel menu="home" />}
+            {isActiveMenu('/app')                   && <MenuLabel menu="app" />}
+            {isActiveMenu('/security')              && <MenuLabel menu="security" />}
+            {isActiveMenu('/markdown')              && <MenuLabel menu="markdown" />}
+            {isActiveMenu('/customize')             && <MenuLabel menu="customize" />}
+            {isActiveMenu('/importer')              && <MenuLabel menu="importer" />}
+            {isActiveMenu('/export')                && <MenuLabel menu="export" />}
+            {(isActiveMenu(['/notification', '/global-notification']))
+                                                    && <MenuLabel menu="notification" />}
+            {isActiveMenu('/slack-integration')     && <MenuLabel menu="slack-integration" />}
+            {isActiveMenu('/users')                 && <MenuLabel menu="users" />}
+            {isActiveMenu(['/user-groups', 'user-group-detail'])
+                                                    && <MenuLabel menu="user-groups" />}
+            {isActiveMenu('/search')                && <MenuLabel menu="search" />}
+            {isActiveMenu('/audit-log')             && <MenuLabel menu="audit-log" />}
+            {isActiveMenu('/plugins')               && <MenuLabel menu="plugins" />}
+            {isActiveMenu('/data-transfer')         && <MenuLabel menu="data-transfer" />}
+            {/* eslint-enable no-multi-spaces */}
+          </span>
+        </button>
+        <div className="dropdown-menu" aria-labelledby="dropdown-admin-navigation">
+          {getListGroupItemOrDropdownItemList(false)}
+        </div>
+      </div>
+
+    </React.Fragment>
+  );
+};

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx

@@ -1,6 +1,6 @@
 import React, { useMemo } from 'react';
 
-import { GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
+import { type GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 import { ThemeColorBox } from './ThemeColorBox';

+ 3 - 2
apps/app/src/components/Admin/Notification/ManageGlobalNotification.tsx

@@ -3,6 +3,7 @@ import React, {
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 import { useRouter } from 'next/router';
 
 import { NotifyType, TriggerEventType } from '~/client/interfaces/global-notification';
@@ -111,10 +112,10 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
   return (
     <>
       <div className="my-3">
-        <a href="/admin/notification#global-notification" className="btn btn-outline-secondary">
+        <Link href="/admin/notification" className="btn btn-outline-secondary">
           <i className="icon-fw ti ti-arrow-left" aria-hidden="true"></i>
           {t('notification_settings.back_to_list')}
-        </a>
+        </Link>
       </div>
 
 

+ 1 - 1
apps/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx

@@ -1,7 +1,7 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
 
-import { pathUtils } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import urljoin from 'url-join';

+ 1 - 1
apps/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { pathUtils } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import urljoin from 'url-join';

+ 1 - 1
apps/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { pathUtils } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import urljoin from 'url-join';

+ 1 - 1
apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -1,7 +1,7 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
 
-import { pathUtils } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { Collapse } from 'reactstrap';

+ 1 - 1
apps/app/src/components/Admin/Security/ShareLinkSetting.tsx

@@ -8,8 +8,8 @@ import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurit
 import { apiv3Delete } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 
+import ShareLinkList from '../../PageAccessoriesModal/ShareLink/ShareLinkList';
 import PaginationWrapper from '../../PaginationWrapper';
-import ShareLinkList from '../../ShareLink/ShareLinkList';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import DeleteAllShareLinksModal from './DeleteAllShareLinksModal';

+ 1 - 1
apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -2,12 +2,12 @@ import React, {
   FC, useCallback, useState, useMemo,
 } from 'react';
 
+import type { IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import { IUserGroupHasId } from '~/interfaces/user';
 
 /**
  * Delete User Group Select component

+ 1 - 2
apps/app/src/components/Admin/UserGroup/UserGroupDropdown.tsx

@@ -1,9 +1,8 @@
 import React, { FC, useCallback } from 'react';
 
+import type { IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
-import type { IUserGroupHasId } from '~/interfaces/user';
-
 type Props = {
   selectableUserGroups?: IUserGroupHasId[]
   onClickAddExistingUserGroupButton?(userGroup: IUserGroupHasId | null): void

+ 1 - 2
apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -1,10 +1,9 @@
 import React, { FC, useCallback, useState } from 'react';
 
+import type { IUserGroupHasId } from '@growi/core';
 import dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 
-import { IUserGroupHasId } from '~/interfaces/user';
-
 type Props = {
   userGroup: IUserGroupHasId,
   parentUserGroup?: IUserGroupHasId,

+ 1 - 3
apps/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -2,14 +2,12 @@ import React, {
   FC, useState, useEffect, useCallback,
 } from 'react';
 
-import { Ref } from '@growi/core';
+import type { Ref, IUserGroup, IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
-
 type Props = {
   userGroup?: IUserGroupHasId,
   buttonLabel?: string,

+ 1 - 1
apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -1,11 +1,11 @@
 import React, { FC, useState, useCallback } from 'react';
 
+import type { IUserGroup, IUserGroupHasId } from '@growi/core';
 import dynamic from 'next/dynamic';
 import { useTranslation } from 'react-i18next';
 
 import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 import { useIsAclEnabled } from '~/stores/context';
 import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
 

+ 3 - 2
apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -5,6 +5,7 @@ import React, {
 import type { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '@growi/core';
 import dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 
 
 type Props = {
@@ -147,7 +148,7 @@ export const UserGroupTable: FC<Props> = (props: Props) => {
               <tr key={group._id}>
                 {props.isAclEnabled
                   ? (
-                    <td><a href={`/admin/user-group-detail/${group._id}`}>{group.name}</a></td>
+                    <td><Link href={`/admin/user-group-detail/${group._id}`}>{group.name}</Link></td>
                   )
                   : (
                     <td>{group.name}</td>
@@ -168,7 +169,7 @@ export const UserGroupTable: FC<Props> = (props: Props) => {
                         <li key={group._id} className="list-inline-item badge badge-success">
                           {props.isAclEnabled
                             ? (
-                              <a href={`/admin/user-group-detail/${group._id}`}>{group.name}</a>
+                              <Link href={`/admin/user-group-detail/${group._id}`}>{group.name}</Link>
                             )
                             : (
                               <p>{group.name}</p>

+ 4 - 4
apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -2,7 +2,8 @@ import React, {
   useState, useCallback, useEffect, useMemo,
 } from 'react';
 
-import { objectIdUtils } from '@growi/core';
+import type { IUserGroup, IUserGroupHasId } from '@growi/core';
+import { objectIdUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Link from 'next/link';
@@ -12,7 +13,6 @@ import {
   apiv3Get, apiv3Put, apiv3Delete, apiv3Post,
 } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 import { SearchTypes, SearchType } from '~/interfaces/user-group';
 import Xss from '~/services/xss';
 import { useIsAclEnabled } from '~/stores/context';
@@ -334,7 +334,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
       <nav aria-label="breadcrumb">
         <ol className="breadcrumb">
           <li className="breadcrumb-item">
-            <Link href="/admin/user-groups" prefetch={false}>
+            <Link href="/admin/user-groups">
               {t('user_group_management.group_list')}
             </Link>
           </li>
@@ -348,7 +348,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
                 { ancestorUserGroup._id === currentUserGroupId ? (
                   <span>{ancestorUserGroup.name}</span>
                 ) : (
-                  <Link href={`/admin/user-group-detail/${ancestorUserGroup._id}`} prefetch={false}>
+                  <Link href={`/admin/user-group-detail/${ancestorUserGroup._id}`}>
                     {ancestorUserGroup.name}
                   </Link>
                 ) }

+ 7 - 3
apps/app/src/components/Admin/UserGroupDetail/UserGroupPageList.tsx

@@ -1,10 +1,10 @@
 import React, { useEffect, useState, useCallback } from 'react';
 
+import type { IPageHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
-import { IPageHasId } from '~/interfaces/page';
 
 import { PageListItemS } from '../../PageList/PageListItemS';
 import PaginationWrapper from '../../PaginationWrapper';
@@ -49,8 +49,12 @@ const UserGroupPageList = (props: Props): JSX.Element => {
 
   return (
     <>
-      <ul className="page-list-ul page-list-ul-flat mb-3">
-        {currentPages.map(page => <li key={page._id}><PageListItemS page={page} /></li>)}
+      <ul className="page-list-ul page-list-ul-flat mt-3 mb-3">
+        { currentPages.map(page => (
+          <li key={page._id} className="mt-2">
+            <PageListItemS page={page} />
+          </li>
+        )) }
       </ul>
       {relatedPages != null && relatedPages.length === 0 ? <p>{t('user_group_management.no_pages')}</p> : (
         <PaginationWrapper

+ 1 - 1
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
+import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';

+ 1 - 1
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.tsx

@@ -1,11 +1,11 @@
 import React from 'react';
 
+import type { IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
-import { IUserGroupHasId } from '~/interfaces/user';
 import { SearchTypes, SearchType } from '~/interfaces/user-group';
 
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';

+ 1 - 1
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
+import { UserPicture } from '@growi/ui/dist/components';
 import dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 

+ 1 - 1
apps/app/src/components/Admin/Users/UserMenu.tsx

@@ -1,6 +1,6 @@
 import React, { useState, useCallback } from 'react';
 
-import { IUserHasId, USER_STATUS } from '@growi/core';
+import { type IUserHasId, USER_STATUS } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   UncontrolledDropdown, DropdownToggle, DropdownMenu,

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

@@ -1,7 +1,7 @@
 import React, { useCallback } from 'react';
 
 import type { IUserHasId } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
+import { UserPicture } from '@growi/ui/dist/components';
 import dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 

+ 4 - 3
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -2,6 +2,7 @@ import {
   FC, useCallback, useState,
 } from 'react';
 
+import type { IPageToDeleteWithMeta } from '@growi/core';
 import { DropdownToggle } from 'reactstrap';
 
 import {
@@ -13,7 +14,6 @@ import { TriangleIcon } from '~/components/Icons/TriangleIcon';
 import {
   BookmarkFolderItems, DragItemDataType, DragItemType, DRAG_ITEM_TYPE,
 } from '~/interfaces/bookmark-info';
-import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
 
@@ -121,7 +121,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     }
   };
 
-  const isDropable = (item: DragItemDataType, type: string | null| symbol): boolean => {
+  const isDropable = (item: DragItemDataType, type: string | null | symbol): boolean => {
     if (type === DRAG_ITEM_TYPE.FOLDER) {
       if (item.bookmarkFolder.parent === bookmarkFolder._id || item.bookmarkFolder._id === bookmarkFolder._id) {
         return false;
@@ -143,6 +143,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     return true;
   };
 
+
   const renderChildFolder = () => {
     return isOpen && children?.map((childFolder) => {
       return (
@@ -256,7 +257,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
               </div>
             </>
           )}
-          { isOperable && (
+          {isOperable && (
             <div className="grw-foldertree-control d-flex">
               <BookmarkFolderItemControl
                 onClickRename={onClickRenameHandler}

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

@@ -120,7 +120,7 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
           className={'grw-bookmark-folder-menu-item text-danger'}
         >
           <i className="fa fa-bookmark"></i>{' '}
-          <span className="mx-2 ">
+          <span className="mx-2">
             {t('bookmark_folder.cancel_bookmark')}
           </span>
         </DropdownItem>
@@ -143,7 +143,7 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
               </div>
             </div>
             {bookmarkFolders?.map(folder => (
-              <div key={folder._id}>
+              <React.Fragment key={`bookmark-folders-${folder._id}`}>
                 <div
                   className="dropdown-item grw-bookmark-folder-menu-item list-group-item list-group-item-action border-0 py-0"
                   style={{ paddingLeft: '40px' }}
@@ -174,7 +174,7 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
                     </div>
                   </div>
                 ))}
-              </div>
+              </React.Fragment>
             ))}
           </>
         )}

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

@@ -64,7 +64,7 @@ $grw-bookmark-item-padding-left: 35px;
 
     .grw-bookmark-item-list{
       min-width: 30px;
-      height: 35px;
+      height: 50px;
 
       .picture {
         width: 16px;

+ 11 - 7
apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -1,10 +1,11 @@
 
 import React, { useCallback } from 'react';
 
+import type { IPageToDeleteWithMeta } from '@growi/core';
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 
 import { toastSuccess } from '~/client/util/toastr';
-import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import {
   useSWRxUserBookmarks, useSWRMUTxCurrentUserBookmarks,
@@ -12,7 +13,7 @@ import {
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useIsReadOnlyUser } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
-import { useSWRMUTxPageInfo, useSWRxCurrentPage } from '~/stores/page';
+import { mutateAllPageInfo, useSWRMUTxPageInfo, useSWRxCurrentPage } from '~/stores/page';
 
 import { BookmarkFolderItem } from './BookmarkFolderItem';
 import { BookmarkItem } from './BookmarkItem';
@@ -36,6 +37,7 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
 
   // const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const { t } = useTranslation();
+  const router = useRouter();
 
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPage } = useSWRxCurrentPage();
@@ -55,13 +57,15 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
   const onClickDeleteMenuItemHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
     const pageDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
       if (typeof pathOrPathsToDelete !== 'string') return;
-
-      toastSuccess(isCompletely ? t('deleted_pages_completely', { pathOrPathsToDelete }) : t('deleted_pages', { pathOrPathsToDelete }));
-
+      toastSuccess(isCompletely ? t('deleted_pages_completely', { path: pathOrPathsToDelete }) : t('deleted_pages', { path: pathOrPathsToDelete }));
       bookmarkFolderTreeMutation();
+      mutateAllPageInfo();
+      if (pageToDelete.data._id === currentPage?._id && _isRecursively) {
+        router.push(`/trash${currentPage.path}`);
+      }
     };
     openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
-  }, [openDeleteModal, t, bookmarkFolderTreeMutation]);
+  }, [openDeleteModal, t, bookmarkFolderTreeMutation, currentPage?._id, currentPage?.path, router]);
 
   /* TODO: update in bookmarks folder v2. */
   // const itemDropHandler = async(item: DragItemDataType, dragType: string | null | symbol) => {
@@ -98,7 +102,7 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
   // };
 
   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`}>
         {bookmarkFolders?.map((bookmarkFolder) => {
           return (

+ 45 - 18
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -2,17 +2,21 @@ import React, { useCallback, useState } from 'react';
 
 import nodePath from 'path';
 
-import { DevidedPagePath, pathUtils } from '@growi/core';
+import type { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '@growi/core';
+import { DevidedPagePath } from '@growi/core/dist/models';
+import { pathUtils } from '@growi/core/dist/utils';
+import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 
-import { bookmark, unbookmark } from '~/client/services/page-operation';
+
+import { bookmark, unbookmark, unlink } from '~/client/services/page-operation';
 import { addBookmarkToFolder, renamePage } from '~/client/util/bookmark-utils';
 import { ValidationTarget } from '~/client/util/input-validator';
-import { toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { BookmarkFolderItems, DragItemDataType, DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
-import { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
-import { useSWRxPageInfo } from '~/stores/page';
+import { usePutBackPageModal } from '~/stores/modal';
+import { mutateAllPageInfo, useSWRMUTxCurrentPage, useSWRxPageInfo } from '~/stores/page';
 
 import ClosableTextInput from '../Common/ClosableTextInput';
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
@@ -29,7 +33,7 @@ type Props = {
   parentFolder: BookmarkFolderItems | null,
   canMoveToRoot: boolean,
   onClickDeleteMenuItemHandler: (pageToDelete: IPageToDeleteWithMeta) => void,
-  bookmarkFolderTreeMutation: () => void
+  bookmarkFolderTreeMutation: () => void,
 }
 
 export const BookmarkItem = (props: Props): JSX.Element => {
@@ -37,15 +41,17 @@ export const BookmarkItem = (props: Props): JSX.Element => {
   const BASE_BOOKMARK_PADDING = 20;
 
   const { t } = useTranslation();
+  const router = useRouter();
 
   const {
     isReadOnlyUser, isOperable, bookmarkedPage, onClickDeleteMenuItemHandler,
     parentFolder, level, canMoveToRoot, bookmarkFolderTreeMutation,
   } = props;
-
+  const { open: openPutBackPageModal } = usePutBackPageModal();
   const [isRenameInputShown, setRenameInputShown] = useState(false);
 
   const { data: pageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(bookmarkedPage._id);
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const dPagePath = new DevidedPagePath(bookmarkedPage.path, false, true);
   const { latter: pageTitle, former: formerPagePath } = dPagePath;
   const bookmarkItemId = `bookmark-item-${bookmarkedPage._id}`;
@@ -116,6 +122,24 @@ export const BookmarkItem = (props: Props): JSX.Element => {
     onClickDeleteMenuItemHandler(pageToDelete);
   }, [bookmarkedPage._id, bookmarkedPage.path, bookmarkedPage.revision, onClickDeleteMenuItemHandler]);
 
+  const putBackClickHandler = useCallback(() => {
+    const { _id: pageId, path } = bookmarkedPage;
+    const putBackedHandler = async() => {
+      try {
+        await unlink(path);
+        mutateAllPageInfo();
+        bookmarkFolderTreeMutation();
+        router.push(`/${pageId}`);
+        mutateCurrentPage();
+        toastSuccess(t('page_has_been_reverted', { path }));
+      }
+      catch (err) {
+        toastError(err);
+      }
+    };
+    openPutBackPageModal({ pageId, path }, { onPutBacked: putBackedHandler });
+  }, [bookmarkedPage, openPutBackPageModal, bookmarkFolderTreeMutation, router, mutateCurrentPage, t]);
+
   return (
     <DragAndDropWrapper
       item={dragItem}
@@ -128,15 +152,17 @@ export const BookmarkItem = (props: Props): JSX.Element => {
         id={bookmarkItemId}
         style={{ paddingLeft }}
       >
-        { isRenameInputShown ? (
-          <ClosableTextInput
-            value={nodePath.basename(bookmarkedPage.path ?? '')}
-            placeholder={t('Input page name')}
-            onClickOutside={() => { setRenameInputShown(false) }}
-            onPressEnter={pressEnterForRenameHandler}
-            validationTarget={ValidationTarget.PAGE}
-          />
-        ) : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle}/>}
+        { isRenameInputShown
+          ? (
+            <ClosableTextInput
+              value={nodePath.basename(bookmarkedPage.path ?? '')}
+              placeholder={t('Input page name')}
+              onClickOutside={() => { setRenameInputShown(false) }}
+              onPressEnter={pressEnterForRenameHandler}
+              validationTarget={ValidationTarget.PAGE}
+            />
+          )
+          : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle} isNarrowView />}
 
         <div className='grw-foldertree-control'>
           <PageItemControl
@@ -148,8 +174,9 @@ export const BookmarkItem = (props: Props): JSX.Element => {
             onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
             onClickRenameMenuItem={renameMenuItemClickHandler}
             onClickDeleteMenuItem={deleteMenuItemClickHandler}
-            additionalMenuItemOnTopRenderer={canMoveToRoot && isOperable
-              ? () => <BookmarkMoveToRootBtn pageId={bookmarkedPage._id} onClickMoveToRootHandler={onClickMoveToRootHandler}/>
+            onClickRevertMenuItem={putBackClickHandler}
+            additionalMenuItemOnTopRenderer={canMoveToRoot
+              ? () => <BookmarkMoveToRootBtn pageId={bookmarkedPage._id} onClickMoveToRootHandler={onClickMoveToRootHandler} />
               : undefined}
           >
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">

+ 13 - 6
apps/app/src/components/Comments.tsx

@@ -1,11 +1,12 @@
 import React, { useEffect, useRef } from 'react';
 
-import { type IRevisionHasId, pagePathUtils } from '@growi/core';
+import type { IRevisionHasId } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
 import dynamic from 'next/dynamic';
 
 import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
-import { useIsTrashPage } from '~/stores/page';
+import { useIsTrashPage, useSWRMUTxPageInfo } from '~/stores/page';
 
 import { useCurrentUser } from '../stores/context';
 
@@ -32,6 +33,7 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   } = props;
 
   const { mutate } = useSWRxPageComment(pageId);
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
   const { data: isDeleted } = useIsTrashPage();
   const { data: currentUser } = useCurrentUser();
 
@@ -41,8 +43,8 @@ export const Comments = (props: CommentsProps): JSX.Element => {
     const parent = pageCommentParentRef.current;
     if (parent == null) return;
 
-    const observerCallback = (mutationRecords:MutationRecord[]) => {
-      mutationRecords.forEach((record:MutationRecord) => {
+    const observerCallback = (mutationRecords: MutationRecord[]) => {
+      mutationRecords.forEach((record: MutationRecord) => {
         const target = record.target as HTMLElement;
 
         for (const child of Array.from(target.children)) {
@@ -69,6 +71,11 @@ export const Comments = (props: CommentsProps): JSX.Element => {
     return <></>;
   }
 
+  const onCommentButtonClickHandler = () => {
+    mutate();
+    mutatePageInfo();
+  };
+
   return (
     <div className="page-comments-row mt-5 py-4 d-edit-none d-print-none">
       <div className="container-lg">
@@ -83,12 +90,12 @@ export const Comments = (props: CommentsProps): JSX.Element => {
             hideIfEmpty={false}
           />
         </div>
-        { !isDeleted && (
+        {!isDeleted && (
           <div id="page-comment-write">
             <CommentEditor
               pageId={pageId}
               isForNewComment
-              onCommentButtonClicked={mutate}
+              onCommentButtonClicked={onCommentButtonClickHandler}
               revisionId={revision._id}
             />
           </div>

+ 3 - 3
apps/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -2,6 +2,9 @@ import React, {
   useState, useCallback, useEffect,
 } from 'react';
 
+import {
+  type IPageInfoAll, isIPageInfoForOperation,
+} from '@growi/core';
 import { getCustomModifiers } from '@growi/ui/dist/utils';
 import { useTranslation } from 'next-i18next';
 import {
@@ -9,9 +12,6 @@ import {
 } from 'reactstrap';
 
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
-import {
-  IPageInfoAll, isIPageInfoForOperation,
-} from '~/interfaces/page';
 import { IPageOperationProcessData } from '~/interfaces/page-operation';
 import { useSWRxPageInfo } from '~/stores/page';
 import loggerFactory from '~/utils/logger';

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

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { IUserHasId } from '@growi/core';
+import type { IUserHasId } from '@growi/core';
 import { Link as ScrollLink } from 'react-scroll';
 
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';

+ 1 - 1
apps/app/src/components/CreateTemplateModal.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { pathUtils } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';

+ 1 - 1
apps/app/src/components/CustomNavigation/CustomNav.tsx

@@ -2,7 +2,7 @@ import React, {
   useEffect, useState, useRef, useMemo, useCallback,
 } from 'react';
 
-import { Breakpoint } from '@growi/ui/dist/interfaces/breakpoints';
+import { Breakpoint } from '@growi/ui/dist/interfaces';
 import {
   Nav, NavItem, NavLink,
 } from 'reactstrap';

+ 6 - 11
apps/app/src/components/DescendantsPageList.tsx

@@ -1,18 +1,16 @@
 import React, { useCallback, useState } from 'react';
 
-import { useTranslation } from 'next-i18next';
-
-import { toastSuccess } from '~/client/util/toastr';
-import {
+import type {
   IDataWithMeta,
   IPageHasId,
   IPageInfoForOperation,
-} from '~/interfaces/page';
+} from '@growi/core';
+import { useTranslation } from 'next-i18next';
+
+import { toastSuccess } from '~/client/util/toastr';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
-import {
-  useIsGuestUser, useIsReadOnlyUser, useIsSharedUser,
-} from '~/stores/context';
+import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/stores/context';
 import {
   mutatePageTree,
   useSWRxPageInfoForList, useSWRxPageList,
@@ -22,7 +20,6 @@ import { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
 import PageList from './PageList/PageList';
 import PaginationWrapper from './PaginationWrapper';
 
-
 type SubstanceProps = {
   pagingResult: IPagingResult<IPageHasId> | undefined,
   activePage: number,
@@ -71,7 +68,6 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
     }
 
     mutatePageTree();
-
     if (onPagesDeleted != null) {
       onPagesDeleted(...args);
     }
@@ -81,7 +77,6 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
     toastSuccess(t('page_has_been_reverted', { path }));
 
     mutatePageTree();
-
     if (onPagePutBacked != null) {
       onPagePutBacked(path);
     }

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

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { pagePathUtils } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 
 

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

@@ -1,6 +1,6 @@
 import React, { FC } from 'react';
 
-import { DevidedPagePath } from '@growi/core';
+import { DevidedPagePath } from '@growi/core/dist/models';
 import { useTranslation } from 'next-i18next';
 
 import { useCurrentPathname } from '~/stores/context';

+ 2 - 2
apps/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -2,8 +2,8 @@ import React, {
   FC, useRef,
 } from 'react';
 
-import { HasObjectId } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
+import type { HasObjectId } from '@growi/core';
+import { UserPicture } from '@growi/ui/dist/components';
 import { DropdownItem } from 'reactstrap';
 
 import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';

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

@@ -1,6 +1,6 @@
 import React, { FC } from 'react';
 
-import { HasObjectId } from '@growi/core';
+import type { HasObjectId } from '@growi/core';
 
 import type { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 

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

@@ -2,8 +2,8 @@ import React, {
   forwardRef, ForwardRefRenderFunction, useImperativeHandle,
 } from 'react';
 
-import { HasObjectId } from '@growi/core';
-import { PagePathLabel } from '@growi/ui/dist/components/PagePath/PagePathLabel';
+import type { HasObjectId } from '@growi/core';
+import { PagePathLabel } from '@growi/ui/dist/components/PagePath';
 import { useRouter } from 'next/router';
 
 import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';

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

@@ -2,7 +2,7 @@ import React, {
   forwardRef, ForwardRefRenderFunction, useImperativeHandle,
 } from 'react';
 
-import { HasObjectId } from '@growi/core';
+import type { HasObjectId } from '@growi/core';
 import { useRouter } from 'next/router';
 
 import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';

+ 6 - 16
apps/app/src/components/InstallerForm.tsx

@@ -19,7 +19,7 @@ const InstallerForm = memo((): JSX.Element => {
   const isSupportedLang = AllLang.includes(i18n.language as Lang);
 
   const [isValidUserName, setValidUserName] = useState(true);
-  const [isSubmittingDisabled, setSubmittingDisabled] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
   const [currentLocale, setCurrentLocale] = useState(isSupportedLang ? i18n.language : Lang.en_US);
 
   const checkUserName = useCallback(async(event) => {
@@ -42,18 +42,7 @@ const InstallerForm = memo((): JSX.Element => {
   const submitHandler: FormEventHandler = useCallback(async(e: any) => {
     e.preventDefault();
 
-    if (isSubmittingDisabled) {
-      return;
-    }
-
-    setSubmittingDisabled(true);
-    setTimeout(() => {
-      setSubmittingDisabled(false);
-    }, 3000);
-
-    if (e.target.elements == null) {
-      return;
-    }
+    setIsLoading(true);
 
     const formData = e.target.elements;
 
@@ -81,6 +70,7 @@ const InstallerForm = memo((): JSX.Element => {
     catch (errs) {
       const err = errs[0];
       const code = err.code;
+      setIsLoading(false);
 
       if (code === 'failed_to_login_after_install') {
         toastError(t('installer.failed_to_login_after_install'));
@@ -89,7 +79,7 @@ const InstallerForm = memo((): JSX.Element => {
 
       toastError(t('installer.failed_to_install'));
     }
-  }, [isSubmittingDisabled, currentLocale, router, t]);
+  }, [currentLocale, router, t]);
 
   const hasErrorClass = isValidUserName ? '' : ' has-error';
   const unavailableUserId = isValidUserName
@@ -220,10 +210,10 @@ const InstallerForm = memo((): JSX.Element => {
               type="submit"
               className="btn-fill btn btn-register"
               id="register"
-              disabled={isSubmittingDisabled}
+              disabled={isLoading}
             >
               <div className="eff"></div>
-              <span className="btn-label"><i className="icon-user-follow" /></span>
+              <span className="btn-label"><i className={isLoading ? 'fa fa-spinner fa-pulse mr-1' : 'icon-user-follow'} /></span>
               <span className="btn-label-text">{ t('Create') }</span>
             </button>
           </div>

+ 6 - 13
apps/app/src/components/InvitedForm.tsx

@@ -18,13 +18,14 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
   const { t } = useTranslation();
   const router = useRouter();
   const { data: user } = useCurrentUser();
-  const [isConnectSuccess, setIsConnectSuccess] = useState<boolean>(false);
   const [loginErrors, setLoginErrors] = useState<Error[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
 
   const { invitedFormUsername, invitedFormName } = props;
 
   const submitHandler = useCallback(async(e) => {
     e.preventDefault();
+    setIsLoading(true);
 
     const formData = e.target.elements;
 
@@ -42,25 +43,17 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
 
     try {
       const res = await apiv3Post('/invited', { invitedForm });
-      setIsConnectSuccess(true);
       const { redirectTo } = res.data;
       router.push(redirectTo ?? '/');
     }
     catch (err) {
       setLoginErrors(err);
+      setIsLoading(false);
     }
   }, [router]);
 
   const formNotification = useCallback(() => {
 
-    if (isConnectSuccess) {
-      return (
-        <p className="alert alert-success">
-          <strong>{ t('message.successfully_connected') }</strong><br></br>
-        </p>
-      );
-    }
-
     return (
       <>
         { loginErrors != null && loginErrors.length > 0 ? (
@@ -77,7 +70,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
         ) }
       </>
     );
-  }, [isConnectSuccess, loginErrors, t]);
+  }, [loginErrors, t]);
 
   if (user == null) {
     return <></>;
@@ -154,9 +147,9 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
         </div>
         {/* Create Button */}
         <div className="input-group justify-content-center d-flex mt-4">
-          <button type="submit" className="btn btn-fill" id="register">
+          <button type="submit" className="btn btn-fill" id="register" disabled={isLoading}>
             <div className="eff"></div>
-            <span className="btn-label"><i className="icon-user-follow"></i></span>
+            <span className="btn-label"><i className={isLoading ? 'fa fa-spinner fa-pulse mr-1' : 'icon-user-follow'} /></span>
             <span className="btn-label-text">{t('Create')}</span>
           </button>
         </div>

+ 1 - 1
apps/app/src/components/Layout/AdminLayout.tsx

@@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
 
 import dynamic from 'next/dynamic';
 
+import { AdminNavigation } from '../Admin/Common/AdminNavigation';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 
 import { RawLayout } from './RawLayout';
@@ -9,7 +10,6 @@ import { RawLayout } from './RawLayout';
 import styles from './Admin.module.scss';
 
 
-const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
 const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
 const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });

+ 4 - 2
apps/app/src/components/Layout/BasicLayout.tsx

@@ -16,13 +16,14 @@ const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr:
 const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
 const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
 const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
+const PutbackPageModal = dynamic(() => import('../PutbackPageModal'), { ssr: false });
 // Page modals
 const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
 const PageDuplicateModal = dynamic(() => import('../PageDuplicateModal'), { ssr: false });
 const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
 const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
-const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal'), { ssr: false });
+const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal').then(mod => mod.PageAccessoriesModal), { ssr: false });
 const DeleteBookmarkFolderModal = dynamic(() => import('../DeleteBookmarkFolderModal').then(mod => mod.DeleteBookmarkFolderModal), { ssr: false });
 // Fab
 const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false });
@@ -40,7 +41,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
         <GrowiNavbar />
 
         <div className="page-wrapper d-flex d-print-block">
-          <div className="grw-sidebar-wrapper" data-testid="grw-sidebar-wrapper">
+          <div className="grw-sidebar-wrapper">
             <Sidebar />
           </div>
 
@@ -59,6 +60,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
         <PageAccessoriesModal />
         <DeleteAttachmentModal />
         <DeleteBookmarkFolderModal />
+        <PutbackPageModal />
       </DndProvider>
 
       <PagePresentationModal />

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

@@ -1,9 +1,9 @@
 import React, { FC, useState, useCallback } from 'react';
 
+import type { IUser } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 
-import { IUser } from '../interfaces/user';
 
 import UserPictureList from './User/UserPictureList';
 

+ 46 - 9
apps/app/src/components/LoginForm.tsx

@@ -48,6 +48,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
   // states
   const [isRegistering, setIsRegistering] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
   // For Login
   const [usernameForLogin, setUsernameForLogin] = useState('');
   const [passwordForLogin, setPasswordForLogin] = useState('');
@@ -93,6 +94,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const handleLoginWithLocalSubmit = useCallback(async(e) => {
     e.preventDefault();
     resetLoginErrors();
+    setIsLoading(true);
 
     const loginForm = {
       username: usernameForLogin,
@@ -112,6 +114,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     catch (err) {
       const errs = toArrayIfNot(err);
       setLoginErrors(errs);
+      setIsLoading(false);
     }
     return;
 
@@ -176,6 +179,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
     return (
       <>
+        {/* !! - DO NOT DELETE HIDDEN ELEMENT - !! -- 7.12 ryoji-s */}
+        {/* Import font-awesome to prevent MongoStore.js "Unable to find the session to touch" error */}
+        <div className='sr-only'>
+          <i className="fa fa-spinner fa-pulse" />
+        </div>
+        {/* !! - END OF HIDDEN ELEMENT - !! */}
         {isLdapSetupFailed && (
           <div className="alert alert-warning small">
             <strong><i className="icon-fw icon-info"></i>{t('login.enabled_ldap_has_configuration_problem')}</strong><br/>
@@ -214,10 +223,16 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
           </div>
 
           <div className="input-group my-4">
-            <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto" data-testid="btnSubmitForLogin">
+            <button
+              type="submit"
+              id="login"
+              className="btn btn-fill rounded-0 login mx-auto"
+              data-testid="btnSubmitForLogin"
+              disabled={isLoading}
+            >
               <div className="eff"></div>
               <span className="btn-label">
-                <i className="icon-login"></i>
+                <i className={isLoading ? 'fa fa-spinner fa-pulse mr-1' : 'icon-login'} />
               </span>
               <span className="btn-label-text">{t('Sign in')}</span>
             </button>
@@ -225,8 +240,18 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         </form>
       </>
     );
-  }, [generateDangerouslySetErrors, generateSafelySetErrors, handleLoginWithLocalSubmit,
-      isLdapSetupFailed, loginErrors, props, separateErrorsBasedOnErrorCode, t]);
+  }, [
+    props,
+    separateErrorsBasedOnErrorCode,
+    loginErrors,
+    generateDangerouslySetErrors,
+    generateSafelySetErrors,
+    isLdapSetupFailed,
+    t,
+    handleLoginWithLocalSubmit,
+    isLoading,
+  ]);
+
 
   const renderExternalAuthInput = useCallback((auth) => {
     const authIconNames = {
@@ -295,6 +320,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     e.preventDefault();
     setEmailForRegistrationOrder('');
     setIsSuccessToRagistration(false);
+    setIsLoading(true);
 
     const registerForm = {
       username: usernameForRegister,
@@ -323,6 +349,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       if (err != null || err.length > 0) {
         setRegisterErrors(err);
       }
+      setIsLoading(false);
     }
     return;
   }, [usernameForRegister, nameForRegister, emailForRegister, passwordForRegister, resetRegisterErrors, router, isEmailAuthenticationEnabled]);
@@ -478,11 +505,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             <button
               className="btn btn-fill rounded-0"
               id="register"
-              disabled={(!isMailerSetup && isEmailAuthenticationEnabled)}
+              disabled={(!isMailerSetup && isEmailAuthenticationEnabled) || isLoading}
             >
               <div className="eff"></div>
               <span className="btn-label">
-                <i className="icon-user-follow"></i>
+                <i className={isLoading ? 'fa fa-spinner fa-pulse mr-1' : 'icon-user-follow'} />
               </span>
               <span className="btn-label-text">{submitText}</span>
             </button>
@@ -493,7 +520,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
         <div className="row">
           <div className="text-right col-12 mt-2 py-2">
-            <a href="#login" id="login" className="link-switch" onClick={switchForm}>
+            <a
+              href="#login"
+              id="login"
+              className="link-switch"
+              style={{ pointerEvents: isLoading ? 'none' : 'auto' }}
+              onClick={switchForm}>
               <i className="icon-fw icon-login"></i>
               {t('Sign in is here')}
             </a>
@@ -503,7 +535,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     );
   }, [
     t, isEmailAuthenticationEnabled, registrationMode, isMailerSetup, registerErrors, isSuccessToRagistration,
-    emailForRegistrationOrder, props.username, props.name, props.email, registrationWhitelist, switchForm, handleRegisterFormSubmit,
+    emailForRegistrationOrder, props.username, props.name, props.email, registrationWhitelist, switchForm, handleRegisterFormSubmit, isLoading,
   ]);
 
   if (registrationMode === RegistrationMode.RESTRICTED && isSuccessToRagistration && !isEmailAuthenticationEnabled) {
@@ -529,7 +561,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 {/* Sign up link */}
                 {isRegistrationEnabled && (
                   <div className="text-right mb-2">
-                    <a href="#register" id="register" className="link-switch" onClick={switchForm}>
+                    <a
+                      href="#register"
+                      id="register"
+                      className="link-switch"
+                      style={{ pointerEvents: isLoading ? 'none' : 'auto' }}
+                      onClick={switchForm}>
                       <i className="ti ti-check-box"></i> {t('Sign up is here')}
                     </a>
                   </div>

+ 2 - 2
apps/app/src/components/Me/DisassociateModal.tsx

@@ -1,5 +1,6 @@
 import React, { useCallback } from 'react';
 
+import type { IExternalAccountHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal,
@@ -9,13 +10,12 @@ import {
 } from 'reactstrap';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IExternalAccount } from '~/interfaces/external-account';
 import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 type Props = {
   isOpen: boolean,
   onClose: () => void,
-  accountForDisassociate: IExternalAccount,
+  accountForDisassociate: IExternalAccountHasId,
 }
 
 

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

@@ -1,8 +1,8 @@
 import React from 'react';
 
-import { pagePathUtils } from '@growi/core';
 import type { IUser } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
+import { pagePathUtils } from '@growi/core/dist/utils';
+import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns';
 import Link from 'next/link';
 

+ 1 - 1
apps/app/src/components/Navbar/GlobalSearch.tsx

@@ -4,7 +4,7 @@ import React, {
 
 import assert from 'assert';
 
-import { pathUtils } from '@growi/core';
+import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 

+ 30 - 31
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,8 +1,11 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
-import {
-  isPopulated, IUser, pagePathUtils, IPagePopulatedToShowRevision,
+import { isPopulated } from '@growi/core';
+import type {
+  IUser, IPagePopulatedToShowRevision,
+  IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
 } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
@@ -11,9 +14,6 @@ import { DropdownItem } from 'reactstrap';
 import { exportAsMarkdown, updateContentWidth, useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { apiPost } from '~/client/util/apiv1-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import {
-  IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
-} from '~/interfaces/page';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
   useCurrentPathname,
@@ -25,8 +25,9 @@ import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
 import {
-  useSWRMUTxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useTemplateTagData,
+  useSWRMUTxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useTemplateTagData, useSWRxPageInfo,
 } from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
@@ -47,7 +48,6 @@ import type { SubNavButtonsProps } from './SubNavButtons';
 import AuthorInfoStyles from './AuthorInfo.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
-const { isUsersHomePage } = pagePathUtils;
 
 const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
 
@@ -98,7 +98,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         <i className="icon-fw grw-page-control-dropdown-icon">
           <PresentationIcon />
         </i>
-        { t('Presentation Mode') }
+        {t('Presentation Mode')}
       </DropdownItem>
 
       {/* Export markdown */}
@@ -139,7 +139,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         {t('attachment_data')}
       </DropdownItem>
 
-      { !isGuestUser && !isReadOnlyUser && !isSharedUser && (
+      {!isGuestUser && !isReadOnlyUser && !isSharedUser && (
         <NotAvailable isDisabled={isLinkSharingDisabled ?? false} title="Disabled by admin">
           <DropdownItem
             onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
@@ -152,7 +152,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
             {t('share_links.share_link_management')}
           </DropdownItem>
         </NotAvailable>
-      ) }
+      )}
     </>
   );
 };
@@ -179,7 +179,7 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme
         data-testid="open-page-template-modal-btn"
       >
         <i className="icon-fw icon-magic-wand grw-page-control-dropdown-icon"></i>
-        { t('template.option_label.create/edit') }
+        {t('template.option_label.create/edit')}
       </DropdownItem>
     </>
   );
@@ -231,6 +231,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { data: templateTagData } = useTemplateTagData();
+  const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId);
 
   const updateStateAfterSave = useUpdateStateAfterSave(pageId);
 
@@ -282,12 +283,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     return;
   }, [mutatePageTagsForEditors]);
 
-  const reload = useCallback(() => {
-    if (currentPathname != null) {
-      router.push(currentPathname);
-    }
-  }, [currentPathname, router]);
-
   const duplicateItemClickedHandler = useCallback(async(page: IPageForPageDuplicateModal) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       router.push(toPath);
@@ -297,10 +292,12 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
   const renameItemClickedHandler = useCallback(async(page: IPageToRenameWithMeta<IPageInfoForEntity>) => {
     const renamedHandler: OnRenamedFunction = () => {
-      reload();
+      mutateCurrentPage();
+      mutatePageInfo();
+      mutatePageTree();
     };
     openRenameModal(page, { onRenamed: renamedHandler });
-  }, [openRenameModal, reload]);
+  }, [mutateCurrentPage, mutatePageInfo, openRenameModal]);
 
   const deleteItemClickedHandler = useCallback((pageWithMeta: IPageWithMeta) => {
     const deletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
@@ -319,9 +316,11 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       }
 
       mutateCurrentPage();
+      mutatePageInfo();
+      mutatePageTree();
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
-  }, [currentPathname, mutateCurrentPage, openDeleteModal, router]);
+  }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]);
 
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
     if (!isSharedPage) {
@@ -341,9 +340,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
         return (
           <>
             {!isReadOnlyUser
-            && <CreateTemplateMenuItems
-              onClickTemplateMenuItem={templateMenuItemClickHandler}
-            />
+              && <CreateTemplateMenuItems
+                onClickTemplateMenuItem={templateMenuItemClickHandler}
+              />
             }
           </>);
       }
@@ -368,9 +367,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       <>
         <div className="d-flex">
           <div className="d-flex flex-column align-items-end justify-content-center py-md-2" style={{ gap: `${isCompactMode ? '5px' : '7px'}` }}>
-            { isViewMode && (
+            {isViewMode && (
               <div className="h-50">
-                { pageId != null && (
+                {pageId != null && (
                   <SubNavButtons
                     isCompactMode={isCompactMode}
                     pageId={pageId}
@@ -386,9 +385,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
                     onClickDeleteMenuItem={deleteItemClickedHandler}
                     onClickSwitchContentWidth={switchContentWidthHandler}
                   />
-                ) }
+                )}
               </div>
-            ) }
+            )}
             {isAbleToChangeEditorMode && (
               <PageEditorModeManager
                 onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
@@ -397,22 +396,22 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
               />
             )}
           </div>
-          { (isAbleToShowPageAuthors && !isCompactMode && !isUsersHomePage(path ?? '')) && (
+          {(isAbleToShowPageAuthors && !isCompactMode && !pagePathUtils.isUsersHomePage(path ?? '')) && (
             <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
               <li className="pb-1">
-                { currentPage != null
+                {currentPage != null
                   ? <AuthorInfo user={currentPage.creator as IUser} date={currentPage.createdAt} mode="create" locate="subnav" />
                   : <AuthorInfoSkeleton />
                 }
               </li>
               <li className="mt-1 pt-1 border-top">
-                { currentPage != null
+                {currentPage != null
                   ? <AuthorInfo user={currentPage.lastUpdateUser as IUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
                   : <AuthorInfoSkeleton />
                 }
               </li>
             </ul>
-          ) }
+          )}
         </div>
 
         {path != null && currentUser != null && !isReadOnlyUser && (

+ 2 - 3
apps/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 
-import { useCurrentUser, useHackmdUri } from '~/stores/context';
+import { useIsAdmin, useHackmdUri } from '~/stores/context';
 import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
 
 import styles from './PageEditorModeManager.module.scss';
@@ -47,10 +47,9 @@ function PageEditorModeManager(props) {
 
   const { t } = useTranslation();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
-  const { data: currentUser } = useCurrentUser();
   const { data: hackmdUri } = useHackmdUri();
 
-  const isAdmin = currentUser?.admin;
+  const { data: isAdmin } = useIsAdmin();
   const isHackmdEnabled = hackmdUri != null;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
 

+ 1 - 1
apps/app/src/components/Navbar/PersonalDropdown.jsx

@@ -1,6 +1,6 @@
 import { useRef, useState } from 'react';
 
-import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
+import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Link from 'next/link';

+ 12 - 6
apps/app/src/components/Navbar/SubNavButtons.tsx

@@ -1,5 +1,11 @@
 import React, { useCallback, useMemo } from 'react';
 
+import type {
+  IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta,
+} from '@growi/core';
+import {
+  isIPageInfoForEntity, isIPageInfoForOperation,
+} from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { DropdownItem } from 'reactstrap';
 
@@ -7,9 +13,6 @@ import {
   toggleLike, toggleSubscribe,
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
-import {
-  IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
-} from '~/interfaces/page';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 
@@ -202,8 +205,11 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     sumOfLikers, sumOfSeenUsers, isLiked,
   } = pageInfo;
 
-  const forceHideMenuItemsWithBookmark = forceHideMenuItems ?? [];
-  forceHideMenuItemsWithBookmark.push(MenuItemType.BOOKMARK);
+  const forceHideMenuItemsWithAdditions = [
+    ...(forceHideMenuItems ?? []),
+    MenuItemType.BOOKMARK,
+    MenuItemType.REVERT,
+  ];
 
   return (
     <div className="d-flex" style={{ gap: '2px' }}>
@@ -244,7 +250,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
           pageInfo={pageInfo}
           isEnableActions={!isGuestUser}
           isReadOnlyUser={!!isReadOnlyUser}
-          forceHideMenuItems={forceHideMenuItemsWithBookmark}
+          forceHideMenuItems={forceHideMenuItemsWithAdditions}
           additionalMenuItemOnTopRenderer={!isReadOnlyUser ? additionalMenuItemOnTopRenderer : undefined}
           additionalMenuItemRenderer={additionalMenuItemRenderer}
           onClickRenameMenuItem={renameMenuItemClickHandler}

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

@@ -10,7 +10,7 @@ export const NotCreatablePage: FC = () => {
       <div className="col-md-12">
         <h2 className="text-muted">
           <i className="icon-ban mr-1" aria-hidden="true"></i>
-          { t('not_creatable_page.could_not_creata_path') }
+          { t('not_creatable_page.message') }
         </h2>
       </div>
     </div>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio