Răsfoiți Sursa

Merge branch 'master'

yuya-o 3 ani în urmă
părinte
comite
358f16b7d8
100 a modificat fișierele cu 909 adăugiri și 647 ștergeri
  1. 2 4
      .github/workflows/ci-app-prod.yml
  2. 3 4
      .github/workflows/ci-app.yml
  3. 5 1
      .github/workflows/reusable-app-prod.yml
  4. 2 6
      .vscode/launch.json
  5. 1 1
      lerna.json
  6. 1 1
      package.json
  7. 0 0
      packages-obsolete/plugin-attachment-refs/.eslintignore
  8. 0 0
      packages-obsolete/plugin-attachment-refs/.gitignore
  9. 0 0
      packages-obsolete/plugin-attachment-refs/README.md
  10. 1 1
      packages-obsolete/plugin-attachment-refs/package.json
  11. 0 0
      packages-obsolete/plugin-attachment-refs/src/client-entry.js
  12. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/css/index.css
  13. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/components/AttachmentList.jsx
  14. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/components/ExtractedAttachments.jsx
  15. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/GalleryContext.js
  16. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js
  17. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js
  18. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/RefsContext.js
  19. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js
  20. 0 0
      packages-obsolete/plugin-attachment-refs/src/index.js
  21. 0 0
      packages-obsolete/plugin-attachment-refs/src/server-entry.js
  22. 0 0
      packages-obsolete/plugin-attachment-refs/src/server/routes/index.js
  23. 0 0
      packages-obsolete/plugin-attachment-refs/src/server/routes/refs.js
  24. 0 0
      packages-obsolete/plugin-attachment-refs/src/utils/logger/index.ts
  25. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.base.json
  26. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.build.cjs.json
  27. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.build.esm.json
  28. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.json
  29. 16 14
      packages/app/_obsolete/src/client/services/PageContainer.js
  30. 0 17
      packages/app/bin/templates/plugin-definitions.js.swig
  31. 2 1
      packages/app/cypress.json
  32. 2 2
      packages/app/docker/Dockerfile
  33. 1 0
      packages/app/next.config.js
  34. 10 10
      packages/app/package.json
  35. 10 8
      packages/app/public/static/locales/en_US/admin.json
  36. 37 2
      packages/app/public/static/locales/en_US/commons.json
  37. 0 20
      packages/app/public/static/locales/en_US/translation.json
  38. 9 7
      packages/app/public/static/locales/ja_JP/admin.json
  39. 37 2
      packages/app/public/static/locales/ja_JP/commons.json
  40. 1 18
      packages/app/public/static/locales/ja_JP/translation.json
  41. 9 7
      packages/app/public/static/locales/zh_CN/admin.json
  42. 37 2
      packages/app/public/static/locales/zh_CN/commons.json
  43. 0 18
      packages/app/public/static/locales/zh_CN/translation.json
  44. 1 1
      packages/app/resource/locales/en_US/sandbox-diagrams.md
  45. 1 1
      packages/app/resource/locales/ja_JP/sandbox-diagrams.md
  46. 1 1
      packages/app/resource/locales/zh_CN/sandbox-diagrams.md
  47. 0 2
      packages/app/src/client/services/AdminHomeContainer.js
  48. 51 0
      packages/app/src/client/services/activate-plugin.ts
  49. 50 37
      packages/app/src/client/services/page-operation.ts
  50. 0 21
      packages/app/src/client/util/editor.ts
  51. 0 8
      packages/app/src/components/Admin/AdminHome/AdminHome.jsx
  52. 0 55
      packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx
  53. 2 2
      packages/app/src/components/Admin/App/AppSetting.jsx
  54. 1 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  55. 6 6
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  56. 21 0
      packages/app/src/components/Admin/Common/AdminInstallButtonRow.tsx
  57. 3 0
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  58. 28 3
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  59. 1 3
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  60. 2 20
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  61. 3 3
      packages/app/src/components/Admin/Notification/NotificationSetting.jsx
  62. 1 1
      packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  63. 13 0
      packages/app/src/components/Admin/PluginsExtension/Loading.js
  64. 68 0
      packages/app/src/components/Admin/PluginsExtension/PluginCard.module.scss
  65. 85 0
      packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx
  66. 91 0
      packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx
  67. 58 0
      packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx
  68. 2 2
      packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  69. 13 11
      packages/app/src/components/Admin/Security/ShareLinkSetting.tsx
  70. 2 2
      packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  71. 1 1
      packages/app/src/components/Admin/UserManagement.tsx
  72. 2 1
      packages/app/src/components/Admin/Users/GiveAdminButton.tsx
  73. 2 1
      packages/app/src/components/Admin/Users/RemoveAdminMenuItem.tsx
  74. 2 1
      packages/app/src/components/Admin/Users/StatusSuspendMenuItem.tsx
  75. 0 132
      packages/app/src/components/Admin/Users/UserMenu.jsx
  76. 5 0
      packages/app/src/components/Admin/Users/UserMenu.module.scss
  77. 114 0
      packages/app/src/components/Admin/Users/UserMenu.tsx
  78. 1 1
      packages/app/src/components/BookmarkButtons.tsx
  79. 6 5
      packages/app/src/components/Comments.tsx
  80. 1 1
      packages/app/src/components/Common/ImageCropModal.tsx
  81. 2 2
      packages/app/src/components/CreateTemplateModal.jsx
  82. 0 117
      packages/app/src/components/Drawio.tsx
  83. 1 1
      packages/app/src/components/EmptyTrashModal.tsx
  84. 17 2
      packages/app/src/components/EventListeneres/HashChanged.tsx
  85. 1 1
      packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
  86. 1 1
      packages/app/src/components/InAppNotification/InAppNotificationElm.tsx
  87. 5 3
      packages/app/src/components/InAppNotification/InAppNotificationPage.tsx
  88. 5 3
      packages/app/src/components/InstallerForm.tsx
  89. 1 1
      packages/app/src/components/Layout/AdminLayout.tsx
  90. 0 2
      packages/app/src/components/Layout/BasicLayout.tsx
  91. 0 10
      packages/app/src/components/Layout/NoLoginLayout.module.scss
  92. 1 1
      packages/app/src/components/LikeButtons.tsx
  93. 11 6
      packages/app/src/components/LoginForm.tsx
  94. 1 1
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  95. 1 1
      packages/app/src/components/Navbar/AuthorInfo.module.scss
  96. 15 5
      packages/app/src/components/Navbar/GlobalSearch.tsx
  97. 9 13
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  98. 9 3
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  99. 2 2
      packages/app/src/components/Navbar/GrowiSubNavigation.module.scss
  100. 3 3
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx

+ 2 - 4
.github/workflows/ci-app-prod.yml

@@ -14,10 +14,9 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
-      - packages/remark-growi-plugin/**
+      - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
-      - packages/plugin-**
   pull_request:
     branches:
       - master
@@ -32,10 +31,9 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
-      - packages/remark-growi-plugin/**
+      - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
-      - packages/plugin-**
   workflow_call:
     inputs:
       cypress-config-video:

+ 3 - 4
.github/workflows/ci-app.yml

@@ -16,10 +16,9 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
-      - packages/remark-growi-plugin/**
+      - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
-      - packages/plugin-*/**
 
 jobs:
   lint:
@@ -55,7 +54,7 @@ jobs:
 
       - name: lerna run lint for plugins
         run: |
-          yarn lerna run lint --scope @growi/remark-growi-plugin --scope @growi/plugin-*
+          yarn lerna run lint --scope @growi/remark-*
       - name: lerna run lint for app
         run: |
           yarn lerna run lint --scope @growi/app --scope @growi/codemirror-textlint --scope @growi/core --scope @growi/slack --scope @growi/ui
@@ -109,7 +108,7 @@ jobs:
 
       - name: lerna run test for plugins
         run: |
-          yarn lerna run test --scope @growi/remark-growi-plugin --scope @growi/plugin-*
+          yarn lerna run test --scope @growi/remark-*
 
       - name: Test app
         working-directory: ./packages/app

+ 5 - 1
.github/workflows/reusable-app-prod.yml

@@ -235,7 +235,11 @@ jobs:
 
     - name: lerna bootstrap
       run: |
-        npx lerna bootstrap -- --frozen-lockfile
+        npx lerna bootstrap -- --production
+
+    - name: lerna add packages needed for CI
+      run: |
+        npx lerna add yargs
 
     - name: Download production files artifact
       uses: actions/download-artifact@v3

+ 2 - 6
.vscode/launch.json

@@ -73,12 +73,8 @@
             "path": "${workspaceFolder}/packages/core"
           },
           {
-            "url": "webpack://_n_e/plugin-attachment-refs",
-            "path": "${workspaceFolder}/packages/plugin-attachment-refs"
-          },
-          {
-            "url": "webpack://_n_e/plugin-lsx",
-            "path": "${workspaceFolder}/packages/plugin-lsx"
+            "url": "webpack://_n_e/remark-lsx",
+            "path": "${workspaceFolder}/packages/remark-lsx"
           },
           {
             "url": "webpack://_n_e/slack",

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "6.0.0-RC.8",
+  "version": "6.0.0-RC.9",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.0.0-RC.8",
+  "version": "6.0.0-RC.9",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 0 - 0
packages/plugin-attachment-refs/.eslintignore → packages-obsolete/plugin-attachment-refs/.eslintignore


+ 0 - 0
packages/plugin-attachment-refs/.gitignore → packages-obsolete/plugin-attachment-refs/.gitignore


+ 0 - 0
packages/plugin-attachment-refs/README.md → packages-obsolete/plugin-attachment-refs/README.md


+ 1 - 1
packages/plugin-attachment-refs/package.json → packages-obsolete/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "6.0.0-RC.8",
+  "version": "6.0.0-RC.9",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 0 - 0
packages/plugin-attachment-refs/src/client-entry.js → packages-obsolete/plugin-attachment-refs/src/client-entry.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/css/index.css → packages-obsolete/plugin-attachment-refs/src/client/css/index.css


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/components/AttachmentList.jsx → packages-obsolete/plugin-attachment-refs/src/client/js/components/AttachmentList.jsx


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/components/ExtractedAttachments.jsx → packages-obsolete/plugin-attachment-refs/src/client/js/components/ExtractedAttachments.jsx


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/GalleryContext.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/GalleryContext.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/RefsContext.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/RefsContext.js


+ 0 - 0
packages/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js → packages-obsolete/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js


+ 0 - 0
packages/plugin-attachment-refs/src/index.js → packages-obsolete/plugin-attachment-refs/src/index.js


+ 0 - 0
packages/plugin-attachment-refs/src/server-entry.js → packages-obsolete/plugin-attachment-refs/src/server-entry.js


+ 0 - 0
packages/plugin-attachment-refs/src/server/routes/index.js → packages-obsolete/plugin-attachment-refs/src/server/routes/index.js


+ 0 - 0
packages/plugin-attachment-refs/src/server/routes/refs.js → packages-obsolete/plugin-attachment-refs/src/server/routes/refs.js


+ 0 - 0
packages/plugin-attachment-refs/src/utils/logger/index.ts → packages-obsolete/plugin-attachment-refs/src/utils/logger/index.ts


+ 0 - 0
packages/plugin-attachment-refs/tsconfig.base.json → packages-obsolete/plugin-attachment-refs/tsconfig.base.json


+ 0 - 0
packages/plugin-attachment-refs/tsconfig.build.cjs.json → packages-obsolete/plugin-attachment-refs/tsconfig.build.cjs.json


+ 0 - 0
packages/plugin-attachment-refs/tsconfig.build.esm.json → packages-obsolete/plugin-attachment-refs/tsconfig.build.esm.json


+ 0 - 0
packages/plugin-attachment-refs/tsconfig.json → packages-obsolete/plugin-attachment-refs/tsconfig.json


+ 16 - 14
packages/app/_obsolete/src/client/services/PageContainer.js

@@ -277,12 +277,13 @@ export default class PageContainer extends Container {
     });
   }
 
-  // request to server so the client to join a room for each page
-  emitJoinPageRoomRequest() {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-    const socket = socketIoContainer.getSocket();
-    socket.emit('join:page', { socketId: socket.id, pageId: this.state.pageId });
-  }
+  // replaced accompanied by omiting container: https://github.com/weseek/growi/pull/6968
+  // // request to server so the client to join a room for each page
+  // emitJoinPageRoomRequest() {
+  //   const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
+  //   const socket = socketIoContainer.getSocket();
+  //   socket.emit('join:page', { socketId: socket.id, pageId: this.state.pageId });
+  // }
 
   addWebSocketEventHandlers() {
     // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -300,15 +301,16 @@ export default class PageContainer extends Container {
       }
     });
 
-    socket.on('page:update', (data) => {
-      logger.debug({ obj: data }, `websocket on 'page:update'`); // eslint-disable-line quotes
+    // replaced accompanied by omiting container: https://github.com/weseek/growi/pull/6968
+    // socket.on('page:update', (data) => {
+    //   logger.debug({ obj: data }, `websocket on 'page:update'`); // eslint-disable-line quotes
 
-      // update remote page data
-      const { s2cMessagePageUpdated } = data;
-      if (s2cMessagePageUpdated.pageId === pageContainer.state.pageId) {
-        pageContainer.setLatestRemotePageData(s2cMessagePageUpdated);
-      }
-    });
+    //   // update remote page data
+    //   const { s2cMessagePageUpdated } = data;
+    //   if (s2cMessagePageUpdated.pageId === pageContainer.state.pageId) {
+    //     pageContainer.setLatestRemotePageData(s2cMessagePageUpdated);
+    //   }
+    // });
 
     socket.on('page:delete', (data) => {
       logger.debug({ obj: data }, `websocket on 'page:delete'`); // eslint-disable-line quotes

+ 0 - 17
packages/app/bin/templates/plugin-definitions.js.swig

@@ -1,17 +0,0 @@
-/*
- * !! don't commit this file !!
- * !!      just revert       !!
- */
-module.exports = [
-  {% for definition in definitions %}{
-    name: '{{ definition.name }}',
-    meta: require('{{ definition.name }}'),
-    entries: [
-      {% for entryPath in definition.entries %}
-      require('{{ entryPath }}').default,
-      {% endfor %}
-    ]
-  },
-  {% endfor %}
-
-]

+ 2 - 1
packages/app/cypress.json

@@ -13,5 +13,6 @@
   "viewportWidth": 1400,
   "viewportHeight": 1024,
 
-  "experimentalSessionSupport": true
+  "experimentalSessionSupport": true,
+  "defaultCommandTimeout": 30000
 }

+ 2 - 2
packages/app/docker/Dockerfile

@@ -105,11 +105,11 @@ COPY ["package.json", "lerna.json", "tsconfig.base.json", "./"]
 COPY packages/app packages/app
 COPY packages/core packages/core
 COPY packages/codemirror-textlint packages/codemirror-textlint
-COPY packages/plugin-attachment-refs packages/plugin-attachment-refs
-COPY packages/plugin-lsx packages/plugin-lsx
 COPY packages/slack packages/slack
 COPY packages/ui packages/ui
+COPY packages/remark-drawio-plugin packages/remark-drawio-plugin
 COPY packages/remark-growi-plugin packages/remark-growi-plugin
+COPY packages/remark-lsx packages/remark-lsx
 COPY packages/hackmd packages/hackmd
 
 # build

+ 1 - 0
packages/app/next.config.js

@@ -25,6 +25,7 @@ const setupTranspileModules = () => {
     // listing ESM packages until experimental.esmExternals works correctly to avoid ERR_REQUIRE_ESM
     'react-markdown',
     'unified',
+    'markdown-table',
     'character-entities-html4',
     'comma-separated-tokens',
     'decode-named-character-reference',

+ 10 - 10
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.0-RC.8",
+  "version": "6.0.0-RC.9",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -47,7 +47,6 @@
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "resources:hackmd": "yarn lerna run build --scope=@growi/hackmd",
     "resources:dummy": "true",
-    "// resources:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
     "// resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
     "ts-node": "node -r ts-node/register -r tsconfig-paths/register -r dotenv-flow/config"
   },
@@ -65,12 +64,13 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.0-RC.8",
-    "@growi/core": "^6.0.0-RC.8",
-    "@growi/hackmd": "^6.0.0-RC.8",
-    "@growi/plugin-attachment-refs": "^6.0.0-RC.8",
-    "@growi/plugin-lsx": "^6.0.0-RC.8",
-    "@growi/slack": "^6.0.0-RC.8",
+    "@growi/codemirror-textlint": "^6.0.0-RC.9",
+    "@growi/core": "^6.0.0-RC.9",
+    "@growi/hackmd": "^6.0.0-RC.9",
+    "@growi/remark-drawio-plugin": "^6.0.0-RC.9",
+    "@growi/remark-growi-plugin": "^6.0.0-RC.9",
+    "@growi/remark-lsx": "^6.0.0-RC.9",
+    "@growi/slack": "^6.0.0-RC.9",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -122,6 +122,7 @@
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
     "lucene-query-parser": "^1.2.0",
+    "markdown-table": "^1.1.1",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "migrate-mongo": "^8.2.3",
@@ -205,7 +206,7 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^6.0.0-RC.8",
+    "@growi/ui": "^6.0.0-RC.9",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
@@ -236,7 +237,6 @@
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",
-    "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
     "next-transpile-modules": "^9.0.0",

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

@@ -16,8 +16,8 @@
     "always_displayed": "Always displayed",
     "displayed_or_hidden": "Displayed / Hidden",
     "Fixed by env var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
-    "Register limitation": "Register limitation",
-    "Register limitation desc": "Restriction of new users' registration",
+    "register_limitation": "Register limitation",
+    "register_limitation_desc": "Restriction of new users' registration",
     "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
     "users_without_account": "Users without account is not accessible",
     "example": "Example",
@@ -285,7 +285,6 @@
     "system_information": "System information",
     "wiki_administrator": "Only wiki administrator can access this page",
     "assign_administrator": "You can assign the selected user to be a wiki administrator on the User Management page using the 'Give admin access' button",
-    "list_of_installed_plugins": "List of installed plugins",
     "package_name": "Package name",
     "specified_version": "Specified version",
     "installed_version": "Installed version",
@@ -329,9 +328,12 @@
     "site_name": "Site name",
     "sitename_change": "You can change site name which is used for header and HTML title.",
     "header_content": "The contents entered here will be shown in the header etc.",
-    "site_url_desc": "This is for the site URL setting.",
-    "site_url_warn": "Some features don't work because the site URL is not set.",
-    "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
+    "site_url": {
+      "title": "Site URL settings",
+      "desc": "This is for the site URL setting.",
+      "warn": "Some features don't work because the site URL is not set.",
+      "help": "Site full URL beginning from <code>http://</code> or <code>https://</code>."
+    },
     "confidential_name": "Confidential name",
     "confidential_example": "ex): internal use only",
     "default_language": "Default language for new users",
@@ -377,7 +379,7 @@
     "custom_endpoint_change": "Input the URL of the endpoint of an object storage service like MinIO that has a S3-compatible API.  Amazon S3 is used if empty.",
     "plugin_settings": "Plugin settings",
     "enable_plugin_loading": "Enable plugin loading",
-    "load_plugins": "Load_plugins",
+    "load_plugins": "Load plugins",
     "enable": "Enable",
     "disable": "Disable",
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
@@ -577,7 +579,7 @@
     "export": "Export",
     "cancel": "Cancel",
     "file": "File",
-    "growi_version": "Growi Version",
+    "growi_version": "GROWI Version",
     "collections": "Collections",
     "exported_at": "Exported At",
     "export_menu": "Export Menu",

+ 37 - 2
packages/app/public/static/locales/en_US/commons.json

@@ -1,4 +1,11 @@
 {
+  "Show": "Show",
+  "Hide": "Hide",
+  "Add": "Add",
+  "Reset": "Reset",
+  "Sign out": "Logout",
+  "New": "New",
+
   "meta": {
     "display_name": "English"
   },
@@ -37,9 +44,22 @@
     "description": "Description"
   },
 
+  "in_app_notification": {
+    "notification_list": "In-App Notification List",
+    "see_all": "See All",
+    "no_notification": "You don't have any notificatios.",
+    "all": "All",
+    "unopend": "Unread",
+    "mark_all_as_read": "Mark all as read"
+  },
+
   "personal_dropdown": {
     "home": "Home",
-    "settings": "Settings"
+    "settings": "Settings",
+    "color_mode": "Color mode",
+    "sidebar_mode": "Sidebar mode",
+    "sidebar_mode_editor": "Sidebar mode on editor",
+    "use_os_settings": "Use OS settings"
   },
 
   "copy_to_clipboard": {
@@ -55,10 +75,25 @@
     "image_crop": "Image Crop",
     "crop": "Crop",
     "save": "Save",
-    "reset": "Reset",
     "cancel": "Cancel"
   },
 
+  "handsontable_modal": {
+    "title": "Edit Table",
+    "data_import": "Data Import",
+    "save": "Save",
+    "cancel": "Cancel",
+    "done": "Done",
+    "data_import_form": {
+      "select_data_format": "Select Data Format",
+      "import_data": "Import Data",
+      "paste_table_data": "Paste table data",
+      "parse_error": "Parse Error",
+      "cancel": "Cancel",
+      "import": "Import"
+    }
+  },
+
   "not_found_page": {
     "page_not_exist": "This page does not exist."
   }

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

@@ -28,7 +28,6 @@
   "administrator": "Admin",
   "Tag": "Tag",
   "Tags": "Tags",
-  "New": "New",
   "Close": "Close",
   "Shortcuts": "Shortcuts",
   "CustomSidebar": "Custom Sidebar",
@@ -100,8 +99,6 @@
   "Updated": "Updated",
   "Upload new image": "Upload new image",
   "Connected": "Connected",
-  "Show": "Show",
-  "Hide": "Hide",
   "Loading": "Loading...",
   "Disclose E-mail": "Disclose E-mail",
   "page exists": "this page already exists",
@@ -115,12 +112,10 @@
   "V5 Page Migration": "Convert To V5 Compatibility",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "See_more_detail_on_new_schema": "See more detail on <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a> <i class='icon-share-alt'></i> ",
-  "Site URL settings": "Site URL settings",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "Basic Settings": "Basic Settings",
   "Basic authentication": "Basic authentication",
-  "Register limitation": "Register limitation",
   "The contents entered here will be shown in the header etc": "The contents entered here will be shown in the header etc",
   "Public": "Public",
   "Anyone with the link": "Anyone with the link",
@@ -141,7 +136,6 @@
   "edited this page": "edited this page.",
   "List Drafts": "Drafts",
   "Deleted Pages": "Deleted Pages",
-  "Sign out": "Logout",
   "Disassociate": "Disassociate",
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "Add to Bookmarks",
@@ -158,12 +152,6 @@
   "not_allowed_to_see_this_page": "You cannot see this page",
   "Confirm": "Confirm",
   "Successfully requested": "Successfully requested.",
-  "personal_dropdown": {
-    "color_mode": "Color mode",
-    "sidebar_mode": "Sidebar mode",
-    "sidebar_mode_editor": "Sidebar mode on editor",
-    "use_os_settings": "Use OS settings"
-  },
   "form_validation": {
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
@@ -246,14 +234,6 @@
   "API Token Settings": "API token settings",
   "Current API Token": "Current API token",
   "Update API Token": "Update API token",
-  "in_app_notification": {
-    "notification_list": "In-App Notification List",
-    "see_all": "See All",
-    "no_notification": "You don't have any notificatios.",
-    "all": "All",
-    "unopend": "Unread",
-    "mark_all_as_read": "Mark all as read"
-  },
   "in_app_notification_settings": {
     "in_app_notification_settings": "In-App Notification Settings",
     "subscribe_settings": "Settings to automatically subscribe (Receive notifications) to pages",

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

@@ -26,8 +26,8 @@
     "always_displayed": "表示 (固定)",
     "displayed_or_hidden": "表示 / 非表示",
     "Fixed by env var": "環境変数 <code>{{forcewikimode}}={{wikimode}}</code> により固定されています。",
-    "Register limitation": "登録の制限",
-    "Register limitation desc": "新しいユーザーを登録する方法を制限します.",
+    "register_limitation": "登録の制限",
+    "register_limitation_desc": "新しいユーザーを登録する方法を制限します。",
     "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "example": "例",
@@ -311,7 +311,6 @@
     "system_information": "システム情報",
     "wiki_administrator": "この画面はWiki管理者のみがアクセスできる画面です。",
     "assign_administrator": "「ユーザー管理」から「管理者にする」ボタンを使ってユーザーをWiki管理者に任命することができます。",
-    "list_of_installed_plugins": "インストールされているプラグイン一覧",
     "package_name": "パッケージ名",
     "specified_version": "指定バージョン",
     "installed_version": "インストールされているバージョン",
@@ -355,9 +354,12 @@
     "site_name": "サイト名",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
-    "site_url_desc": "サイトURLを設定します。",
-    "site_url_warn": "サイトURLが設定されていないため、一部機能が動作しない状態になっています。",
-    "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL",
+    "site_url": {
+      "title": "サイトURL設定",
+      "desc": "サイトURLを設定します。",
+      "warn": "サイトURLが設定されていないため、一部機能が動作しない状態になっています。",
+      "help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL"
+    },
     "confidential_name": "コンフィデンシャル表示",
     "confidential_example": "例: 社外秘",
     "default_language": "新規ユーザーのデフォルト設定言語",
@@ -531,7 +533,7 @@
     "export": "エクスポート",
     "cancel": "キャンセル",
     "file": "ファイル名",
-    "growi_version": "Growi バージョン",
+    "growi_version": "GROWI バージョン",
     "collections": "コレクション",
     "exported_at": "エクスポートされた時間",
     "export_menu": "エクスポートメニュー",

+ 37 - 2
packages/app/public/static/locales/ja_JP/commons.json

@@ -1,4 +1,11 @@
 {
+  "Show": "公開",
+  "Hide": "非公開",
+  "Add": "追加",
+  "Reset": "リセット",
+  "Sign out": "ログアウト",
+  "New": "作成",
+
   "meta": {
     "display_name": "日本語"
   },
@@ -37,9 +44,22 @@
     "description": "概要"
   },
 
+  "in_app_notification": {
+    "notification_list": "アプリ内通知一覧",
+    "see_all": "通知一覧を見る",
+    "no_notification": "通知はありません",
+    "all": "全て",
+    "unopend": "未読",
+    "mark_all_as_read": "全て既読にする"
+  },
+
   "personal_dropdown": {
     "home": "ホーム",
-    "settings": "設定"
+    "settings": "設定",
+    "color_mode": "カラーモード",
+    "sidebar_mode": "サイドバーモード",
+    "sidebar_mode_editor": "サイドバーモード(編集時)",
+    "use_os_settings": "OS設定を利用する"
   },
 
   "copy_to_clipboard": {
@@ -55,10 +75,25 @@
     "image_crop": "画像の切り抜き",
     "crop": "トリミング",
     "save": "保存",
-    "reset": "リセット",
     "cancel": "キャンセル"
   },
 
+  "handsontable_modal": {
+    "title": "テーブル編集",
+    "data_import": "データインポート",
+    "save": "保存",
+    "cancel": "キャンセル",
+    "done": "完了",
+    "data_import_form": {
+      "select_data_format": "データフォーマット",
+      "import_data": "インポートデータ",
+      "paste_table_data": "テーブルデータを貼り付け",
+      "parse_error": "パーザーエラー",
+      "cancel": "キャンセル",
+      "import": "インポート"
+    }
+  },
+
   "not_found_page": {
     "page_not_exist": "このページは存在しません。"
   }

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

@@ -28,7 +28,6 @@
   "administrator": "管理者",
   "Tag": "タグ",
   "Tags": "タグ",
-  "New": "作成",
   "Close": "閉じる",
   "Shortcuts": "ショートカット",
   "CustomSidebar": "カスタムサイドバー",
@@ -110,7 +109,6 @@
   "V5 Page Migration": "V5 互換形式 への変換",
   "GROWI.5.0_new_schema": "GROWI.5.0における新スキーマについて",
   "See_more_detail_on_new_schema": "詳しくは<a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html#新しい-v5-互換形式について' target='_blank'>{{title}}</a><i class='icon-share-alt'></i>を参照ください。",
-  "Site URL settings": "サイトURL設定",
   "external_account_management": "外部アカウント管理",
   "UserGroup": "グループ",
   "Basic Settings": "基本設定",
@@ -134,7 +132,6 @@
   "edited this page": "さんがこのページを編集しました。",
   "List Drafts": "下書き一覧",
   "Deleted Pages": "削除済みページ",
-  "Sign out": "ログアウト",
   "Disassociate": "連携解除",
   "Color mode": "カラーモード",
   "Sidebar mode": "サイドバーモード",
@@ -154,12 +151,6 @@
   "not_allowed_to_see_this_page": "このページは閲覧できません",
   "Confirm": "確認",
   "Successfully requested": "正常に処理を受け付けました",
-  "personal_dropdown": {
-    "color_mode": "カラーモード",
-    "sidebar_mode": "サイドバーモード",
-    "sidebar_mode_editor": "サイドバーモード(編集時)",
-    "use_os_settings": "OS設定を利用する"
-  },
   "form_validation": {
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
@@ -242,14 +233,6 @@
   "API Token Settings": "API Token設定",
   "Current API Token": "現在のAPI Token",
   "Update API Token": "API Tokenを更新",
-  "in_app_notification": {
-    "notification_list": "アプリ内通知一覧",
-    "see_all": "通知一覧を見る",
-    "no_notification": "通知はありません",
-    "all": "全て",
-    "unopend": "未読",
-    "mark_all_as_read": "全て既読にする"
-  },
   "in_app_notification_settings": {
     "in_app_notification_settings": "アプリ内通知設定",
     "subscribe_settings": "自動でページをサブスクライブする(通知を受け取る)設定",
@@ -324,7 +307,7 @@
   },
   "page_page": {
     "notice": {
-      "version": "これは現在の版ではありません。",
+      "version": "これは最新のバージョンではありません。",
       "redirected": "リダイレクト元 >>",
       "redirected_period":"",
       "unlinked": "このページへのリダイレクトは削除されました。",

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

@@ -25,8 +25,8 @@
     "displayed_or_hidden": "显示/隐藏",
     "Guest Users Access": "来宾用户访问",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
-		"Register limitation": "注册限制",
-		"Register limitation desc": "限制新用户注册",
+		"register_limitation": "注册限制",
+		"register_limitation_desc": "限制新用户注册",
 		"The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
 		"users_without_account": "无法访问没有帐户的用户",
 		"example": "例子",
@@ -316,7 +316,6 @@
     "system_information": "系统信息",
     "wiki_administrator": "只有wiki管理员可以访问此页",
     "assign_administrator": "您可以使用“授予管理员访问权限”按钮在“用户管理”页上将所选用户指定为wiki管理员",
-    "list_of_installed_plugins": "已安装插件列表",
     "package_name": "包名称",
     "specified_version": "指定版本",
     "installed_version": "已安装版本",
@@ -360,9 +359,12 @@
     "site_name": "网站名称 ",
     "sitename_change": "您可以更改用于标题和HTML标题的网站名称。",
     "header_content": "此处输入的内容将显示在标题等中。",
-    "site_url_desc": "用于网站URL设置。",
-    "site_url_warn": "某些功能不起作用,因为未设置网站URL。",
-    "siteurl_help": "网站完整URL起始于 <code>http://</code> or <code>https://</code>.",
+    "site_url": {
+      "title": "主页URL设置",
+      "desc": "用于网站URL设置。",
+      "warn": "某些功能不起作用,因为未设置网站URL。",
+      "help": "网站完整URL起始于 <code>http://</code> or <code>https://</code>."
+    },
     "confidential_name": "内部名称",
     "confidential_example": "ex):仅供内部使用",
     "default_language": "新用户的默认语言",
@@ -623,7 +625,7 @@
     "export": "导出",
     "cancel": "取消",
     "file": "文件",
-    "growi_version": "Growi Version",
+    "growi_version": "GROWI Version",
     "collections": "Collections",
     "exported_at": "Exported At",
     "export_menu": "导出菜单",

+ 37 - 2
packages/app/public/static/locales/zh_CN/commons.json

@@ -1,4 +1,11 @@
 {
+	"Show": "显示",
+	"Hide": "隐藏",
+  "Add": "添加",
+  "Reset": "重启",
+	"Sign out": "退出",
+  "New": "新建",
+
   "meta": {
     "display_name": "简体中文"
   },
@@ -37,9 +44,22 @@
     "description": "Description"
   },
 
+  "in_app_notification": {
+    "notification_list": "应用内通知列表",
+    "see_all": "查看通知列表",
+    "no_notification": "您没有任何通知",
+    "all": "全部",
+    "unopend": "未读",
+    "mark_all_as_read" : "标记为已读"
+  },
+
   "personal_dropdown": {
     "home": "家",
-    "settings": "设置"
+    "settings": "设置",
+		"color_mode": "颜色模式",
+		"sidebar_mode": "边栏模式",
+		"sidebar_mode_editor": "编辑器上的边栏模式",
+		"use_os_settings": "使用操作系统设置"
   },
 
 	"copy_to_clipboard": {
@@ -55,10 +75,25 @@
     "image_crop": "图像裁剪",
     "crop": "修剪",
     "save": "节省",
-    "reset": "重启",
     "cancel": "取消"
   },
 
+  "handsontable_modal": {
+    "title": "编辑表格",
+    "data_import": "数据导入",
+    "save": "节省",
+    "cancel": "取消",
+    "done": "完毕",
+    "data_import_form": {
+      "select_data_format": "数据格式",
+      "import_data": "导入数据",
+      "paste_table_data": "粘贴表格数据",
+      "parse_error": "解析错误",
+      "cancel": "取消",
+      "import": "导入"
+    }
+  },
+
   "not_found_page": {
     "page_not_exist": "该页面不存在"
   }

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

@@ -28,7 +28,6 @@
 	"Admin": "管理",
 	"administrator": "管理员",
 	"Tags": "Tags",
-  "New": "新建",
   "Close": "Close",
 	"Shortcuts": "快捷方式",
   "CustomSidebar": "Custom Sidebar",
@@ -117,7 +116,6 @@
   "V5 Page Migration": "转换为V5的兼容性",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <i class='icon-share-alt'></i> ",
-	"Site URL settings": "主页URL设置",
 	"Markdown Settings": "Markdown设置",
 	"Notification Settings": "通知设置",
 	"external_account_management": "外部账户管理",
@@ -125,7 +123,6 @@
   "ChildUserGroup": "儿童用户组",
 	"Basic Settings": "基础设置",
 	"Basic authentication": "基本身份验证",
-	"Register limitation": "注册限制",
 	"The contents entered here will be shown in the header etc": "此处输入的内容将显示在标题等中",
 	"Public": "公共",
 	"Anyone with the link": "任何人",
@@ -146,7 +143,6 @@
 	"edited this page": "edited this page.",
 	"List Drafts": "草稿",
 	"Deleted Pages": "已删除页",
-	"Sign out": "退出",
   "Disassociate": "解除关联",
   "No bookmarks yet": "暂无书签",
   "add_bookmark": "添加到书签",
@@ -229,14 +225,6 @@
 	"API Token Settings": "API token 设置",
 	"Current API Token": "当前 API token",
 	"Update API Token": "更新 API token",
-  "in_app_notification": {
-    "notification_list": "应用内通知列表",
-    "see_all": "查看通知列表",
-    "no_notification": "您没有任何通知",
-    "all": "全部",
-    "unopend": "未读",
-    "mark_all_as_read" : "标记为已读"
-  },
   "in_app_notification_settings": {
     "in_app_notification_settings": "在应用程序通知设置",
     "subscribe_settings": "自动订阅(接收通知)页面的设置",
@@ -567,12 +555,6 @@
     "Invalid_Number_of_Date" : "You entered invalid value",
     "link_sharing_is_disabled": "链接共享已被禁用"
   },
-	"personal_dropdown": {
-		"color_mode": "颜色模式",
-		"sidebar_mode": "边栏模式",
-		"sidebar_mode_editor": "编辑器上的边栏模式",
-		"use_os_settings": "使用操作系统设置"
-	},
 	"search_result": {
 		"result_meta": "搜索结果:",
 		"deletion_mode_btn_lavel": "选择并删除页面",

Fișier diff suprimat deoarece este prea mare
+ 1 - 1
packages/app/resource/locales/en_US/sandbox-diagrams.md


Fișier diff suprimat deoarece este prea mare
+ 1 - 1
packages/app/resource/locales/ja_JP/sandbox-diagrams.md


Fișier diff suprimat deoarece este prea mare
+ 1 - 1
packages/app/resource/locales/zh_CN/sandbox-diagrams.md


+ 0 - 2
packages/app/src/client/services/AdminHomeContainer.js

@@ -3,7 +3,6 @@ import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
-import { toastError } from '../util/apiNotification';
 import { apiv3Get } from '../util/apiv3-client';
 
 // eslint-disable-next-line no-unused-vars
@@ -66,7 +65,6 @@ export default class AdminHomeContainer extends Container {
         nodeVersion: adminHomeParams.nodeVersion,
         npmVersion: adminHomeParams.npmVersion,
         yarnVersion: adminHomeParams.yarnVersion,
-        installedPlugins: adminHomeParams.installedPlugins,
         envVars: adminHomeParams.envVars,
         isV5Compatible: adminHomeParams.isV5Compatible,
         isMaintenanceMode: adminHomeParams.isMaintenanceMode,

+ 51 - 0
packages/app/src/client/services/activate-plugin.ts

@@ -0,0 +1,51 @@
+import { readFileSync } from 'fs';
+import path from 'path';
+
+import { GrowiPlugin } from '~/interfaces/plugin';
+import { initializeGrowiFacade } from '~/utils/growi-facade';
+import { resolveFromRoot } from '~/utils/project-dir-utils';
+
+
+declare global {
+  // eslint-disable-next-line vars-on-top, no-var
+  var pluginActivators: {
+    [key: string]: {
+      activate: () => void,
+      deactivate: () => void,
+    },
+  };
+}
+
+
+export type GrowiPluginManifestEntries = [growiPlugin: GrowiPlugin, manifest: any][];
+
+
+export class ActivatePluginService {
+
+  static async retrievePluginManifests(growiPlugins: GrowiPlugin[]): Promise<GrowiPluginManifestEntries> {
+    const entries: GrowiPluginManifestEntries = [];
+
+    growiPlugins.forEach(async(growiPlugin) => {
+      const manifestPath = resolveFromRoot(path.join('tmp/plugins', growiPlugin.installedPath, 'dist/manifest.json'));
+      const customManifestStr: string = await readFileSync(manifestPath, 'utf-8');
+      entries.push([growiPlugin, JSON.parse(customManifestStr)]);
+    });
+
+    return entries;
+  }
+
+  static activateAll(): void {
+    initializeGrowiFacade();
+
+    const { pluginActivators } = window;
+
+    if (pluginActivators == null) {
+      return;
+    }
+
+    Object.entries(pluginActivators).forEach(([, activator]) => {
+      activator.activate();
+    });
+  }
+
+}

+ 50 - 37
packages/app/src/client/services/page-operation.ts

@@ -1,7 +1,8 @@
 import { SubscriptionStatusType, Nullable } from '@growi/core';
 import urljoin from 'url-join';
 
-import { OptionsToSave } from '~/interfaces/editor-settings';
+import { OptionsToSave } from '~/interfaces/page-operation';
+import { useIsEnabledUnsavedWarning } from '~/stores/editor';
 import loggerFactory from '~/utils/logger';
 
 import { toastError } from '../util/apiNotification';
@@ -118,43 +119,55 @@ type PageInfo= {
   revisionId: Nullable<string>,
 }
 
+type SaveOrUpdateFunction = (markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) => any;
+
 // TODO: define return type
-export const saveOrUpdate = async(optionsToSave: OptionsToSave, pageInfo: PageInfo, markdown: string) => {
-  const { path, pageId, revisionId } = pageInfo;
-
-  const options = Object.assign({}, optionsToSave);
-
-  /*
-  * Note: variable "markdown" will be received from params
-  * please delete the following code after implemating HackMD editor function
-  */
-  // let markdown;
-  // if (editorMode === EditorMode.HackMD) {
-  // const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
-  // markdown = await pageEditorByHackmd.getMarkdown();
-  // // set option to sync
-  // options.isSyncRevisionToHackmd = true;
-  // revisionId = this.state.revisionIdHackmdSynced;
-  // }
-  // else {
-  // const pageEditor = this.appContainer.getComponentInstance('PageEditor');
-  // const pageEditor = getComponentInstance('PageEditor');
-  // markdown = pageEditor.getMarkdown();
-  // }
-
-  const isNoRevisionPage = pageId != null && revisionId == null;
-
-  let res;
-  if (pageId == null || isNoRevisionPage) {
-    res = await createPage(path, markdown, options);
-  }
-  else {
-    if (revisionId == null) {
-      const msg = '\'revisionId\' is required to update page';
-      throw new Error(msg);
+export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
+  /* eslint-disable react-hooks/rules-of-hooks */
+  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  /* eslint-enable react-hooks/rules-of-hooks */
+
+  return async function(markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) {
+    const { path, pageId, revisionId } = pageInfo;
+
+    const options: OptionsToSave = Object.assign({}, optionsToSave);
+    /*
+    * Note: variable "markdown" will be received from params
+    * please delete the following code after implemating HackMD editor function
+    */
+    // let markdown;
+    // if (editorMode === EditorMode.HackMD) {
+    // const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
+    // markdown = await pageEditorByHackmd.getMarkdown();
+    // // set option to sync
+    // options.isSyncRevisionToHackmd = true;
+    // revisionId = this.state.revisionIdHackmdSynced;
+    // }
+    // else {
+    // const pageEditor = this.appContainer.getComponentInstance('PageEditor');
+    // const pageEditor = getComponentInstance('PageEditor');
+    // markdown = pageEditor.getMarkdown();
+    // }
+
+    const isNoRevisionPage = pageId != null && revisionId == null;
+
+    let res;
+    if (pageId == null || isNoRevisionPage) {
+      res = await createPage(path, markdown, options);
+    }
+    else {
+      if (revisionId == null) {
+        const msg = '\'revisionId\' is required to update page';
+        throw new Error(msg);
+      }
+      res = await updatePage(pageId, revisionId, markdown, options);
     }
-    res = await updatePage(pageId, revisionId, markdown, options);
-  }
 
-  return res;
+    // The updateFn should be a promise or asynchronous function to handle the remote mutation
+    // it should return updated data. see: https://swr.vercel.app/docs/mutation#optimistic-updates
+    // Moreover, `async() => false` does not work since it's too fast to be calculated.
+    await mutateIsEnabledUnsavedWarning(new Promise(r => setTimeout(() => r(false), 10)), { optimisticData: () => false });
+
+    return res;
+  };
 };

+ 0 - 21
packages/app/src/client/util/editor.ts

@@ -1,21 +0,0 @@
-import type { OptionsToSave } from '~/interfaces/editor-settings';
-
-export const getOptionsToSave = (
-    isSlackEnabled: boolean,
-    slackChannels: string,
-    grant: number,
-    grantUserGroupId: string | null | undefined,
-    grantUserGroupName: string | null | undefined,
-    pageTags: string[],
-    isSyncRevisionToHackmd?: boolean,
-): OptionsToSave => {
-  return {
-    pageTags,
-    isSlackEnabled,
-    slackChannels,
-    grant,
-    grantUserGroupId,
-    grantUserGroupName,
-    isSyncRevisionToHackmd,
-  };
-};

+ 0 - 8
packages/app/src/components/Admin/AdminHome/AdminHome.jsx

@@ -15,7 +15,6 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 import EnvVarsTable from './EnvVarsTable';
-import InstalledPluginTable from './InstalledPluginTable';
 import SystemInfomationTable from './SystemInfomationTable';
 
 const logger = loggerFactory('growi:admin');
@@ -85,13 +84,6 @@ const AdminHome = (props) => {
         </div>
       </div>
 
-      <div className="row mb-5">
-        <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('admin:admin_top.list_of_installed_plugins')}</h2>
-          <InstalledPluginTable />
-        </div>
-      </div>
-
       <div className="row mb-5">
         <div className="col-md-12">
           <h2 className="admin-setting-header">{t('admin:admin_top.list_of_env_vars')}</h2>

+ 0 - 55
packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx

@@ -1,55 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-const InstalledPluginTable = (props) => {
-  const { t } = useTranslation();
-  const { adminHomeContainer } = props;
-
-  const { installedPlugins } = adminHomeContainer.state;
-
-  if (installedPlugins == null) {
-    return <></>;
-  }
-
-  return (
-    <table data-testid="admin-installed-plugin-table" className="table table-bordered">
-      <thead>
-        <tr>
-          <th className="text-center">{t('admin:admin_top.package_name')}</th>
-          <th className="text-center">{t('admin:admin_top.specified_version')}</th>
-          <th className="text-center">{t('admin:admin_top.installed_version')}</th>
-        </tr>
-      </thead>
-      <tbody>
-        {adminHomeContainer.state.installedPlugins.map((plugin) => {
-          return (
-            <tr key={plugin.name}>
-              <td>{plugin.name}</td>
-              <td data-hide-in-vrt className="text-center">{plugin.requiredVersion}</td>
-              <td data-hide-in-vrt className="text-center">{plugin.installedVersion}</td>
-            </tr>
-          );
-        })}
-      </tbody>
-    </table>
-  );
-
-};
-
-InstalledPluginTable.propTypes = {
-  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
-};
-
-
-/**
- * Wrapper component for using unstated
- */
-const InstalledPluginTableWrapper = withUnstatedContainers(InstalledPluginTable, [AdminHomeContainer]);
-
-export default InstalledPluginTableWrapper;

+ 2 - 2
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -120,7 +120,7 @@ const AppSetting = (props) => {
               checked={adminAppContainer.state.isEmailPublishedForNewUser === true}
               onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(true) }}
             />
-            <label className="custom-control-label" htmlFor="radio-email-show">{t('Show')}</label>
+            <label className="custom-control-label" htmlFor="radio-email-show">{t('commons:Show')}</label>
           </div>
 
           <div className="custom-control custom-radio custom-control-inline">
@@ -132,7 +132,7 @@ const AppSetting = (props) => {
               checked={adminAppContainer.state.isEmailPublishedForNewUser === false}
               onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(false) }}
             />
-            <label className="custom-control-label" htmlFor="radio-email-hide">{t('Hide')}</label>
+            <label className="custom-control-label" htmlFor="radio-email-hide">{t('commons:Hide')}</label>
           </div>
 
         </div>

+ 1 - 1
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -89,7 +89,7 @@ const AppSettingsPageContents = (props: Props) => {
 
       <div className="row mt-5">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('Site URL settings')}</h2>
+          <h2 className="admin-setting-header">{t('app_setting.site_url.title')}</h2>
           <SiteUrlSetting />
         </div>
       </div>

+ 6 - 6
packages/app/src/components/Admin/App/SiteUrlSetting.tsx

@@ -17,14 +17,14 @@ type Props = {
 }
 
 const SiteUrlSetting = (props: Props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin', { keyPrefix: 'app_setting' });
   const { adminAppContainer } = props;
 
 
   const submitHandler = useCallback(async() => {
     try {
       await adminAppContainer.updateSiteUrlSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('Site URL settings'), ns: 'commons' }));
+      toastSuccess(t('toaster.update_successed', { target: t('site_url.title') }));
     }
     catch (err) {
       toastError(err);
@@ -34,9 +34,9 @@ const SiteUrlSetting = (props: Props) => {
 
   return (
     <React.Fragment>
-      <p className="card well">{t('admin:app_setting.site_url_desc')}</p>
+      <p className="card well">{t('site_url.desc')}</p>
       {!adminAppContainer.state.isSetSiteUrl
-          && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('admin:app_setting.site_url_warn')}</p>)}
+          && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('site_url.warn')}</p>)}
 
       <div className="row form-group">
         <div className="col-md-9 offset-md-3">
@@ -64,14 +64,14 @@ const SiteUrlSetting = (props: Props) => {
                   />
                   <p className="form-text text-muted">
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.siteurl_help') }} />
+                    <span dangerouslySetInnerHTML={{ __html: t('site_url.help') }} />
                   </p>
                 </td>
                 <td>
                   <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
                   <p className="form-text text-muted">
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
+                    <span dangerouslySetInnerHTML={{ __html: t('use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
                   </p>
                 </td>
               </tr>

+ 21 - 0
packages/app/src/components/Admin/Common/AdminInstallButtonRow.tsx

@@ -0,0 +1,21 @@
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+type Props = {
+  onClick: () => void,
+  disabled: boolean,
+
+}
+
+export const AdminInstallButtonRow = (props: Props): JSX.Element => {
+  // TODO: const { t } = useTranslation('admin');
+
+  return (
+    <div className="row my-3">
+      <div className="mx-auto">
+        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>Install</button>
+      </div>
+    </div>
+  );
+};

+ 3 - 0
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -36,6 +36,7 @@ const AdminNavigation = (props) => {
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
       case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
+      case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{           'Plugins Extention'}</>;
       case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
       default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
       /* eslint-enable no-multi-spaces, max-len */
@@ -91,6 +92,7 @@ const AdminNavigation = (props) => {
         <MenuLink menu="users"        isListGroupItems isActive={isActiveMenu('/users')} />
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
+        <MenuLink menu="plugins"      isListGroupItems isActive={isActiveMenu('/plugins')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (
@@ -140,6 +142,7 @@ const AdminNavigation = (props) => {
             {isActiveMenu('/user-groups') &&       <MenuLabel menu="user-groups" />}
             {isActiveMenu('/search') &&            <MenuLabel menu="search" />}
             {isActiveMenu('/audit-log') &&         <MenuLabel menu="audit-log" />}
+            {isActiveMenu('/plugins') &&           <MenuLabel menu="plugins" />}
             {/* eslint-enable no-multi-spaces */}
           </span>
         </button>

+ 28 - 3
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -1,4 +1,6 @@
-import React, { useCallback, useState } from 'react';
+import React, {
+  useCallback, useEffect, useState,
+} from 'react';
 
 import { useTranslation } from 'next-i18next';
 
@@ -6,16 +8,31 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { useSWRxLayoutSetting } from '~/stores/admin/customize';
 import { useNextThemes } from '~/stores/use-next-themes';
 
+const useIsContainerFluid = () => {
+  const { data: layoutSetting, update: updateLayoutSetting } = useSWRxLayoutSetting();
+  const [isContainerFluid, setIsContainerFluid] = useState<boolean>();
+
+  useEffect(() => {
+    setIsContainerFluid(layoutSetting?.isContainerFluid);
+  }, [layoutSetting?.isContainerFluid]);
+
+  return {
+    isContainerFluid,
+    setIsContainerFluid,
+    updateLayoutSetting,
+  };
+};
+
 const CustomizeLayoutSetting = (): JSX.Element => {
   const { t } = useTranslation('admin');
 
   const { resolvedTheme } = useNextThemes();
-  const { data: layoutSetting, update: updateLayoutSetting } = useSWRxLayoutSetting();
 
-  const [isContainerFluid, setIsContainerFluid] = useState<boolean>(layoutSetting?.isContainerFluid ?? false);
+  const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
   const [retrieveError, setRetrieveError] = useState<any>();
 
   const onClickSubmit = useCallback(async() => {
+    if (isContainerFluid == null) { return }
     try {
       await updateLayoutSetting({ isContainerFluid });
       toastSuccess(t('toaster.update_successed', { target: t('customize_settings.layout'), ns: 'commons' }));
@@ -25,6 +42,14 @@ const CustomizeLayoutSetting = (): JSX.Element => {
     }
   }, [isContainerFluid, updateLayoutSetting, t]);
 
+  if (isContainerFluid == null) {
+    return (
+      <div className="text-muted text-center">
+        <i className="fa fa-2x fa-spinner fa-pulse"></i>
+      </div>
+    );
+  }
+
   return (
     <React.Fragment>
       <div className="row">

+ 1 - 3
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -54,17 +54,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
     try {
       const response = await apiv3Put('/customize-setting/customize-logo', {
         isDefaultLogo,
-        customizedLogoSrc,
       });
       const { customizedParams } = response.data;
       setIsDefaultLogo(customizedParams.isDefaultLogo);
-      setCustomizedLogoSrc(customizedParams.customizedLogoSrc);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
     }
-  }, [t, isDefaultLogo, customizedLogoSrc]);
+  }, [t, isDefaultLogo]);
 
   const onClickDeleteBtn = useCallback(async() => {
     try {

+ 2 - 20
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -43,26 +43,8 @@ class XssForm extends React.Component {
     return (
       <div className="form-group col-12 my-3">
         <div className="row">
-          <div className="col-md-4 col-sm-12 align-self-start mb-4">
-            <div className="custom-control custom-radio ">
-              <input
-                type="radio"
-                className="custom-control-input"
-                id="xssOption1"
-                name="XssOption"
-                checked={xssOption === 1}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
-              />
-              <label className="custom-control-label w-100" htmlFor="xssOption1">
-                <p className="font-weight-bold">{t('markdown_settings.xss_options.remove_all_tags')}</p>
-                <div className="mt-4">
-                  {t('markdown_settings.xss_options.remove_all_tags_desc')}
-                </div>
-              </label>
-            </div>
-          </div>
 
-          <div className="col-md-4 col-sm-12 align-self-start mb-4">
+          <div className="col-md-6 col-sm-12 align-self-start mb-4">
             <div className="custom-control custom-radio">
               <input
                 type="radio"
@@ -104,7 +86,7 @@ class XssForm extends React.Component {
             </div>
           </div>
 
-          <div className="col-md-4 col-sm-12 align-self-start mb-4">
+          <div className="col-md-6 col-sm-12 align-self-start mb-4">
             <div className="custom-control custom-radio">
               <input
                 type="radio"

+ 3 - 3
packages/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -35,7 +35,7 @@ const Badge = ({ isEnabled }) => {
     : <span className="badge badge-secondary">{t('external_notification.disabled')}</span>;
 };
 
-const SkeltonListItem = () => (
+const SkeletonListItem = () => (
   <li className="list-group-item">
     <h4 className="mb-2">
       <span className="badge badge-secondary">――</span>
@@ -74,7 +74,7 @@ const LegacySlackIntegrationListItem = ({ isEnabled }) => {
     <li className="list-group-item">
       <h4>
         <Badge isEnabled={isEnabled} />
-        <a href="/admin/slack-integration-legacy" className="ml-2">{t('legacy_slack_integration')}</a>
+        <a href="/admin/slack-integration-legacy" className="ml-2">{t('slack_integration_legacy.slack_integration_legacy')}</a>
       </h4>
       { isEnabled && (
         <ul className="mt-2 pl-4">
@@ -144,7 +144,7 @@ function NotificationSetting(props) {
     <div data-testid="admin-notification">
       <h2 className="admin-setting-header">{t('external_notification.header_status')}</h2>
       <ul className="list-group">
-        { !isMounted && <SkeltonListItem />}
+        { !isMounted && <SkeletonListItem />}
         { isMounted && (
           <>
             <SlackIntegrationListItem isEnabled={isSlackEnabled} currentBotType={currentBotType} />

+ 1 - 1
packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx

@@ -129,7 +129,7 @@ class UserTriggerNotification extends React.Component {
                 </p>
               </td>
               <td>
-                <button type="button" className="btn btn-primary" disabled={!this.validateForm()} onClick={this.onClickSubmit}>{t('add')}</button>
+                <button type="button" className="btn btn-primary" disabled={!this.validateForm()} onClick={this.onClickSubmit}>{t('commons:Add')}</button>
               </td>
             </tr>
             {userNotifications.length > 0 && userNotifications.map((notification) => {

+ 13 - 0
packages/app/src/components/Admin/PluginsExtension/Loading.js

@@ -0,0 +1,13 @@
+import {
+  Spinner,
+} from 'reactstrap';
+
+const Loading = () => {
+  return (
+    <Spinner className='d-flex justify-content-center aligh-items-center'>
+      Loading...
+    </Spinner>
+  );
+};
+
+export default Loading;

+ 68 - 0
packages/app/src/components/Admin/PluginsExtension/PluginCard.module.scss

@@ -0,0 +1,68 @@
+// TODO: Rewrite according to guidelines
+.plugin_card :global {
+
+  .switch__label {
+    position: relative;
+    display: inline-block;
+    width: 50px;
+  }
+  .switch__content {
+    position: relative;
+    display: block;
+    height: 31px;
+    overflow: hidden;
+    cursor: pointer;
+    border-radius: 30px;
+  }
+  .switch__content:before {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: block;
+    width: calc(100% - 3px);
+    height: calc(100% - 3px);
+    content: '';
+    background-color: #fff;
+    border: 1.5px solid #E5E5EA;
+    border-radius: 30px;
+  }
+  .switch__content:after {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    display: block;
+    width: 0;
+    height: 0;
+    content: '';
+    background-color: transparent;
+    border-radius: 30px;
+    transition: all .5s;
+  }
+  .switch__input {
+    display: none;
+  }
+
+  .switch__circle {
+    position: absolute;
+    top: 2px;
+    left: 2px;
+    display: block;
+    width: 27px;
+    height: 27px;
+    background-color: #fff;
+    border-radius: 20px;
+    box-shadow: 0 2px 6px #999;
+    transition: all .5s;
+  }
+  .switch__input:checked ~ .switch__circle {
+    left: 21px;
+  }
+
+  .switch__input:checked ~ .switch__content:after {
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: #0078D7;
+  }
+}

+ 85 - 0
packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx

@@ -0,0 +1,85 @@
+// import { faCircleArrowDown, faCircleCheck } from '@fortawesome/free-solid-svg-icons';
+// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import Link from 'next/link';
+
+import styles from './PluginCard.module.scss';
+
+
+type Props = {
+  name: string,
+  url: string,
+  description: string,
+}
+
+export const PluginCard = (props: Props): JSX.Element => {
+  const {
+    name, url, description,
+  } = props;
+  // const [isEnabled, setIsEnabled] = useState(true);
+
+  // const checkboxHandler = useCallback(() => {
+  //   setIsEnabled(false);
+  // }, []);
+
+  return (
+    <div className="card shadow border-0" key={name}>
+      <div className="card-body px-5 py-4 mt-3">
+        <div className="row mb-3">
+          <div className="col-9">
+            <h2 className="card-title h3 border-bottom pb-2 mb-3">
+              <Link href={`${url}`}>{name}</Link>
+            </h2>
+            <p className="card-text text-muted">{description}</p>
+          </div>
+          <div className='col-3'>
+            <div className={`${styles.plugin_card}`}>
+              <div className="switch">
+                <label className="switch__label">
+                  <input type="checkbox" className="switch__input" checked/>
+                  <span className="switch__content"></span>
+                  <span className="switch__circle"></span>
+                </label>
+              </div>
+            </div>
+            {/* <div className="custom-control custom-switch custom-switch-lg custom-switch-slack">
+              <input
+                type="checkbox"
+                className="custom-control-input border-0"
+                checked={isEnabled}
+                onChange={checkboxHandler}
+              />
+              <label className="custom-control-label align-center"></label>
+            </div> */}
+            {/* <Image className="mx-auto" alt="GitHub avator image" src={owner.avatar_url} width={250} height={250} /> */}
+          </div>
+        </div>
+        <div className="row">
+          <div className="col-12 d-flex flex-wrap gap-2">
+            {/* {topics?.map((topic: string) => {
+              return (
+                <span key={`${name}-${topic}`} className="badge rounded-1 mp-bg-light-blue text-dark fw-normal">
+                  {topic}
+                </span>
+              );
+            })} */}
+          </div>
+        </div>
+      </div>
+      <div className="card-footer px-5 border-top-0 mp-bg-light-blue">
+        <p className="d-flex justify-content-between align-self-center mb-0">
+          <span>
+            {/* {owner.login === 'weseek' ? <FontAwesomeIcon icon={faCircleCheck} className="me-1 text-primary" /> : <></>}
+
+            <a href={owner.html_url} target="_blank" rel="noreferrer">
+              {owner.login}
+            </a> */}
+          </span>
+          {/* <span>
+            <FontAwesomeIcon icon={faCircleArrowDown} className="me-1" /> {stargazersCount}
+          </span> */}
+        </p>
+      </div>
+    </div>
+  );
+};

+ 91 - 0
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -0,0 +1,91 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
+
+import AdminInstallButtonRow from '../Common/AdminUpdateButtonRow';
+// TODO: error notification (toast, loggerFactory)
+// TODO: i18n
+
+export const PluginInstallerForm = (): JSX.Element => {
+  // const { t } = useTranslation('admin');
+
+  const submitHandler = useCallback(async(e) => {
+    e.preventDefault();
+
+    const formData = e.target.elements;
+
+    const {
+      'pluginInstallerForm[url]': { value: url },
+      // 'pluginInstallerForm[ghBranch]': { value: ghBranch },
+      // 'pluginInstallerForm[ghTag]': { value: ghTag },
+    } = formData;
+
+    const pluginInstallerForm = {
+      url,
+      // ghBranch,
+      // ghTag,
+    };
+
+    try {
+      await apiv3Post('/plugins-extention', { pluginInstallerForm });
+      toastSuccess('Plugin Install Successed!');
+    }
+    catch (err) {
+      toastError(err);
+      // logger.error(err);
+    }
+  }, []);
+
+  return (
+    <form role="form" onSubmit={submitHandler}>
+      <div className='form-group row'>
+        <label className="text-left text-md-right col-md-3 col-form-label">GitHub Repository URL</label>
+        <div className="col-md-6">
+          <input
+            className="form-control"
+            type="text"
+            // defaultValue={adminAppContainer.state.title || ''}
+            name="pluginInstallerForm[url]"
+            placeholder="https://github.com/weseek/growi-plugin-lsx"
+            required
+          />
+          <p className="form-text text-muted">You can install plugins by inputting the GitHub URL.</p>
+          {/* <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p> */}
+        </div>
+      </div>
+      {/* <div className='form-group row'>
+        <label className="text-left text-md-right col-md-3 col-form-label">branch</label>
+        <div className="col-md-6">
+          <input
+            className="form-control"
+            type="text"
+            name="pluginInstallerForm[ghBranch]"
+            placeholder="main"
+          />
+          <p className="form-text text-muted">branch name</p>
+        </div>
+      </div>
+      <div className='form-group row'>
+        <label className="text-left text-md-right col-md-3 col-form-label">tag</label>
+        <div className="col-md-6">
+          <input
+            className="form-control"
+            type="text"
+            name="pluginInstallerForm[ghTag]"
+            placeholder="tags"
+          />
+          <p className="form-text text-muted">tag name</p>
+        </div>
+      </div> */}
+
+      <div className="row my-3">
+        <div className="mx-auto">
+          <button type="submit" className="btn btn-primary">Install</button>
+        </div>
+      </div>
+    </form>
+  );
+};

+ 58 - 0
packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx

@@ -0,0 +1,58 @@
+import React from 'react';
+
+import type { SearchResultItem } from '~/interfaces/github-api';
+import { useInstalledPlugins } from '~/stores/useInstalledPlugins';
+
+import Loading from './Loading';
+import { PluginCard } from './PluginCard';
+import { PluginInstallerForm } from './PluginInstallerForm';
+
+
+// TODO: i18n
+
+export const PluginsExtensionPageContents = (): JSX.Element => {
+  // const { data, error } = useInstalledPlugins();
+
+  // if (data == null) {
+  //   return <Loading />;
+  // }
+
+  return (
+    <div>
+
+      <div className="row mb-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">Plugin Installer</h2>
+          <PluginInstallerForm />
+        </div>
+      </div>
+
+      <div className="row mb-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">Plugins</h2>
+          <div className="d-grid gap-5">
+            <PluginCard
+              name={'growi-plugin-templates-for-office'}
+              url={'https://github.com/weseek/growi-plugin-templates-for-office'}
+              description={'GROWI markdown templates for office.'}
+            />
+            {/* <PluginCard
+              name={'growi-plugin-theme-welcome-to-fumiya-room'}
+              url={'https://github.com/weseek/growi-plugin-theme-welcome-to-fumiya-room'}
+              description={'Welcome to fumiya\'s room! This is very very "latest" design...'}
+            /> */}
+            <PluginCard
+              name={'growi-plugin-copy-code-to-clipboard'}
+              url={'https://github.com/weseek/growi-plugin-copy-code-to-clipboard'}
+              description={'Add copy button on code blocks.'}
+            />
+            {/* {data?.items.map((item: SearchResultItem) => {
+              return <PluginCard key={item.name} {...item} />;
+            })} */}
+          </div>
+        </div>
+      </div>
+
+    </div>
+  );
+};

+ 2 - 2
packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -100,7 +100,7 @@ class LocalSecuritySettingContents extends React.Component {
 
             <div className="row">
               <div className="col-12 col-md-3 text-left text-md-right py-2">
-                <strong>{t('Register limitation')}</strong>
+                <strong>{t('security_settings.register_limitation')}</strong>
               </div>
               <div className="col-12 col-md-6">
                 <div className="dropdown">
@@ -147,7 +147,7 @@ class LocalSecuritySettingContents extends React.Component {
                   </div>
                 </div>
 
-                <p className="form-text text-muted small">{t('security_settings.Register limitation desc')}</p>
+                <p className="form-text text-muted small">{t('security_settings.register_limitation_desc')}</p>
               </div>
             </div>
             <div className="row">

+ 13 - 11
packages/app/src/components/Admin/Security/ShareLinkSetting.tsx

@@ -134,19 +134,21 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
         </div>
       </div>
       <h4>{t('security_settings.all_share_links')}</h4>
-      <Pager
-        activePage={shareLinksActivePage}
-        pagingHandler={getShareLinkList}
-        totalLinks={totalshareLinks}
-        limit={shareLinksPagingLimit}
-      />
 
       {(shareLinks.length !== 0) ? (
-        <ShareLinkList
-          shareLinks={shareLinks}
-          onClickDeleteButton={deleteLinkById}
-          isAdmin
-        />
+        <>
+          <Pager
+            activePage={shareLinksActivePage}
+            pagingHandler={getShareLinkList}
+            totalLinks={totalshareLinks}
+            limit={shareLinksPagingLimit}
+          />
+          <ShareLinkList
+            shareLinks={shareLinks}
+            onClickDeleteButton={deleteLinkById}
+            isAdmin
+          />
+        </>
       )
         : (<p className="text-center">{t('security_settings.No_share_links')}</p>
         )

+ 2 - 2
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -126,7 +126,7 @@ export const UserGroupTable: FC<Props> = (props: Props) => {
   }, [props.userGroupRelations, props.childUserGroups]);
 
   return (
-    <>
+    <div data-testid="grw-user-group-table">
       <h2>{props.headerLabel}</h2>
 
       <table className="table table-bordered table-user-list">
@@ -216,6 +216,6 @@ export const UserGroupTable: FC<Props> = (props: Props) => {
           })}
         </tbody>
       </table>
-    </>
+    </div>
   );
 };

+ 1 - 1
packages/app/src/components/Admin/UserManagement.tsx

@@ -182,7 +182,7 @@ const UserManagement = (props: UserManagementProps) => {
               onClick={resetButtonClickHandler}
             >
               <span className="icon-refresh mr-1"></span>
-              Reset
+              {t('commons:Reset')}
             </button>
           </div>
         </div>

+ 2 - 1
packages/app/src/components/Admin/Users/GiveAdminButton.tsx

@@ -39,6 +39,7 @@ const GiveAdminButton = (props: GiveAdminButtonProps): JSX.Element => {
 /**
  * Wrapper component for using unstated
  */
-const GiveAdminButtonWrapper = withUnstatedContainers(GiveAdminButton, [AdminUsersContainer]);
+// eslint-disable-next-line max-len
+const GiveAdminButtonWrapper: React.ForwardRefExoticComponent<Pick<any, string | number | symbol> & React.RefAttributes<any>> = withUnstatedContainers(GiveAdminButton, [AdminUsersContainer]);
 
 export default GiveAdminButtonWrapper;

+ 2 - 1
packages/app/src/components/Admin/Users/RemoveAdminMenuItem.tsx

@@ -58,6 +58,7 @@ const RemoveAdminMenuItem = (props: Props): JSX.Element => {
 /**
 * Wrapper component for using unstated
 */
-const RemoveAdminMenuItemWrapper = withUnstatedContainers(RemoveAdminMenuItem, [AdminUsersContainer]);
+// eslint-disable-next-line max-len
+const RemoveAdminMenuItemWrapper: React.ForwardRefExoticComponent<Pick<any, string | number | symbol> & React.RefAttributes<any>> = withUnstatedContainers(RemoveAdminMenuItem, [AdminUsersContainer]);
 
 export default RemoveAdminMenuItemWrapper;

+ 2 - 1
packages/app/src/components/Admin/Users/StatusSuspendMenuItem.tsx

@@ -56,6 +56,7 @@ const StatusSuspendMenuItem = (props: Props): JSX.Element => {
 /**
  * Wrapper component for using unstated
  */
-const StatusSuspendMenuItemWrapper = withUnstatedContainers(StatusSuspendMenuItem, [AdminUsersContainer]);
+// eslint-disable-next-line max-len
+const StatusSuspendMenuItemWrapper: React.ForwardRefExoticComponent<Pick<any, string | number | symbol> & React.RefAttributes<any>> = withUnstatedContainers(StatusSuspendMenuItem, [AdminUsersContainer]);
 
 export default StatusSuspendMenuItemWrapper;

+ 0 - 132
packages/app/src/components/Admin/Users/UserMenu.jsx

@@ -1,132 +0,0 @@
-import React, { Fragment } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import {
-  UncontrolledDropdown, DropdownToggle, DropdownMenu,
-} from 'reactstrap';
-
-import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import GiveAdminButton from './GiveAdminButton';
-import RemoveAdminMenuItem from './RemoveAdminMenuItem';
-import SendInvitationEmailButton from './SendInvitationEmailButton';
-import StatusActivateButton from './StatusActivateButton';
-import StatusSuspendedMenuItem from './StatusSuspendMenuItem';
-import UserRemoveButton from './UserRemoveButton';
-
-
-class UserMenu extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isInvitationEmailSended: this.props.user.isInvitationEmailSended,
-    };
-
-    this.onPasswordResetClicked = this.onPasswordResetClicked.bind(this);
-    this.onSuccessfullySentInvitationEmail = this.onSuccessfullySentInvitationEmail.bind(this);
-  }
-
-  onPasswordResetClicked() {
-    this.props.adminUsersContainer.showPasswordResetModal(this.props.user);
-  }
-
-  onSuccessfullySentInvitationEmail() {
-    this.setState({ isInvitationEmailSended: true });
-  }
-
-  renderEditMenu() {
-    const { t } = this.props;
-
-    return (
-      <Fragment>
-        <li className="dropdown-divider"></li>
-        <li className="dropdown-header">{t('admin:user_management.user_table.edit_menu')}</li>
-        <li>
-          <button className="dropdown-item" type="button" onClick={this.onPasswordResetClicked}>
-            <i className="icon-fw icon-key"></i>{ t('admin:user_management.reset_password') }
-          </button>
-        </li>
-      </Fragment>
-    );
-  }
-
-  renderStatusMenu() {
-    const { t, user } = this.props;
-    const { isInvitationEmailSended } = this.state;
-
-    return (
-      <Fragment>
-        <li className="dropdown-divider"></li>
-        <li className="dropdown-header">{t('user_management.status')}</li>
-        <li>
-          {(user.status === 1 || user.status === 3) && <StatusActivateButton user={user} />}
-          {user.status === 2 && <StatusSuspendedMenuItem user={user} />}
-          {user.status === 5 && (
-            <SendInvitationEmailButton
-              user={user}
-              isInvitationEmailSended={isInvitationEmailSended}
-              onSuccessfullySentInvitationEmail={this.onSuccessfullySentInvitationEmail}
-            />
-          )}
-          {(user.status === 1 || user.status === 3 || user.status === 5) && <UserRemoveButton user={user} />}
-        </li>
-      </Fragment>
-    );
-  }
-
-  renderAdminMenu() {
-    const { t, user } = this.props;
-
-    return (
-      <Fragment>
-        <li className="dropdown-divider pl-0"></li>
-        <li className="dropdown-header">{t('admin:user_management.user_table.administrator_menu')}</li>
-        <li>
-          {user.admin === true && <RemoveAdminMenuItem user={user} />}
-          {user.admin === false && <GiveAdminButton user={user} />}
-        </li>
-      </Fragment>
-    );
-  }
-
-  render() {
-    const { user } = this.props;
-    const { isInvitationEmailSended } = this.state;
-
-    return (
-      <UncontrolledDropdown id="userMenu" size="sm">
-        <DropdownToggle caret color="secondary" outline>
-          <i className="icon-settings" />
-          {(user.status === 5 && !isInvitationEmailSended) && <i className="fa fa-circle text-danger grw-usermenu-notification-icon" />}
-        </DropdownToggle>
-        <DropdownMenu positionFixed>
-          {this.renderEditMenu()}
-          {user.status !== 4 && this.renderStatusMenu()}
-          {user.status === 2 && this.renderAdminMenu()}
-        </DropdownMenu>
-      </UncontrolledDropdown>
-    );
-  }
-
-}
-
-const UserMenuWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-  return <UserMenu t={t} {...props} />;
-};
-
-const UserMenuWrapper = withUnstatedContainers(UserMenuWrapperFC, [AdminUsersContainer]);
-
-UserMenu.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
-
-  user: PropTypes.object.isRequired,
-};
-
-export default UserMenuWrapper;

+ 5 - 0
packages/app/src/components/Admin/Users/UserMenu.module.scss

@@ -0,0 +1,5 @@
+.grw-usermenu-notification-icon :global {
+  position: absolute;
+  top: -4px;
+  left: 30px;
+}

+ 114 - 0
packages/app/src/components/Admin/Users/UserMenu.tsx

@@ -0,0 +1,114 @@
+import React, { useState, useCallback } from 'react';
+
+import { IUserHasId, USER_STATUS } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+import {
+  UncontrolledDropdown, DropdownToggle, DropdownMenu,
+} from 'reactstrap';
+
+import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+import GiveAdminButton from './GiveAdminButton';
+import RemoveAdminMenuItem from './RemoveAdminMenuItem';
+import SendInvitationEmailButton from './SendInvitationEmailButton';
+import StatusActivateButton from './StatusActivateButton';
+import StatusSuspendedMenuItem from './StatusSuspendMenuItem';
+import UserRemoveButton from './UserRemoveButton';
+
+import styles from './UserMenu.module.scss';
+
+type UserMenuProps = {
+  adminUsersContainer: AdminUsersContainer,
+  user: IUserHasId,
+}
+
+const UserMenu = (props: UserMenuProps) => {
+  const { t } = useTranslation('admin');
+
+  const { adminUsersContainer, user } = props;
+
+  const [isInvitationEmailSended, setIsInvitationEmailSended] = useState<boolean>(user.isInvitationEmailSended);
+
+  const onClickPasswordResetHandler = useCallback(async() => {
+    await adminUsersContainer.showPasswordResetModal(user);
+  }, [adminUsersContainer, user]);
+
+  const onSuccessfullySentInvitationEmailHandler = useCallback(() => {
+    setIsInvitationEmailSended(true);
+  }, []);
+
+  const renderEditMenu = useCallback(() => {
+    return (
+      <>
+        <li className="dropdown-divider"></li>
+        <li className="dropdown-header">{t('user_management.user_table.edit_menu')}</li>
+        <li>
+          <button className="dropdown-item" type="button" onClick={onClickPasswordResetHandler}>
+            <i className="icon-fw icon-key"></i>{ t('user_management.reset_password') }
+          </button>
+        </li>
+      </>
+    );
+  }, [onClickPasswordResetHandler, t]);
+
+  const renderStatusMenu = useCallback(() => {
+    return (
+      <>
+        <li className="dropdown-divider"></li>
+        <li className="dropdown-header">{t('user_management.status')}</li>
+        <li>
+          {(user.status === USER_STATUS.REGISTERED || user.status === USER_STATUS.SUSPENDED) && <StatusActivateButton user={user} />}
+          {user.status === USER_STATUS.ACTIVE && <StatusSuspendedMenuItem user={user} />}
+          {user.status === USER_STATUS.INVITED && (
+            <SendInvitationEmailButton
+              user={user}
+              isInvitationEmailSended={isInvitationEmailSended}
+              onSuccessfullySentInvitationEmail={onSuccessfullySentInvitationEmailHandler}
+            />
+          )}
+          {(user.status === USER_STATUS.REGISTERED || user.status === USER_STATUS.SUSPENDED || user.status === USER_STATUS.INVITED)
+          && <UserRemoveButton user={user} />}
+        </li>
+      </>
+    );
+  }, [isInvitationEmailSended, onSuccessfullySentInvitationEmailHandler, t, user]);
+
+  const renderAdminMenu = useCallback(() => {
+    return (
+      <>
+        <li className="dropdown-divider pl-0"></li>
+        <li className="dropdown-header">{t('user_management.user_table.administrator_menu')}</li>
+        <li>
+          {user.admin === true && <RemoveAdminMenuItem user={user} />}
+          {user.admin === false && <GiveAdminButton user={user} />}
+        </li>
+      </>
+    );
+  }, [t, user]);
+
+  return (
+    <UncontrolledDropdown id="userMenu" size="sm">
+      <DropdownToggle caret color="secondary" outline>
+        <i className="icon-settings" />
+        {(user.status === USER_STATUS.INVITED && !isInvitationEmailSended)
+        && <i className={`fa fa-circle text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`} />}
+      </DropdownToggle>
+      <DropdownMenu positionFixed>
+        {renderEditMenu()}
+        {user.status !== USER_STATUS.DELETED && renderStatusMenu()}
+        {user.status === USER_STATUS.ACTIVE && renderAdminMenu()}
+      </DropdownMenu>
+    </UncontrolledDropdown>
+  );
+
+};
+
+/**
+* Wrapper component for using unstated
+*/
+// eslint-disable-next-line max-len
+const UserMenuWrapper: React.ForwardRefExoticComponent<Pick<any, string | number | symbol> & React.RefAttributes<any>> = withUnstatedContainers(UserMenu, [AdminUsersContainer]);
+
+export default UserMenuWrapper;

+ 1 - 1
packages/app/src/components/BookmarkButtons.tsx

@@ -63,7 +63,7 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
       </button>
 
-      <UncontrolledTooltip placement="top" target="bookmark-button" fade={false}>
+      <UncontrolledTooltip data-testid="bookmark-button-tooltip" placement="top" target="bookmark-button" fade={false}>
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
 

+ 6 - 5
packages/app/src/components/Comments.tsx

@@ -3,7 +3,7 @@ import React from 'react';
 import { IRevisionHasId } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import { PageComment } from '~/components/PageComment';
+import { PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useIsTrashPage } from '~/stores/page';
 
@@ -11,18 +11,18 @@ import { useCurrentUser } from '../stores/context';
 
 import { CommentEditorProps } from './PageComment/CommentEditor';
 
-
+const PageComment = dynamic<PageCommentProps>(() => import('~/components/PageComment').then(mod => mod.PageComment), { ssr: false });
 const CommentEditor = dynamic<CommentEditorProps>(() => import('./PageComment/CommentEditor').then(mod => mod.CommentEditor), { ssr: false });
 
-
 type CommentsProps = {
   pageId: string,
+  pagePath: string,
   revision: IRevisionHasId,
 }
 
 export const Comments = (props: CommentsProps): JSX.Element => {
 
-  const { pageId, revision } = props;
+  const { pageId, pagePath, revision } = props;
 
   const { mutate } = useSWRxPageComment(pageId);
   const { data: isDeleted } = useIsTrashPage();
@@ -33,13 +33,13 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   }
 
   return (
-    // TODO: Check and refactor CSS import
     <div className="page-comments-row mt-5 py-4 d-edit-none d-print-none">
       <div className="container-lg">
         <div className="page-comments">
           <div id="page-comments-list" className="page-comments-list">
             <PageComment
               pageId={pageId}
+              pagePath={pagePath}
               revision={revision}
               currentUser={currentUser}
               isReadOnly={false}
@@ -53,6 +53,7 @@ export const Comments = (props: CommentsProps): JSX.Element => {
                 pageId={pageId}
                 isForNewComment
                 onCommentButtonClicked={mutate}
+                revisionId={revision._id}
               />
             </div>
           )}

+ 1 - 1
packages/app/src/components/Common/ImageCropModal.tsx

@@ -158,7 +158,7 @@ const ImageCropModal: FC<Props> = (props: Props) => {
       </ModalBody>
       <ModalFooter>
         <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" disabled={!isCropImage} onClick={reset}>
-          {t('crop_image_modal.reset')}
+          {t('commons:Reset')}
         </button>
         { !showCropOption && (
           <div className="mr-auto">

+ 2 - 2
packages/app/src/components/CreateTemplateModal.jsx

@@ -1,8 +1,8 @@
 import React from 'react';
 
 import { pathUtils } from '@growi/core';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import urljoin from 'url-join';
 
@@ -42,7 +42,7 @@ const CreateTemplateModal = (props) => {
   }
 
   return (
-    <Modal isOpen={props.isOpen} toggle={props.onClose} data-testid="page-template-modal" className="grw-create-page">
+    <Modal isOpen={props.isOpen} toggle={props.onClose} data-testid="page-template-modal">
       <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
         {t('template.modal_label.Create/Edit Template Page')}
       </ModalHeader>

+ 0 - 117
packages/app/src/components/Drawio.tsx

@@ -1,117 +0,0 @@
-import React, {
-  useCallback, useEffect, useMemo, useRef, useState,
-} from 'react';
-
-import EventEmitter from 'events';
-
-import { useTranslation } from 'next-i18next';
-import { debounce } from 'throttle-debounce';
-
-import { CustomWindow } from '~/interfaces/global';
-import { IGraphViewer, isGraphViewer } from '~/interfaces/graph-viewer';
-
-import NotAvailableForGuest from './NotAvailableForGuest';
-
-type Props = {
-  GraphViewer: IGraphViewer,
-  drawioContent: string,
-  rangeLineNumberOfMarkdown: { beginLineNumber: number, endLineNumber: number },
-  isPreview?: boolean,
-}
-
-// It calls callback when GraphViewer is not null.
-// eslint-disable-next-line @typescript-eslint/ban-types
-const waitForGraphViewer = async(callback: Function) => {
-  const MAX_WAIT_COUNT = 10; // no reason for 10
-
-  for (let i = 0; i < MAX_WAIT_COUNT; i++) {
-    if (isGraphViewer((window as CustomWindow).GraphViewer)) {
-      callback((window as CustomWindow).GraphViewer);
-      break;
-    }
-    // Sleep 500 ms
-    // eslint-disable-next-line no-await-in-loop
-    await new Promise<void>(r => setTimeout(() => r(), 500));
-  }
-};
-
-const Drawio = (props: Props): JSX.Element => {
-
-  const { t } = useTranslation();
-
-  // Wrap with a function since GraphViewer is a function.
-  // This applies when call setGraphViewer as well.
-  const [GraphViewer, setGraphViewer] = useState<IGraphViewer | undefined>(() => (window as CustomWindow).GraphViewer);
-
-  const { drawioContent, rangeLineNumberOfMarkdown, isPreview } = props;
-
-  // const { open: openDrawioModal } = useDrawioModalForPage();
-
-  const drawioContainerRef = useRef<HTMLDivElement>(null);
-
-  const globalEmitter: EventEmitter = (window as CustomWindow).globalEmitter;
-
-  const editButtonClickHandler = useCallback(() => {
-    const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
-    globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
-  }, [rangeLineNumberOfMarkdown, globalEmitter]);
-
-  const renderDrawio = useCallback((GraphViewer: IGraphViewer) => {
-    if (drawioContainerRef.current == null) {
-      return;
-    }
-
-    const mxgraphs = drawioContainerRef.current.getElementsByClassName('mxgraph');
-    if (mxgraphs.length > 0) {
-      // GROWI では、mxgraph element は最初のものをレンダリングする前提とする
-      const div = mxgraphs[0];
-
-      if (div != null) {
-        div.innerHTML = '';
-        GraphViewer.createViewerForElement(div);
-      }
-    }
-  }, [drawioContainerRef]);
-
-  const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
-
-  useEffect(() => {
-    if (GraphViewer == null) {
-      waitForGraphViewer((gv: IGraphViewer) => {
-        setGraphViewer(() => gv);
-      });
-      return;
-    }
-
-    renderDrawioWithDebounce(GraphViewer);
-  }, [renderDrawioWithDebounce, GraphViewer]);
-
-  return (
-    <div className="editable-with-drawio position-relative">
-      { !isPreview && (
-        <NotAvailableForGuest>
-          <button type="button" className="drawio-iframe-trigger position-absolute btn btn-outline-secondary" onClick={editButtonClickHandler}>
-            <i className="icon-note mr-1"></i>{t('Edit')}
-          </button>
-        </NotAvailableForGuest>
-      ) }
-      <div
-        className="drawio"
-        style={
-          {
-            borderRadius: 3,
-            border: '1px solid #d7d7d7',
-            margin: '20px 0',
-          }
-        }
-        ref={drawioContainerRef}
-        // eslint-disable-next-line react/no-danger
-        dangerouslySetInnerHTML={{ __html: drawioContent }}
-      >
-      </div>
-    </div>
-  );
-
-};
-
-export default Drawio;

+ 1 - 1
packages/app/src/components/EmptyTrashModal.tsx

@@ -59,7 +59,7 @@ const EmptyTrashModal: FC = () => {
   };
 
   return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeEmptyTrashModal} data-testid="page-delete-modal" className="grw-create-page">
+    <Modal size="lg" isOpen={isOpened} toggle={closeEmptyTrashModal} data-testid="page-delete-modal">
       <ModalHeader tag="h4" toggle={closeEmptyTrashModal} className="bg-danger text-light">
         <i className="icon-fw icon-fire"></i>
         {t('modal_empty.empty_the_trash')}

+ 17 - 2
packages/app/src/components/EventListeneres/HashChanged.tsx

@@ -1,12 +1,15 @@
 import React, { useCallback, useEffect } from 'react';
 
-import { useEditorMode, determineEditorModeByHash } from '~/stores/ui';
+import { useRouter } from 'next/router';
+
 import { useIsEditable } from '~/stores/context';
+import { useEditorMode, determineEditorModeByHash } from '~/stores/ui';
 
 /**
  * Change editorMode by browser forward/back operation
  */
 const HashChanged = (): JSX.Element => {
+  const router = useRouter();
   const { data: isEditable } = useIsEditable();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
 
@@ -31,7 +34,19 @@ const HashChanged = (): JSX.Element => {
       window.removeEventListener('hashchange', hashchangeHandler);
     };
 
-  }, [hashchangeHandler, isEditable, mutateEditorMode]);
+  }, [hashchangeHandler, isEditable]);
+
+  /*
+  * Route changes by Next Router
+  * https://nextjs.org/docs/api-reference/next/router
+  */
+  useEffect(() => {
+    router.events.on('routeChangeComplete', hashchangeHandler);
+
+    return () => {
+      router.events.off('routeChangeComplete', hashchangeHandler);
+    };
+  }, [hashchangeHandler, router.events]);
 
   return <></>;
 };

+ 1 - 1
packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -20,7 +20,7 @@ const logger = loggerFactory('growi:InAppNotificationDropdown');
 
 
 export const InAppNotificationDropdown = (): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('commons');
 
   const [isOpen, setIsOpen] = useState(false);
   const limit = 6;

+ 1 - 1
packages/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -97,7 +97,7 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
       break;
     case 'PAGE_UPDATE':
       actionMsg = 'updated on';
-      actionIcon = 'ti-agenda';
+      actionIcon = 'ti ti-agenda';
       break;
     case 'PAGE_RENAME':
       actionMsg = 'renamed';

+ 5 - 3
packages/app/src/components/InAppNotification/InAppNotificationPage.tsx

@@ -20,7 +20,7 @@ const logger = loggerFactory('growi:InAppNotificationPage');
 
 
 export const InAppNotificationPage: FC = () => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('commons');
   const { mutate } = useSWRxInAppNotificationStatus();
 
   const { data: showPageLimitationXL } = useShowPageLimitationXL();
@@ -64,7 +64,7 @@ export const InAppNotificationPage: FC = () => {
 
     if (notificationData == null) {
       return (
-        <div className="wiki">
+        <div className="wiki" data-testid="grw-in-app-notification-page-spinner">
           <div className="text-muted text-center">
             <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
           </div>
@@ -138,7 +138,9 @@ export const InAppNotificationPage: FC = () => {
   };
 
   return (
-    <CustomNavAndContents navTabMapping={navTabMapping} tabContentClasses={['mt-4']} />
+    <div data-testid="grw-in-app-notification-page">
+      <CustomNavAndContents navTabMapping={navTabMapping} tabContentClasses={['mt-4']} />
+    </div>
   );
 };
 

+ 5 - 3
packages/app/src/components/InstallerForm.tsx

@@ -106,11 +106,13 @@ const InstallerForm = memo((): JSX.Element => {
       <div className="row">
         <form role="form" id="register-form" className="col-md-12" onSubmit={submitHandler}>
           <div className="dropdown mb-3">
-            <div className="d-flex dropdown-with-icon">
-              <i className="icon-bubbles border-0 rounded-0" />
+            <div className="input-group">
+              <div className="input-group-prepend dropdown-with-icon">
+                <i className="input-group-text icon-bubbles border-0 rounded-0" />
+              </div>
               <button
                 type="button"
-                className="btn btn-secondary dropdown-toggle text-right w-100 border-0 shadow-none"
+                className="btn btn-secondary dropdown-toggle form-control text-right rounded-right"
                 id="dropdownLanguage"
                 data-testid="dropdownLanguage"
                 data-toggle="dropdown"

+ 1 - 1
packages/app/src/components/Layout/AdminLayout.tsx

@@ -28,7 +28,7 @@ const AdminLayout = ({
   return (
     <RawLayout title={title}>
       <div className={`admin-page ${styles['admin-page']}`}>
-        <GrowiNavbar />
+        <GrowiNavbar isGlobalSearchHidden={true} />
 
         <header className="py-0 container-fluid">
           <h1 className="title px-3">{componentTitle}</h1>

+ 0 - 2
packages/app/src/components/Layout/BasicLayout.tsx

@@ -22,7 +22,6 @@ const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false
 const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal'), { ssr: false });
-const DrawioModal = dynamic(() => import('../PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 // Fab
 const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false });
 
@@ -64,7 +63,6 @@ export const BasicLayout = ({
         <PageDeleteModal />
         <PageRenameModal />
         <PageAccessoriesModal />
-        <DrawioModal />
       </DndProvider>
 
       <PagePresentationModal />

+ 0 - 10
packages/app/src/components/Layout/NoLoginLayout.module.scss

@@ -63,16 +63,6 @@
     }
   }
 
-  .dropdown-with-icon {
-    .dropdown-toggle {
-      @extend .form-control;
-    }
-    i {
-      @extend .input-group-text;
-      margin-right: -1px;
-    }
-  }
-
   .input-group {
     margin-bottom: 10px;
 

+ 1 - 1
packages/app/src/components/LikeButtons.tsx

@@ -56,7 +56,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>
       </button>
 
-      <UncontrolledTooltip placement="top" target="like-button" fade={false}>
+      <UncontrolledTooltip data-testid="like-button-tooltip" placement="top" target="like-button" autohide={false} fade={false}>
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
 

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

@@ -7,8 +7,9 @@ import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
+import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
-import { IErrorV3 } from '~/interfaces/errors/v3-error';
+import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 
@@ -18,7 +19,6 @@ type LoginFormProps = {
   username?: string,
   name?: string,
   email?: string,
-  isRegistrationEnabled: boolean,
   isEmailAuthenticationEnabled: boolean,
   registrationMode: RegistrationMode,
   registrationWhiteList: string[],
@@ -27,14 +27,15 @@ type LoginFormProps = {
   isLdapStrategySetup: boolean,
   isLdapSetupFailed: boolean,
   objOfIsExternalAuthEnableds?: any,
-  isMailerSetup?: boolean
+  isMailerSetup?: boolean,
+  externalAccountLoginError?: IExternalAccountLoginError,
 }
 export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const { t } = useTranslation();
   const router = useRouter();
 
   const {
-    isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled, isRegistrationEnabled,
+    isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
     isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup, objOfIsExternalAuthEnableds,
   } = props;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
@@ -57,6 +58,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
   const [isSuccessToRagistration, setIsSuccessToRagistration] = useState(false);
 
+  const isRegistrationEnabled = isLocalStrategySetup && registrationMode !== RegistrationMode.CLOSED;
 
   useEffect(() => {
     const { hash } = window.location;
@@ -129,7 +131,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   }, [t]);
 
   // wrap error elements which do not use dangerouslySetInnerHtml
-  const generateSafelySetErrors = useCallback((errors: IErrorV3[]): JSX.Element => {
+  const generateSafelySetErrors = useCallback((errors: (IErrorV3 | IExternalAccountLoginError)[]): JSX.Element => {
     if (errors == null || errors.length === 0) return <></>;
     return (
       <ul className="alert alert-danger">
@@ -151,7 +153,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     // Generate login error elements using dangerouslySetInnerHTML
     const loginErrorElementWithDangerouslySetInnerHTML = generateDangerouslySetErrors(loginErrorListForDangerouslySetInnerHTML);
     // Generate login error elements using <ul>, <li>
-    const loginErrorElement = generateSafelySetErrors(loginErrorList);
+
+    const loginErrorElement = props.externalAccountLoginError != null
+      ? generateSafelySetErrors([...loginErrorList, props.externalAccountLoginError])
+      : generateSafelySetErrors(loginErrorList);
 
     return (
       <>

+ 1 - 1
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -20,7 +20,7 @@ type AppearanceModeDropdownProps = {
 }
 export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: AppearanceModeDropdownProps) => {
 
-  const { t } = useTranslation();
+  const { t } = useTranslation('commons');
 
   const { isAuthenticated } = props;
 

+ 1 - 1
packages/app/src/components/Navbar/AuthorInfo.module.scss

@@ -25,7 +25,7 @@ $date-font-size: 11px;
   }
 }
 
-.grw-author-info-skelton :global {
+.grw-author-info-skeleton :global {
   width: 139px;
   height: calc((#{$author-font-size} + #{$date-font-size}) * #{bs.$line-height-base});
 }

+ 15 - 5
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -1,7 +1,10 @@
-import React, { useState, useCallback, useRef } from 'react';
+import React, {
+  useState, useCallback, useRef, useEffect,
+} from 'react';
 
 import assert from 'assert';
 
+import { pathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 
@@ -27,6 +30,8 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
 
   const { dropup } = props;
 
+  const { returnPathForURL } = pathUtils;
+
   const router = useRouter();
 
   const globalSearchFormRef = useRef<IFocusable>(null);
@@ -38,9 +43,13 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
 
   const [text, setText] = useState('');
-  const [isScopeChildren, setScopeChildren] = useState<boolean|undefined>(isSearchScopeChildrenAsDefault);
+  const [isScopeChildren, setScopeChildren] = useState<boolean|undefined>(isSearchScopeChildrenAsDefault ?? false);
   const [isFocused, setFocused] = useState<boolean>(false);
 
+  useEffect(() => {
+    setScopeChildren(isSearchScopeChildrenAsDefault);
+  }, [isSearchScopeChildrenAsDefault]);
+
 
   const gotoPage = useCallback((data: IPageWithSearchMeta[]) => {
     assert(data.length > 0);
@@ -49,9 +58,9 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
 
     // navigate to page
     if (page != null) {
-      router.push(`/${page._id}`);
+      router.push(returnPathForURL(page.path, page._id));
     }
-  }, [router]);
+  }, [returnPathForURL, router]);
 
   const search = useCallback(() => {
     const url = new URL(window.location.href);
@@ -73,6 +82,7 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
 
   const isIndicatorShown = !isFocused && (text.length === 0);
 
+
   if (isScopeChildren == null || isSearchServiceReachable == null) {
     return <></>;
   }
@@ -116,7 +126,7 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
         </div>
         <SearchForm
           ref={globalSearchFormRef}
-          isSearchServiceReachable={isSearchServiceReachable}
+          isSearchServiceReachable={isSearchServiceReachable || false}
           dropup={dropup}
           onChange={gotoPage}
           onBlur={() => setFocused(false)}

+ 9 - 13
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -15,8 +15,7 @@ import {
 import { IResTagsUpdateApiv1 } from '~/interfaces/tag';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
-  useCurrentPageId, useCurrentPathname,
-  useIsNotFound,
+  useCurrentPageId, useCurrentPathname, useIsNotFound,
   useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useTemplateTagData, useIsContainerFluid,
 } from '~/stores/context';
 import { usePageTagsForEditors } from '~/stores/editor';
@@ -35,7 +34,7 @@ import AttachmentIcon from '../Icons/AttachmentIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
-import { Skelton } from '../Skelton';
+import { Skeleton } from '../Skeleton';
 
 import type { AuthorInfoProps } from './AuthorInfo';
 import { GrowiSubNavigation } from './GrowiSubNavigation';
@@ -45,22 +44,22 @@ import AuthorInfoStyles from './AuthorInfo.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
 
-const AuthorInfoSkelton = () => <Skelton additionalClass={`${AuthorInfoStyles['grw-author-info-skelton']} py-1`} />;
+const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
 
 
 const PageEditorModeManager = dynamic(
   () => import('./PageEditorModeManager'),
-  { ssr: false, loading: () => <Skelton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skelton']}`} /> },
+  { ssr: false, loading: () => <Skeleton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skeleton']}`} /> },
 );
 // TODO: If enable skeleton, we get hydration error when create a page from PageCreateModal
-// { ssr: false, loading: () => <Skelton additionalClass='btn-skelton py-2' /> },
+// { ssr: false, loading: () => <Skeleton additionalClass='btn-skeleton py-2' /> },
 const SubNavButtons = dynamic<SubNavButtonsProps>(
   () => import('./SubNavButtons').then(mod => mod.SubNavButtons),
   { ssr: false, loading: () => <></> },
 );
 const AuthorInfo = dynamic<AuthorInfoProps>(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), {
   ssr: false,
-  loading: AuthorInfoSkelton,
+  loading: AuthorInfoSkeleton,
 });
 
 type PageOperationMenuItemsProps = {
@@ -229,10 +228,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
   useEffect(() => {
     if (pageId === null && templateTagData != null) {
-      const tags = templateTagData.split(',').filter((str: string) => {
-        return str !== ''; // filter empty values
-      });
-      mutatePageTagsForEditors(tags);
+      mutatePageTagsForEditors(templateTagData);
     }
   }, [pageId, mutatePageTagsForEditors, templateTagData, mutateSWRTagsInfo]);
 
@@ -387,13 +383,13 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
               <li className="pb-1">
                 { currentPage != null
                   ? <AuthorInfo user={currentPage.creator as IUser} date={currentPage.createdAt} mode="create" locate="subnav" />
-                  : <AuthorInfoSkelton />
+                  : <AuthorInfoSkeleton />
                 }
               </li>
               <li className="mt-1 pt-1 border-top">
                 { currentPage != null
                   ? <AuthorInfo user={currentPage.lastUpdateUser as IUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
-                  : <AuthorInfoSkelton />
+                  : <AuthorInfoSkeleton />
                 }
               </li>
             </ul>

+ 9 - 3
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -59,7 +59,7 @@ const NavbarRight = memo((): JSX.Element => {
             onClick={() => openCreateModal(currentPagePath || '')}
           >
             <i className="icon-pencil mr-2"></i>
-            <span className="d-none d-lg-block">{ t('New') }</span>
+            <span className="d-none d-lg-block">{ t('commons:New') }</span>
           </button>
         </li>
 
@@ -136,7 +136,13 @@ const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
 
 GrowiNavbarLogo.displayName = 'GrowiNavbarLogo';
 
-export const GrowiNavbar = (): JSX.Element => {
+type Props = {
+  isGlobalSearchHidden?: boolean
+}
+
+export const GrowiNavbar = (props: Props): JSX.Element => {
+
+  const { isGlobalSearchHidden } = props;
 
   const GlobalSearch = dynamic<GlobalSearchProps>(() => import('./GlobalSearch').then(mod => mod.GlobalSearch), { ssr: false });
 
@@ -169,7 +175,7 @@ export const GrowiNavbar = (): JSX.Element => {
       </ul>
 
       <div className="grw-global-search-container position-absolute">
-        { isSearchServiceConfigured && !isDeviceSmallerThanMd && !isSearchPage && (
+        { !isGlobalSearchHidden && isSearchServiceConfigured && !isDeviceSmallerThanMd && !isSearchPage && (
           <GlobalSearch />
         ) }
       </div>

+ 2 - 2
packages/app/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -59,7 +59,7 @@
       }
     }
 
-    .btn-skelton {
+    .btn-skeleton {
       @extend %subnav-buttons-height;
       width: 100%;
     }
@@ -140,7 +140,7 @@
         min-height: 90px;
       }
 
-      .btn-skelton {
+      .btn-skeleton {
         @extend %compact-subnav-buttons-height;
         width: 100%;
       }

+ 3 - 3
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -6,7 +6,7 @@ import {
   EditorMode, useEditorMode,
 } from '~/stores/ui';
 
-import { TagLabelsSkelton } from '../Page/TagLabels';
+import { TagLabelsSkeleton } from '../Page/TagLabels';
 import PagePathNav from '../PagePathNav';
 
 import DrawerToggler from './DrawerToggler';
@@ -17,7 +17,7 @@ import styles from './GrowiSubNavigation.module.scss';
 
 const TagLabels = dynamic(() => import('../Page/TagLabels').then(mod => mod.TagLabels), {
   ssr: false,
-  loading: TagLabelsSkelton,
+  loading: TagLabelsSkeleton,
 });
 
 
@@ -71,7 +71,7 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
             <div className="grw-taglabels-container">
               { tags != null
                 ? <TagLabels tags={tags} isGuestUser={isGuestUser ?? false} tagsUpdateInvoked={tagsUpdatedHandler} />
-                : <TagLabelsSkelton />
+                : <TagLabelsSkeleton />
               }
             </div>
           ) }

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff