Răsfoiți Sursa

Merge remote-tracking branch 'origin/master' into imprv/error-handling

mizozobu 3 ani în urmă
părinte
comite
6270dd1c7c
100 a modificat fișierele cu 1077 adăugiri și 1439 ștergeri
  1. 0 0
      .github/dependabot.yml.org
  2. 2 6
      .github/workflows/ci-app-prod.yml
  3. 9 8
      .github/workflows/ci-app.yml
  4. 2 2
      .github/workflows/release-rc.yml
  5. 2 2
      .github/workflows/release-slackbot-proxy.yml
  6. 9 8
      .github/workflows/release.yml
  7. 5 1
      .github/workflows/reusable-app-prod.yml
  8. 2 6
      .vscode/launch.json
  9. 1 1
      lerna.json
  10. 4 2
      package.json
  11. 0 0
      packages-obsolete/plugin-attachment-refs/.eslintignore
  12. 0 0
      packages-obsolete/plugin-attachment-refs/.gitignore
  13. 0 0
      packages-obsolete/plugin-attachment-refs/README.md
  14. 0 0
      packages-obsolete/plugin-attachment-refs/package.json
  15. 0 0
      packages-obsolete/plugin-attachment-refs/src/client-entry.js
  16. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/css/index.css
  17. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/components/AttachmentList.jsx
  18. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/components/ExtractedAttachments.jsx
  19. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/GalleryContext.js
  20. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js
  21. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js
  22. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/RefsContext.js
  23. 0 0
      packages-obsolete/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js
  24. 0 0
      packages-obsolete/plugin-attachment-refs/src/index.js
  25. 0 0
      packages-obsolete/plugin-attachment-refs/src/server-entry.js
  26. 0 0
      packages-obsolete/plugin-attachment-refs/src/server/routes/index.js
  27. 0 0
      packages-obsolete/plugin-attachment-refs/src/server/routes/refs.js
  28. 0 0
      packages-obsolete/plugin-attachment-refs/src/utils/logger/index.ts
  29. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.base.json
  30. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.build.cjs.json
  31. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.build.esm.json
  32. 0 0
      packages-obsolete/plugin-attachment-refs/tsconfig.json
  33. 0 0
      packages/app/_obsolete/src/client/legacy/crowi.js
  34. 0 2
      packages/app/_obsolete/src/client/nologin.jsx
  35. 16 14
      packages/app/_obsolete/src/client/services/PageContainer.js
  36. 0 17
      packages/app/bin/templates/plugin-definitions.js.swig
  37. 29 0
      packages/app/cypress.config.ts
  38. 0 18
      packages/app/cypress.json
  39. 4 4
      packages/app/docker/Dockerfile
  40. 1 0
      packages/app/next.config.js
  41. 19 31
      packages/app/package.json
  42. 38 60
      packages/app/public/static/locales/en_US/admin.json
  43. 3 5
      packages/app/public/static/locales/en_US/translation.json
  44. 29 49
      packages/app/public/static/locales/ja_JP/admin.json
  45. 3 3
      packages/app/public/static/locales/ja_JP/translation.json
  46. 9 44
      packages/app/public/static/locales/zh_CN/admin.json
  47. 3 5
      packages/app/public/static/locales/zh_CN/translation.json
  48. 0 77
      packages/app/src/client/services/AdminBasicSecurityContainer.js
  49. 8 12
      packages/app/src/client/services/AdminCustomizeContainer.js
  50. 0 18
      packages/app/src/client/services/AdminGeneralSecurityContainer.js
  51. 0 2
      packages/app/src/client/services/AdminHomeContainer.js
  52. 14 11
      packages/app/src/client/services/AdminMarkDownContainer.js
  53. 0 104
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  54. 34 0
      packages/app/src/client/services/activate-plugin.ts
  55. 32 0
      packages/app/src/client/services/layout.ts
  56. 43 1
      packages/app/src/client/services/page-operation.ts
  57. 6 52
      packages/app/src/client/util/apiNotification.js
  58. 91 0
      packages/app/src/client/util/toastr.ts
  59. 0 8
      packages/app/src/components/Admin/AdminHome/AdminHome.jsx
  60. 0 55
      packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx
  61. 1 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  62. 3 2
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  63. 21 0
      packages/app/src/components/Admin/Common/AdminInstallButtonRow.tsx
  64. 6 3
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  65. 8 8
      packages/app/src/components/Admin/Customize/Customize.jsx
  66. 4 1
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  67. 33 21
      packages/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  68. 28 45
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  69. 0 105
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  70. 67 0
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx
  71. 37 24
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  72. 0 47
      packages/app/src/components/Admin/Customize/ThemeColorBox.jsx
  73. 42 0
      packages/app/src/components/Admin/Customize/ThemeColorBox.tsx
  74. 4 1
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  75. 15 11
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  76. 13 0
      packages/app/src/components/Admin/PluginsExtension/Loading.js
  77. 68 0
      packages/app/src/components/Admin/PluginsExtension/PluginCard.module.scss
  78. 85 0
      packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx
  79. 85 0
      packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx
  80. 58 0
      packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx
  81. 0 42
      packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx
  82. 0 139
      packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx
  83. 2 2
      packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx
  84. 10 28
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  85. 0 41
      packages/app/src/components/Admin/Security/TwitterSecuritySetting.jsx
  86. 0 223
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  87. 2 2
      packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  88. 1 4
      packages/app/src/components/BookmarkButtons.tsx
  89. 26 20
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  90. 1 1
      packages/app/src/components/CompleteUserRegistration.tsx
  91. 2 2
      packages/app/src/components/CreateTemplateModal.jsx
  92. 1 1
      packages/app/src/components/DescendantsPageListModal.module.scss
  93. 2 2
      packages/app/src/components/DescendantsPageListModal.tsx
  94. 1 1
      packages/app/src/components/EmptyTrashModal.tsx
  95. 2 2
      packages/app/src/components/InAppNotification/InAppNotificationList.tsx
  96. 3 6
      packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  97. 5 3
      packages/app/src/components/InstallerForm.tsx
  98. 2 2
      packages/app/src/components/Layout/Admin.module.scss
  99. 6 7
      packages/app/src/components/Layout/AdminLayout.tsx
  100. 15 14
      packages/app/src/components/Layout/BasicLayout.tsx

+ 0 - 0
.github/dependabot.yml → .github/dependabot.yml.org


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

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

+ 9 - 8
.github/workflows/ci-app.yml

@@ -16,11 +16,9 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
-      - packages/remark-drawio-plugin/**
-      - packages/remark-growi-plugin/**
+      - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
-      - packages/plugin-*/**
 
 jobs:
   lint:
@@ -54,12 +52,15 @@ jobs:
         run: |
           npx lerna bootstrap -- --frozen-lockfile
 
-      - name: lerna run lint for plugins
+      - name: lerna run lint for dependent packages
         run: |
-          yarn lerna run lint --scope @growi/remark-* --scope @growi/plugin-*
+          yarn lerna run lint --scope @growi/codemirror-textlint --scope @growi/core --scope @growi/hackmd --scope @growi/preset-themes --scope @growi/remark-* --scope @growi/slack --scope @growi/ui
+      - name: build dependent packages
+        run: |
+          yarn lerna run build --scope @growi/preset-themes
       - 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
+          yarn lerna run lint --scope @growi/app
 
       - name: Slack Notification
         uses: weseek/ghaction-slack-notification@master
@@ -110,7 +111,7 @@ jobs:
 
       - name: lerna run test for plugins
         run: |
-          yarn lerna run test --scope @growi/remark-* --scope @growi/plugin-*
+          yarn lerna run test --scope @growi/remark-*
 
       - name: Test app
         working-directory: ./packages/app
@@ -125,7 +126,7 @@ jobs:
           name: Coverage Report
           path: |
             packages/app/coverage
-            packages/remark-growi-plugin/coverage
+            packages/remark-growi-directive/coverage
 
       - name: Slack Notification
         uses: weseek/ghaction-slack-notification@master

+ 2 - 2
.github/workflows/release-rc.yml

@@ -34,14 +34,14 @@ jobs:
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
 
     - name: Login to GitHub Container Registry
-      uses: docker/login-action@v1
+      uses: docker/login-action@v2
       with:
         registry: ghcr.io
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v1
+      uses: docker/setup-buildx-action@v2
 
     - name: Build and push
       uses: docker/build-push-action@v2

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

@@ -36,7 +36,7 @@ jobs:
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
 
     - name: Login to GitHub Container Registry
-      uses: docker/login-action@v1
+      uses: docker/login-action@v2
       with:
         registry: ghcr.io
         username: wsmoogle
@@ -55,7 +55,7 @@ jobs:
         gcloud auth configure-docker --quiet
 
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v1
+      uses: docker/setup-buildx-action@v2
 
     - name: Build and push
       uses: docker/build-push-action@v2

+ 9 - 8
.github/workflows/release.yml

@@ -144,7 +144,7 @@ jobs:
 
     - name: Docker meta
       id: meta
-      uses: docker/metadata-action@v3
+      uses: docker/metadata-action@v4
       with:
         images: weseek/growi,ghcr.io/weseek/growi
         flavor: |
@@ -155,22 +155,24 @@ jobs:
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
 
-    - name: Login to docker.io registry
-      run: |
-        echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
+    - name: Login to Docker Hub
+      uses: docker/login-action@v2
+      with:
+        username: wsmoogle
+        password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
 
     - name: Login to GitHub Container Registry
-      uses: docker/login-action@v1
+      uses: docker/login-action@v2
       with:
         registry: ghcr.io
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v1
+      uses: docker/setup-buildx-action@v2
 
     - name: Build and push
-      uses: docker/build-push-action@v2
+      uses: docker/build-push-action@v3
       with:
         context: .
         file: ./packages/app/docker/Dockerfile
@@ -202,4 +204,3 @@ jobs:
       run: |
         STATUS=`git status --porcelain`
         if [ -z "$STATUS" ]; then exit 0; else exit 1; fi
-

+ 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.9",
+  "version": "6.0.0-RC.13",
   "packages": [
     "packages/*"
   ]

+ 4 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.0.0-RC.9",
+  "version": "6.0.0-RC.13",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -61,7 +61,8 @@
     "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
-    "cypress": "^9.2.0",
+    "cypress": "^12.0.1",
+    "cypress-wait-until": "^1.7.2",
     "eslint": "^8.18.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.0",
@@ -89,6 +90,7 @@
     "ts-node": "^10.9.1",
     "tsconfig-paths": "^3.9.0",
     "typescript": "~4.7",
+    "vite": "^3.2.5",
     "yargs": "^17.3.1"
   },
   "engines": {

+ 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


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


+ 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


+ 0 - 0
packages/app/src/client/legacy/crowi.js → packages/app/_obsolete/src/client/legacy/crowi.js


+ 0 - 2
packages/app/_obsolete/src/client/nologin.jsx

@@ -49,10 +49,8 @@ if (loginFormElem) {
     google: loginFormElem.dataset.isGoogleAuthEnabled === 'true',
     github: loginFormElem.dataset.isGithubAuthEnabled === 'true',
     facebook: loginFormElem.dataset.isFacebookAuthEnabled === 'true',
-    twitter: loginFormElem.dataset.isTwitterAuthEnabled === 'true',
     saml: loginFormElem.dataset.isSamlAuthEnabled === 'true',
     oidc: loginFormElem.dataset.isOidcAuthEnabled === 'true',
-    basic: loginFormElem.dataset.isBasicAuthEnabled === 'true',
   };
 
   Object.assign(componentMappings, {

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

+ 29 - 0
packages/app/cypress.config.ts

@@ -0,0 +1,29 @@
+import { defineConfig } from 'cypress';
+
+export default defineConfig({
+  e2e: {
+    baseUrl: 'http://localhost:3000',
+    specPattern: 'test/cypress/integration/',
+    supportFile: 'test/cypress/support/index.ts',
+    setupNodeEvents: (on) => {
+      // change screen size
+      // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
+      on('before:browser:launch', (browser, launchOptions) => {
+        if (browser.name === 'chrome' && browser.isHeadless) {
+          launchOptions.args.push('--window-size=1400,1024');
+          launchOptions.args.push('--force-device-scale-factor=1');
+        }
+        return launchOptions;
+      });
+    },
+    defaultCommandTimeout: 7000,
+  },
+  fileServerFolder: 'test/cypress',
+  fixturesFolder: 'test/cypress/fixtures',
+  screenshotsFolder: 'test/cypress/screenshots',
+  videosFolder: 'test/cypress/videos',
+
+  viewportWidth: 1400,
+  viewportHeight: 1024,
+
+});

+ 0 - 18
packages/app/cypress.json

@@ -1,18 +0,0 @@
-{
-  "baseUrl": "http://localhost:3000",
-
-  "fileServerFolder": "test/cypress",
-  "fixturesFolder": "test/cypress/fixtures",
-  "integrationFolder": "test/cypress/integration",
-  "screenshotsFolder": "test/cypress/screenshots",
-  "videosFolder": "test/cypress/videos",
-  "supportFile": "test/cypress/support/index.ts",
-  "pluginsFile": "test/cypress/plugins/index.ts",
-  "testFiles": "**/*.spec.ts",
-
-  "viewportWidth": 1400,
-  "viewportHeight": 1024,
-
-  "experimentalSessionSupport": true,
-  "defaultCommandTimeout": 30000
-}

+ 4 - 4
packages/app/docker/Dockerfile

@@ -105,13 +105,13 @@ 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-drawio packages/remark-drawio
+COPY packages/remark-growi-directive packages/remark-growi-directive
+COPY packages/remark-lsx packages/remark-lsx
 COPY packages/hackmd packages/hackmd
+COPY packages/preset-themes packages/preset-themes
 
 # build
 RUN yarn lerna run 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',

+ 19 - 31
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.0-RC.9",
+  "version": "6.0.0-RC.13",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -46,14 +46,14 @@
     "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "resources:hackmd": "yarn lerna run build --scope=@growi/hackmd",
-    "resources:dummy": "true",
-    "// resources:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
+    "resources:preset-themes": "yarn lerna run build --scope=@growi/preset-themes",
     "// 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"
   },
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
     "escape-string-regexp": "5.0.0 or above exports only ESM",
+    "next": "/Sandbox rendering is crashed with v12.3 or above ",
     "string-width": "5.0.0 or above exports only ESM."
   },
   "dependencies": {
@@ -65,22 +65,22 @@
     "@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.9",
-    "@growi/core": "^6.0.0-RC.9",
-    "@growi/hackmd": "^6.0.0-RC.9",
-    "@growi/plugin-attachment-refs": "^6.0.0-RC.9",
-    "@growi/plugin-lsx": "^6.0.0-RC.9",
-    "@growi/slack": "^6.0.0-RC.9",
+    "@growi/codemirror-textlint": "^6.0.0-RC.13",
+    "@growi/core": "^6.0.0-RC.13",
+    "@growi/hackmd": "^6.0.0-RC.13",
+    "@growi/preset-themes": "^6.0.0-RC.13",
+    "@growi/remark-drawio": "^6.0.0-RC.13",
+    "@growi/remark-growi-directive": "^6.0.0-RC.13",
+    "@growi/remark-lsx": "^6.0.0-RC.13",
+    "@growi/slack": "^6.0.0-RC.13",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
-    "@slack/events-api": "^3.0.0",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "JSONStream": "^1.3.5",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
     "async-canvas-to-blob": "^1.0.3",
-    "aws-sdk": "^2.1044.0",
     "axios": "^0.24.0",
     "axios-retry": "^3.2.4",
     "body-parser": "^1.18.2",
@@ -123,6 +123,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",
@@ -133,7 +134,7 @@
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
-    "next": "^12.2.5",
+    "next": "~12.2",
     "next-i18next": "^12.1.0",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.0",
@@ -145,18 +146,15 @@
     "passport": "^0.6.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
-    "passport-http": "^0.3.0",
     "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
-    "passport-twitter": "^1.0.4",
-    "prism-themes": "^1.9.0",
-    "prom-client": "^13.0.0",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react-bootstrap-typeahead": "^5.2.2",
     "react-card-flip": "^1.0.10",
     "react-datepicker": "^4.7.0",
+    "react-disable": "^0.1.1",
     "react-dnd": "^14.0.5",
     "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
@@ -165,8 +163,9 @@
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-syntax-highlighter": "^15.5.0",
+    "react-toastify": "^9.1.1",
     "react-use-ripple": "^1.5.2",
-    "reactstrap": "^8.9.0",
+    "reactstrap": "^8.10.1",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "rehype-katex": "^6.0.2",
@@ -179,10 +178,7 @@
     "remark-gfm": "^3.0.1",
     "remark-math": "^5.1.1",
     "remark-wiki-link": "^1.0.4",
-    "rimraf": "^3.0.0",
-    "simplebar-react": "^2.3.6",
     "socket.io": "^4.2.0",
-    "sticky-events": "^3.4.11",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
@@ -206,23 +202,16 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^6.0.0-RC.9",
+    "@growi/ui": "^6.0.0-RC.13",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
-    "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
-    "@types/jquery": "^3.5.8",
-    "@types/multer": "^1.4.5",
     "@types/react-scroll": "^1.8.4",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
     "bootstrap": "^4.6.1",
-    "browser-sync": "^2.27.7",
-    "bunyan-debug": "^2.0.0",
-    "cli": "~1.0.1",
     "codemirror": "^5.64.0",
-    "colors": "=1.4.0",
     "connect-browser-sync": "^2.1.0",
     "core-js": "=2.6.9",
     "diff2html": "^3.1.2",
@@ -237,27 +226,26 @@
     "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",
-    "normalize-path": "^3.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "prettier": "^1.19.1",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",
-    "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-waypoint": "^10.1.0",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
     "reveal.js": "^4.3.1",
     "sass": "^1.53.0",
+    "simplebar-react": "^2.3.6",
     "simple-line-icons": "^2.5.5",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^4.2.0",
+    "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
     "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9"

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

@@ -2,11 +2,13 @@
   "meta": {
     "display_name": "English"
   },
-  "wiki_management_home_page": "Wiki Management Home Page",
   "last_login": "Last login",
-  "anyone_with_the_link": "anyone with the link",
-  "only_me": "only me",
-  "only_inside_the_group": "only inside the group",
+  "wiki_management_home_page": "Wiki Management Home Page",
+  "public": "Public",
+  "anyone_with_the_link": "Anyone with the link",
+  "specified_users": "Specified users",
+  "only_me": "Only me",
+  "only_inside_the_group": "Only inside the group",
   "security_settings": {
     "security_settings": "Security Settings",
     "scope_of_page_disclosure": "Scope of page disclosure",
@@ -155,13 +157,6 @@
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
     },
-    "Basic": {
-      "enable_basic": "Enable Basic",
-      "name": "Basic Authentication",
-      "desc_1": "Login with <code>username</code> in Authorization header.",
-      "desc_2": "User will be automatically generated if not exist.",
-      "updated_basic": "Succeeded to update Basic setting"
-    },
     "OAuth": {
       "enable_oidc": "Enable OIDC",
       "register": "Register for %s",
@@ -179,16 +174,6 @@
       "Facebook": {
         "name": "Facebook OAuth"
       },
-      "Twitter": {
-        "enable_twitter": "Enable Twitter OAuth",
-        "name": "Twitter OAuth",
-        "register_1": "Access {{link}}",
-        "register_2": "Sign in Twitter",
-        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
-        "register_5": "Copy and paste your ClientID and Client Secret above",
-        "updated_twitter": "Succeeded to update Twitter OAuth setting"
-      },
       "GitHub": {
         "enable_github": "Enable GitHub OAuth",
         "name": "GitHub OAuth",
@@ -203,7 +188,7 @@
         "username_detail": "Specification of mappings for <code>username</code> when creating new users",
         "name_detail": "Specification of mappings for <code>name</code> when creating new users",
         "mapping_detail": "Specification of mappings for %s when creating new users",
-        "register_1": "Contant to OIDC IdP Administrator",
+        "register_1": "Contact to OIDC IdP Administrator",
         "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
         "register_3": "Copy and paste your ClientID and Client Secret above",
         "updated_oidc": "Succeeded to update OpenID Connect",
@@ -212,7 +197,6 @@
       "how_to": {
         "google": "How to configure Google OAuth?",
         "github": "How to configure GitHub OAuth?",
-        "twitter": "How to configure Twitter OAuth?",
         "oidc": "How to configure OIDC?"
       }
     },
@@ -279,13 +263,35 @@
     "toggle_notification": "Updated setting of {{path}}",
     "not_found_global_notification_triggerid": "Not found the global notification id"
   },
+  "full_text_search_management": {
+    "full_text_search_management": "Full Text Search Management",
+    "elasticsearch_management": "Elasticsearch management",
+    "connection_status": "Connection status",
+    "connection_status_label_unconfigured": "UNCONFIGURED",
+    "connection_status_label_connected": "CONNECTED",
+    "connection_status_label_disconnected": "DISCONNECTED",
+    "connection_status_label_erroroccured": "ERROR OCCURED ON SEARCH SERVICE",
+    "indices_status": "Indices Status",
+    "indices_status_label_normalized": "NORMALIZED",
+    "indices_status_label_unnormalized": "REBUILDING or BROKEN",
+    "indices_summary": "Indices summary",
+    "reconnect": "Reconnect",
+    "reconnect_button": "Try to reconnect",
+    "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
+    "normalize": "Normalize",
+    "normalize_button": "Normalize indices",
+    "normalize_description": "Click the button to repair broken indices.",
+    "rebuild": "Rebuild",
+    "rebuild_button": "Rebuild index",
+    "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
+    "rebuild_description_2": "This may take a while."
+  },
   "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
     "management_wiki": "Management Wiki",
     "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",
@@ -481,8 +487,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - The site name of this wiki.",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - The page name of the current page.",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - The page path of the current page.",
-    "custom_header": "Custom HTML header",
-    "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
+    "custom_noscript": "Custom Noscript",
+    "custom_noscript_detail": "You can customize Noscript code that applies all pages. Your custom Noscript will be inserted into the <code>&lt;noscript&gt;</code> tag that is located as the first element of body.<br>Relaod page to see changes.",
     "custom_css": "Custom CSS",
     "write_css": "You can write CSS that is applied to whole system.",
     "ctrl_space": "Ctrl+Space to autocomplete",
@@ -498,6 +504,11 @@
   },
   "importer_management": {
     "import_data": "Import Data",
+    "article": "Article",
+    "category": "Category",
+    "tag": "Tag",
+    "page": "Page",
+    "page_path": "Page Path",
     "beta_warning": "This function is Beta.",
     "import_from": "Import from {{from}}",
     "import_growi_archive": "Import GROWI archive",
@@ -636,7 +647,6 @@
     "integration_procedure": "Integration Procedure",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
     "integration_failed":"Integration failed",
-    "official_bot_settings": "Official bot Settings",
     "reset": "Reset",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Delete Slack Bot settings",
@@ -709,7 +719,6 @@
   "slack_integration_legacy": {
     "slack_integration_legacy": "Legacy Slack Integration",
     "alert_disabled": "This 'Slack Legacy Intenfation' has been currently disabled since <a href='/admin/slack-integration'>New settings</a> are enabled",
-    "alert_Pd": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
     "alert_deplicated": "This 'Legacy Slack Integration' is outdated and will be discontinued in the future. Use <a href='/admin/slack-integration'>the new settings</a> instead. "
   },
   "user_management": {
@@ -819,29 +828,6 @@
       "force_update_parents_description": "Enable this option to force the addition of missing users to the ancestor groups if they exist after changing a parent group."
     }
   },
-  "full_text_search_management": {
-    "full_text_search_management": "Full Text Search Management",
-    "elasticsearch_management": "Elasticsearch management",
-    "connection_status": "Connection status",
-    "connection_status_label_unconfigured": "UNCONFIGURED",
-    "connection_status_label_connected": "CONNECTED",
-    "connection_status_label_disconnected": "DISCONNECTED",
-    "connection_status_label_erroroccured": "ERROR OCCURED ON SEARCH SERVICE",
-    "indices_status": "Indices Status",
-    "indices_status_label_normalized": "NORMALIZED",
-    "indices_status_label_unnormalized": "REBUILDING or BROKEN",
-    "indices_summary": "Indices summary",
-    "reconnect": "Reconnect",
-    "reconnect_button": "Try to reconnect",
-    "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
-    "normalize": "Normalize",
-    "normalize_button": "Normalize indices",
-    "normalize_description": "Click the button to repair broken indices.",
-    "rebuild": "Rebuild",
-    "rebuild_button": "Rebuild index",
-    "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
-    "rebuild_description_2": "This may take a while."
-  },
   "audit_log_management": {
     "audit_log": "Audit Log",
     "audit_log_settings": "Audit Log Settings",
@@ -884,10 +870,8 @@
     "USER_LOGIN_WITH_LDAP": "Login with LDAP",
     "USER_LOGIN_WITH_GOOGLE": "Login with Google",
     "USER_LOGIN_WITH_GITHUB": "Login with GitHub",
-    "USER_LOGIN_WITH_TWITTER": "Login with Twitter",
     "USER_LOGIN_WITH_OIDC": "Login with OIDC",
     "USER_LOGIN_WITH_SAML": "Login with SAML",
-    "USER_LOGIN_WITH_BASIC": "Login with BASIC",
     "USER_LOGIN_FAILURE": "Login failure",
     "USER_LOGOUT": "Logout",
     "USER_FOGOT_PASSWORD": "Request password reset",
@@ -965,18 +949,12 @@
     "ADMIN_AUTH_OIDC_ENABLED": "Enable OIDC auth",
     "ADMIN_AUTH_OIDC_DISABLED": "Disable OIDC auth",
     "ADMIN_AUTH_OIDC_UPDATE": "Update OIDC settings",
-    "ADMIN_AUTH_BASIC_ENABLED": "Enable BASIC auth",
-    "ADMIN_AUTH_BASIC_DISABLED": "Disable BASIC auth",
-    "ADMIN_AUTH_BASIC_UPDATE": "Update BASIC auth settings",
     "ADMIN_AUTH_GOOGLE_ENABLED": "Enable Google auth",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Disable Google auth",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Update Google auth settings",
     "ADMIN_AUTH_GITHUB_ENABLED": "Enable GitHub auth",
     "ADMIN_AUTH_GITHUB_DISABLED": "Disable GitHub auth",
     "ADMIN_AUTH_GITHUB_UPDATE": "Update GitHub auth settings",
-    "ADMIN_AUTH_TWITTER_ENABLED": "Enable Twitter auth",
-    "ADMIN_AUTH_TWITTER_DISABLED": "Disable Twitter auth",
-    "ADMIN_AUTH_TWITTER_UPDATE": "Update Twitter auth settings",
     "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "Update Link Break settings",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "Update Indent settings",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "Update Presentation setting",
@@ -987,7 +965,7 @@
     "ADMIN_FUNCTION_UPDATE": "Update Function",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "Update Code Highlight",
     "ADMIN_CUSTOM_TITLE_UPDATE": "Update Custom Title",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "Update Custom HTML header",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "Update Custom noscript",
     "ADMIN_CUSTOM_CSS_UPDATE": "Update Custom CSS",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "Update Custom script",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "Upload Archived Data",

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

@@ -35,7 +35,6 @@
   "add": "Add",
   "Undo": "Undo",
   "Article": "Article",
-  "Page": "Page",
   "Page Path": "Page path",
   "Category": "Category",
   "User": "User",
@@ -115,7 +114,6 @@
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "Basic Settings": "Basic Settings",
-  "Basic authentication": "Basic authentication",
   "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",
@@ -586,6 +584,7 @@
     }
   },
   "private_legacy_pages": {
+    "title": "Private Legacy Pages",
     "bulk_operation": "Bulk operation",
     "convert_all_selected_pages": "Convert all to new v5 compatible format",
     "input_path_to_convert": "Input a path to convert pages",
@@ -620,8 +619,8 @@
     }
   },
   "login": {
-    "Sign in error": "Login error",
-    "Registration successful": "Registration successful. Please wait for administrator approval.",
+    "sign_in_error": "Login error",
+    "registration_successful": "registration_successful. Please wait for administrator approval.",
     "Setup": "Setup",
     "enabled_ldap_has_configuration_problem":"LDAP is enabled but the configuration has something wrong.",
     "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"
@@ -747,7 +746,6 @@
     "logout": "Logout"
   },
   "pagetree": {
-    "private_legacy_pages": "Private Legacy Pages",
     "cannot_rename_a_title_that_contains_slash": "Cannot rename a title that contains '/'",
     "you_cannot_move_this_page_now": "You cannot move this page now",
     "something_went_wrong_with_moving_page": "Something went wrong with moving page"

+ 29 - 49
packages/app/public/static/locales/ja_JP/admin.json

@@ -64,6 +64,9 @@
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
+    "providerName": "プロバイダ名",
+    "issuerHost": "発行ホスト",
+    "scope": "範囲",
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "authorization_endpoint": "認可エンドポイント",
     "token_endpoint": "トークンエンドポイント",
@@ -162,13 +165,6 @@
       "attr_based_login_control_rule_exampl2": "<h5>エスケープの例</h5>ルールに URL を利用したい場合は、次のようにエスケープしてください:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
     },
-    "Basic": {
-      "enable_basic": "Basic を有効にする",
-      "name": "Basic 認証",
-      "desc_1": "Authorization ヘッダに格納されている <code>username</code> でログインします。",
-      "desc_2": "ユーザーが存在しなかった場合は自動生成します。",
-      "updated_basic": "Basic認証 を更新しました"
-    },
     "OAuth": {
       "enable_oidc": "OIDC を有効にする",
       "register": "%sに登録",
@@ -186,16 +182,6 @@
       "Facebook": {
         "name": "Facebook OAuth"
       },
-      "Twitter": {
-        "enable_twitter": "Twitter OAuth を有効にする",
-        "name": "Twitter OAuth",
-        "register_1": "{{link}} へアクセス",
-        "register_2": "Twitterにサインイン",
-        "register_3": "Create New Appをクリック &rightarrow; Application Detailsの各項目を入力",
-        "register_4": "Create your Twitter Applicationで作成",
-        "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力",
-        "updated_twitter": "Twitter OAuth を更新しました"
-      },
       "GitHub": {
         "enable_github": "GitHub OAuth を有効にする",
         "name": "GitHub OAuth",
@@ -210,13 +196,16 @@
         "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
         "name_detail": "新規ユーザー名(<code>name</code>)に関連付ける属性",
         "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
+        "register_1": "OIDC IdP Administrator へ接続します。",
+        "register_2": "OIDCアプリの認証コールバックURLを<code>%s</code>として登録します。",
+        "register_3": "上記のClientIDとClient Secretをコピー&ペーストしてください。",
         "updated_oidc": "OpenID Connect を更新しました",
         "Use discovered URL if empty": "データベース側の値が空の場合、\"Issuer Host\"から検出した値を利用します。"
       },
       "how_to": {
         "google": "Google OAuth の設定方法",
         "github": "GitHub OAuth の設定方法",
-        "twitter": "Twitter OAuth の設定方法"
+        "oidc": "OIDC の設定方法"
       }
     },
     "form_item_name": {
@@ -311,7 +300,6 @@
     "system_information": "システム情報",
     "wiki_administrator": "この画面はWiki管理者のみがアクセスできる画面です。",
     "assign_administrator": "「ユーザー管理」から「管理者にする」ボタンを使ってユーザーをWiki管理者に任命することができます。",
-    "list_of_installed_plugins": "インストールされているプラグイン一覧",
     "package_name": "パッケージ名",
     "specified_version": "指定バージョン",
     "installed_version": "インストールされているバージョン",
@@ -507,8 +495,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - この Wiki のサイト名",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - 現在表示中のページ名",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - 現在表示中のページパス",
-    "custom_header": "カスタム HTML Header",
-    "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
+    "custom_noscript": "カスタム Noscript",
+    "custom_noscript_detail": "システム全体に適用される HTML を記述できます。<code>&lt;body&gt;</code> タグ内の最初の <code>&lt;noscript&gt;</code> タグ内に展開されます。<br>変更の反映はページの更新が必要です。",
     "custom_css": "カスタム CSS",
     "write_css": " システム全体に適用されるCSSを記述できます。",
     "ctrl_space": "Ctrl+Space でコード補完",
@@ -522,25 +510,6 @@
     "upload_new_logo": "新しいロゴをアップロードする",
     "delete_logo": "ロゴを削除"
   },
-  "export_management": {
-    "export_archive_data": "データアーカイブ",
-    "exporting_collection_list": "エクスポート中のコレクション",
-    "exported_data_list": "エクスポートされたアーカイブリスト",
-    "export_collections": "コレクションのエクスポート",
-    "check_all": "全てにチェックを付ける",
-    "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
-    "create_new_archive_data": "アーカイブデータの新規作成",
-    "export": "エクスポート",
-    "cancel": "キャンセル",
-    "file": "ファイル名",
-    "growi_version": "GROWI バージョン",
-    "collections": "コレクション",
-    "exported_at": "エクスポートされた時間",
-    "export_menu": "エクスポートメニュー",
-    "download": "ダウンロード",
-    "delete": "削除"
-  },
   "importer_management": {
     "import_data": "データインポート",
     "article": "記事",
@@ -618,6 +587,25 @@
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
+  "export_management": {
+    "export_archive_data": "データアーカイブ",
+    "exporting_collection_list": "エクスポート中のコレクション",
+    "exported_data_list": "エクスポートされたアーカイブリスト",
+    "export_collections": "コレクションのエクスポート",
+    "check_all": "全てにチェックを付ける",
+    "uncheck_all": "全てからチェックを外す",
+    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
+    "create_new_archive_data": "アーカイブデータの新規作成",
+    "export": "エクスポート",
+    "cancel": "キャンセル",
+    "file": "ファイル名",
+    "growi_version": "GROWI バージョン",
+    "collections": "コレクション",
+    "exported_at": "エクスポートされた時間",
+    "export_menu": "エクスポートメニュー",
+    "download": "ダウンロード",
+    "delete": "削除"
+  },
   "external_notification": {
     "external_notification": "外部ツールへの通知",
     "enabled": "有効",
@@ -890,10 +878,8 @@
     "USER_LOGIN_WITH_LDAP": "LDAP 認証でログイン",
     "USER_LOGIN_WITH_GOOGLE": "Google 認証でログイン",
     "USER_LOGIN_WITH_GITHUB": "GitHub 認証でログイン",
-    "USER_LOGIN_WITH_TWITTER": "Twitter 認証でログイン",
     "USER_LOGIN_WITH_OIDC": "OIDC 認証でログイン",
     "USER_LOGIN_WITH_SAML": "SAML 認証でログイン",
-    "USER_LOGIN_WITH_BASIC": "BASIC 認証でログイン",
     "USER_LOGIN_FAILURE": "ログイン失敗",
     "USER_LOGOUT": "ログアウト",
     "USER_FOGOT_PASSWORD": "パスワードリセットのリクエスト",
@@ -971,18 +957,12 @@
     "ADMIN_AUTH_OIDC_ENABLED": "OIDC 認証を有効",
     "ADMIN_AUTH_OIDC_DISABLED": "OIDC 認証を無効",
     "ADMIN_AUTH_OIDC_UPDATE": "OIDC 認証設定の更新",
-    "ADMIN_AUTH_BASIC_ENABLED": "BASIC 認証の有効",
-    "ADMIN_AUTH_BASIC_DISABLED": "BASIC 認証の無効",
-    "ADMIN_AUTH_BASIC_UPDATE": "BASIC 認証設定の更新",
     "ADMIN_AUTH_GOOGLE_ENABLED": "Google 認証の有効",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Google 認証の無効",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Google 認証設定の更新",
     "ADMIN_AUTH_GITHUB_ENABLED": "GitHub 認証の有効",
     "ADMIN_AUTH_GITHUB_DISABLED": "GitHub 認証の無効",
     "ADMIN_AUTH_GITHUB_UPDATE": "GitHub 認証設定の更新",
-    "ADMIN_AUTH_TWITTER_ENABLED": "Twitter 認証の有効",
-    "ADMIN_AUTH_TWITTER_DISABLED": "Twitter 認証の無効",
-    "ADMIN_AUTH_TWITTER_UPDATE": "Twitter 認証設定の更新",
     "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "Line Break 設定の更新",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "インデント設定の更新",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "プレゼンテーション設定の更新",
@@ -993,7 +973,7 @@
     "ADMIN_FUNCTION_UPDATE": "機能設定の更新",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "コードハイライト設定の更新",
     "ADMIN_CUSTOM_TITLE_UPDATE": "カスタムタイトル設定の更新",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "カスタム HTML Header 設定の更新",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "カスタム noscript 設定の更新",
     "ADMIN_CUSTOM_CSS_UPDATE": "カスタム CSS 設定の更新",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "カスタムスクリプト設定の更新",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "アーカイブデータのアップロード",

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

@@ -583,6 +583,7 @@
     }
   },
   "private_legacy_pages": {
+    "title": "旧形式のプライベートページ",
     "bulk_operation": "一括操作",
     "convert_all_selected_pages": "新しい v5 互換形式に一括変換",
     "input_path_to_convert": "パスを入力して変換",
@@ -617,8 +618,8 @@
     }
   },
   "login": {
-    "Sign in error": "ログインエラー",
-    "Registration successful": "登録が完了しました。管理者の承認をお待ちください。",
+    "sign_in_error": "ログインエラー",
+    "registration_successful": "登録が完了しました。管理者の承認をお待ちください。",
     "Setup": "セットアップ",
     "enabled_ldap_has_configuration_problem":"LDAPは有効ですが、設定に問題があります。",
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
@@ -744,7 +745,6 @@
     "logout": "ログアウト"
   },
   "pagetree": {
-    "private_legacy_pages": "旧形式のプライベートページ",
     "cannot_rename_a_title_that_contains_slash": "`/` が含まれているタイトルにリネームできません",
     "you_cannot_move_this_page_now": "現在、このページを移動することはできません",
     "something_went_wrong_with_moving_page": "ページの移動に問題が発生しました"

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

@@ -7,6 +7,7 @@
   "User": "用户",
   "Name": "姓名",
   "Created": "创建",
+  "Page": "页面",
   "Edit": "编辑",
   "Description": "描述",
   "last_login": "上次登录",
@@ -79,7 +80,7 @@
 		"client_secret": "客户机密",
 		"updated_general_security_setting": "更新安全设置成功",
 		"setup_not_completed_yet": "安装尚未完成",
-		"guest_mode": {
+    "guest_mode": {
 			"deny": "拒绝(仅限注册用户)",
 			"readonly": "接受(来宾可以只读)"
 		},
@@ -164,13 +165,6 @@
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
 		},
-		"Basic": {
-			"enable_basic": "Enable Basic",
-			"name": "Basic Authentication",
-			"desc_1": "Login with <code>username</code> in Authorization header.",
-			"desc_2": "User will be automatically generated if not exist.",
-			"updated_basic": "Succeeded to update Basic setting"
-		},
 		"OAuth": {
 			"enable_oidc": "Enable OIDC",
 			"register": "Register for %s",
@@ -188,16 +182,6 @@
 			"Facebook": {
 				"name": "Facebook OAuth"
 			},
-			"Twitter": {
-				"enable_twitter": "Enable Twitter OAuth",
-				"name": "Twitter OAuth",
-				"register_1": "Access {{link}}",
-				"register_2": "Sign in Twitter",
-				"register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-				"register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
-				"register_5": "Copy and paste your ClientID and Client Secret above",
-				"updated_twitter": "Succeeded to update Twitter OAuth setting"
-			},
 			"GitHub": {
 				"enable_github": "Enable GitHub OAuth",
 				"name": "GitHub OAuth",
@@ -212,7 +196,7 @@
 				"username_detail": "Specification of mappings for <code>username</code> when creating new users",
 				"name_detail": "Specification of mappings for <code>name</code> when creating new users",
 				"mapping_detail": "Specification of mappings for %s when creating new users",
-				"register_1": "Contant to OIDC IdP Administrator",
+				"register_1": "Contact to OIDC IdP Administrator",
 				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
 				"register_3": "Copy and paste your ClientID and Client Secret above",
 				"updated_oidc": "Succeeded to update OpenID Connect",
@@ -221,7 +205,6 @@
 			"how_to": {
 				"google": "How to configure Google OAuth?",
 				"github": "How to configure GitHub OAuth?",
-				"twitter": "How to configure Twitter OAuth?",
 				"oidc": "How to configure OIDC?"
 			}
 		},
@@ -238,6 +221,7 @@
 		}
   },
   "notification_settings": {
+    "notification_settings": "通知设置",
 		"slack_incoming_configuration": "Slack Incoming Webhooks configuration",
 		"prioritize_webhook": "Prioritize incoming webhook than Slack App",
 		"prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
@@ -316,7 +300,6 @@
     "system_information": "系统信息",
     "wiki_administrator": "只有wiki管理员可以访问此页",
     "assign_administrator": "您可以使用“授予管理员访问权限”按钮在“用户管理”页上将所选用户指定为wiki管理员",
-    "list_of_installed_plugins": "已安装插件列表",
     "package_name": "包名称",
     "specified_version": "指定版本",
     "installed_version": "已安装版本",
@@ -400,9 +383,10 @@
     "aws_label": "AWS(S3)",
     "local_label": "Local",
     "gridfs_label": "MongoDB(GridFS)",
+    "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
+    "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "ses_settings": "SES设置",
     "test_connection": "测试邮件服务器连接",
-    "": "如果您没有SMTP设置,电子邮件将通过SES发送。您需要从电子邮件地址和生产设置进行验证。",
     "change_setting": "注意:如果你更改此设置未完成,您将无法访问迄今为止上传的文件。",
     "region": "Region",
     "bucket_name": "Bucket name",
@@ -475,17 +459,6 @@
       "expanded": "内容宽度100% "
     },
     "theme": "主体",
-    "behavior": "行为",
-    "behavior_desc": {
-      "growi_text1": "<code>/page</code> and <code>/page/</code> 都显示同一页。",
-      "growi_text2": "<code>/nonexistent_page</code> 显示编辑表单",
-      "growi_text3": "如果使用GROWI增强布局,则所有页面都显示子页面列表",
-      "crowi_text1": "<code>/page</code> 显示页面",
-      "crowi_text2": "<code>/page/</code> 显示子页列表",
-      "crowi_text3": "如果portal应用于<code>/page/</code>,则会显示portal和子页面列表",
-      "crowi_text4": "<code>/nonexistent_page</code> 显示编辑表单<",
-      "crowi_text5": "<code>/nonexistent_page/</code> 子页列表"
-    },
     "theme_desc": {
       "light_and_dark": "明暗模式",
       "unique": "只有一种模式"
@@ -522,8 +495,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;站点名称&#125;&#125;</code>-此wiki的站点名称。",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;页名&#125;&#125;</code>-当前页的页名。",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;页面路径&#125;&#125;</code>-当前页面的页面路径。",
-    "custom_header": "自定义HTML标题",
-    "custom_header_detail": "您可以自定义应用所有页面的HTML标题。您的自定义脚本将插入<code>&lt;header&gt;</code>中,但位于其他<code>&lt;script&gt;</code>标记之上。<br>重新链接页面以查看更改。",
+    "custom_noscript": "自定义 Noscript 标题",
+    "custom_noscript_detail": "您可以自定义应用所有页面的 Noscript 代码。 您的自定义 Noscript 将被插入到作为 body 的第一个元素的 <code>&lt;noscript&gt;</code> 标签中。<br>重新链接页面以查看更改。",
     "custom_css": "自定义CSS",
     "write_css": "您可以编写应用于整个系统的CSS。",
     "ctrl_space": "Ctrl+Space 自动完成",
@@ -905,10 +878,8 @@
     "USER_LOGIN_WITH_LDAP": "使用 LDAP 登录",
     "USER_LOGIN_WITH_GOOGLE": "用谷歌登录",
     "USER_LOGIN_WITH_GITHUB": "使用 GitHub 登录",
-    "USER_LOGIN_WITH_TWITTER": "使用 Twitter 登录",
     "USER_LOGIN_WITH_OIDC": "使用 OIDC 登录",
     "USER_LOGIN_WITH_SAML": "使用 SAML 登录",
-    "USER_LOGIN_WITH_BASIC": "使用 BASIC 登录",
     "USER_LOGIN_FAILURE": "登录失败",
     "USER_LOGOUT": "注销",
     "USER_FOGOT_PASSWORD": "要求重置密码",
@@ -986,18 +957,12 @@
     "ADMIN_AUTH_OIDC_ENABLED": "启用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_DISABLED": "禁用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_UPDATE": "更新 OIDC 设置",
-    "ADMIN_AUTH_BASIC_ENABLED": "启用基本身份验证",
-    "ADMIN_AUTH_BASIC_DISABLED": "禁用基本身份验证",
-    "ADMIN_AUTH_BASIC_UPDATE": "更新基本认证设置",
     "ADMIN_AUTH_GOOGLE_ENABLED": "启用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_DISABLED": "禁用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_UPDATE": "更新谷歌授权设置",
     "ADMIN_AUTH_GITHUB_ENABLED": "启用 GitHub 身份验证",
     "ADMIN_AUTH_GITHUB_DISABLED": "禁用 GitHub 身份验证",
     "ADMIN_AUTH_GITHUB_UPDATE": "更新 GitHub 授权设置",
-    "ADMIN_AUTH_TWITTER_ENABLED": "启用 Twitter 身份验证",
-    "ADMIN_AUTH_TWITTER_DISABLED": "禁用 Twitter 身份验证",
-    "ADMIN_AUTH_TWITTER_UPDATE": "更新 Twitter 授权设置",
     "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "更新链接中断设置",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "更新缩进设置",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "更新演示设置",
@@ -1008,7 +973,7 @@
     "ADMIN_FUNCTION_UPDATE": "更新函数",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "更新代码高亮",
     "ADMIN_CUSTOM_TITLE_UPDATE": "更新自定义标题",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "更新自定义 HTML 标头",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "更新自定义 noscript 标头",
     "ADMIN_CUSTOM_CSS_UPDATE": "更新自定义 CSS",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "更新自定义脚本",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "上传存档数据",

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

@@ -117,12 +117,10 @@
   "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> ",
 	"Markdown Settings": "Markdown设置",
-	"Notification Settings": "通知设置",
 	"external_account_management": "外部账户管理",
   "UserGroup": "用户组",
   "ChildUserGroup": "儿童用户组",
 	"Basic Settings": "基础设置",
-	"Basic authentication": "基本身份验证",
 	"The contents entered here will be shown in the header etc": "此处输入的内容将显示在标题等中",
 	"Public": "公共",
 	"Anyone with the link": "任何人",
@@ -591,6 +589,7 @@
     }
 	},
   "private_legacy_pages": {
+    "title": "私人遗留页面",
     "bulk_operation": "批量操作",
     "convert_all_selected_pages": "全部转换为新的v5兼容格式",
 		"input_path_to_convert": "输入一个转换页面的路径",
@@ -625,8 +624,8 @@
     }
   },
 	"login": {
-		"Sign in error": "登录错误",
-		"Registration successful": "注册成功。请等待管理员批准",
+		"sign_in_error": "登录错误",
+		"registration_successful": "注册成功。请等待管理员批准",
 		"Setup": "安装程序",
     "enabled_ldap_has_configuration_problem":"启用了LDAP,但配置有问题。",
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
@@ -752,7 +751,6 @@
     "logout": "登出"
   },
   "pagetree": {
-    "private_legacy_pages": "私人遗留页面",
     "cannot_rename_a_title_that_contains_slash": "不能重命名包含 ’/' 的标题",
     "you_cannot_move_this_page_now": "你现在不能移动这个页面",
     "something_went_wrong_with_moving_page": "移动页面时出了问题"

+ 0 - 77
packages/app/src/client/services/AdminBasicSecurityContainer.js

@@ -1,77 +0,0 @@
-import { isServer } from '@growi/core';
-import { Container } from 'unstated';
-
-import loggerFactory from '~/utils/logger';
-import { removeNullPropertyFromObject } from '~/utils/object-utils';
-
-import { apiv3Get, apiv3Put } from '../util/apiv3-client';
-
-const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
-
-/**
- * Service container for admin security page (BasicSecuritySetting.jsx)
- * @extends {Container} unstated Container
- */
-export default class AdminBasicSecurityContainer extends Container {
-
-  constructor() {
-    super();
-
-    if (isServer()) {
-      return;
-    }
-
-    this.state = {
-      isSameUsernameTreatedAsIdenticalUser: false,
-    };
-  }
-
-  /**
-   * retrieve security data
-   */
-  async retrieveSecurityData() {
-    try {
-      const response = await apiv3Get('/security-setting/');
-      const { basicAuth } = response.data.securityParams;
-      this.setState({
-        isSameUsernameTreatedAsIdenticalUser: basicAuth.isSameUsernameTreatedAsIdenticalUser,
-      });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to fetch data');
-    }
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AdminBasicSecurityContainer';
-  }
-
-  /**
-   * Switch isSameUsernameTreatedAsIdenticalUser
-   */
-  switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
-  }
-
-  /**
-   * Update basicSetting
-   */
-  async updateBasicSetting() {
-    let requestParams = { isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser };
-
-    requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await apiv3Put('/security-setting/basic', requestParams);
-    const { securitySettingParams } = response.data;
-
-    this.setState({
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
-    });
-    return response;
-  }
-
-}

+ 8 - 12
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -36,7 +36,7 @@ export default class AdminCustomizeContainer extends Container {
       isAllReplyShown: false,
       isSearchScopeChildrenAsDefault: false,
       currentCustomizeTitle: '',
-      currentCustomizeHeader: '',
+      currentCustomizeNoscript: '',
       currentCustomizeCss: '',
       currentCustomizeScript: '',
     };
@@ -73,7 +73,7 @@ export default class AdminCustomizeContainer extends Container {
         isAllReplyShown: customizeParams.isAllReplyShown,
         isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
         currentCustomizeTitle: customizeParams.customizeTitle,
-        currentCustomizeHeader: customizeParams.customizeHeader,
+        currentCustomizeNoscript: customizeParams.customizeNoscript,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeScript: customizeParams.customizeScript,
       });
@@ -160,8 +160,8 @@ export default class AdminCustomizeContainer extends Container {
   /**
    * Change customize Html header
    */
-  changeCustomizeHeader(inputValue) {
-    this.setState({ currentCustomizeHeader: inputValue });
+  changeCustomizeNoscript(inputValue) {
+    this.setState({ currentCustomizeNoscript: inputValue });
   }
 
   /**
@@ -235,18 +235,14 @@ export default class AdminCustomizeContainer extends Container {
     }
   }
 
-  /**
-   * Update customHeader
-   * @memberOf AdminCustomizeContainer
-   */
-  async updateCustomizeHeader() {
+  async updateCustomizeNoscript() {
     try {
-      const response = await apiv3Put('/customize-setting/customize-header', {
-        customizeHeader: this.state.currentCustomizeHeader,
+      const response = await apiv3Put('/customize-setting/customize-noscript', {
+        customizeNoscript: this.state.currentCustomizeNoscript,
       });
       const { customizedParams } = response.data;
       this.setState({
-        currentCustomizeHeader: customizedParams.customizeHeader,
+        currentCustomizeNoscript: customizedParams.customizeNoscript,
       });
     }
     catch (err) {

+ 0 - 18
packages/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -42,10 +42,8 @@ export default class AdminGeneralSecurityContainer extends Container {
       isLdapEnabled: false,
       isSamlEnabled: false,
       isOidcEnabled: false,
-      isBasicEnabled: false,
       isGoogleEnabled: false,
       isGitHubEnabled: false,
-      isTwitterEnabled: false,
       setupStrategies: [],
       disableLinkSharing: false,
       shareLinks: [],
@@ -82,10 +80,8 @@ export default class AdminGeneralSecurityContainer extends Container {
       isLdapEnabled: generalAuth.isLdapEnabled,
       isSamlEnabled: generalAuth.isSamlEnabled,
       isOidcEnabled: generalAuth.isOidcEnabled,
-      isBasicEnabled: generalAuth.isBasicEnabled,
       isGoogleEnabled: generalAuth.isGoogleEnabled,
       isGitHubEnabled: generalAuth.isGitHubEnabled,
-      isTwitterEnabled: generalAuth.isTwitterEnabled,
     });
   }
 
@@ -318,13 +314,6 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.switchAuthentication('isOidcEnabled', 'oidc');
   }
 
-  /**
-   * Switch Basic enabled
-   */
-  async switchIsBasicEnabled() {
-    this.switchAuthentication('isBasicEnabled', 'basic');
-  }
-
   /**
    * Switch GoogleOAuth enabled
    */
@@ -339,11 +328,4 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.switchAuthentication('isGitHubEnabled', 'github');
   }
 
-  /**
-   * Switch TwitterOAuth enabled
-   */
-  async switchIsTwitterOAuthEnabled() {
-    this.switchAuthentication('isTwitterEnabled', 'twitter');
-  }
-
 }

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

+ 14 - 11
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -27,9 +27,9 @@ export default class AdminMarkDownContainer extends Container {
       pageBreakSeparator: 1,
       pageBreakCustomSeparator: '',
       isEnabledXss: false,
-      xssOption: 1,
+      xssOption: '',
       tagWhiteList: '',
-      attrWhiteList: '',
+      attrWhiteList: '{}',
     };
 
     this.switchEnableXss = this.switchEnableXss.bind(this);
@@ -86,9 +86,6 @@ export default class AdminMarkDownContainer extends Container {
    * Switch enableXss
    */
   switchEnableXss() {
-    if (this.state.isEnabledXss) {
-      this.setState({ xssOption: null });
-    }
     this.setState({ isEnabledXss: !this.state.isEnabledXss });
   }
 
@@ -122,19 +119,25 @@ export default class AdminMarkDownContainer extends Container {
    * Update Xss Setting
    */
   async updateXssSetting() {
-    let { tagWhiteList, attrWhiteList } = this.state;
+    let { tagWhiteList } = this.state;
+    const { attrWhiteList } = this.state;
 
     tagWhiteList = Array.isArray(tagWhiteList) ? tagWhiteList : tagWhiteList.split(',');
-    attrWhiteList = Array.isArray(attrWhiteList) ? attrWhiteList : attrWhiteList.split(',');
 
-    const response = await apiv3Put('/markdown-setting/xss', {
+    try {
+      // Check if parsing is possible
+      JSON.parse(attrWhiteList);
+    }
+    catch (err) {
+      throw Error(err);
+    }
+
+    await apiv3Put('/markdown-setting/xss', {
       isEnabledXss: this.state.isEnabledXss,
       xssOption: this.state.xssOption,
       tagWhiteList,
-      attrWhiteList,
+      attrWhiteList: attrWhiteList ?? '{}',
     });
-
-    return response;
   }
 
   /**

+ 0 - 104
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -1,104 +0,0 @@
-import { isServer, pathUtils } from '@growi/core';
-import { Container } from 'unstated';
-import urljoin from 'url-join';
-
-import loggerFactory from '~/utils/logger';
-import { removeNullPropertyFromObject } from '~/utils/object-utils';
-
-import { apiv3Get, apiv3Put } from '../util/apiv3-client';
-
-const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
-
-/**
- * Service container for admin security page (TwitterSecurityManagement.jsx)
- * @extends {Container} unstated Container
- */
-export default class AdminTwitterSecurityContainer extends Container {
-
-  constructor(appContainer) {
-    super();
-
-    if (isServer()) {
-      return;
-    }
-
-    this.appContainer = appContainer;
-
-    this.state = {
-      twitterConsumerKey: '',
-      twitterConsumerSecret: '',
-      isSameUsernameTreatedAsIdenticalUser: false,
-    };
-
-    this.updateTwitterSetting = this.updateTwitterSetting.bind(this);
-  }
-
-  /**
-   * retrieve security data
-   */
-  async retrieveSecurityData() {
-    try {
-      const response = await apiv3Get('/security-setting/');
-      const { twitterOAuth } = response.data.securityParams;
-      this.setState({
-        twitterConsumerKey: twitterOAuth.twitterConsumerKey,
-        twitterConsumerSecret: twitterOAuth.twitterConsumerSecret,
-        isSameUsernameTreatedAsIdenticalUser: twitterOAuth.isSameUsernameTreatedAsIdenticalUser,
-      });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to fetch data');
-    }
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AdminTwitterSecurityContainer';
-  }
-
-  /**
-   * Change twitterConsumerKey
-   */
-  changeTwitterConsumerKey(value) {
-    this.setState({ twitterConsumerKey: value });
-  }
-
-  /**
-   * Change twitterConsumerSecret
-   */
-  changeTwitterConsumerSecret(value) {
-    this.setState({ twitterConsumerSecret: value });
-  }
-
-  /**
-   * Switch isSameUsernameTreatedAsIdenticalUser
-   */
-  switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
-  }
-
-  /**
-   * Update twitterSetting
-   */
-  async updateTwitterSetting() {
-    const { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser } = this.state;
-
-    let requestParams = { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser };
-
-    requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await apiv3Put('/security-setting/twitter-oauth', requestParams);
-    const { securitySettingParams } = response.data;
-
-    this.setState({
-      twitterConsumerKey: securitySettingParams.twitterConsumerKey,
-      twitterConsumerSecret: securitySettingParams.twitterConsumerSecret,
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
-    });
-    return response;
-  }
-
-}

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

@@ -0,0 +1,34 @@
+import { initializeGrowiFacade } from '~/utils/growi-facade';
+import loggerFactory from '~/utils/logger';
+
+
+declare global {
+  // eslint-disable-next-line vars-on-top, no-var
+  var pluginActivators: {
+    [key: string]: {
+      activate: () => void,
+      deactivate: () => void,
+    },
+  };
+}
+
+const logger = loggerFactory('growi:cli:ActivatePluginService');
+
+
+export class ActivatePluginService {
+
+  static activateAll(): void {
+    initializeGrowiFacade();
+
+    const { pluginActivators } = window;
+
+    if (pluginActivators == null) {
+      return;
+    }
+
+    Object.entries(pluginActivators).forEach(([, activator]) => {
+      activator.activate();
+    });
+  }
+
+}

+ 32 - 0
packages/app/src/client/services/layout.ts

@@ -0,0 +1,32 @@
+import { useIsContainerFluid, useShareLinkId } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useEditorMode } from '~/stores/ui';
+
+export const useEditorModeClassName = (): string => {
+  const { getClassNamesByEditorMode } = useEditorMode();
+
+  // TODO: Enable `editing-sidebar` class somehow
+  // https://redmine.weseek.co.jp/issues/111527
+  // const classNames: string[] = [];
+  // if (currentPage != null) {
+  //   const isSidebar = currentPage.path === '/Sidebar';
+  //   classNames.push(...getClassNamesByEditorMode(/* isSidebar */));
+  // }
+
+  return `${getClassNamesByEditorMode().join(' ') ?? ''}`;
+};
+
+export const useCurrentGrowiLayoutFluidClassName = (): string => {
+  const { data: shareLinkId } = useShareLinkId();
+  const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+
+  const { data: dataIsContainerFluid } = useIsContainerFluid();
+
+  const isContainerFluidEachPage = currentPage == null || !('expandContentWidth' in currentPage)
+    ? null
+    : currentPage.expandContentWidth;
+  const isContainerFluidDefault = dataIsContainerFluid;
+  const isContainerFluid = isContainerFluidEachPage ?? isContainerFluidDefault;
+
+  return isContainerFluid ? 'growi-layout-fluid' : '';
+};

+ 43 - 1
packages/app/src/client/services/page-operation.ts

@@ -2,7 +2,10 @@ import { SubscriptionStatusType, Nullable } from '@growi/core';
 import urljoin from 'url-join';
 
 import { OptionsToSave } from '~/interfaces/page-operation';
-import { useIsEnabledUnsavedWarning } from '~/stores/editor';
+import { useCurrentPageId } from '~/stores/context';
+import { useEditingMarkdown, useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
+import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 
 import { toastError } from '../util/apiNotification';
@@ -171,3 +174,42 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
     return res;
   };
 };
+
+export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => Promise<void>) | undefined => {
+  const { mutate: mutateCurrentPageId } = useCurrentPageId();
+  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+  const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
+  const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
+  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+
+  if (pageId == null) { return }
+
+  // update swr 'currentPageId', 'currentPage', remote states
+  return async() => {
+    await mutateCurrentPageId(pageId);
+    const updatedPage = await mutateCurrentPage();
+
+    await mutateTagsInfo(); // get from DB
+    syncTagsInfoForEditor(); // sync global state for client
+
+    if (updatedPage == null) { return }
+
+    mutateEditingMarkdown(updatedPage.revision.body);
+
+    const remoterevisionData = {
+      remoteRevisionId: updatedPage.revision._id,
+      remoteRevisionBody: updatedPage.revision.body,
+      remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
+      remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
+      revisionIdHackmdSynced: updatedPage.revisionHackmdSynced?.toString(),
+      hasDraftOnHackmd: updatedPage.hasDraftOnHackmd,
+    };
+
+    setRemoteLatestPageData(remoterevisionData);
+  };
+};
+
+export const unlink = async(path: string): Promise<void> => {
+  await apiPost('/pages.unlink', { path });
+};

+ 6 - 52
packages/app/src/client/util/apiNotification.js

@@ -1,53 +1,7 @@
-// show API error/sucess toastr
+import { legacy } from './toastr';
 
-import * as toastr from 'toastr';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-const toastrOption = {
-  error: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '0',
-  },
-  success: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '3000',
-  },
-  warning: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '6000',
-  },
-};
-
-// accepts both a single error and an array of errors
-export const toastError = (err, header = 'Error', option = toastrOption.error) => {
-  const errs = toArrayIfNot(err);
-
-  if (err.length === 0) {
-    toastr.error('', header);
-  }
-
-  for (const err of errs) {
-    toastr.error(err.message || err, header, option);
-  }
-};
-
-// only accepts a single item
-export const toastSuccess = (body, header = 'Success', option = toastrOption.success) => {
-  toastr.success(body, header, option);
-};
-
-export const toastWarning = (body, header = 'Warning', option = toastrOption.warning) => {
-  toastr.warning(body, header, option);
-};
+// DEPRECATED -- 2022.12.07 Yuki Takei
+// Use methods from './toastr.ts' instead
+export const toastError = legacy.toastError;
+export const toastSuccess = legacy.toastSuccess;
+export const toastWarning = legacy.toastWarning;

+ 91 - 0
packages/app/src/client/util/toastr.ts

@@ -0,0 +1,91 @@
+import { toast, ToastContent, ToastOptions } from 'react-toastify';
+import * as toastrLegacy from 'toastr';
+
+import { toArrayIfNot } from '~/utils/array-utils';
+
+
+export const toastErrorOption: ToastOptions = {
+  autoClose: false,
+  closeButton: true,
+};
+export const toastError = (err: string | Error | Error[], option: ToastOptions = toastErrorOption): void => {
+  const errs = toArrayIfNot(err);
+
+  if (errs.length === 0) {
+    return;
+  }
+
+  for (const err of errs) {
+    const message = (typeof err === 'string') ? err : err.message;
+    toast.error(message || err, option);
+  }
+};
+
+export const toastSuccessOption: ToastOptions = {
+  autoClose: 2000,
+  closeButton: true,
+};
+export const toastSuccess = (content: ToastContent, option: ToastOptions = toastSuccessOption): void => {
+  toast.success(content, option);
+};
+
+export const toastWarningOption: ToastOptions = {
+  autoClose: 5000,
+  closeButton: true,
+};
+export const toastWarning = (content: ToastContent, option: ToastOptions = toastWarningOption): void => {
+  toastrLegacy.warning(content, option);
+};
+
+
+const toastrLegacyOption = {
+  error: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '0',
+  },
+  success: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '3000',
+  },
+  warning: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '6000',
+  },
+};
+
+export const legacy = {
+  // accepts both a single error and an array of errors
+  toastError: (err: string | Error | Error[], header = 'Error', option = toastrLegacyOption.error): void => {
+    const errs = toArrayIfNot(err);
+
+    if (errs.length === 0) {
+      toastrLegacy.error('', header);
+    }
+
+    for (const err of errs) {
+      const message = (typeof err === 'string') ? err : err.message;
+      toastrLegacy.error(message || err, header, option);
+    }
+  },
+
+  // only accepts a single item
+  toastSuccess: (body: string, header = 'Success', option = toastrLegacyOption.success): void => {
+    toastrLegacy.success(body, header, option);
+  },
+
+  toastWarning: (body: string, header = 'Warning', option = toastrLegacyOption.warning): void => {
+    toastrLegacy.warning(body, header, option);
+  },
+};

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

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

@@ -73,7 +73,7 @@ const AppSettingsPageContents = (props: Props) => {
           && (
             <div className="row">
               <div className="col-lg-12">
-                <h2 className="admin-setting-header">{t('V5 Page Migration')}</h2>
+                <h2 className="admin-setting-header" data-testid="v5-page-migration">{t('V5 Page Migration')}</h2>
                 <V5PageMigration />
               </div>
             </div>

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

@@ -18,19 +18,20 @@ type Props = {
 
 const SiteUrlSetting = (props: Props) => {
   const { t } = useTranslation('admin', { keyPrefix: 'app_setting' });
+  const { t: tCommon } = useTranslation('commons');
   const { adminAppContainer } = props;
 
 
   const submitHandler = useCallback(async() => {
     try {
       await adminAppContainer.updateSiteUrlSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('site_url.title') }));
+      toastSuccess(tCommon('toaster.update_successed', { target: t('site_url.title') }));
     }
     catch (err) {
       toastError(err);
       logger.error(err);
     }
-  }, [adminAppContainer, t]);
+  }, [adminAppContainer, t, tCommon]);
 
   return (
     <React.Fragment>

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

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

@@ -35,9 +35,10 @@ const AdminNavigation = (props) => {
       case 'users':                    return <><i className="mr-1 icon-fw icon-user"></i>{            t('user_management.user_management') }</>;
       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 'data-transfer':            return <><i className="mr-1 icon-fw icon-arrow-right"></i>{          t('g2g_data_transfer.data_transfer') }</>;
-      case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings') } </>;
+      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'}</>;
+      case 'data-transfer':            return <><i className="mr-1 icon-fw icon-arrow-right"></i>{     t('g2g_data_transfer.data_transfer')}</>;
+      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 */
     }
@@ -92,6 +93,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="data-transfer" isListGroupItems isActive={isActiveMenu('/data-transfer')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         {growiCloudUri != null && growiAppIdForGrowiCloud != null
@@ -142,6 +144,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" />}
             {isActiveMenu('/data-transfer') &&     <MenuLabel menu="data-transfer" />}
             {/* eslint-enable no-multi-spaces */}
           </span>

+ 8 - 8
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -12,9 +12,9 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
-import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLogoSetting from './CustomizeLogoSetting';
+import CustomizeNoscriptSetting from './CustomizeNoscriptSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
@@ -44,10 +44,13 @@ function Customize(props) {
   return (
     <div data-testid="admin-customize">
       <div className="mb-5">
-        <CustomizeLayoutSetting />
+        <CustomizeThemeSetting />
       </div>
       <div className="mb-5">
-        <CustomizeThemeSetting />
+        <CustomizeLogoSetting />
+      </div>
+      <div className="mb-5">
+        <CustomizeLayoutSetting />
       </div>
       <div className="mb-5">
         <CustomizeSidebarSetting />
@@ -59,16 +62,13 @@ function Customize(props) {
         <CustomizeTitle />
       </div>
       <div className="mb-5">
-        <CustomizeHeaderSetting />
+        <CustomizeScriptSetting />
       </div>
       <div className="mb-5">
         <CustomizeCssSetting />
       </div>
       <div className="mb-5">
-        <CustomizeScriptSetting />
-      </div>
-      <div className="mb-5">
-        <CustomizeLogoSetting />
+        <CustomizeNoscriptSetting />
       </div>
     </div>
   );

+ 4 - 1
packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -45,13 +45,16 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
             <textarea
               className="form-control"
               name="customizeCss"
-              value={adminCustomizeContainer.state.currentCustomizeCss || ''}
+              rows={8}
+              defaultValue={adminCustomizeContainer.state.currentCustomizeCss || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
             />
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
               {t('admin:customize_settings.ctrl_space')}
             </p>
+            */}
           </div>
 
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

+ 33 - 21
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx → packages/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -1,6 +1,8 @@
 import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
@@ -13,15 +15,15 @@ type Props = {
   adminCustomizeContainer: AdminCustomizeContainer
 }
 
-const CustomizeHeaderSetting = (props: Props): JSX.Element => {
+const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
 
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
   const onClickSubmit = useCallback(async() => {
     try {
-      await adminCustomizeContainer.updateCustomizeHeader();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_header'), ns: 'commons' }));
+      await adminCustomizeContainer.updateCustomizeNoscript();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_noscript'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
@@ -32,38 +34,48 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_header')}</h2>
+          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_noscript')}</h2>
 
           <Card className="card well my-3">
             <CardBody className="px-0 py-2">
               <span
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_header_detail') }}
+                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_noscript_detail') }}
               />
             </CardBody>
           </Card>
-          <div className="form-text text-muted">
-            { t('Example') }:
-            <pre>
-              {/* eslint-disable-next-line react/no-unescaped-entities */}
-              <code className="text-wrap">&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js"
-                defer&gt;&lt;/script&gt;
-              </code>
-            </pre>
-          </div>
 
           <div className="form-group">
             <textarea
               className="form-control"
-              name="customizeHeader"
-              value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
-              onChange={(e) => { adminCustomizeContainer.changeCustomizeHeader(e.target.value) }}
+              name="customizeNoscript"
+              rows={8}
+              defaultValue={adminCustomizeContainer.state.currentCustomizeNoscript || ''}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeNoscript(e.target.value) }}
             />
-            <p className="form-text text-muted text-right">
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
+            <span className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
               {t('admin:customize_settings.ctrl_space')}
-            </p>
+            </span>
+            */}
+          </div>
+
+          <a className="text-muted"
+            data-toggle="collapse" href="#collapseExampleHtml" role="button" aria-expanded="false" aria-controls="collapseExampleHtml">
+            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            Example for Google Tag Manager
+          </a>
+          <div className="collapse" id="collapseExampleHtml">
+            <PrismAsyncLight style={oneDark} language={'javascript'}
+            >
+              {`<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
+  height="0"
+  width="0"
+  style="display:none;visibility:hidden"></iframe>`}
+            </PrismAsyncLight>
           </div>
+
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
         </div>
       </div>
@@ -72,6 +84,6 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
 
 };
 
-const CustomizeHeaderSettingWrapper = withUnstatedContainers(CustomizeHeaderSetting, [AdminCustomizeContainer]);
+const CustomizeNoscriptSettingWrapper = withUnstatedContainers(CustomizeNoscriptSetting, [AdminCustomizeContainer]);
 
-export default CustomizeHeaderSettingWrapper;
+export default CustomizeNoscriptSettingWrapper;

+ 28 - 45
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -1,6 +1,8 @@
 import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
@@ -28,14 +30,6 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
     }
   }, [t, adminCustomizeContainer]);
 
-  const getExampleCode = useCallback(() => {
-    return `console.log($('.main-container'));
-    window.addEventListener('load', (event) => {
-      console.log('config: ', appContainer.config);
-    });
-    `;
-  }, []);
-
   return (
     <React.Fragment>
       <div className="row">
@@ -48,51 +42,40 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
             </CardBody>
           </Card>
 
-          <div className="form-text text-muted">
-            Placeholders:<br />
-            (Available after <code>load</code> event)
-          </div>
-          <table className="table table-borderless table-sm form-text text-muted offset-1 col-11">
-            <tbody>
-              <tr>
-                <th className="text-right"><code>$</code></th>
-                <td>jQuery instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>appContainer</code></th>
-                <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">unstated container</a></td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>growiRenderer</code></th>
-                <td>GROWI Renderer origin instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>growiPlugin</code></th>
-                <td>GROWI Plugin Manager instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>Crowi</code></th>
-                <td>Crowi legacy instance (jQuery based)</td>
-              </tr>
-            </tbody>
-          </table>
-
-          <div className="form-text text-muted">
-            Examples:
-            <pre><code className='language-javascript'>{getExampleCode()}</code></pre>
-          </div>
-
           <div className="form-group">
             <textarea
               className="form-control"
               name="customizeScript"
-              value={adminCustomizeContainer.state.currentCustomizeScript || ''}
+              rows={8}
+              defaultValue={adminCustomizeContainer.state.currentCustomizeScript || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
             />
-            <p className="form-text text-muted text-right">
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
+            <span className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
               {t('admin:customize_settings.ctrl_space')}
-            </p>
+            </span>
+            */}
+          </div>
+
+          <a className="text-muted"
+            data-toggle="collapse" href="#collapseExampleScript" role="button" aria-expanded="false" aria-controls="collapseExampleScript">
+            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            Example for Google Tag Manager
+          </a>
+          <div className="collapse" id="collapseExampleScript">
+            <PrismAsyncLight style={oneDark} language={'javascript'}
+            >
+              {`(function(w,d,s,l,i){
+w[l]=w[l]||[];
+w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
+var f=d.getElementsByTagName(s)[0],
+  j=d.createElement(s),
+  dl=l!='dataLayer'?'&l='+l:'';
+j.async=true;
+j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
+})(window,document,'script','dataLayer','GTM-XXXXXX');`}
+            </PrismAsyncLight>
           </div>
 
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

+ 0 - 105
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -1,105 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { GrowiThemes } from '~/interfaces/theme';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import ThemeColorBox from './ThemeColorBox';
-
-/* eslint-disable no-multi-spaces */
-const lightNDarkTheme = [{
-  name: GrowiThemes.DEFAULT,      bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
-}, {
-  name: GrowiThemes.MONO_BLUE,    bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
-}, {
-  name: GrowiThemes.HUFFLEPUFF,   bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
-}, {
-  name: GrowiThemes.FIRE_RED,     bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
-}, {
-  name: GrowiThemes.JADE_GREEN,   bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
-}];
-
-const uniqueTheme = [{
-  name: GrowiThemes.NATURE,       bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
-}, {
-  name: GrowiThemes.WOOD,         bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
-}, {
-  name: GrowiThemes.ISLAND,       bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
-}, {
-  name: GrowiThemes.CHRISTMAS,    bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
-}, {
-  name: GrowiThemes.ANTARCTIC,    bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
-}, {
-  name: GrowiThemes.SPRING,       bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
-}, {
-  name: GrowiThemes.FUTURE,       bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
-}, {
-  name: GrowiThemes.HALLOWEEN,    bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
-}, {
-  name: GrowiThemes.KIBELA,       bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
-}, {
-  name: GrowiThemes.BLACKBOARD,   bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
-}];
-
-
-const CustomizeThemeOptions = (props) => {
-
-  const { adminCustomizeContainer, currentTheme } = props;
-  const { currentLayout } = adminCustomizeContainer.state;
-
-  const { t } = useTranslation('admin');
-
-
-  return (
-    <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
-      {/* Light and Dark Themes */}
-      <div>
-        <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
-        <div className="d-flex flex-wrap">
-          {lightNDarkTheme.map((theme) => {
-            return (
-              <ThemeColorBox
-                key={theme.name}
-                isSelected={currentTheme === theme.name}
-                onSelected={() => props.onSelected(theme.name)}
-                {...theme}
-              />
-            );
-          })}
-        </div>
-      </div>
-      {/* Unique Theme */}
-      <div className="mt-3">
-        <h3>{t('customize_settings.theme_desc.unique')}</h3>
-        <div className="d-flex flex-wrap">
-          {uniqueTheme.map((theme) => {
-            return (
-              <ThemeColorBox
-                key={theme.name}
-                isSelected={currentTheme === theme.name}
-                onSelected={() => props.onSelected(theme.name)}
-                {...theme}
-              />
-            );
-          })}
-        </div>
-      </div>
-    </div>
-  );
-
-};
-
-const CustomizeThemeOptionsWrapper = withUnstatedContainers(CustomizeThemeOptions, [AdminCustomizeContainer]);
-
-CustomizeThemeOptions.propTypes = {
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-  onSelected: PropTypes.func,
-  currentTheme: PropTypes.string,
-};
-
-export default CustomizeThemeOptionsWrapper;

+ 67 - 0
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx

@@ -0,0 +1,67 @@
+import React, { useMemo } from 'react';
+
+import { GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+
+import { ThemeColorBox } from './ThemeColorBox';
+
+
+type Props = {
+  availableThemes: GrowiThemeMetadata[],
+  selectedTheme?: string,
+  onSelected?: (themeName: string) => void,
+};
+
+const CustomizeThemeOptions = (props: Props): JSX.Element => {
+  const { t } = useTranslation('admin');
+
+  const { availableThemes, selectedTheme, onSelected } = props;
+
+  const lightNDarkThemes = useMemo(() => {
+    return availableThemes.filter(s => s.schemeType === GrowiThemeSchemeType.BOTH);
+  }, [availableThemes]);
+  const oneModeThemes = useMemo(() => {
+    return availableThemes.filter(s => s.schemeType !== GrowiThemeSchemeType.BOTH);
+  }, [availableThemes]);
+
+  return (
+    <div id="themeOptions">
+      {/* Light and Dark Themes */}
+      <div>
+        <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
+        <div className="d-flex flex-wrap">
+          {lightNDarkThemes.map((theme) => {
+            return (
+              <ThemeColorBox
+                key={theme.name}
+                isSelected={selectedTheme != null && selectedTheme === theme.name}
+                metadata={theme}
+                onSelected={() => onSelected?.(theme.name)}
+              />
+            );
+          })}
+        </div>
+      </div>
+      {/* Only one mode Theme */}
+      <div className="mt-3">
+        <h3>{t('customize_settings.theme_desc.unique')}</h3>
+        <div className="d-flex flex-wrap">
+          {oneModeThemes.map((theme) => {
+            return (
+              <ThemeColorBox
+                key={theme.name}
+                isSelected={selectedTheme != null && selectedTheme === theme.name}
+                metadata={theme}
+                onSelected={() => onSelected?.(theme.name)}
+              />
+            );
+          })}
+        </div>
+      </div>
+    </div>
+  );
+
+};
+
+
+export default CustomizeThemeOptions;

+ 37 - 24
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -1,41 +1,46 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
+import { PresetThemes, PresetThemesMetadatas } from '@growi/preset-themes';
 import { useTranslation } from 'next-i18next';
 
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { useGrowiTheme } from '~/stores/context';
+import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
+import { useSWRxGrowiThemeSetting } from '~/stores/admin/customize';
 
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 
+
+// eslint-disable-next-line @typescript-eslint/ban-types
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
 }
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
-
-  const { adminCustomizeContainer } = props;
-  const { data: currentTheme } = useGrowiTheme();
   const { t } = useTranslation();
 
-  const selectedHandler = useCallback((themeName) => {
-    // TODO: preview without using mutate of useGrowiTheme
-    // https://github.com/weseek/growi/pull/6860
-    // mutateGrowiTheme(themeName);
+  const { data, error } = useSWRxGrowiThemeSetting();
+  const [currentTheme, setCurrentTheme] = useState(data?.currentTheme);
+
+  useEffect(() => {
+    setCurrentTheme(data?.currentTheme);
+  }, [data?.currentTheme]);
+
+  const selectedHandler = useCallback((themeName: string) => {
+    setCurrentTheme(themeName);
   }, []);
 
   const submitHandler = useCallback(async() => {
+    if (currentTheme == null) {
+      toastWarning('The selected theme is undefined');
+      return;
+    }
+
     try {
-      if (currentTheme != null) {
-        await apiv3Put('/customize-setting/theme', {
-          themeType: currentTheme,
-        });
-      }
+      await apiv3Put('/customize-setting/theme', {
+        theme: currentTheme,
+      });
 
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme'), ns: 'commons' }));
     }
@@ -44,17 +49,25 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
     }
   }, [currentTheme, t]);
 
+  const availableThemes = data?.pluginThemesMetadatas == null
+    ? PresetThemesMetadatas
+    : PresetThemesMetadatas.concat(data.pluginThemesMetadatas);
+
+  const selectedTheme = availableThemes.find(t => t.name === currentTheme)?.name ?? PresetThemes.DEFAULT;
+
   return (
     <div className="row">
       <div className="col-12">
         <h2 className="admin-setting-header">{t('admin:customize_settings.theme')}</h2>
-        <CustomizeThemeOptions onSelected={selectedHandler} currentTheme={currentTheme} />
-        <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        <CustomizeThemeOptions
+          onSelected={selectedHandler}
+          availableThemes={availableThemes}
+          selectedTheme={selectedTheme}
+        />
+        <AdminUpdateButtonRow onClick={submitHandler} disabled={error != null} />
       </div>
     </div>
   );
 };
 
-const CustomizeThemeSettingWrapper = withUnstatedContainers(CustomizeThemeSetting, [AdminCustomizeContainer]);
-
-export default CustomizeThemeSettingWrapper;
+export default CustomizeThemeSetting;

+ 0 - 47
packages/app/src/components/Admin/Customize/ThemeColorBox.jsx

@@ -1,47 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-
-class ThemeColorBox extends React.PureComponent {
-
-  render() {
-    const {
-      isSelected, onSelected, name, bg, topbar, sidebar, theme,
-    } = this.props;
-
-    return (
-      <div
-        id={`theme-option-${name}`}
-        className={`theme-option-container d-flex flex-column align-items-center ${isSelected && 'active'}`}
-        onClick={onSelected}
-      >
-        <a id={name} role="button" className={`m-0 ${name} theme-button`}>
-          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
-            <g>
-              <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={bg}></path>
-              <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={topbar}></path>
-              <path d="M -1 15 L15 15 L15 65 L-1 65 L-1 15 Z" fill={sidebar}></path>
-              <path d="M 65 45 L65 65 L45 65 L65 45 Z" fill={theme}></path>
-            </g>
-          </svg>
-        </a>
-        <span className="theme-option-name"><b>{ name }</b></span>
-      </div>
-    );
-  }
-
-}
-
-
-ThemeColorBox.propTypes = {
-  isSelected: PropTypes.bool.isRequired,
-  onSelected: PropTypes.func.isRequired,
-  name: PropTypes.string.isRequired,
-  bg: PropTypes.string.isRequired,
-  topbar: PropTypes.string.isRequired,
-  sidebar: PropTypes.string.isRequired,
-  theme: PropTypes.string.isRequired,
-};
-
-export default ThemeColorBox;

+ 42 - 0
packages/app/src/components/Admin/Customize/ThemeColorBox.tsx

@@ -0,0 +1,42 @@
+import React from 'react';
+
+import type { GrowiThemeMetadata } from '@growi/core';
+
+
+type Props = {
+  isSelected: boolean,
+  metadata: GrowiThemeMetadata,
+  onSelected?: () => void,
+};
+
+export const ThemeColorBox = (props: Props): JSX.Element => {
+
+  const {
+    isSelected, metadata, onSelected,
+  } = props;
+  const {
+    name, bg, topbar, sidebar, accent, isPresetTheme,
+  } = metadata;
+
+  return (
+    <div
+      id={`theme-option-${name}`}
+      className={`theme-option-container d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
+      onClick={onSelected}
+    >
+      <a id={name} role="button" className={`m-0 ${name} theme-button`}>
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
+          <g>
+            <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={bg}></path>
+            <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={topbar}></path>
+            <path d="M -1 15 L15 15 L15 65 L-1 65 L-1 15 Z" fill={sidebar}></path>
+            <path d="M 65 45 L65 65 L45 65 L65 45 Z" fill={accent}></path>
+          </g>
+        </svg>
+      </a>
+      <span className="theme-option-name"><b>{ name }</b></span>
+      { !isPresetTheme && <span className='theme-option-badge badge badge-primary mt-1'>Plugin</span> }
+    </div>
+  );
+
+};

+ 4 - 1
packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx

@@ -54,9 +54,12 @@ const ElasticsearchManagement = () => {
             setIsConfigured(false);
           }
         }
+        toastError(errors as Error[]);
+      }
+      else {
+        toastError(errors as Error);
       }
 
-      toastError(errors);
     }
     finally {
       setIsInitialized(true);

+ 15 - 11
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -2,10 +2,11 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
+import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { tags, attrs } from '~/services/xss/recommended-whitelist';
+import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -40,6 +41,9 @@ class XssForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { xssOption } = adminMarkDownContainer.state;
 
+    const rehypeRecommendedTags = sanitizeDefaultSchema.tagNames;
+    const rehypeRecommendedAttributes = JSON.stringify(sanitizeDefaultSchema.attributes);
+
     return (
       <div className="form-group col-12 my-3">
         <div className="row">
@@ -49,12 +53,12 @@ class XssForm extends React.Component {
               <input
                 type="radio"
                 className="custom-control-input"
-                id="xssOption2"
+                id="xssOption1"
                 name="XssOption"
-                checked={xssOption === 2}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
+                checked={xssOption === RehypeSanitizeOption.RECOMMENDED}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.RECOMMENDED }) }}
               />
-              <label className="custom-control-label w-100" htmlFor="xssOption2">
+              <label className="custom-control-label w-100" htmlFor="xssOption1">
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
                 <div className="mt-4">
                   <div className="d-flex justify-content-between">
@@ -66,7 +70,7 @@ class XssForm extends React.Component {
                     rows="6"
                     cols="40"
                     readOnly
-                    defaultValue={tags}
+                    defaultValue={rehypeRecommendedTags}
                   />
                 </div>
                 <div className="mt-4">
@@ -79,7 +83,7 @@ class XssForm extends React.Component {
                     rows="6"
                     cols="40"
                     readOnly
-                    defaultValue={attrs}
+                    defaultValue={rehypeRecommendedAttributes}
                   />
                 </div>
               </label>
@@ -91,12 +95,12 @@ class XssForm extends React.Component {
               <input
                 type="radio"
                 className="custom-control-input"
-                id="xssOption3"
+                id="xssOption2"
                 name="XssOption"
-                checked={xssOption === 3}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
+                checked={xssOption === RehypeSanitizeOption.CUSTOM}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.CUSTOM }) }}
               />
-              <label className="custom-control-label w-100" htmlFor="xssOption3">
+              <label className="custom-control-label w-100" htmlFor="xssOption2">
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
                 <WhiteListInput customizable />
               </label>

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

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

@@ -0,0 +1,85 @@
+import React, { useCallback } from 'react';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+
+
+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', { pluginInstallerForm });
+      toastSuccess('Plugin Install Successed!');
+    }
+    catch (e) {
+      toastError(e);
+    }
+  }, []);
+
+  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>
+  );
+};

+ 0 - 42
packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx

@@ -1,42 +0,0 @@
-import React, { useEffect, useCallback } from 'react';
-
-import PropTypes from 'prop-types';
-
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import BasicSecurityManagementContents from './BasicSecuritySettingContents';
-
-const BasicSecurityManagement = (props) => {
-  const { adminBasicSecurityContainer } = props;
-
-  const fetchBasicSecuritySettingsData = useCallback(async() => {
-    try {
-      await adminBasicSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
-      const errs = toArrayIfNot(err);
-      toastError(errs);
-    }
-  }, [adminBasicSecurityContainer]);
-
-  useEffect(() => {
-    fetchBasicSecuritySettingsData();
-  }, [adminBasicSecurityContainer, fetchBasicSecuritySettingsData]);
-
-
-  return <BasicSecurityManagementContents />;
-};
-
-BasicSecurityManagement.propTypes = {
-  adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
-};
-
-const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(BasicSecurityManagement, [
-  AdminBasicSecurityContainer,
-]);
-
-export default BasicSecurityManagementWithUnstatedContainer;

+ 0 - 139
packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx

@@ -1,139 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class BasicSecurityManagementContents extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminBasicSecurityContainer, adminGeneralSecurityContainer } = this.props;
-
-    try {
-      await adminBasicSecurityContainer.updateBasicSetting();
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.Basic.updated_basic'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminGeneralSecurityContainer, adminBasicSecurityContainer } = this.props;
-    const { isBasicEnabled } = adminGeneralSecurityContainer.state;
-
-    return (
-      <React.Fragment>
-
-        <h2 className="alert-anchor border-bottom">
-          { t('security_settings.Basic.name') }
-        </h2>
-
-        {adminBasicSecurityContainer.state.retrieveError != null && (
-          <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {adminBasicSecurityContainer.state.retrieveError}</p>
-          </div>
-        )}
-
-        <div className="form-group row">
-          <div className="col-6 offset-3">
-            <div className="custom-control custom-switch custom-checkbox-success">
-              <input
-                id="isBasicEnabled"
-                className="custom-control-input"
-                type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isBasicEnabled}
-                onChange={() => { adminGeneralSecurityContainer.switchIsBasicEnabled() }}
-              />
-              <label className="custom-control-label" htmlFor="isBasicEnabled">
-                { t('security_settings.Basic.enable_basic') }
-              </label>
-            </div>
-            <p className="form-text text-muted">
-              <small>
-                <span dangerouslySetInnerHTML={{ __html: t('security_settings.Basic.desc_1') }} /><br />
-                { t('security_settings.Basic.desc_2')}
-              </small>
-            </p>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('basic') && isBasicEnabled)
-            && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
-          </div>
-        </div>
-
-        {isBasicEnabled && (
-          <React.Fragment>
-            <div className="row mb-5">
-              <div className="offset-md-3 col-md-6">
-                <div className="custom-control custom-checkbox custom-checkbox-success">
-                  <input
-                    id="bindByEmail-basic"
-                    className="custom-control-input"
-                    type="checkbox"
-                    checked={adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminBasicSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
-                  />
-                  <label
-                    className="custom-control-label"
-                    htmlFor="bindByEmail-basic"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical', 'username') }}
-                  />
-                </div>
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn', 'username') }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row my-3">
-              <div className="offset-4 col-5">
-                <button
-                  type="button"
-                  className="btn btn-primary"
-                  disabled={adminBasicSecurityContainer.state.retrieveError != null}
-                  onClick={this.onClickSubmit}
-                >
-                  {t('Update')}
-                </button>
-              </div>
-            </div>
-
-          </React.Fragment>
-        )}
-
-      </React.Fragment>
-    );
-  }
-
-}
-
-BasicSecurityManagementContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
-  adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
-};
-
-const BasicSecurityManagementContentsWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-
-  return <BasicSecurityManagementContents t={t} {...props} />;
-};
-
-const BasicSecurityManagementContentsWrapper = withUnstatedContainers(BasicSecurityManagementContentsWrapperFC, [
-  AdminGeneralSecurityContainer,
-  AdminBasicSecurityContainer,
-]);
-
-export default BasicSecurityManagementContentsWrapper;

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

@@ -31,6 +31,6 @@ FacebookSecurityManagement.propTypes = {
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
-const TwitterSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AdminGeneralSecurityContainer]);
+const FacebookSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AdminGeneralSecurityContainer]);
 
-export default withTranslation()(TwitterSecurityManagementWrapper);
+export default withTranslation()(FacebookSecurityManagementWrapper);

+ 10 - 28
packages/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -6,8 +6,7 @@ import { TabContent, TabPane } from 'reactstrap';
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 
-import BasicSecuritySetting from './BasicSecuritySetting';
-import FacebookSecuritySetting from './FacebookSecuritySetting';
+// import FacebookSecuritySetting from './FacebookSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
@@ -16,7 +15,6 @@ import OidcSecuritySetting from './OidcSecuritySetting';
 import SamlSecuritySetting from './SamlSecuritySetting';
 import SecuritySetting from './SecuritySetting';
 import ShareLinkSetting from './ShareLinkSetting';
-import TwitterSecuritySetting from './TwitterSecuritySetting';
 
 const SecurityManagementContents = () => {
   const { t } = useTranslation('admin');
@@ -51,31 +49,21 @@ const SecurityManagementContents = () => {
         i18n: 'OIDC',
         index: 3,
       },
-      passport_basic: {
-        Icon: () => <i className="fa fa-lock" />,
-        i18n: 'BASIC',
-        index: 4,
-      },
       passport_google: {
         Icon: () => <i className="fa fa-google" />,
         i18n: 'Google',
-        index: 5,
+        index: 4,
       },
       passport_github: {
         Icon: () => <i className="fa fa-github" />,
         i18n: 'GitHub',
-        index: 6,
-      },
-      passport_twitter: {
-        Icon: () => <i className="fa fa-twitter" />,
-        i18n: 'Twitter',
-        index: 7,
-      },
-      passport_facebook: {
-        Icon: () => <i className="fa fa-facebook" />,
-        i18n: '(TBD) Facebook',
-        index: 8,
+        index: 5,
       },
+      // passport_facebook: {
+      //   Icon: () => <i className="fa fa-facebook" />,
+      //   i18n: '(TBD) Facebook',
+      //   index: 7,
+      // },
     };
   }, []);
 
@@ -126,21 +114,15 @@ const SecurityManagementContents = () => {
           <TabPane tabId="passport_oidc">
             {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
           </TabPane>
-          <TabPane tabId="passport_basic">
-            {activeComponents.has('passport_basic') && <BasicSecuritySetting />}
-          </TabPane>
           <TabPane tabId="passport_google">
             {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_github">
             {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
           </TabPane>
-          <TabPane tabId="passport_twitter">
-            {activeComponents.has('passport_twitter') && <TwitterSecuritySetting />}
-          </TabPane>
-          <TabPane tabId="passport_facebook">
+          {/* <TabPane tabId="passport_facebook">
             {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />}
-          </TabPane>
+          </TabPane> */}
         </TabContent>
       </div>
     </div>

+ 0 - 41
packages/app/src/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -1,41 +0,0 @@
-import React, { useEffect, useCallback } from 'react';
-
-import PropTypes from 'prop-types';
-
-import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import TwitterSecuritySettingContents from './TwitterSecuritySettingContents';
-
-const TwitterSecurityManagement = (props) => {
-  const { adminTwitterSecurityContainer } = props;
-
-  const fetchTwitterSecuritySettingsData = useCallback(async() => {
-    try {
-      await adminTwitterSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
-      const errs = toArrayIfNot(err);
-      toastError(errs);
-    }
-  }, [adminTwitterSecurityContainer]);
-
-  useEffect(() => {
-    fetchTwitterSecuritySettingsData();
-  }, [adminTwitterSecurityContainer, fetchTwitterSecuritySettingsData]);
-
-  return <TwitterSecuritySettingContents />;
-};
-
-TwitterSecurityManagement.propTypes = {
-  adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
-};
-
-const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(TwitterSecurityManagement, [
-  AdminTwitterSecurityContainer,
-]);
-
-export default TwitterSecurityManagementWithUnstatedContainer;

+ 0 - 223
packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx

@@ -1,223 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-
-import { pathUtils } from '@growi/core';
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import urljoin from 'url-join';
-
-
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { useSiteUrl } from '~/stores/context';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class TwitterSecuritySettingContents extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminTwitterSecurityContainer, adminGeneralSecurityContainer } = this.props;
-
-    try {
-      await adminTwitterSecurityContainer.updateTwitterSetting();
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.OAuth.Twitter.updated_twitter'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const {
-      t, adminGeneralSecurityContainer, adminTwitterSecurityContainer, siteUrl,
-    } = this.props;
-    const { isTwitterEnabled } = adminGeneralSecurityContainer.state;
-    const twitterCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/twitter/callback');
-
-    return (
-
-      <React.Fragment>
-
-        <h2 className="alert-anchor border-bottom">
-          {t('security_settings.OAuth.Twitter.name')}
-        </h2>
-
-        {adminTwitterSecurityContainer.state.retrieveError != null && (
-          <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {adminTwitterSecurityContainer.state.retrieveError}</p>
-          </div>
-        )}
-
-        <div className="form-group row">
-          <div className="col-6 offset-3">
-            <div className="custom-control custom-switch custom-checkbox-success">
-              <input
-                id="isTwitterEnabled"
-                className="custom-control-input"
-                type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isTwitterEnabled}
-                onChange={() => { adminGeneralSecurityContainer.switchIsTwitterOAuthEnabled() }}
-              />
-              <label className="custom-control-label" htmlFor="isTwitterEnabled">
-                {t('security_settings.OAuth.Twitter.enable_twitter')}
-              </label>
-            </div>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('twitter') && isTwitterEnabled)
-              && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
-          </div>
-        </div>
-
-        <div className="row mb-5">
-          <label className="col-md-3 text-md-right py-2">{t('security_settings.callback_URL')}</label>
-          <div className="col-md-6">
-            <input
-              className="form-control"
-              type="text"
-              value={twitterCallbackUrl}
-              readOnly
-            />
-            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-            {(siteUrl == null || siteUrl === '') && (
-              <div className="alert alert-danger">
-                <i
-                  className="icon-exclamation"
-                  // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
-                />
-              </div>
-            )}
-          </div>
-        </div>
-
-
-        {isTwitterEnabled && (
-          <React.Fragment>
-
-            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
-
-            <div className="row mb-5">
-              <label htmlFor="TwitterConsumerId" className="col-md-3 text-md-right py-2">{t('security_settings.clientID')}</label>
-              <div className="col-md-6">
-                <input
-                  className="form-control"
-                  type="text"
-                  name="TwitterConsumerId"
-                  defaultValue={adminTwitterSecurityContainer.state.twitterConsumerKey || ''}
-                  onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerKey(e.target.value)}
-                />
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_KEY' }) }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row mb-5">
-              <label htmlFor="TwitterConsumerSecret" className="col-md-3 text-md-right py-2">{t('security_settings.client_secret')}</label>
-              <div className="col-md-6">
-                <input
-                  className="form-control"
-                  type="text"
-                  name="TwitterConsumerSecret"
-                  defaultValue={adminTwitterSecurityContainer.state.twitterConsumerSecret || ''}
-                  onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerSecret(e.target.value)}
-                />
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_SECRET' }) }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row mb-5">
-              <div className="offset-md-3 col-md-6">
-                <div className="custom-control custom-checkbox custom-checkbox-success">
-                  <input
-                    id="bindByUserNameTwitter"
-                    className="custom-control-input"
-                    type="checkbox"
-                    checked={adminTwitterSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminTwitterSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
-                  />
-                  <label
-                    className="custom-control-label"
-                    htmlFor="bindByUserNameTwitter"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
-                  />
-                </div>
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row my-3">
-              <div className="offset-4 col-5">
-                <button
-                  type="button"
-                  className="btn btn-primary"
-                  disabled={adminTwitterSecurityContainer.state.retrieveError != null}
-                  onClick={this.onClickSubmit}
-                >
-                  {t('Update')}
-                </button>
-              </div>
-            </div>
-
-          </React.Fragment>
-        )}
-
-        <hr />
-
-        <div style={{ minHeight: '300px' }}>
-          <h4>
-            <i className="icon-question" aria-hidden="true"></i>
-            <a href="#collapseHelpForTwitterOauth" data-toggle="collapse"> {t('security_settings.OAuth.how_to.twitter')}</a>
-          </h4>
-          <ol id="collapseHelpForTwitterOauth" className="collapse">
-            {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_1', { link: '<a href="https://apps.twitter.com/" target=_blank>Twitter Application Management</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_2') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_3') }} />
-            {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_4', { url: adminTwitterSecurityContainer.state.callbackUrl }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_5') }} />
-          </ol>
-        </div>
-
-      </React.Fragment>
-
-
-    );
-  }
-
-}
-
-TwitterSecuritySettingContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
-  adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
-  siteUrl: PropTypes.string,
-};
-
-const TwitterSecuritySettingContentsWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-  const { data: siteUrl } = useSiteUrl();
-  return <TwitterSecuritySettingContents t={t} siteUrl={siteUrl} {...props} />;
-};
-
-/**
- * Wrapper component for using unstated
- */
-const TwitterSecuritySettingContentsWrapper = withUnstatedContainers(TwitterSecuritySettingContentsWrapperFC, [
-  AdminGeneralSecurityContainer,
-  AdminTwitterSecurityContainer,
-]);
-
-export default TwitterSecuritySettingContentsWrapper;

+ 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 - 4
packages/app/src/components/BookmarkButtons.tsx

@@ -41,15 +41,12 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
   };
 
   const getTooltipMessage = useCallback(() => {
-    if (isGuestUser) {
-      return 'Not available for guest';
-    }
 
     if (isBookmarked) {
       return 'tooltip.cancel_bookmark';
     }
     return 'tooltip.bookmark';
-  }, [isGuestUser, isBookmarked]);
+  }, [isBookmarked]);
 
   return (
     <div className={`btn-group btn-group-bookmark ${styles['btn-group-bookmark']}`} role="group" aria-label="Bookmark buttons">

+ 26 - 20
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -5,6 +5,7 @@ import {
   Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
 } from 'reactstrap';
 
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import {
   IPageInfoAll, isIPageInfoForOperation,
 } from '~/interfaces/page';
@@ -247,9 +248,10 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   return (
     <DropdownMenu
       data-testid="page-item-control-menu"
-      positionFixed
-      modifiers={{ preventOverflow: { boundariesElement: undefined } }}
+      modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}
       right={alignRight}
+      container="body"
+      style={{ zIndex: 1055 }} /* make it larger than $zindex-modal of bootstrap */
     >
       {contents}
     </DropdownMenu>
@@ -330,24 +332,28 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
   }, [onClickPathRecoveryMenuItem, pageId]);
 
   return (
-    <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} data-testid="open-page-item-control-btn">
-      { children ?? (
-        <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
-          <i className="icon-options"></i>
-        </DropdownToggle>
-      ) }
-
-      <PageItemControlDropdownMenu
-        {...props}
-        isLoading={isLoading}
-        pageInfo={fetchedPageInfo ?? presetPageInfo}
-        onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
-        onClickRenameMenuItem={renameMenuItemClickHandler}
-        onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
-        onClickDeleteMenuItem={deleteMenuItemClickHandler}
-        onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
-      />
-    </Dropdown>
+    <NotAvailableForGuest>
+      <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} data-testid="open-page-item-control-btn">
+        { children ?? (
+          <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
+            <i className="icon-options"></i>
+          </DropdownToggle>
+        ) }
+
+        <PageItemControlDropdownMenu
+          {...props}
+          isLoading={isLoading}
+          pageInfo={fetchedPageInfo ?? presetPageInfo}
+          onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
+          onClickRenameMenuItem={renameMenuItemClickHandler}
+          onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
+          onClickDeleteMenuItem={deleteMenuItemClickHandler}
+          onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
+        />
+      </Dropdown>
+
+    </NotAvailableForGuest>
+
   );
 
 };

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

@@ -11,7 +11,7 @@ export const CompleteUserRegistration: FC = () => {
       <div className="row mx-0">
         <div className="col-12 mb-3 text-center">
           <p className="alert alert-success">
-            <span>{t('login.Registration successful')}</span>
+            <span>{t('login.registration_successful')}</span>
           </p>
           {/* If the transition source is "/login", use <a /> tag since the transition will not occur if next/link is used. */}
           <a href='/login'>

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

+ 1 - 1
packages/app/src/components/DescendantsPageListModal.module.scss

@@ -1,4 +1,4 @@
-.grw-page-accessories-modal :global {
+.grw-descendants-page-list-modal :global {
   .modal-header {
     button.close {
       margin: auto 0rem auto auto;

+ 2 - 2
packages/app/src/components/DescendantsPageListModal.tsx

@@ -93,8 +93,8 @@ export const DescendantsPageListModal = (): JSX.Element => {
       size="xl"
       isOpen={isOpened}
       toggle={close}
-      data-testid="page-accessories-modal"
-      className={`grw-page-accessories-modal ${styles['grw-page-accessories-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
+      data-testid="descendants-page-list-modal"
+      className={`grw-descendants-page-list-modal ${styles['grw-descendants-page-list-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
     >
       <ModalHeader className="p-0" toggle={close} close={buttons}>
         <CustomNavTab

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

+ 2 - 2
packages/app/src/components/InAppNotification/InAppNotificationList.tsx

@@ -2,7 +2,7 @@ import React, { FC } from 'react';
 
 import { HasObjectId } from '@growi/core';
 
-import { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+import type { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 
 import InAppNotificationElm from './InAppNotificationElm';
 
@@ -26,7 +26,7 @@ const InAppNotificationList: FC<Props> = (props: Props) => {
     );
   }
 
-  const notifications = inAppNotificationData.docs;
+  const notifications = inAppNotificationData.docs.filter((notification) => { return notification.parsedSnapshot != null });
 
   return (
     <>

+ 3 - 6
packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -6,10 +6,9 @@ import { HasObjectId } from '@growi/core';
 import { PagePathLabel } from '@growi/ui';
 import { useRouter } from 'next/router';
 
-import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
-import { IInAppNotification } from '~/interfaces/in-app-notification';
+import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
-import { parseSnapshot } from '../../../models/serializers/in-app-notification-snapshot/page';
 import FormattedDistanceDate from '../../FormattedDistanceDate';
 
 interface Props {
@@ -27,8 +26,6 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
 
   const router = useRouter();
 
-  const snapshot = parseSnapshot(notification.snapshot);
-
   // publish open()
   useImperativeHandle(ref, () => ({
     open() {
@@ -45,7 +42,7 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
   return (
     <div className="p-2 overflow-hidden">
       <div className="text-truncate">
-        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={snapshot.path} />
+        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
       </div>
       <i className={`${actionIcon} mr-2`} />
       <FormattedDistanceDate

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

+ 2 - 2
packages/app/src/components/Layout/Admin.module.scss

@@ -267,12 +267,12 @@ $slack-work-space-name-card-border: #efc1f6;
       background-color: $gray-50;
       border: 1px solid $border-color;
     }
-    .theme-option-name {
+    .theme-option-name, .theme-option-badge {
       opacity: 0.3;
     }
     // style (active)
     .theme-option-container.active {
-      .theme-option-name {
+      .theme-option-name, .theme-option-badge {
         opacity: 1;
       }
     }

+ 6 - 7
packages/app/src/components/Layout/AdminLayout.tsx

@@ -8,27 +8,26 @@ import { RawLayout } from './RawLayout';
 
 import styles from './Admin.module.scss';
 
+
+const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
+const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 
 
 type Props = {
-  title?: string
   componentTitle?: string
   children?: ReactNode
 }
 
 
 const AdminLayout = ({
-  children, title, componentTitle,
+  children, componentTitle,
 }: Props): JSX.Element => {
 
-  const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
-  const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
-
   return (
-    <RawLayout title={title}>
+    <RawLayout>
       <div className={`admin-page ${styles['admin-page']}`}>
-        <GrowiNavbar />
+        <GrowiNavbar isGlobalSearchHidden={true} />
 
         <header className="py-0 container-fluid">
           <h1 className="title px-3">{componentTitle}</h1>

+ 15 - 14
packages/app/src/components/Layout/BasicLayout.tsx

@@ -4,6 +4,7 @@ import dynamic from 'next/dynamic';
 import { DndProvider } from 'react-dnd';
 import { HTML5Backend } from 'react-dnd-html5-backend';
 
+import { useEditorModeClassName } from '../../client/services/layout';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import Sidebar from '../Sidebar';
 
@@ -22,32 +23,23 @@ 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 });
 
 
 type Props = {
-  title: string
-  className?: string,
-  expandContainer?: boolean,
   children?: ReactNode
+  className?: string
 }
 
-export const BasicLayout = ({
-  children, title, className, expandContainer,
-}: Props): JSX.Element => {
-
-  const myClassName = `${className ?? ''} ${expandContainer ? 'growi-layout-fluid' : ''}`;
-
+export const BasicLayout = ({ children, className }: Props): JSX.Element => {
   return (
-    <RawLayout title={title} className={myClassName}>
-
+    <RawLayout className={className ?? ''}>
       <DndProvider backend={HTML5Backend}>
         <GrowiNavbar />
 
         <div className="page-wrapper d-flex d-print-block">
-          <div className="grw-sidebar-wrapper">
+          <div className="grw-sidebar-wrapper" data-testid="grw-sidebar-wrapper">
             <Sidebar />
           </div>
 
@@ -64,7 +56,6 @@ export const BasicLayout = ({
         <PageDeleteModal />
         <PageRenameModal />
         <PageAccessoriesModal />
-        <DrawioModal />
       </DndProvider>
 
       <PagePresentationModal />
@@ -77,3 +68,13 @@ export const BasicLayout = ({
     </RawLayout>
   );
 };
+
+export const BasicLayoutWithEditorMode = ({ children }: Props): JSX.Element => {
+  const className = useEditorModeClassName();
+
+  return (
+    <BasicLayout className={className}>
+      {children}
+    </BasicLayout>
+  );
+};

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