Explorar o código

110234 fix conflict

soumaeda %!s(int64=2) %!d(string=hai) anos
pai
achega
42ecf212fc
Modificáronse 100 ficheiros con 397 adicións e 321 borrados
  1. 1 1
      .github/ISSUE_TEMPLATE/bug-report.md
  2. 2 1
      .github/workflows/release-slackbot-proxy.yml
  3. 4 3
      .github/workflows/release.yml
  4. 12 11
      apps/app/package.json
  5. 2 1
      apps/app/public/static/locales/en_US/commons.json
  6. 2 1
      apps/app/public/static/locales/ja_JP/commons.json
  7. 2 1
      apps/app/public/static/locales/zh_CN/commons.json
  8. 0 0
      apps/app/resource/locales/en_US/admin/userInvitation.ejs
  9. 0 0
      apps/app/resource/locales/en_US/admin/userResetPassword.ejs
  10. 0 0
      apps/app/resource/locales/en_US/admin/userWaitingActivation.ejs
  11. 1 1
      apps/app/resource/locales/en_US/notifications/comment.ejs
  12. 0 0
      apps/app/resource/locales/en_US/notifications/notActiveUser.ejs
  13. 1 1
      apps/app/resource/locales/en_US/notifications/pageCreate.ejs
  14. 1 1
      apps/app/resource/locales/en_US/notifications/pageDelete.ejs
  15. 1 1
      apps/app/resource/locales/en_US/notifications/pageEdit.ejs
  16. 1 1
      apps/app/resource/locales/en_US/notifications/pageLike.ejs
  17. 1 1
      apps/app/resource/locales/en_US/notifications/pageMove.ejs
  18. 0 0
      apps/app/resource/locales/en_US/notifications/passwordReset.ejs
  19. 0 0
      apps/app/resource/locales/en_US/notifications/passwordResetSuccessful.ejs
  20. 0 0
      apps/app/resource/locales/en_US/notifications/userActivation.ejs
  21. 1 1
      apps/app/resource/locales/en_US/welcome.md
  22. 0 0
      apps/app/resource/locales/ja_JP/admin/userInvitation.ejs
  23. 0 0
      apps/app/resource/locales/ja_JP/admin/userResetPassword.ejs
  24. 0 0
      apps/app/resource/locales/ja_JP/admin/userWaitingActivation.ejs
  25. 9 0
      apps/app/resource/locales/ja_JP/notifications/comment.ejs
  26. 0 0
      apps/app/resource/locales/ja_JP/notifications/notActiveUser.ejs
  27. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageCreate.ejs
  28. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageCreate.txt
  29. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageDelete.ejs
  30. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageDelete.txt
  31. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageEdit.ejs
  32. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageEdit.txt
  33. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageLike.ejs
  34. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageLike.txt
  35. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageMove.ejs
  36. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageMove.txt
  37. 0 0
      apps/app/resource/locales/ja_JP/notifications/passwordReset.ejs
  38. 0 0
      apps/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.ejs
  39. 0 0
      apps/app/resource/locales/ja_JP/notifications/userActivation.ejs
  40. 0 0
      apps/app/resource/locales/zh_CN/admin/userInvitation.ejs
  41. 0 0
      apps/app/resource/locales/zh_CN/admin/userResetPassword.ejs
  42. 0 0
      apps/app/resource/locales/zh_CN/admin/userWaitingActivation.ejs
  43. 1 1
      apps/app/resource/locales/zh_CN/notifications/comment.ejs
  44. 0 0
      apps/app/resource/locales/zh_CN/notifications/notActiveUser.ejs
  45. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageCreate.ejs
  46. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageDelete.ejs
  47. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageEdit.ejs
  48. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageLike.ejs
  49. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageMove.ejs
  50. 0 0
      apps/app/resource/locales/zh_CN/notifications/passwordReset.ejs
  51. 0 0
      apps/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.ejs
  52. 0 0
      apps/app/resource/locales/zh_CN/notifications/userActivation.ejs
  53. 11 2
      apps/app/src/components/Me/BasicInfoSettings.tsx
  54. 3 1
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  55. 4 3
      apps/app/src/components/PageAlert/TrashPageAlert.tsx
  56. 31 31
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  57. 20 4
      apps/app/src/components/SearchPage.tsx
  58. 14 5
      apps/app/src/components/SearchPage/SearchPageBase.tsx
  59. 25 4
      apps/app/src/pages/[[...path]].page.tsx
  60. 20 6
      apps/app/src/pages/share/[[...path]].page.tsx
  61. 24 2
      apps/app/src/pages/utils/commons.ts
  62. 5 4
      apps/app/src/server/models/obsolete-page.js
  63. 2 0
      apps/app/src/server/models/page.ts
  64. 16 0
      apps/app/src/server/models/user.js
  65. 8 8
      apps/app/src/server/routes/apiv3/forgot-password.js
  66. 9 0
      apps/app/src/server/routes/apiv3/personal-setting.js
  67. 6 9
      apps/app/src/server/routes/apiv3/user-activation.ts
  68. 3 2
      apps/app/src/server/routes/apiv3/users.js
  69. 4 3
      apps/app/src/server/routes/login.js
  70. 6 0
      apps/app/src/server/service/config-loader.ts
  71. 3 2
      apps/app/src/server/service/global-notification/global-notification-mail.js
  72. 7 3
      apps/app/src/stores/page.tsx
  73. 5 3
      apps/app/src/stores/personal-settings.tsx
  74. 2 2
      apps/app/src/styles/theme/apply-colors.scss
  75. 0 1
      apps/app/tsconfig.json
  76. 1 1
      apps/slackbot-proxy/package.json
  77. 0 4
      bin/github-actions/bump-versions/README.md
  78. 0 18
      bin/github-actions/bump-versions/cli.js
  79. 0 77
      bin/github-actions/bump-versions/flow/bump-versions.js
  80. 0 16
      bin/github-actions/bump-versions/index.js
  81. 0 58
      bin/github-actions/bump-versions/step/printHelp.js
  82. 0 12
      bump-versions.config.js
  83. 2 6
      package.json
  84. 2 1
      packages/core/package.json
  85. 1 0
      packages/core/src/interfaces/page.ts
  86. 2 1
      packages/hackmd/package.json
  87. 1 0
      packages/pluginkit/.eslintignore
  88. 5 0
      packages/pluginkit/.eslintrc.js
  89. 1 0
      packages/pluginkit/.gitignore
  90. 23 0
      packages/pluginkit/package.json
  91. 1 0
      packages/pluginkit/src/consts/index.ts
  92. 6 0
      packages/pluginkit/src/consts/types.ts
  93. 2 0
      packages/pluginkit/src/index.ts
  94. 12 0
      packages/pluginkit/src/model/growi-plugin-validation-data.ts
  95. 15 0
      packages/pluginkit/src/model/growi-plugin-validation-error.ts
  96. 2 0
      packages/pluginkit/src/model/index.ts
  97. 2 0
      packages/pluginkit/src/server/utils/v4/index.ts
  98. 11 0
      packages/pluginkit/src/server/utils/v4/package-json/import.spec.ts
  99. 6 0
      packages/pluginkit/src/server/utils/v4/package-json/import.ts
  100. 2 0
      packages/pluginkit/src/server/utils/v4/package-json/index.ts

+ 1 - 1
.github/ISSUE_TEMPLATE/bug-report.md

@@ -1,7 +1,7 @@
 ---
 ---
 name: Bug report
 name: Bug report
 about: Create a report to help us improve
 about: Create a report to help us improve
-labels: ['phase/new']
+labels: ['0️⃣ phase/new']
 ---
 ---
 
 
 Environment
 Environment

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

@@ -112,8 +112,9 @@ jobs:
         yarn --frozen-lockfile
         yarn --frozen-lockfile
 
 
     - name: Bump versions for next RC
     - name: Bump versions for next RC
+      working-directory: ./apps/slackbot-proxy
       run: |
       run: |
-        yarn bump-versions:slackbot-proxy
+        yarn version --no-git-tag-version --prepatch --preid=slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0
       uses: myrotvorets/info-from-package-json-action@1.2.0

+ 4 - 3
.github/workflows/release.yml

@@ -35,7 +35,8 @@ jobs:
 
 
     - name: Bump versions
     - name: Bump versions
       run: |
       run: |
-        yarn bump-versions:patch
+        turbo run bump-versions:patch
+        yarn upgrade --scope=@growi
         sh ./apps/app/bin/github-actions/update-readme.sh
         sh ./apps/app/bin/github-actions/update-readme.sh
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
@@ -97,8 +98,8 @@ jobs:
 
 
     - name: Bump versions for next RC
     - name: Bump versions for next RC
       run: |
       run: |
-        yarn bump-versions:rc
-        yarn bump-versions:slackbot-proxy
+        turbo run bump-versions:rc
+        yarn upgrade --scope=@growi
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0
       uses: myrotvorets/info-from-package-json-action@1.2.0

+ 12 - 11
apps/app/package.json

@@ -46,7 +46,8 @@
     "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "ts-node": "node -r ts-node/register -r tsconfig-paths/register -r dotenv-flow/config",
     "ts-node": "node -r ts-node/register -r tsconfig-paths/register -r dotenv-flow/config",
-    "ts-node-dev": "ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config"
+    "ts-node-dev": "ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config",
+    "version": "yarn version --no-git-tag-version"
   },
   },
   "// comments for dependencies": {
   "// comments for dependencies": {
     "escape-string-regexp": "5.0.0 or above exports only ESM",
     "escape-string-regexp": "5.0.0 or above exports only ESM",
@@ -63,14 +64,14 @@
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/core": "^6.1.4-RC.0",
-    "@growi/hackmd": "^6.1.4-RC.0",
-    "@growi/preset-themes": "^6.1.4-RC.0",
-    "@growi/remark-attachment-refs": "^6.1.4-RC.0",
-    "@growi/remark-drawio": "^6.1.4-RC.0",
-    "@growi/remark-growi-directive": "^6.1.4-RC.0",
-    "@growi/remark-lsx": "^6.1.4-RC.0",
-    "@growi/slack": "^6.1.4-RC.0",
+    "@growi/core": "link:../../packages/core",
+    "@growi/hackmd": "link:../../packages/hackmd",
+    "@growi/preset-themes": "link:../../packages/preset-themes",
+    "@growi/remark-attachment-refs": "link:../../packages/remark-attachment-refs",
+    "@growi/remark-drawio": "link:../../packages/remark-drawio",
+    "@growi/remark-growi-directive": "link:../../packages/remark-growi-directive",
+    "@growi/remark-lsx": "link:../../packages/remark-lsx",
+    "@growi/slack": "link:../../packages/slack",
     "@promster/express": "^7.0.6",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
@@ -208,8 +209,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@growi/presentation": "^6.1.4-RC.0",
-    "@growi/ui": "^6.1.4-RC.0",
+    "@growi/presentation": "link:../../packages/presentation",
+    "@growi/ui": "link:../../packages/ui",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^13.2.3",
     "@next/bundle-analyzer": "^13.2.3",

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

@@ -23,7 +23,8 @@
   "alert": {
   "alert": {
     "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "please_enable_mailer": "Please setup mailer first.",
     "please_enable_mailer": "Please setup mailer first.",
-    "password_reset_please_enable_mailer": "Please setup mailer first."
+    "password_reset_please_enable_mailer": "Please setup mailer first.",
+    "email_is_already_in_use": "The email address is already in use."
   },
   },
   "headers": {
   "headers": {
     "app_settings": "App Settings"
     "app_settings": "App Settings"

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

@@ -22,7 +22,8 @@
   "alert": {
   "alert": {
     "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "please_enable_mailer": "メール認証を有効にするには、メール設定を完了させてください。",
     "please_enable_mailer": "メール認証を有効にするには、メール設定を完了させてください。",
-    "password_reset_please_enable_mailer": "パスワード再設定を有効にするには、メール設定を完了させてください。"
+    "password_reset_please_enable_mailer": "パスワード再設定を有効にするには、メール設定を完了させてください。",
+    "email_is_already_in_use": "そのメールアドレスは既に使用されています。"
   },
   },
   "headers": {
   "headers": {
     "app_settings": "アプリ設定"
     "app_settings": "アプリ設定"

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

@@ -23,7 +23,8 @@
   "alert": {
   "alert": {
     "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",
     "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",
     "please_enable_mailer": "请先设置邮件程序。",
     "please_enable_mailer": "请先设置邮件程序。",
-    "password_reset_please_enable_mailer": "请先设置邮件程序。"
+    "password_reset_please_enable_mailer": "请先设置邮件程序。",
+    "email_is_already_in_use": "这个电子邮件地址已经在使用了。"
   },
   },
   "headers": {
   "headers": {
     "app_settings": "系统设置"
     "app_settings": "系统设置"

+ 0 - 0
apps/app/resource/locales/en_US/admin/userInvitation.txt → apps/app/resource/locales/en_US/admin/userInvitation.ejs


+ 0 - 0
apps/app/resource/locales/en_US/admin/userResetPassword.txt → apps/app/resource/locales/en_US/admin/userResetPassword.ejs


+ 0 - 0
apps/app/resource/locales/en_US/admin/userWaitingActivation.txt → apps/app/resource/locales/en_US/admin/userWaitingActivation.ejs


+ 1 - 1
apps/app/resource/locales/en_US/notifications/comment.txt → apps/app/resource/locales/en_US/notifications/comment.ejs

@@ -6,4 +6,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/en_US/notifications/notActiveUser.txt → apps/app/resource/locales/en_US/notifications/notActiveUser.ejs


+ 1 - 1
apps/app/resource/locales/zh_CN/notifications/pageCreate.txt → apps/app/resource/locales/en_US/notifications/pageCreate.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 1 - 1
apps/app/resource/locales/zh_CN/notifications/pageDelete.txt → apps/app/resource/locales/en_US/notifications/pageDelete.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 1 - 1
apps/app/resource/locales/zh_CN/notifications/pageEdit.txt → apps/app/resource/locales/en_US/notifications/pageEdit.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 1 - 1
apps/app/resource/locales/en_US/notifications/pageLike.txt → apps/app/resource/locales/en_US/notifications/pageLike.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 1 - 1
apps/app/resource/locales/en_US/notifications/pageMove.txt → apps/app/resource/locales/en_US/notifications/pageMove.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/en_US/notifications/passwordReset.txt → apps/app/resource/locales/en_US/notifications/passwordReset.ejs


+ 0 - 0
apps/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt → apps/app/resource/locales/en_US/notifications/passwordResetSuccessful.ejs


+ 0 - 0
apps/app/resource/locales/en_US/notifications/userActivation.txt → apps/app/resource/locales/en_US/notifications/userActivation.ejs


+ 1 - 1
apps/app/resource/locales/en_US/welcome.md

@@ -60,5 +60,5 @@ We can display the content list using a table and `$lsx`.
 
 
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 
 
-We welcome newcomers joining our slack channel to help improve Growi.
+We welcome newcomers joining our slack channel to help improve GROWI.
 In addition to discussing development, we are also happy to answer your questions when you join.
 In addition to discussing development, we are also happy to answer your questions when you join.

+ 0 - 0
apps/app/resource/locales/ja_JP/admin/userInvitation.txt → apps/app/resource/locales/ja_JP/admin/userInvitation.ejs


+ 0 - 0
apps/app/resource/locales/ja_JP/admin/userResetPassword.txt → apps/app/resource/locales/ja_JP/admin/userResetPassword.ejs


+ 0 - 0
apps/app/resource/locales/ja_JP/admin/userWaitingActivation.txt → apps/app/resource/locales/ja_JP/admin/userWaitingActivation.ejs


+ 9 - 0
apps/app/resource/locales/ja_JP/notifications/comment.ejs

@@ -0,0 +1,9 @@
+<%- username %> が <%- path %> にコメントしました。
+
+----------------------
+
+<%- comment %>
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/notActiveUser.txt → apps/app/resource/locales/ja_JP/notifications/notActiveUser.ejs


+ 5 - 0
apps/app/resource/locales/ja_JP/notifications/pageCreate.ejs

@@ -0,0 +1,5 @@
+<%- username %> が <%- path %> を作成しました。
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/pageCreate.txt


+ 5 - 0
apps/app/resource/locales/ja_JP/notifications/pageDelete.ejs

@@ -0,0 +1,5 @@
+<%- username %> が <%- path %> を削除しました。
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/pageDelete.txt


+ 5 - 0
apps/app/resource/locales/ja_JP/notifications/pageEdit.ejs

@@ -0,0 +1,5 @@
+<%- username %> が <%- path %> を編集しました。
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/pageEdit.txt


+ 5 - 0
apps/app/resource/locales/ja_JP/notifications/pageLike.ejs

@@ -0,0 +1,5 @@
+<%- username %> が <%- path %> を「いいね」しました。
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/pageLike.txt


+ 5 - 0
apps/app/resource/locales/ja_JP/notifications/pageMove.ejs

@@ -0,0 +1,5 @@
+<%- username %> が <%- oldPath %> を <%- newPath %> に移動(名前を変更)しました。
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/pageMove.txt


+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/passwordReset.txt → apps/app/resource/locales/ja_JP/notifications/passwordReset.ejs


+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt → apps/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.ejs


+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/userActivation.txt → apps/app/resource/locales/ja_JP/notifications/userActivation.ejs


+ 0 - 0
apps/app/resource/locales/zh_CN/admin/userInvitation.txt → apps/app/resource/locales/zh_CN/admin/userInvitation.ejs


+ 0 - 0
apps/app/resource/locales/zh_CN/admin/userResetPassword.txt → apps/app/resource/locales/zh_CN/admin/userResetPassword.ejs


+ 0 - 0
apps/app/resource/locales/zh_CN/admin/userWaitingActivation.txt → apps/app/resource/locales/zh_CN/admin/userWaitingActivation.ejs


+ 1 - 1
apps/app/resource/locales/zh_CN/notifications/comment.txt → apps/app/resource/locales/zh_CN/notifications/comment.ejs

@@ -6,4 +6,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/zh_CN/notifications/notActiveUser.txt → apps/app/resource/locales/zh_CN/notifications/notActiveUser.ejs


+ 1 - 1
apps/app/resource/locales/en_US/notifications/pageCreate.txt → apps/app/resource/locales/zh_CN/notifications/pageCreate.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 1 - 1
apps/app/resource/locales/en_US/notifications/pageDelete.txt → apps/app/resource/locales/zh_CN/notifications/pageDelete.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 1 - 1
apps/app/resource/locales/en_US/notifications/pageEdit.txt → apps/app/resource/locales/zh_CN/notifications/pageEdit.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 1 - 1
apps/app/resource/locales/zh_CN/notifications/pageLike.txt → apps/app/resource/locales/zh_CN/notifications/pageLike.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 1 - 1
apps/app/resource/locales/zh_CN/notifications/pageMove.txt → apps/app/resource/locales/zh_CN/notifications/pageMove.ejs

@@ -2,4 +2,4 @@
 
 
 ----------------------
 ----------------------
 
 
-Growi: <%- appTitle %>
+GROWI: <%- appTitle %>

+ 0 - 0
apps/app/resource/locales/zh_CN/notifications/passwordReset.txt → apps/app/resource/locales/zh_CN/notifications/passwordReset.ejs


+ 0 - 0
apps/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt → apps/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.ejs


+ 0 - 0
apps/app/resource/locales/zh_CN/notifications/userActivation.txt → apps/app/resource/locales/zh_CN/notifications/userActivation.ejs


+ 11 - 2
apps/app/src/components/Me/BasicInfoSettings.tsx

@@ -24,8 +24,17 @@ export const BasicInfoSettings = (): JSX.Element => {
       sync();
       sync();
       toastSuccess(t('toaster.update_successed', { target: t('Basic Info'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('Basic Info'), ns: 'commons' }));
     }
     }
-    catch (err) {
-      toastError(err);
+    catch (errs) {
+      const err = errs[0];
+      const message = err.message;
+      const code = err.code;
+
+      if (code === 'email-is-already-in-use') {
+        toastError(t('alert.email_is_already_in_use', { ns: 'commons' }));
+      }
+      else {
+        toastError(message);
+      }
     }
     }
   };
   };
 
 

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

@@ -317,9 +317,11 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       else if (currentPathname != null) {
       else if (currentPathname != null) {
         router.push(currentPathname);
         router.push(currentPathname);
       }
       }
+
+      mutateCurrentPage();
     };
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
-  }, [currentPathname, openDeleteModal, router]);
+  }, [currentPathname, mutateCurrentPage, openDeleteModal, router]);
 
 
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
     if (!isSharedPage) {
     if (!isSharedPage) {

+ 4 - 3
apps/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -9,7 +9,7 @@ import { unlink } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
 import {
 import {
-  useCurrentPagePath, useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage,
+  useCurrentPagePath, useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage, useSWRMUTxCurrentPage,
 } from '~/stores/page';
 } from '~/stores/page';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 
 
@@ -33,11 +33,11 @@ export const TrashPageAlert = (): JSX.Element => {
   const pagePath = pageData?.path;
   const pagePath = pageData?.path;
   const { data: pageInfo } = useSWRxPageInfo(pageId ?? null);
   const { data: pageInfo } = useSWRxPageInfo(pageId ?? null);
 
 
-
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
 
 
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
 
   const deleteUser = pageData?.deleteUser;
   const deleteUser = pageData?.deleteUser;
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
@@ -55,13 +55,14 @@ export const TrashPageAlert = (): JSX.Element => {
       try {
       try {
         unlink(currentPagePath);
         unlink(currentPagePath);
         router.push(`/${pageId}`);
         router.push(`/${pageId}`);
+        mutateCurrentPage();
       }
       }
       catch (err) {
       catch (err) {
         toastError(err);
         toastError(err);
       }
       }
     };
     };
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
-  }, [currentPagePath, openPutBackPageModal, pageId, pagePath, router]);
+  }, [currentPagePath, mutateCurrentPage, openPutBackPageModal, pageId, pagePath, router]);
 
 
   const openPageDeleteModalHandler = useCallback(() => {
   const openPageDeleteModalHandler = useCallback(() => {
     if (pageId === undefined || revisionId === undefined || pagePath === undefined) {
     if (pageId === undefined || revisionId === undefined || pagePath === undefined) {

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

@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
 
 
 import { HotTable } from '@handsontable/react';
 import { HotTable } from '@handsontable/react';
 import Handsontable from 'handsontable';
 import Handsontable from 'handsontable';
@@ -7,7 +7,6 @@ import {
   Collapse,
   Collapse,
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
-import { debounce } from 'throttle-debounce';
 
 
 import MarkdownTable from '~/client/models/MarkdownTable';
 import MarkdownTable from '~/client/models/MarkdownTable';
 import mtu from '~/components/PageEditor/MarkdownTableUtil';
 import mtu from '~/components/PageEditor/MarkdownTableUtil';
@@ -19,6 +18,7 @@ import { MarkdownTableDataImportForm } from './MarkdownTableDataImportForm';
 
 
 import styles from './HandsontableModal.module.scss';
 import styles from './HandsontableModal.module.scss';
 import 'handsontable/dist/handsontable.full.min.css';
 import 'handsontable/dist/handsontable.full.min.css';
+import { debounce } from 'throttle-debounce';
 
 
 const DEFAULT_HOT_HEIGHT = 300;
 const DEFAULT_HOT_HEIGHT = 300;
 const MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING = {
 const MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING = {
@@ -88,13 +88,37 @@ export const HandsontableModal = (): JSX.Element => {
   const [markdownTable, setMarkdownTable] = useState<MarkdownTable>(defaultMarkdownTable);
   const [markdownTable, setMarkdownTable] = useState<MarkdownTable>(defaultMarkdownTable);
   const [markdownTableOnInit, setMarkdownTableOnInit] = useState<MarkdownTable>(defaultMarkdownTable);
   const [markdownTableOnInit, setMarkdownTableOnInit] = useState<MarkdownTable>(defaultMarkdownTable);
   const [handsontableHeight, setHandsontableHeight] = useState<number>(DEFAULT_HOT_HEIGHT);
   const [handsontableHeight, setHandsontableHeight] = useState<number>(DEFAULT_HOT_HEIGHT);
+  const [handsontableWidth, setHandsontableWidth] = useState<number>(0);
 
 
-  useEffect(() => {
+  const handleWindowExpandedChange = () => {
+    if (hotTableContainer != null) {
+      // Get the width and height of hotTableContainer
+      const { width, height } = hotTableContainer.getBoundingClientRect();
+      setHandsontableWidth(width);
+      setHandsontableHeight(height);
+    }
+  };
+
+  const debouncedHandleWindowExpandedChange = debounce(100, handleWindowExpandedChange);
+
+  const handleModalOpen = () => {
     const initTableInstance = table == null ? defaultMarkdownTable : table.clone();
     const initTableInstance = table == null ? defaultMarkdownTable : table.clone();
     setMarkdownTable(table ?? defaultMarkdownTable);
     setMarkdownTable(table ?? defaultMarkdownTable);
     setMarkdownTableOnInit(initTableInstance);
     setMarkdownTableOnInit(initTableInstance);
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [isOpened]);
+    debouncedHandleWindowExpandedChange();
+  };
+
+  const expandWindow = () => {
+    setIsWindowExpanded(true);
+    debouncedHandleWindowExpandedChange();
+  };
+
+  const contractWindow = () => {
+    setIsWindowExpanded(false);
+    // Set the height to the default value
+    setHandsontableHeight(DEFAULT_HOT_HEIGHT);
+    debouncedHandleWindowExpandedChange();
+  };
 
 
   const markdownTableOption = {
   const markdownTableOption = {
     get latest() {
     get latest() {
@@ -363,32 +387,6 @@ export const HandsontableModal = (): JSX.Element => {
     toggleDataImportArea();
     toggleDataImportArea();
   };
   };
 
 
-  /**
-   * Expand the height of the Handsontable
-   *  by updating 'handsontableHeight' state
-   *  according to the height of this.refs.hotTableContainer
-   */
-  const expandHotTableHeight = () => {
-    if (isWindowExpanded && hotTableContainer != null) {
-      const height = hotTableContainer.getBoundingClientRect().height;
-      setHandsontableHeight(height);
-    }
-  };
-
-  const expandWindow = () => {
-    setIsWindowExpanded(true);
-
-    // create debounced method for expanding HotTable
-    // invoke updateHotTableHeight method with delay
-    // cz. Resizing this.refs.hotTableContainer is completed after a little delay after 'isWindowExpanded' set with 'true'
-    debounce(100, expandHotTableHeight);
-  };
-
-  const contractWindow = () => {
-    setIsWindowExpanded(false);
-    setHandsontableHeight(DEFAULT_HOT_HEIGHT);
-  };
-
   const createCustomizedContextMenu = () => {
   const createCustomizedContextMenu = () => {
     return {
     return {
       items: {
       items: {
@@ -453,6 +451,7 @@ export const HandsontableModal = (): JSX.Element => {
       size="lg"
       size="lg"
       wrapClassName={`${styles['grw-handsontable']}`}
       wrapClassName={`${styles['grw-handsontable']}`}
       className={`handsontable-modal ${isWindowExpanded && 'grw-modal-expanded'}`}
       className={`handsontable-modal ${isWindowExpanded && 'grw-modal-expanded'}`}
+      onOpened={handleModalOpen}
     >
     >
       <ModalHeader tag="h4" toggle={cancel} close={closeButton} className="bg-primary text-light">
       <ModalHeader tag="h4" toggle={cancel} close={closeButton} className="bg-primary text-light">
         {t('handsontable_modal.title')}
         {t('handsontable_modal.title')}
@@ -493,6 +492,7 @@ export const HandsontableModal = (): JSX.Element => {
             data={markdownTable.table}
             data={markdownTable.table}
             settings={handsontableSettings as Handsontable.DefaultSettings}
             settings={handsontableSettings as Handsontable.DefaultSettings}
             height={handsontableHeight}
             height={handsontableHeight}
+            width={handsontableWidth}
             afterLoadData={afterLoadDataHandler}
             afterLoadData={afterLoadDataHandler}
             modifyColWidth={modifyColWidthHandler}
             modifyColWidth={modifyColWidthHandler}
             beforeColumnMove={beforeColumnMoveHandler}
             beforeColumnMove={beforeColumnMoveHandler}

+ 20 - 4
apps/app/src/components/SearchPage.tsx

@@ -92,7 +92,10 @@ SearchResultListHead.displayName = 'SearchResultListHead';
 export const SearchPage = (): JSX.Element => {
 export const SearchPage = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: showPageLimitationL } = useShowPageLimitationL();
   const { data: showPageLimitationL } = useShowPageLimitationL();
+
+  // routerRef solve the problem of infinite redrawing that occurs with routers
   const router = useRouter();
   const router = useRouter();
+  const routerRef = useRef(router);
 
 
   // parse URL Query
   // parse URL Query
   const queries = router.query.q;
   const queries = router.query.q;
@@ -165,10 +168,10 @@ export const SearchPage = (): JSX.Element => {
 
 
   const initialSearchConditions: Partial<ISearchConditions> = useMemo(() => {
   const initialSearchConditions: Partial<ISearchConditions> = useMemo(() => {
     return {
     return {
-      keyword: initQ,
+      keyword,
       limit: INITIAL_PAGIONG_SIZE,
       limit: INITIAL_PAGIONG_SIZE,
     };
     };
-  }, [initQ]);
+  }, [keyword]);
 
 
   // for bulk deletion
   // for bulk deletion
   const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
   const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
@@ -177,8 +180,21 @@ export const SearchPage = (): JSX.Element => {
   useEffect(() => {
   useEffect(() => {
     const newUrl = new URL('/_search', 'http://example.com');
     const newUrl = new URL('/_search', 'http://example.com');
     newUrl.searchParams.append('q', keyword);
     newUrl.searchParams.append('q', keyword);
-    window.history.pushState('', `Search - ${keyword}`, `${newUrl.pathname}${newUrl.search}`);
-  }, [keyword]);
+    routerRef.current.push(`${newUrl.pathname}${newUrl.search}`, '', { shallow: true });
+  }, [keyword, routerRef]);
+
+  // browser back and forward
+  useEffect(() => {
+    routerRef.current.beforePopState(({ url }) => {
+      const newUrl = new URL(url, 'https://exmple.com');
+      const newKeyword = newUrl.searchParams.get('q');
+      if (newKeyword != null) {
+        setKeyword(newKeyword);
+      }
+      return true;
+    });
+  }, [setKeyword, routerRef]);
+
   const hitsCount = data?.meta.hitsCount;
   const hitsCount = data?.meta.hitsCount;
 
 
   const allControl = useMemo(() => {
   const allControl = useMemo(() => {

+ 14 - 5
apps/app/src/components/SearchPage/SearchPageBase.tsx

@@ -43,8 +43,13 @@ type Props = {
   searchPager: React.ReactNode,
   searchPager: React.ReactNode,
 }
 }
 
 
+
+const SearchResultContent = dynamic(() => import('./SearchResultContent').then(mod => mod.SearchResultContent), {
+  ssr: false,
+  loading: () => <></>,
+});
 const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props:Props, ref) => {
 const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props:Props, ref) => {
-  const SearchResultContent = dynamic(import('./SearchResultContent').then(mod => mod.SearchResultContent), { ssr: false });
+
   const {
   const {
     pages,
     pages,
     searchingKeyword,
     searchingKeyword,
@@ -62,6 +67,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
 
 
   const [selectedPageIdsByCheckboxes] = useState<Set<string>>(new Set());
   const [selectedPageIdsByCheckboxes] = useState<Set<string>>(new Set());
   // const [allPageIds] = useState<Set<string>>(new Set());
   // const [allPageIds] = useState<Set<string>>(new Set());
+
   const [selectedPageWithMeta, setSelectedPageWithMeta] = useState<IPageWithSearchMeta | undefined>();
   const [selectedPageWithMeta, setSelectedPageWithMeta] = useState<IPageWithSearchMeta | undefined>();
 
 
   // publish selectAll()
   // publish selectAll()
@@ -108,10 +114,13 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
 
 
   // select first item on load
   // select first item on load
   useEffect(() => {
   useEffect(() => {
-    if (selectedPageWithMeta == null && pages != null && pages.length > 0) {
+    if ((pages == null || pages.length === 0)) {
+      setSelectedPageWithMeta(undefined);
+    }
+    else if ((pages != null && pages.length > 0)) {
       setSelectedPageWithMeta(pages[0]);
       setSelectedPageWithMeta(pages[0]);
     }
     }
-  }, [pages, selectedPageWithMeta]);
+  }, [pages, setSelectedPageWithMeta]);
 
 
   // reset selectedPageIdsByCheckboxes
   // reset selectedPageIdsByCheckboxes
   useEffect(() => {
   useEffect(() => {
@@ -189,7 +198,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
                       pages={pages}
                       pages={pages}
                       selectedPageId={selectedPageWithMeta?.data._id}
                       selectedPageId={selectedPageWithMeta?.data._id}
                       forceHideMenuItems={forceHideMenuItems}
                       forceHideMenuItems={forceHideMenuItems}
-                      onPageSelected={page => setSelectedPageWithMeta(page)}
+                      onPageSelected={page => (setSelectedPageWithMeta(page)) }
                       onCheckboxChanged={checkboxChangedHandler}
                       onCheckboxChanged={checkboxChangedHandler}
                     />
                     />
                   </div>
                   </div>
@@ -205,7 +214,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
         </div>
         </div>
 
 
         <div className="mw-0 flex-grow-1 flex-basis-0 d-none d-lg-block search-result-content">
         <div className="mw-0 flex-grow-1 flex-basis-0 d-none d-lg-block search-result-content">
-          { selectedPageWithMeta != null && (
+          {pages != null && pages.length !== 0 && selectedPageWithMeta != null && (
             <SearchResultContent
             <SearchResultContent
               pageWithMeta={selectedPageWithMeta}
               pageWithMeta={selectedPageWithMeta}
               highlightKeywords={highlightKeywords}
               highlightKeywords={highlightKeywords}

+ 25 - 4
apps/app/src/pages/[[...path]].page.tsx

@@ -41,7 +41,8 @@ import {
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import {
 import {
-  useSWRxCurrentPage, useSWRxIsGrantNormalized, useCurrentPageId, useIsNotFound, useIsLatestRevision, useTemplateTagData, useTemplateBodyData,
+  useSWRxCurrentPage, useSWRMUTxCurrentPage, useSWRxIsGrantNormalized, useCurrentPageId,
+  useIsNotFound, useIsLatestRevision, useTemplateTagData, useTemplateBodyData,
 } from '~/stores/page';
 } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
@@ -57,7 +58,7 @@ import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
 import type { NextPageWithLayout } from './_app.page';
 import type { NextPageWithLayout } from './_app.page';
 import type { CommonProps } from './utils/commons';
 import type { CommonProps } from './utils/commons';
 import {
 import {
-  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig, skipSSR,
 } from './utils/commons';
 } from './utils/commons';
 
 
 
 
@@ -172,6 +173,7 @@ type Props = CommonProps & {
   adminPreferredIndentSize: number,
   adminPreferredIndentSize: number,
   isIndentSizeForced: boolean,
   isIndentSizeForced: boolean,
   disableLinkSharing: boolean,
   disableLinkSharing: boolean,
+  skipSSR: boolean,
 
 
   grantData?: IPageGrantData,
   grantData?: IPageGrantData,
 
 
@@ -237,9 +239,11 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
 
   useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
   useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
 
 
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+
   const { mutate: mutateIsNotFound } = useIsNotFound();
   const { mutate: mutateIsNotFound } = useIsNotFound();
 
 
-  const { mutate: mutateCurrentPageId } = useCurrentPageId();
+  const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
 
 
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
   const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
@@ -262,6 +266,22 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     ? _isTrashPage(pageWithMeta.data.path)
     ? _isTrashPage(pageWithMeta.data.path)
     : false;
     : false;
 
 
+
+  useEffect(() => {
+    if (!props.skipSSR) {
+      return;
+    }
+
+    if (currentPageId != null && !props.isNotFound) {
+      const mutatePageData = async() => {
+        const pageData = await mutateCurrentPage();
+        mutateEditingMarkdown(pageData?.revision.body);
+      };
+
+      mutatePageData();
+    }
+  }, [currentPageId, mutateCurrentPage, mutateEditingMarkdown, props.isNotFound, props.skipSSR]);
+
   // sync grant data
   // sync grant data
   useEffect(() => {
   useEffect(() => {
     const grantDataToApply = props.grantData ? props.grantData : grantData?.grantData.currentPageGrant;
     const grantDataToApply = props.grantData ? props.grantData : grantData?.grantData.currentPageGrant;
@@ -464,8 +484,9 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   // populate & check if the revision is latest
   // populate & check if the revision is latest
   if (page != null) {
   if (page != null) {
     page.initLatestRevisionField(revisionId);
     page.initLatestRevisionField(revisionId);
-    await page.populateDataToShowRevision();
     props.isLatestRevision = page.isLatestRevision();
     props.isLatestRevision = page.isLatestRevision();
+    props.skipSSR = await skipSSR(page);
+    await page.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
   }
   }
 
 
   if (page == null && user != null) {
   if (page == null && user != null) {

+ 20 - 6
apps/app/src/pages/share/[[...path]].page.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 
 
 import type { IUserHasId, IPagePopulatedToShowRevision } from '@growi/core';
 import type { IUserHasId, IPagePopulatedToShowRevision } from '@growi/core';
 import type {
 import type {
@@ -22,12 +22,12 @@ import {
   useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
   useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useIsContainerFluid,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useIsContainerFluid,
 } from '~/stores/context';
 } from '~/stores/context';
-import { useCurrentPageId, useIsNotFound } from '~/stores/page';
+import { useCurrentPageId, useIsNotFound, useSWRMUTxCurrentPage } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import type { NextPageWithLayout } from '../_app.page';
 import type { NextPageWithLayout } from '../_app.page';
 import {
 import {
-  getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig, CommonProps,
+  getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig, CommonProps, skipSSR,
 } from '../utils/commons';
 } from '../utils/commons';
 
 
 const logger = loggerFactory('growi:next-page:share');
 const logger = loggerFactory('growi:next-page:share');
@@ -43,6 +43,7 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
   drawioUri: string | null,
   drawioUri: string | null,
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
+  skipSSR: boolean,
 };
 };
 
 
 type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
 type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
@@ -92,6 +93,18 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsContainerFluid(props.isContainerFluid);
   useIsContainerFluid(props.isContainerFluid);
 
 
+  const { trigger: mutateCurrentPage, data: currentPage } = useSWRMUTxCurrentPage();
+
+  useEffect(() => {
+    if (!props.skipSSR) {
+      return;
+    }
+
+    if (props.shareLink?.relatedPage._id != null && !props.isNotFound) {
+      mutateCurrentPage();
+    }
+  }, [mutateCurrentPage, props.isNotFound, props.shareLink?.relatedPage._id, props.skipSSR]);
+
 
 
   const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName(props.shareLinkRelatedPage);
   const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName(props.shareLinkRelatedPage);
 
 
@@ -107,7 +120,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 
 
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
         <header className="py-0 position-relative">
         <header className="py-0 position-relative">
-          <GrowiContextualSubNavigationForSharedPage page={props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
+          <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
         </header>
         </header>
 
 
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
@@ -115,7 +128,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
         <ShareLinkPageView
         <ShareLinkPageView
           pagePath={pagePath}
           pagePath={pagePath}
           rendererConfig={props.rendererConfig}
           rendererConfig={props.rendererConfig}
-          page={props.shareLinkRelatedPage}
+          page={currentPage ?? props.shareLinkRelatedPage}
           shareLink={props.shareLink}
           shareLink={props.shareLink}
           isExpired={props.isExpired}
           isExpired={props.isExpired}
           disableLinkSharing={props.disableLinkSharing}
           disableLinkSharing={props.disableLinkSharing}
@@ -221,7 +234,8 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     }
     }
     else {
     else {
       props.isNotFound = false;
       props.isNotFound = false;
-      props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision();
+      props.skipSSR = await skipSSR(shareLink.relatedPage);
+      props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
       props.isExpired = shareLink.isExpired();
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();
       props.shareLink = shareLink.toObject();
     }
     }

+ 24 - 2
apps/app/src/pages/utils/commons.ts

@@ -1,6 +1,6 @@
 import type { ColorScheme, IUserHasId } from '@growi/core';
 import type { ColorScheme, IUserHasId } from '@growi/core';
 import {
 import {
-  DevidedPagePath, Lang, AllLang,
+  DevidedPagePath, Lang, AllLang, isServer,
 } from '@growi/core';
 } from '@growi/core';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { SSRConfig, UserConfig } from 'next-i18next';
 import type { SSRConfig, UserConfig } from 'next-i18next';
@@ -11,6 +11,7 @@ import { detectLocaleFromBrowserAcceptLanguage } from '~/client/util/locale-util
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import type { PageDocument } from '~/server/models/page';
 import type { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import type { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import {
 import {
   useCurrentProductNavWidth, useCurrentSidebarContents, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
   useCurrentProductNavWidth, useCurrentSidebarContents, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
@@ -74,7 +75,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
   const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
   const forcedColorScheme = crowi.customizeService.forcedColorScheme;
   const forcedColorScheme = crowi.customizeService.forcedColorScheme;
 
 
-  // retrieve UserUISettings
+  // retrieve UserUISett ings
   const UserUISettings = getModelSafely<UserUISettingsDocument>('UserUISettings');
   const UserUISettings = getModelSafely<UserUISettingsDocument>('UserUISettings');
   const userUISettings = user != null && UserUISettings != null
   const userUISettings = user != null && UserUISettings != null
     ? await UserUISettings.findOne({ user: user._id }).exec()
     ? await UserUISettings.findOne({ user: user._id }).exec()
@@ -168,3 +169,24 @@ export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettin
   useCurrentSidebarContents(userUISettings?.currentSidebarContents);
   useCurrentSidebarContents(userUISettings?.currentSidebarContents);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
 };
 };
+
+
+export const skipSSR = async(page: PageDocument): Promise<boolean> => {
+  if (!isServer()) {
+    throw new Error('This method is not available on the client-side');
+  }
+
+  // page document only stores the bodyLength of the latest revision
+  if (!page.isLatestRevision() || page.latestRevisionBodyLength == null) {
+    return true;
+  }
+
+  const { configManager } = await import('~/server/service/config-manager');
+  await configManager.loadConfigs();
+  const ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+  if (ssrMaxRevisionBodyLength < page.latestRevisionBodyLength) {
+    return true;
+  }
+
+  return false;
+};

+ 5 - 4
apps/app/src/server/models/obsolete-page.js

@@ -63,16 +63,17 @@ export const extractToAncestorsPaths = (pagePath) => {
  * populate page (Query or Document) to show revision
  * populate page (Query or Document) to show revision
  * @param {any} page Query or Document
  * @param {any} page Query or Document
  * @param {string} userPublicFields string to set to select
  * @param {string} userPublicFields string to set to select
+ * @param {boolean} shouldExcludeBody boolean indicating whether to include 'revision.body' or not
  */
  */
 /* eslint-disable object-curly-newline, object-property-newline */
 /* eslint-disable object-curly-newline, object-property-newline */
-export const populateDataToShowRevision = (page, userPublicFields) => {
+export const populateDataToShowRevision = (page, userPublicFields, shouldExcludeBody = false) => {
   return page
   return page
     .populate([
     .populate([
       { path: 'lastUpdateUser', model: 'User', select: userPublicFields },
       { path: 'lastUpdateUser', model: 'User', select: userPublicFields },
       { path: 'creator', model: 'User', select: userPublicFields },
       { path: 'creator', model: 'User', select: userPublicFields },
       { path: 'deleteUser', model: 'User', select: userPublicFields },
       { path: 'deleteUser', model: 'User', select: userPublicFields },
       { path: 'grantedGroup', model: 'UserGroup' },
       { path: 'grantedGroup', model: 'UserGroup' },
-      { path: 'revision', model: 'Revision', populate: {
+      { path: 'revision', model: 'Revision', select: shouldExcludeBody ? '-body' : undefined, populate: {
         path: 'author', model: 'User', select: userPublicFields,
         path: 'author', model: 'User', select: userPublicFields,
       } },
       } },
     ]);
     ]);
@@ -233,11 +234,11 @@ export const getPageSchema = (crowi) => {
     }
     }
   };
   };
 
 
-  pageSchema.methods.populateDataToShowRevision = async function() {
+  pageSchema.methods.populateDataToShowRevision = async function(shouldExcludeBody) {
     validateCrowi();
     validateCrowi();
 
 
     const User = crowi.model('User');
     const User = crowi.model('User');
-    return populateDataToShowRevision(this, User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
+    return populateDataToShowRevision(this, User.USER_FIELDS_EXCEPT_CONFIDENTIAL, shouldExcludeBody);
   };
   };
 
 
   pageSchema.methods.populateDataToMakePresentation = async function(revisionId) {
   pageSchema.methods.populateDataToMakePresentation = async function(revisionId) {

+ 2 - 0
apps/app/src/server/models/page.ts

@@ -93,6 +93,7 @@ const schema = new Schema<PageDocument, PageModel>({
     type: String, required: true, index: true,
     type: String, required: true, index: true,
   },
   },
   revision: { type: ObjectId, ref: 'Revision' },
   revision: { type: ObjectId, ref: 'Revision' },
+  latestRevisionBodyLength: { type: Number },
   status: { type: String, default: STATUS_PUBLISHED, index: true },
   status: { type: String, default: STATUS_PUBLISHED, index: true },
   grant: { type: Number, default: GRANT_PUBLIC, index: true },
   grant: { type: Number, default: GRANT_PUBLIC, index: true },
   grantedUsers: [{ type: ObjectId, ref: 'User' }],
   grantedUsers: [{ type: ObjectId, ref: 'User' }],
@@ -714,6 +715,7 @@ export async function pushRevision(pageData, newRevision, user) {
   await newRevision.save();
   await newRevision.save();
 
 
   pageData.revision = newRevision;
   pageData.revision = newRevision;
+  pageData.latestRevisionBodyLength = newRevision.body.length;
   pageData.lastUpdateUser = user?._id ?? user;
   pageData.lastUpdateUser = user?._id ?? user;
   pageData.updatedAt = Date.now();
   pageData.updatedAt = Date.now();
 
 

+ 16 - 0
apps/app/src/server/models/user.js

@@ -144,6 +144,22 @@ module.exports = function(crowi) {
     return hasher.digest('base64');
     return hasher.digest('base64');
   }
   }
 
 
+  userSchema.methods.isUniqueEmail = async function() {
+    const query = this.model('User').find();
+
+    const count = await query.count((
+      {
+        username: { $ne: this.username },
+        email: this.email,
+      }
+    ));
+
+    if (count > 0) {
+      return false;
+    }
+    return true;
+  };
+
   userSchema.methods.isPasswordSet = function() {
   userSchema.methods.isPasswordSet = function() {
     if (this.password) {
     if (this.password) {
       return true;
       return true;

+ 8 - 8
apps/app/src/server/routes/apiv3/forgot-password.js

@@ -5,13 +5,13 @@ import { SupportedAction } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import injectResetOrderByTokenMiddleware from '~/server/middlewares/inject-reset-order-by-token-middleware';
 import injectResetOrderByTokenMiddleware from '~/server/middlewares/inject-reset-order-by-token-middleware';
 import PasswordResetOrder from '~/server/models/password-reset-order';
 import PasswordResetOrder from '~/server/models/password-reset-order';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import httpErrorHandler from '../../middlewares/http-error-handler';
 import httpErrorHandler from '../../middlewares/http-error-handler';
 import { checkForgotPasswordEnabledMiddlewareFactory } from '../forgot-password';
 import { checkForgotPasswordEnabledMiddlewareFactory } from '../forgot-password';
 
 
-
 const logger = loggerFactory('growi:routes:apiv3:forgotPassword'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:forgotPassword'); // eslint-disable-line no-unused-vars
 
 
 const express = require('express');
 const express = require('express');
@@ -22,7 +22,7 @@ const { serializeUserSecurely } = require('../../models/serializers/user-seriali
 const router = express.Router();
 const router = express.Router();
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-  const { appService, mailService, configManager } = crowi;
+  const { appService, mailService } = crowi;
   const User = crowi.model('User');
   const User = crowi.model('User');
   const path = require('path');
   const path = require('path');
 
 
@@ -30,7 +30,7 @@ module.exports = (crowi) => {
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
 
 
-  const minPasswordLength = crowi.configManager.getConfig('crowi', 'app:minPasswordLength');
+  const minPasswordLength = configManager.getConfig('crowi', 'app:minPasswordLength');
 
 
   const validator = {
   const validator = {
     password: [
     password: [
@@ -47,11 +47,11 @@ module.exports = (crowi) => {
 
 
   const checkPassportStrategyMiddleware = checkForgotPasswordEnabledMiddlewareFactory(crowi, true);
   const checkPassportStrategyMiddleware = checkForgotPasswordEnabledMiddlewareFactory(crowi, true);
 
 
-  async function sendPasswordResetEmail(txtFileName, i18n, email, url, expiredAt) {
+  async function sendPasswordResetEmail(templateFileName, locale, email, url, expiredAt) {
     return mailService.send({
     return mailService.send({
       to: email,
       to: email,
       subject: '[GROWI] Password Reset',
       subject: '[GROWI] Password Reset',
-      template: path.join(crowi.localeDir, `${i18n}/notifications/${txtFileName}.txt`),
+      template: path.join(crowi.localeDir, `${locale}/notifications/${templateFileName}.ejs`),
       vars: {
       vars: {
         appTitle: appService.getAppTitle(),
         appTitle: appService.getAppTitle(),
         email,
         email,
@@ -63,7 +63,7 @@ module.exports = (crowi) => {
 
 
   router.post('/', checkPassportStrategyMiddleware, addActivity, async(req, res) => {
   router.post('/', checkPassportStrategyMiddleware, addActivity, async(req, res) => {
     const { email } = req.body;
     const { email } = req.body;
-    const i18n = configManager.getConfig('crowi', 'app:globalLang');
+    const locale = configManager.getConfig('crowi', 'app:globalLang');
     const appUrl = appService.getSiteUrl();
     const appUrl = appService.getSiteUrl();
 
 
     try {
     try {
@@ -71,7 +71,7 @@ module.exports = (crowi) => {
 
 
       // when the user is not found or active
       // when the user is not found or active
       if (user == null || user.status !== 2) {
       if (user == null || user.status !== 2) {
-        await sendPasswordResetEmail('notActiveUser', i18n, email, appUrl);
+        await sendPasswordResetEmail('notActiveUser', locale, email, appUrl);
         return res.apiv3();
         return res.apiv3();
       }
       }
 
 
@@ -81,7 +81,7 @@ module.exports = (crowi) => {
       const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;
       const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;
       const expiredAt = subSeconds(passwordResetOrderData.expiredAt, grwTzoffsetSec);
       const expiredAt = subSeconds(passwordResetOrderData.expiredAt, grwTzoffsetSec);
       const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
       const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
-      await sendPasswordResetEmail('passwordReset', i18n, email, oneTimeUrl, formattedExpiredAt);
+      await sendPasswordResetEmail('passwordReset', locale, email, oneTimeUrl, formattedExpiredAt);
 
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_USER_FOGOT_PASSWORD });
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_USER_FOGOT_PASSWORD });
 
 

+ 9 - 0
apps/app/src/server/routes/apiv3/personal-setting.js

@@ -1,5 +1,7 @@
+import { ErrorV3 } from '@growi/core';
 import { body } from 'express-validator';
 import { body } from 'express-validator';
 
 
+
 import { i18n } from '^/config/next-i18next.config';
 import { i18n } from '^/config/next-i18next.config';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
@@ -238,6 +240,13 @@ module.exports = (crowi) => {
       user.isEmailPublished = req.body.isEmailPublished;
       user.isEmailPublished = req.body.isEmailPublished;
       user.slackMemberId = req.body.slackMemberId;
       user.slackMemberId = req.body.slackMemberId;
 
 
+      const isUniqueEmail = await user.isUniqueEmail();
+
+      if (!isUniqueEmail) {
+        logger.error('email-is-not-unique');
+        return res.apiv3Err(new ErrorV3('The email is already in use', 'email-is-already-in-use'));
+      }
+
       const updatedUser = await user.save();
       const updatedUser = await user.save();
 
 
       const parameters = { action: SupportedAction.ACTION_USER_PERSONAL_SETTINGS_UPDATE };
       const parameters = { action: SupportedAction.ACTION_USER_PERSONAL_SETTINGS_UPDATE };

+ 6 - 9
apps/app/src/server/routes/apiv3/user-activation.ts

@@ -7,9 +7,9 @@ import { body, validationResult } from 'express-validator';
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import UserRegistrationOrder from '~/server/models/user-registration-order';
 import UserRegistrationOrder from '~/server/models/user-registration-order';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 const logger = loggerFactory('growi:routes:apiv3:user-activation');
 const logger = loggerFactory('growi:routes:apiv3:user-activation');
 
 
 const PASSOWRD_MINIMUM_NUMBER = 8;
 const PASSOWRD_MINIMUM_NUMBER = 8;
@@ -69,7 +69,6 @@ export const completeRegistrationAction = (crowi) => {
   const User = crowi.model('User');
   const User = crowi.model('User');
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
   const {
   const {
-    configManager,
     aclService,
     aclService,
     appService,
     appService,
     mailService,
     mailService,
@@ -142,7 +141,8 @@ export const completeRegistrationAction = (crowi) => {
           if (isMailerSetup) {
           if (isMailerSetup) {
             const admins = await User.findAdmins();
             const admins = await User.findAdmins();
             const appTitle = appService.getAppTitle();
             const appTitle = appService.getAppTitle();
-            const template = path.join(crowi.localeDir, 'en_US/admin/userWaitingActivation.txt');
+            const locale = configManager.getConfig('crowi', 'app:globalLang');
+            const template = path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`);
             const url = appService.getSiteUrl();
             const url = appService.getSiteUrl();
 
 
             sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url);
             sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url);
@@ -205,7 +205,6 @@ export const validateRegisterForm = (req, res, next) => {
 
 
 async function makeRegistrationEmailToken(email, crowi) {
 async function makeRegistrationEmailToken(email, crowi) {
   const {
   const {
-    configManager,
     mailService,
     mailService,
     localeDir,
     localeDir,
     appService,
     appService,
@@ -216,8 +215,7 @@ async function makeRegistrationEmailToken(email, crowi) {
     throw Error('mailService is not setup');
     throw Error('mailService is not setup');
   }
   }
 
 
-  const grobalLang = configManager.getConfig('crowi', 'app:globalLang');
-  const i18n = grobalLang;
+  const locale = configManager.getConfig('crowi', 'app:globalLang');
   const appUrl = appService.getSiteUrl();
   const appUrl = appService.getSiteUrl();
 
 
   const userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email);
   const userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email);
@@ -226,12 +224,11 @@ async function makeRegistrationEmailToken(email, crowi) {
   const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
   const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
   const url = new URL(`/user-activation/${userRegistrationOrder.token}`, appUrl);
   const url = new URL(`/user-activation/${userRegistrationOrder.token}`, appUrl);
   const oneTimeUrl = url.href;
   const oneTimeUrl = url.href;
-  const txtFileName = 'userActivation';
 
 
   return mailService.send({
   return mailService.send({
     to: email,
     to: email,
     subject: '[GROWI] User Activation',
     subject: '[GROWI] User Activation',
-    template: path.join(localeDir, `${i18n}/notifications/${txtFileName}.txt`),
+    template: path.join(localeDir, `${locale}/notifications/userActivation.ejs`),
     vars: {
     vars: {
       appTitle: appService.getAppTitle(),
       appTitle: appService.getAppTitle(),
       email,
       email,
@@ -248,7 +245,7 @@ export const registerAction = (crowi) => {
     const registerForm = req.body.registerForm || {};
     const registerForm = req.body.registerForm || {};
     const email = registerForm.email;
     const email = registerForm.email;
     const isRegisterableEmail = await User.isRegisterableEmail(email);
     const isRegisterableEmail = await User.isRegisterableEmail(email);
-    const registrationMode = crowi.configManager.getConfig('crowi', 'security:registrationMode') as RegistrationMode;
+    const registrationMode = configManager.getConfig('crowi', 'security:registrationMode') as RegistrationMode;
     const isEmailValid = await User.isEmailValid(email);
     const isEmailValid = await User.isEmailValid(email);
 
 
     if (registrationMode === RegistrationMode.CLOSED) {
     if (registrationMode === RegistrationMode.CLOSED) {

+ 3 - 2
apps/app/src/server/routes/apiv3/users.js

@@ -2,12 +2,12 @@ import { ErrorV3 } from '@growi/core';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import Activity from '~/server/models/activity';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 
-
 const logger = loggerFactory('growi:routes:apiv3:users');
 const logger = loggerFactory('growi:routes:apiv3:users');
 
 
 const path = require('path');
 const path = require('path');
@@ -150,6 +150,7 @@ module.exports = (crowi) => {
   const sendEmailByUserList = async(userList) => {
   const sendEmailByUserList = async(userList) => {
     const { appService, mailService } = crowi;
     const { appService, mailService } = crowi;
     const appTitle = appService.getAppTitle();
     const appTitle = appService.getAppTitle();
+    const locale = configManager.getConfig('crowi', 'app:globalLang');
     const failedToSendEmailList = [];
     const failedToSendEmailList = [];
 
 
     for (const user of userList) {
     for (const user of userList) {
@@ -158,7 +159,7 @@ module.exports = (crowi) => {
         await mailService.send({
         await mailService.send({
           to: user.email,
           to: user.email,
           subject: `Invitation to ${appTitle}`,
           subject: `Invitation to ${appTitle}`,
-          template: path.join(crowi.localeDir, 'en_US/admin/userInvitation.txt'),
+          template: path.join(crowi.localeDir, `${locale}/admin/userInvitation.ejs`),
           vars: {
           vars: {
             email: user.email,
             email: user.email,
             password: user.password,
             password: user.password,

+ 4 - 3
apps/app/src/server/routes/login.js

@@ -1,4 +1,5 @@
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 // disable all of linting
 // disable all of linting
@@ -10,7 +11,7 @@ module.exports = function(crowi, app) {
   const path = require('path');
   const path = require('path');
   const User = crowi.model('User');
   const User = crowi.model('User');
   const {
   const {
-    configManager, appService, aclService, mailService, activityService,
+    appService, aclService, mailService, activityService,
   } = crowi;
   } = crowi;
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
 
 
@@ -19,14 +20,14 @@ module.exports = function(crowi, app) {
   async function sendEmailToAllAdmins(userData) {
   async function sendEmailToAllAdmins(userData) {
     // send mails to all admin users (derived from crowi) -- 2020.06.18 Yuki Takei
     // send mails to all admin users (derived from crowi) -- 2020.06.18 Yuki Takei
     const admins = await User.findAdmins();
     const admins = await User.findAdmins();
-
     const appTitle = appService.getAppTitle();
     const appTitle = appService.getAppTitle();
+    const locale = configManager.getConfig('crowi', 'app:globalLang');
 
 
     const promises = admins.map((admin) => {
     const promises = admins.map((admin) => {
       return mailService.send({
       return mailService.send({
         to: admin.email,
         to: admin.email,
         subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
         subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
-        template: path.join(crowi.localeDir, 'en_US/admin/userWaitingActivation.txt'),
+        template: path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`),
         vars: {
         vars: {
           adminUser: admin,
           adminUser: admin,
           createdUser: userData,
           createdUser: userData,

+ 6 - 0
apps/app/src/server/service/config-loader.ts

@@ -682,6 +682,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type: ValueType.STRING,
     type: ValueType.STRING,
     default: null,
     default: null,
   },
   },
+  SSR_MAX_REVISION_BODY_LENGTH: {
+    ns: 'crowi',
+    key: 'app:ssrMaxRevisionBodyLength',
+    type: ValueType.NUMBER,
+    default: 30000,
+  },
 };
 };
 
 
 
 

+ 3 - 2
apps/app/src/server/service/global-notification/global-notification-mail.js

@@ -1,3 +1,4 @@
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:service:GlobalNotificationMailService'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:service:GlobalNotificationMailService'); // eslint-disable-line no-unused-vars
@@ -50,13 +51,13 @@ class GlobalNotificationMailService {
    * @return  {{ subject: string, template: string, vars: object }}
    * @return  {{ subject: string, template: string, vars: object }}
    */
    */
   generateOption(event, page, triggeredBy, { comment, oldPath }) {
   generateOption(event, page, triggeredBy, { comment, oldPath }) {
-    const defaultLang = this.crowi.configManager.getConfig('crowi', 'app:globalLang');
+    const locale = configManager.getConfig('crowi', 'app:globalLang');
     // validate for all events
     // validate for all events
     if (event == null || page == null || triggeredBy == null) {
     if (event == null || page == null || triggeredBy == null) {
       throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
       throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
     }
     }
 
 
-    const template = nodePath.join(this.crowi.localeDir, `${defaultLang}/notifications/${event}.txt`);
+    const template = nodePath.join(this.crowi.localeDir, `${locale}/notifications/${event}.ejs`);
 
 
     const path = page.path;
     const path = page.path;
     const appTitle = this.crowi.appService.getAppTitle();
     const appTitle = this.crowi.appService.getAppTitle();

+ 7 - 3
apps/app/src/stores/page.tsx

@@ -4,7 +4,7 @@ import type {
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable, SWRInfinitePageRevisionsResponse,
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable, SWRInfinitePageRevisionsResponse,
 } from '@growi/core';
 } from '@growi/core';
 import { Ref, isClient, pagePathUtils } from '@growi/core';
 import { Ref, isClient, pagePathUtils } from '@growi/core';
-import useSWR, { mutate, type SWRResponse } from 'swr';
+import useSWR, { mutate, useSWRConfig, type SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 import useSWRInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
 import useSWRInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
 import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
@@ -47,18 +47,22 @@ export const useTemplateBodyData = (initialData?: string): SWRResponse<string, E
   return useStaticSWR<string, Error>('templateBodyData', initialData);
   return useStaticSWR<string, Error>('templateBodyData', initialData);
 };
 };
 
 
+/** "useSWRxCurrentPage" is intended for initial data retrieval only. Use "useSWRMUTxCurrentPage" for revalidation */
 export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
 export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
   const key = 'currentPage';
   const key = 'currentPage';
 
 
+  const { cache } = useSWRConfig();
+  const shouldMutate = initialData?._id !== cache.get(key)?.data?._id && initialData !== undefined;
+
   useEffect(() => {
   useEffect(() => {
-    if (initialData !== undefined) {
+    if (shouldMutate) {
       mutate(key, initialData, {
       mutate(key, initialData, {
         optimisticData: initialData,
         optimisticData: initialData,
         populateCache: true,
         populateCache: true,
         revalidate: false,
         revalidate: false,
       });
       });
     }
     }
-  }, [initialData, key]);
+  }, [initialData, key, shouldMutate]);
 
 
   return useSWR(key, null, {
   return useSWR(key, null, {
     keepPreviousData: true,
     keepPreviousData: true,

+ 5 - 3
apps/app/src/stores/personal-settings.tsx

@@ -1,3 +1,4 @@
+import { ErrorV3 } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
 
@@ -10,6 +11,7 @@ import { apiv3Get, apiv3Put } from '../client/util/apiv3-client';
 
 
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
+
 const logger = loggerFactory('growi:stores:personal-settings');
 const logger = loggerFactory('growi:stores:personal-settings');
 
 
 
 
@@ -66,9 +68,9 @@ export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUse
       await apiv3Put('/personal-setting/', updateData);
       await apiv3Put('/personal-setting/', updateData);
       i18n.changeLanguage(updateData.lang);
       i18n.changeLanguage(updateData.lang);
     }
     }
-    catch (err) {
-      logger.error(err);
-      throw new Error('Failed to update personal data');
+    catch (errs) {
+      logger.error(errs);
+      throw errs;
     }
     }
   };
   };
 
 

+ 2 - 2
apps/app/src/styles/theme/apply-colors.scss

@@ -145,8 +145,8 @@ ul.pagination {
 //
 //
 
 
 .grw-logo {
 .grw-logo {
-  set transition for fill
-  svg * {
+  // set transition for fill
+  svg, svg * {
     transition: fill 0.8s ease-out;
     transition: fill 0.8s ease-out;
   }
   }
 
 

+ 0 - 1
apps/app/tsconfig.json

@@ -14,7 +14,6 @@
     "paths": {
     "paths": {
       "~/*": ["./src/*"],
       "~/*": ["./src/*"],
       "^/*": ["./*"],
       "^/*": ["./*"],
-
       "debug": ["./src/server/utils/logger/alias-for-debug"]
       "debug": ["./src/server/utils/logger/alias-for-debug"]
     }
     }
   },
   },

+ 1 - 1
apps/slackbot-proxy/package.json

@@ -25,7 +25,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.1.4-RC.0",
+    "@growi/slack": "link:../../packages/slack",
     "@slack/oauth": "^2.0.1",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",
     "@tsed/common": "^6.43.0",

+ 0 - 4
bin/github-actions/bump-versions/README.md

@@ -1,4 +0,0 @@
-bump-versions.js
-==============
-
-Custom cli for bumping package versions based on [algolia/shipjs@0.23.3](https://github.com/algolia/shipjs/tree/v0.23.3/packages/shipjs)

+ 0 - 18
bin/github-actions/bump-versions/cli.js

@@ -1,18 +0,0 @@
-import { print, parseArgs } from 'shipjs/src/util';
-import bumpVersions from './flow/bump-versions';
-
-export async function cli(argv) {
-  const { fn, arg: argSpec } = bumpVersions;
-  try {
-    const opts = parseArgs(argSpec, argv);
-    await fn(opts);
-  }
-  catch (error) {
-    if (error.code === 'ARG_UNKNOWN_OPTION') {
-      print(error);
-    }
-    else {
-      throw error;
-    }
-  }
-}

+ 0 - 77
bin/github-actions/bump-versions/flow/bump-versions.js

@@ -1,77 +0,0 @@
-import semver from 'semver';
-import { loadConfig, getCurrentVersion, getReleaseType } from 'shipjs-lib';
-
-import printDryRunBanner from 'shipjs/src/step/printDryRunBanner';
-import confirmNextVersion from 'shipjs/src/step/prepare/confirmNextVersion';
-import updateVersion from 'shipjs/src/step/prepare/updateVersion';
-import updateVersionMonorepo from 'shipjs/src/step/prepare/updateVersionMonorepo';
-import installDependencies from 'shipjs/src/step/prepare/installDependencies';
-
-import printHelp from '../step/printHelp';
-
-async function bumpVersions({
-  help = false,
-  dir = '.',
-  dryRun = false,
-  updateDependencies = true,
-  increment = 'patch',
-  preid = 'RC',
-}) {
-  if (help) {
-    printHelp();
-    return;
-  }
-  if (dryRun) {
-    printDryRunBanner();
-  }
-
-  const config = await loadConfig(dir, 'bump-versions.config');
-
-  const { monorepo } = config;
-  if (!updateDependencies) {
-    monorepo.updateDependencies = false;
-  }
-
-  // get current version
-  const currentVersion = monorepo && monorepo.mainVersionFile
-    ? getCurrentVersion(dir, monorepo.mainVersionFile)
-    : getCurrentVersion(dir);
-
-  // determine next version
-  let nextVersion = semver.inc(currentVersion, increment, preid); // set preid if type is 'prerelease'
-  nextVersion = await confirmNextVersion({
-    yes: true,
-    currentVersion,
-    nextVersion,
-    dryRun,
-  });
-  const releaseType = getReleaseType(nextVersion);
-
-  // update
-  const updateVersionFn = monorepo
-    ? updateVersionMonorepo
-    : updateVersion;
-  await updateVersionFn({
-    config, nextVersion, releaseType, dir, dryRun,
-  });
-}
-
-const arg = {
-  '--dir': String,
-  '--help': Boolean,
-  '--dry-run': Boolean,
-  '--update-dependencies': Boolean,
-  '--increment': String,
-  '--preid': String,
-
-  // Aliases
-  '-d': '--dir',
-  '-h': '--help',
-  '-D': '--dry-run',
-  '-i': '--increment',
-};
-
-export default {
-  arg,
-  fn: bumpVersions,
-};

+ 0 - 16
bin/github-actions/bump-versions/index.js

@@ -1,16 +0,0 @@
-#!/usr/bin/env node
-
-// eslint-disable-next-line no-global-assign
-require = require('esm')(module);
-
-(async function() {
-  try {
-    process.env.SHIPJS = true;
-    await require('./cli').cli(process.argv);
-  }
-  catch (e) {
-    // eslint-disable-next-line no-console
-    console.error(e);
-    process.exit(1);
-  }
-}());

+ 0 - 58
bin/github-actions/bump-versions/step/printHelp.js

@@ -1,58 +0,0 @@
-import runStep from 'shipjs/src/step/runStep';
-import { print } from 'shipjs/src/util';
-import { bold, underline } from 'shipjs/src/color';
-
-export default () => runStep({}, () => {
-  const indent = line => `\t${line}`;
-
-  const help = '--help';
-  const dir = `--dir ${underline('PATH')}`;
-  const increment = `--increment ${underline('LEVEL')}`;
-  const preId = `--preid ${underline('IDENTIFIER')}`;
-  const updateDependencies = `--update-dependencies ${underline('true/false')}`;
-  const dryRun = '--dry-run';
-  const all = [help, dir, increment, preId, updateDependencies, dryRun]
-    .map(x => `[${x}]`)
-    .join(' ');
-
-  const messages = [
-    bold('NAME'),
-    indent('bump-versions - Bump versions of packages.'),
-    '',
-    bold('USAGE'),
-    indent(`node ./bin/github-actions/bump-versions ${all}`),
-    '',
-    bold('OPTIONS'),
-    indent(`-h, ${help}`),
-    indent('  Print this help'),
-    '',
-    indent(`-d, ${dir}`),
-    indent(
-      `  Specify the ${underline(
-        'PATH',
-      )} of the repository (default: the current directory).`,
-    ),
-    '',
-    indent(`-i, ${increment}`),
-    indent(
-      `  Specify the ${underline(
-        'LEVEL',
-      )} for semver.inc() to increment a version (default: 'patch').`,
-    ),
-    '',
-    indent(`${preId}`),
-    indent(
-      `  Specify the ${underline(
-        'IDENTIFIER',
-      )} for semver.inc() with 'prerelease' type (default: 'RC').`,
-    ),
-    '',
-    indent(`${updateDependencies}`),
-    indent('  Update dependencies or not (default: true).'),
-    '',
-    indent(`-D, ${dryRun}`),
-    indent('  Displays the steps without actually doing them.'),
-    '',
-  ];
-  print(messages.join('\n'));
-});

+ 0 - 12
bump-versions.config.js

@@ -1,12 +0,0 @@
-/*
- * Reference: https://community.algolia.com/shipjs/
- */
-module.exports = {
-  monorepo: {
-    mainVersionFile: 'package.json',
-    packagesToBump: [
-      'apps/*',
-      'packages/*',
-    ],
-  },
-};

+ 2 - 6
package.json

@@ -38,11 +38,8 @@
     "app:server": "cd apps/app && yarn server",
     "app:server": "cd apps/app && yarn server",
     "slackbot-proxy:build": "turbo run build --filter @growi/slackbot-proxy",
     "slackbot-proxy:build": "turbo run build --filter @growi/slackbot-proxy",
     "slackbot-proxy:server": "cd apps/slackbot-proxy && yarn start:prod",
     "slackbot-proxy:server": "cd apps/slackbot-proxy && yarn start:prod",
-    "bump-versions:premajor": "node ./bin/github-actions/bump-versions -i premajor",
-    "bump-versions:preminor": "node ./bin/github-actions/bump-versions -i preminor",
-    "bump-versions:patch": "node ./bin/github-actions/bump-versions -i patch",
-    "bump-versions:rc": "node ./bin/github-actions/bump-versions -i prerelease",
-    "bump-versions:slackbot-proxy": "node ./bin/github-actions/bump-versions -i prerelease -d apps/slackbot-proxy --preid slackbot-proxy --update-dependencies false"
+    "bump-versions:patch": "turbo run version -- --patch",
+    "bump-versions:rc": "turbo run version -- --prepatch --preid=RC"
   },
   },
   "dependencies": {
   "dependencies": {
     "cross-env": "^7.0.0",
     "cross-env": "^7.0.0",
@@ -89,7 +86,6 @@
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
     "reg-suit": "^0.12.1",
     "reg-suit": "^0.12.1",
-    "shipjs": "^0.24.1",
     "stylelint": "^14.2.0",
     "stylelint": "^14.2.0",
     "stylelint-config-recess-order": "^3.0.0",
     "stylelint-config-recess-order": "^3.0.0",
     "ts-node-dev": "^2.0.0",
     "ts-node-dev": "^2.0.0",

+ 2 - 1
packages/core/package.json

@@ -20,7 +20,8 @@
     "lint:js": "yarn eslint **/*.{js,ts}",
     "lint:js": "yarn eslint **/*.{js,ts}",
     "lint:typecheck": "tsc",
     "lint:typecheck": "tsc",
     "lint": "npm-run-all -p lint:*",
     "lint": "npm-run-all -p lint:*",
-    "test": "vitest run --coverage"
+    "test": "vitest run --coverage",
+    "version": "yarn version --no-git-tag-version"
   },
   },
   "// comments for dependencies": {
   "// comments for dependencies": {
     "escape-string-regexp": "5.0.0 or above exports only ESM"
     "escape-string-regexp": "5.0.0 or above exports only ESM"

+ 1 - 0
packages/core/src/interfaces/page.ts

@@ -31,6 +31,7 @@ export type IPage = {
   deleteUser: Ref<IUser>,
   deleteUser: Ref<IUser>,
   deletedAt: Date,
   deletedAt: Date,
   latestRevision?: Ref<IRevision>,
   latestRevision?: Ref<IRevision>,
+  latestRevisionBodyLength?: number,
   expandContentWidth?: boolean,
   expandContentWidth?: boolean,
 }
 }
 
 

+ 2 - 1
packages/hackmd/package.json

@@ -14,7 +14,8 @@
     "watch": "yarn dev -w --emptyOutDir=false",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",
     "lint:js": "yarn eslint **/*.{js,ts}",
     "lint:typecheck": "tsc",
     "lint:typecheck": "tsc",
-    "lint": "npm-run-all -p lint:*"
+    "lint": "npm-run-all -p lint:*",
+    "version": "yarn version --no-git-tag-version"
   },
   },
   "dependencies": {},
   "dependencies": {},
   "devDependencies": {
   "devDependencies": {

+ 1 - 0
packages/pluginkit/.eslintignore

@@ -0,0 +1 @@
+/dist/**

+ 5 - 0
packages/pluginkit/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  extends: [
+    'plugin:vitest/recommended',
+  ],
+};

+ 1 - 0
packages/pluginkit/.gitignore

@@ -0,0 +1 @@
+/dist

+ 23 - 0
packages/pluginkit/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "@growi/pluginkit",
+  "version": "0.1.0",
+  "license": "MIT",
+  "main": "dist/index.js",
+  "module": "dist/index.mjs",
+  "types": "dist/index.d.ts",
+  "scripts": {
+    "build": "vite build",
+    "clean": "npx -y shx rm -rf dist",
+    "dev": "vite build --mode dev",
+    "watch": "yarn dev -w --emptyOutDir=false",
+    "lint:js": "yarn eslint **/*.{js,ts}",
+    "lint:typecheck": "tsc",
+    "lint": "npm-run-all -p lint:*",
+    "test": "vitest run --coverage"
+  },
+  "dependencies": {
+    "extensible-custom-error": "^0.0.7"
+  },
+  "devDependencies": {
+  }
+}

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

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

+ 6 - 0
packages/pluginkit/src/consts/types.ts

@@ -0,0 +1,6 @@
+export const GrowiPluginType = {
+  SCRIPT: 'script',
+  TEMPLATE: 'template',
+  THEME: 'theme',
+} as const;
+export type GrowiPluginType = typeof GrowiPluginType[keyof typeof GrowiPluginType];

+ 2 - 0
packages/pluginkit/src/index.ts

@@ -0,0 +1,2 @@
+export * from './consts';
+export * from './model';

+ 12 - 0
packages/pluginkit/src/model/growi-plugin-validation-data.ts

@@ -0,0 +1,12 @@
+import { GrowiPluginType } from '../consts/types';
+
+export type GrowiPluginValidationData = {
+  projectDirRoot: string,
+  schemaVersion?: number,
+  expectedPluginType?: GrowiPluginType,
+  actualPluginTypes?: GrowiPluginType[],
+};
+
+export type GrowiTemplatePluginValidationData = GrowiPluginValidationData & {
+  supportingLocales: string[],
+}

+ 15 - 0
packages/pluginkit/src/model/growi-plugin-validation-error.ts

@@ -0,0 +1,15 @@
+import ExtensibleCustomError from 'extensible-custom-error';
+
+import type { GrowiPluginValidationData } from './growi-plugin-validation-data';
+
+
+export class GrowiPluginValidationError<E extends GrowiPluginValidationData = GrowiPluginValidationData> extends ExtensibleCustomError {
+
+  data?: E;
+
+  constructor(message: string, data?: E) {
+    super(message);
+    this.data = data;
+  }
+
+}

+ 2 - 0
packages/pluginkit/src/model/index.ts

@@ -0,0 +1,2 @@
+export * from './growi-plugin-validation-data';
+export * from './growi-plugin-validation-error';

+ 2 - 0
packages/pluginkit/src/server/utils/v4/index.ts

@@ -0,0 +1,2 @@
+export * from './package-json';
+export * from './template';

+ 11 - 0
packages/pluginkit/src/server/utils/v4/package-json/import.spec.ts

@@ -0,0 +1,11 @@
+import path from 'path';
+
+import { importPackageJson } from './import';
+
+it('importPackageJson() returns an object', async() => {
+  // when
+  const pkg = await importPackageJson(path.resolve(__dirname, '../../../../../test/fixtures/example-package/template1'));
+
+  // then
+  expect(pkg).not.toBeNull();
+});

+ 6 - 0
packages/pluginkit/src/server/utils/v4/package-json/import.ts

@@ -0,0 +1,6 @@
+import path from 'path';
+
+export const importPackageJson = async(projectDirRoot: string): Promise<any> => {
+  const packageJsonUrl = path.resolve(projectDirRoot, 'package.json');
+  return import(packageJsonUrl);
+};

+ 2 - 0
packages/pluginkit/src/server/utils/v4/package-json/index.ts

@@ -0,0 +1,2 @@
+export * from './import';
+export * from './validate';

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio