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

Merge branch 'master' into feat/gw7830-bookmarks-on-sidebar

jam411 3 лет назад
Родитель
Сommit
97275ef004
100 измененных файлов с 689 добавлено и 681 удалено
  1. 78 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 1 1
      packages/app/docker/README.md
  5. 11 11
      packages/app/package.json
  6. 3 3
      packages/app/public/static/locales/en_US/admin.json
  7. 1 1
      packages/app/public/static/locales/en_US/translation.json
  8. 3 3
      packages/app/public/static/locales/ja_JP/admin.json
  9. 1 1
      packages/app/public/static/locales/ja_JP/translation.json
  10. 3 3
      packages/app/public/static/locales/zh_CN/admin.json
  11. 1 1
      packages/app/public/static/locales/zh_CN/translation.json
  12. 6 6
      packages/app/resource/locales/en_US/admin/userInvitation.txt
  13. 11 0
      packages/app/resource/locales/en_US/admin/userResetPassword.txt
  14. 8 8
      packages/app/resource/locales/en_US/admin/userWaitingActivation.txt
  15. 3 3
      packages/app/resource/locales/en_US/notifications/comment.txt
  16. 4 4
      packages/app/resource/locales/en_US/notifications/notActiveUser.txt
  17. 2 2
      packages/app/resource/locales/en_US/notifications/pageCreate.txt
  18. 2 2
      packages/app/resource/locales/en_US/notifications/pageDelete.txt
  19. 2 2
      packages/app/resource/locales/en_US/notifications/pageEdit.txt
  20. 2 2
      packages/app/resource/locales/en_US/notifications/pageLike.txt
  21. 2 2
      packages/app/resource/locales/en_US/notifications/pageMove.txt
  22. 4 4
      packages/app/resource/locales/en_US/notifications/passwordReset.txt
  23. 1 1
      packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt
  24. 4 4
      packages/app/resource/locales/en_US/notifications/userActivation.txt
  25. 6 6
      packages/app/resource/locales/ja_JP/admin/userInvitation.txt
  26. 12 0
      packages/app/resource/locales/ja_JP/admin/userResetPassword.txt
  27. 8 8
      packages/app/resource/locales/ja_JP/admin/userWaitingActivation.txt
  28. 4 4
      packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt
  29. 4 4
      packages/app/resource/locales/ja_JP/notifications/passwordReset.txt
  30. 1 1
      packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt
  31. 4 4
      packages/app/resource/locales/ja_JP/notifications/userActivation.txt
  32. 6 6
      packages/app/resource/locales/zh_CN/admin/userInvitation.txt
  33. 11 0
      packages/app/resource/locales/zh_CN/admin/userResetPassword.txt
  34. 8 8
      packages/app/resource/locales/zh_CN/admin/userWaitingActivation.txt
  35. 3 3
      packages/app/resource/locales/zh_CN/notifications/comment.txt
  36. 4 4
      packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt
  37. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageCreate.txt
  38. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageDelete.txt
  39. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageEdit.txt
  40. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageLike.txt
  41. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageMove.txt
  42. 3 3
      packages/app/resource/locales/zh_CN/notifications/passwordReset.txt
  43. 1 1
      packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt
  44. 4 4
      packages/app/resource/locales/zh_CN/notifications/userActivation.txt
  45. 8 2
      packages/app/src/client/services/side-effects/page-updated.ts
  46. 1 6
      packages/app/src/client/services/user-ui-settings.ts
  47. 5 11
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  48. 1 6
      packages/app/src/components/LoginForm.tsx
  49. 44 39
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  50. 3 3
      packages/app/src/components/PageAlert/PageGrantAlert.tsx
  51. 11 6
      packages/app/src/components/PageComment/CommentEditor.tsx
  52. 19 9
      packages/app/src/components/PageEditor.tsx
  53. 6 2
      packages/app/src/components/PageEditor/ConflictDiffModal.tsx
  54. 21 8
      packages/app/src/components/PagePresentationModal.tsx
  55. 20 4
      packages/app/src/components/UnsavedAlertDialog.tsx
  56. 2 0
      packages/app/src/interfaces/crowi-request.ts
  57. 3 1
      packages/app/src/interfaces/user.ts
  58. 44 65
      packages/app/src/pages/[[...path]].page.tsx
  59. 0 3
      packages/app/src/pages/_app.page.tsx
  60. 2 29
      packages/app/src/pages/_private-legacy-pages.page.tsx
  61. 20 30
      packages/app/src/pages/_search.page.tsx
  62. 2 22
      packages/app/src/pages/me/[[...path]].page.tsx
  63. 18 31
      packages/app/src/pages/tags.page.tsx
  64. 17 30
      packages/app/src/pages/trash.page.tsx
  65. 15 0
      packages/app/src/pages/utils/commons.ts
  66. 0 5
      packages/app/src/server/crowi/express-init.js
  67. 0 37
      packages/app/src/server/middlewares/inject-user-ui-settings-to-localvars.ts
  68. 4 9
      packages/app/src/server/middlewares/login-required.js
  69. 0 86
      packages/app/src/server/models/config.ts
  70. 2 2
      packages/app/src/server/models/user-ui-settings.ts
  71. 1 1
      packages/app/src/server/routes/apiv3/index.js
  72. 15 2
      packages/app/src/server/routes/apiv3/user-ui-settings.ts
  73. 28 13
      packages/app/src/server/routes/apiv3/users.js
  74. 6 6
      packages/app/src/server/routes/index.js
  75. 17 7
      packages/app/src/server/routes/login-passport.js
  76. 17 0
      packages/app/src/server/util/createRedirectToForUnauthenticated.ts
  77. 2 1
      packages/app/src/server/util/mongoose-utils.ts
  78. 1 6
      packages/app/src/stores/editor.tsx
  79. 4 9
      packages/app/src/stores/ui.tsx
  80. 5 12
      packages/app/src/stores/use-context-swr.tsx
  81. 7 2
      packages/app/src/stores/use-next-themes.tsx
  82. 10 10
      packages/app/src/stores/use-static-swr.tsx
  83. 8 0
      packages/app/src/styles/theme/_apply-colors.scss
  84. 1 1
      packages/app/test/integration/middlewares/login-required.test.js
  85. 1 1
      packages/codemirror-textlint/package.json
  86. 1 1
      packages/core/package.json
  87. 1 1
      packages/hackmd/package.json
  88. 4 9
      packages/presentation/package.json
  89. 9 3
      packages/presentation/src/components/Slides.tsx
  90. 1 0
      packages/presentation/src/consts/index.ts
  91. 19 4
      packages/presentation/src/services/renderer/extract-sections.ts
  92. 1 1
      packages/preset-themes/package.json
  93. 5 0
      packages/preset-themes/src/styles/mono-blue.scss
  94. 1 1
      packages/remark-drawio/package.json
  95. 1 1
      packages/remark-growi-directive/package.json
  96. 4 4
      packages/remark-lsx/package.json
  97. 1 1
      packages/slack/package.json
  98. 2 2
      packages/slackbot-proxy/package.json
  99. 2 2
      packages/ui/package.json
  100. 2 1
      packages/ui/src/index.ts

+ 78 - 1
CHANGELOG.md

@@ -1,9 +1,86 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.5...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.7...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v6.0.7](https://github.com/weseek/growi/compare/v6.0.6...v6.0.7) - 2023-02-21
+
+### 💎 Features
+
+- feat: Manage guest ui setting with session (#7401) @yukendev
+- feat: Manage guest sidebar mode with session (#7393) @yukendev
+
+### 🚀 Improvement
+
+- imprv: UnsavedAlertDialog and page transition when next routing (#7400) @miya
+- imprv: PasswordResetModal styles and ejs format (#7404) @jam411
+- imprv: Initialize UserUISettings (#7397) @yuki-takei
+- imprv: Presentation behavior (#7399) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: PageStatusAlert is displayed on unnecessary pages (#7413) @miya
+- fix: PageStatusAlert does not disappear after loading latest revision (#7412) @miya
+- fix: Unable to transition requested page after login (#7402) @miya
+- fix: Page body is blank when opening editor after duplicating page (#7394) @miya
+- fix: Error when pressing the conflict resolution button on PageStatusAlert (#7395) @miya
+- fix: mono-blue subnavigation color (#7398) @ayaka0417
+- imprv: Add send email to user feat to `/reset-password` endpoint v6 (#7356) @jam411
+- fix: Behavior when color schema is forced by GROWI themes (#7391) @yuki-takei
+- fix: Sidebar mode on editor doesn't work in HackMD tab (#7396) @yuki-takei
+- fix: Can't controll slack notification button in comment editor (#7389) @yukendev
+
+## [v6.0.6](https://github.com/weseek/growi/compare/v6.0.5...v6.0.6) - 2023-02-14
+
+### 💎 Features
+
+- feat: Presentation (#7367) @yuki-takei
+- feat: Detect i18n locale from browser accept languages (#7341) @jam411
+- feat: Server Side Rendering (#7352) @yuki-takei
+
+### 🚀 Improvement
+
+- imprv: Allow iframe tag (#7368) @yuki-takei
+- imprv: User data serialization (#7355) @miya
+- imprv: Anchor link (#7354) @yuki-takei
+- imprv: classname for fluid layout (#7353) @yuki-takei
+- imprv: Disable lsx in shared page (#7333) @miya
+- imprv: Data mutation (#7336) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Make collapse work for anchor tags (#7381) @jam411
+- fix: Revision short body is not displayed on search results page (#7373) @miya
+- fix: Error when clicking on a page you are not authorized to view on the search results page (#7343) @miya
+- fix: Omit S3 credentials from the response for /_api/v3/app-settings (#7369) @miya
+- fix: Omit S3 credentials from the response for /_api/v3/app-settings (#7369) @miya
+- fix: Keep showing page restricted alert (#7371) @yukendev
+- fix: Recent changes and Timeline (#7366) @yuki-takei
+- fix: Login screen background (#7350) @ayaka0417
+- fix: Comment form background (#7365) @ayaka0417
+- fix: Scroll into view by anchor (#7360) @yuki-takei
+- fix: Routing after creating page with shortcut (#7359) @yuki-takei
+- fix: Border-color in edit mode (#7349) @ayaka0417
+- fix: Can't controll slack notification switch in editor (#7332) @yukendev
+- fix: Show load latest revision button when update drawio or table from view (#7324) @yukendev
+- fix: Can delete own user (#7321) @miya
+- fix: Request to "/_api/v3/page/is-grant-normalized" occurs when in guest mode (#7313) @miya
+
+### 🧰 Maintenance
+
+- support: create README.md for v6 migration (#7380) @yukendev
+- support: Bump SWR to v2.0.3 (#7362) @yuki-takei
+- feat: Refactor common processes in Next Page (#7357) @yukendev
+- support: Migrate Notation for v5 to v6 (#7326) @yukendev
+- ci(deps-dev): bump sass from 1.53.0 to 1.57.1 (#7223) @dependabot
+- ci(deps): bump amannn/action-semantic-pull-request from 4.2.0 to 5.0.2 (#7338) @dependabot
+- ci(deps): bump bakunyo/git-pr-release-action from 281e1fe424fac01f3992542266805e4202a22fe0 to master (#7340) @dependabot
+- ci(deps): bump docker/build-push-action from 2 to 4 (#7339) @dependabot
+- ci(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 (#7344) @dependabot
+- support: Bump SWR to v2 (#7318) @yuki-takei
+- support: Add test for View and Edit contents when saving (#7323) @yukendev
+
 ## [v6.0.5](https://github.com/weseek/growi/compare/v6.0.4...v6.0.5) - 2023-01-30
 ## [v6.0.5](https://github.com/weseek/growi/compare/v6.0.4...v6.0.5) - 2023-01-30
 
 
 ### 🚀 Improvement
 ### 🚀 Improvement

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
 {
   "npmClient": "yarn",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "useWorkspaces": true,
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "packages": [
   "packages": [
     "packages/*"
     "packages/*"
   ]
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",

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

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`6.0.5`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.5/packages/app/docker/Dockerfile)
+* [`6.0.7`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.7/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 11 - 11
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -67,14 +67,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.6-RC.0",
-    "@growi/core": "^6.0.6-RC.0",
-    "@growi/hackmd": "^6.0.6-RC.0",
-    "@growi/preset-themes": "^6.0.6-RC.0",
-    "@growi/remark-drawio": "^6.0.6-RC.0",
-    "@growi/remark-growi-directive": "^6.0.6-RC.0",
-    "@growi/remark-lsx": "^6.0.6-RC.0",
-    "@growi/slack": "^6.0.6-RC.0",
+    "@growi/codemirror-textlint": "^6.0.8-RC.0",
+    "@growi/core": "^6.0.8-RC.0",
+    "@growi/hackmd": "^6.0.8-RC.0",
+    "@growi/preset-themes": "^6.0.8-RC.0",
+    "@growi/remark-drawio": "^6.0.8-RC.0",
+    "@growi/remark-growi-directive": "^6.0.8-RC.0",
+    "@growi/remark-lsx": "^6.0.8-RC.0",
+    "@growi/slack": "^6.0.8-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
@@ -203,8 +203,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@growi/presentation": "^6.0.6-RC.0",
-    "@growi/ui": "^6.0.6-RC.0",
+    "@growi/presentation": "^6.0.8-RC.0",
+    "@growi/ui": "^6.0.8-RC.0",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
     "@next/bundle-analyzer": "^12.2.3",

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

@@ -741,9 +741,9 @@
     },
     },
     "reset_password": "Reset Password",
     "reset_password": "Reset Password",
     "reset_password_modal": {
     "reset_password_modal": {
-      "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
-      "password_reset_message": "Let the user know the new password below and strongly recommend to change another one immediately.",
-      "send_new_password": "Please send the new password to the user.",
+      "reset_password_info": "When a password is reset, a newly password is sent to the target user.",
+      "password_reset_message": "The temporary password was sent to the below user and strongly recommend to change another one immediately.",
+      "reset_password_alert": "If the e-mail transmission fails, please make sure that e-mail settings are correct and reset password again.",
       "target_user": "Target User",
       "target_user": "Target User",
       "new_password": "New Password"
       "new_password": "New Password"
     },
     },

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

@@ -328,7 +328,7 @@
     "notice": {
     "notice": {
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
     },
     },
-    "changes_not_saved": "Changes you made may not be saved."
+    "changes_not_saved": "Changes you made may not be saved. Are you sure you want to move?"
   },
   },
   "page_comment": {
   "page_comment": {
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",

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

@@ -749,9 +749,9 @@
     },
     },
     "reset_password": "パスワードのリセット",
     "reset_password": "パスワードのリセット",
     "reset_password_modal": {
     "reset_password_modal": {
-      "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
-      "password_reset_message": "対象ユーザーに下記のパスワードを伝え、すぐに新しく別のパスワードを設定するよう伝えてください。",
-      "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
+      "reset_password_info": "パスワードをリセットすると新規発行したパスワードを対象ユーザーに送信します。",
+      "password_reset_message": "対象ユーザーに一時的なパスワードを送信しました。新しく別のパスワードを設定するよう伝えてください。",
+      "reset_password_alert": "送信に失敗した場合はメール設定が正しいことを確認し再度パスワードのリセットを行ってください",
       "target_user": "対象ユーザー",
       "target_user": "対象ユーザー",
       "new_password": "新しいパスワード"
       "new_password": "新しいパスワード"
     },
     },

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

@@ -328,7 +328,7 @@
     "notice": {
     "notice": {
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
     },
     },
-    "changes_not_saved": "変更が保存されていない可能性があります。"
+    "changes_not_saved": "変更が保存されていない可能性があります。本当に移動しますか?"
   },
   },
   "page_comment": {
   "page_comment": {
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",

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

@@ -749,9 +749,9 @@
     },
     },
     "reset_password": "重置密码",
     "reset_password": "重置密码",
     "reset_password_modal": {
     "reset_password_modal": {
-      "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
-      "password_reset_message": "Let the user know the new password below and strongly recommend to change another one immediately.",
-      "send_new_password": "Please send the new password to the user.",
+      "reset_password_info": "When a password is reset, a newly password is sent to the target user.",
+      "password_reset_message": "The temporary password was sent to the below user and strongly recommend to change another one immediately.",
+      "reset_password_alert": "If the e-mail transmission fails, please make sure that e-mail settings are correct and reset password again.",
       "target_user": "Target User",
       "target_user": "Target User",
       "new_password": "New Password"
       "new_password": "New Password"
     },
     },

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

@@ -318,7 +318,7 @@
 		"notice": {
 		"notice": {
 			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
 			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
 		},
 		},
-    "changes_not_saved": "您所做的更改可能不会保存。"
+    "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
   },
   },
   "page_comment": {
   "page_comment": {
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",

+ 6 - 6
packages/app/resource/locales/en_US/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, <%- email -%>
+Hi, <%- email %>
 
 
 You are invited to our Wiki, you can log in with following account:
 You are invited to our Wiki, you can log in with following account:
 
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 (This password was auto generated. Update required at the first time you logging in)
 
 
 We are waiting for you!
 We are waiting for you!
-<%- url -%>
+<%- url %>
 
 
 --
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>

+ 11 - 0
packages/app/resource/locales/en_US/admin/userResetPassword.txt

@@ -0,0 +1,11 @@
+Hi, <%- email %>
+
+Your password has been reset by the administrator, you can log in with following account:
+
+Email: <%- email %>
+New Password: <%- password %>
+(This password was auto generated. Update required at the first time you logging in)
+
+--
+<%- appTitle %>
+<%- url %>

+ 8 - 8
packages/app/resource/locales/en_US/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, <%- adminUser.name -%>
+Hi, <%- adminUser.name %>
 
 
-A user registered to <%- appTitle -%>.
+A user registered to <%- appTitle %>.
 
 
 
 
 ====
 ====
 Created user:
 Created user:
 
 
-Name: <%- createdUser.name -%>
-User Name: <%- createdUser.username -%>
-Email: <%- createdUser.email -%>
+Name: <%- createdUser.name %>
+User Name: <%- createdUser.username %>
+Email: <%- createdUser.email %>
 ====
 ====
 
 
 Please do some action with following URL:
 Please do some action with following URL:
-<%- url -%>/admin/users
+<%- url %>/admin/users
 
 
 
 
 --
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>
 
 

+ 3 - 3
packages/app/resource/locales/en_US/notifications/comment.txt

@@ -1,9 +1,9 @@
-<%- username }} commented on {{ path -%>.
+<%- username %> commented on <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-<%- comment -%>
+<%- comment %>
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 4 - 4
packages/app/resource/locales/en_US/notifications/notActiveUser.txt

@@ -1,13 +1,13 @@
 Password Reset
 Password Reset
 
 
-Hi, <%- email -%>
+Hi, <%- email %>
 
 
-A request has been received to change the password from <%- appTitle -%>.
+A request has been received to change the password from <%- appTitle %>.
 However, this email is not registerd. Please try again with different email.
 However, this email is not registerd. Please try again with different email.
 
 
 If you did not request a password reset, you can safely ignore this email.
 If you did not request a password reset, you can safely ignore this email.
 
 
 -------------------------------------------------------------------------
 -------------------------------------------------------------------------
 
 
-GROWI: <%- appTitle -%>
-URL: <%- url -%>
+GROWI: <%- appTitle %>
+URL: <%- url %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageCreate.txt

@@ -1,5 +1,5 @@
-<%- username -%> created a new page under <%- path -%>.
+<%- username %> created a new page under <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageDelete.txt

@@ -1,5 +1,5 @@
-<%- username -%> deleted the page  <%- path -%>.
+<%- username %> deleted the page  <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageEdit.txt

@@ -1,5 +1,5 @@
-<%- username -%> edited the page <%- path -%>.
+<%- username %> edited the page <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageLike.txt

@@ -1,5 +1,5 @@
-<%- username -%> liked the page <%- path -%>.
+<%- username %> liked the page <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageMove.txt

@@ -1,5 +1,5 @@
-<%- username -%> renamed the page <%- oldPath -%> to <%- newPath -%>.
+<%- username %> renamed the page <%- oldPath %> to <%- newPath %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 4 - 4
packages/app/resource/locales/en_US/notifications/passwordReset.txt

@@ -1,12 +1,12 @@
 Password Reset
 Password Reset
 
 
-Hi, <%- email -%>
+Hi, <%- email %>
 
 
-A request has been received to change the password your GROWI (<%- appTitle -%>) account.
+A request has been received to change the password your GROWI (<%- appTitle %>) account.
 To reset your password, click on the link below.
 To reset your password, click on the link below.
 
 
-<%- url -%>
+<%- url %>
 
 
-This link will expire in 10 minutes at  <%- expiredAt -%>.
+This link will expire in 10 minutes at  <%- expiredAt %>.
 
 
 If you did not request a password reset, you can safely ignore this email.
 If you did not request a password reset, you can safely ignore this email.

+ 1 - 1
packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 Password Reset Successful
 Password Reset Successful
 
 
-Hi <%- email -%>
+Hi <%- email %>
 
 
 Your password has been successfully reset.
 Your password has been successfully reset.
 Please log in with your new password.
 Please log in with your new password.

+ 4 - 4
packages/app/resource/locales/en_US/notifications/userActivation.txt

@@ -1,12 +1,12 @@
 Account confirmation
 Account confirmation
 
 
-Hi, <%- email -%>
+Hi, <%- email %>
 
 
-An acount has been created in GROWI (<%- appTitle -%>).
+An acount has been created in GROWI (<%- appTitle %>).
 To activate your account, click on the link below.
 To activate your account, click on the link below.
 
 
-<%- url -%>
+<%- url %>
 
 
-This link will expire in 1 hour at  <%- expiredAt -%>.
+This link will expire in 1 hour at  <%- expiredAt %>.
 
 
 If you did not created the account, you can safely ignore this email.
 If you did not created the account, you can safely ignore this email.

+ 6 - 6
packages/app/resource/locales/ja_JP/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, <%- email -%>
+Hi, <%- email %>
 
 
 You are invited to our Wiki, you can log in with following account:
 You are invited to our Wiki, you can log in with following account:
 
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 (This password was auto generated. Update required at the first time you logging in)
 
 
 We are waiting for you!
 We are waiting for you!
-<%- url -%>
+<%- url %>
 
 
 --
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>

+ 12 - 0
packages/app/resource/locales/ja_JP/admin/userResetPassword.txt

@@ -0,0 +1,12 @@
+Hi, <%- email %>
+
+Your password has been reset by the administrator, you can log in with following account:
+
+Email: <%- email %>
+New Password: <%- password %>
+(This password was auto generated. Update required at the first time you logging in)
+
+--
+<%- appTitle %>
+<%- url %>
+

+ 8 - 8
packages/app/resource/locales/ja_JP/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, <%- adminUser.name -%>
+Hi, <%- adminUser.name %>
 
 
-A user registered to <%- appTitle -%>.
+A user registered to <%- appTitle %>.
 
 
 
 
 ====
 ====
 Created user:
 Created user:
 
 
-Name: <%- createdUser.name -%>
-User Name: <%- createdUser.username -%>
-Email: <%- createdUser.email -%>
+Name: <%- createdUser.name %>
+User Name: <%- createdUser.username %>
+Email: <%- createdUser.email %>
 ====
 ====
 
 
 Please do some action with following URL:
 Please do some action with following URL:
-<%- url -%>/admin/users
+<%- url %>/admin/users
 
 
 
 
 --
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>
 
 

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt

@@ -1,13 +1,13 @@
 パスワードリセット
 パスワードリセット
 
 
-こんにちは、 <%- email -%>
+こんにちは、 <%- email %>
 
 
-<%- appTitle -%> からパスワード再設定のリクエストがありましたが、このemailは登録されておりません。
+<%- appTitle %> からパスワード再設定のリクエストがありましたが、このemailは登録されておりません。
 他のemailアドレスで再度お試しください。
 他のemailアドレスで再度お試しください。
 
 
 もしこのリクエストに心当たりがない場合は、このメールを無視してください。
 もしこのリクエストに心当たりがない場合は、このメールを無視してください。
 
 
 -------------------------------------------------------------------------
 -------------------------------------------------------------------------
 
 
-GROWI: <%- appTitle -%>
-URL: <%- url -%>
+GROWI: <%- appTitle %>
+URL: <%- url %>

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/passwordReset.txt

@@ -1,12 +1,12 @@
 パスワード リセット
 パスワード リセット
 
 
-こんにちは, <%- email -%>
+こんにちは, <%- email %>
 
 
-あなたのGROWI (<%- appTitle -%>) アカウントから、パスワード再設定のリクエストがありました。
+あなたのGROWI (<%- appTitle %>) アカウントから、パスワード再設定のリクエストがありました。
 パスワードをリセットするには、以下のリンクをクリックしてください。
 パスワードをリセットするには、以下のリンクをクリックしてください。
 
 
-<%- url -%>
+<%- url %>
 
 
-このリンクは10分後の <%- expiredAt -%> に失効します。
+このリンクは10分後の <%- expiredAt %> に失効します。
 
 
 もしこのリクエストに心当たりがない場合は、このメールを無視してください。
 もしこのリクエストに心当たりがない場合は、このメールを無視してください。

+ 1 - 1
packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 パスワードリセットに成功
 パスワードリセットに成功
 
 
-こんにちは、 <%- email -%>
+こんにちは、 <%- email %>
 
 
 あなたのパスワードは正常にリセットされました。
 あなたのパスワードは正常にリセットされました。
 新しいパスワードでログインしてください。
 新しいパスワードでログインしてください。

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/userActivation.txt

@@ -1,13 +1,13 @@
 仮登録完了のお知らせ
 仮登録完了のお知らせ
 
 
-<%- email -%> さん
+<%- email %> さん
 
 
-GROWI (<%- appTitle -%>) で仮登録が完了いたしました。
+GROWI (<%- appTitle %>) で仮登録が完了いたしました。
 
 
 ご本人様確認のため、下記リンクをクリックし、アカウントの本登録を完了させて下さい。
 ご本人様確認のため、下記リンクをクリックし、アカウントの本登録を完了させて下さい。
 
 
-<%- url -%>
+<%- url %>
 
 
-このリンクは1時間後の <%- expiredAt -%> に失効します。
+このリンクは1時間後の <%- expiredAt %> に失効します。
 
 
 ※当メールの内容に心当たりがない場合は、このメールを無視してください。
 ※当メールの内容に心当たりがない場合は、このメールを無視してください。

+ 6 - 6
packages/app/resource/locales/zh_CN/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, <%- email -%>
+Hi, <%- email %>
 
 
 You are invited to our Wiki, you can log in with following account:
 You are invited to our Wiki, you can log in with following account:
 
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 (This password was auto generated. Update required at the first time you logging in)
 
 
 We are waiting for you!
 We are waiting for you!
-<%- url -%>
+<%- url %>
 
 
 --
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>

+ 11 - 0
packages/app/resource/locales/zh_CN/admin/userResetPassword.txt

@@ -0,0 +1,11 @@
+Hi, <%- email %>
+
+Your password has been reset by the administrator, you can log in with following account:
+
+Email: <%- email %>
+New Password: <%- password %>
+(This password was auto generated. Update required at the first time you logging in)
+
+--
+<%- appTitle %>
+<%- url %>

+ 8 - 8
packages/app/resource/locales/zh_CN/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, <%- adminUser.name -%>
+Hi, <%- adminUser.name %>
 
 
-A user registered to <%- appTitle -%>.
+A user registered to <%- appTitle %>.
 
 
 
 
 ====
 ====
 Created user:
 Created user:
 
 
-Name: <%- createdUser.name -%>
-User Name: <%- createdUser.username -%>
-Email: <%- createdUser.email -%>
+Name: <%- createdUser.name %>
+User Name: <%- createdUser.username %>
+Email: <%- createdUser.email %>
 ====
 ====
 
 
 Please do some action with following URL:
 Please do some action with following URL:
-<%- url -%>/admin/users
+<%- url %>/admin/users
 
 
 
 
 --
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>
 
 

+ 3 - 3
packages/app/resource/locales/zh_CN/notifications/comment.txt

@@ -1,9 +1,9 @@
-<%- username -%> commented on <%- path -%>.
+<%- username %> commented on <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-<%- comment -%>
+<%- comment %>
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 4 - 4
packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt

@@ -1,13 +1,13 @@
 重设密码
 重设密码
 
 
-嗨,<%-电子邮件-%>
+嗨,<%-电子邮件%>
 
 
-已收到来自 <%-appTitle-%> 的更改密码请求。
+已收到来自 <%-appTitle%> 的更改密码请求。
 但是,此电子邮件未注册。请使用其他电子邮件重试。
 但是,此电子邮件未注册。请使用其他电子邮件重试。
 
 
 如果您没有要求重置密码,则可以放心地忽略此电子邮件。
 如果您没有要求重置密码,则可以放心地忽略此电子邮件。
 
 
 -------------------------------------------------------------------------
 -------------------------------------------------------------------------
 
 
-GROWI: <%- appTitle -%>
-URL: <%- url -%>
+GROWI: <%- appTitle %>
+URL: <%- url %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageCreate.txt

@@ -1,5 +1,5 @@
-<%- username -%> created a new page under <%- path -%>.
+<%- username %> created a new page under <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageDelete.txt

@@ -1,5 +1,5 @@
-<%- username -%> deleted the page  <%- path -%>.
+<%- username %> deleted the page  <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageEdit.txt

@@ -1,5 +1,5 @@
-<%- username -%> edited the page <%- path -%>.
+<%- username %> edited the page <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageLike.txt

@@ -1,5 +1,5 @@
-<%- username -%> liked the page <%- path -%>.
+<%- username %> liked the page <%- path %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageMove.txt

@@ -1,5 +1,5 @@
-<%- username -%> renamed the page <%- oldPath -%> to <%- newPath -%>.
+<%- username %> renamed the page <%- oldPath %> to <%- newPath %>.
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 3 - 3
packages/app/resource/locales/zh_CN/notifications/passwordReset.txt

@@ -1,11 +1,11 @@
 重设密码
 重设密码
 
 
-嗨,<%- email -%>
+嗨,<%- email %>
 
 
-已收到更改您 GROWI (<%-appTitle-%>) 帐户 密码的请求。
+已收到更改您 GROWI (<%-appTitle%>) 帐户 密码的请求。
 要重置密码,请单击下面的链接。
 要重置密码,请单击下面的链接。
 
 
-<%- url -%>
+<%- url %>
 
 
 这个链接在10分钟后的{ expiredAt }}失效。
 这个链接在10分钟后的{ expiredAt }}失效。
 
 

+ 1 - 1
packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 密码重置成功
 密码重置成功
 
 
-嗨, <%-email-%>
+嗨, <%-email%>
 
 
 您的密码已成功重置。
 您的密码已成功重置。
 请使用您的新密码登录。
 请使用您的新密码登录。

+ 4 - 4
packages/app/resource/locales/zh_CN/notifications/userActivation.txt

@@ -1,12 +1,12 @@
 确认账户创建
 确认账户创建
 
 
-致<%- email -%>,
+致<%- email %>,
 
 
-已使用 GROWI (<%- appTitle -%>) 创建帐户。
+已使用 GROWI (<%- appTitle %>) 创建帐户。
 单击下面的链接以激活您的帐户。
 单击下面的链接以激活您的帐户。
 
 
-<%- url -%>
+<%- url %>
 
 
-这个链接将在1小时后即<%- expiredAt -%>失效。
+这个链接将在1小时后即<%- expiredAt %>失效。
 
 
 如果您尚未创建,请忽略此电子邮件。
 如果您尚未创建,请忽略此电子邮件。

+ 8 - 2
packages/app/src/client/services/side-effects/page-updated.ts

@@ -1,6 +1,7 @@
 import { useCallback, useEffect } from 'react';
 import { useCallback, useEffect } from 'react';
 
 
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
+import { useCurrentPageId } from '~/stores/context';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useGlobalSocket } from '~/stores/websocket';
 import { useGlobalSocket } from '~/stores/websocket';
 
 
@@ -9,6 +10,7 @@ export const usePageUpdatedEffect = (): void => {
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
 
 
   const { data: socket } = useGlobalSocket();
   const { data: socket } = useGlobalSocket();
+  const { data: currentPageId } = useCurrentPageId();
 
 
   const setLatestRemotePageData = useCallback((data) => {
   const setLatestRemotePageData = useCallback((data) => {
     const { s2cMessagePageUpdated } = data;
     const { s2cMessagePageUpdated } = data;
@@ -21,8 +23,12 @@ export const usePageUpdatedEffect = (): void => {
       revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
       revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
       hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
       hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
     };
     };
-    setRemoteLatestPageData(remoteData);
-  }, [setRemoteLatestPageData]);
+
+    if (currentPageId != null && currentPageId === s2cMessagePageUpdated.pageId) {
+      setRemoteLatestPageData(remoteData);
+    }
+
+  }, [currentPageId, setRemoteLatestPageData]);
 
 
   // listen socket for someone updating this page
   // listen socket for someone updating this page
   useEffect(() => {
   useEffect(() => {

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

@@ -1,11 +1,9 @@
 // eslint-disable-next-line no-restricted-imports
 // eslint-disable-next-line no-restricted-imports
 import { AxiosResponse } from 'axios';
 import { AxiosResponse } from 'axios';
-
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import { useIsGuestUser } from '~/stores/context';
 
 
 let settingsForBulk: Partial<IUserUISettings> = {};
 let settingsForBulk: Partial<IUserUISettings> = {};
 const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> => {
 const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> => {
@@ -33,11 +31,8 @@ type UserUISettingsUtil = {
   scheduleToPut: ScheduleToPutFunction | (() => void),
   scheduleToPut: ScheduleToPutFunction | (() => void),
 }
 }
 export const useUserUISettings = (): UserUISettingsUtil => {
 export const useUserUISettings = (): UserUISettingsUtil => {
-  const { data: isGuestUser } = useIsGuestUser();
 
 
   return {
   return {
-    scheduleToPut: isGuestUser
-      ? () => {}
-      : scheduleToPut,
+    scheduleToPut,
   };
   };
 };
 };

+ 5 - 11
packages/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -9,7 +9,6 @@ import {
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 class PasswordResetModal extends React.Component {
 class PasswordResetModal extends React.Component {
 
 
@@ -17,7 +16,6 @@ class PasswordResetModal extends React.Component {
     super(props);
     super(props);
 
 
     this.state = {
     this.state = {
-      temporaryPassword: [],
       isPasswordResetDone: false,
       isPasswordResetDone: false,
     };
     };
 
 
@@ -27,9 +25,8 @@ class PasswordResetModal extends React.Component {
   async resetPassword() {
   async resetPassword() {
     const { t, userForPasswordResetModal } = this.props;
     const { t, userForPasswordResetModal } = this.props;
     try {
     try {
-      const res = await apiv3Put('/users/reset-password', { id: userForPasswordResetModal._id });
-      const { newPassword } = res.data;
-      this.setState({ temporaryPassword: newPassword, isPasswordResetDone: true });
+      await apiv3Put('/users/reset-password', { id: userForPasswordResetModal._id });
+      this.setState({ isPasswordResetDone: true });
     }
     }
     catch (err) {
     catch (err) {
       toastError(err, t('toaster.failed_to_reset_password'));
       toastError(err, t('toaster.failed_to_reset_password'));
@@ -42,8 +39,8 @@ class PasswordResetModal extends React.Component {
     return (
     return (
       <>
       <>
         <p>
         <p>
-          {t('user_management.reset_password_modal.password_never_seen')}<br />
-          <span className="text-danger">{t('user_management.reset_password_modal.send_new_password')}</span>
+          {t('user_management.reset_password_modal.reset_password_info')}<br />
+          <span className="text-danger">{t('user_management.reset_password_modal.reset_password_alert')}</span>
         </p>
         </p>
         <p>
         <p>
           {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
           {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
@@ -57,13 +54,10 @@ class PasswordResetModal extends React.Component {
 
 
     return (
     return (
       <>
       <>
-        <p className="alert alert-danger">{t('user_management.reset_password_modal.password_reset_message')}</p>
+        <p className="text-danger">{t('user_management.reset_password_modal.password_reset_message')}</p>
         <p>
         <p>
           {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
           {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
         </p>
         </p>
-        <p>
-          {t('user_management.reset_password_modal.new_password')}: <code>{this.state.temporaryPassword}</code>
-        </p>
       </>
       </>
     );
     );
   }
   }

+ 1 - 6
packages/app/src/components/LoginForm.tsx

@@ -2,7 +2,6 @@ import React, {
   useState, useEffect, useCallback,
   useState, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
-import { USER_STATUS } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 import ReactCardFlip from 'react-card-flip';
@@ -95,16 +94,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
 
     try {
     try {
       const res = await apiv3Post('/login', { loginForm });
       const res = await apiv3Post('/login', { loginForm });
-      const { redirectTo, userStatus } = res.data;
+      const { redirectTo } = res.data;
 
 
       if (redirectTo != null) {
       if (redirectTo != null) {
         return router.push(redirectTo);
         return router.push(redirectTo);
       }
       }
 
 
-      if (userStatus !== USER_STATUS.ACTIVE) {
-        window.location.href = '/';
-      }
-
       return router.push('/');
       return router.push('/');
     }
     }
     catch (err) {
     catch (err) {

+ 44 - 39
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -25,7 +25,7 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
   const { isAuthenticated } = props;
   const { isAuthenticated } = props;
 
 
   const {
   const {
-    setTheme, resolvedTheme, useOsSettings, isDarkMode,
+    setTheme, resolvedTheme, useOsSettings, isDarkMode, isForcedByGrowiTheme,
   } = useNextThemes();
   } = useNextThemes();
   const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
   const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
@@ -114,55 +114,60 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
 
 
         {/* sidebar mode */}
         {/* sidebar mode */}
         {renderSidebarModeSwitch(false)}
         {renderSidebarModeSwitch(false)}
-        {dropdownDivider}
 
 
         {/* side bar mode on editor */}
         {/* side bar mode on editor */}
         {isAuthenticated && (
         {isAuthenticated && (
           <>
           <>
-            {renderSidebarModeSwitch(true)}
             {dropdownDivider}
             {dropdownDivider}
+            {renderSidebarModeSwitch(true)}
           </>
           </>
         )}
         )}
 
 
         {/* color mode */}
         {/* color mode */}
-        <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
-        <form className="px-4">
-          <div className="form-row justify-content-center">
-            <div className="form-group col-auto d-flex align-items-center">
-              <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                <SunIcon />
-              </IconWithTooltip>
-              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
-                <input
-                  id="swUserPreference"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={isDarkMode}
-                  disabled={useOsSettings}
-                  onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
-                />
-                <label className="custom-control-label" htmlFor="swUserPreference"></label>
+        { !isForcedByGrowiTheme && (
+          <>
+            {dropdownDivider}
+            <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
+            <form className="px-4">
+              <div className="form-row justify-content-center">
+                <div className="form-group col-auto d-flex align-items-center">
+                  <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
+                    <SunIcon />
+                  </IconWithTooltip>
+                  <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                    <input
+                      id="swUserPreference"
+                      className="custom-control-input"
+                      type="checkbox"
+                      checked={isDarkMode}
+                      disabled={useOsSettings}
+                      onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
+                    />
+                    <label className="custom-control-label" htmlFor="swUserPreference"></label>
+                  </div>
+                  <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
+                    <MoonIcon />
+                  </IconWithTooltip>
+                </div>
               </div>
               </div>
-              <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                <MoonIcon />
-              </IconWithTooltip>
-            </div>
-          </div>
-          <div className="form-row">
-            <div className="form-group col-auto">
-              <div className="custom-control custom-checkbox">
-                <input
-                  id="cbFollowOs"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={useOsSettings}
-                  onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
-                />
-                <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
+              <div className="form-row">
+                <div className="form-group col-auto">
+                  <div className="custom-control custom-checkbox">
+                    <input
+                      id="cbFollowOs"
+                      className="custom-control-input"
+                      type="checkbox"
+                      checked={useOsSettings}
+                      onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
+                    />
+                    <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
+                  </div>
+                </div>
               </div>
               </div>
-            </div>
-          </div>
-        </form>
+            </form>
+          </>
+        ) }
+
       </div>
       </div>
 
 
     </>
     </>

+ 3 - 3
packages/app/src/components/PageAlert/PageGrantAlert.tsx

@@ -18,21 +18,21 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 2) {
       if (pageData.grant === 2) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-link"></i><strong>{t('Anyone with the link')} only</strong>
+            <i className="icon-fw icon-link"></i><strong>{t('Anyone with the link')}</strong>
           </>
           </>
         );
         );
       }
       }
       if (pageData.grant === 4) {
       if (pageData.grant === 4) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-lock"></i><strong>{t('Only me')} only</strong>
+            <i className="icon-fw icon-lock"></i><strong>{t('Only me')}</strong>
           </>
           </>
         );
         );
       }
       }
       if (pageData.grant === 5) {
       if (pageData.grant === 5) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-organization"></i><strong>{pageData.grantedGroup.name} only</strong>
+            <i className="icon-fw icon-organization"></i><strong>{pageData.grantedGroup.name}</strong>
           </>
           </>
         );
         );
       }
       }

+ 11 - 6
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -85,12 +85,16 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     setActiveTab(activeTab);
     setActiveTab(activeTab);
   }, []);
   }, []);
 
 
+  // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
+  const slackChannelsDataString = slackChannelsData?.toString();
+  const initializeSlackEnabled = useCallback(() => {
+    setSlackChannels(slackChannelsDataString ?? '');
+    mutateIsSlackEnabled(false);
+  }, [mutateIsSlackEnabled, slackChannelsDataString]);
+
   useEffect(() => {
   useEffect(() => {
-    if (slackChannelsData != null) {
-      setSlackChannels(slackChannelsData.toString());
-      mutateIsSlackEnabled(false);
-    }
-  }, [mutateIsSlackEnabled, slackChannelsData]);
+    initializeSlackEnabled();
+  }, [initializeSlackEnabled]);
 
 
   const isSlackEnabledToggleHandler = (isSlackEnabled: boolean) => {
   const isSlackEnabledToggleHandler = (isSlackEnabled: boolean) => {
     mutateIsSlackEnabled(isSlackEnabled, false);
     mutateIsSlackEnabled(isSlackEnabled, false);
@@ -104,10 +108,11 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     setComment('');
     setComment('');
     setActiveTab('comment_editor');
     setActiveTab('comment_editor');
     setError(undefined);
     setError(undefined);
+    initializeSlackEnabled();
     // reset value
     // reset value
     if (editorRef.current == null) { return }
     if (editorRef.current == null) { return }
     editorRef.current.setValue('');
     editorRef.current.setValue('');
-  }, []);
+  }, [initializeSlackEnabled]);
 
 
   const cancelButtonClickedHandler = useCallback(() => {
   const cancelButtonClickedHandler = useCallback(() => {
     // change state to not ready
     // change state to not ready

+ 19 - 9
packages/app/src/components/PageEditor.tsx

@@ -16,7 +16,7 @@ import { throttle, debounce } from 'throttle-debounce';
 
 
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
-import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
@@ -35,6 +35,12 @@ import {
   useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
   useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
 } from '~/stores/page';
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import { mutatePageTree } from '~/stores/page-listing';
+import {
+  useRemoteRevisionId,
+  useRemoteRevisionBody,
+  useRemoteRevisionLastUpdatedAt,
+  useRemoteRevisionLastUpdateUser,
+} from '~/stores/remote-latest-page';
 import { usePreviewOptions } from '~/stores/renderer';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
 import {
   EditorMode,
   EditorMode,
@@ -92,6 +98,10 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
+  const { mutate: mutateRemotePageId } = useRemoteRevisionId();
+  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionBody();
+  const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
+  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -218,18 +228,18 @@ const PageEditor = React.memo((): JSX.Element => {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
       toastError(error);
       toastError(error);
       if (error.code === 'conflict') {
       if (error.code === 'conflict') {
-        toastWarning('(TBD) resolve conflict');
-        // pageContainer.setState({
-        //   remoteRevisionId: error.data.revisionId,
-        //   remoteRevisionBody: error.data.revisionBody,
-        //   remoteRevisionUpdateAt: error.data.createdAt,
-        //   lastUpdateUser: error.data.user,
-        // });
+        mutateRemotePageId(error.data.revisionId);
+        mutateRemoteRevisionId(error.data.revisionBody);
+        mutateRemoteRevisionLastUpdatedAt(error.data.createdAt);
+        mutateRemoteRevisionLastUpdateUser(error.data.user);
       }
       }
       return null;
       return null;
     }
     }
 
 
-  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId]);
+  }, [
+    currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId,
+    currentPagePath, currentRevisionId, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser,
+  ]);
 
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     if (editorMode !== EditorMode.Editor) {
     if (editorMode !== EditorMode.Editor) {

+ 6 - 2
packages/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -4,7 +4,7 @@ import React, {
 
 
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
 import CodeMirror from 'codemirror/lib/codemirror';
 import CodeMirror from 'codemirror/lib/codemirror';
-import { format } from 'date-fns';
+import { format, parseISO } from 'date-fns';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -305,6 +305,10 @@ export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element =>
     return <></>;
     return <></>;
   }
   }
 
 
+  const currentPageCreatedAtFixed = typeof currentPage.updatedAt === 'string'
+    ? parseISO(currentPage.updatedAt)
+    : currentPage.updatedAt;
+
   const request: IRevisionOnConflictWithStringDate = {
   const request: IRevisionOnConflictWithStringDate = {
     revisionId: '',
     revisionId: '',
     revisionBody: props.markdownOnEdit,
     revisionBody: props.markdownOnEdit,
@@ -314,7 +318,7 @@ export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element =>
   const origin: IRevisionOnConflictWithStringDate = {
   const origin: IRevisionOnConflictWithStringDate = {
     revisionId: currentPage?.revision._id,
     revisionId: currentPage?.revision._id,
     revisionBody: currentPage?.revision.body,
     revisionBody: currentPage?.revision.body,
-    createdAt: format(currentPage.updatedAt, 'yyyy/MM/dd HH:mm:ss'),
+    createdAt: format(currentPageCreatedAtFixed, 'yyyy/MM/dd HH:mm:ss'),
     user: currentPage?.lastUpdateUser,
     user: currentPage?.lastUpdateUser,
   };
   };
   const latest: IRevisionOnConflictWithStringDate = {
   const latest: IRevisionOnConflictWithStringDate = {

+ 21 - 8
packages/app/src/components/PagePresentationModal.tsx

@@ -1,13 +1,13 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
 import type { PresentationProps } from '@growi/presentation';
 import type { PresentationProps } from '@growi/presentation';
+import { useFullScreen } from '@growi/ui';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import {
 import {
   Modal, ModalBody,
   Modal, ModalBody,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-
 import { usePagePresentationModal } from '~/stores/modal';
 import { usePagePresentationModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { usePresentationViewOptions } from '~/stores/renderer';
 import { usePresentationViewOptions } from '~/stores/renderer';
@@ -30,13 +30,26 @@ const PagePresentationModal = (): JSX.Element => {
   const { data: presentationModalData, close: closePresentationModal } = usePagePresentationModal();
   const { data: presentationModalData, close: closePresentationModal } = usePagePresentationModal();
 
 
   const { isDarkMode } = useNextThemes();
   const { isDarkMode } = useNextThemes();
+  const fullscreen = useFullScreen();
 
 
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: rendererOptions } = usePresentationViewOptions();
   const { data: rendererOptions } = usePresentationViewOptions();
 
 
-  const requestFullscreen = useCallback(() => {
-    document.documentElement.requestFullscreen();
-  }, []);
+  const toggleFullscreenHandler = useCallback(() => {
+    if (fullscreen.active) {
+      fullscreen.exit();
+    }
+    else {
+      fullscreen.enter();
+    }
+  }, [fullscreen]);
+
+  const closeHandler = useCallback(() => {
+    if (fullscreen.active) {
+      fullscreen.exit();
+    }
+    closePresentationModal();
+  }, [fullscreen, closePresentationModal]);
 
 
   const isOpen = presentationModalData?.isOpened ?? false;
   const isOpen = presentationModalData?.isOpened ?? false;
 
 
@@ -49,15 +62,15 @@ const PagePresentationModal = (): JSX.Element => {
   return (
   return (
     <Modal
     <Modal
       isOpen={isOpen}
       isOpen={isOpen}
-      toggle={closePresentationModal}
+      toggle={closeHandler}
       data-testid="page-presentation-modal"
       data-testid="page-presentation-modal"
       className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
       className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
     >
     >
       <div className="grw-presentation-controls d-flex">
       <div className="grw-presentation-controls d-flex">
-        <button className="close btn-fullscreen" type="button" aria-label="fullscreen" onClick={requestFullscreen}>
-          <i className="ti ti-fullscreen" aria-hidden></i>
+        <button className="close btn-fullscreen" type="button" aria-label="fullscreen" onClick={toggleFullscreenHandler}>
+          <i className={`${fullscreen.active ? 'icon-size-actual' : 'icon-size-fullscreen'}`} aria-hidden></i>
         </button>
         </button>
-        <button className="close btn-close" type="button" aria-label="close" onClick={closePresentationModal}>
+        <button className="close btn-close" type="button" aria-label="close" onClick={closeHandler}>
           <i className="ti ti-close" aria-hidden></i>
           <i className="ti ti-close" aria-hidden></i>
         </button>
         </button>
       </div>
       </div>

+ 20 - 4
packages/app/src/components/UnsavedAlertDialog.tsx

@@ -8,7 +8,7 @@ import { useIsEnabledUnsavedWarning } from '~/stores/editor';
 const UnsavedAlertDialog = (): JSX.Element => {
 const UnsavedAlertDialog = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const router = useRouter();
   const router = useRouter();
-  const { data: isEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { data: isEnabledUnsavedWarning, mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
 
 
   const alertUnsavedWarningByBrowser = useCallback((e) => {
   const alertUnsavedWarningByBrowser = useCallback((e) => {
     if (isEnabledUnsavedWarning) {
     if (isEnabledUnsavedWarning) {
@@ -23,12 +23,20 @@ const UnsavedAlertDialog = (): JSX.Element => {
 
 
   const alertUnsavedWarningByNextRouter = useCallback(() => {
   const alertUnsavedWarningByNextRouter = useCallback(() => {
     if (isEnabledUnsavedWarning) {
     if (isEnabledUnsavedWarning) {
-    // eslint-disable-next-line no-alert
-      window.alert(t('page_edit.changes_not_saved'));
+      // see: https://zenn.dev/qaynam/articles/c4794537a163d2
+      // eslint-disable-next-line no-alert
+      const answer = window.confirm(t('page_edit.changes_not_saved'));
+      if (!answer) {
+      // eslint-disable-next-line no-throw-literal
+        throw 'Abort route';
+      }
     }
     }
-    return;
   }, [isEnabledUnsavedWarning, t]);
   }, [isEnabledUnsavedWarning, t]);
 
 
+  const onRouterChangeComplete = useCallback(() => {
+    mutateIsEnabledUnsavedWarning(false);
+  }, [mutateIsEnabledUnsavedWarning]);
+
   /*
   /*
   * Route changes by Browser
   * Route changes by Browser
   * Example: window.location.href, F5
   * Example: window.location.href, F5
@@ -53,6 +61,14 @@ const UnsavedAlertDialog = (): JSX.Element => {
   }, [alertUnsavedWarningByNextRouter, router.events]);
   }, [alertUnsavedWarningByNextRouter, router.events]);
 
 
 
 
+  useEffect(() => {
+    router.events.on('routeChangeComplete', onRouterChangeComplete);
+    return () => {
+      router.events.off('routeChangeComplete', onRouterChangeComplete);
+    };
+  }, [onRouterChangeComplete, router.events]);
+
+
   return <></>;
   return <></>;
 };
 };
 
 

+ 2 - 0
packages/app/src/interfaces/crowi-request.ts

@@ -9,6 +9,8 @@ export interface CrowiRequest<U extends IUser = IUserHasId> extends Request {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   crowi: any,
   crowi: any,
 
 
+  session: any,
+
   // provided by csurf
   // provided by csurf
   csrfToken: () => string,
   csrfToken: () => string,
 
 

+ 3 - 1
packages/app/src/interfaces/user.ts

@@ -1,3 +1,5 @@
 export type {
 export type {
-  IUser, IUserGroupRelation, IUserGroup, IUserHasId, IUserGroupHasId, IUserGroupRelationHasId,
+  IUser, IUserGroupRelation, IUserGroup, IUserHasId, IUserGroupHasId, IUserGroupRelationHasId, IUserStatus,
 } from '@growi/core';
 } from '@growi/core';
+
+export { USER_STATUS } from '@growi/core';

+ 44 - 65
packages/app/src/pages/[[...path]].page.tsx

@@ -10,7 +10,7 @@ import type {
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUserHasId,
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUserHasId,
 } from '@growi/core';
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import ExtensibleCustomError from 'extensible-custom-error';
-import {
+import type {
   GetServerSideProps, GetServerSidePropsContext,
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@@ -26,44 +26,38 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { EditorConfig } from '~/interfaces/editor-settings';
 import type { EditorConfig } from '~/interfaces/editor-settings';
 import type { IPageGrantData } from '~/interfaces/page';
 import type { IPageGrantData } from '~/interfaces/page';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
-import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
-import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
+import {
+  useCurrentUser,
+  useIsLatestRevision,
+  useIsForbidden, useIsNotFound, useIsSharedUser,
+  useIsEnabledStaleNotification, useIsIdenticalPath,
+  useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
+  useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
+  useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
+  useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
+  useIsSlackConfigured, useRendererConfig,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
+} from '~/stores/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
-import {
-  useSelectedGrant,
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
+import { useSelectedGrant } from '~/stores/ui';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { DescendantsPageListModal } from '../components/DescendantsPageListModal';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
 import type { GrowiSubNavigationSwitcherProps } from '../components/Navbar/GrowiSubNavigationSwitcher';
 import type { GrowiSubNavigationSwitcherProps } from '../components/Navbar/GrowiSubNavigationSwitcher';
 import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
 import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
-import {
-  useCurrentUser,
-  useIsLatestRevision,
-  useIsForbidden, useIsNotFound, useIsSharedUser,
-  useIsEnabledStaleNotification, useIsIdenticalPath,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
-  useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
-  useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
-  useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
-  useIsSlackConfigured, useRendererConfig,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
-} from '../stores/context';
 
 
-import { NextPageWithLayout } from './_app.page';
+import type { NextPageWithLayout } from './_app.page';
+import type { CommonProps } from './utils/commons';
 import {
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
 } from './utils/commons';
 } from './utils/commons';
 
 
 
 
@@ -73,6 +67,7 @@ declare global {
 }
 }
 
 
 
 
+const DescendantsPageListModal = dynamic(() => import('../components/DescendantsPageListModal').then(mod => mod.DescendantsPageListModal), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() => import('../components/Navbar/GrowiSubNavigationSwitcher')
 const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() => import('../components/Navbar/GrowiSubNavigationSwitcher')
   .then(mod => mod.GrowiSubNavigationSwitcher), { ssr: false });
   .then(mod => mod.GrowiSubNavigationSwitcher), { ssr: false });
@@ -177,31 +172,22 @@ type Props = CommonProps & {
   grantData?: IPageGrantData,
   grantData?: IPageGrantData,
 
 
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
-
-  // UI
-  userUISettings?: IUserUISettings
-  // Sidebar
-  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const Page: NextPageWithLayout<Props> = (props: Props) => {
 const Page: NextPageWithLayout<Props> = (props: Props) => {
-  // const { t } = useTranslation();
-  const router = useRouter();
-
-  const { data: currentUser } = useCurrentUser(props.currentUser ?? null);
-
   // register global EventEmitter
   // register global EventEmitter
   if (isClient() && window.globalEmitter == null) {
   if (isClient() && window.globalEmitter == null) {
     window.globalEmitter = new EventEmitter();
     window.globalEmitter = new EventEmitter();
   }
   }
 
 
+  const router = useRouter();
+
+  useCurrentUser(props.currentUser ?? null);
+
   // commons
   // commons
   useEditorConfig(props.editorConfig);
   useEditorConfig(props.editorConfig);
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
 
 
-  // init sidebar config with UserUISettings and sidebarConfig
-  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
-
   // page
   // page
   useIsLatestRevision(props.isLatestRevision);
   useIsLatestRevision(props.isLatestRevision);
   useIsContainerFluid(props.isContainerFluid);
   useIsContainerFluid(props.isContainerFluid);
@@ -241,15 +227,13 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsUploadableFile(props.editorConfig.upload.isUploadableFile);
   useIsUploadableFile(props.editorConfig.upload.isUploadableFile);
   useIsUploadableImage(props.editorConfig.upload.isUploadableImage);
   useIsUploadableImage(props.editorConfig.upload.isUploadableImage);
 
 
-  const { pageWithMeta, userUISettings } = props;
+  const { pageWithMeta } = props;
 
 
   const pageId = pageWithMeta?.data._id;
   const pageId = pageWithMeta?.data._id;
   const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
   const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
   const revisionBody = pageWithMeta?.data.revision?.body;
   const revisionBody = pageWithMeta?.data.revision?.body;
 
 
   useCurrentPageId(pageId ?? null);
   useCurrentPageId(pageId ?? null);
-  useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
-  useRemoteRevisionId(pageWithMeta?.data.revision?._id);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
@@ -261,6 +245,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
 
 
+  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
+  const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
+
   useSetupGlobalSocket();
   useSetupGlobalSocket();
   useSetupGlobalSocketForPage(pageId);
   useSetupGlobalSocketForPage(pageId);
 
 
@@ -286,9 +273,17 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   }, [props.currentPathname, router]);
   }, [props.currentPathname, router]);
 
 
   // initialize mutateEditingMarkdown only once per page
   // initialize mutateEditingMarkdown only once per page
+  // need to include useCurrentPathname not useCurrentPagePath
+  useEffect(() => {
+    if (props.currentPathname != null) {
+      mutateEditingMarkdown(revisionBody);
+    }
+  }, [mutateEditingMarkdown, revisionBody, props.currentPathname]);
+
   useEffect(() => {
   useEffect(() => {
-    mutateEditingMarkdown(revisionBody);
-  }, [mutateEditingMarkdown, revisionBody]);
+    mutateRemoteRevisionId(pageWithMeta?.data.revision?._id);
+    mutateRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
+  }, [mutateRemoteRevisionId, mutateRevisionIdHackmdSynced, pageWithMeta?.data.revision?._id, pageWithMeta?.data.revisionHackmdSynced]);
 
 
   const title = generateCustomTitleForPage(props, pagePath);
   const title = generateCustomTitleForPage(props, pagePath);
 
 
@@ -329,14 +324,16 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   );
   );
 };
 };
 
 
-type LayoutProps = {
+type LayoutProps = Props & {
   children?: ReactNode
   children?: ReactNode
-  className?: string
 }
 }
 
 
-const Layout = ({ children }: LayoutProps): JSX.Element => {
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
   const className = useEditorModeClassName();
   const className = useEditorModeClassName();
 
 
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
+
   return (
   return (
     <BasicLayout className={className}>
     <BasicLayout className={className}>
       {children}
       {children}
@@ -344,12 +341,12 @@ const Layout = ({ children }: LayoutProps): JSX.Element => {
   );
   );
 };
 };
 
 
-Page.getLayout = function getLayout(page) {
+Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
   return (
   return (
     <>
     <>
       <DrawioViewerScript />
       <DrawioViewerScript />
 
 
-      <Layout>
+      <Layout {...page.props}>
         {page}
         {page}
       </Layout>
       </Layout>
       <UnsavedAlertDialog />
       <UnsavedAlertDialog />
@@ -459,19 +456,6 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   props.pageWithMeta = pageWithMeta;
   props.pageWithMeta = pageWithMeta;
 }
 }
 
 
-async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
-  const { model: mongooseModel } = await import('mongoose');
-
-  const req = context.req as CrowiRequest<IUserHasId & any>;
-  const { user } = req;
-  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
-
-  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
-  if (userUISettings != null) {
-    props.userUISettings = userUISettings.toObject();
-  }
-}
-
 async function injectRoutingInformation(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectRoutingInformation(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
@@ -530,7 +514,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
   const {
   const {
-    appService, searchService, configManager, aclService, slackNotificationService, mailService,
+    searchService, configManager, aclService,
   } = crowi;
   } = crowi;
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
@@ -577,10 +561,6 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
   };
 
 
-  props.sidebarConfig = {
-    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
-  };
 }
 }
 
 
 /**
 /**
@@ -633,7 +613,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     }
     }
   }
   }
 
 
-  await injectUserUISettings(context, props);
   await injectRoutingInformation(context, props);
   await injectRoutingInformation(context, props);
   injectServerConfigurations(context, props);
   injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['translation']);
   await injectNextI18NextConfigurations(context, props, ['translation']);

+ 0 - 3
packages/app/src/pages/_app.page.tsx

@@ -50,7 +50,6 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
 
 
 
 
   const commonPageProps = pageProps as CommonProps;
   const commonPageProps = pageProps as CommonProps;
-  // useInterceptorManager(new InterceptorManager());
   useAppTitle(commonPageProps.appTitle);
   useAppTitle(commonPageProps.appTitle);
   useSiteUrl(commonPageProps.siteUrl);
   useSiteUrl(commonPageProps.siteUrl);
   useConfidential(commonPageProps.confidential);
   useConfidential(commonPageProps.confidential);
@@ -68,6 +67,4 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   );
   );
 }
 }
 
 
-// export default appWithTranslation(GrowiApp);
-
 export default appWithTranslation(GrowiApp, nextI18nConfig);
 export default appWithTranslation(GrowiApp, nextI18nConfig);

+ 2 - 29
packages/app/src/pages/_private-legacy-pages.page.tsx

@@ -9,21 +9,15 @@ import Head from 'next/head';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUser, IUserHasId } from '~/interfaces/user';
 import type { IUser, IUserHasId } from '~/interfaces/user';
-import type { IUserUISettings } from '~/interfaces/user-ui-settings';
-import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import {
 import {
   useCsrfToken, useCurrentUser, useDrawioUri, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useDrawioUri, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig,
 } from '~/stores/context';
 } from '~/stores/context';
-import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
-  useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
 
 
+import type { CommonProps } from './utils/commons';
 import {
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 } from './utils/commons';
 
 
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
@@ -37,11 +31,6 @@ type Props = CommonProps & {
 
 
   drawioUri: string | null,
   drawioUri: string | null,
 
 
-  // UI
-  userUISettings?: IUserUISettings
-  // Sidebar
-  sidebarConfig: ISidebarConfig,
-
   // Render config
   // Render config
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 
 
@@ -50,8 +39,6 @@ type Props = CommonProps & {
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const { userUISettings } = props;
-
   const PrivateLegacyPages = dynamic(() => import('~/components/PrivateLegacyPages'), { ssr: false });
   const PrivateLegacyPages = dynamic(() => import('~/components/PrivateLegacyPages'), { ssr: false });
 
 
   // commons
   // commons
@@ -92,19 +79,6 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   );
   );
 };
 };
 
 
-async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
-  const { model: mongooseModel } = await import('mongoose');
-
-  const req = context.req as CrowiRequest<IUserHasId & any>;
-  const { user } = req;
-
-  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
-  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
-  if (userUISettings != null) {
-    props.userUISettings = userUISettings.toObject();
-  }
-}
-
 async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
@@ -168,7 +142,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = user.toObject();
     props.currentUser = user.toObject();
   }
   }
 
 
-  await injectUserUISettings(context, props);
   await injectServerConfigurations(context, props);
   await injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['translation']);
   await injectNextI18NextConfigurations(context, props, ['translation']);
 
 

+ 20 - 30
packages/app/src/pages/_search.page.tsx

@@ -1,3 +1,5 @@
+import { ReactNode } from 'react';
+
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@@ -8,24 +10,18 @@ import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUser, IUserHasId } from '~/interfaces/user';
 import type { IUser, IUserHasId } from '~/interfaces/user';
-import type { IUserUISettings } from '~/interfaces/user-ui-settings';
-import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import {
 import {
   useCsrfToken, useCurrentUser, useDrawioUri, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useDrawioUri, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL,
 } from '~/stores/context';
 } from '~/stores/context';
-import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
-  useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
 
 
 import { SearchPage } from '../components/SearchPage';
 import { SearchPage } from '../components/SearchPage';
 
 
 import type { NextPageWithLayout } from './_app.page';
 import type { NextPageWithLayout } from './_app.page';
+import type { CommonProps } from './utils/commons';
 import {
 import {
-  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, CommonProps, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 } from './utils/commons';
 
 
 
 
@@ -38,11 +34,6 @@ type Props = CommonProps & {
 
 
   drawioUri: string | null,
   drawioUri: string | null,
 
 
-  // UI
-  userUISettings?: IUserUISettings
-  // Sidebar
-  sidebarConfig: ISidebarConfig,
-
   // Render config
   // Render config
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 
 
@@ -54,8 +45,6 @@ type Props = CommonProps & {
 };
 };
 
 
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
-  const { userUISettings } = props;
-
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   // commons
   // commons
@@ -102,28 +91,30 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
   );
   );
 };
 };
 
 
+type LayoutProps = Props & {
+  children?: ReactNode
+}
+
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
+
+  return (
+    <SearchResultLayout>
+      {children}
+    </SearchResultLayout>
+  );
+};
+
 SearchResultPage.getLayout = function getLayout(page) {
 SearchResultPage.getLayout = function getLayout(page) {
   return (
   return (
     <>
     <>
       <DrawioViewerScript />
       <DrawioViewerScript />
-      <SearchResultLayout>{page}</SearchResultLayout>
+      <Layout {...page.props}>{page}</Layout>
     </>
     </>
   );
   );
 };
 };
 
 
-async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
-  const { model: mongooseModel } = await import('mongoose');
-
-  const req = context.req as CrowiRequest<IUserHasId & any>;
-  const { user } = req;
-
-  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
-  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
-  if (userUISettings != null) {
-    props.userUISettings = userUISettings.toObject();
-  }
-}
-
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
@@ -190,7 +181,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = user.toObject();
     props.currentUser = user.toObject();
   }
   }
 
 
-  await injectUserUISettings(context, props);
   injectServerConfigurations(context, props);
   injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['translation']);
   await injectNextI18NextConfigurations(context, props, ['translation']);
 
 

+ 2 - 22
packages/app/src/pages/me/[[...path]].page.tsx

@@ -1,7 +1,6 @@
 import React, { useMemo } from 'react';
 import React, { useMemo } from 'react';
 
 
 import { IUserHasId } from '@growi/core';
 import { IUserHasId } from '@growi/core';
-import { model as mongooseModel } from 'mongoose';
 import {
 import {
   GetServerSideProps, GetServerSidePropsContext,
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
@@ -14,23 +13,18 @@ import { useRouter } from 'next/router';
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import { ISidebarConfig } from '~/interfaces/sidebar-config';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import {
 import {
   useCurrentUser, useIsSearchPage,
   useCurrentUser, useIsSearchPage,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useCsrfToken, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useIsSearchScopeChildrenAsDefault,
   useRegistrationWhiteList, useShowPageLimitationXL, useRendererConfig,
   useRegistrationWhiteList, useShowPageLimitationXL, useRendererConfig,
 } from '~/stores/context';
 } from '~/stores/context';
-import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { NextPageWithLayout } from '../_app.page';
 import { NextPageWithLayout } from '../_app.page';
+import type { CommonProps } from '../utils/commons';
 import {
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from '../utils/commons';
 } from '../utils/commons';
 
 
 
 
@@ -40,8 +34,6 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
-  userUISettings?: IUserUISettings
-  sidebarConfig: ISidebarConfig,
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
   showPageLimitationXL: number,
   showPageLimitationXL: number,
 
 
@@ -139,17 +131,6 @@ MePage.getLayout = function getLayout(page) {
   );
   );
 };
 };
 
 
-async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
-  const req = context.req as CrowiRequest<IUserHasId & any>;
-  const { user } = req;
-  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
-
-  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
-  if (userUISettings != null) {
-    props.userUISettings = userUISettings.toObject();
-  }
-}
-
 async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
@@ -221,7 +202,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = userData.toObject();
     props.currentUser = userData.toObject();
   }
   }
 
 
-  await injectUserUISettings(context, props);
   await injectServerConfigurations(context, props);
   await injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['translation', 'admin', 'commons']);
   await injectNextI18NextConfigurations(context, props, ['translation', 'admin', 'commons']);
 
 

+ 18 - 31
packages/app/src/pages/tags.page.tsx

@@ -1,7 +1,7 @@
-import React, { useState, useCallback } from 'react';
+import React, { useState, useCallback, ReactNode } from 'react';
 
 
 import type { IUser, IUserHasId } from '@growi/core';
 import type { IUser, IUserHasId } from '@growi/core';
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -9,14 +9,8 @@ import Head from 'next/head';
 
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IDataTagCount } from '~/interfaces/tag';
 import type { IDataTagCount } from '~/interfaces/tag';
-import type { IUserUISettings } from '~/interfaces/user-ui-settings';
-import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import { useSWRxTagsList } from '~/stores/tag';
 import { useSWRxTagsList } from '~/stores/tag';
-import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
 import {
@@ -26,8 +20,9 @@ import {
 } from '../stores/context';
 } from '../stores/context';
 
 
 import { NextPageWithLayout } from './_app.page';
 import { NextPageWithLayout } from './_app.page';
+import type { CommonProps } from './utils/commons';
 import {
 import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, useInitSidebarConfig,
+  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 } from './utils/commons';
 
 
 const PAGING_LIMIT = 10;
 const PAGING_LIMIT = 10;
@@ -38,12 +33,6 @@ type Props = CommonProps & {
   isSearchServiceReachable: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
 
 
-  // ui
-  userUISettings?: IUserUISettings
-
-  // sidebar
-  sidebarConfig: ISidebarConfig,
-
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 };
 };
 
 
@@ -115,26 +104,25 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   );
   );
 };
 };
 
 
+type LayoutProps = Props & {
+  children?: ReactNode
+}
+
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
+
+  return <BasicLayout>{children}</BasicLayout>;
+};
+
 TagPage.getLayout = function getLayout(page) {
 TagPage.getLayout = function getLayout(page) {
   return (
   return (
-    <BasicLayout>{page}</BasicLayout>
+    <Layout {...page.props}>
+      {page}
+    </Layout>
   );
   );
 };
 };
 
 
-async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
-  const { model: mongooseModel } = await import('mongoose');
-
-  const req = context.req as CrowiRequest<IUserHasId & any>;
-  const { user } = req;
-
-  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
-  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
-
-  if (userUISettings != null) {
-    props.userUISettings = userUISettings.toObject();
-  }
-}
-
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
@@ -194,7 +182,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = user.toObject();
     props.currentUser = user.toObject();
   }
   }
 
 
-  await injectUserUISettings(context, props);
   injectServerConfigurations(context, props);
   injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['translation']);
   await injectNextI18NextConfigurations(context, props, ['translation']);
 
 

+ 17 - 30
packages/app/src/pages/trash.page.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { ReactNode } from 'react';
 
 
 import type { IUser, IUserHasId } from '@growi/core';
 import type { IUser, IUserHasId } from '@growi/core';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
@@ -9,12 +9,7 @@ import Head from 'next/head';
 import { GrowiSubNavigation } from '~/components/Navbar/GrowiSubNavigation';
 import { GrowiSubNavigation } from '~/components/Navbar/GrowiSubNavigation';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
-import type { IUserUISettings } from '~/interfaces/user-ui-settings';
-import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
-import {
-  useCurrentProductNavWidth, useCurrentSidebarContents, useDrawerMode, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
-} from '~/stores/ui';
+import { useDrawerMode } from '~/stores/ui';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
 import {
@@ -24,8 +19,9 @@ import {
 } from '../stores/context';
 } from '../stores/context';
 
 
 import type { NextPageWithLayout } from './_app.page';
 import type { NextPageWithLayout } from './_app.page';
+import type { CommonProps } from './utils/commons';
 import {
 import {
-  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, CommonProps, useInitSidebarConfig,
+  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, useInitSidebarConfig,
 } from './utils/commons';
 } from './utils/commons';
 
 
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
@@ -39,11 +35,6 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
   showPageLimitationXL: number,
   showPageLimitationXL: number,
 
 
-  // UI
-  userUISettings?: IUserUISettings
-  // Sidebar
-  sidebarConfig: ISidebarConfig,
-
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 };
 };
 
 
@@ -96,32 +87,29 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   );
   );
 };
 };
 
 
+type LayoutProps = Props & {
+  children?: ReactNode,
+}
+
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
+
+  return <BasicLayout>{children}</BasicLayout>;
+};
+
 TrashPage.getLayout = function getLayout(page) {
 TrashPage.getLayout = function getLayout(page) {
   return (
   return (
     <>
     <>
-      <BasicLayout>
+      <Layout {...page.props}>
         {page}
         {page}
-      </BasicLayout>
+      </Layout>
       <EmptyTrashModal />
       <EmptyTrashModal />
       <PutbackPageModal />
       <PutbackPageModal />
     </>
     </>
   );
   );
 };
 };
 
 
-async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
-  const { model: mongooseModel } = await import('mongoose');
-
-  const req = context.req as CrowiRequest<IUserHasId & any>;
-  const { user } = req;
-
-  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
-  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
-
-  if (userUISettings != null) {
-    props.userUISettings = userUISettings.toObject();
-  }
-}
-
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
@@ -181,7 +169,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   if (user != null) {
   if (user != null) {
     props.currentUser = user.toObject();
     props.currentUser = user.toObject();
   }
   }
-  await injectUserUISettings(context, props);
   injectServerConfigurations(context, props);
   injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['translation']);
   await injectNextI18NextConfigurations(context, props, ['translation']);
 
 

+ 15 - 0
packages/app/src/pages/utils/commons.ts

@@ -11,6 +11,7 @@ import { detectLocaleFromBrowserAcceptLanguage } from '~/client/util/locale-util
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import {
 import {
   useCurrentProductNavWidth, useCurrentSidebarContents, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
   useCurrentProductNavWidth, useCurrentSidebarContents, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
 } from '~/stores/ui';
 } from '~/stores/ui';
@@ -30,10 +31,13 @@ export type CommonProps = {
   isDefaultLogo: boolean,
   isDefaultLogo: boolean,
   currentUser?: IUserHasId,
   currentUser?: IUserHasId,
   forcedColorScheme?: ColorScheme,
   forcedColorScheme?: ColorScheme,
+  sidebarConfig: ISidebarConfig,
+  userUISettings?: IUserUISettings
 } & Partial<SSRConfig>;
 } & Partial<SSRConfig>;
 
 
 // eslint-disable-next-line max-len
 // eslint-disable-next-line max-len
 export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(context: GetServerSidePropsContext) => {
 export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(context: GetServerSidePropsContext) => {
+  const getModelSafely = await import('~/server/util/mongoose-utils').then(mod => mod.getModelSafely);
 
 
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { crowi, user } = req;
   const { crowi, user } = req;
@@ -70,6 +74,12 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
   const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
   const forcedColorScheme = crowi.customizeService.forcedColorScheme;
   const forcedColorScheme = crowi.customizeService.forcedColorScheme;
 
 
+  // retrieve UserUISettings
+  const UserUISettings = getModelSafely<UserUISettingsDocument>('UserUISettings');
+  const userUISettings = user != null && UserUISettings != null
+    ? await UserUISettings.findOne({ user: user._id }).exec()
+    : req.session.uiSettings; // for guests
+
   const props: CommonProps = {
   const props: CommonProps = {
     namespacesRequired: ['translation'],
     namespacesRequired: ['translation'],
     currentPathname,
     currentPathname,
@@ -85,6 +95,11 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     currentUser,
     currentUser,
     isDefaultLogo,
     isDefaultLogo,
     forcedColorScheme,
     forcedColorScheme,
+    sidebarConfig: {
+      isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
+      isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    },
+    userUISettings: userUISettings?.toObject?.() ?? userUISettings,
   };
   };
 
 
   return { props };
   return { props };

+ 0 - 5
packages/app/src/server/crowi/express-init.js

@@ -70,17 +70,12 @@ module.exports = function(crowi, app) {
   app.use((req, res, next) => {
   app.use((req, res, next) => {
     const now = new Date();
     const now = new Date();
     // for datez
     // for datez
-
-    const Page = crowi.model('Page');
-    const User = crowi.model('User');
-    const Config = mongoose.model('Config');
     app.set('tzoffset', crowi.appService.getTzoffset());
     app.set('tzoffset', crowi.appService.getTzoffset());
 
 
     res.locals.req = req;
     res.locals.req = req;
     res.locals.baseUrl = crowi.appService.getSiteUrl();
     res.locals.baseUrl = crowi.appService.getSiteUrl();
     res.locals.env = env;
     res.locals.env = env;
     res.locals.now = now;
     res.locals.now = now;
-    res.locals.local_config = Config.getLocalconfig(crowi); // config for browser context
 
 
     next();
     next();
   });
   });

+ 0 - 37
packages/app/src/server/middlewares/inject-user-ui-settings-to-localvars.ts

@@ -1,37 +0,0 @@
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import loggerFactory from '~/utils/logger';
-
-import UserUISettings from '../models/user-ui-settings';
-
-const logger = loggerFactory('growi:middleware:inject-user-ui-settings-to-localvars');
-
-async function getSettings(userId: string): Promise<IUserUISettings | null> {
-  const doc = await UserUISettings.findOne({ user: userId }).exec();
-
-  let userUISettings: IUserUISettings | null = null;
-  if (doc != null) {
-    const obj = doc.toObject();
-    // omit user property
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    userUISettings = (({ user, ...rest }) => rest)(obj);
-  }
-
-  return userUISettings;
-}
-
-module.exports = () => {
-  return async(req, res, next) => {
-    if (req.user == null) {
-      return next();
-    }
-
-    try {
-      res.locals.userUISettings = await getSettings(req.user._id);
-    }
-    catch (err: unknown) {
-      logger.error(err);
-    }
-
-    next();
-  };
-};

+ 4 - 9
packages/app/src/server/middlewares/login-required.js

@@ -1,3 +1,4 @@
+import { createRedirectToForUnauthenticated } from '~/server/util/createRedirectToForUnauthenticated';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:middleware:login-required');
 const logger = loggerFactory('growi:middleware:login-required');
@@ -20,15 +21,9 @@ module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
         // Active の人だけ先に進める
         // Active の人だけ先に進める
         return next();
         return next();
       }
       }
-      if (req.user.status === User.STATUS_REGISTERED) {
-        return res.redirect('/login/error/registered');
-      }
-      if (req.user.status === User.STATUS_SUSPENDED) {
-        return res.redirect('/login/error/suspended');
-      }
-      if (req.user.status === User.STATUS_INVITED) {
-        return res.redirect('/invited');
-      }
+
+      const redirectTo = createRedirectToForUnauthenticated(req.user.status) ?? '/login';
+      return res.redirect(redirectTo);
     }
     }
 
 
     // check the route config and ACL
     // check the route config and ACL

+ 0 - 86
packages/app/src/server/models/config.ts

@@ -161,90 +161,4 @@ export const defaultNotificationConfigs: { [key: string]: any } = {
   'slack:token': undefined,
   'slack:token': undefined,
 };
 };
 
 
-/**
- * It is deprecated to use this for anything other than ConfigLoader#load.
- */
-// configSchema.statics.getDefaultCrowiConfigsObject = function() {
-//   return getDefaultCrowiConfigs();
-// };
-
-/**
- * It is deprecated to use this for anything other than ConfigLoader#load.
- */
-// configSchema.statics.getDefaultMarkdownConfigsObject = function() {
-//   return getDefaultMarkdownConfigs();
-// };
-
-/**
- * It is deprecated to use this for anything other than ConfigLoader#load.
- */
-// configSchema.statics.getDefaultNotificationConfigsObject = function() {
-//   return getDefaultNotificationConfigs();
-// };
-
-schema.statics.getLocalconfig = function(crowi) {
-  const env = process.env;
-
-  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
-
-  const localConfig = {
-    crowi: {
-      title: crowi.appService.getAppTitle(),
-      url: crowi.appService.getSiteUrl(),
-      confidential: crowi.appService.getAppConfidential(),
-      version: crowi.version,
-    },
-    upload: {
-      image: crowi.fileUploadService.getIsUploadable(),
-      file: crowi.fileUploadService.getFileUploadEnabled(),
-    },
-    registrationWhiteList: crowi.configManager.getConfig('crowi', 'security:registrationWhiteList'),
-    disableLinkSharing: crowi.configManager.getConfig('crowi', 'security:disableLinkSharing'),
-    themeType: crowi.configManager.getConfig('crowi', 'customize:theme'),
-    isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
-    adminPreferredIndentSize: crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
-    isIndentSizeForced: crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
-    isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-    isEnabledTimeline: crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
-    isAllReplyShown: crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
-    isSearchScopeChildrenAsDefault: crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
-    xssOption: crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
-    tagWhiteList: crowi.xssService.getTagWhiteList(),
-    attrWhiteList: crowi.xssService.getAttrWhiteList(),
-    highlightJsStyle: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
-    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
-    customizeTitle: crowi.configManager.getConfig('crowi', 'customize:title'),
-    customizeHeader: crowi.configManager.getConfig('crowi', 'customize:header'),
-    customizeCss: crowi.configManager.getConfig('crowi', 'customize:css'),
-    isEnabledAttachTitleHeader: crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
-    customizeScript: crowi.configManager.getConfig('crowi', 'customize:script'),
-    isSlackConfigured: crowi.slackIntegrationService.isSlackConfigured,
-    env: {
-      PLANTUML_URI: env.PLANTUML_URI || null,
-      BLOCKDIAG_URI: env.BLOCKDIAG_URI || null,
-      DRAWIO_URI: env.DRAWIO_URI || null,
-      HACKMD_URI: env.HACKMD_URI || null,
-      MATHJAX: env.MATHJAX || null,
-      GROWI_CLOUD_URI: env.GROWI_CLOUD_URI || null,
-      GROWI_APP_ID_FOR_GROWI_CLOUD: env.GROWI_APP_ID_FOR_GROWI_CLOUD || null,
-    },
-    isEnabledStaleNotification: crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
-    isAclEnabled: crowi.aclService.isAclEnabled(),
-    isSearchServiceConfigured: crowi.searchService.isConfigured,
-    isSearchServiceReachable: crowi.searchService.isReachable,
-    isMailerSetup: crowi.mailService.isMailerSetup,
-    globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
-    pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
-    pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
-    auditLogEnabled: crowi.configManager.getConfig('crowi', 'app:auditLogEnabled'),
-    activityExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds'),
-    auditLogAvailableActions: crowi.activityService.getAvailableActions(false),
-    isSidebarDrawerMode: crowi.configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-    isSidebarClosedAtDockMode: crowi.configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
-  };
-
-  return localConfig;
-};
-
 export default getOrCreateModel<Config, ModelMethods>('Config', schema);
 export default getOrCreateModel<Config, ModelMethods>('Config', schema);

+ 2 - 2
packages/app/src/server/models/user-ui-settings.ts

@@ -1,11 +1,11 @@
-import { Ref, IUser } from '@growi/core';
+import type { Ref, IUser } from '@growi/core';
 import {
 import {
   Schema, Model, Document,
   Schema, Model, Document,
 } from 'mongoose';
 } from 'mongoose';
 
 
 
 
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
+import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 

+ 1 - 1
packages/app/src/server/routes/apiv3/index.js

@@ -51,7 +51,7 @@ module.exports = (crowi, app) => {
   const loginPassport = require('../login-passport')(crowi, app);
   const loginPassport = require('../login-passport')(crowi, app);
 
 
   routerForAuth.post('/login', applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation,
   routerForAuth.post('/login', applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation,
-    addActivity, loginPassport.isEnableLoginWithLocalOrLdap, loginPassport.loginWithLocal, loginPassport.loginWithLdap,
+    addActivity, loginPassport.injectRedirectTo, loginPassport.isEnableLoginWithLocalOrLdap, loginPassport.loginWithLocal, loginPassport.loginWithLdap,
     loginPassport.cannotLoginErrorHadnler, loginPassport.loginFailure);
     loginPassport.cannotLoginErrorHadnler, loginPassport.loginFailure);
 
 
   routerForAuth.use('/invited', require('./invited')(crowi));
   routerForAuth.use('/invited', require('./invited')(crowi));

+ 15 - 2
packages/app/src/server/routes/apiv3/user-ui-settings.ts

@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:routes:apiv3:user-ui-settings');
 const router = express.Router();
 const router = express.Router();
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
 
 
   const validatorForPut = [
   const validatorForPut = [
     body('settings').exists().withMessage('The body param \'settings\' is required'),
     body('settings').exists().withMessage('The body param \'settings\' is required'),
@@ -25,7 +24,7 @@ module.exports = (crowi) => {
   ];
   ];
 
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  router.put('/', loginRequiredStrictly, validatorForPut, apiV3FormValidator, async(req: any, res: any) => {
+  router.put('/', validatorForPut, apiV3FormValidator, async(req: any, res: any) => {
     const { user } = req;
     const { user } = req;
     const { settings } = req.body;
     const { settings } = req.body;
 
 
@@ -37,6 +36,20 @@ module.exports = (crowi) => {
       preferDrawerModeByUser: settings.preferDrawerModeByUser,
       preferDrawerModeByUser: settings.preferDrawerModeByUser,
       preferDrawerModeOnEditByUser: settings.preferDrawerModeOnEditByUser,
       preferDrawerModeOnEditByUser: settings.preferDrawerModeOnEditByUser,
     };
     };
+
+    if (user == null) {
+      if (req.session.uiSettings == null) {
+        req.session.uiSettings = {};
+      }
+      Object.keys(updateData).forEach((setting) => {
+        if (updateData[setting] != null) {
+          req.session.uiSettings[setting] = updateData[setting];
+        }
+      });
+      return res.apiv3(updateData);
+    }
+
+
     // remove the keys that have null value
     // remove the keys that have null value
     Object.keys(updateData).forEach((key) => {
     Object.keys(updateData).forEach((key) => {
       if (updateData[key] == null) {
       if (updateData[key] == null) {

+ 28 - 13
packages/app/src/server/routes/apiv3/users.js

@@ -841,31 +841,46 @@ module.exports = (crowi) => {
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: success resrt password
    *            description: success resrt password
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    newPassword:
-   *                      type: string
-   *                    user:
-   *                      type: object
-   *                      description: Target user
    */
    */
   router.put('/reset-password', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
   router.put('/reset-password', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
+    const { appService, mailService } = crowi;
     const { id } = req.body;
     const { id } = req.body;
 
 
+    let newPassword;
+    let user;
+
     try {
     try {
-      const [newPassword, user] = await Promise.all([
+      [newPassword, user] = await Promise.all([
         await User.resetPasswordByRandomString(id),
         await User.resetPasswordByRandomString(id),
         await User.findById(id)]);
         await User.findById(id)]);
 
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_PASSWORD_RESET });
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_PASSWORD_RESET });
+    }
+    catch (err) {
+      const msg = 'Error occurred during password reset request procedure.';
+      logger.error(err);
+      return res.apiv3Err(`${msg} Cause: ${err}`);
+    }
 
 
-      return res.apiv3({ newPassword, user });
+    try {
+      await mailService.send({
+        to: user.email,
+        subject: 'Your password has been reset by the administrator',
+        template: path.join(crowi.localeDir, 'en_US/admin/userResetPassword.txt'),
+        vars: {
+          email: user.email,
+          password: newPassword,
+          url: crowi.appService.getSiteUrl(),
+          appTitle: appService.getAppTitle(),
+        },
+      });
+
+      return res.apiv3({});
     }
     }
     catch (err) {
     catch (err) {
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(err));
+      const msg = 'Error occurred during password reset send e-mail.';
+      logger.error(err);
+      return res.apiv3Err(`${msg} Cause: ${err}`);
     }
     }
   });
   });
 
 

+ 6 - 6
packages/app/src/server/routes/index.js

@@ -91,12 +91,12 @@ module.exports = function(crowi, app) {
   // OAuth
   // OAuth
   app.get('/passport/google'                      , loginPassport.loginWithGoogle, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/google'                      , loginPassport.loginWithGoogle, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/github'                      , loginPassport.loginWithGitHub, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/github'                      , loginPassport.loginWithGitHub, loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/oidc'                        , loginPassport.loginWithOidc, loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/saml'                        , loginPassport.loginWithSaml, loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/oidc/callback'               , loginPassport.loginPassportOidcCallback     , loginPassport.loginFailureForExternalAccount);
-  app.post('/passport/saml/callback'              , addActivity, loginPassport.loginPassportSamlCallback, loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/oidc'                        , loginPassport.loginWithOidc,   loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/saml'                        , loginPassport.loginWithSaml,   loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/google/callback'             , loginPassport.injectRedirectTo, loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/github/callback'             , loginPassport.injectRedirectTo, loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailureForExternalAccount);
+  app.get('/passport/oidc/callback'               , loginPassport.injectRedirectTo, loginPassport.loginPassportOidcCallback     , loginPassport.loginFailureForExternalAccount);
+  app.post('/passport/saml/callback'              , addActivity, loginPassport.injectRedirectTo, loginPassport.loginPassportSamlCallback, loginPassport.loginFailureForExternalAccount);
 
 
   app.post('/_api/login/testLdap'    , loginRequiredStrictly , loginFormValidator.loginRules() , loginFormValidator.loginValidation , loginPassport.testLdapCredentials);
   app.post('/_api/login/testLdap'    , loginRequiredStrictly , loginFormValidator.loginRules() , loginFormValidator.loginValidation , loginPassport.testLdapCredentials);
 
 

+ 17 - 7
packages/app/src/server/routes/login-passport.js

@@ -6,6 +6,7 @@ import { SupportedAction } from '~/interfaces/activity';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { ExternalAccountLoginError } from '~/models/vo/external-account-login-error';
 import { ExternalAccountLoginError } from '~/models/vo/external-account-login-error';
 import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
+import { createRedirectToForUnauthenticated } from '~/server/util/createRedirectToForUnauthenticated';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
@@ -16,7 +17,6 @@ module.exports = function(crowi, app) {
   const logger = loggerFactory('growi:routes:login-passport');
   const logger = loggerFactory('growi:routes:login-passport');
   const passport = require('passport');
   const passport = require('passport');
   const ExternalAccount = crowi.model('ExternalAccount');
   const ExternalAccount = crowi.model('ExternalAccount');
-  const User = crowi.model('User');
   const passportService = crowi.passportService;
   const passportService = crowi.passportService;
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
@@ -120,17 +120,26 @@ module.exports = function(crowi, app) {
 
 
     await crowi.activityService.createActivity(parameters);
     await crowi.activityService.createActivity(parameters);
 
 
+    const redirectToForUnauthenticated = createRedirectToForUnauthenticated(req.user.status);
+    const redirectTo = redirectToForUnauthenticated ?? res.locals.redirectTo ?? '/';
+
     if (isExternalAccount) {
     if (isExternalAccount) {
-      return res.redirect('/');
+      return res.redirect(redirectTo);
     }
     }
 
 
-    // check for redirection to '/invited'
-    const redirectTo = req.user.status === User.STATUS_INVITED ? '/invited' : req.session.redirectTo;
+    return res.apiv3({ redirectTo });
+  };
+
+  const injectRedirectTo = (req, res, next) => {
 
 
-    // remove session.redirectTo
-    delete req.session.redirectTo;
+    // Move "req.session.redirectTo" to "res.locals.redirectTo"
+    // Because the session is regenerated when req.login() is called
+    const redirectTo = req.session.redirectTo;
+    if (redirectTo != null) {
+      res.locals.redirectTo = redirectTo;
+    }
 
 
-    return res.apiv3({ redirectTo, userStatus: req.user.status });
+    next();
   };
   };
 
 
   const isEnableLoginWithLocalOrLdap = (req, res, next) => {
   const isEnableLoginWithLocalOrLdap = (req, res, next) => {
@@ -595,6 +604,7 @@ module.exports = function(crowi, app) {
 
 
   return {
   return {
     cannotLoginErrorHadnler,
     cannotLoginErrorHadnler,
+    injectRedirectTo,
     isEnableLoginWithLocalOrLdap,
     isEnableLoginWithLocalOrLdap,
     loginFailure,
     loginFailure,
     loginFailureForExternalAccount,
     loginFailureForExternalAccount,

+ 17 - 0
packages/app/src/server/util/createRedirectToForUnauthenticated.ts

@@ -0,0 +1,17 @@
+import { USER_STATUS } from '~/interfaces/user';
+import type { IUserStatus } from '~/interfaces/user';
+
+export const createRedirectToForUnauthenticated = (userStatus: IUserStatus): string | null => {
+  switch (userStatus) {
+    case USER_STATUS.REGISTERED:
+      return '/login/error/registered';
+    case USER_STATUS.SUSPENDED:
+      return '/login/error/suspended';
+    case USER_STATUS.INVITED:
+      return '/invited';
+    case USER_STATUS.DELETED:
+      return '/login';
+    default:
+      return null;
+  }
+};

+ 2 - 1
packages/app/src/server/util/mongoose-utils.ts

@@ -1,4 +1,5 @@
-import mongoose, {
+import mongoose from 'mongoose';
+import type {
   Model, Document, Schema, ConnectOptions,
   Model, Document, Schema, ConnectOptions,
 } from 'mongoose';
 } from 'mongoose';
 
 

+ 1 - 6
packages/app/src/stores/editor.tsx

@@ -10,7 +10,6 @@ import { IEditorSettings } from '~/interfaces/editor-settings';
 import { SlackChannels } from '~/interfaces/user-trigger-notification';
 import { SlackChannels } from '~/interfaces/user-trigger-notification';
 
 
 import {
 import {
-  useCurrentPathname,
   useCurrentUser, useDefaultIndentSize, useIsGuestUser,
   useCurrentUser, useDefaultIndentSize, useIsGuestUser,
 } from './context';
 } from './context';
 // import { localStorageMiddleware } from './middlewares/sync-to-storage';
 // import { localStorageMiddleware } from './middlewares/sync-to-storage';
@@ -19,11 +18,7 @@ import { useStaticSWR } from './use-static-swr';
 
 
 
 
 export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Error> => {
 export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Error> => {
-  // need to include useCurrentPathname not useCurrentPagePath
-  // https://github.com/weseek/growi/pull/7301
-  const { data: currentPagePath } = useCurrentPathname();
-
-  return useStaticSWR(['editingMarkdown', currentPagePath], initialData);
+  return useStaticSWR('editingMarkdown', initialData);
 };
 };
 
 
 
 

+ 4 - 9
packages/app/src/stores/ui.tsx

@@ -22,10 +22,9 @@ import type { UpdateDescCountData } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import {
 import {
-  useCurrentPageId, useIsEditable, useIsGuestUser,
+  useCurrentPageId, useIsEditable,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId, useIsNotFound,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId, useIsNotFound,
 } from './context';
 } from './context';
-import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { useCurrentPagePath, useIsTrashPage } from './page';
 import { useCurrentPagePath, useIsTrashPage } from './page';
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
@@ -235,18 +234,14 @@ type PreferDrawerModeByUserUtils = {
 }
 }
 
 
 export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWithUtils<PreferDrawerModeByUserUtils, boolean> => {
 export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWithUtils<PreferDrawerModeByUserUtils, boolean> => {
-  const { data: isGuestUser } = useIsGuestUser();
   const { scheduleToPut } = useUserUISettings();
   const { scheduleToPut } = useUserUISettings();
 
 
-  const swrResponse: SWRResponse<boolean, Error> = useStaticSWR('preferDrawerModeByUser', initialData, { use: isGuestUser ? [localStorageMiddleware] : [] });
+  const swrResponse: SWRResponse<boolean, Error> = useStaticSWR('preferDrawerModeByUser', initialData);
 
 
   const utils: PreferDrawerModeByUserUtils = {
   const utils: PreferDrawerModeByUserUtils = {
     update: (preferDrawerMode: boolean) => {
     update: (preferDrawerMode: boolean) => {
       swrResponse.mutate(preferDrawerMode);
       swrResponse.mutate(preferDrawerMode);
-
-      if (!isGuestUser) {
-        scheduleToPut({ preferDrawerModeByUser: preferDrawerMode });
-      }
+      scheduleToPut({ preferDrawerModeByUser: preferDrawerMode });
     },
     },
   };
   };
 
 
@@ -292,7 +287,7 @@ export const useDrawerMode = (): SWRResponse<boolean, Error> => {
   };
   };
 
 
   const isViewModeWithPreferDrawerMode = editorMode === EditorMode.View && preferDrawerModeByUser;
   const isViewModeWithPreferDrawerMode = editorMode === EditorMode.View && preferDrawerModeByUser;
-  const isEditModeWithPreferDrawerMode = editorMode === EditorMode.Editor && preferDrawerModeOnEditByUser;
+  const isEditModeWithPreferDrawerMode = editorMode !== EditorMode.View && preferDrawerModeOnEditByUser;
   const isDrawerModeFixed = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
   const isDrawerModeFixed = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
 
 
   return useSWRImmutable(
   return useSWRImmutable(

+ 5 - 12
packages/app/src/stores/use-context-swr.tsx

@@ -1,9 +1,10 @@
 import assert from 'assert';
 import assert from 'assert';
 
 
 import {
 import {
-  Key, SWRConfiguration, SWRResponse, useSWRConfig,
+  Key, SWRConfiguration, SWRResponse,
 } from 'swr';
 } from 'swr';
-import useSWRImmutable from 'swr/immutable';
+
+import { useStaticSWR } from './use-static-swr';
 
 
 
 
 export function useContextSWR<Data, Error>(key: Key): SWRResponse<Data, Error>;
 export function useContextSWR<Data, Error>(key: Key): SWRResponse<Data, Error>;
@@ -20,17 +21,9 @@ export function useContextSWR<Data, Error>(
 
 
   assert.notStrictEqual(configuration?.fetcher, null, 'useContextSWR does not support \'configuration.fetcher\'');
   assert.notStrictEqual(configuration?.fetcher, null, 'useContextSWR does not support \'configuration.fetcher\'');
 
 
-  const { cache } = useSWRConfig();
-  const swrResponse = useSWRImmutable(key, null, {
-    ...configuration,
-    fallbackData: configuration?.fallbackData ?? cache.get(key)?.data,
-  });
-
-  // write data to cache directly
-  if (data !== undefined) {
-    cache.set(key, { ...cache.get(key), data });
-  }
+  const swrResponse = useStaticSWR(key, data, configuration);
 
 
+  // overwrite mutate
   const result = Object.assign(swrResponse, { mutate: () => { throw Error('mutate can not be used in context') } });
   const result = Object.assign(swrResponse, { mutate: () => { throw Error('mutate can not be used in context') } });
 
 
   return result;
   return result;

+ 7 - 2
packages/app/src/stores/use-next-themes.tsx

@@ -24,17 +24,22 @@ type UseThemeExtendedProps = Omit<UseThemeProps, 'theme'|'resolvedTheme'> & {
   resolvedTheme: ColorScheme,
   resolvedTheme: ColorScheme,
   useOsSettings: boolean,
   useOsSettings: boolean,
   isDarkMode: boolean,
   isDarkMode: boolean,
+  isForcedByGrowiTheme: boolean,
   resolvedThemeByAttributes?: ColorScheme,
   resolvedThemeByAttributes?: ColorScheme,
 }
 }
 
 
 export const useNextThemes = (): UseThemeProps & UseThemeExtendedProps => {
 export const useNextThemes = (): UseThemeProps & UseThemeExtendedProps => {
   const props = useTheme();
   const props = useTheme();
+  const { data: forcedColorScheme } = useForcedColorScheme();
+
+  const resolvedTheme = forcedColorScheme ?? props.resolvedTheme as ColorScheme;
 
 
   return Object.assign(props, {
   return Object.assign(props, {
     theme: props.theme as Themes,
     theme: props.theme as Themes,
-    resolvedTheme: props.resolvedTheme as ColorScheme,
+    resolvedTheme,
     useOsSettings: props.theme === Themes.SYSTEM,
     useOsSettings: props.theme === Themes.SYSTEM,
-    isDarkMode: props.resolvedTheme === ColorScheme.DARK,
+    isDarkMode: resolvedTheme === ColorScheme.DARK,
+    isForcedByGrowiTheme: forcedColorScheme != null,
     resolvedThemeByAttributes: isClient() ? document.documentElement.getAttribute(ATTRIBUTE) as ColorScheme : undefined,
     resolvedThemeByAttributes: isClient() ? document.documentElement.getAttribute(ATTRIBUTE) as ColorScheme : undefined,
   });
   });
 };
 };

+ 10 - 10
packages/app/src/stores/use-static-swr.tsx

@@ -1,9 +1,7 @@
-import { useEffect } from 'react';
-
 import assert from 'assert';
 import assert from 'assert';
 
 
 import {
 import {
-  mutate, Key, SWRConfiguration, SWRResponse,
+  Key, SWRConfiguration, SWRResponse, useSWRConfig,
 } from 'swr';
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
@@ -22,14 +20,16 @@ export function useStaticSWR<Data, Error>(
 
 
   assert.notStrictEqual(configuration?.fetcher, null, 'useStaticSWR does not support \'configuration.fetcher\'');
   assert.notStrictEqual(configuration?.fetcher, null, 'useStaticSWR does not support \'configuration.fetcher\'');
 
 
-  const swrResponse = useSWRImmutable(key, null, configuration);
+  const { cache } = useSWRConfig();
+  const swrResponse = useSWRImmutable(key, null, {
+    ...configuration,
+    fallbackData: configuration?.fallbackData ?? cache.get(key)?.data,
+  });
 
 
-  // Do mutate with `data` from args
-  useEffect(() => {
-    if (data !== undefined) {
-      mutate(key, data);
-    }
-  }, [data, key]);
+  // write data to cache directly
+  if (data !== undefined) {
+    cache.set(key, { ...cache.get(key), data });
+  }
 
 
   return swrResponse;
   return swrResponse;
 }
 }

+ 8 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -360,6 +360,14 @@ ul.pagination {
   }
   }
 }
 }
 
 
+.grw-page-accessories-modal,.grw-descendants-page-list-modal {
+  .modal-header {
+    .close {
+      color: #{hsl.contrast(var(--bgcolor-global))};
+    }
+  }
+}
+
 .grw-custom-nav-tab {
 .grw-custom-nav-tab {
   .nav-item {
   .nav-item {
     &:hover,
     &:hover,

+ 1 - 1
packages/app/test/integration/middlewares/login-required.test.js

@@ -286,7 +286,7 @@ describe('loginRequired', () => {
       expect(res.redirect).toHaveBeenCalledTimes(1);
       expect(res.redirect).toHaveBeenCalledTimes(1);
       expect(res.redirect).toHaveBeenCalledWith('/login');
       expect(res.redirect).toHaveBeenCalledWith('/login');
       expect(result).toBe('redirect');
       expect(result).toBe('redirect');
-      expect(req.session.redirectTo).toBe('original url 1');
+      expect(req.session.redirectTo).toBe(undefined);
     });
     });
 
 
   });
   });

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/codemirror-textlint",
   "name": "@growi/codemirror-textlint",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "scripts": {
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/core",
   "name": "@growi/core",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "description": "GROWI Core Libraries",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/hackmd",
   "name": "@growi/hackmd",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",

+ 4 - 9
packages/presentation/package.json

@@ -1,16 +1,11 @@
 {
 {
   "name": "@growi/presentation",
   "name": "@growi/presentation",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "description": "GROWI plugin for presentation",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "license": "MIT",
-  "keywords": [
-    "growi",
-    "growi-plugin"
-  ],
+  "keywords": ["growi", "growi-plugin"],
   "main": "dist/index.js",
   "main": "dist/index.js",
-  "files": [
-    "dist"
-  ],
+  "files": ["dist"],
   "scripts": {
   "scripts": {
     "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
     "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
     "clean": "npx -y shx rm -rf dist",
     "clean": "npx -y shx rm -rf dist",
@@ -20,7 +15,7 @@
     "test": ""
     "test": ""
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.6-RC.0"
+    "@growi/core": "^6.0.8-RC.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@marp-team/marp-core": "^3.4.2",
     "@marp-team/marp-core": "^3.4.2",

+ 9 - 3
packages/presentation/src/components/Slides.tsx

@@ -32,9 +32,15 @@ type Props = {
 
 
 export const Slides = (props: Props): JSX.Element => {
 export const Slides = (props: Props): JSX.Element => {
   const { options, children } = props;
   const { options, children } = props;
-  const { rendererOptions, isDarkMode } = options;
-
-  rendererOptions.remarkPlugins?.push([extractSections.remarkPlugin, { isDarkMode }]);
+  const { rendererOptions, isDarkMode, disableSeparationByHeader } = options;
+
+  rendererOptions.remarkPlugins?.push([
+    extractSections.remarkPlugin,
+    {
+      isDarkMode,
+      disableSeparationByHeader,
+    },
+  ]);
 
 
   const { css } = marp.render('', { htmlAsArray: true });
   const { css } = marp.render('', { htmlAsArray: true });
 
 

+ 1 - 0
packages/presentation/src/consts/index.ts

@@ -5,4 +5,5 @@ export type PresentationOptions = {
   rendererOptions: ReactMarkdownOptions,
   rendererOptions: ReactMarkdownOptions,
   revealOptions?: RevealOptions,
   revealOptions?: RevealOptions,
   isDarkMode?: boolean,
   isDarkMode?: boolean,
+  disableSeparationByHeader?: boolean,
 }
 }

+ 19 - 4
packages/presentation/src/services/renderer/extract-sections.ts

@@ -37,33 +37,48 @@ function removeElement(parentNode: Parent, elem: Node): void {
 
 
 export type ExtractSectionsPluginParams = {
 export type ExtractSectionsPluginParams = {
   isDarkMode?: boolean,
   isDarkMode?: boolean,
+  disableSeparationByHeader?: boolean,
 }
 }
 
 
 export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) => {
 export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) => {
-  const { isDarkMode } = options;
+  const { isDarkMode, disableSeparationByHeader } = options;
+
+  const startCondition = (node: Node) => {
+    if (!disableSeparationByHeader && node.type === 'heading') {
+      return true;
+    }
+    return node.type !== 'thematicBreak';
+  };
+  const endCondition = (node: Node) => {
+    if (!disableSeparationByHeader && node.type === 'heading') {
+      return true;
+    }
+    return node.type === 'thematicBreak';
+  };
 
 
   return (tree) => {
   return (tree) => {
     // wrap with <section>
     // wrap with <section>
     visit(
     visit(
       tree,
       tree,
-      node => node.type !== 'thematicBreak',
+      startCondition,
       (node, index, parent: Parent) => {
       (node, index, parent: Parent) => {
         if (parent == null || parent.type !== 'root') {
         if (parent == null || parent.type !== 'root') {
           return;
           return;
         }
         }
 
 
         const startElem = node;
         const startElem = node;
-        const endElem = findAfter(parent, startElem, node => node.type === 'thematicBreak');
+        const endElem = findAfter(parent, startElem, endCondition);
 
 
         wrapWithSection(parent, startElem, endElem, isDarkMode);
         wrapWithSection(parent, startElem, endElem, isDarkMode);
 
 
         // remove <hr>
         // remove <hr>
-        if (endElem != null) {
+        if (endElem != null && endElem.type === 'thematicBreak') {
           removeElement(parent, endElem);
           removeElement(parent, endElem);
         }
         }
       },
       },
     );
     );
   };
   };
+
 };
 };
 
 
 
 

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@growi/preset-themes",
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
   "description": "GROWI preset themes",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/libs/index.js",
   "main": "dist/libs/index.js",
   "files": [
   "files": [

+ 5 - 0
packages/preset-themes/src/styles/mono-blue.scss

@@ -91,6 +91,11 @@
   // Sidebar list group
   // Sidebar list group
   // --bgcolor-sidebar-list-group: #; // optional
   // --bgcolor-sidebar-list-group: #; // optional
 
 
+  // Subnavigation
+  --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
+  --bgcolor-subnav-hs: var(--bgcolor-global-hs);
+  --bgcolor-subnav-l: calc(var(--bgcolor-global-l) - 3%);
+
   // Icon colors
   // Icon colors
   --color-editor-icons: var(--color-global);
   --color-editor-icons: var(--color-global);
 
 

+ 1 - 1
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-drawio",
   "name": "@growi/remark-drawio",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-growi-directive",
   "name": "@growi/remark-growi-directive",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 4 - 4
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-lsx",
   "name": "@growi/remark-lsx",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "description": "GROWI plugin to list pages",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
   "keywords": ["growi", "growi-plugin"],
@@ -20,9 +20,9 @@
     "test": ""
     "test": ""
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.6-RC.0",
-    "@growi/remark-growi-directive": "^6.0.6-RC.0",
-    "@growi/ui": "^6.0.6-RC.0",
+    "@growi/core": "^6.0.8-RC.0",
+    "@growi/remark-growi-directive": "^6.0.8-RC.0",
+    "@growi/ui": "^6.0.8-RC.0",
     "swr": "^2.0.3"
     "swr": "^2.0.3"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slack",
   "name": "@growi/slack",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slackbot-proxy",
   "name": "@growi/slackbot-proxy",
-  "version": "6.0.6-slackbot-proxy.0",
+  "version": "6.0.8-slackbot-proxy.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -26,7 +26,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.0.6-RC.0",
+    "@growi/slack": "^6.0.8-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",
     "@tsed/common": "^6.43.0",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/ui",
   "name": "@growi/ui",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "description": "GROWI UI Libraries",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": ["growi"],
   "keywords": ["growi"],
@@ -17,7 +17,7 @@
     "test": "jest --verbose"
     "test": "jest --verbose"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.6-RC.0"
+    "@growi/core": "^6.0.8-RC.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",

+ 2 - 1
packages/ui/src/index.ts

@@ -3,7 +3,8 @@ export * from './interfaces/breakpoints';
 export * from './components/Attachment/Attachment';
 export * from './components/Attachment/Attachment';
 export * from './components/PagePath/PageListMeta';
 export * from './components/PagePath/PageListMeta';
 export * from './components/PagePath/PagePathLabel';
 export * from './components/PagePath/PagePathLabel';
+export * from './components/SearchPage/FootstampIcon';
 export * from './components/User/UserPicture';
 export * from './components/User/UserPicture';
 
 
 export * from './utils/browser-utils';
 export * from './utils/browser-utils';
-export * from './components/SearchPage/FootstampIcon';
+export * from './utils/use-fullscreen';

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