Browse Source

Merge branch 'master' into feat/rich-attachment

ryoji-s 2 years ago
parent
commit
2318408706
100 changed files with 735 additions and 287 deletions
  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
 
     - name: Bump versions for next RC
+      working-directory: ./apps/slackbot-proxy
       run: |
-        yarn bump-versions:slackbot-proxy
+        yarn version --no-git-tag-version --prepatch --preid=slackbot-proxy
 
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.2.0

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

@@ -35,7 +35,8 @@ jobs:
 
     - name: Bump versions
       run: |
-        yarn bump-versions:patch
+        turbo run bump-versions:patch
+        yarn upgrade --scope=@growi
         sh ./apps/app/bin/github-actions/update-readme.sh
 
     - name: Retrieve information from package.json
@@ -97,8 +98,8 @@ jobs:
 
     - name: Bump versions for next RC
       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
       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: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-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": {
     "escape-string-regexp": "5.0.0 or above exports only ESM",
@@ -63,14 +64,14 @@
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
     "@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/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
@@ -208,8 +209,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "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",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^13.2.3",

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

@@ -23,7 +23,8 @@
   "alert": {
     "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "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": {
     "app_settings": "App Settings"

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

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

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

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

+ 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();
       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 Handsontable from 'handsontable';
@@ -7,7 +7,6 @@ import {
   Collapse,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
-import { debounce } from 'throttle-debounce';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
 import mtu from '~/components/PageEditor/MarkdownTableUtil';
@@ -19,6 +18,7 @@ import { MarkdownTableDataImportForm } from './MarkdownTableDataImportForm';
 
 import styles from './HandsontableModal.module.scss';
 import 'handsontable/dist/handsontable.full.min.css';
+import { debounce } from 'throttle-debounce';
 
 const DEFAULT_HOT_HEIGHT = 300;
 const MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING = {
@@ -88,13 +88,37 @@ export const HandsontableModal = (): JSX.Element => {
   const [markdownTable, setMarkdownTable] = useState<MarkdownTable>(defaultMarkdownTable);
   const [markdownTableOnInit, setMarkdownTableOnInit] = useState<MarkdownTable>(defaultMarkdownTable);
   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();
     setMarkdownTable(table ?? defaultMarkdownTable);
     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 = {
     get latest() {
@@ -363,32 +387,6 @@ export const HandsontableModal = (): JSX.Element => {
     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 = () => {
     return {
       items: {
@@ -453,6 +451,7 @@ export const HandsontableModal = (): JSX.Element => {
       size="lg"
       wrapClassName={`${styles['grw-handsontable']}`}
       className={`handsontable-modal ${isWindowExpanded && 'grw-modal-expanded'}`}
+      onOpened={handleModalOpen}
     >
       <ModalHeader tag="h4" toggle={cancel} close={closeButton} className="bg-primary text-light">
         {t('handsontable_modal.title')}
@@ -493,6 +492,7 @@ export const HandsontableModal = (): JSX.Element => {
             data={markdownTable.table}
             settings={handsontableSettings as Handsontable.DefaultSettings}
             height={handsontableHeight}
+            width={handsontableWidth}
             afterLoadData={afterLoadDataHandler}
             modifyColWidth={modifyColWidthHandler}
             beforeColumnMove={beforeColumnMoveHandler}

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

@@ -92,7 +92,10 @@ SearchResultListHead.displayName = 'SearchResultListHead';
 export const SearchPage = (): JSX.Element => {
   const { t } = useTranslation();
   const { data: showPageLimitationL } = useShowPageLimitationL();
+
+  // routerRef solve the problem of infinite redrawing that occurs with routers
   const router = useRouter();
+  const routerRef = useRef(router);
 
   // parse URL Query
   const queries = router.query.q;
@@ -165,10 +168,10 @@ export const SearchPage = (): JSX.Element => {
 
   const initialSearchConditions: Partial<ISearchConditions> = useMemo(() => {
     return {
-      keyword: initQ,
+      keyword,
       limit: INITIAL_PAGIONG_SIZE,
     };
-  }, [initQ]);
+  }, [keyword]);
 
   // for bulk deletion
   const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
@@ -177,8 +180,21 @@ export const SearchPage = (): JSX.Element => {
   useEffect(() => {
     const newUrl = new URL('/_search', 'http://example.com');
     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 allControl = useMemo(() => {

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

@@ -43,8 +43,13 @@ type Props = {
   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 SearchResultContent = dynamic(import('./SearchResultContent').then(mod => mod.SearchResultContent), { ssr: false });
+
   const {
     pages,
     searchingKeyword,
@@ -62,6 +67,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
 
   const [selectedPageIdsByCheckboxes] = useState<Set<string>>(new Set());
   // const [allPageIds] = useState<Set<string>>(new Set());
+
   const [selectedPageWithMeta, setSelectedPageWithMeta] = useState<IPageWithSearchMeta | undefined>();
 
   // publish selectAll()
@@ -108,10 +114,13 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
 
   // select first item on load
   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]);
     }
-  }, [pages, selectedPageWithMeta]);
+  }, [pages, setSelectedPageWithMeta]);
 
   // reset selectedPageIdsByCheckboxes
   useEffect(() => {
@@ -189,7 +198,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
                       pages={pages}
                       selectedPageId={selectedPageWithMeta?.data._id}
                       forceHideMenuItems={forceHideMenuItems}
-                      onPageSelected={page => setSelectedPageWithMeta(page)}
+                      onPageSelected={page => (setSelectedPageWithMeta(page)) }
                       onCheckboxChanged={checkboxChangedHandler}
                     />
                   </div>
@@ -205,7 +214,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
         </div>
 
         <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
               pageWithMeta={selectedPageWithMeta}
               highlightKeywords={highlightKeywords}

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

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

+ 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 { i18n } from '^/config/next-i18next.config';
 
 import { SupportedAction } from '~/interfaces/activity';
@@ -238,6 +240,13 @@ module.exports = (crowi) => {
       user.isEmailPublished = req.body.isEmailPublished;
       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 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 { RegistrationMode } from '~/interfaces/registration-mode';
 import UserRegistrationOrder from '~/server/models/user-registration-order';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:routes:apiv3:user-activation');
 
 const PASSOWRD_MINIMUM_NUMBER = 8;
@@ -69,7 +69,6 @@ export const completeRegistrationAction = (crowi) => {
   const User = crowi.model('User');
   const activityEvent = crowi.event('activity');
   const {
-    configManager,
     aclService,
     appService,
     mailService,
@@ -142,7 +141,8 @@ export const completeRegistrationAction = (crowi) => {
           if (isMailerSetup) {
             const admins = await User.findAdmins();
             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();
 
             sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url);
@@ -205,7 +205,6 @@ export const validateRegisterForm = (req, res, next) => {
 
 async function makeRegistrationEmailToken(email, crowi) {
   const {
-    configManager,
     mailService,
     localeDir,
     appService,
@@ -216,8 +215,7 @@ async function makeRegistrationEmailToken(email, crowi) {
     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 userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email);
@@ -226,12 +224,11 @@ async function makeRegistrationEmailToken(email, crowi) {
   const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
   const url = new URL(`/user-activation/${userRegistrationOrder.token}`, appUrl);
   const oneTimeUrl = url.href;
-  const txtFileName = 'userActivation';
 
   return mailService.send({
     to: email,
     subject: '[GROWI] User Activation',
-    template: path.join(localeDir, `${i18n}/notifications/${txtFileName}.txt`),
+    template: path.join(localeDir, `${locale}/notifications/userActivation.ejs`),
     vars: {
       appTitle: appService.getAppTitle(),
       email,
@@ -248,7 +245,7 @@ export const registerAction = (crowi) => {
     const registerForm = req.body.registerForm || {};
     const email = registerForm.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);
 
     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 Activity from '~/server/models/activity';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
-
 const logger = loggerFactory('growi:routes:apiv3:users');
 
 const path = require('path');
@@ -150,6 +150,7 @@ module.exports = (crowi) => {
   const sendEmailByUserList = async(userList) => {
     const { appService, mailService } = crowi;
     const appTitle = appService.getAppTitle();
+    const locale = configManager.getConfig('crowi', 'app:globalLang');
     const failedToSendEmailList = [];
 
     for (const user of userList) {
@@ -158,7 +159,7 @@ module.exports = (crowi) => {
         await mailService.send({
           to: user.email,
           subject: `Invitation to ${appTitle}`,
-          template: path.join(crowi.localeDir, 'en_US/admin/userInvitation.txt'),
+          template: path.join(crowi.localeDir, `${locale}/admin/userInvitation.ejs`),
           vars: {
             email: user.email,
             password: user.password,

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

@@ -1,4 +1,5 @@
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
 // disable all of linting
@@ -10,7 +11,7 @@ module.exports = function(crowi, app) {
   const path = require('path');
   const User = crowi.model('User');
   const {
-    configManager, appService, aclService, mailService, activityService,
+    appService, aclService, mailService, activityService,
   } = crowi;
   const activityEvent = crowi.event('activity');
 
@@ -19,14 +20,14 @@ module.exports = function(crowi, app) {
   async function sendEmailToAllAdmins(userData) {
     // send mails to all admin users (derived from crowi) -- 2020.06.18 Yuki Takei
     const admins = await User.findAdmins();
-
     const appTitle = appService.getAppTitle();
+    const locale = configManager.getConfig('crowi', 'app:globalLang');
 
     const promises = admins.map((admin) => {
       return mailService.send({
         to: admin.email,
         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: {
           adminUser: admin,
           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';
 
 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 }}
    */
   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
     if (event == null || page == null || triggeredBy == null) {
       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 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 useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
@@ -10,6 +11,7 @@ import { apiv3Get, apiv3Put } from '../client/util/apiv3-client';
 
 import { useStaticSWR } from './use-static-swr';
 
+
 const logger = loggerFactory('growi:stores:personal-settings');
 
 
@@ -66,9 +68,9 @@ export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUse
       await apiv3Put('/personal-setting/', updateData);
       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 {
-  set transition for fill
-  svg * {
+  // set transition for fill
+  svg, svg * {
     transition: fill 0.8s ease-out;
   }
 

+ 0 - 1
apps/app/tsconfig.json

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

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

@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.1.4-RC.0",
+    "@growi/slack": "link:../../packages/slack",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@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",
     "slackbot-proxy:build": "turbo run build --filter @growi/slackbot-proxy",
     "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": {
     "cross-env": "^7.0.0",
@@ -89,7 +86,6 @@
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
     "reg-suit": "^0.12.1",
-    "shipjs": "^0.24.1",
     "stylelint": "^14.2.0",
     "stylelint-config-recess-order": "^3.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:typecheck": "tsc",
     "lint": "npm-run-all -p lint:*",
-    "test": "vitest run --coverage"
+    "test": "vitest run --coverage",
+    "version": "yarn version --no-git-tag-version"
   },
   "// comments for dependencies": {
     "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",
     "lint:js": "yarn eslint **/*.{js,ts}",
     "lint:typecheck": "tsc",
-    "lint": "npm-run-all -p lint:*"
+    "lint": "npm-run-all -p lint:*",
+    "version": "yarn version --no-git-tag-version"
   },
   "dependencies": {},
   "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:styles": "stylelint --allow-empty-input src/**/*.scss src/**/*.css",
     "lint:typecheck": "tsc",
-    "lint": "run-p lint:*"
+    "lint": "run-p lint:*",
+    "version": "yarn version --no-git-tag-version"
   },
   "dependencies": {
-    "@growi/core": "^6.1.4-RC.0"
+    "@growi/core": "link:../core"
   },
   "devDependencies": {
     "@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"
+    ]
+  }
+}

Some files were not shown because too many files changed in this diff