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

Merge branch 'dev/7.0.x' into imprv/141126-141349-design-of-input-field-for-slack-channel

Shun Miyazawa 2 лет назад
Родитель
Сommit
19bd22117a
100 измененных файлов с 694 добавлено и 480 удалено
  1. 1 1
      .devcontainer/Dockerfile
  2. 6 6
      .github/workflows/ci-app-prod.yml
  3. 3 3
      .github/workflows/ci-app.yml
  4. 3 3
      .github/workflows/ci-slackbot-proxy.yml
  5. 1 1
      .github/workflows/list-unhealthy-branches.yml
  6. 1 1
      .github/workflows/release-slackbot-proxy.yml
  7. 2 2
      .github/workflows/release.yml
  8. 3 3
      .mergify.yml
  9. 1 1
      README.md
  10. 1 1
      README_JP.md
  11. 0 0
      apps/app/_obsolete/src/components/PageEditor/AbstractEditor.tsx
  12. 0 0
      apps/app/_obsolete/src/components/PageEditor/Editor.module.scss
  13. 1 1
      apps/app/_obsolete/src/components/PageEditor/Editor.tsx
  14. 0 0
      apps/app/_obsolete/src/components/PageEditor/EditorIcon.jsx
  15. 0 0
      apps/app/_obsolete/src/components/PageEditor/PasteHelper.js
  16. 0 0
      apps/app/_obsolete/src/components/PageEditor/PreventMarkdownListInterceptor.js
  17. 0 0
      apps/app/_obsolete/src/components/PageEditor/TextAreaEditor.jsx
  18. 4 4
      apps/app/docker/Dockerfile
  19. 5 5
      apps/app/package.json
  20. 4 3
      apps/app/public/static/locales/en_US/admin.json
  21. 12 0
      apps/app/public/static/locales/en_US/translation.json
  22. 4 3
      apps/app/public/static/locales/ja_JP/admin.json
  23. 12 0
      apps/app/public/static/locales/ja_JP/translation.json
  24. 4 3
      apps/app/public/static/locales/zh_CN/admin.json
  25. 12 0
      apps/app/public/static/locales/zh_CN/translation.json
  26. 1 1
      apps/app/src/client/services/create-page/use-create-template-page.ts
  27. 11 0
      apps/app/src/client/services/page-operation.ts
  28. 9 10
      apps/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  29. 5 1
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  30. 1 1
      apps/app/src/components/Admin/Security/SecurityManagementContents.jsx
  31. 2 2
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  32. 9 3
      apps/app/src/components/Admin/SlackIntegration/Bridge.jsx
  33. 1 1
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  34. 1 1
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  35. 22 23
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  36. 2 3
      apps/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  37. 4 2
      apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  38. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  39. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UpdateParentConfirmModal.tsx
  40. 4 3
      apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  41. 0 169
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  42. 122 0
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.tsx
  43. 6 5
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.tsx
  44. 2 2
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  45. 1 1
      apps/app/src/components/Admin/UserManagement.module.scss
  46. 4 3
      apps/app/src/components/Admin/UserManagement.tsx
  47. 1 1
      apps/app/src/components/Admin/Users/ExternalAccountTable.tsx
  48. 2 1
      apps/app/src/components/Admin/Users/UserMenu.tsx
  49. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderItemControl.tsx
  50. 0 4
      apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  51. 1 1
      apps/app/src/components/Bookmarks/BookmarkItem.tsx
  52. 3 2
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  53. 6 2
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  54. 4 2
      apps/app/src/components/ExpandOrContractButton.tsx
  55. 0 5
      apps/app/src/components/ItemsTree/ItemsTree.module.scss
  56. 4 2
      apps/app/src/components/ItemsTree/ItemsTree.tsx
  57. 1 1
      apps/app/src/components/Me/ExternalAccountLinkedMe.jsx
  58. 14 0
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  59. 3 1
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  60. 1 1
      apps/app/src/components/Page/PageView.tsx
  61. 2 0
      apps/app/src/components/PageAlert/PageAlerts.tsx
  62. 53 0
      apps/app/src/components/PageAlert/WipPageAlert.tsx
  63. 2 2
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  64. 3 3
      apps/app/src/components/PageComment/CommentEditor.tsx
  65. 2 2
      apps/app/src/components/PageComment/ReplyComments.tsx
  66. 1 1
      apps/app/src/components/PageControls/PageControls.tsx
  67. 1 1
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  68. 37 30
      apps/app/src/components/PageEditor/PageEditor.tsx
  69. 1 0
      apps/app/src/components/PageEditor/Preview.module.scss
  70. 7 10
      apps/app/src/components/PageEditor/Preview.tsx
  71. 5 1
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  72. 2 1
      apps/app/src/components/PagePresentationModal.tsx
  73. 7 19
      apps/app/src/components/PageTags/TagsInput.tsx
  74. 29 1
      apps/app/src/components/SavePageControls.tsx
  75. 5 5
      apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx
  76. 24 24
      apps/app/src/components/SearchTypeahead.tsx
  77. 4 6
      apps/app/src/components/Sidebar/Bookmarks.tsx
  78. 4 6
      apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx
  79. 13 4
      apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx
  80. 1 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx
  81. 3 5
      apps/app/src/components/Sidebar/InAppNotification/InAppNotification.tsx
  82. 1 1
      apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-new-page.ts
  83. 1 1
      apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-todays-memo.tsx
  84. 10 6
      apps/app/src/components/Sidebar/PageTree/PageTree.tsx
  85. 40 2
      apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx
  86. 3 2
      apps/app/src/components/Sidebar/PageTree/PrivateLegacyPagesLink.tsx
  87. 1 1
      apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx
  88. 2 2
      apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.module.scss
  89. 2 0
      apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.tsx
  90. 10 6
      apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx
  91. 0 1
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss
  92. 55 18
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  93. 3 0
      apps/app/src/components/Sidebar/SidebarContents.module.scss
  94. 1 1
      apps/app/src/components/Sidebar/SidebarHeaderReloadButton.tsx
  95. 3 3
      apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.tsx
  96. 7 8
      apps/app/src/components/Sidebar/Tag.tsx
  97. 1 1
      apps/app/src/components/TagList.tsx
  98. 1 1
      apps/app/src/components/TreeItem/NewPageInput/NewPageCreateButton.tsx
  99. 5 2
      apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx
  100. 12 4
      apps/app/src/components/TreeItem/SimpleItem.tsx

+ 1 - 1
.devcontainer/Dockerfile

@@ -3,7 +3,7 @@
 # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
 # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
 #-------------------------------------------------------------------------------------------------------------
 #-------------------------------------------------------------------------------------------------------------
 
 
-FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18
+FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-20
 
 
 # The node image includes a non-root user with sudo access. Use the
 # The node image includes a non-root user with sudo access. Use the
 # "remoteUser" property in devcontainer.json to use it. On Linux, update
 # "remoteUser" property in devcontainer.json to use it. On Linux, update

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

@@ -51,16 +51,16 @@ jobs:
   test-prod-node16:
   test-prod-node16:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@dev/7.0.x
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@dev/7.0.x
     with:
     with:
-      node-version: 16.x
+      node-version: 18.x
       skip-cypress: true
       skip-cypress: true
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
 
 
-  test-prod-node18:
+  test-prod-node20:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@dev/7.0.x
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@dev/7.0.x
     with:
     with:
-      node-version: 18.x
+      node-version: 20.x
       skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       cypress-report-artifact-name: Cypress report
       cypress-report-artifact-name: Cypress report
       cypress-config-video: ${{ inputs.cypress-config-video || false }}
       cypress-config-video: ${{ inputs.cypress-config-video || false }}
@@ -68,15 +68,15 @@ jobs:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
 
 
-  run-reg-suit-node18:
-    needs: [test-prod-node18]
+  run-reg-suit-node20:
+    needs: [test-prod-node20]
 
 
     uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@dev/7.0.x
     uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@dev/7.0.x
 
 
     if: always()
     if: always()
 
 
     with:
     with:
-      node-version: 18.x
+      node-version: 20.x
       skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       cypress-report-artifact-name: Cypress report
       cypress-report-artifact-name: Cypress report
     secrets:
     secrets:

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

@@ -27,7 +27,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
 
     steps:
     steps:
       - uses: actions/checkout@v3
       - uses: actions/checkout@v3
@@ -92,7 +92,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
 
     services:
     services:
       mongodb:
       mongodb:
@@ -174,7 +174,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
 
     services:
     services:
       mongodb:
       mongodb:

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

@@ -29,7 +29,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v3
     - uses: actions/checkout@v3
@@ -94,7 +94,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
 
     services:
     services:
       mysql:
       mysql:
@@ -179,7 +179,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
 
     services:
     services:
       mysql:
       mysql:

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

@@ -16,7 +16,7 @@ jobs:
 
 
     - uses: actions/setup-node@v3
     - uses: actions/setup-node@v3
       with:
       with:
-        node-version: '16'
+        node-version: '18'
 
 
     - name: List branches
     - name: List branches
       id: list-branches
       id: list-branches

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

@@ -102,7 +102,7 @@ jobs:
 
 
     - uses: actions/setup-node@v3
     - uses: actions/setup-node@v3
       with:
       with:
-        node-version: '16'
+        node-version: '18'
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 

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

@@ -24,7 +24,7 @@ jobs:
 
 
     - uses: actions/setup-node@v3
     - uses: actions/setup-node@v3
       with:
       with:
-        node-version: '18'
+        node-version: '20'
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 
@@ -189,7 +189,7 @@ jobs:
 
 
     - uses: actions/setup-node@v3
     - uses: actions/setup-node@v3
       with:
       with:
-        node-version: '18'
+        node-version: '20'
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 

+ 3 - 3
.mergify.yml

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

+ 1 - 1
README.md

@@ -79,7 +79,7 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 
 
 ## Dependencies
 ## Dependencies
 
 
-- Node.js v16.x or v18.x
+- Node.js v18.x or v20.x
 - npm 6.x
 - npm 6.x
 - yarn
 - yarn
 - [Turborepo](https://turbo.build/repo)
 - [Turborepo](https://turbo.build/repo)

+ 1 - 1
README_JP.md

@@ -78,7 +78,7 @@ Crowi からの移行は **[こちら](https://docs.growi.org/en/admin-guide/mig
 
 
 ## 依存関係
 ## 依存関係
 
 
-- Node.js v16.x or v18.x
+- Node.js v18.x or v20.x
 - npm 6.x
 - npm 6.x
 - yarn
 - yarn
 - [Turborepo](https://turbo.build/repo)
 - [Turborepo](https://turbo.build/repo)

+ 0 - 0
apps/app/src/components/PageEditor/AbstractEditor.tsx → apps/app/_obsolete/src/components/PageEditor/AbstractEditor.tsx


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


+ 1 - 1
apps/app/src/components/PageEditor/Editor.tsx → apps/app/_obsolete/src/components/PageEditor/Editor.tsx

@@ -343,7 +343,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
               className="btn btn-outline-secondary btn-open-dropzone"
               className="btn btn-outline-secondary btn-open-dropzone"
               onClick={addAttachmentHandler}
               onClick={addAttachmentHandler}
             >
             >
-              <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
+              <span className="material-symbols-outlined" aria-hidden="true">attachment</span>&nbsp;
               Attach files
               Attach files
               <span className="d-none d-sm-inline">
               <span className="d-none d-sm-inline">
               &nbsp;by dragging &amp; dropping,&nbsp;
               &nbsp;by dragging &amp; dropping,&nbsp;

+ 0 - 0
apps/app/src/components/PageEditor/EditorIcon.jsx → apps/app/_obsolete/src/components/PageEditor/EditorIcon.jsx


+ 0 - 0
apps/app/src/components/PageEditor/PasteHelper.js → apps/app/_obsolete/src/components/PageEditor/PasteHelper.js


+ 0 - 0
apps/app/src/components/PageEditor/PreventMarkdownListInterceptor.js → apps/app/_obsolete/src/components/PageEditor/PreventMarkdownListInterceptor.js


+ 0 - 0
apps/app/src/components/PageEditor/TextAreaEditor.jsx → apps/app/_obsolete/src/components/PageEditor/TextAreaEditor.jsx


+ 4 - 4
apps/app/docker/Dockerfile

@@ -4,7 +4,7 @@
 ##
 ##
 ## base
 ## base
 ##
 ##
-FROM node:18-slim AS base
+FROM node:20-slim AS base
 
 
 ENV optDir /opt
 ENV optDir /opt
 
 
@@ -18,7 +18,7 @@ RUN turbo prune --scope=@growi/app --docker
 ##
 ##
 ## deps-resolver
 ## deps-resolver
 ##
 ##
-FROM node:18-slim AS deps-resolver
+FROM node:20-slim AS deps-resolver
 
 
 ENV optDir /opt
 ENV optDir /opt
 
 
@@ -62,7 +62,7 @@ RUN tar -cf node_modules.tar \
 ##
 ##
 ## builder
 ## builder
 ##
 ##
-FROM node:18-slim AS builder
+FROM node:20-slim AS builder
 
 
 ENV optDir /opt
 ENV optDir /opt
 
 
@@ -107,7 +107,7 @@ RUN tar -cf packages.tar \
 ##
 ##
 ## release
 ## release
 ##
 ##
-FROM node:18-slim
+FROM node:20-slim
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
 
 ENV NODE_ENV production
 ENV NODE_ENV production

+ 5 - 5
apps/app/package.json

@@ -62,7 +62,7 @@
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/s3-request-presigner": "3.454.0",
     "@aws-sdk/s3-request-presigner": "3.454.0",
-    "@azure/identity": "^3.3.2",
+    "@azure/identity": "^4.0.1",
     "@azure/storage-blob": "^12.16.0",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
@@ -101,7 +101,7 @@
     "connect-redis": "^4.0.4",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "cookie-parser": "^1.4.5",
     "csurf": "^1.11.0",
     "csurf": "^1.11.0",
-    "csv-to-markdown-table": "^1.1.0",
+    "csv-to-markdown-table": "^1.4.1",
     "date-fns": "^2.23.0",
     "date-fns": "^2.23.0",
     "dayjs": "^1.11.7",
     "dayjs": "^1.11.7",
     "detect-indent": "^7.0.0",
     "detect-indent": "^7.0.0",
@@ -131,7 +131,7 @@
     "is-iso-date": "^0.0.1",
     "is-iso-date": "^0.0.1",
     "ldapjs": "^3.0.2",
     "ldapjs": "^3.0.2",
     "lucene-query-parser": "^1.2.0",
     "lucene-query-parser": "^1.2.0",
-    "markdown-table": "^1.1.1",
+    "markdown-table": "^3.0.3",
     "md5": "^2.2.1",
     "md5": "^2.2.1",
     "mermaid": "^10.1.0",
     "mermaid": "^10.1.0",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
@@ -163,7 +163,7 @@
     "qs": "^6.11.1",
     "qs": "^6.11.1",
     "rate-limiter-flexible": "^2.3.7",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react": "^18.2.0",
-    "react-bootstrap-typeahead": "^5.2.2",
+    "react-bootstrap-typeahead": "^6.3.2",
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
     "react-datepicker": "^4.7.0",
     "react-datepicker": "^4.7.0",
     "react-disable": "^0.1.1",
     "react-disable": "^0.1.1",
@@ -214,7 +214,7 @@
     "xss": "^1.0.14",
     "xss": "^1.0.14",
     "y-mongodb-provider": "^0.1.7",
     "y-mongodb-provider": "^0.1.7",
     "y-socket.io": "^1.1.0",
     "y-socket.io": "^1.1.0",
-    "yjs": "^13.6.7"
+    "yjs": "^13.6.12"
   },
   },
   "// comments for defDependencies": {
   "// comments for defDependencies": {
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",

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

@@ -746,7 +746,7 @@
       "description1":"Temporarily issue new users by email addresses.",
       "description1":"Temporarily issue new users by email addresses.",
       "description2":"A temporary password will be generated for the first login.",
       "description2":"A temporary password will be generated for the first login.",
       "invite_thru_email": "Send invitation email",
       "invite_thru_email": "Send invitation email",
-      "mail_setting_link":"<i class='icon-settings me-2'></i><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link":"<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "Valid email address is required",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
       "send_new_password": "Please send the new password to the user.",
@@ -834,9 +834,10 @@
       "dropdown_desc": "Choose an action for private pages",
       "dropdown_desc": "Choose an action for private pages",
       "select_group": "Select a group",
       "select_group": "Select a group",
       "no_groups": "No groups to select",
       "no_groups": "No groups to select",
-      "publish_pages": "Publish all",
+      "publish_pages": "Publish pages that are publishable",
       "delete_pages": "Delete all",
       "delete_pages": "Delete all",
-      "transfer_pages": "Transfer to another group"
+      "transfer_pages": "Transfer to another group",
+      "option_explanation": "A \"publishable\" page is a page visible only to the group you want to delete. Pages that can be viewed by other groups will not be published."
     },
     },
     "update_parent_confirm_modal": {
     "update_parent_confirm_modal": {
       "header": "The parent of the group will be changed",
       "header": "The parent of the group will be changed",

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

@@ -826,5 +826,17 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "Select page location"
     "select_page_location": "Select page location"
+  },
+  "wip_page": {
+    "save_as_wip": "Save as WIP (Currently drafting)",
+    "success_save_as_wip": "Successfully saved as a WIP page",
+    "fail_save_as_wip": "Failed to save as a WIP page",
+    "alert": "This page is a work in progress",
+    "publish_page": "Publish page",
+    "success_publish_page": "Page has been published",
+    "fail_publish_page": "Failed to publish the Page"
+  },
+  "sidebar_header": {
+    "show_wip_page": "Show WIP"
   }
   }
 }
 }

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

@@ -756,7 +756,7 @@
       "description1": "メールアドレスを使用して新規ユーザーを仮発行します。",
       "description1": "メールアドレスを使用して新規ユーザーを仮発行します。",
       "description2": "初回のログイン時に使用する仮パスワードが生成されます。",
       "description2": "初回のログイン時に使用する仮パスワードが生成されます。",
       "invite_thru_email": "招待メールを送信する",
       "invite_thru_email": "招待メールを送信する",
-      "mail_setting_link": "<i class='icon-settings me-2'></i><a href='/admin/app'>メールの設定</a>",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>メールの設定</a>",
       "valid_email": "メールアドレスを入力してください。",
       "valid_email": "メールアドレスを入力してください。",
       "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
       "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
       "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
       "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
@@ -844,9 +844,10 @@
       "dropdown_desc": "削除するグループの限定公開ページの処理を選択してください",
       "dropdown_desc": "削除するグループの限定公開ページの処理を選択してください",
       "select_group": "グループを選択してください",
       "select_group": "グループを選択してください",
       "no_groups": "グループがありません",
       "no_groups": "グループがありません",
-      "publish_pages": "全て公開する",
+      "publish_pages": "公開可能なページを公開する",
       "delete_pages": "全て削除する",
       "delete_pages": "全て削除する",
-      "transfer_pages": "全て他のグループに移譲する"
+      "transfer_pages": "全て他のグループに移譲する",
+      "option_explanation": "「公開可能なページ」とは、削除するグループにのみ限定公開されているページを指します。他のグループも閲覧可能なページは公開対象となりません。"
     },
     },
     "update_parent_confirm_modal": {
     "update_parent_confirm_modal": {
       "header": "グループの親が変更されます",
       "header": "グループの親が変更されます",

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

@@ -859,5 +859,17 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "ページの場所を選択"
     "select_page_location": "ページの場所を選択"
+  },
+  "wip_page": {
+    "save_as_wip": "WIP (執筆中) として保存",
+    "success_save_as_wip": "WIP ページとして保存しました",
+    "fail_save_as_wip": "WIP ページとして保存できませんでした",
+    "alert": "このページは作業途中です",
+    "publish_page": "WIP を解除",
+    "success_publish_page": "WIP を解除しました",
+    "fail_publish_page": "WIP を解除できませんでした"
+  },
+  "sidebar_header": {
+    "show_wip_page": "WIP を表示"
   }
   }
 }
 }

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

@@ -754,7 +754,7 @@
       "emails": "电子邮件",
       "emails": "电子邮件",
       "description1": "通过电子邮件地址临时发布新用户。",
       "description1": "通过电子邮件地址临时发布新用户。",
       "description2": "将为首次登录生成一个临时密码。",
       "description2": "将为首次登录生成一个临时密码。",
-      "mail_setting_link": "<i class='icon-settings me-2'></i><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "需要有效的电子邮件地址",
       "valid_email": "需要有效的电子邮件地址",
       "invite_thru_email": "发送邀请电子邮件",
       "invite_thru_email": "发送邀请电子邮件",
       "temporary_password": "创建的用户具有临时密码",
       "temporary_password": "创建的用户具有临时密码",
@@ -843,9 +843,10 @@
       "dropdown_desc": "为私人页选择操作",
       "dropdown_desc": "为私人页选择操作",
       "select_group": "选择组",
       "select_group": "选择组",
       "no_groups": "没有可选择的组",
       "no_groups": "没有可选择的组",
-      "publish_pages": "全部发布",
+      "publish_pages": "发布可以发布的页面",
       "delete_pages": "全部删除",
       "delete_pages": "全部删除",
-      "transfer_pages": "转移到另一组"
+      "transfer_pages": "转移到另一组",
+      "option_explanation": "\"可发布页面\"是指仅对您要删除的群组可见的页面。其他群组可以查看的页面将不会被发布。"
     },
     },
     "update_parent_confirm_modal": {
     "update_parent_confirm_modal": {
       "header": "该组的父组被改变",
       "header": "该组的父组被改变",

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

@@ -829,5 +829,17 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "选择页面位置"
     "select_page_location": "选择页面位置"
+  },
+  "wip_page": {
+    "save_as_wip": "保存为 WIP(书面)",
+    "success_save_as_wip": "成功保存为 WIP 页面",
+    "fail_save_as_wip": "保存为 WIP 页失败",
+    "alert": "本页面正在制作中",
+    "publish_page": "发布 WIP",
+    "success_publish_page": "WIP 已停用",
+    "fail_publish_page": "无法停用 WIP"
+  },
+  "sidebar_header": {
+    "show_wip_page": "显示 WIP"
   }
   }
 }
 }

+ 1 - 1
apps/app/src/client/services/create-page/use-create-template-page.ts

@@ -25,7 +25,7 @@ export const useCreateTemplatePage: UseCreateTemplatePage = () => {
     if (isLoadingPagePath || !isCreatable) return;
     if (isLoadingPagePath || !isCreatable) return;
 
 
     return createAndTransit(
     return createAndTransit(
-      { path: normalizePath(`${currentPagePath}/${label}`) },
+      { path: normalizePath(`${currentPagePath}/${label}`), wip: false },
       { shouldCheckPageExists: true },
       { shouldCheckPageExists: true },
     );
     );
   }, [currentPagePath, isCreatable, isLoadingPagePath, createAndTransit]);
   }, [currentPagePath, isCreatable, isLoadingPagePath, createAndTransit]);

+ 11 - 0
apps/app/src/client/services/page-operation.ts

@@ -1,5 +1,6 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
+import type { IPageHasId } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
@@ -159,3 +160,13 @@ export const exist = async(path: string): Promise<PageExistResponse> => {
   const res = await apiv3Get<PageExistResponse>('/page/exist', { path });
   const res = await apiv3Get<PageExistResponse>('/page/exist', { path });
   return res.data;
   return res.data;
 };
 };
+
+export const publish = async(pageId: string): Promise<IPageHasId> => {
+  const res = await apiv3Put(`/page/${pageId}/publish`);
+  return res.data;
+};
+
+export const unpublish = async(pageId: string): Promise<IPageHasId> => {
+  const res = await apiv3Put(`/page/${pageId}/unpublish`);
+  return res.data;
+};

+ 9 - 10
apps/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -1,11 +1,13 @@
+import type { ForwardRefRenderFunction } from 'react';
 import React, {
 import React, {
-  Fragment, useState, useCallback, useRef, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
+  Fragment, useState, useCallback, forwardRef, useRef, useImperativeHandle,
 } from 'react';
 } from 'react';
 
 
+import type { TypeaheadRef } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { IClearable } from '~/client/interfaces/clearable';
+import type { IClearable } from '~/client/interfaces/clearable';
 import { useSWRxUsernames } from '~/stores/user';
 import { useSWRxUsernames } from '~/stores/user';
 
 
 
 
@@ -30,7 +32,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
   const { onChange } = props;
   const { onChange } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const typeaheadRef = useRef<IClearable>(null);
+  const typeaheadRef = useRef<TypeaheadRef>(null);
 
 
   /*
   /*
    * State
    * State
@@ -41,11 +43,11 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
    * Fetch
    * Fetch
    */
    */
   const requestOptions = { isIncludeActiveUser: true, isIncludeInactiveUser: true, isIncludeActivitySnapshotUser: true };
   const requestOptions = { isIncludeActiveUser: true, isIncludeInactiveUser: true, isIncludeActivitySnapshotUser: true };
-  const { data: usernameData, error } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
+  const { data: usernameData, error, isLoading: _isLoading } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
   const activeUsernames = usernameData?.activeUser?.usernames != null ? usernameData.activeUser.usernames : [];
   const activeUsernames = usernameData?.activeUser?.usernames != null ? usernameData.activeUser.usernames : [];
   const inactiveUsernames = usernameData?.inactiveUser?.usernames != null ? usernameData.inactiveUser.usernames : [];
   const inactiveUsernames = usernameData?.inactiveUser?.usernames != null ? usernameData.inactiveUser.usernames : [];
   const activitySnapshotUsernames = usernameData?.activitySnapshotUser?.usernames != null ? usernameData.activitySnapshotUser.usernames : [];
   const activitySnapshotUsernames = usernameData?.activitySnapshotUser?.usernames != null ? usernameData.activitySnapshotUser.usernames : [];
-  const isLoading = usernameData === undefined && error == null;
+  const isLoading = _isLoading === true && error == null;
 
 
   const allUser: UserDataType[] = [];
   const allUser: UserDataType[] = [];
   const pushToAllUser = (usernames: string[], category: CategoryType) => {
   const pushToAllUser = (usernames: string[], category: CategoryType) => {
@@ -59,10 +61,8 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
    * Functions
    * Functions
    */
    */
   const changeHandler = useCallback((userData: UserDataType[]) => {
   const changeHandler = useCallback((userData: UserDataType[]) => {
-    if (onChange != null) {
-      const usernames = userData.map(user => user.username);
-      onChange(usernames);
-    }
+    const usernames = userData.map(user => user.username);
+    onChange(usernames);
   }, [onChange]);
   }, [onChange]);
 
 
   const searchHandler = useCallback((text: string) => {
   const searchHandler = useCallback((text: string) => {
@@ -120,7 +120,6 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
         delay={400}
         delay={400}
         minLength={0}
         minLength={0}
         placeholder={t('admin:audit_log_management.username')}
         placeholder={t('admin:audit_log_management.username')}
-        caseSensitive={false}
         isLoading={isLoading}
         isLoading={isLoading}
         options={allUser}
         options={allUser}
         onSearch={searchHandler}
         onSearch={searchHandler}

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

@@ -484,7 +484,11 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                               aria-expanded="true"
                               aria-expanded="true"
                               aria-controls="ablchelp"
                               aria-controls="ablchelp"
                             >
                             >
-                              <i className={`icon-fw ${this.state.isHelpOpened ? 'icon-arrow-down' : 'icon-arrow-right'} small`}></i> Show more...
+                              <span
+                                className="material-symbols-outlined me-1"
+                                small
+                              >{this.state.isHelpOpened ? 'expand_more' : 'chevron_right'}
+                              </span> Show more...
                             </button>
                             </button>
                           </h2>
                           </h2>
                           <Collapse isOpen={this.state.isHelpOpened}>
                           <Collapse isOpen={this.state.isHelpOpened}>

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

@@ -81,7 +81,7 @@ const SecurityManagementContents = () => {
             href="/admin/markdown/#preventXSS"
             href="/admin/markdown/#preventXSS"
             style={{ fontSize: 'large' }}
             style={{ fontSize: 'large' }}
           >
           >
-            <i className="fa-fw icon-login"></i> {t('security_settings.xss_prevent_setting_link')}
+            <span className="material-symbols-outlined me-1">login</span> {t('security_settings.xss_prevent_setting_link')}
           </Link>
           </Link>
         </div>
         </div>
       </div>
       </div>

+ 2 - 2
apps/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -303,7 +303,7 @@ class SecuritySetting extends React.Component {
                     <div className="pb-4">
                     <div className="pb-4">
                       <p className="card custom-card">
                       <p className="card custom-card">
                         <span className="text-warning">
                         <span className="text-warning">
-                          <i className="icon-info"></i>
+                          <span className="material-symbols-outlined">info</span>
                           {/* eslint-disable-next-line react/no-danger */}
                           {/* eslint-disable-next-line react/no-danger */}
                           <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
                           <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
                         </span>
                         </span>
@@ -526,7 +526,7 @@ class SecuritySetting extends React.Component {
             <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_settings.max_age_desc') }} />
             <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_settings.max_age_desc') }} />
             <p className="card custom-card">
             <p className="card custom-card">
               <span className="text-warning">
               <span className="text-warning">
-                <i className="icon-info"></i> {t('security_settings.max_age_caution')}
+                <span className="material-symbols-outlined">info</span> {t('security_settings.max_age_caution')}
               </span>
               </span>
             </p>
             </p>
           </div>
           </div>

+ 9 - 3
apps/app/src/components/Admin/SlackIntegration/Bridge.jsx

@@ -15,14 +15,14 @@ const ProxyCircle = () => (
 
 
 const BridgeCore = (props) => {
 const BridgeCore = (props) => {
   const {
   const {
-    description, iconClass, hrClass, withProxy,
+    description, iconClass, iconName, hrClass, withProxy,
   } = props;
   } = props;
 
 
   return (
   return (
     <>
     <>
       <div id="grw-bridge-container" className={`grw-bridge-container ${withProxy ? 'with-proxy' : ''}`}>
       <div id="grw-bridge-container" className={`grw-bridge-container ${withProxy ? 'with-proxy' : ''}`}>
         <p className={`${withProxy ? 'mt-0' : 'mt-2'}`}>
         <p className={`${withProxy ? 'mt-0' : 'mt-2'}`}>
-          <i className={iconClass} />
+          <span className={iconClass}>{iconName}</span>
           <small
           <small
             className="ms-2 d-none d-lg-inline"
             className="ms-2 d-none d-lg-inline"
             // eslint-disable-next-line react/no-danger
             // eslint-disable-next-line react/no-danger
@@ -47,6 +47,7 @@ const BridgeCore = (props) => {
 BridgeCore.propTypes = {
 BridgeCore.propTypes = {
   description: PropTypes.string.isRequired,
   description: PropTypes.string.isRequired,
   iconClass: PropTypes.string.isRequired,
   iconClass: PropTypes.string.isRequired,
+  iconName: PropTypes.string.isRequired,
   hrClass: PropTypes.string.isRequired,
   hrClass: PropTypes.string.isRequired,
   withProxy: PropTypes.bool,
   withProxy: PropTypes.bool,
 };
 };
@@ -58,24 +59,28 @@ const Bridge = (props) => {
 
 
   let description;
   let description;
   let iconClass;
   let iconClass;
+  let iconName;
   let hrClass;
   let hrClass;
 
 
   // empty or all failed
   // empty or all failed
   if (totalCount === 0 || errorCount === totalCount) {
   if (totalCount === 0 || errorCount === totalCount) {
     description = t('admin:slack_integration.integration_sentence.integration_is_not_complete');
     description = t('admin:slack_integration.integration_sentence.integration_is_not_complete');
-    iconClass = 'icon-info text-danger';
+    iconClass = 'material-symbols-outlined text-danger';
+    iconName = 'info';
     hrClass = 'border-danger admin-border-failed';
     hrClass = 'border-danger admin-border-failed';
   }
   }
   // all green
   // all green
   else if (errorCount === 0) {
   else if (errorCount === 0) {
     description = t('admin:slack_integration.integration_sentence.integration_successful');
     description = t('admin:slack_integration.integration_sentence.integration_successful');
     iconClass = 'fa fa-check text-success';
     iconClass = 'fa fa-check text-success';
+    iconName = '';
     hrClass = 'border-success admin-border-success';
     hrClass = 'border-success admin-border-success';
   }
   }
   // some of them failed
   // some of them failed
   else {
   else {
     description = t('admin:slack_integration.integration_sentence.integration_some_ws_is_not_complete');
     description = t('admin:slack_integration.integration_sentence.integration_some_ws_is_not_complete');
     iconClass = 'fa fa-check text-warning';
     iconClass = 'fa fa-check text-warning';
+    iconName = '';
     hrClass = 'border-warning admin-border-failed';
     hrClass = 'border-warning admin-border-failed';
   }
   }
 
 
@@ -83,6 +88,7 @@ const Bridge = (props) => {
     <BridgeCore
     <BridgeCore
       description={description}
       description={description}
       iconClass={iconClass}
       iconClass={iconClass}
+      iconName={iconName}
       hrClass={hrClass}
       hrClass={hrClass}
       withProxy={withProxy}
       withProxy={withProxy}
     />
     />

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -142,7 +142,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       >
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center text-warning">
         <p className="text-center text-warning">
-          <i className="icon-info">{t('admin:slack_integration.accordion.test_connection_only_public_channel')}</i>
+          <span className="material-symbols-outlined">info</span>{t('admin:slack_integration.accordion.test_connection_only_public_channel')}
         </p>
         </p>
         <div className="d-flex justify-content-center">
         <div className="d-flex justify-content-center">
           <form className="align-items-center" onSubmit={e => submitForm(e)}>
           <form className="align-items-center" onSubmit={e => submitForm(e)}>

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -240,7 +240,7 @@ const TestProcess = ({
     <>
     <>
       <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
       <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
       <p className="text-center text-warning">
       <p className="text-center text-warning">
-        <i className="icon-info">{t('admin:slack_integration.accordion.test_connection_only_public_channel')}</i>
+        <span className="material-symbols-outlined me-1">info</span>{t('admin:slack_integration.accordion.test_connection_only_public_channel')}
       </p>
       </p>
       <div className="d-flex justify-content-center">
       <div className="d-flex justify-content-center">
         <form className="justify-content-center" onSubmit={e => submitForm(e)}>
         <form className="justify-content-center" onSubmit={e => submitForm(e)}>

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

@@ -1,6 +1,5 @@
-import React, {
-  FC, useCallback, useState, useMemo,
-} from 'react';
+import type { FC } from 'react';
+import React, { useCallback, useState, useMemo } from 'react';
 
 
 import type { IUserGroupHasId } from '@growi/core';
 import type { IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
@@ -8,6 +7,8 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import { PageActionOnGroupDelete } from '~/interfaces/user-group';
+
 
 
 /**
 /**
  * Delete User Group Select component
  * Delete User Group Select component
@@ -19,26 +20,19 @@ import {
 type Props = {
 type Props = {
   userGroups: IUserGroupHasId[],
   userGroups: IUserGroupHasId[],
   deleteUserGroup?: IUserGroupHasId,
   deleteUserGroup?: IUserGroupHasId,
-  onDelete?: (deleteGroupId: string, actionName: string, transferToUserGroupId: string) => Promise<void> | void,
+  onDelete?: (deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => Promise<void> | void,
   isShow: boolean,
   isShow: boolean,
   onHide?: () => Promise<void> | void,
   onHide?: () => Promise<void> | void,
 };
 };
 
 
 type AvailableOption = {
 type AvailableOption = {
   id: number,
   id: number,
-  actionForPages: string,
+  actionForPages: PageActionOnGroupDelete,
   iconClass: string,
   iconClass: string,
   styleClass: string,
   styleClass: string,
   label: string,
   label: string,
 };
 };
 
 
-// actionName master constants
-const actionForPages = {
-  public: 'public',
-  delete: 'delete',
-  transfer: 'transfer',
-};
-
 export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
 export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -51,21 +45,21 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     return [
     return [
       {
       {
         id: 1,
         id: 1,
-        actionForPages: actionForPages.public,
+        actionForPages: PageActionOnGroupDelete.publicize,
         iconClass: 'icon-people',
         iconClass: 'icon-people',
         styleClass: '',
         styleClass: '',
         label: t('admin:user_group_management.delete_modal.publish_pages'),
         label: t('admin:user_group_management.delete_modal.publish_pages'),
       },
       },
       {
       {
         id: 2,
         id: 2,
-        actionForPages: actionForPages.delete,
+        actionForPages: PageActionOnGroupDelete.delete,
         iconClass: 'icon-trash',
         iconClass: 'icon-trash',
         styleClass: 'text-danger',
         styleClass: 'text-danger',
         label: t('admin:user_group_management.delete_modal.delete_pages'),
         label: t('admin:user_group_management.delete_modal.delete_pages'),
       },
       },
       {
       {
         id: 3,
         id: 3,
-        actionForPages: actionForPages.transfer,
+        actionForPages: PageActionOnGroupDelete.transfer,
         iconClass: 'icon-options',
         iconClass: 'icon-options',
         styleClass: '',
         styleClass: '',
         label: t('admin:user_group_management.delete_modal.transfer_pages'),
         label: t('admin:user_group_management.delete_modal.transfer_pages'),
@@ -76,14 +70,14 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   /*
   /*
    * State
    * State
    */
    */
-  const [actionName, setActionName] = useState<string>('');
+  const [actionName, setActionName] = useState<PageActionOnGroupDelete | null>(null);
   const [transferToUserGroupId, setTransferToUserGroupId] = useState<string>('');
   const [transferToUserGroupId, setTransferToUserGroupId] = useState<string>('');
 
 
   /*
   /*
    * Function
    * Function
    */
    */
   const resetStates = useCallback(() => {
   const resetStates = useCallback(() => {
-    setActionName('');
+    setActionName(null);
     setTransferToUserGroupId('');
     setTransferToUserGroupId('');
   }, []);
   }, []);
 
 
@@ -107,7 +101,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   }, []);
   }, []);
 
 
   const handleSubmit = useCallback((e) => {
   const handleSubmit = useCallback((e) => {
-    if (onDelete == null || deleteUserGroup == null) {
+    if (onDelete == null || deleteUserGroup == null || actionName == null) {
       return;
       return;
     }
     }
 
 
@@ -130,7 +124,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         name="actionName"
         name="actionName"
         className="form-control"
         className="form-control"
         placeholder="select"
         placeholder="select"
-        value={actionName}
+        value={actionName ?? ''}
         onChange={handleActionChange}
         onChange={handleActionChange}
       >
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
@@ -158,7 +152,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     return (
     return (
       <select
       <select
         name="transferToUserGroupId"
         name="transferToUserGroupId"
-        className={`form-control ${actionName === actionForPages.transfer ? '' : 'd-none'}`}
+        className={`form-control ${actionName === PageActionOnGroupDelete.transfer ? '' : 'd-none'}`}
         value={transferToUserGroupId}
         value={transferToUserGroupId}
         onChange={handleGroupChange}
         onChange={handleGroupChange}
       >
       >
@@ -171,10 +165,10 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   const validateForm = useCallback(() => {
   const validateForm = useCallback(() => {
     let isValid = true;
     let isValid = true;
 
 
-    if (actionName === '') {
+    if (actionName === null) {
       isValid = false;
       isValid = false;
     }
     }
-    else if (actionName === actionForPages.transfer) {
+    else if (actionName === PageActionOnGroupDelete.transfer) {
       isValid = transferToUserGroupId !== '';
       isValid = transferToUserGroupId !== '';
     }
     }
 
 
@@ -196,7 +190,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
         <form className="d-flex justify-content-between w-100" onSubmit={handleSubmit}>
         <form className="d-flex justify-content-between w-100" onSubmit={handleSubmit}>
-          <div className="d-flex mb-0">
+          <div className="d-flex mb-0 me-3">
             {renderPageActionSelector()}
             {renderPageActionSelector()}
             {renderGroupSelector()}
             {renderGroupSelector()}
           </div>
           </div>
@@ -204,6 +198,11 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
             <span className="material-symbols-outlined">delete_forever</span> {t('Delete')}
             <span className="material-symbols-outlined">delete_forever</span> {t('Delete')}
           </button>
           </button>
         </form>
         </form>
+        {actionName === PageActionOnGroupDelete.publicize && (
+          <div className="form-text text-muted">
+            <small>{t('admin:user_group_management.delete_modal.option_explanation')}</small>
+          </div>
+        )}
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>
   );
   );

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

@@ -1,6 +1,5 @@
-import React, {
-  FC, useState, useEffect, useCallback,
-} from 'react';
+import type { FC } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 
 
 import type { Ref, IUserGroup, IUserGroupHasId } from '@growi/core';
 import type { Ref, IUserGroup, IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 
 import type { IUserGroup, IUserGroupHasId } from '@growi/core';
 import type { IUserGroup, IUserGroupHasId } from '@growi/core';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -9,6 +10,7 @@ import { toastSuccess, toastError } from '~/client/util/toastr';
 import { ExternalGroupManagement } from '~/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement';
 import { ExternalGroupManagement } from '~/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement';
 import { useIsAclEnabled } from '~/stores/context';
 import { useIsAclEnabled } from '~/stores/context';
 import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
 import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
+import { PageActionOnGroupDelete } from '~/interfaces/user-group';
 
 
 
 
 const UserGroupDeleteModal = dynamic(() => import('./UserGroupDeleteModal').then(mod => mod.UserGroupDeleteModal), { ssr: false });
 const UserGroupDeleteModal = dynamic(() => import('./UserGroupDeleteModal').then(mod => mod.UserGroupDeleteModal), { ssr: false });
@@ -126,7 +128,7 @@ export const UserGroupPage: FC = () => {
     }
     }
   }, [t, mutateUserGroups, hideUpdateModal]);
   }, [t, mutateUserGroups, hideUpdateModal]);
 
 
-  const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: string, transferToUserGroupId: string) => {
+  const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => {
     try {
     try {
       await apiv3Delete(`/user-groups/${deleteGroupId}`, {
       await apiv3Delete(`/user-groups/${deleteGroupId}`, {
         actionName,
         actionName,

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

@@ -205,7 +205,7 @@ export const UserGroupTable: FC<Props> = ({
                           className="btn btn-outline-secondary btn-sm dropdown-toggle"
                           className="btn btn-outline-secondary btn-sm dropdown-toggle"
                           data-bs-toggle="dropdown"
                           data-bs-toggle="dropdown"
                         >
                         >
-                          <i className="icon-settings"></i>
+                          <span className="material-symbols-outlined fs-5">settings</span>
                         </button>
                         </button>
                         <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${group._id}`}>
                         <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${group._id}`}>
                           <button className="dropdown-item" type="button" role="button" onClick={onClickEdit} data-user-group-id={group._id}>
                           <button className="dropdown-item" type="button" role="button" onClick={onClickEdit} data-user-group-id={group._id}>

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

@@ -28,7 +28,7 @@ export const UpdateParentConfirmModal: FC = () => {
   return (
   return (
     <Modal className="modal-md" isOpen={isOpened} toggle={closeModal}>
     <Modal className="modal-md" isOpen={isOpened} toggle={closeModal}>
       <ModalHeader tag="h4" toggle={closeModal} className="bg-warning text-light">
       <ModalHeader tag="h4" toggle={closeModal} className="bg-warning text-light">
-        <i className="icon icon-warning"></i> {t('admin:user_group_management.update_parent_confirm_modal.header')}
+        <span className="material-symbols-outlined">warning</span> {t('admin:user_group_management.update_parent_confirm_modal.header')}
       </ModalHeader>
       </ModalHeader>
       {
       {
         targetGroup != null && updateData != null ? (
         targetGroup != null && updateData != null ? (

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

@@ -13,8 +13,9 @@ import {
   apiv3Get, apiv3Put, apiv3Delete, apiv3Post,
   apiv3Get, apiv3Put, apiv3Delete, apiv3Post,
 } from '~/client/util/apiv3-client';
 } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
-import { SearchTypes, SearchType } from '~/interfaces/user-group';
+import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
+import type { PageActionOnGroupDelete, SearchType } from '~/interfaces/user-group';
+import { SearchTypes } from '~/interfaces/user-group';
 import Xss from '~/services/xss';
 import Xss from '~/services/xss';
 import { useIsAclEnabled } from '~/stores/context';
 import { useIsAclEnabled } from '~/stores/context';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
@@ -296,7 +297,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
     setDeleteModalShown(false);
     setDeleteModalShown(false);
   }, [setSelectedUserGroup, setDeleteModalShown]);
   }, [setSelectedUserGroup, setDeleteModalShown]);
 
 
-  const deleteChildUserGroupById = useCallback(async(deleteGroupId: string, actionName: string, transferToUserGroupId: string) => {
+  const deleteChildUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => {
     const url = isExternalGroup ? `/external-user-groups/${deleteGroupId}` : `/user-groups/${deleteGroupId}`;
     const url = isExternalGroup ? `/external-user-groups/${deleteGroupId}` : `/user-groups/${deleteGroupId}`;
     try {
     try {
       const res = await apiv3Delete(url, {
       const res = await apiv3Delete(url, {

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

@@ -1,169 +0,0 @@
-import React from 'react';
-
-import { UserPicture } from '@growi/ui/dist/components';
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { AsyncTypeahead } from 'react-bootstrap-typeahead';
-import { debounce } from 'throttle-debounce';
-
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import Xss from '~/services/xss';
-
-class UserGroupUserFormByInput extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      keyword: '',
-      inputUser: '',
-      applicableUsers: [],
-      isLoading: false,
-      searchError: null,
-    };
-
-    this.xss = new Xss();
-
-    this.addUserBySubmit = this.addUserBySubmit.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-    this.handleChange = this.handleChange.bind(this);
-    this.handleSearch = this.handleSearch.bind(this);
-    this.onKeyDown = this.onKeyDown.bind(this);
-    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
-
-    this.searhApplicableUsersDebounce = debounce(1000, this.searhApplicableUsers);
-  }
-
-  async addUserBySubmit() {
-    const { userGroup, onClickAddUserBtn } = this.props;
-
-    if (this.state.inputUser.length === 0) { return }
-    const userName = this.state.inputUser[0].username;
-
-    try {
-      await onClickAddUserBtn(userName);
-      toastSuccess(`Added "${this.xss.process(userName)}" to "${this.xss.process(userGroup.name)}"`);
-      this.setState({ inputUser: '' });
-    }
-    catch (err) {
-      toastError(new Error(`Unable to add "${this.xss.process(userName)}" to "${this.xss.process(userGroup.name)}"`));
-    }
-
-
-  }
-
-  validateForm() {
-    return this.state.inputUser !== '';
-  }
-
-  async searhApplicableUsers() {
-    const { onSearchApplicableUsers } = this.props;
-
-    try {
-      const users = await onSearchApplicableUsers(this.state.keyword);
-      this.setState({ applicableUsers: users, isLoading: false });
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  /**
-   * Reflect when forecast is clicked
-   * @param {object} inputUser
-   */
-  handleChange(inputUser) {
-    this.setState({ inputUser });
-  }
-
-  handleSearch(keyword) {
-    if (keyword === '') {
-      return;
-    }
-
-    this.setState({ keyword, isLoading: true });
-    this.searhApplicableUsersDebounce();
-  }
-
-  onKeyDown(event) {
-    // 13 is Enter key
-    if (event.keyCode === 13) {
-      this.addUserBySubmit();
-    }
-  }
-
-  renderMenuItemChildren(option) {
-    const { isAlsoNameSearched, isAlsoMailSearched } = this.props;
-    const user = option;
-    return (
-      <>
-        <UserPicture user={user} size="sm" noLink noTooltip />
-        <strong className="ms-2">{user.username}</strong>
-        {isAlsoNameSearched && <span className="ms-2">{user.name}</span>}
-        {isAlsoMailSearched && <span className="ms-2">{user.email}</span>}
-      </>
-    );
-  }
-
-  getEmptyLabel() {
-    return (this.state.searchError !== null) && 'Error on searching.';
-  }
-
-  render() {
-    const { t } = this.props;
-
-    const inputProps = { autoComplete: 'off' };
-
-    return (
-      <div className="row">
-        <div className="col-8 pe-0">
-          <AsyncTypeahead
-            {...this.props}
-            id="name-typeahead-asynctypeahead"
-            ref={(c) => { this.typeahead = c }}
-            inputProps={inputProps}
-            isLoading={this.state.isLoading}
-            labelKey={user => `${user.username} ${user.name} ${user.email}`}
-            minLength={0}
-            options={this.state.applicableUsers} // Search result
-            searchText={(this.state.isLoading ? 'Searching...' : this.getEmptyLabel())}
-            renderMenuItemChildren={this.renderMenuItemChildren}
-            align="left"
-            onChange={this.handleChange}
-            onSearch={this.handleSearch}
-            onKeyDown={this.onKeyDown}
-            caseSensitive={false}
-            clearButton
-          />
-        </div>
-        <div className="col-2 ps-0">
-          <button
-            type="button"
-            className="btn btn-success"
-            disabled={!this.validateForm()}
-            onClick={this.addUserBySubmit}
-          >
-            {t('add')}
-          </button>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-UserGroupUserFormByInput.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  isAlsoMailSearched: PropTypes.bool.isRequired,
-  isAlsoNameSearched: PropTypes.bool.isRequired,
-  onClickAddUserBtn: PropTypes.func,
-  onSearchApplicableUsers: PropTypes.func,
-  userGroup: PropTypes.object,
-};
-
-const UserGroupUserFormByInputWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <UserGroupUserFormByInput t={t} {...props} />;
-};
-
-export default UserGroupUserFormByInputWrapperFC;

+ 122 - 0
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.tsx

@@ -0,0 +1,122 @@
+import type { FC, KeyboardEvent } from 'react';
+import React, { useState, useRef } from 'react';
+
+import type { IUserGroupHasId, IUserHasId } from '@growi/core';
+import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
+import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import type { SearchType } from '~/interfaces/user-group';
+import Xss from '~/services/xss';
+
+type Props = {
+  userGroup: IUserGroupHasId,
+  onClickAddUserBtn: (username: string) => Promise<void>,
+  onSearchApplicableUsers: (searchWord: string) => Promise<IUserHasId[]>,
+  isAlsoNameSearched: boolean,
+  isAlsoMailSearched: boolean,
+  searchType: SearchType,
+}
+
+export const UserGroupUserFormByInput: FC<Props> = (props) => {
+  const {
+    userGroup, onClickAddUserBtn, onSearchApplicableUsers, isAlsoNameSearched, isAlsoMailSearched, searchType,
+  } = props;
+
+  const { t } = useTranslation();
+  const typeaheadRef = useRef(null);
+  const [inputUser, setInputUser] = useState<IUserHasId[]>([]);
+  const [applicableUsers, setApplicableUsers] = useState<IUserHasId[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [isSearchError, setIsSearchError] = useState(false);
+
+  const xss = new Xss();
+
+  const addUserBySubmit = async() => {
+    if (inputUser.length === 0) { return }
+    const userName = inputUser[0].username;
+
+    try {
+      await onClickAddUserBtn(userName);
+      toastSuccess(`Added "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`);
+      setInputUser([]);
+    }
+    catch (err) {
+      toastError(new Error(`Unable to add "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`));
+    }
+  };
+
+  const searchApplicableUsers = async(keyword: string) => {
+    try {
+      const users = await onSearchApplicableUsers(keyword);
+      setApplicableUsers(users);
+      setIsLoading(false);
+    }
+    catch (err) {
+      setIsSearchError(true);
+      toastError(err);
+    }
+  };
+
+  const handleChange = (inputUser: IUserHasId[]) => {
+    setInputUser(inputUser);
+  };
+
+  const handleSearch = async(keyword: string) => {
+    setIsLoading(true);
+    await searchApplicableUsers(keyword);
+  };
+
+  const onKeyDown = (event: KeyboardEvent) => {
+    if (event.key === 'Enter') {
+      addUserBySubmit();
+    }
+  };
+
+  const renderMenuItemChildren = (option: IUserHasId) => {
+    const user = option;
+
+    return (
+      <>
+        <UserPicture user={user} size="sm" noLink noTooltip />
+        <strong className="ms-2">{user.username}</strong>
+        {isAlsoNameSearched && <span className="ms-2">{user.name}</span>}
+        {isAlsoMailSearched && <span className="ms-2">{user.email}</span>}
+      </>
+    );
+  };
+
+  return (
+    <div className="row">
+      <div className="col-8 pe-0">
+        <AsyncTypeahead
+          key={`${searchType}-${isAlsoNameSearched}-${isAlsoMailSearched}`} // The searched keywords are not re-searched, so re-rendered by key.
+          id="name-typeahead-asynctypeahead"
+          inputProps={{ autoComplete: 'off' }}
+          isLoading={isLoading}
+          labelKey={(user: IUserHasId) => `${user.username} ${user.name} ${user.email}`}
+          options={applicableUsers} // Search result
+          onSearch={handleSearch}
+          onChange={handleChange}
+          onKeyDown={onKeyDown}
+          minLength={1}
+          searchText={isLoading ? 'Searching...' : (isSearchError && 'Error on searching.')}
+          renderMenuItemChildren={renderMenuItemChildren}
+          align="left"
+          clearButton
+        />
+      </div>
+      <div className="col-2 ps-0">
+        <button
+          type="button"
+          className="btn btn-success"
+          disabled={inputUser.length === 0}
+          onClick={addUserBySubmit}
+        >
+          {t('add')}
+        </button>
+      </div>
+    </div>
+  );
+};

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

@@ -1,16 +1,17 @@
 import React from 'react';
 import React from 'react';
 
 
-import type { IUserGroupHasId } from '@growi/core';
+import type { IUserGroupHasId, IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Modal, ModalHeader, ModalBody,
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { SearchTypes, SearchType } from '~/interfaces/user-group';
+import type { SearchType } from '~/interfaces/user-group';
+import { SearchTypes } from '~/interfaces/user-group';
 
 
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
 import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
-import UserGroupUserFormByInput from './UserGroupUserFormByInput';
+import { UserGroupUserFormByInput } from './UserGroupUserFormByInput';
 
 
 type Props = {
 type Props = {
   isOpen: boolean,
   isOpen: boolean,
@@ -19,7 +20,7 @@ type Props = {
   isAlsoMailSearched: boolean,
   isAlsoMailSearched: boolean,
   isAlsoNameSearched: boolean,
   isAlsoNameSearched: boolean,
   onClickAddUserBtn: (username: string) => Promise<void>,
   onClickAddUserBtn: (username: string) => Promise<void>,
-  onSearchApplicableUsers: (searchWord: string) => Promise<void>,
+  onSearchApplicableUsers: (searchWord: string) => Promise<IUserHasId[]>,
   onSwitchSearchType: (searchType: SearchType) => void
   onSwitchSearchType: (searchType: SearchType) => void
   onClose: () => void,
   onClose: () => void,
   onToggleIsAlsoMailSearched: () => void,
   onToggleIsAlsoMailSearched: () => void,
@@ -54,9 +55,9 @@ const UserGroupUserModal = (props: Props): JSX.Element => {
             userGroup={userGroup}
             userGroup={userGroup}
             onClickAddUserBtn={onClickAddUserBtn}
             onClickAddUserBtn={onClickAddUserBtn}
             onSearchApplicableUsers={onSearchApplicableUsers}
             onSearchApplicableUsers={onSearchApplicableUsers}
-            onClose={onClose}
             isAlsoNameSearched={isAlsoNameSearched}
             isAlsoNameSearched={isAlsoNameSearched}
             isAlsoMailSearched={isAlsoMailSearched}
             isAlsoMailSearched={isAlsoMailSearched}
+            searchType={searchType}
           />
           />
         </div>
         </div>
         <h2 className="border-bottom">{t('admin:user_group_management.add_modal.search_option')}</h2>
         <h2 className="border-bottom">{t('admin:user_group_management.add_modal.search_option')}</h2>

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

@@ -52,9 +52,9 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
                       type="button"
                       type="button"
                       id={`admin-group-menu-button-${relatedUser._id}`}
                       id={`admin-group-menu-button-${relatedUser._id}`}
                       className="btn btn-outline-secondary btn-sm dropdown-toggle"
                       className="btn btn-outline-secondary btn-sm dropdown-toggle"
-                      data-toggle="dropdown"
+                      data-bs-toggle="dropdown"
                     >
                     >
-                      <i className="icon-settings"></i>
+                      <span className="material-symbols-outlined fs-5">settings</span>
                     </button>
                     </button>
                     <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${relatedUser._id}`}>
                     <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${relatedUser._id}`}>
                       <button
                       <button

+ 1 - 1
apps/app/src/components/Admin/UserManagement.module.scss

@@ -12,7 +12,7 @@
   }
   }
   .search-clear {
   .search-clear {
     position: absolute;
     position: absolute;
-    top: 12px;
+    top: 15px;
     right: 1px;
     right: 1px;
     z-index: 3;
     z-index: 3;
     width: 24px;
     width: 24px;

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

@@ -149,15 +149,16 @@ const UserManagement = (props: UserManagementProps) => {
               {
               {
                 adminUsersContainer.state.searchText.length > 0
                 adminUsersContainer.state.searchText.length > 0
                   ? (
                   ? (
-                    <i
-                      className="icon-close search-clear"
+                    <span
+                      className="material-symbols-outlined me-1 search-clear"
                       onClick={async() => {
                       onClick={async() => {
                         await adminUsersContainer.clearSearchText();
                         await adminUsersContainer.clearSearchText();
                         if (inputRef.current != null) {
                         if (inputRef.current != null) {
                           inputRef.current.value = '';
                           inputRef.current.value = '';
                         }
                         }
                       }}
                       }}
-                    />
+                    >cancel
+                    </span>
                   )
                   )
                   : ''
                   : ''
               }
               }

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

@@ -92,7 +92,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
                 <td>
                 <td>
                   <div className="btn-group admin-user-menu">
                   <div className="btn-group admin-user-menu">
                     <button type="button" className="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
                     <button type="button" className="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
-                      <i className="icon-settings"></i> <span className="caret"></span>
+                      <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
                     </button>
                     </button>
                     <ul className="dropdown-menu" role="menu">
                     <ul className="dropdown-menu" role="menu">
                       <li className="dropdown-header">{t('user_management.user_table.edit_menu')}</li>
                       <li className="dropdown-header">{t('user_management.user_table.edit_menu')}</li>

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

@@ -95,7 +95,8 @@ const UserMenu = (props: UserMenuProps) => {
   return (
   return (
     <UncontrolledDropdown id="userMenu" size="sm">
     <UncontrolledDropdown id="userMenu" size="sm">
       <DropdownToggle caret color="secondary" outline>
       <DropdownToggle caret color="secondary" outline>
-        <i className="icon-settings" />
+        {/* TODO:fontsize: 20px */}
+        <span className="material-symbols-outlined fs-5">settings</span>
         {(user.status === USER_STATUS.INVITED && !isInvitationEmailSended)
         {(user.status === USER_STATUS.INVITED && !isInvitationEmailSended)
         && <i className={`fa fa-circle text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`} />}
         && <i className={`fa fa-circle text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`} />}
       </DropdownToggle>
       </DropdownToggle>

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

@@ -23,7 +23,7 @@ export const BookmarkFolderItemControl: React.FC<{
     <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
     <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
       { children ?? (
       { children ?? (
         <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
         <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
-          <i className="icon-options"></i>
+          <span className="material-symbols-outlined">more_horiz</span>
         </DropdownToggle>
         </DropdownToggle>
       ) }
       ) }
       <DropdownMenu
       <DropdownMenu

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

@@ -24,10 +24,6 @@ $grw-bookmark-item-padding-left: 35px;
 
 
 .grw-foldertree :global {
 .grw-foldertree :global {
 
 
-  .btn-page-item-control .icon-plus::before {
-    font-size: 18px;
-  }
-
   .list-group-item {
   .list-group-item {
     .grw-visible-on-hover {
     .grw-visible-on-hover {
       display: none;
       display: none;

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

@@ -181,7 +181,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
               : undefined}
               : undefined}
           >
           >
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover me-1">
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover me-1">
-              <i className="icon-options fa fa-rotate-90 p-1"></i>
+              <span className="material-symbols-outlined p-1">more_vert</span>
             </DropdownToggle>
             </DropdownToggle>
           </PageItemControl>
           </PageItemControl>
         </div>
         </div>

+ 3 - 2
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -3,7 +3,7 @@ import React, { memo, useCallback } from 'react';
 import Link from 'next/link';
 import Link from 'next/link';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import LinkedPagePath from '../../../models/linked-page-path';
+import type LinkedPagePath from '../../../models/linked-page-path';
 
 
 import styles from './PagePathHierarchicalLink.module.scss';
 import styles from './PagePathHierarchicalLink.module.scss';
 
 
@@ -51,7 +51,8 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
         <RootElm>
         <RootElm>
           <span className="path-segment">
           <span className="path-segment">
             <Link href="/" prefetch={false}>
             <Link href="/" prefetch={false}>
-              <i className="icon-home"></i>
+              {/* TODO: Size adjust */}
+              <span className="material-symbols-outlined">home</span>
               <span className={`separator ${styles.separator}`}>/</span>
               <span className={`separator ${styles.separator}`}>/</span>
             </Link>
             </Link>
           </span>
           </span>

+ 6 - 2
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -20,6 +20,7 @@ const { isTrashPage } = pagePathUtils;
 type Props = {
 type Props = {
   pagePath: string,
   pagePath: string,
   pageId?: string | null,
   pageId?: string | null,
+  isWipPage?: boolean,
   isSingleLineMode?: boolean,
   isSingleLineMode?: boolean,
   isCollapseParents?: boolean,
   isCollapseParents?: boolean,
   formerLinkClassName?: string,
   formerLinkClassName?: string,
@@ -37,7 +38,7 @@ const Separator = (): JSX.Element => {
 
 
 export const PagePathNav: FC<Props> = (props: Props) => {
 export const PagePathNav: FC<Props> = (props: Props) => {
   const {
   const {
-    pageId, pagePath, isSingleLineMode, isCollapseParents,
+    pageId, pagePath, isWipPage, isSingleLineMode, isCollapseParents,
     formerLinkClassName, latterLinkClassName,
     formerLinkClassName, latterLinkClassName,
   } = props;
   } = props;
   const dPagePath = new DevidedPagePath(pagePath, false, true);
   const dPagePath = new DevidedPagePath(pagePath, false, true);
@@ -94,7 +95,10 @@ export const PagePathNav: FC<Props> = (props: Props) => {
           {latterLink}
           {latterLink}
         </h1>
         </h1>
         { pageId != null && !isNotFound && (
         { pageId != null && !isNotFound && (
-          <div className="mx-2">
+          <div className="d-flex align-items-center ms-2">
+            { isWipPage && (
+              <span className="badge rounded-pill text-bg-secondary ms-1 me-1">WIP</span>
+            )}
             <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
             <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
               <i className="ti ti-clipboard"></i>
               <i className="ti ti-clipboard"></i>
             </CopyDropdown>
             </CopyDropdown>

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

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 type Props = {
 type Props = {
   isWindowExpanded: boolean,
   isWindowExpanded: boolean,
@@ -24,9 +25,10 @@ const ExpandOrContractButton: FC<Props> = (props: Props) => {
   return (
   return (
     <button
     <button
       type="button"
       type="button"
-      className={`btn ${isWindowExpanded ? 'icon-size-actual' : 'icon-size-fullscreen'}`}
+      className="btn material-symbols-outlined"
       onClick={isWindowExpanded ? clickContractButtonHandler : clickExpandButtonHandler}
       onClick={isWindowExpanded ? clickContractButtonHandler : clickExpandButtonHandler}
     >
     >
+      {isWindowExpanded ? 'close_fullscreen' : 'open_in_full'}
     </button>
     </button>
   );
   );
 };
 };

+ 0 - 5
apps/app/src/components/ItemsTree/ItemsTree.module.scss

@@ -17,11 +17,6 @@ $grw-pagetree-item-container-height: 40px;
   }
   }
 
 
   :global {
   :global {
-    .btn-page-item-control {
-      .icon-plus::before {
-        font-size: 18px;
-      }
-    }
 
 
     .list-group-item {
     .list-group-item {
       .grw-visible-on-hover {
       .grw-visible-on-hover {

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

@@ -91,6 +91,7 @@ const isSecondStageRenderingCondition = (condition: RenderingCondition|SecondSta
 type ItemsTreeProps = {
 type ItemsTreeProps = {
   isEnableActions: boolean
   isEnableActions: boolean
   isReadOnlyUser: boolean
   isReadOnlyUser: boolean
+  isWipPageShown?: boolean
   targetPath: string
   targetPath: string
   targetPathOrId?: Nullable<string>
   targetPathOrId?: Nullable<string>
   targetAndAncestorsData?: TargetAndAncestors
   targetAndAncestorsData?: TargetAndAncestors
@@ -103,7 +104,7 @@ type ItemsTreeProps = {
  */
  */
 export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const {
   const {
-    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser, CustomTreeItem, onClickTreeItem,
+    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser, isWipPageShown, CustomTreeItem, onClickTreeItem,
   } = props;
   } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -274,13 +275,14 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
 
   if (initialItemNode != null) {
   if (initialItemNode != null) {
     return (
     return (
-      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`} ref={rootElemRef}>
+      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-4`} ref={rootElemRef}>
         <CustomTreeItem
         <CustomTreeItem
           key={initialItemNode.page.path}
           key={initialItemNode.page.path}
           targetPathOrId={targetPathOrId}
           targetPathOrId={targetPathOrId}
           itemNode={initialItemNode}
           itemNode={initialItemNode}
           isOpen
           isOpen
           isEnableActions={isEnableActions}
           isEnableActions={isEnableActions}
+          isWipPageShown={isWipPageShown}
           isReadOnlyUser={isReadOnlyUser}
           isReadOnlyUser={isReadOnlyUser}
           onRenamed={onRenamed}
           onRenamed={onRenamed}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}

+ 1 - 1
apps/app/src/components/Me/ExternalAccountLinkedMe.jsx

@@ -64,7 +64,7 @@ class ExternalAccountLinkedMe extends React.Component {
             className="btn btn-outline-secondary btn-sm pull-right"
             className="btn btn-outline-secondary btn-sm pull-right"
             onClick={this.openAssociateModal}
             onClick={this.openAssociateModal}
           >
           >
-            <i className="icon-plus" aria-hidden="true" />
+            <span className="material-symbols-outlined" aria-hidden="true">add_circle</span>
             Add
             Add
           </button>
           </button>
           { t('admin:user_management.external_accounts') }
           { t('admin:user_management.external_accounts') }

+ 14 - 0
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -8,6 +8,7 @@ import type {
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Link from 'next/link';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
@@ -167,6 +168,8 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const { currentPage } = props;
   const { currentPage } = props;
 
 
+  const { t } = useTranslation();
+
   const router = useRouter();
   const router = useRouter();
 
 
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
@@ -317,6 +320,17 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             // grantUserGroupId={grantUserGroupId}
             // grantUserGroupId={grantUserGroupId}
           />
           />
         )}
         )}
+
+        { isGuestUser && (
+          <>
+            <Link href="/login#register" className="btn" prefetch={false}>
+              <span className="material-symbols-outlined me-1">person_add</span>{t('Sign up')}
+            </Link>
+            <Link href="/login#login" className="btn btn-primary" prefetch={false}>
+              <span className="material-symbols-outlined me-1">login</span>{t('Sign in')}
+            </Link>
+          </>
+        ) }
       </div>
       </div>
 
 
       {path != null && currentUser != null && !isReadOnlyUser && (
       {path != null && currentUser != null && !isReadOnlyUser && (

+ 3 - 1
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -7,6 +7,8 @@ import { toastError } from '~/client/util/toastr';
 import { useIsNotFound } from '~/stores/page';
 import { useIsNotFound } from '~/stores/page';
 import { EditorMode, useEditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
 import { EditorMode, useEditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
 
 
+import { shouldCreateWipPage } from '../../utils/should-create-wip-page';
+
 
 
 import styles from './PageEditorModeManager.module.scss';
 import styles from './PageEditorModeManager.module.scss';
 
 
@@ -72,7 +74,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
 
 
     try {
     try {
       await createAndTransit(
       await createAndTransit(
-        { path },
+        { path, wip: shouldCreateWipPage(path) },
         { shouldCheckPageExists: true },
         { shouldCheckPageExists: true },
       );
       );
     }
     }

+ 1 - 1
apps/app/src/components/Page/PageView.tsx

@@ -103,7 +103,7 @@ export const PageView = (props: Props): JSX.Element => {
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
 
 
   const headerContents = (
   const headerContents = (
-    <PagePathNavSticky pageId={page?._id} pagePath={pagePath} />
+    <PagePathNavSticky pageId={page?._id} pagePath={pagePath} isWipPage={page?.wip} />
   );
   );
 
 
   const sideContents = !isNotFound && !isNotCreatable
   const sideContents = !isNotFound && !isNotCreatable

+ 2 - 0
apps/app/src/components/PageAlert/PageAlerts.tsx

@@ -8,6 +8,7 @@ import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 import { PageStaleAlert } from './PageStaleAlert';
+import { WipPageAlert } from './WipPageAlert';
 
 
 const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 // dynamic import because TrashPageAlert uses localStorageMiddleware
 // dynamic import because TrashPageAlert uses localStorageMiddleware
@@ -22,6 +23,7 @@ export const PageAlerts = (): JSX.Element => {
       <div className="col-sm-12">
       <div className="col-sm-12">
         {/* alerts */}
         {/* alerts */}
         { !isNotFound && <FixPageGrantAlert /> }
         { !isNotFound && <FixPageGrantAlert /> }
+        <WipPageAlert />
         <PageGrantAlert />
         <PageGrantAlert />
         <TrashPageAlert />
         <TrashPageAlert />
         <PageStaleAlert />
         <PageStaleAlert />

+ 53 - 0
apps/app/src/components/PageAlert/WipPageAlert.tsx

@@ -0,0 +1,53 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
+
+import { publish } from '../../client/services/page-operation';
+
+
+export const WipPageAlert = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+
+  const clickPagePublishButton = useCallback(async() => {
+    const pageId = currentPage?._id;
+
+    if (pageId == null) {
+      return;
+    }
+
+    try {
+      await publish(pageId);
+      await mutateCurrentPage();
+      await mutatePageTree();
+      toastSuccess(t('wip_page.success_publish_page'));
+    }
+    catch {
+      toastError(t('wip_page.fail_publish_page'));
+    }
+  }, [currentPage?._id, mutateCurrentPage, t]);
+
+
+  if (!currentPage?.wip) {
+    return <></>;
+  }
+
+  return (
+    <p className="d-flex align-items-center alert alert-secondary py-3 px-4">
+      <span className="material-symbols-outlined me-1 fs-5">info</span>
+      <span>{t('wip_page.alert')}</span>
+      <button
+        type="button"
+        className="btn btn-outline-secondary ms-auto"
+        onClick={clickPagePublishButton}
+      >
+        {t('wip_page.publish_page') }
+      </button>
+    </p>
+  );
+};

+ 2 - 2
apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -20,7 +20,7 @@ import styles from './DeleteAttachmentModal.module.scss';
 const logger = loggerFactory('growi:attachmentDelete');
 const logger = loggerFactory('growi:attachmentDelete');
 
 
 const iconByFormat = (format: string): string => {
 const iconByFormat = (format: string): string => {
-  return format.match(/image\/.+/i) ? 'icon-picture' : 'icon-doc';
+  return format.match(/image\/.+/i) ? 'image' : 'description';
 };
 };
 
 
 export const DeleteAttachmentModal: React.FC = () => {
 export const DeleteAttachmentModal: React.FC = () => {
@@ -73,7 +73,7 @@ export const DeleteAttachmentModal: React.FC = () => {
     return (
     return (
       <div className="attachment-delete-image">
       <div className="attachment-delete-image">
         <p>
         <p>
-          <i className={iconByFormat(attachment.fileFormat)}></i> {attachment.originalName}
+          <span className="material-symbols-outlined">{iconByFormat(attachment.fileFormat)}</span> {attachment.originalName}
         </p>
         </p>
         <p>
         <p>
           uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator as IUser}></Username>
           uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator as IUser}></Username>

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

@@ -44,11 +44,11 @@ const SlackNotification = dynamic(() => import('../SlackNotification').then(mod
 
 
 const navTabMapping = {
 const navTabMapping = {
   comment_editor: {
   comment_editor: {
-    Icon: () => <i className="icon-settings" />,
+    Icon: () => <span className="material-symbols-outlined">edit_square</span>,
     i18n: 'Write',
     i18n: 'Write',
   },
   },
   comment_preview: {
   comment_preview: {
-    Icon: () => <i className="icon-settings" />,
+    Icon: () => <span className="material-symbols-outlined">play_arrow</span>,
     i18n: 'Preview',
     i18n: 'Preview',
   },
   },
 };
 };
@@ -263,7 +263,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
               onClick={() => setIsReadyToUse(true)}
               onClick={() => setIsReadyToUse(true)}
               data-testid="open-comment-editor-button"
               data-testid="open-comment-editor-button"
             >
             >
-              <i className="icon-bubble"></i> Add Comment
+              <span className="material-symbols-outlined">comment</span> Add Comment
             </button>
             </button>
           </NotAvailableForReadOnlyUser>
           </NotAvailableForReadOnlyUser>
         </NotAvailableForGuest>
         </NotAvailableForGuest>

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

@@ -68,8 +68,8 @@ export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
   }
   }
 
 
   const areThereHiddenReplies = (replyList.length > 2);
   const areThereHiddenReplies = (replyList.length > 2);
-  const toggleButtonIconName = isOlderRepliesShown ? 'icon-arrow-up' : 'icon-options-vertical';
-  const toggleButtonIcon = <i className={`icon-fw ${toggleButtonIconName}`}></i>;
+  const toggleButtonIconName = isOlderRepliesShown ? 'expand_less' : 'more_vert';
+  const toggleButtonIcon = <span className="material-icons-outlined me-1">{toggleButtonIconName}</span>;
   const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
   const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
   const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
   const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
   const hiddenReplies = replyList.slice(0, replyList.length - 2);
   const hiddenReplies = replyList.slice(0, replyList.length - 2);

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

@@ -52,7 +52,7 @@ const Tags = (props: TagsProps): JSX.Element => {
         className="btn btn-link btn-edit-tags text-muted border border-secondary p-1 d-flex align-items-center"
         className="btn btn-link btn-edit-tags text-muted border border-secondary p-1 d-flex align-items-center"
         onClick={onClickEditTagsButton}
         onClick={onClickEditTagsButton}
       >
       >
-        <i className="icon-tag me-2" />
+        <span className="material-symbols-outlined me-2">local_offer</span>
         Tags
         Tags
       </button>
       </button>
     </div>
     </div>

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

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
 
 
 import { useHandsontableModalForEditor } from '@growi/editor/src/stores/use-handsontable';
 import { useHandsontableModalForEditor } from '@growi/editor/src/stores/use-handsontable';
 import { HotTable } from '@handsontable/react';
 import { HotTable } from '@handsontable/react';
-import Handsontable from 'handsontable';
+import type Handsontable from 'handsontable';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Collapse,
   Collapse,

+ 37 - 30
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -1,3 +1,4 @@
+import type { CSSProperties } from 'react';
 import React, {
 import React, {
   useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
   useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
 } from 'react';
 } from 'react';
@@ -12,9 +13,9 @@ import {
   CodeMirrorEditorMain, GlobalCodeMirrorEditorKey,
   CodeMirrorEditorMain, GlobalCodeMirrorEditorKey,
   useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
   useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
 } from '@growi/editor';
 } from '@growi/editor';
+import { useRect } from '@growi/ui/dist/utils';
 import detectIndent from 'detect-indent';
 import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import { useRouter } from 'next/router';
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
 
 
 
 
@@ -31,7 +32,7 @@ import {
 } from '~/stores/context';
 } from '~/stores/context';
 import {
 import {
   useEditorSettings,
   useEditorSettings,
-  useCurrentIndentSize, useIsSlackEnabled, usePageTagsForEditors,
+  useCurrentIndentSize, usePageTagsForEditors,
   useIsConflict,
   useIsConflict,
   useEditingMarkdown,
   useEditingMarkdown,
   useWaitingSaveProcessing,
   useWaitingSaveProcessing,
@@ -59,7 +60,6 @@ import { PageHeader } from '../PageHeader/PageHeader';
 
 
 // import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 // import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 // import { ConflictDiffModal } from './ConflictDiffModal';
 // import { ConflictDiffModal } from './ConflictDiffModal';
-// import Editor from './Editor';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
 import Preview from './Preview';
 import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
 import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
@@ -86,9 +86,8 @@ type Props = {
 export const PageEditor = React.memo((props: Props): JSX.Element => {
 export const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const router = useRouter();
 
 
-  const previewRef = useRef<HTMLDivElement>(null);
+  const [previewRect, previewRef] = useRect();
 
 
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
@@ -105,7 +104,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
   const { mutate: mutateWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { mutate: mutateWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
-  const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: defaultIndentSize } = useDefaultIndentSize();
   const { data: defaultIndentSize } = useDefaultIndentSize();
@@ -164,15 +162,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const setMarkdownPreviewWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string) => {
   const setMarkdownPreviewWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string) => {
     setMarkdownToPreview(value);
     setMarkdownToPreview(value);
   })), []);
   })), []);
-  // const mutateIsEnabledUnsavedWarningWithDebounce = useMemo(() => debounce(600, throttle(900, (value: string) => {
-  //   // Displays an unsaved warning alert
-  //   mutateIsEnabledUnsavedWarning(value !== initialValueRef.current);
-  // })), [mutateIsEnabledUnsavedWarning]);
 
 
   const markdownChangedHandler = useCallback((value: string) => {
   const markdownChangedHandler = useCallback((value: string) => {
     setMarkdownPreviewWithDebounce(value);
     setMarkdownPreviewWithDebounce(value);
-    // mutateIsEnabledUnsavedWarningWithDebounce(value);
-  // }, [mutateIsEnabledUnsavedWarningWithDebounce, setMarkdownPreviewWithDebounce]);
   }, [setMarkdownPreviewWithDebounce]);
   }, [setMarkdownPreviewWithDebounce]);
 
 
 
 
@@ -200,9 +192,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   }, [socket, checkIsConflict]);
   }, [socket, checkIsConflict]);
 
 
   const save = useCallback(async(opts?: {slackChannels: string, overwriteScopesOfDescendants?: boolean}): Promise<IPageHasId | null> => {
   const save = useCallback(async(opts?: {slackChannels: string, overwriteScopesOfDescendants?: boolean}): Promise<IPageHasId | null> => {
-    if (pageId == null || currentPagePath == null || currentRevisionId == null || grantData == null) {
+    if (pageId == null || currentRevisionId == null || grantData == null) {
       logger.error('Some materials to save are invalid', {
       logger.error('Some materials to save are invalid', {
-        pageId, currentPagePath, currentRevisionId, grantData,
+        pageId, currentRevisionId, grantData,
       });
       });
       throw new Error('Some materials to save are invalid');
       throw new Error('Some materials to save are invalid');
     }
     }
@@ -242,7 +234,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     }
     }
 
 
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [codeMirrorEditor, grantData, pageId, currentPagePath, currentRevisionId, mutateWaitingSaveProcessing, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser]);
+  }, [codeMirrorEditor, grantData, pageId, currentRevisionId, mutateWaitingSaveProcessing, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser]);
 
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     const page = await save(opts);
     const page = await save(opts);
@@ -390,6 +382,19 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     }
     }
   }, [initialValue, isIndentSizeForced, mutateCurrentIndentSize]);
   }, [initialValue, isIndentSizeForced, mutateCurrentIndentSize]);
 
 
+  // set handler to set caret line
+  useEffect(() => {
+    const handler = (lineNumber?: number) => {
+      codeMirrorEditor?.setCaretLine(lineNumber);
+
+      // TODO: scroll to the caret line
+    };
+    globalEmitter.on('setCaretLine', handler);
+
+    return function cleanup() {
+      globalEmitter.removeListener('setCaretLine', handler);
+    };
+  }, [codeMirrorEditor]);
 
 
   // TODO: Check the reproduction conditions that made this code necessary and confirm reproduction
   // TODO: Check the reproduction conditions that made this code necessary and confirm reproduction
   // // when transitioning to a different page, if the initialValue is the same,
   // // when transitioning to a different page, if the initialValue is the same,
@@ -406,6 +411,17 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   //   };
   //   };
   // }, [onRouterChangeComplete, router.events]);
   // }, [onRouterChangeComplete, router.events]);
 
 
+  const pastEndStyle: CSSProperties | undefined = useMemo(() => {
+    if (previewRect == null) {
+      return undefined;
+    }
+
+    const previewRectHeight = previewRect.height;
+
+    // containerHeight - 1.5 line height
+    return { paddingBottom: `calc(${previewRectHeight}px - 2em)` };
+  }, [previewRect]);
+
   if (!isEditable) {
   if (!isEditable) {
     return <></>;
     return <></>;
   }
   }
@@ -421,18 +437,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       </div>
       </div>
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
         <div className="page-editor-editor-container flex-expand-vert">
         <div className="page-editor-editor-container flex-expand-vert">
-          {/* <Editor
-            ref={editorRef}
-            value={initialValue}
-            isUploadable={isUploadable}
-            isUploadAllFileAllowed={isUploadAllFileAllowed}
-            indentSize={currentIndentSize}
-            onScroll={editorScrolledHandler}
-            onScrollCursorIntoView={editorScrollCursorIntoViewHandler}
-            onChange={markdownChangedHandler}
-            onUpload={uploadHandler}
-            onSave={saveWithShortcut}
-          /> */}
           <CodeMirrorEditorMain
           <CodeMirrorEditorMain
             onChange={markdownChangedHandler}
             onChange={markdownChangedHandler}
             onSave={saveWithShortcut}
             onSave={saveWithShortcut}
@@ -448,14 +452,17 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             editorKeymap={editorSettings?.keymapMode}
             editorKeymap={editorSettings?.keymapMode}
           />
           />
         </div>
         </div>
-        <div ref={previewRef} onScroll={scrollPreviewHandlerThrottle} className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">
+        <div
+          ref={previewRef}
+          onScroll={scrollPreviewHandlerThrottle}
+          className="page-editor-preview-container flex-expand-vert d-none d-lg-flex"
+        >
           <Preview
           <Preview
             rendererOptions={rendererOptions}
             rendererOptions={rendererOptions}
             markdown={markdownToPreview}
             markdown={markdownToPreview}
             pagePath={currentPagePath}
             pagePath={currentPagePath}
             expandContentWidth={shouldExpandContent}
             expandContentWidth={shouldExpandContent}
-            // TODO: Dynamic changes by height or resizing the last element
-            pastEnd={500}
+            style={pastEndStyle}
           />
           />
         </div>
         </div>
         {/*
         {/*

+ 1 - 0
apps/app/src/components/PageEditor/Preview.module.scss

@@ -3,6 +3,7 @@
 .page-editor-preview-body :global {
 .page-editor-preview-body :global {
   .wiki {
   .wiki {
     max-width: 980px;
     max-width: 980px;
+    padding: 0px 15px;
     margin: 0 auto;
     margin: 0 auto;
   }
   }
 }
 }

+ 7 - 10
apps/app/src/components/PageEditor/Preview.tsx

@@ -1,6 +1,5 @@
-import React, {
-  SyntheticEvent, RefObject,
-} from 'react';
+import type { CSSProperties } from 'react';
+import React from 'react';
 
 
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 
 
@@ -17,15 +16,15 @@ type Props = {
   markdown?: string,
   markdown?: string,
   pagePath?: string | null,
   pagePath?: string | null,
   expandContentWidth?: boolean,
   expandContentWidth?: boolean,
-  pastEnd?: number,
+  style?: CSSProperties,
   onScroll?: (scrollTop: number) => void,
   onScroll?: (scrollTop: number) => void,
 }
 }
 
 
-const Preview = React.forwardRef((props: Props): JSX.Element => {
+const Preview = (props: Props): JSX.Element => {
 
 
   const {
   const {
     rendererOptions,
     rendererOptions,
-    markdown, pagePath, pastEnd,
+    markdown, pagePath, style,
     expandContentWidth,
     expandContentWidth,
   } = props;
   } = props;
 
 
@@ -34,7 +33,7 @@ const Preview = React.forwardRef((props: Props): JSX.Element => {
   return (
   return (
     <div
     <div
       className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
-      style={{ paddingBottom: pastEnd }}
+      style={style}
     >
     >
       { markdown != null && (
       { markdown != null && (
         <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>
         <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>
@@ -42,8 +41,6 @@ const Preview = React.forwardRef((props: Props): JSX.Element => {
     </div>
     </div>
   );
   );
 
 
-});
-
-Preview.displayName = 'Preview';
+};
 
 
 export default Preview;
 export default Preview;

+ 5 - 1
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -92,7 +92,11 @@ export const PageTitleHeader: FC<Props> = (props) => {
         </h1>
         </h1>
       </div>
       </div>
 
 
-      <div className={`${isRenameInputShown ? 'invisible' : ''}`}>
+      <div className={`${isRenameInputShown ? 'invisible' : ''} d-flex align-items-center`}>
+        { currentPage.wip && (
+          <span className="badge rounded-pill text-bg-secondary ms-2">WIP</span>
+        )}
+
         <CopyDropdown
         <CopyDropdown
           pageId={currentPage._id}
           pageId={currentPage._id}
           pagePath={currentPage.path}
           pagePath={currentPage.path}

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

@@ -71,11 +71,12 @@ const PagePresentationModal = (): JSX.Element => {
     >
     >
       <div className="grw-presentation-controls d-flex">
       <div className="grw-presentation-controls d-flex">
         <button
         <button
-          className={`btn ${fullscreen.active ? 'icon-size-actual' : 'icon-size-fullscreen'}`}
+          className="btn material-symbols-outlined"
           type="button"
           type="button"
           aria-label="fullscreen"
           aria-label="fullscreen"
           onClick={toggleFullscreenHandler}
           onClick={toggleFullscreenHandler}
         >
         >
+          {fullscreen.active ? 'close_fullscreen' : 'open_in_full'}
         </button>
         </button>
         <button className="btn-close" type="button" aria-label="Close" onClick={closeHandler}></button>
         <button className="btn-close" type="button" aria-label="Close" onClick={closeHandler}></button>
       </div>
       </div>

+ 7 - 19
apps/app/src/components/PageTags/TagsInput.tsx

@@ -1,19 +1,12 @@
-import React, {
-  FC, useRef, useState, useCallback,
-} from 'react';
+import type { FC, KeyboardEvent } from 'react';
+import React, { useRef, useState, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import type { TypeaheadRef } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 
 import { useSWRxTagsSearch } from '~/stores/tag';
 import { useSWRxTagsSearch } from '~/stores/tag';
 
 
-type TypeaheadInstance = {
-  _handleMenuItemSelect: (activeItem: string, event: React.KeyboardEvent) => void,
-  state: {
-    initialItem: string,
-  },
-}
-
 type Props = {
 type Props = {
   tags: string[],
   tags: string[],
   autoFocus: boolean,
   autoFocus: boolean,
@@ -24,7 +17,7 @@ export const TagsInput: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { tags, autoFocus, onTagsUpdated } = props;
   const { tags, autoFocus, onTagsUpdated } = props;
 
 
-  const tagsInputRef = useRef<TypeaheadInstance>(null);
+  const tagsInputRef = useRef<TypeaheadRef>(null);
   const [resultTags, setResultTags] = useState<string[]>([]);
   const [resultTags, setResultTags] = useState<string[]>([]);
   const [searchQuery, setSearchQuery] = useState('');
   const [searchQuery, setSearchQuery] = useState('');
 
 
@@ -33,20 +26,17 @@ export const TagsInput: FC<Props> = (props: Props) => {
   const isLoading = error == null && tagsSearch === undefined;
   const isLoading = error == null && tagsSearch === undefined;
 
 
   const changeHandler = useCallback((selected: string[]) => {
   const changeHandler = useCallback((selected: string[]) => {
-    if (onTagsUpdated != null) {
-      onTagsUpdated(selected);
-    }
+    onTagsUpdated(selected);
   }, [onTagsUpdated]);
   }, [onTagsUpdated]);
 
 
-  const searchHandler = useCallback(async(query: string) => {
+  const searchHandler = useCallback((query: string) => {
     const tagsSearchData = tagsSearch?.tags || [];
     const tagsSearchData = tagsSearch?.tags || [];
     setSearchQuery(query);
     setSearchQuery(query);
     tagsSearchData.unshift(query);
     tagsSearchData.unshift(query);
     setResultTags(Array.from(new Set(tagsSearchData)));
     setResultTags(Array.from(new Set(tagsSearchData)));
-
   }, [tagsSearch?.tags]);
   }, [tagsSearch?.tags]);
 
 
-  const keyDownHandler = useCallback((event: React.KeyboardEvent) => {
+  const keyDownHandler = useCallback((event: KeyboardEvent<HTMLElement>) => {
     if (event.key === ' ') {
     if (event.key === ' ') {
       event.preventDefault();
       event.preventDefault();
 
 
@@ -64,12 +54,10 @@ export const TagsInput: FC<Props> = (props: Props) => {
       <AsyncTypeahead
       <AsyncTypeahead
         id="tag-typeahead-asynctypeahead"
         id="tag-typeahead-asynctypeahead"
         ref={tagsInputRef}
         ref={tagsInputRef}
-        caseSensitive={false}
         defaultSelected={tags}
         defaultSelected={tags}
         isLoading={isLoading}
         isLoading={isLoading}
         minLength={1}
         minLength={1}
         multiple
         multiple
-        newSelectionPrefix=""
         onChange={changeHandler}
         onChange={changeHandler}
         onSearch={searchHandler}
         onSearch={searchHandler}
         onKeyDown={keyDownHandler}
         onKeyDown={keyDownHandler}

+ 29 - 1
apps/app/src/components/SavePageControls.tsx

@@ -9,15 +9,19 @@ import {
   DropdownToggle, DropdownMenu, DropdownItem,
   DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { IPageGrantData } from '~/interfaces/page';
 import type { IPageGrantData } from '~/interfaces/page';
 import {
 import {
   useIsEditable, useIsAclEnabled,
   useIsEditable, useIsAclEnabled,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useWaitingSaveProcessing } from '~/stores/editor';
 import { useWaitingSaveProcessing } from '~/stores/editor';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import { useSelectedGrant } from '~/stores/ui';
 import { useSelectedGrant } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { unpublish } from '../client/services/page-operation';
+
 import { GrantSelector } from './SavePageControls/GrantSelector';
 import { GrantSelector } from './SavePageControls/GrantSelector';
 
 
 
 
@@ -41,6 +45,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
   const { data: isAclEnabled } = useIsAclEnabled();
   const { data: isAclEnabled } = useIsAclEnabled();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
 
   const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
   const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
 
 
@@ -58,6 +63,25 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
     globalEmitter.emit('saveAndReturnToView', { overwriteScopesOfDescendants: true, slackChannels });
     globalEmitter.emit('saveAndReturnToView', { overwriteScopesOfDescendants: true, slackChannels });
   }, [slackChannels]);
   }, [slackChannels]);
 
 
+  const clickUnpublishButtonHandler = useCallback(async() => {
+    const pageId = currentPage?._id;
+
+    if (pageId == null) {
+      return;
+    }
+
+    try {
+      await unpublish(pageId);
+      await mutateCurrentPage();
+      await mutatePageTree();
+      toastSuccess(t('wip_page.success_save_as_wip'));
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(t('wip_page.fail_save_as_wip'));
+    }
+  }, [currentPage?._id, mutateCurrentPage, t]);
+
 
 
   if (isEditable == null || isAclEnabled == null || grantData == null) {
   if (isEditable == null || isAclEnabled == null || grantData == null) {
     return null;
     return null;
@@ -72,6 +96,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
   const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
   const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
   const labelSubmitButton = t('Update');
   const labelSubmitButton = t('Update');
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
+  const labelUnpublishPage = t('wip_page.save_as_wip');
 
 
   return (
   return (
     <div className="d-flex align-items-center flex-nowrap">
     <div className="d-flex align-items-center flex-nowrap">
@@ -108,6 +133,9 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
           <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
           <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
             {labelOverwriteScopes}
             {labelOverwriteScopes}
           </DropdownItem>
           </DropdownItem>
+          <DropdownItem onClick={clickUnpublishButtonHandler}>
+            {labelUnpublishPage}
+          </DropdownItem>
         </DropdownMenu>
         </DropdownMenu>
       </UncontrolledButtonDropdown>
       </UncontrolledButtonDropdown>
 
 

+ 5 - 5
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -18,18 +18,18 @@ import { useMyUserGroups } from './use-my-user-groups';
 
 
 const AVAILABLE_GRANTS = [
 const AVAILABLE_GRANTS = [
   {
   {
-    grant: PageGrant.GRANT_PUBLIC, iconClass: 'icon-people', btnStyleClass: 'outline-info', label: 'Public',
+    grant: PageGrant.GRANT_PUBLIC, iconName: 'group', btnStyleClass: 'outline-info', label: 'Public',
   },
   },
   {
   {
-    grant: PageGrant.GRANT_RESTRICTED, iconClass: 'icon-link', btnStyleClass: 'outline-teal', label: 'Anyone with the link',
+    grant: PageGrant.GRANT_RESTRICTED, iconName: 'link', btnStyleClass: 'outline-teal', label: 'Anyone with the link',
   },
   },
   // { grant: 3, iconClass: '', label: 'Specified users only' },
   // { grant: 3, iconClass: '', label: 'Specified users only' },
   {
   {
-    grant: PageGrant.GRANT_OWNER, iconClass: 'icon-lock', btnStyleClass: 'outline-danger', label: 'Only me',
+    grant: PageGrant.GRANT_OWNER, iconName: 'lock', btnStyleClass: 'outline-danger', label: 'Only me',
   },
   },
   {
   {
     grant: PageGrant.GRANT_USER_GROUP,
     grant: PageGrant.GRANT_USER_GROUP,
-    iconClass: 'icon-options',
+    iconName: 'more_horiz',
     btnStyleClass: 'outline-purple',
     btnStyleClass: 'outline-purple',
     label: 'Only inside the group',
     label: 'Only inside the group',
     reselectLabel: 'Reselect the group',
     reselectLabel: 'Reselect the group',
@@ -119,7 +119,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
 
       const labelElm = (
       const labelElm = (
         <span>
         <span>
-          <i className={`icon icon-fw ${opt.iconClass}`}></i>
+          <span className="material-symbols-outlined me-2">{opt.iconName}</span>
           <span className="label">{t(label)}</span>
           <span className="label">{t(label)}</span>
         </span>
         </span>
       );
       );

+ 24 - 24
apps/app/src/components/SearchTypeahead.tsx

@@ -1,14 +1,18 @@
+import type {
+  FC, ForwardRefRenderFunction,
+  KeyboardEvent, MouseEvent,
+} from 'react';
 import React, {
 import React, {
-  FC, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
-  KeyboardEvent, useCallback, useRef, useState, MouseEvent, useEffect,
+  forwardRef, useImperativeHandle, useCallback, useRef, useState, useEffect,
 } from 'react';
 } from 'react';
 
 
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui/dist/components';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui/dist/components';
+import type { TypeaheadRef } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 
 
-import { IFocusable } from '~/client/interfaces/focusable';
-import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
-import { IPageWithSearchMeta } from '~/interfaces/search';
+import type { IFocusable } from '~/client/interfaces/focusable';
+import type { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
+import type { IPageWithSearchMeta } from '~/interfaces/search';
 import { useSWRxSearch } from '~/stores/search';
 import { useSWRxSearch } from '~/stores/search';
 
 
 
 
@@ -43,15 +47,6 @@ type Props = TypeaheadProps & {
   helpElement?: React.ReactNode,
   helpElement?: React.ReactNode,
 };
 };
 
 
-// see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
-type TypeaheadInstance = {
-  setState(input: { text: string | undefined; }): void;
-  clear: () => void,
-  focus: () => void,
-  toggleMenu: () => void,
-  state: { selected: IPageWithSearchMeta[] }
-}
-
 const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
 const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
   const {
   const {
     onSearchError, onSearch, onInputChange, onChange, onSubmit,
     onSearchError, onSearch, onInputChange, onChange, onSubmit,
@@ -61,7 +56,7 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
 
 
   const [input, setInput] = useState(keywordOnInit);
   const [input, setInput] = useState(keywordOnInit);
   const [searchKeyword, setSearchKeyword] = useState('');
   const [searchKeyword, setSearchKeyword] = useState('');
-  const [isForcused, setFocused] = useState(false);
+  const [isFocused, setFocused] = useState(false);
 
 
   const { data: searchResult, error: searchError, isLoading } = useSWRxSearch(
   const { data: searchResult, error: searchError, isLoading } = useSWRxSearch(
     disableIncrementalSearch ? null : searchKeyword,
     disableIncrementalSearch ? null : searchKeyword,
@@ -69,7 +64,7 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
     { limit: 10 },
     { limit: 10 },
   );
   );
 
 
-  const typeaheadRef = useRef<TypeaheadInstance>(null);
+  const typeaheadRef = useRef<TypeaheadRef>(null);
 
 
   const focusToTypeahead = () => {
   const focusToTypeahead = () => {
     const instance = typeaheadRef.current;
     const instance = typeaheadRef.current;
@@ -141,14 +136,18 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
     if (selectedItems.length > 0) {
     if (selectedItems.length > 0) {
       setInput(selectedItems[0].data.path);
       setInput(selectedItems[0].data.path);
 
 
+      if (onInputChange != null) {
+        onInputChange(selectedItems[0].data.path);
+      }
+
       if (onChange != null) {
       if (onChange != null) {
         onChange(selectedItems);
         onChange(selectedItems);
       }
       }
     }
     }
-  }, [onChange]);
+  }, [onChange, onInputChange]);
 
 
   const keyDownHandler = useCallback((event: KeyboardEvent) => {
   const keyDownHandler = useCallback((event: KeyboardEvent) => {
-    if (event.keyCode === 13) { // Enter key
+    if (event.key === 'Enter') {
       if (onSubmit != null && input != null && input.length > 0) {
       if (onSubmit != null && input != null && input.length > 0) {
         // schedule to submit with 100ms delay
         // schedule to submit with 100ms delay
         timeoutIdRef.current = setTimeout(() => onSubmit(input), DELAY_FOR_SUBMISSION);
         timeoutIdRef.current = setTimeout(() => onSubmit(input), DELAY_FOR_SUBMISSION);
@@ -168,19 +167,20 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
   useEffect(() => {
   useEffect(() => {
     // update input with Next Link
     // update input with Next Link
     // update input workaround. see: https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
     // update input workaround. see: https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
-    if (typeaheadRef.current != null) {
+    if (typeaheadRef.current != null && keywordOnInit != null) {
       typeaheadRef.current.setState({
       typeaheadRef.current.setState({
         text: keywordOnInit,
         text: keywordOnInit,
       });
       });
     }
     }
   }, [keywordOnInit]);
   }, [keywordOnInit]);
 
 
-  const labelKey = useCallback((option?: IPageWithSearchMeta) => {
-    return option?.data.path ?? '';
+
+  const labelKey = useCallback((option: IPageWithSearchMeta) => {
+    return option.data.path ?? '';
   }, []);
   }, []);
 
 
   const renderMenu = useCallback((options: IPageWithSearchMeta[], menuProps) => {
   const renderMenu = useCallback((options: IPageWithSearchMeta[], menuProps) => {
-    if (!isForcused) {
+    if (!isFocused) {
       return <></>;
       return <></>;
     }
     }
 
 
@@ -216,7 +216,7 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
         ))}
         ))}
       </Menu>
       </Menu>
     );
     );
-  }, [disableIncrementalSearch, helpElement, input, isForcused]);
+  }, [disableIncrementalSearch, helpElement, input, isFocused]);
 
 
   const isOpenAlways = helpElement != null;
   const isOpenAlways = helpElement != null;
 
 
@@ -232,7 +232,7 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
         isLoading={isLoading}
         isLoading={isLoading}
         labelKey={labelKey}
         labelKey={labelKey}
         defaultInputValue={keywordOnInit}
         defaultInputValue={keywordOnInit}
-        options={searchResult?.data} // Search result (Some page names)
+        options={searchResult?.data ?? []} // Search result (Some page names)
         align="left"
         align="left"
         open={isOpenAlways || undefined}
         open={isOpenAlways || undefined}
         renderMenu={renderMenu}
         renderMenu={renderMenu}

+ 4 - 6
apps/app/src/components/Sidebar/Bookmarks.tsx

@@ -12,11 +12,9 @@ export const Bookmarks = () : JSX.Element => {
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
   return (
   return (
-    <>
-      {/* TODO : #139425 Match the space specification method to others */}
-      {/* ref.  https://redmine.weseek.co.jp/issues/139425 */}
-      <div className="grw-sidebar-content-header p-3">
-        <h3 className="mb-0">{t('Bookmarks')}</h3>
+    <div className="px-3">
+      <div className="grw-sidebar-content-header">
+        <h4 className="mb-0 py-4">{t('Bookmarks')}</h4>
       </div>
       </div>
       {isGuestUser ? (
       {isGuestUser ? (
         <h4 className="ps-3">
         <h4 className="ps-3">
@@ -25,6 +23,6 @@ export const Bookmarks = () : JSX.Element => {
       ) : (
       ) : (
         <BookmarkContents />
         <BookmarkContents />
       )}
       )}
-    </>
+    </div>
   );
   );
 };
 };

+ 4 - 6
apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx

@@ -18,14 +18,12 @@ export const CustomSidebar = (): JSX.Element => {
   const { mutate, isLoading } = useSWRxPageByPath('/Sidebar');
   const { mutate, isLoading } = useSWRxPageByPath('/Sidebar');
 
 
   return (
   return (
-    // TODO : #139425 Match the space specification method to others
-    // ref.  https://redmine.weseek.co.jp/issues/139425
-    <div className="px-3">
-      <div className="grw-sidebar-content-header py-3 d-flex">
-        <h3 className="mb-0">
+    <div className="pt-4 pb-3 px-3">
+      <div className="grw-sidebar-content-header d-flex">
+        <h4 className="mb-0">
           {t('CustomSidebar')}
           {t('CustomSidebar')}
           { !isLoading && <Link href="/Sidebar#edit" className="h6 ms-2"><span className="material-symbols-outlined">edit</span></Link> }
           { !isLoading && <Link href="/Sidebar#edit" className="h6 ms-2"><span className="material-symbols-outlined">edit</span></Link> }
-        </h3>
+        </h4>
         { !isLoading && <SidebarHeaderReloadButton onClick={() => mutate()} /> }
         { !isLoading && <SidebarHeaderReloadButton onClick={() => mutate()} /> }
       </div>
       </div>
 
 

+ 13 - 4
apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx

@@ -1,16 +1,25 @@
-import Link from 'next/link';
+import { useCallback } from 'react';
+
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
+import { useCreatePageAndTransit } from '~/client/services/create-page';
+
 export const SidebarNotFound = (): JSX.Element => {
 export const SidebarNotFound = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
+  const { createAndTransit } = useCreatePageAndTransit();
+
+  const clickCreateButtonHandler = useCallback(async() => {
+    createAndTransit({ path: '/Sidebar', wip: false });
+  }, [createAndTransit]);
+
   return (
   return (
-    <div className="grw-sidebar-content-header h5 text-center py-3">
-      <Link href="/Sidebar#edit">
+    <div>
+      <button type="button" className="btn btn-lg btn-link" onClick={clickCreateButtonHandler}>
         <span className="material-symbols-outlined">edit_note</span>
         <span className="material-symbols-outlined">edit_note</span>
         {/* eslint-disable-next-line react/no-danger */}
         {/* eslint-disable-next-line react/no-danger */}
         <span dangerouslySetInnerHTML={{ __html: t('Create Sidebar Page') }}></span>
         <span dangerouslySetInnerHTML={{ __html: t('Create Sidebar Page') }}></span>
-      </Link>
+      </button>
     </div>
     </div>
   );
   );
 };
 };

+ 1 - 1
apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx

@@ -22,7 +22,7 @@ export const CustomSidebarSubstance = (): JSX.Element => {
   const markdown = page?.revision?.body;
   const markdown = page?.revision?.body;
 
 
   return (
   return (
-    <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
+    <div className={`py-4 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
       { markdown == null
       { markdown == null
         ? <SidebarNotFound />
         ? <SidebarNotFound />
         : (
         : (

+ 3 - 5
apps/app/src/components/Sidebar/InAppNotification/InAppNotification.tsx

@@ -15,13 +15,11 @@ export const InAppNotification = (): JSX.Element => {
   const [isUnopendNotificationsVisible, setUnopendNotificationsVisible] = useState(false);
   const [isUnopendNotificationsVisible, setUnopendNotificationsVisible] = useState(false);
 
 
   return (
   return (
-    // TODO : #139425 Match the space specification method to others
-    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="px-3">
     <div className="px-3">
-      <div className="grw-sidebar-content-header py-3 d-flex">
-        <h3 className="mb-0">
+      <div className="grw-sidebar-content-header py-4 d-flex">
+        <h4 className="mb-0">
           {t('In-App Notification')}
           {t('In-App Notification')}
-        </h3>
+        </h4>
       </div>
       </div>
 
 
       <InAppNotificationForms
       <InAppNotificationForms

+ 1 - 1
apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-new-page.ts

@@ -18,7 +18,7 @@ export const useCreateNewPage: UseCreateNewPage = () => {
     if (isLoadingPagePath) return;
     if (isLoadingPagePath) return;
 
 
     return createAndTransit(
     return createAndTransit(
-      { parentPath: currentPagePath, optionalParentPath: '/' },
+      { parentPath: currentPagePath, optionalParentPath: '/', wip: true },
     );
     );
   }, [createAndTransit, currentPagePath, isLoadingPagePath]);
   }, [createAndTransit, currentPagePath, isLoadingPagePath]);
 
 

+ 1 - 1
apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-todays-memo.tsx

@@ -32,7 +32,7 @@ export const useCreateTodaysMemo: UseCreateTodaysMemo = () => {
     if (!isCreatable || todaysPath == null) return;
     if (!isCreatable || todaysPath == null) return;
 
 
     return createAndTransit(
     return createAndTransit(
-      { path: todaysPath },
+      { path: todaysPath, wip: true },
       { shouldCheckPageExists: true },
       { shouldCheckPageExists: true },
     );
     );
   }, [createAndTransit, isCreatable, todaysPath]);
   }, [createAndTransit, isCreatable, todaysPath]);

+ 10 - 6
apps/app/src/components/Sidebar/PageTree/PageTree.tsx

@@ -1,9 +1,10 @@
-import { Suspense } from 'react';
+import { Suspense, useState } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import ItemsTreeContentSkeleton from '../../ItemsTree/ItemsTreeContentSkeleton';
 import ItemsTreeContentSkeleton from '../../ItemsTree/ItemsTreeContentSkeleton';
+
 import { PageTreeHeader } from './PageTreeSubstance';
 import { PageTreeHeader } from './PageTreeSubstance';
 
 
 const PageTreeContent = dynamic(
 const PageTreeContent = dynamic(
@@ -15,19 +16,22 @@ const PageTreeContent = dynamic(
 export const PageTree = (): JSX.Element => {
 export const PageTree = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
+  const [isWipPageShown, setIsWipPageShown] = useState(true);
+
   return (
   return (
-    // TODO : #139425 Match the space specification method to others
-    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="pt-4 pb-3 px-3">
     <div className="pt-4 pb-3 px-3">
       <div className="grw-sidebar-content-header d-flex">
       <div className="grw-sidebar-content-header d-flex">
-        <h3 className="mb-0">{t('Page Tree')}</h3>
+        <h4 className="mb-0">{t('Page Tree')}</h4>
         <Suspense>
         <Suspense>
-          <PageTreeHeader />
+          <PageTreeHeader
+            isWipPageShown={isWipPageShown}
+            onWipPageShownChange={() => { setIsWipPageShown(!isWipPageShown) }}
+          />
         </Suspense>
         </Suspense>
       </div>
       </div>
 
 
       <Suspense fallback={<ItemsTreeContentSkeleton />}>
       <Suspense fallback={<ItemsTreeContentSkeleton />}>
-        <PageTreeContent />
+        <PageTreeContent isWipPageShown={isWipPageShown} />
       </Suspense>
       </Suspense>
     </div>
     </div>
   );
   );

+ 40 - 2
apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -1,6 +1,9 @@
 import React, { memo, useCallback } from 'react';
 import React, { memo, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import {
+  UncontrolledButtonDropdown, DropdownMenu, DropdownToggle, DropdownItem,
+} from 'reactstrap';
 
 
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
@@ -12,8 +15,14 @@ import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 
 
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 
 
+type HeaderProps = {
+  isWipPageShown: boolean,
+  onWipPageShownChange?: () => void
+}
+
+export const PageTreeHeader = memo(({ isWipPageShown, onWipPageShownChange }: HeaderProps) => {
+  const { t } = useTranslation();
 
 
-export const PageTreeHeader = memo(() => {
   const { mutate: mutateRootPage } = useSWRxRootPage({ suspense: true });
   const { mutate: mutateRootPage } = useSWRxRootPage({ suspense: true });
   useSWRxV5MigrationStatus({ suspense: true });
   useSWRxV5MigrationStatus({ suspense: true });
 
 
@@ -25,6 +34,29 @@ export const PageTreeHeader = memo(() => {
   return (
   return (
     <>
     <>
       <SidebarHeaderReloadButton onClick={() => mutate()} />
       <SidebarHeaderReloadButton onClick={() => mutate()} />
+
+      <UncontrolledButtonDropdown className="me-1">
+        <DropdownToggle color="transparent" className="p-0 border-0">
+          <span className="material-symbols-outlined">more_horiz</span>
+        </DropdownToggle>
+
+        <DropdownMenu container="body">
+          <DropdownItem onClick={onWipPageShownChange} className="">
+            <div className="form-check form-switch">
+              <input
+                id="wipPageVisibility"
+                className="form-check-input"
+                type="checkbox"
+                checked={isWipPageShown}
+                onChange={() => {}}
+              />
+              <label className="form-label form-check-label text-muted" htmlFor="wipPageVisibility">
+                {t('sidebar_header.show_wip_page')}
+              </label>
+            </div>
+          </DropdownItem>
+        </DropdownMenu>
+      </UncontrolledButtonDropdown>
     </>
     </>
   );
   );
 });
 });
@@ -44,7 +76,12 @@ const PageTreeUnavailable = () => {
   );
   );
 };
 };
 
 
-export const PageTreeContent = memo(() => {
+type PageTreeContentProps = {
+  isWipPageShown: boolean,
+}
+
+export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) => {
+
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: currentPath } = useCurrentPagePath();
@@ -73,6 +110,7 @@ export const PageTreeContent = memo(() => {
       <ItemsTree
       <ItemsTree
         isEnableActions={!isGuestUser}
         isEnableActions={!isGuestUser}
         isReadOnlyUser={!!isReadOnlyUser}
         isReadOnlyUser={!!isReadOnlyUser}
+        isWipPageShown={isWipPageShown}
         targetPath={path}
         targetPath={path}
         targetPathOrId={targetPathOrId}
         targetPathOrId={targetPathOrId}
         targetAndAncestorsData={targetAndAncestorsData}
         targetAndAncestorsData={targetAndAncestorsData}

+ 3 - 2
apps/app/src/components/Sidebar/PageTree/PrivateLegacyPagesLink.tsx

@@ -1,4 +1,5 @@
-import React, { FC, memo } from 'react';
+import type { FC } from 'react';
+import React, { memo } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
@@ -13,7 +14,7 @@ export const PrivateLegacyPagesLink: FC = memo(() => {
       className="h5 grw-private-legacy-pages-anchor text-decoration-none"
       className="h5 grw-private-legacy-pages-anchor text-decoration-none"
       prefetch={false}
       prefetch={false}
     >
     >
-      <i className="icon-drawer me-2"></i> {t('private_legacy_pages.title')}
+      <span className="material-symbols-outlined me-2">bottom_drawer</span> {t('private_legacy_pages.title')}
     </Link>
     </Link>
   );
   );
 });
 });

+ 1 - 1
apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx

@@ -162,7 +162,7 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
           >
           >
             {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
             {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
-              <i id="option-button-in-page-tree" className="icon-options fa fa-rotate-90 p-1"></i>
+              <span id="option-button-in-page-tree" className="material-symbols-outlined p-1">more_vert</span>
             </DropdownToggle>
             </DropdownToggle>
           </PageItemControl>
           </PageItemControl>
         </div>
         </div>

+ 2 - 2
apps/app/src/components/ItemsTree/Item.module.scss → apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.module.scss

@@ -1,7 +1,6 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use '@growi/core/scss/bootstrap/init' as bs;
 
 
-// TODO: relocate following styles into PageTreeItem.mdoule.scss after refactoring
-// https://redmine.weseek.co.jp/issues/127544
+// == Colors
 @include bs.color-mode(light) {
 @include bs.color-mode(light) {
   .pagetree-item :global {
   .pagetree-item :global {
     .list-group-item-action {
     .list-group-item-action {
@@ -13,6 +12,7 @@
     }
     }
   }
   }
 }
 }
+
 @include bs.color-mode(dark) {
 @include bs.color-mode(dark) {
   .pagetree-item :global {
   .pagetree-item :global {
     .list-group-item-action {
     .list-group-item-action {

+ 2 - 0
apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.tsx

@@ -24,6 +24,7 @@ import {
 
 
 import { Ellipsis } from './Ellipsis';
 import { Ellipsis } from './Ellipsis';
 
 
+import styles from './PageTreeItem.module.scss';
 
 
 const logger = loggerFactory('growi:cli:Item');
 const logger = loggerFactory('growi:cli:Item');
 
 
@@ -177,6 +178,7 @@ export const PageTreeItem: FC<TreeItemProps> = (props) => {
       isOpen={isOpen}
       isOpen={isOpen}
       isEnableActions={props.isEnableActions}
       isEnableActions={props.isEnableActions}
       isReadOnlyUser={props.isReadOnlyUser}
       isReadOnlyUser={props.isReadOnlyUser}
+      isWipPageShown={props.isWipPageShown}
       onClick={itemSelectedHandler}
       onClick={itemSelectedHandler}
       onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
       onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
       onClickDeleteMenuItem={props.onClickDeleteMenuItem}
       onClickDeleteMenuItem={props.onClickDeleteMenuItem}

+ 10 - 6
apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx

@@ -16,20 +16,24 @@ export const RecentChanges = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const [isSmall, setIsSmall] = useState(false);
   const [isSmall, setIsSmall] = useState(false);
+  const [isWipPageShown, setIsWipPageShown] = useState(true);
 
 
   return (
   return (
-    // TODO : #139425 Match the space specification method to others
-    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="px-3" data-testid="grw-recent-changes">
     <div className="px-3" data-testid="grw-recent-changes">
-      <div className="grw-sidebar-content-header py-3 d-flex">
-        <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>
+      <div className="grw-sidebar-content-header py-4 d-flex">
+        <h4 className="mb-0 text-nowrap">{t('Recent Changes')}</h4>
         <Suspense>
         <Suspense>
-          <RecentChangesHeader isSmall={isSmall} onSizeChange={setIsSmall} />
+          <RecentChangesHeader
+            isSmall={isSmall}
+            onSizeChange={setIsSmall}
+            isWipPageShown={isWipPageShown}
+            onWipPageShownChange={() => { setIsWipPageShown(!isWipPageShown) }}
+          />
         </Suspense>
         </Suspense>
       </div>
       </div>
 
 
       <Suspense fallback={<RecentChangesContentSkeleton />}>
       <Suspense fallback={<RecentChangesContentSkeleton />}>
-        <RecentChangesContent isSmall={isSmall} />
+        <RecentChangesContent isWipPageShown={isWipPageShown} isSmall={isSmall} />
       </Suspense>
       </Suspense>
     </div>
     </div>
   );
   );

+ 0 - 1
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss

@@ -1,7 +1,6 @@
 @use '~/styles/mixins' as *;
 @use '~/styles/mixins' as *;
 
 
 .grw-recent-changes-resize-button :global {
 .grw-recent-changes-resize-button :global {
-  font-size: 12px;
   line-height: normal;
   line-height: normal;
   transform: translateY(-2px);
   transform: translateY(-2px);
 
 

+ 55 - 18
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -7,6 +7,10 @@ import {
 } from '@growi/core';
 } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'react-i18next';
+import {
+  DropdownItem, DropdownMenu, DropdownToggle, UncontrolledButtonDropdown,
+} from 'reactstrap';
 
 
 import { useKeywordManager } from '~/client/services/search-operation';
 import { useKeywordManager } from '~/client/services/search-operation';
 import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
 import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
@@ -116,8 +120,11 @@ const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Elemen
               { !dPagePath.isRoot && <FormerLink /> }
               { !dPagePath.isRoot && <FormerLink /> }
             </div>
             </div>
 
 
-            <h6 className={`col-12 ${isSmall ? 'mb-0 text-truncate' : 'mb-0'}`}>
+            <h6 className={`col-12 d-flex align-items-center ${isSmall ? 'mb-0 text-truncate' : 'mb-0'}`}>
               <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
               <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+              { page.wip && (
+                <span className="wip-page-badge badge rounded-pill text-bg-secondary ms-2">WIP</span>
+              ) }
               {locked}
               {locked}
             </h6>
             </h6>
 
 
@@ -143,12 +150,17 @@ PageItem.displayName = 'PageItem';
 type HeaderProps = {
 type HeaderProps = {
   isSmall: boolean,
   isSmall: boolean,
   onSizeChange: (isSmall: boolean) => void,
   onSizeChange: (isSmall: boolean) => void,
+  isWipPageShown: boolean,
+  onWipPageShownChange: () => void,
 }
 }
 
 
 const PER_PAGE = 20;
 const PER_PAGE = 20;
-export const RecentChangesHeader = ({ isSmall, onSizeChange }: HeaderProps): JSX.Element => {
+export const RecentChangesHeader = ({
+  isSmall, onSizeChange, isWipPageShown, onWipPageShownChange,
+}: HeaderProps): JSX.Element => {
+  const { t } = useTranslation();
 
 
-  const { mutate } = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
+  const { mutate } = useSWRINFxRecentlyUpdated(PER_PAGE, isWipPageShown, { suspense: true });
 
 
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
@@ -169,28 +181,53 @@ export const RecentChangesHeader = ({ isSmall, onSizeChange }: HeaderProps): JSX
   return (
   return (
     <>
     <>
       <SidebarHeaderReloadButton onClick={() => mutate()} />
       <SidebarHeaderReloadButton onClick={() => mutate()} />
-      <div className="d-flex align-items-center">
-        <div className={`grw-recent-changes-resize-button ${styles['grw-recent-changes-resize-button']} form-check form-switch ms-1`}>
-          <input
-            id="recentChangesResize"
-            className="form-check-input"
-            type="checkbox"
-            checked={isSmall}
-            onChange={changeSizeHandler}
-          />
-          <label className="form-label form-check-label" htmlFor="recentChangesResize" />
-        </div>
-      </div>
+
+      <UncontrolledButtonDropdown className="me-1">
+        <DropdownToggle color="transparent" className="p-0 border-0">
+          <span className="material-symbols-outlined">more_horiz</span>
+        </DropdownToggle>
+
+        <DropdownMenu container="body">
+          <DropdownItem onClick={changeSizeHandler}>
+            <div className={`${styles['grw-recent-changes-resize-button']} form-check form-switch`}>
+              <input
+                id="recentChangesResize"
+                className="form-check-input"
+                type="checkbox"
+                checked={isSmall}
+                onChange={() => {}}
+              />
+              <label className="form-label form-check-label text-muted" htmlFor="recentChangesResize" />
+            </div>
+          </DropdownItem>
+
+          <DropdownItem onClick={onWipPageShownChange}>
+            <div className="form-check form-switch">
+              <input
+                id="wipPageVisibility"
+                className="form-check-input"
+                type="checkbox"
+                checked={isWipPageShown}
+                onChange={() => {}}
+              />
+              <label className="form-label form-check-label text-muted" htmlFor="wipPageVisibility">
+                {t('sidebar_header.show_wip_page')}
+              </label>
+            </div>
+          </DropdownItem>
+        </DropdownMenu>
+      </UncontrolledButtonDropdown>
     </>
     </>
   );
   );
 };
 };
 
 
 type ContentProps = {
 type ContentProps = {
   isSmall: boolean,
   isSmall: boolean,
+  isWipPageShown: boolean,
 }
 }
 
 
-export const RecentChangesContent = ({ isSmall }: ContentProps): JSX.Element => {
-  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
+export const RecentChangesContent = ({ isSmall, isWipPageShown }: ContentProps): JSX.Element => {
+  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, isWipPageShown, { suspense: true });
   const { data } = swrInifinitexRecentlyUpdated;
   const { data } = swrInifinitexRecentlyUpdated;
 
 
   const { pushState } = useKeywordManager();
   const { pushState } = useKeywordManager();
@@ -199,7 +236,7 @@ export const RecentChangesContent = ({ isSmall }: ContentProps): JSX.Element =>
   const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
   const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
 
 
   return (
   return (
-    <div className="grw-recent-changes py-3">
+    <div className="grw-recent-changes">
       <ul className="list-group list-group-flush">
       <ul className="list-group list-group-flush">
         <InfiniteScroll
         <InfiniteScroll
           swrInifiniteResponse={swrInifinitexRecentlyUpdated}
           swrInifiniteResponse={swrInifinitexRecentlyUpdated}

+ 3 - 0
apps/app/src/components/Sidebar/SidebarContents.module.scss

@@ -11,4 +11,7 @@
     --bs-list-group-bg: transparent;
     --bs-list-group-bg: transparent;
   }
   }
 
 
+  .wip-page-badge {
+    --bs-badge-font-size: 0.5rem;
+  }
 }
 }

+ 1 - 1
apps/app/src/components/Sidebar/SidebarHeaderReloadButton.tsx

@@ -8,7 +8,7 @@ export const SidebarHeaderReloadButton = ({ onClick }: Props) => {
 
 
   return (
   return (
     <button type="button" className="btn btn-sm ms-auto py-0 grw-btn-reload" onClick={onClick}>
     <button type="button" className="btn btn-sm ms-auto py-0 grw-btn-reload" onClick={onClick}>
-      <i className="icon icon-reload"></i>
+      <span className="material-symbols-outlined">refresh</span>
     </button>
     </button>
   );
   );
 };
 };

+ 3 - 3
apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.tsx

@@ -54,11 +54,11 @@ export const PersonalDropdown = (): JSX.Element => {
           data-testid="personal-dropdown-menu"
           data-testid="personal-dropdown-menu"
         >
         >
           <DropdownItem header>
           <DropdownItem header>
-            <div className="mt-2 mb-3">
+            <div className="mt-2">
               <UserPicture user={currentUser} size="lg" noLink noTooltip />
               <UserPicture user={currentUser} size="lg" noLink noTooltip />
             </div>
             </div>
-            <h5 className="ms-1">{currentUser.name}</h5>
-            <div className="d-flex align-items-center">
+            <div className="mt-3 ms-1 fs-5">{currentUser.name}</div>
+            <div className="mt-2 d-flex align-items-center">
               <span className="material-symbols-outlined me-1">person</span>
               <span className="material-symbols-outlined me-1">person</span>
               {currentUser.username}
               {currentUser.username}
             </div>
             </div>

+ 7 - 8
apps/app/src/components/Sidebar/Tag.tsx

@@ -1,9 +1,10 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
-import { IDataTagCount } from '~/interfaces/tag';
+import type { IDataTagCount } from '~/interfaces/tag';
 import { useSWRxTagsList } from '~/stores/tag';
 import { useSWRxTagsList } from '~/stores/tag';
 
 
 import TagCloudBox from '../TagCloudBox';
 import TagCloudBox from '../TagCloudBox';
@@ -44,15 +45,13 @@ const Tag: FC = () => {
 
 
   // todo: adjust design by XD
   // todo: adjust design by XD
   return (
   return (
-    // TODO : #139425 Match the space specification method to others
-    // ref.  https://redmine.weseek.co.jp/issues/139425
-    <div className="container-lg px-4 mb-5 pb-5" data-testid="grw-sidebar-content-tags">
+    <div className="container-lg px-3 mb-5 pb-5" data-testid="grw-sidebar-content-tags">
       <div className="grw-sidebar-content-header py-3 d-flex">
       <div className="grw-sidebar-content-header py-3 d-flex">
-        <h3 className="mb-0">{t('Tags')}</h3>
+        <h4 className="mb-0">{t('Tags')}</h4>
         <SidebarHeaderReloadButton onClick={() => onReload()} />
         <SidebarHeaderReloadButton onClick={() => onReload()} />
       </div>
       </div>
 
 
-      <h3 className="my-3">{t('tag_list')}</h3>
+      <h6 className="my-3 pb-1 border-bottom">{t('tag_list')}</h6>
 
 
       { isLoading
       { isLoading
         ? (
         ? (
@@ -81,7 +80,7 @@ const Tag: FC = () => {
         </button>
         </button>
       </div>
       </div>
 
 
-      <h3 className="my-3">{t('popular_tags')}</h3>
+      <h6 className="my-3 pb-1 border-bottom">{t('popular_tags')}</h6>
 
 
       <TagCloudBox tags={tagCloudData} />
       <TagCloudBox tags={tagCloudData} />
     </div>
     </div>

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

@@ -53,7 +53,7 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
   }, [pushState]);
   }, [pushState]);
 
 
   if (!isTagExist) {
   if (!isTagExist) {
-    return <h3>{ t('You have no tag, You can set tags on pages') }</h3>;
+    return <h6>{ t('You have no tag, You can set tags on pages') }</h6>;
   }
   }
 
 
   return (
   return (

+ 1 - 1
apps/app/src/components/TreeItem/NewPageInput/NewPageCreateButton.tsx

@@ -27,7 +27,7 @@ export const NewPageCreateButton: FC<NewPageCreateButtonProps> = (props) => {
               className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
               className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
               onClick={onClick}
               onClick={onClick}
             >
             >
-              <i className="icon-plus d-block p-0" />
+              <span className="material-symbols-outlined d-block p-0">add_circle</span>
             </button>
             </button>
           </NotAvailableForReadOnlyUser>
           </NotAvailableForReadOnlyUser>
         </NotAvailableForGuest>
         </NotAvailableForGuest>

+ 5 - 2
apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -1,14 +1,16 @@
 import React, { useState, type FC, useCallback } from 'react';
 import React, { useState, type FC, useCallback } from 'react';
 
 
 import { createPage } from '~/client/services/page-operation';
 import { createPage } from '~/client/services/page-operation';
-import { useSWRxPageChildren } from '~/stores/page-listing';
+import { useSWRxPageChildren, mutatePageTree } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 
 
+import { shouldCreateWipPage } from '../../../utils/should-create-wip-page';
 import type { TreeItemToolProps } from '../interfaces';
 import type { TreeItemToolProps } from '../interfaces';
 
 
 import { NewPageCreateButton } from './NewPageCreateButton';
 import { NewPageCreateButton } from './NewPageCreateButton';
 import { NewPageInput } from './NewPageInput';
 import { NewPageInput } from './NewPageInput';
 
 
+
 type UseNewPageInput = {
 type UseNewPageInput = {
   Input: FC<TreeItemToolProps>,
   Input: FC<TreeItemToolProps>,
   CreateButton: FC<TreeItemToolProps>,
   CreateButton: FC<TreeItemToolProps>,
@@ -73,9 +75,10 @@ export const useNewPageInput = (): UseNewPageInput => {
         // keep grant info undefined to inherit from parent
         // keep grant info undefined to inherit from parent
         grant: undefined,
         grant: undefined,
         grantUserGroupIds: undefined,
         grantUserGroupIds: undefined,
+        wip: shouldCreateWipPage(newPagePath),
       });
       });
 
 
-      mutateChildren();
+      mutatePageTree();
 
 
       if (!hasDescendants) {
       if (!hasDescendants) {
         stateHandlers?.setIsOpen(true);
         stateHandlers?.setIsOpen(true);

+ 12 - 4
apps/app/src/components/TreeItem/SimpleItem.tsx

@@ -58,7 +58,12 @@ const SimpleItemContent = ({ page }: { page: IPageForItem }) => {
       )}
       )}
       {page != null && page.path != null && page._id != null && (
       {page != null && page.path != null && page._id != null && (
         <div className="grw-pagetree-title-anchor flex-grow-1">
         <div className="grw-pagetree-title-anchor flex-grow-1">
-          <p className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</p>
+          <div className="d-flex align-items-center">
+            <span className={`text-truncate ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</span>
+            { page.wip && (
+              <span className="wip-page-badge badge rounded-pill text-bg-secondary ms-2">WIP</span>
+            )}
+          </div>
         </div>
         </div>
       )}
       )}
     </div>
     </div>
@@ -90,7 +95,7 @@ type SimpleItemProps = TreeItemProps & {
 export const SimpleItem: FC<SimpleItemProps> = (props) => {
 export const SimpleItem: FC<SimpleItemProps> = (props) => {
   const {
   const {
     itemNode, targetPathOrId, isOpen: _isOpen = false,
     itemNode, targetPathOrId, isOpen: _isOpen = false,
-    onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
+    onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser, isWipPageShown = true,
     itemRef, itemClass, mainClassName,
     itemRef, itemClass, mainClassName,
   } = props;
   } = props;
 
 
@@ -165,6 +170,7 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
     isEnableActions,
     isEnableActions,
     isReadOnlyUser,
     isReadOnlyUser,
     isOpen: false,
     isOpen: false,
+    isWipPageShown,
     targetPathOrId,
     targetPathOrId,
     onRenamed,
     onRenamed,
     onClickDuplicateMenuItem,
     onClickDuplicateMenuItem,
@@ -178,6 +184,9 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
 
 
   const CustomNextComponents = props.customNextComponents;
   const CustomNextComponents = props.customNextComponents;
 
 
+  if (!isWipPageShown && page.wip) {
+    return <></>;
+  }
 
 
   return (
   return (
     <div
     <div
@@ -188,8 +197,7 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
       <li
       <li
         ref={itemRef}
         ref={itemRef}
         role="button"
         role="button"
-        className={`list-group-item list-group-item-action rounded border-0 py-0 pr-3 d-flex align-items-center
-        ${page.isTarget ? 'grw-pagetree-current-page-item' : ''}`}
+        className={`list-group-item border-0 py-0 pr-3 d-flex align-items-center text-muted ${page.isTarget ? 'active' : 'list-group-item-action'}`}
         id={page.isTarget ? 'grw-pagetree-current-page-item' : `grw-pagetree-list-${page._id}`}
         id={page.isTarget ? 'grw-pagetree-current-page-item' : `grw-pagetree-list-${page._id}`}
         onClick={itemClickHandler}
         onClick={itemClickHandler}
       >
       >

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