Quellcode durchsuchen

Merge branch 'master' into feat/rich-attachment

ryoji-s vor 2 Jahren
Ursprung
Commit
2318408706
100 geänderte Dateien mit 735 neuen und 287 gelöschten Zeilen
  1. 2 1
      .github/workflows/release-slackbot-proxy.yml
  2. 4 3
      .github/workflows/release.yml
  3. 12 11
      apps/app/package.json
  4. 2 1
      apps/app/public/static/locales/en_US/commons.json
  5. 2 1
      apps/app/public/static/locales/ja_JP/commons.json
  6. 2 1
      apps/app/public/static/locales/zh_CN/commons.json
  7. 0 0
      apps/app/resource/locales/en_US/admin/userInvitation.ejs
  8. 0 0
      apps/app/resource/locales/en_US/admin/userResetPassword.ejs
  9. 0 0
      apps/app/resource/locales/en_US/admin/userWaitingActivation.ejs
  10. 1 1
      apps/app/resource/locales/en_US/notifications/comment.ejs
  11. 0 0
      apps/app/resource/locales/en_US/notifications/notActiveUser.ejs
  12. 1 1
      apps/app/resource/locales/en_US/notifications/pageCreate.ejs
  13. 1 1
      apps/app/resource/locales/en_US/notifications/pageDelete.ejs
  14. 1 1
      apps/app/resource/locales/en_US/notifications/pageEdit.ejs
  15. 1 1
      apps/app/resource/locales/en_US/notifications/pageLike.ejs
  16. 1 1
      apps/app/resource/locales/en_US/notifications/pageMove.ejs
  17. 0 0
      apps/app/resource/locales/en_US/notifications/passwordReset.ejs
  18. 0 0
      apps/app/resource/locales/en_US/notifications/passwordResetSuccessful.ejs
  19. 0 0
      apps/app/resource/locales/en_US/notifications/userActivation.ejs
  20. 1 1
      apps/app/resource/locales/en_US/welcome.md
  21. 0 0
      apps/app/resource/locales/ja_JP/admin/userInvitation.ejs
  22. 0 0
      apps/app/resource/locales/ja_JP/admin/userResetPassword.ejs
  23. 0 0
      apps/app/resource/locales/ja_JP/admin/userWaitingActivation.ejs
  24. 9 0
      apps/app/resource/locales/ja_JP/notifications/comment.ejs
  25. 0 0
      apps/app/resource/locales/ja_JP/notifications/notActiveUser.ejs
  26. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageCreate.ejs
  27. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageCreate.txt
  28. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageDelete.ejs
  29. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageDelete.txt
  30. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageEdit.ejs
  31. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageEdit.txt
  32. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageLike.ejs
  33. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageLike.txt
  34. 5 0
      apps/app/resource/locales/ja_JP/notifications/pageMove.ejs
  35. 0 0
      apps/app/resource/locales/ja_JP/notifications/pageMove.txt
  36. 0 0
      apps/app/resource/locales/ja_JP/notifications/passwordReset.ejs
  37. 0 0
      apps/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.ejs
  38. 0 0
      apps/app/resource/locales/ja_JP/notifications/userActivation.ejs
  39. 0 0
      apps/app/resource/locales/zh_CN/admin/userInvitation.ejs
  40. 0 0
      apps/app/resource/locales/zh_CN/admin/userResetPassword.ejs
  41. 0 0
      apps/app/resource/locales/zh_CN/admin/userWaitingActivation.ejs
  42. 1 1
      apps/app/resource/locales/zh_CN/notifications/comment.ejs
  43. 0 0
      apps/app/resource/locales/zh_CN/notifications/notActiveUser.ejs
  44. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageCreate.ejs
  45. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageDelete.ejs
  46. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageEdit.ejs
  47. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageLike.ejs
  48. 1 1
      apps/app/resource/locales/zh_CN/notifications/pageMove.ejs
  49. 0 0
      apps/app/resource/locales/zh_CN/notifications/passwordReset.ejs
  50. 0 0
      apps/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.ejs
  51. 0 0
      apps/app/resource/locales/zh_CN/notifications/userActivation.ejs
  52. 11 2
      apps/app/src/components/Me/BasicInfoSettings.tsx
  53. 31 31
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  54. 20 4
      apps/app/src/components/SearchPage.tsx
  55. 14 5
      apps/app/src/components/SearchPage/SearchPageBase.tsx
  56. 16 0
      apps/app/src/server/models/user.js
  57. 8 8
      apps/app/src/server/routes/apiv3/forgot-password.js
  58. 9 0
      apps/app/src/server/routes/apiv3/personal-setting.js
  59. 6 9
      apps/app/src/server/routes/apiv3/user-activation.ts
  60. 3 2
      apps/app/src/server/routes/apiv3/users.js
  61. 4 3
      apps/app/src/server/routes/login.js
  62. 3 2
      apps/app/src/server/service/global-notification/global-notification-mail.js
  63. 5 3
      apps/app/src/stores/personal-settings.tsx
  64. 2 2
      apps/app/src/styles/theme/apply-colors.scss
  65. 0 1
      apps/app/tsconfig.json
  66. 1 1
      apps/slackbot-proxy/package.json
  67. 0 4
      bin/github-actions/bump-versions/README.md
  68. 0 18
      bin/github-actions/bump-versions/cli.js
  69. 0 77
      bin/github-actions/bump-versions/flow/bump-versions.js
  70. 0 16
      bin/github-actions/bump-versions/index.js
  71. 0 58
      bin/github-actions/bump-versions/step/printHelp.js
  72. 2 6
      package.json
  73. 2 1
      packages/core/package.json
  74. 2 1
      packages/hackmd/package.json
  75. 1 0
      packages/pluginkit/.eslintignore
  76. 5 0
      packages/pluginkit/.eslintrc.js
  77. 1 0
      packages/pluginkit/.gitignore
  78. 23 0
      packages/pluginkit/package.json
  79. 1 0
      packages/pluginkit/src/consts/index.ts
  80. 6 0
      packages/pluginkit/src/consts/types.ts
  81. 2 0
      packages/pluginkit/src/index.ts
  82. 12 0
      packages/pluginkit/src/model/growi-plugin-validation-data.ts
  83. 15 0
      packages/pluginkit/src/model/growi-plugin-validation-error.ts
  84. 2 0
      packages/pluginkit/src/model/index.ts
  85. 2 0
      packages/pluginkit/src/server/utils/v4/index.ts
  86. 11 0
      packages/pluginkit/src/server/utils/v4/package-json/import.spec.ts
  87. 6 0
      packages/pluginkit/src/server/utils/v4/package-json/import.ts
  88. 2 0
      packages/pluginkit/src/server/utils/v4/package-json/index.ts
  89. 116 0
      packages/pluginkit/src/server/utils/v4/package-json/validate.spec.ts
  90. 41 0
      packages/pluginkit/src/server/utils/v4/package-json/validate.ts
  91. 162 0
      packages/pluginkit/src/server/utils/v4/template.ts
  92. 0 0
      packages/pluginkit/test/fixtures/example-package/template1/index.js
  93. 14 0
      packages/pluginkit/test/fixtures/example-package/template1/package.json
  94. 19 0
      packages/pluginkit/tsconfig.json
  95. 36 0
      packages/pluginkit/vite.config.ts
  96. 19 0
      packages/pluginkit/vitest.config.ts
  97. 3 2
      packages/presentation/package.json
  98. 3 0
      packages/preset-templates/dist/example/ja_JP/meta.json
  99. 1 0
      packages/preset-templates/dist/example/ja_JP/template.md
  100. 22 0
      packages/preset-templates/package.json

+ 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);
+      }
     }
     }
   };
   };
 
 

+ 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}

+ 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,

+ 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();

+ 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'));
-});

+ 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"

+ 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';

+ 116 - 0
packages/pluginkit/src/server/utils/v4/package-json/validate.spec.ts

@@ -0,0 +1,116 @@
+import examplePkg from '^/test/fixtures/example-package/template1/package.json';
+
+import { GrowiPluginType } from '~/consts';
+
+import { validatePackageJson } from './validate';
+
+const mocks = vi.hoisted(() => {
+  return {
+    importPackageJsonMock: vi.fn(),
+  };
+});
+
+vi.mock('./import', () => {
+  return { importPackageJson: mocks.importPackageJsonMock };
+});
+
+describe('validatePackageJson()', () => {
+
+  it('returns a data object', async() => {
+    // setup
+    mocks.importPackageJsonMock.mockResolvedValue(examplePkg);
+
+    // when
+    const data = await validatePackageJson('package.json');
+
+    // then
+    expect(data).not.toBeNull();
+  });
+
+  it("with the 'expectedPluginType' argument returns a data object", async() => {
+    // setup
+    mocks.importPackageJsonMock.mockResolvedValue(examplePkg);
+
+    // when
+    const data = await validatePackageJson('package.json', GrowiPluginType.TEMPLATE);
+
+    // then
+    expect(data).not.toBeNull();
+  });
+
+  describe('should throw an GrowiPluginValidationError', () => {
+
+    it("when the pkg does not have 'growiPlugin' directive", async() => {
+      // setup
+      mocks.importPackageJsonMock.mockResolvedValue({});
+
+      // when
+      const caller = async() => { await validatePackageJson('package.json') };
+
+      // then
+      await expect(caller).rejects.toThrow("The package.json does not have 'growiPlugin' directive.");
+    });
+
+    it("when the 'schemaVersion' is NaN", async() => {
+      // setup
+      mocks.importPackageJsonMock.mockResolvedValue({
+        growiPlugin: {
+          schemaVersion: 'foo',
+        },
+      });
+
+      // when
+      const caller = async() => { await validatePackageJson('package.json') };
+
+      // then
+      await expect(caller).rejects.toThrow("The growiPlugin directive must have a valid 'schemaVersion' directive.");
+    });
+
+    it("when the 'schemaVersion' is less than 4", async() => {
+      // setup
+      mocks.importPackageJsonMock.mockResolvedValue({
+        growiPlugin: {
+          schemaVersion: 3,
+        },
+      });
+
+      // when
+      const caller = async() => { await validatePackageJson('package.json') };
+
+      // then
+      await expect(caller).rejects.toThrow("The growiPlugin directive must have a valid 'schemaVersion' directive.");
+    });
+
+    it("when the 'types' directive does not exist", async() => {
+      // setup
+      mocks.importPackageJsonMock.mockResolvedValue({
+        growiPlugin: {
+          schemaVersion: 4,
+        },
+      });
+
+      // when
+      const caller = async() => { await validatePackageJson('package.json') };
+
+      // then
+      await expect(caller).rejects.toThrow("The growiPlugin directive does not have 'types' directive.");
+    });
+
+    it("when the 'types' directive does not have expected plugin type", async() => {
+      // setup
+      mocks.importPackageJsonMock.mockResolvedValue({
+        growiPlugin: {
+          schemaVersion: 4,
+          types: [GrowiPluginType.TEMPLATE],
+        },
+      });
+
+      // when
+      const caller = async() => { await validatePackageJson('package.json', GrowiPluginType.SCRIPT) };
+
+      // then
+      await expect(caller).rejects.toThrow("The growiPlugin directive does not have expected plugin type in 'types' directive.");
+    });
+  });
+
+});

+ 41 - 0
packages/pluginkit/src/server/utils/v4/package-json/validate.ts

@@ -0,0 +1,41 @@
+import { GrowiPluginType } from '~/consts';
+import { type GrowiPluginValidationData, GrowiPluginValidationError } from '~/model';
+
+import { importPackageJson } from './import';
+
+
+export const validatePackageJson = async(projectDirRoot: string, expectedPluginType?: GrowiPluginType): Promise<GrowiPluginValidationData> => {
+  const pkg = await importPackageJson(projectDirRoot);
+
+  const data: GrowiPluginValidationData = { projectDirRoot };
+
+  const { growiPlugin } = pkg;
+
+  if (growiPlugin == null) {
+    throw new GrowiPluginValidationError("The package.json does not have 'growiPlugin' directive.", data);
+  }
+
+  // schema version checking
+  const schemaVersion = Number(growiPlugin.schemaVersion);
+  data.schemaVersion = schemaVersion;
+  if (Number.isNaN(schemaVersion) || schemaVersion < 4) {
+    throw new GrowiPluginValidationError("The growiPlugin directive must have a valid 'schemaVersion' directive.", data);
+  }
+
+  const types: GrowiPluginType[] = growiPlugin.types;
+  data.actualPluginTypes = types;
+  if (types == null) {
+    throw new GrowiPluginValidationError("The growiPlugin directive does not have 'types' directive.", data);
+  }
+
+  // type checking
+  if (expectedPluginType != null) {
+    data.expectedPluginType = expectedPluginType;
+
+    if (!types.includes(expectedPluginType)) {
+      throw new GrowiPluginValidationError("The growiPlugin directive does not have expected plugin type in 'types' directive.", data);
+    }
+  }
+
+  return data;
+};

+ 162 - 0
packages/pluginkit/src/server/utils/v4/template.ts

@@ -0,0 +1,162 @@
+import assert from 'assert';
+import fs from 'fs';
+import path from 'path';
+import { promisify } from 'util';
+
+import { GrowiPluginType } from '~/consts';
+import type { GrowiPluginValidationData, GrowiTemplatePluginValidationData } from '~/model';
+import { GrowiPluginValidationError } from '~/model';
+
+import { importPackageJson, validatePackageJson } from './package-json';
+
+
+const statAsync = promisify(fs.stat);
+
+
+/**
+ * An utility for template plugin which wrap 'validatePackageJson' of './package-json.ts' module
+ * @param projectDirRoot
+ */
+export const validateTemplatePluginPackageJson = async(projectDirRoot: string): Promise<GrowiTemplatePluginValidationData> => {
+  const data = await validatePackageJson(projectDirRoot, GrowiPluginType.TEMPLATE);
+
+  const pkg = await importPackageJson(projectDirRoot);
+
+  // check supporting locales
+  const supportingLocales: string[] | undefined = pkg.growiPlugin.locales;
+  if (supportingLocales == null || supportingLocales.length === 0) {
+    throw new GrowiPluginValidationError<GrowiPluginValidationData & { supportingLocales?: string[] }>(
+      "Template plugin must have 'supportingLocales' and that must have one or more locales",
+      {
+        ...data,
+        supportingLocales,
+      },
+    );
+  }
+
+  return {
+    ...data,
+    supportingLocales,
+  };
+};
+
+export type TemplateStatus = {
+  id: string,
+  locale: string,
+  isValid: boolean,
+  invalidReason?: string,
+}
+
+type TemplateDirStatus = {
+  isTemplateExists: boolean,
+  isMetaDataFileExists: boolean,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  meta?: any,
+}
+
+async function getStats(tplDir: string): Promise<TemplateDirStatus> {
+  const markdownPath = path.resolve(tplDir, 'template.md');
+  const statForMarkdown = await statAsync(markdownPath);
+  const isTemplateExists = statForMarkdown.isFile();
+
+  const metaDataPath = path.resolve(tplDir, 'meta.json');
+  const statForMetaDataFile = await statAsync(metaDataPath);
+  const isMetaDataFileExists = statForMetaDataFile.isFile();
+
+  const result: TemplateDirStatus = {
+    isTemplateExists,
+    isMetaDataFileExists,
+  };
+
+  if (isMetaDataFileExists) {
+    result.meta = await import(metaDataPath);
+  }
+
+  return result;
+}
+
+export const scanTemplateStatus = async(projectDirRoot: string, templateId: string, data: GrowiTemplatePluginValidationData): Promise<TemplateStatus[]> => {
+  const status: TemplateStatus[] = [];
+
+  const tplRootDirPath = path.resolve(projectDirRoot, 'dist', templateId);
+
+  for await (const locale of data.supportingLocales) {
+    const tplDir = path.resolve(tplRootDirPath, locale);
+
+    try {
+      const {
+        isTemplateExists, isMetaDataFileExists, meta,
+      } = await getStats(tplDir);
+
+      if (!isTemplateExists) throw new Error("'template.md does not exist.");
+      if (!isMetaDataFileExists) throw new Error("'meta.md does not exist.");
+      if (meta?.title == null) throw new Error("'meta.md does not contain the title.");
+
+      status.push({ id: templateId, locale, isValid: true });
+    }
+    catch (err) {
+      status.push({
+        id: templateId,
+        locale,
+        isValid: false,
+        invalidReason: err.message,
+      });
+    }
+  }
+
+  // eslint-disable-next-line no-console
+  console.debug({ status });
+
+  return status;
+};
+
+export const scanAllTemplateStatus = async(projectDirRoot: string, data: GrowiTemplatePluginValidationData): Promise<TemplateStatus[]> => {
+  const status: TemplateStatus[] = [];
+
+  const distDirPath = path.resolve(projectDirRoot, 'dist');
+  const distDirFiles = fs.readdirSync(distDirPath);
+
+  for await (const templateId of distDirFiles) {
+    status.push(...await scanTemplateStatus(projectDirRoot, templateId, data));
+  }
+
+  return status;
+};
+
+
+export const validateTemplatePlugin = async(projectDirRoot: string): Promise<boolean> => {
+  const data = await validateTemplatePluginPackageJson(projectDirRoot);
+
+  const results = await scanAllTemplateStatus(projectDirRoot, data);
+
+  if (results.length === 0) {
+    throw new Error('This plugin does not have any templates');
+  }
+
+  // construct map
+  // key: id
+  // value: isValid properties
+  const idValidMap: { [id: string]: boolean[] } = {};
+  results.forEach((status) => {
+    const validMap = idValidMap[status.id] ?? [];
+    validMap.push(status.isValid);
+    idValidMap[status.id] = validMap;
+  });
+
+  for (const [id, validMap] of Object.entries(idValidMap)) {
+    assert(validMap.length === data.supportingLocales.length);
+
+    // warn
+    if (!validMap.every(bool => bool)) {
+      // eslint-disable-next-line no-console
+      console.warn(`[WARN] Template '${id}' has invalid status`);
+    }
+
+    // This means the template directory does not have any valid template
+    if (!validMap.some(bool => bool)) {
+      return false;
+    }
+  }
+
+  return true;
+};

+ 0 - 0
apps/app/resource/locales/ja_JP/notifications/comment.txt → packages/pluginkit/test/fixtures/example-package/template1/index.js


+ 14 - 0
packages/pluginkit/test/fixtures/example-package/template1/package.json

@@ -0,0 +1,14 @@
+{
+  "name": "example-package-template1",
+  "version": "1.0.0",
+  "main": "index.js",
+  "growiPlugin": {
+    "schemaVersion": "4",
+    "types": [
+      "template"
+    ],
+    "locales": [
+      "en_US", "ja_JP", "zh_CN"
+    ]
+  }
+}

+ 19 - 0
packages/pluginkit/tsconfig.json

@@ -0,0 +1,19 @@
+{
+  "$schema": "http://json.schemastore.org/tsconfig",
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "module": "CommonJS",
+    "types": [
+      "node",
+      "vitest/globals"
+    ],
+    "baseUrl": ".",
+    "paths": {
+      "^/*": ["./*"],
+      "~/*": ["./src/*"]
+    }
+  },
+  "include": [
+    "src"
+  ]
+}

+ 36 - 0
packages/pluginkit/vite.config.ts

@@ -0,0 +1,36 @@
+import path from 'path';
+
+
+import glob from 'glob';
+import { defineConfig } from 'vite';
+import dts from 'vite-plugin-dts';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    dts(),
+    tsconfigPaths(),
+  ],
+  build: {
+    outDir: 'dist',
+    sourcemap: true,
+    lib: {
+      entry: glob.sync(path.resolve(__dirname, 'src/**/*.ts')),
+      name: 'pluginkit-libs',
+      formats: ['es', 'cjs'],
+    },
+    rollupOptions: {
+      output: {
+        preserveModules: true,
+        preserveModulesRoot: 'src',
+      },
+      external: [
+        'assert',
+        'fs',
+        'path',
+        'util',
+      ],
+    },
+  },
+});

+ 19 - 0
packages/pluginkit/vitest.config.ts

@@ -0,0 +1,19 @@
+import tsconfigPaths from 'vite-tsconfig-paths';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  plugins: [
+    tsconfigPaths(),
+  ],
+  test: {
+    environment: 'node',
+    clearMocks: true,
+    globals: true,
+    coverage: {
+      lines: 100,
+      functions: 100,
+      branches: 100,
+      statements: 100,
+    },
+  },
+});

+ 3 - 2
packages/presentation/package.json

@@ -15,10 +15,11 @@
     "lint:js": "yarn eslint **/*.{js,jsx,ts,tsx}",
     "lint:js": "yarn eslint **/*.{js,jsx,ts,tsx}",
     "lint:styles": "stylelint --allow-empty-input src/**/*.scss src/**/*.css",
     "lint:styles": "stylelint --allow-empty-input src/**/*.scss src/**/*.css",
     "lint:typecheck": "tsc",
     "lint:typecheck": "tsc",
-    "lint": "run-p lint:*"
+    "lint": "run-p lint:*",
+    "version": "yarn version --no-git-tag-version"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.1.4-RC.0"
+    "@growi/core": "link:../core"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@marp-team/marp-core": "^3.6.0",
     "@marp-team/marp-core": "^3.6.0",

+ 3 - 0
packages/preset-templates/dist/example/ja_JP/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "Example"
+}

+ 1 - 0
packages/preset-templates/dist/example/ja_JP/template.md

@@ -0,0 +1 @@
+# Example

+ 22 - 0
packages/preset-templates/package.json

@@ -0,0 +1,22 @@
+{
+  "name": "@growi/preset-templates",
+  "version": "6.1.4-RC.0",
+  "scripts": {
+    "test": "vitest run",
+    "version": "yarn version --no-git-tag-version"
+  },
+  "dependencies": {
+  },
+  "devDependencies": {
+    "@growi/pluginkit": "link:../pluginkit"
+  },
+  "growiPlugin": {
+    "schemaVersion": "4",
+    "types": [
+      "template"
+    ],
+    "locales": [
+      "en_US", "ja_JP", "zh_CN"
+    ]
+  }
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.