فهرست منبع

Merge branch 'master' into merge-master-to-questionnaire-branch

Futa Arai 3 سال پیش
والد
کامیت
0f6ed5177e
100فایلهای تغییر یافته به همراه686 افزوده شده و 677 حذف شده
  1. 51 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. 1 6
      packages/app/src/client/services/user-ui-settings.ts
  46. 5 11
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  47. 1 6
      packages/app/src/components/LoginForm.tsx
  48. 44 39
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  49. 3 3
      packages/app/src/components/PageAlert/PageGrantAlert.tsx
  50. 11 6
      packages/app/src/components/PageComment/CommentEditor.tsx
  51. 6 2
      packages/app/src/components/PageEditor/ConflictDiffModal.tsx
  52. 21 8
      packages/app/src/components/PagePresentationModal.tsx
  53. 13 7
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  54. 20 4
      packages/app/src/components/UnsavedAlertDialog.tsx
  55. 2 0
      packages/app/src/interfaces/crowi-request.ts
  56. 3 1
      packages/app/src/interfaces/user.ts
  57. 36 63
      packages/app/src/pages/[[...path]].page.tsx
  58. 0 3
      packages/app/src/pages/_app.page.tsx
  59. 2 29
      packages/app/src/pages/_private-legacy-pages.page.tsx
  60. 20 30
      packages/app/src/pages/_search.page.tsx
  61. 2 22
      packages/app/src/pages/me/[[...path]].page.tsx
  62. 18 31
      packages/app/src/pages/tags.page.tsx
  63. 17 30
      packages/app/src/pages/trash.page.tsx
  64. 15 0
      packages/app/src/pages/utils/commons.ts
  65. 0 5
      packages/app/src/server/crowi/express-init.js
  66. 0 37
      packages/app/src/server/middlewares/inject-user-ui-settings-to-localvars.ts
  67. 4 9
      packages/app/src/server/middlewares/login-required.js
  68. 0 86
      packages/app/src/server/models/config.ts
  69. 2 2
      packages/app/src/server/models/user-ui-settings.ts
  70. 1 1
      packages/app/src/server/routes/apiv3/index.js
  71. 15 2
      packages/app/src/server/routes/apiv3/user-ui-settings.ts
  72. 28 13
      packages/app/src/server/routes/apiv3/users.js
  73. 6 6
      packages/app/src/server/routes/index.js
  74. 17 7
      packages/app/src/server/routes/login-passport.js
  75. 17 0
      packages/app/src/server/util/createRedirectToForUnauthenticated.ts
  76. 2 1
      packages/app/src/server/util/mongoose-utils.ts
  77. 4 2
      packages/app/src/services/renderer/renderer.tsx
  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. 1 1
      packages/app/test/integration/middlewares/login-required.test.js
  84. 1 1
      packages/codemirror-textlint/package.json
  85. 1 1
      packages/core/package.json
  86. 1 1
      packages/hackmd/package.json
  87. 4 9
      packages/presentation/package.json
  88. 9 3
      packages/presentation/src/components/Slides.tsx
  89. 1 0
      packages/presentation/src/consts/index.ts
  90. 19 4
      packages/presentation/src/services/renderer/extract-sections.ts
  91. 1 1
      packages/preset-themes/package.json
  92. 5 0
      packages/preset-themes/src/styles/mono-blue.scss
  93. 1 1
      packages/remark-drawio/package.json
  94. 1 1
      packages/remark-growi-directive/package.json
  95. 4 4
      packages/remark-lsx/package.json
  96. 1 1
      packages/slack/package.json
  97. 2 2
      packages/slackbot-proxy/package.json
  98. 2 2
      packages/ui/package.json
  99. 2 1
      packages/ui/src/index.ts
  100. 50 0
      packages/ui/src/utils/use-fullscreen.ts

+ 51 - 1
CHANGELOG.md

@@ -1,9 +1,59 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.5...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.6...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [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
 
 ### 🚀 Improvement

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

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

@@ -10,7 +10,7 @@ GROWI Official docker image
 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.6`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.6/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 11 - 11
packages/app/package.json

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

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

@@ -751,9 +751,9 @@
     },
     "reset_password": "Reset Password",
     "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",
       "new_password": "New Password"
     },

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

@@ -332,7 +332,7 @@
     "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."
     },
-    "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": {
     "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

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

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

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

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

@@ -759,9 +759,9 @@
     },
     "reset_password": "重置密码",
     "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",
       "new_password": "New Password"
     },

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

@@ -321,7 +321,7 @@
 		"notice": {
 			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
 		},
-    "changes_not_saved": "您所做的更改可能不会保存。"
+    "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
   },
   "page_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:
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 
 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:
 
-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:
-<%- 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
 
-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.
 
 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
 
-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.
 
-<%- 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.

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

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

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

@@ -1,12 +1,12 @@
 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.
 
-<%- 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.

+ 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:
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 
 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:
 
-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:
-<%- 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アドレスで再度お試しください。
 
 もしこのリクエストに心当たりがない場合は、このメールを無視してください。
 
 -------------------------------------------------------------------------
 
-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:
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 
 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:
 
-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:
-<%- 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 }}失效。
 

+ 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 %>失效。
 
 如果您尚未创建,请忽略此电子邮件。

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

@@ -1,11 +1,9 @@
 // eslint-disable-next-line no-restricted-imports
 import { AxiosResponse } from 'axios';
-
 import { debounce } from 'throttle-debounce';
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import { useIsGuestUser } from '~/stores/context';
 
 let settingsForBulk: Partial<IUserUISettings> = {};
 const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> => {
@@ -33,11 +31,8 @@ type UserUISettingsUtil = {
   scheduleToPut: ScheduleToPutFunction | (() => void),
 }
 export const useUserUISettings = (): UserUISettingsUtil => {
-  const { data: isGuestUser } = useIsGuestUser();
 
   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 { apiv3Put } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class PasswordResetModal extends React.Component {
 
@@ -17,7 +16,6 @@ class PasswordResetModal extends React.Component {
     super(props);
 
     this.state = {
-      temporaryPassword: [],
       isPasswordResetDone: false,
     };
 
@@ -27,9 +25,8 @@ class PasswordResetModal extends React.Component {
   async resetPassword() {
     const { t, userForPasswordResetModal } = this.props;
     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) {
       toastError(err, t('toaster.failed_to_reset_password'));
@@ -42,8 +39,8 @@ class PasswordResetModal extends React.Component {
     return (
       <>
         <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>
           {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
@@ -57,13 +54,10 @@ class PasswordResetModal extends React.Component {
 
     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>
           {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
         </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,
 } from 'react';
 
-import { USER_STATUS } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
@@ -95,16 +94,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
     try {
       const res = await apiv3Post('/login', { loginForm });
-      const { redirectTo, userStatus } = res.data;
+      const { redirectTo } = res.data;
 
       if (redirectTo != null) {
         return router.push(redirectTo);
       }
 
-      if (userStatus !== USER_STATUS.ACTIVE) {
-        window.location.href = '/';
-      }
-
       return router.push('/');
     }
     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 {
-    setTheme, resolvedTheme, useOsSettings, isDarkMode,
+    setTheme, resolvedTheme, useOsSettings, isDarkMode, isForcedByGrowiTheme,
   } = useNextThemes();
   const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
@@ -114,55 +114,60 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
 
         {/* sidebar mode */}
         {renderSidebarModeSwitch(false)}
-        {dropdownDivider}
 
         {/* side bar mode on editor */}
         {isAuthenticated && (
           <>
-            {renderSidebarModeSwitch(true)}
             {dropdownDivider}
+            {renderSidebarModeSwitch(true)}
           </>
         )}
 
         {/* 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>
-              <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>
-        </form>
+            </form>
+          </>
+        ) }
+
       </div>
 
     </>

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

@@ -18,21 +18,21 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 2) {
         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) {
         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) {
         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);
   }, []);
 
+  // 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(() => {
-    if (slackChannelsData != null) {
-      setSlackChannels(slackChannelsData.toString());
-      mutateIsSlackEnabled(false);
-    }
-  }, [mutateIsSlackEnabled, slackChannelsData]);
+    initializeSlackEnabled();
+  }, [initializeSlackEnabled]);
 
   const isSlackEnabledToggleHandler = (isSlackEnabled: boolean) => {
     mutateIsSlackEnabled(isSlackEnabled, false);
@@ -104,10 +108,11 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     setComment('');
     setActiveTab('comment_editor');
     setError(undefined);
+    initializeSlackEnabled();
     // reset value
     if (editorRef.current == null) { return }
     editorRef.current.setValue('');
-  }, []);
+  }, [initializeSlackEnabled]);
 
   const cancelButtonClickedHandler = useCallback(() => {
     // change state to not ready

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

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

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

@@ -1,13 +1,13 @@
 import React, { useCallback } from 'react';
 
 import type { PresentationProps } from '@growi/presentation';
+import { useFullScreen } from '@growi/ui';
 import dynamic from 'next/dynamic';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import {
   Modal, ModalBody,
 } from 'reactstrap';
 
-
 import { usePagePresentationModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { usePresentationViewOptions } from '~/stores/renderer';
@@ -30,13 +30,26 @@ const PagePresentationModal = (): JSX.Element => {
   const { data: presentationModalData, close: closePresentationModal } = usePagePresentationModal();
 
   const { isDarkMode } = useNextThemes();
+  const fullscreen = useFullScreen();
 
   const { data: currentPage } = useSWRxCurrentPage();
   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;
 
@@ -49,15 +62,15 @@ const PagePresentationModal = (): JSX.Element => {
   return (
     <Modal
       isOpen={isOpen}
-      toggle={closePresentationModal}
+      toggle={closeHandler}
       data-testid="page-presentation-modal"
       className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
     >
       <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 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>
         </button>
       </div>

+ 13 - 7
packages/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -28,9 +28,10 @@ type Props = Omit<LinkProps, 'href'> & {
   className?: string,
 };
 
-export const NextLink = ({
-  href, children, className, ...props
-}: Props): JSX.Element => {
+export const NextLink = (props: Props): JSX.Element => {
+  const {
+    href, children, className, ...rest
+  } = props;
 
   const { data: siteUrl } = useSiteUrl();
 
@@ -38,24 +39,29 @@ export const NextLink = ({
     return <a className={className}>{children}</a>;
   }
 
+  // extract 'data-*' props
+  const dataAttributes = Object.fromEntries(
+    Object.entries(rest).filter(([key]) => key.startsWith('data-')),
+  );
+
   // when href is an anchor link
   if (isAnchorLink(href)) {
     return (
-      <a href={href} className={className}>{children}</a>
+      <a href={href} className={className} {...dataAttributes}>{children}</a>
     );
   }
 
   if (isExternalLink(href, siteUrl)) {
     return (
-      <a href={href} className={className} target="_blank" rel="noopener noreferrer">
+      <a href={href} className={className} target="_blank" rel="noopener noreferrer" {...dataAttributes}>
         {children}&nbsp;<i className='icon-share-alt small'></i>
       </a>
     );
   }
 
   return (
-    <Link {...props} href={href} prefetch={false}>
-      <a href={href} className={className}>{children}</a>
+    <Link {...rest} href={href} prefetch={false}>
+      <a href={href} className={className} {...dataAttributes}>{children}</a>
     </Link>
   );
 };

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

@@ -8,7 +8,7 @@ import { useIsEnabledUnsavedWarning } from '~/stores/editor';
 const UnsavedAlertDialog = (): JSX.Element => {
   const { t } = useTranslation();
   const router = useRouter();
-  const { data: isEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { data: isEnabledUnsavedWarning, mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
 
   const alertUnsavedWarningByBrowser = useCallback((e) => {
     if (isEnabledUnsavedWarning) {
@@ -23,12 +23,20 @@ const UnsavedAlertDialog = (): JSX.Element => {
 
   const alertUnsavedWarningByNextRouter = useCallback(() => {
     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]);
 
+  const onRouterChangeComplete = useCallback(() => {
+    mutateIsEnabledUnsavedWarning(false);
+  }, [mutateIsEnabledUnsavedWarning]);
+
   /*
   * Route changes by Browser
   * Example: window.location.href, F5
@@ -53,6 +61,14 @@ const UnsavedAlertDialog = (): JSX.Element => {
   }, [alertUnsavedWarningByNextRouter, router.events]);
 
 
+  useEffect(() => {
+    router.events.on('routeChangeComplete', onRouterChangeComplete);
+    return () => {
+      router.events.off('routeChangeComplete', onRouterChangeComplete);
+    };
+  }, [onRouterChangeComplete, router.events]);
+
+
   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
   crowi: any,
 
+  session: any,
+
   // provided by csurf
   csrfToken: () => string,
 

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

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

+ 36 - 63
packages/app/src/pages/[[...path]].page.tsx

@@ -10,7 +10,7 @@ import type {
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUserHasId,
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
-import {
+import type {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@@ -27,44 +27,38 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { EditorConfig } from '~/interfaces/editor-settings';
 import type { IPageGrantData } from '~/interfaces/page';
 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 { 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 { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 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 loggerFactory from '~/utils/logger';
 
-import { DescendantsPageListModal } from '../components/DescendantsPageListModal';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
 import type { GrowiSubNavigationSwitcherProps } from '../components/Navbar/GrowiSubNavigationSwitcher';
 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 {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
 } from './utils/commons';
 
 
@@ -74,6 +68,7 @@ declare global {
 }
 
 
+const DescendantsPageListModal = dynamic(() => import('../components/DescendantsPageListModal').then(mod => mod.DescendantsPageListModal), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() => import('../components/Navbar/GrowiSubNavigationSwitcher')
   .then(mod => mod.GrowiSubNavigationSwitcher), { ssr: false });
@@ -178,31 +173,22 @@ type Props = CommonProps & {
   grantData?: IPageGrantData,
 
   rendererConfig: RendererConfig,
-
-  // UI
-  userUISettings?: IUserUISettings
-  // Sidebar
-  sidebarConfig: ISidebarConfig,
 };
 
 const Page: NextPageWithLayout<Props> = (props: Props) => {
-  // const { t } = useTranslation();
-  const router = useRouter();
-
-  const { data: currentUser } = useCurrentUser(props.currentUser ?? null);
-
   // register global EventEmitter
   if (isClient() && window.globalEmitter == null) {
     window.globalEmitter = new EventEmitter();
   }
 
+  const router = useRouter();
+
+  useCurrentUser(props.currentUser ?? null);
+
   // commons
   useEditorConfig(props.editorConfig);
   useCsrfToken(props.csrfToken);
 
-  // init sidebar config with UserUISettings and sidebarConfig
-  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
-
   // page
   useIsLatestRevision(props.isLatestRevision);
   useIsContainerFluid(props.isContainerFluid);
@@ -242,7 +228,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsUploadableFile(props.editorConfig.upload.isUploadableFile);
   useIsUploadableImage(props.editorConfig.upload.isUploadableImage);
 
-  const { pageWithMeta, userUISettings } = props;
+  const { pageWithMeta } = props;
 
   const pageId = pageWithMeta?.data._id;
   const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
@@ -287,9 +273,12 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   }, [props.currentPathname, router]);
 
   // initialize mutateEditingMarkdown only once per page
+  // need to include useCurrentPathname not useCurrentPagePath
   useEffect(() => {
-    mutateEditingMarkdown(revisionBody);
-  }, [mutateEditingMarkdown, revisionBody]);
+    if (props.currentPathname != null) {
+      mutateEditingMarkdown(revisionBody);
+    }
+  }, [mutateEditingMarkdown, revisionBody, props.currentPathname]);
 
   const title = generateCustomTitleForPage(props, pagePath);
 
@@ -330,14 +319,16 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
-type LayoutProps = {
+type LayoutProps = Props & {
   children?: ReactNode
-  className?: string
 }
 
-const Layout = ({ children }: LayoutProps): JSX.Element => {
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
   const className = useEditorModeClassName();
 
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
+
   return (
     <BasicLayout className={className}>
       {children}
@@ -345,12 +336,12 @@ const Layout = ({ children }: LayoutProps): JSX.Element => {
   );
 };
 
-Page.getLayout = function getLayout(page) {
+Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
   return (
     <>
       <DrawioViewerScript />
 
-      <Layout>
+      <Layout {...page.props}>
         {page}
       </Layout>
       <UnsavedAlertDialog />
@@ -461,19 +452,6 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   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> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
@@ -532,7 +510,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const {
-    appService, searchService, configManager, aclService, slackNotificationService, mailService,
+    searchService, configManager, aclService,
   } = crowi;
 
   props.isSearchServiceConfigured = searchService.isConfigured;
@@ -579,10 +557,6 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 
-  props.sidebarConfig = {
-    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
-  };
 }
 
 /**
@@ -635,7 +609,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     }
   }
 
-  await injectUserUISettings(context, props);
   await injectRoutingInformation(context, props);
   injectServerConfigurations(context, props);
   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;
-  // useInterceptorManager(new InterceptorManager());
   useAppTitle(commonPageProps.appTitle);
   useSiteUrl(commonPageProps.siteUrl);
   useConfidential(commonPageProps.confidential);
@@ -68,6 +67,4 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   );
 }
 
-// export default appWithTranslation(GrowiApp);
-
 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 type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 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 {
   useCsrfToken, useCurrentUser, useDrawioUri, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig,
 } from '~/stores/context';
-import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
-  useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
 
+import type { CommonProps } from './utils/commons';
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
@@ -37,11 +31,6 @@ type Props = CommonProps & {
 
   drawioUri: string | null,
 
-  // UI
-  userUISettings?: IUserUISettings
-  // Sidebar
-  sidebarConfig: ISidebarConfig,
-
   // Render config
   rendererConfig: RendererConfig,
 
@@ -50,8 +39,6 @@ type Props = CommonProps & {
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation();
 
-  const { userUISettings } = props;
-
   const PrivateLegacyPages = dynamic(() => import('~/components/PrivateLegacyPages'), { ssr: false });
 
   // 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> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
@@ -168,7 +142,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = user.toObject();
   }
 
-  await injectUserUISettings(context, props);
   await injectServerConfigurations(context, props);
   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 { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@@ -8,24 +10,18 @@ import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 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 {
   useCsrfToken, useCurrentUser, useDrawioUri, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL,
 } from '~/stores/context';
-import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
-  useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
 
 import { SearchPage } from '../components/SearchPage';
 
 import type { NextPageWithLayout } from './_app.page';
+import type { CommonProps } from './utils/commons';
 import {
-  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, CommonProps, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 
 
@@ -38,11 +34,6 @@ type Props = CommonProps & {
 
   drawioUri: string | null,
 
-  // UI
-  userUISettings?: IUserUISettings
-  // Sidebar
-  sidebarConfig: ISidebarConfig,
-
   // Render config
   rendererConfig: RendererConfig,
 
@@ -54,8 +45,6 @@ type Props = CommonProps & {
 };
 
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
-  const { userUISettings } = props;
-
   const { t } = useTranslation();
 
   // 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) {
   return (
     <>
       <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 {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
@@ -190,7 +181,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = user.toObject();
   }
 
-  await injectUserUISettings(context, props);
   injectServerConfigurations(context, props);
   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 { IUserHasId } from '@growi/core';
-import { model as mongooseModel } from 'mongoose';
 import {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -14,23 +13,18 @@ import { useRouter } from 'next/router';
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 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 {
   useCurrentUser, useIsSearchPage,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useCsrfToken, useIsSearchScopeChildrenAsDefault,
   useRegistrationWhiteList, useShowPageLimitationXL, useRendererConfig,
 } from '~/stores/context';
-import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
 import { NextPageWithLayout } from '../_app.page';
+import type { CommonProps } from '../utils/commons';
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from '../utils/commons';
 
 
@@ -40,8 +34,6 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
-  userUISettings?: IUserUISettings
-  sidebarConfig: ISidebarConfig,
   rendererConfig: RendererConfig,
   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> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
@@ -221,7 +202,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = userData.toObject();
   }
 
-  await injectUserUISettings(context, props);
   await injectServerConfigurations(context, props);
   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 { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
@@ -9,14 +9,8 @@ import Head from 'next/head';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 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 {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-} from '~/stores/ui';
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
@@ -26,8 +20,9 @@ import {
 } from '../stores/context';
 
 import { NextPageWithLayout } from './_app.page';
+import type { CommonProps } from './utils/commons';
 import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, useInitSidebarConfig,
+  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 
 const PAGING_LIMIT = 10;
@@ -38,12 +33,6 @@ type Props = CommonProps & {
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
 
-  // ui
-  userUISettings?: IUserUISettings
-
-  // sidebar
-  sidebarConfig: ISidebarConfig,
-
   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) {
   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 {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
@@ -194,7 +182,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = user.toObject();
   }
 
-  await injectUserUISettings(context, props);
   injectServerConfigurations(context, props);
   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 { GetServerSideProps, GetServerSidePropsContext } from 'next';
@@ -9,12 +9,7 @@ import Head from 'next/head';
 import { GrowiSubNavigation } from '~/components/Navbar/GrowiSubNavigation';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 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 {
@@ -24,8 +19,9 @@ import {
 } from '../stores/context';
 
 import type { NextPageWithLayout } from './_app.page';
+import type { CommonProps } from './utils/commons';
 import {
-  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, CommonProps, useInitSidebarConfig,
+  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, useInitSidebarConfig,
 } from './utils/commons';
 
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
@@ -39,11 +35,6 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
   showPageLimitationXL: number,
 
-  // UI
-  userUISettings?: IUserUISettings
-  // Sidebar
-  sidebarConfig: ISidebarConfig,
-
   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) {
   return (
     <>
-      <BasicLayout>
+      <Layout {...page.props}>
         {page}
-      </BasicLayout>
+      </Layout>
       <EmptyTrashModal />
       <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 {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
@@ -181,7 +169,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   if (user != null) {
     props.currentUser = user.toObject();
   }
-  await injectUserUISettings(context, props);
   injectServerConfigurations(context, props);
   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 { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import {
   useCurrentProductNavWidth, useCurrentSidebarContents, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
 } from '~/stores/ui';
@@ -30,10 +31,13 @@ export type CommonProps = {
   isDefaultLogo: boolean,
   currentUser?: IUserHasId,
   forcedColorScheme?: ColorScheme,
+  sidebarConfig: ISidebarConfig,
+  userUISettings?: IUserUISettings
 } & Partial<SSRConfig>;
 
 // eslint-disable-next-line max-len
 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 { crowi, user } = req;
@@ -70,6 +74,12 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
   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 = {
     namespacesRequired: ['translation'],
     currentPathname,
@@ -85,6 +95,11 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     currentUser,
     isDefaultLogo,
     forcedColorScheme,
+    sidebarConfig: {
+      isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
+      isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    },
+    userUISettings: userUISettings?.toObject?.() ?? userUISettings,
   };
 
   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) => {
     const now = new Date();
     // for datez
-
-    const Page = crowi.model('Page');
-    const User = crowi.model('User');
-    const Config = mongoose.model('Config');
     app.set('tzoffset', crowi.appService.getTzoffset());
 
     res.locals.req = req;
     res.locals.baseUrl = crowi.appService.getSiteUrl();
     res.locals.env = env;
     res.locals.now = now;
-    res.locals.local_config = Config.getLocalconfig(crowi); // config for browser context
 
     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';
 
 const logger = loggerFactory('growi:middleware:login-required');
@@ -20,15 +21,9 @@ module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
         // Active の人だけ先に進める
         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

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

@@ -161,90 +161,4 @@ export const defaultNotificationConfigs: { [key: string]: any } = {
   '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);

+ 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 {
   Schema, Model, Document,
 } from 'mongoose';
 
 
 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';
 

+ 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);
 
   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);
 
   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();
 
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
 
   const validatorForPut = [
     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
-  router.put('/', loginRequiredStrictly, validatorForPut, apiV3FormValidator, async(req: any, res: any) => {
+  router.put('/', validatorForPut, apiV3FormValidator, async(req: any, res: any) => {
     const { user } = req;
     const { settings } = req.body;
 
@@ -37,6 +36,20 @@ module.exports = (crowi) => {
       preferDrawerModeByUser: settings.preferDrawerModeByUser,
       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
     Object.keys(updateData).forEach((key) => {
       if (updateData[key] == null) {

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

@@ -841,31 +841,46 @@ module.exports = (crowi) => {
    *        responses:
    *          200:
    *            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) => {
+    const { appService, mailService } = crowi;
     const { id } = req.body;
 
+    let newPassword;
+    let user;
+
     try {
-      const [newPassword, user] = await Promise.all([
+      [newPassword, user] = await Promise.all([
         await User.resetPasswordByRandomString(id),
         await User.findById(id)]);
 
       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) {
-      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
   app.get('/passport/google'                      , loginPassport.loginWithGoogle, 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);
 

+ 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 { ExternalAccountLoginError } from '~/models/vo/external-account-login-error';
 import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
+import { createRedirectToForUnauthenticated } from '~/server/util/createRedirectToForUnauthenticated';
 import loggerFactory from '~/utils/logger';
 
 
@@ -16,7 +17,6 @@ module.exports = function(crowi, app) {
   const logger = loggerFactory('growi:routes:login-passport');
   const passport = require('passport');
   const ExternalAccount = crowi.model('ExternalAccount');
-  const User = crowi.model('User');
   const passportService = crowi.passportService;
 
   const activityEvent = crowi.event('activity');
@@ -120,17 +120,26 @@ module.exports = function(crowi, app) {
 
     await crowi.activityService.createActivity(parameters);
 
+    const redirectToForUnauthenticated = createRedirectToForUnauthenticated(req.user.status);
+    const redirectTo = redirectToForUnauthenticated ?? res.locals.redirectTo ?? '/';
+
     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) => {
@@ -595,6 +604,7 @@ module.exports = function(crowi, app) {
 
   return {
     cannotLoginErrorHadnler,
+    injectRedirectTo,
     isEnableLoginWithLocalOrLdap,
     loginFailure,
     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,
 } from 'mongoose';
 

+ 4 - 2
packages/app/src/services/renderer/renderer.tsx

@@ -71,7 +71,9 @@ const baseSanitizeSchema = {
   tagNames: ['iframe', 'section'],
   attributes: {
     iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
-    '*': ['class', 'className', 'style'],
+    // The special value 'data*' as a property name can be used to allow all data properties.
+    // see: https://github.com/syntax-tree/hast-util-sanitize/
+    '*': ['class', 'className', 'style', 'data*'],
   },
 };
 
@@ -87,7 +89,7 @@ let isInjectedCustomSanitaizeOption = false;
 
 const injectCustomSanitizeOption = (config: RendererConfig) => {
   if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    commonSanitizeOption.tagNames = deepmerge(baseSanitizeSchema.tagNames, config.tagWhiteList ?? []);
+    commonSanitizeOption.tagNames = baseSanitizeSchema.tagNames.concat(config.tagWhiteList ?? []);
     commonSanitizeOption.attributes = deepmerge(baseSanitizeSchema.attributes, config.attrWhiteList ?? {});
     isInjectedCustomSanitaizeOption = true;
   }

+ 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 {
-  useCurrentPathname,
   useCurrentUser, useDefaultIndentSize, useIsGuestUser,
 } from './context';
 // 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> => {
-  // 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 {
-  useCurrentPageId, useIsEditable, useIsGuestUser,
+  useCurrentPageId, useIsEditable,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId, useIsNotFound,
 } from './context';
-import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { useCurrentPagePath, useIsTrashPage } from './page';
 import { useStaticSWR } from './use-static-swr';
 
@@ -235,18 +234,14 @@ type PreferDrawerModeByUserUtils = {
 }
 
 export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWithUtils<PreferDrawerModeByUserUtils, boolean> => {
-  const { data: isGuestUser } = useIsGuestUser();
   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 = {
     update: (preferDrawerMode: boolean) => {
       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 isEditModeWithPreferDrawerMode = editorMode === EditorMode.Editor && preferDrawerModeOnEditByUser;
+  const isEditModeWithPreferDrawerMode = editorMode !== EditorMode.View && preferDrawerModeOnEditByUser;
   const isDrawerModeFixed = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
 
   return useSWRImmutable(

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

@@ -1,9 +1,10 @@
 import assert from 'assert';
 
 import {
-  Key, SWRConfiguration, SWRResponse, useSWRConfig,
+  Key, SWRConfiguration, SWRResponse,
 } from 'swr';
-import useSWRImmutable from 'swr/immutable';
+
+import { useStaticSWR } from './use-static-swr';
 
 
 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\'');
 
-  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') } });
 
   return result;

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

@@ -24,17 +24,22 @@ type UseThemeExtendedProps = Omit<UseThemeProps, 'theme'|'resolvedTheme'> & {
   resolvedTheme: ColorScheme,
   useOsSettings: boolean,
   isDarkMode: boolean,
+  isForcedByGrowiTheme: boolean,
   resolvedThemeByAttributes?: ColorScheme,
 }
 
 export const useNextThemes = (): UseThemeProps & UseThemeExtendedProps => {
   const props = useTheme();
+  const { data: forcedColorScheme } = useForcedColorScheme();
+
+  const resolvedTheme = forcedColorScheme ?? props.resolvedTheme as ColorScheme;
 
   return Object.assign(props, {
     theme: props.theme as Themes,
-    resolvedTheme: props.resolvedTheme as ColorScheme,
+    resolvedTheme,
     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,
   });
 };

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

@@ -1,9 +1,7 @@
-import { useEffect } from 'react';
-
 import assert from 'assert';
 
 import {
-  mutate, Key, SWRConfiguration, SWRResponse,
+  Key, SWRConfiguration, SWRResponse, useSWRConfig,
 } from 'swr';
 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\'');
 
-  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;
 }

+ 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).toHaveBeenCalledWith('/login');
       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",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.7-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/hackmd/package.json

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

+ 4 - 9
packages/presentation/package.json

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

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

@@ -5,4 +5,5 @@ export type PresentationOptions = {
   rendererOptions: ReactMarkdownOptions,
   revealOptions?: RevealOptions,
   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 = {
   isDarkMode?: boolean,
+  disableSeparationByHeader?: boolean,
 }
 
 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) => {
     // wrap with <section>
     visit(
       tree,
-      node => node.type !== 'thematicBreak',
+      startCondition,
       (node, index, parent: Parent) => {
         if (parent == null || parent.type !== 'root') {
           return;
         }
 
         const startElem = node;
-        const endElem = findAfter(parent, startElem, node => node.type === 'thematicBreak');
+        const endElem = findAfter(parent, startElem, endCondition);
 
         wrapWithSection(parent, startElem, endElem, isDarkMode);
 
         // remove <hr>
-        if (endElem != null) {
+        if (endElem != null && endElem.type === 'thematicBreak') {
           removeElement(parent, endElem);
         }
       },
     );
   };
+
 };
 
 

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

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

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

@@ -91,6 +91,11 @@
   // Sidebar list group
   // --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
   --color-editor-icons: var(--color-global);
 

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

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

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

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.7-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
@@ -20,9 +20,9 @@
     "test": ""
   },
   "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.7-RC.0",
+    "@growi/remark-growi-directive": "^6.0.7-RC.0",
+    "@growi/ui": "^6.0.7-RC.0",
     "swr": "^2.0.3"
   },
   "devDependencies": {

+ 1 - 1
packages/slack/package.json

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

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

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

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.7-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": ["growi"],
@@ -17,7 +17,7 @@
     "test": "jest --verbose"
   },
   "dependencies": {
-    "@growi/core": "^6.0.6-RC.0"
+    "@growi/core": "^6.0.7-RC.0"
   },
   "devDependencies": {
     "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/PagePath/PageListMeta';
 export * from './components/PagePath/PagePathLabel';
+export * from './components/SearchPage/FootstampIcon';
 export * from './components/User/UserPicture';
 
 export * from './utils/browser-utils';
-export * from './components/SearchPage/FootstampIcon';
+export * from './utils/use-fullscreen';

+ 50 - 0
packages/ui/src/utils/use-fullscreen.ts

@@ -0,0 +1,50 @@
+import {
+  useCallback, useEffect, useMemo, useState,
+} from 'react';
+
+export interface FullScreenHandle {
+  active: boolean;
+  enter: () => Promise<void>;
+  exit: () => Promise<void>;
+}
+
+export const useFullScreen = (): FullScreenHandle => {
+  const [active, setActive] = useState(false);
+
+  useEffect(() => {
+    const handleChange = () => {
+      setActive(document.fullscreenElement != null);
+    };
+
+    document.addEventListener('fullscreenchange', handleChange);
+    return function cleanup() {
+      document.removeEventListener('fullscreenchange', handleChange);
+    };
+  }, []);
+
+  const enter = useCallback((elem?: HTMLElement) => {
+    if (document.fullscreenElement != null) {
+      return Promise.resolve();
+    }
+
+    const targetElem = elem ?? document.documentElement;
+    return targetElem.requestFullscreen();
+  }, []);
+
+  const exit = useCallback(() => {
+    if (document.fullscreenElement == null) {
+      return Promise.resolve();
+    }
+
+    return document.exitFullscreen();
+  }, []);
+
+  return useMemo(
+    () => ({
+      active,
+      enter,
+      exit,
+    }),
+    [active, enter, exit],
+  );
+};