Просмотр исходного кода

Merge branch 'feat/6450-copy-for-slack-command-permission' into feat/7014-restrict-by-channel

zahmis 4 лет назад
Родитель
Сommit
521ba91866
100 измененных файлов с 1359 добавлено и 492 удалено
  1. 2 0
      .devcontainer/Dockerfile
  2. 1 0
      .devcontainer/devcontainer.json
  3. 4 1
      .devcontainer/docker-compose.yml
  4. 4 0
      .eslintrc.js
  5. 2 2
      .github/workflows/ci-slackbot-proxy.yml
  6. 12 9
      .github/workflows/ci.yml
  7. 1 1
      .github/workflows/release-rc.yml
  8. 2 2
      .github/workflows/release.yml
  9. 4 0
      .markdownlint.yml
  10. 20 0
      .vscode/launch.json
  11. 30 2
      CHANGES.md
  12. 23 5
      package.json
  13. 5 6
      packages/app/.env.development
  14. 1 1
      packages/app/.env.production
  15. 1 0
      packages/app/.eslintignore
  16. 1 1
      packages/app/.gitignore
  17. 22 14
      packages/app/bin/generate-plugin-definitions-source.ts
  18. 2 0
      packages/app/config/ci/.env.local.for-ci
  19. 34 24
      packages/app/docker/Dockerfile
  20. 3 3
      packages/app/docker/docker-entrypoint.sh
  21. 1 0
      packages/app/jest.config.js
  22. 28 36
      packages/app/package.json
  23. BIN
      packages/app/public/images/slack-integration/activate-public-dist.png
  24. BIN
      packages/app/public/images/slack-integration/basicinfo-all-checked.png
  25. BIN
      packages/app/public/images/slack-integration/click-add-to-slack.png
  26. BIN
      packages/app/public/images/slack-integration/growi-register-sentence.png
  27. 25 3
      packages/app/resource/locales/en_US/admin/admin.json
  28. 8 0
      packages/app/resource/locales/en_US/notifications/PasswordResetSuccessful.txt
  29. 13 0
      packages/app/resource/locales/en_US/notifications/notActiveUser.txt
  30. 10 0
      packages/app/resource/locales/en_US/notifications/passwordReset.txt
  31. 22 22
      packages/app/resource/locales/en_US/sandbox.md
  32. 19 2
      packages/app/resource/locales/en_US/translation.json
  33. 53 16
      packages/app/resource/locales/en_US/welcome.md
  34. 18 2
      packages/app/resource/locales/ja_JP/admin/admin.json
  35. 13 0
      packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt
  36. 10 0
      packages/app/resource/locales/ja_JP/notifications/passwordReset.txt
  37. 6 0
      packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt
  38. 19 2
      packages/app/resource/locales/ja_JP/translation.json
  39. 43 10
      packages/app/resource/locales/ja_JP/welcome.md
  40. 18 2
      packages/app/resource/locales/zh_CN/admin/admin.json
  41. 6 0
      packages/app/resource/locales/zh_CN/notifications/PasswordResetSuccessful.txt
  42. 13 0
      packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt
  43. 10 0
      packages/app/resource/locales/zh_CN/notifications/passwordReset.txt
  44. 20 3
      packages/app/resource/locales/zh_CN/translation.json
  45. 53 16
      packages/app/resource/locales/zh_CN/welcome.md
  46. 5 3
      packages/app/src/client/admin.jsx
  47. 20 1
      packages/app/src/client/models/Linker.js
  48. 34 0
      packages/app/src/client/nologin.jsx
  49. 4 2
      packages/app/src/client/plugin.js
  50. 1 1
      packages/app/src/client/services/AdminGitHubSecurityContainer.js
  51. 1 1
      packages/app/src/client/services/AdminGoogleSecurityContainer.js
  52. 50 2
      packages/app/src/client/services/AdminHomeContainer.js
  53. 12 1
      packages/app/src/client/services/AdminLocalSecurityContainer.js
  54. 9 37
      packages/app/src/client/services/AdminNotificationContainer.js
  55. 1 1
      packages/app/src/client/services/AdminOidcSecurityContainer.js
  56. 1 1
      packages/app/src/client/services/AdminSamlSecurityContainer.js
  57. 91 0
      packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js
  58. 1 1
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  59. 3 1
      packages/app/src/client/services/PageContainer.js
  60. 5 1
      packages/app/src/client/util/apiNotification.js
  61. 21 0
      packages/app/src/components/Admin/AdminHome/AdminHome.jsx
  62. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  63. 71 0
      packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  64. 21 21
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  65. 114 20
      packages/app/src/components/Admin/Notification/NotificationSetting.jsx
  66. 0 80
      packages/app/src/components/Admin/Notification/SlackIntegrationNotificationSetting.jsx
  67. 22 1
      packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  68. 8 5
      packages/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx
  69. 39 17
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  70. 3 3
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  71. 7 5
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx
  72. 40 16
      packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  73. 53 0
      packages/app/src/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx
  74. 10 5
      packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  75. 55 41
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  76. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  77. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  78. 1 1
      packages/app/src/components/Admin/Users/UserTable.jsx
  79. 3 1
      packages/app/src/components/ComparePathsTable.jsx
  80. 3 1
      packages/app/src/components/ContentLinkButtons.jsx
  81. 1 1
      packages/app/src/components/CreateTemplateModal.jsx
  82. 3 1
      packages/app/src/components/DuplicatedPathsTable.jsx
  83. 1 1
      packages/app/src/components/Fab.jsx
  84. 7 0
      packages/app/src/components/LoginForm.jsx
  85. 4 2
      packages/app/src/components/Navbar/AuthorInfo.jsx
  86. 1 1
      packages/app/src/components/Navbar/GrowiSubNavigation.jsx
  87. 1 1
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  88. 3 1
      packages/app/src/components/Page/CopyDropdown.jsx
  89. 3 1
      packages/app/src/components/Page/PageManagement.jsx
  90. 1 1
      packages/app/src/components/Page/TrashPageAlert.jsx
  91. 1 1
      packages/app/src/components/PageAttachment/DeleteAttachmentModal.jsx
  92. 1 1
      packages/app/src/components/PageAttachment/PageAttachmentList.jsx
  93. 1 1
      packages/app/src/components/PageComment/Comment.jsx
  94. 3 3
      packages/app/src/components/PageComment/CommentEditor.jsx
  95. 1 1
      packages/app/src/components/PageComment/DeleteCommentModal.jsx
  96. 6 4
      packages/app/src/components/PageCreateModal.jsx
  97. 10 0
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  98. 7 1
      packages/app/src/components/PageEditor/Editor.jsx
  99. 7 9
      packages/app/src/components/PageEditor/EditorIcon.jsx
  100. 3 3
      packages/app/src/components/PageEditor/EditorNavbarBottom.jsx

+ 2 - 0
.devcontainer/Dockerfile

@@ -14,6 +14,8 @@ ARG USER_UID=1000
 ARG USER_GID=$USER_UID
 
 RUN mkdir -p /workspace/growi/node_modules
+RUN mkdir -p /workspace/growi/packages/app/node_modules
+RUN mkdir -p /workspace/growi/packages/slackbot-proxy/node_modules
 
 # [Optional] Update UID/GID if needed
 RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \

+ 1 - 0
.devcontainer/devcontainer.json

@@ -16,6 +16,7 @@
 	"extensions": [
 		"dbaeumer.vscode-eslint",
 		"eamodio.gitlens",
+    "firsttris.vscode-jest-runner",
 		"msjsdiag.debugger-for-chrome",
 		"firefox-devtools.vscode-firefox-debug",
 		"editorconfig.editorconfig",

+ 4 - 1
.devcontainer/docker-compose.yml

@@ -24,8 +24,9 @@ services:
     volumes:
       - ..:/workspace/growi:delegated
       - node_modules:/workspace/growi/node_modules
+      - node_modules_app:/workspace/growi/packages/app/node_modules
+      - node_modules_slackbot-proxy:/workspace/growi/packages/slackbot-proxy/node_modules
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
-      - ../../node_modules:/workspace/node_modules:delegated
 
     tty: true
 
@@ -81,3 +82,5 @@ services:
       - /files/sqlite
 volumes:
   node_modules:
+  node_modules_app:
+  node_modules_slackbot-proxy:

+ 4 - 0
.eslintrc.js

@@ -26,5 +26,9 @@ module.exports = {
         FunctionExpression: { body: 1, parameters: 2 },
       },
     ],
+    'jest/no-standalone-expect': [
+      'error',
+      { additionalTestBlockFunctions: ['each.test'] },
+    ],
   },
 };

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

@@ -120,7 +120,7 @@ jobs:
     - name: yarn dev:ci
       working-directory: ./packages/slackbot-proxy
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.development.local
         yarn dev:ci
       env:
         TYPEORM_CONNECTION: mysql
@@ -211,7 +211,7 @@ jobs:
     - name: yarn start:prod:ci
       working-directory: ./packages/slackbot-proxy
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn start:prod:ci
       env:
         TYPEORM_CONNECTION: mysql

+ 12 - 9
.github/workflows/ci.yml

@@ -51,10 +51,10 @@ jobs:
         yarn list --depth=0
     - name: lerna run lint for plugins
       run: |
-        yarn lerna run lint --scope @growi/plugin-pukiwiki-like-linker
+        yarn lerna run lint --scope @growi/plugin-*
     - name: lerna run lint for app
       run: |
-        yarn lerna run lint --scope @growi/app
+        yarn lerna run lint --scope @growi/app --scope @growi/core --scope @growi/ui
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
@@ -130,6 +130,12 @@ jobs:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi_test
 
+    - name: Upload coverage report as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Coverage Report
+        path: packages/app/coverage
+
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       if: failure()
@@ -206,7 +212,7 @@ jobs:
     - name: yarn dev:ci
       working-directory: ./packages/app
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.development.local
         yarn dev:ci
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
@@ -281,7 +287,7 @@ jobs:
         yarn list --depth=0
     - name: Build
       run: |
-        yarn lerna run build --scope @growi/slack --scope @growi/app
+        yarn lerna run build --scope @growi/core --scope @growi/slack --scope @growi/plugin-* --scope @growi/app
     - name: lerna bootstrap --production
       run: |
         npx lerna bootstrap -- --production
@@ -297,20 +303,17 @@ jobs:
     - name: yarn server:ci
       working-directory: ./packages/app
       run: |
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn server:ci
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
     - name: yarn server:ci with MongoDB 3.6
       working-directory: ./packages/app
       run: |
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn server:ci
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
-    - name: Upload report as artifact
-      uses: actions/upload-artifact@v2
-      with:
-        name: Report
-        path: report
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master

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

@@ -55,7 +55,7 @@ jobs:
       uses: docker/build-push-action@v2
       with:
         context: .
-        file: ./docker/Dockerfile
+        file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         push: true
         cache-from: type=local,src=/tmp/.buildx-cache

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

@@ -113,7 +113,7 @@ jobs:
       uses: docker/build-push-action@v2
       with:
         context: .
-        file: ./docker/Dockerfile
+        file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         push: true
         cache-from: type=local,src=/tmp/.buildx-cache
@@ -131,7 +131,7 @@ jobs:
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         repository: weseek/growi
-        readme-filepath: ./docker/README.md
+        readme-filepath: ./packages/app/docker/README.md
 
     - name: Slack Notification
       uses: weseek/ghaction-release-slack-notification@master

+ 4 - 0
.markdownlint.yml

@@ -7,3 +7,7 @@ no-multiple-blanks: false
 no-duplicate-heading: false
 no-inline-html: false
 no-trailing-punctuation: false
+
+MD002: false
+MD012: false
+MD041: false

+ 20 - 0
.vscode/launch.json

@@ -44,6 +44,26 @@
         "url": "http://localhost:3000",
         "webRoot": "${workspaceFolder}/packages/app/public",
         "pathMappings": [
+          {
+            "url": "webpack:///core",
+            "path": "${workspaceFolder}/packages/core"
+          },
+          {
+            "url": "webpack:///plugin-attachment-refs",
+            "path": "${workspaceFolder}/packages/plugin-attachment-refs"
+          },
+          {
+            "url": "webpack:///plugin-pukiwiki-like-linker",
+            "path": "${workspaceFolder}/packages/plugin-pukiwiki-like-linker"
+          },
+          {
+            "url": "webpack:///plugin-lsx",
+            "path": "${workspaceFolder}/packages/plugin-lsx"
+          },
+          {
+            "url": "webpack:///ui",
+            "path": "${workspaceFolder}/packages/ui"
+          },
           {
             "url": "webpack:///src",
             "path": "${workspaceFolder}/packages/app/src"

+ 30 - 2
CHANGES.md

@@ -1,11 +1,39 @@
 # CHANGES
 
-## v4.3.3-RC
+## v4.4.0-RC
 
+### BREAKING CHANGES
+
+* Official plugins are now preinstalled
+
+### Updates
+
+* Feature: Password resetting by user
+* Feature: User trigger notification and Global notification are available by new Slack integration
+* Improvement: Add attachment button in editor navbar
+* Fix: Recursive rename operation from `/parent` to `/parent/child` ([#4101](https://github.com/weseek/growi/pull/4101))
+* Fix: Encode spaces in page path in LinkEditModal
+* Support: Create @growi/core package
+* Support: Create @growi/ui package
+* Support: Improve error handling for @growi/slackbot-proxy
+* Support: Include official plugins as sub packages
 * Support: Upgrade libs
     * @slack/web-api
-    * escape-string-regexp
+    * date-fns
+    * helmet
     * morgan
+    * socket.io
+
+## v4.3.3
+
+* Improvement: Welcome page markdown
+* Fix: Some recursive operation exclude descendant pages that are restricted for groups
+    * Rename / Delete / Delete completely / Put back / Duplicate
+* Fix: Layout is broken when editing users page ([#4128](https://github.com/weseek/growi/issues/4128))
+* Support: Upgrade libs
+    * @slack/web-api
+    * date-fns
+    * escape-string-regexp
 
 ## v4.3.2
 

+ 23 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.3.3-RC",
+  "version": "4.4.0-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -21,13 +21,17 @@
   },
   "private": true,
   "workspaces": {
-    "packages": ["packages/*"],
-    "nohoist": ["**/slackbot-proxy/bootstrap"]
+    "packages": [
+      "packages/*"
+    ],
+    "nohoist": [
+      "**/slackbot-proxy/bootstrap"
+    ]
   },
   "scripts": {
     "start": "yarn app:server",
     "prestart": "yarn app:build",
-    "app:build": "yarn lerna run build --scope @growi/app --scope @growi/slack --scope @growi/plugin-pukiwiki-like-linker",
+    "app:build": "yarn lerna run build --scope @growi/app --scope @growi/slack --scope @growi/plugin-*",
     "app:server": "yarn lerna run server --scope @growi/app",
     "slackbot-proxy:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",
@@ -37,8 +41,17 @@
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
   },
   "dependencies": {
+    "cross-env": "^7.0.0",
+    "dotenv-flow": "^3.2.0",
+    "npm-run-all": "^4.1.5",
+    "ts-node": "^9.1.1",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
   },
   "devDependencies": {
+    "@types/jest": "^26.0.22",
+    "@types/node": "^14.14.35",
+    "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "eslint": "^7.31.0",
@@ -48,7 +61,12 @@
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react-hooks": "^4.2.0",
-    "lerna": "^4.0.0"
+    "jest": "^27.0.6",
+    "jest-date-mock": "^1.0.8",
+    "jest-localstorage-mock": "^2.4.14",
+    "lerna": "^4.0.0",
+    "rewire": "^5.0.0",
+    "ts-jest": "^27.0.4"
   },
   "engines": {
     "node": "^12 || ^14",

+ 5 - 6
packages/app/.env.development

@@ -3,13 +3,14 @@
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 FILE_UPLOAD=mongodb
-# MONGO_GRIDFS_TOTAL_LIMIT=10485760 # 10MB
+# MONGO_GRIDFS_TOTAL_LIMIT=10485760
 MATHJAX=1
 # NO_CDN=true
 MONGO_URI="mongodb://mongo:27017/growi"
 # REDIS_URI="http://redis:6379"
 # NCHAN_URI="http://nchan"
 ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
+ELASTICSEARCH_REQUEST_TIMEOUT=15000
 HACKMD_URI="http://localhost:3010"
 HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
@@ -17,11 +18,9 @@ HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 # PUBLISH_OPEN_API=true
 # USER_UPPER_LIMIT=0
 # DEV_HTTPS=true
-# FORCE_WIKI_MODE=private # 'public', 'private', undefined
+# FORCE_WIKI_MODE=private
 # PROMSTER_ENABLED=true
-# SLACK_SIGNING_SECRET=''
-# SLACK_BOT_TOKEN=''
-SALT_FOR_GTOP_TOKEN="proxy"
-SALT_FOR_PTOG_TOKEN="growi"
+# SLACKBOT_WITHOUT_PROXY_SIGNING_SECRET=''
+# SLACKBOT_WITHOUT_PROXY_BOT_TOKEN=''
 # GROWI_CLOUD_URI='http://growi.cloud'
 # GROWI_APP_ID_FOR_GROWI_CLOUD=012345

+ 1 - 1
packages/app/.env.production

@@ -2,4 +2,4 @@
 ## Handled by Next.js with dotenv or dotenv-flow
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
-FORMAT_NODE_LOG=false # default: true
+FORMAT_NODE_LOG=false

+ 1 - 0
packages/app/.eslintignore

@@ -1,3 +1,4 @@
+/dist/**
 /public/**
 /src/client/legacy/thirdparty-js/**
 /src/client/util/reveal/plugins/markdown.js

+ 1 - 1
packages/app/.gitignore

@@ -13,7 +13,7 @@
 
 # dist (for GROWI v4.x and below)
 /public/*.chunk.js
-/public/*.chunk.js.LICENSE
+/public/*.chunk.js.LICENSE.txt
 /public/*.bundle.js
 /public/manifest.json
 /public/dll

+ 22 - 14
packages/app/bin/generate-plugin-definitions-source.ts

@@ -7,6 +7,8 @@ import fs from 'graceful-fs';
 import normalize from 'normalize-path';
 import swig from 'swig-templates';
 
+import { PluginDefinitionV4 } from '@growi/core';
+
 import PluginUtils from '../src/server/plugins/plugin-utils';
 import loggerFactory from '../src/utils/logger';
 import { resolveFromRoot } from '../src/utils/project-dir-utils';
@@ -23,26 +25,32 @@ const OUT = resolveFromRoot('tmp/plugins/plugin-definitions.js');
 const pluginNames: string[] = pluginUtils.listPluginNames();
 logger.info('Detected plugins: ', pluginNames);
 
-// get definitions
-const definitions = pluginNames
-  .map((name) => {
-    return pluginUtils.generatePluginDefinition(name, true);
-  })
-  .map((definition) => {
-    if (definition == null) {
-      return null;
+async function main(): Promise<void> {
+
+  // get definitions
+  const definitions: PluginDefinitionV4[] = [];
+  for (const pluginName of pluginNames) {
+    // eslint-disable-next-line no-await-in-loop
+    const definition = await pluginUtils.generatePluginDefinition(pluginName, true);
+    if (definition != null) {
+      definitions.push(definition);
     }
+  }
 
+  definitions.map((definition) => {
     // convert backslash to slash
     definition.entries = definition.entries.map((entryPath) => {
       return normalize(entryPath);
     });
     return definition;
-  })
-  .filter(definition => definition != null);
+  });
+
+  const compiledTemplate = swig.compileFile(TEMPLATE);
+  const code = compiledTemplate({ definitions });
+
+  // write
+  fs.writeFileSync(OUT, code);
 
-const compiledTemplate = swig.compileFile(TEMPLATE);
-const code = compiledTemplate({ definitions });
+}
 
-// write
-fs.writeFileSync(OUT, code);
+main();

+ 2 - 0
packages/app/config/ci/.env.local.for-ci

@@ -1,2 +1,4 @@
+FORMAT_NODE_LOG=true
+
 # disable Elasticsearch
 ELASTICSEARCH_URI=

+ 34 - 24
packages/app/docker/Dockerfile

@@ -17,16 +17,21 @@ COPY ./package.json .
 COPY ./yarn.lock .
 COPY ./lerna.json .
 COPY ./packages/app/package.json packages/app/
+COPY ./packages/core/package.json packages/core/
+COPY ./packages/plugin-attachment-refs/package.json packages/plugin-attachment-refs/
+COPY ./packages/plugin-lsx/package.json packages/plugin-lsx/
+COPY ./packages/plugin-pukiwiki-like-linker/package.json packages/plugin-pukiwiki-like-linker/
 COPY ./packages/slack/package.json packages/slack/
+COPY ./packages/ui/package.json packages/ui/
 
 # setup
 RUN yarn config set network-timeout 300000
 RUN npx lerna bootstrap
 
 # make artifacts
-RUN tar cf node_modules.tar node_modules \
-  packages/app/node_modules \
-  packages/slack/node_modules
+RUN tar cf node_modules.tar \
+  node_modules \
+  packages/*/node_modules
 
 
 
@@ -39,9 +44,9 @@ FROM deps-resolver AS deps-resolver-prod
 RUN yarn install --production
 
 # make artifacts
-RUN tar cf node_modules.tar node_modules \
-  packages/app/node_modules \
-  packages/slack/node_modules
+RUN tar cf node_modules.tar \
+  node_modules \
+  packages/*/node_modules
 
 
 
@@ -88,16 +93,14 @@ COPY ./package.json ./
 COPY ./yarn.lock ./
 COPY ./lerna.json ./
 COPY ./tsconfig.base.json ./
-COPY ./babel.config.js ./
-COPY ./bin ./bin
-COPY ./config ./config
-COPY ./public ./public
-COPY ./resource ./resource
-COPY ./src ./src
-COPY ./tmp ./tmp
 # copy all related packages
-COPY packages/slack packages/slack
 COPY packages/app packages/app
+COPY packages/core packages/core
+COPY packages/plugin-attachment-refs packages/plugin-attachment-refs
+COPY packages/plugin-lsx packages/plugin-lsx
+COPY packages/plugin-pukiwiki-like-linker packages/plugin-pukiwiki-like-linker
+COPY packages/slack packages/slack
+COPY packages/ui packages/ui
 
 # build
 RUN yarn lerna run build
@@ -106,15 +109,18 @@ RUN yarn lerna run build
 RUN tar cf packages.tar \
   package.json \
   yarn.lock \
-  config \
-  public \
-  resource \
-  src \
-  tmp \
+  tsconfig.base.json \
   packages/app/package.json \
-  packages/slack/package.json \
-  packages/slack/dist
-
+  packages/app/config \
+  packages/app/dist \
+  packages/app/public \
+  packages/app/resource \
+  packages/app/tmp \
+  packages/app/.env.production \
+  packages/app/tsconfig.base.json \
+  packages/app/tsconfig.json \
+  packages/*/package.json \
+  packages/*/dist
 
 
 
@@ -124,6 +130,8 @@ RUN tar cf packages.tar \
 FROM node:14-slim
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
+ENV NODE_ENV production
+
 ENV appDir /opt/growi
 
 # Add gosu
@@ -154,12 +162,14 @@ RUN rm node_modules.tar packages.tar
 
 USER root
 
-COPY docker/docker-entrypoint.sh /
+COPY packages/app/docker/docker-entrypoint.sh /
 RUN chmod 700 /docker-entrypoint.sh
 RUN chown node:node ${appDir}
 
+WORKDIR ${appDir}/packages/app
+
 VOLUME /data
 EXPOSE 3000
 
 ENTRYPOINT ["/tini", "-e", "143", "--", "/docker-entrypoint.sh"]
-CMD ["yarn", "server:prod"]
+CMD ["node", "-r", "dotenv-flow/config", "--expose_gc", "dist/server/app.js"]

+ 3 - 3
packages/app/docker/docker-entrypoint.sh

@@ -4,11 +4,11 @@ set -e
 
 # Support `FILE_UPLOAD=local`
 mkdir -p /data/uploads
-if [ ! -e "$appDir/public/uploads" ]; then
-  ln -s /data/uploads $appDir/public/uploads
+if [ ! -e "./public/uploads" ]; then
+  ln -s /data/uploads ./public/uploads
 fi
 
 chown -R node:node /data/uploads
-chown -h node:node $appDir/public/uploads
+chown -h node:node ./public/uploads
 
 gosu node $@

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

@@ -5,6 +5,7 @@
 const MODULE_NAME_MAPPING = {
   '^\\^/(.+)$': '<rootDir>/$1',
   '^~/(.+)$': '<rootDir>/src/$1',
+  '^@growi/(.+)$': '<rootDir>/../$1/src',
 };
 
 module.exports = {

+ 28 - 36
packages/app/package.json

@@ -1,25 +1,26 @@
 {
   "name": "@growi/app",
-  "version": "4.3.3-RC",
+  "version": "4.4.0-RC",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
     "start": "yarn build && yarn server",
     "build": "run-p build:*",
-    "build:client": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js --profile --bail",
-    "build:server": "cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server.json",
-    "prebuild": "run-p resources:*",
-    "postbuild": "npx shx mv transpiled/src dist && npx shx rm -r transpiled",
-    "server": "cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
+    "build:client": "yarn cross-env NODE_ENV=production webpack --config config/webpack.prod.js --profile --bail",
+    "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server.json",
+    "clean": "npx shx rm -rf dist transpiled",
+    "prebuild": "yarn cross-env NODE_ENV=production run-p clean resources:*",
+    "postbuild": "npx shx mv transpiled/src dist && npx shx rm -rf transpiled",
+    "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
     "server:ci": "yarn server --ci",
-    "preserver": "yarn migrate",
+    "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
     "//// for development": "",
     "dev": "run-p dev:client dev:server",
-    "dev:client": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js --progress --watch",
-    "dev:client:nowatch": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
-    "dev:server": "cross-env NODE_ENV=development yarn ts-node-dev src/server/app.ts --expose_gc --inspect",
-    "predev:client": "run-p resources:*",
-    "predev:server": "yarn migrate",
+    "dev:client": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js --progress --watch",
+    "dev:client:nowatch": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
+    "dev:server": "yarn cross-env NODE_ENV=development yarn ts-node-dev src/server/app.ts --expose_gc",
+    "predev:client": "yarn cross-env NODE_ENV=development run-p resources:*",
+    "predev:server": "yarn cross-env NODE_ENV=development yarn migrate",
     "//// for CI": "",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "predev:ci": "run-p resources:*",
@@ -32,10 +33,10 @@
     "prelint:eslint": "yarn resources:plugin",
     "prelint:swagger2openapi": "yarn openapi:v3",
     "//// misc": "",
-    "console": "cross-env NODE_ENV=development yarn ts-node --experimental-repl-await src/server/console.js",
+    "console": "yarn cross-env NODE_ENV=development yarn ts-node --experimental-repl-await src/server/console.js",
     "swagger-jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
-    "openapi:v3": "cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
-    "openapi:v1": "cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
+    "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
+    "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "resources:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
     "resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
     "migrate": "yarn migrate:up",
@@ -44,7 +45,7 @@
     "migrate:up": "yarn ts-node node_modules/.bin/migrate-mongo up -f config/migrate.js",
     "migrate:down": "yarn ts-node node_modules/.bin/migrate-mongo down -f config/migrate.js",
     "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only",
-    "ts-node-dev": "ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config --transpile-only"
+    "ts-node-dev": "ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config --inspect --transpile-only"
   },
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
@@ -54,8 +55,10 @@
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/plugin-pukiwiki-like-linker": "^4.3.3-RC",
-    "@growi/slack": "^4.3.3-RC",
+    "@growi/plugin-attachment-refs": "^4.4.0-RC",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.0-RC",
+    "@growi/plugin-lsx": "^4.4.0-RC",
+    "@growi/slack": "^4.4.0-RC",
     "@kobalab/socket.io-session": "^1.0.3",
     "@promster/express": "^5.0.1",
     "@promster/server": "^6.0.0",
@@ -76,12 +79,10 @@
     "connect-mongo": "^4.4.1",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
-    "cross-env": "^7.0.0",
     "csrf": "^3.1.0",
-    "date-fns": "^2.0.0",
+    "date-fns": "^2.23.0",
     "detect-indent": "^6.0.0",
     "diff": "^5.0.0",
-    "dotenv-flow": "^3.2.0",
     "elasticsearch": "^16.0.0",
     "entities": "^2.0.0",
     "esa-nodejs": "^0.0.7",
@@ -90,14 +91,14 @@
     "express-bunyan-logger": "^1.3.3",
     "express-form": "~0.12.0",
     "express-mongo-sanitize": "^2.1.0",
+    "express-rate-limit": "^5.3.0",
     "express-session": "^1.16.1",
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
-    "growi-plugin-attachment-refs": "^2.0.2",
-    "growi-plugin-lsx": "^4.0.3",
-    "helmet": "^3.13.0",
+    "helmet": "^4.6.0",
+    "nocache": "^3.0.1",
     "http-errors": "~1.6.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
@@ -117,7 +118,6 @@
     "multer-autoreap": "^1.0.3",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
-    "npm-run-all": "^4.1.2",
     "openid-client": "=2.5.0",
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
@@ -133,14 +133,11 @@
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "rimraf": "^3.0.0",
-    "socket.io": "^2.3.0",
+    "socket.io": "^4.0.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "swagger-jsdoc": "^3.4.0",
     "swig-templates": "^2.0.2",
-    "ts-node": "^9.1.1",
-    "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
@@ -158,11 +155,11 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
+    "@growi/ui": "^4.4.0-RC",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/multer": "^1.4.5",
-    "@types/node": "^14.14.35",
     "@types/react-dom": "^17.0.9",
     "autoprefixer": "^9.0.0",
     "bootstrap": "^4.5.0",
@@ -182,8 +179,6 @@
     "hard-source-webpack-plugin": "^0.13.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "imports-loader": "^0.8.0",
-    "jest": "^27.0.6",
-    "jest-date-mock": "^1.0.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
@@ -221,15 +216,13 @@
     "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-i18next": "^11.1.0",
-    "react-images": "1.0.0",
-    "react-motion": "^0.5.2",
     "react-waypoint": "^10.1.0",
     "reactstrap": "^8.9.0",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "sass-loader": "^8.0.0",
     "simple-load-script": "^1.0.2",
-    "socket.io-client": "^2.3.0",
+    "socket.io-client": "^4.0.0",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
@@ -239,7 +232,6 @@
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
-    "ts-jest": "^27.0.4",
     "ts-loader": "^8.3.0",
     "ts-node-dev": "^1.1.6",
     "tsc-alias": "^1.2.9",

BIN
packages/app/public/images/slack-integration/activate-public-dist.png


BIN
packages/app/public/images/slack-integration/basicinfo-all-checked.png


BIN
packages/app/public/images/slack-integration/click-add-to-slack.png


BIN
packages/app/public/images/slack-integration/growi-register-sentence.png


+ 25 - 3
packages/app/resource/locales/en_US/admin/admin.json

@@ -11,7 +11,13 @@
     "installed_version": "Installed version",
     "list_of_env_vars":"List of environment variables",
     "env_var_priority": "For environment variables other than security, the value of the database is obtained preferentially.",
-    "about_security": "Check <a href='/admin/security'>Securtiy Settings</a> for security environment variables."
+    "about_security": "Check <a href='/admin/security'>Security Settings</a> for security environment variables.",
+    "copy_prefilled_host_information": {
+      "default": "Copy prefilled host information",
+      "done": "Copied to clipboard!"
+    },
+    "bug_report": "Submitting a bug report",
+    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>then submit your issue to GitHub.</a>"
   },
   "app_setting": {
     "site_name": "Site name",
@@ -252,6 +258,12 @@
     "download": "Download",
     "delete": "Delete"
   },
+  "external_notification": {
+    "enabled": "Enabled",
+    "disabled": "Disabled",
+    "header_status": "Slack Integration Status",
+    "caution_enabled": "CAUTION: Currently, notifications that are configured in this page will send only to the Slack Workspace set as primary."
+  },
   "slack_integration": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
@@ -282,6 +294,9 @@
       "cancel": "Cancel",
       "change": "Change"
     },
+    "toastr": {
+      "delete_slack_integration_procedure": "Succeeded to delete the slack integration procedure"
+    },
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
     "access_token_settings": {
       "regenerate": "Regenerate"
@@ -309,10 +324,13 @@
       "paste_growi_url": "Since a modal is displayed, enter the following URL in <b>GROWI URL</b>.",
       "enter_access_token_for_growi_and_proxy": "Enter <b>Access Token Proxy to GROWI</b> and <b>Access Token GROWI to Proxy</b>",
       "set_proxy_url_on_growi": "Set Proxy URL on GROWI",
-      "copy_proxy_url": "1. When the above step are completed successfully, the Proxy URL will be displayed in the Slack Channel you selected in the modal, so copy it.",
-      "enter_proxy_url_and_update": "2. Enter and update the Proxy URL that you copied in step in the <b>Proxy URL</b>  of the <b>Custom bot with proxy integration</b> on this page.",
+      "copy_proxy_url": "When the above step are completed successfully, the Proxy URL will be displayed in the Slack Channel you selected in the modal, so copy it.",
+      "enter_proxy_url_and_update": "Enter and update the Proxy URL that you copied in the above step in the <b>Proxy URL</b> of the <b>Custom bot with proxy integration</b> on this page.",
       "dont_need_update": "※If the value is already in there, there is no need to update it.",
       "select_install_your_app": "Select \"Install your app\".",
+      "go-to-manage-distribution": "Go to \"Manage Settings\" > \"Manage distribution\" on your Slack App page.",
+      "activate-public-distribution": "In \"Share Your App with Other Workspaces\", make sure all items are checked and click \"Activate Public Distribution\"",
+      "click-add-to-slack-button": "Click \"Add to Slack\" button.",
       "select_install_to_workspace": "Select \"Install to Workspace\".",
       "register_proxy_url": "Register Proxy URL with GROWI",
       "click_allow": "Select \"Allow\".",
@@ -346,6 +364,10 @@
       "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
     }
   },
+  "slack_integration_legacy": {
+    "alert_disabled": "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": {
     "invite_users": "Temporarily issue a new user",
     "click_twice_same_checkbox": "You should check at least one checkbox.",

+ 8 - 0
packages/app/resource/locales/en_US/notifications/PasswordResetSuccessful.txt

@@ -0,0 +1,8 @@
+Password Reset Successful
+
+Hi {{ email }}
+
+Your password has been successfully reset.
+Please log in with your new password.
+
+Thank you,

+ 13 - 0
packages/app/resource/locales/en_US/notifications/notActiveUser.txt

@@ -0,0 +1,13 @@
+Password Reset
+
+Hi, {{ email }}
+
+A request has been received to change the password from {{ appTitle }}.
+However, this email is not registerd. Please try again with different email.
+
+If you did not request a password reset, you can safely ignore this email.
+
+-------------------------------------------------------------------------
+
+GROWI: {{ appTitle }}
+URL: {{ url }}

+ 10 - 0
packages/app/resource/locales/en_US/notifications/passwordReset.txt

@@ -0,0 +1,10 @@
+Password Reset
+
+Hi, {{ email }}
+
+A request has been received to change the password your GROWI account {{ appTitle }}.
+To reset your password, click on the link below.
+
+{{ url }}
+
+If you did not request a password reset, you can safely ignore this email.

+ 22 - 22
packages/app/resource/locales/en_US/sandbox.md

@@ -37,7 +37,7 @@ Add one `#` per level at the start of the line
 
 ## Block paragraph
 
-Pararaphs are created by inserting a newline character
+Paragraphs are created by inserting a newline character
 A paragraph can be created by pressing Enter at the end of the previous paragraph.
 
 ```
@@ -53,17 +53,17 @@ paragraph2
 ## Br new line
 
 Add two spaces before break.
-***This behaviour can be modified in the options menu.***
+***This behavior can be modified in the options menu.***
 
 ```
-hoge
-fuga(two spaces)
-piyo
+foo
+bar(two spaces)
+baz
 ```
 
-hoge
-fuga
-piyo
+foo
+bar
+baz
 
 ## Blockquotes
 
@@ -84,7 +84,7 @@ Add one `>` per level at the start of the line
 Wrap code with three back quotes or tildes.
 
 ```
-print 'hoge'
+print 'foo'
 ```
 
 ### Syntax highlight and file name
@@ -131,16 +131,16 @@ This is  `Inline Code`.
 Code blocks should be preceded by four spaces or one tab.
 
 ```
-    class Hoge
-        def hoge
-            print 'hoge'
+    class Foo
+        def foo
+            print 'foo'
         end
     end
 ```
 
-    class Hoge
-        def hoge
-            print 'hoge'
+    class Foo
+        def foo
+            print 'foo'
         end
     end
 
@@ -166,7 +166,7 @@ ___
 
 ### Italic
 
-To italicize text, add One asterisk or underscores before and after a word or phrase.
+To italicize text, add one asterisk or underscores before and after a word or phrase.
 
 ```
 This is *Italic* .
@@ -178,7 +178,7 @@ This is _Italic_ .
 
 ### Bold
 
-To bold text, add two asterisks or underscores before and after a word or phrase.
+To make text bold, add two asterisks or underscores before and after a word or phrase.
 
 ```
 This is **bold**.
@@ -263,7 +263,7 @@ Example of Bootstrap4 is[[here>./Bootstrap4]]
 
 ## Ul Bulleted list
 
-To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in front of line items. 
+To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in front of line items.
 Items can be nested using indentation.
 
 ```
@@ -286,7 +286,7 @@ Items can be nested using indentation.
 
 ## Ol Numbered List
 
-To create an ordered list, add line items with numbers followed by periods. 
+To create an ordered list, add line items with numbers followed by periods.
 The numbers don’t have to be in numerical order, but the list should start with the number one.
 
 ```
@@ -449,9 +449,9 @@ See [emojione](https://www.emojione.com/)
 
 # :heavy_plus_sign: More..
 
-- Try to attach Bootstrap4 Tags?
+- Want to attach Bootstrap4 Tags?
     - :arrow_right: [/Sandbox/Bootstrap4]
-- Try to draw Diagrams?
+- Want to draw Diagrams?
     - :arrow_right: [/Sandbox/Diagrams]
-- Try to write Math Formulas?
+- Want to write Math Formulas?
     - :arrow_right: [/Sandbox/Math]

+ 19 - 2
packages/app/resource/locales/en_US/translation.json

@@ -438,7 +438,6 @@
     "initialize_successed": "Succeeded to initialize {{target}}",
     "give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin",
-    "delete_slack_integration_procedure": "Succeeded to delete the slack integration procedure",
     "activate_user_success": "Succeeded to activating {{username}}",
     "deactivate_user_success": "Succeeded to deactivate {{username}}",
     "remove_user_success": "Succeeded to removing {{username}}",
@@ -605,7 +604,10 @@
     "Local": {
       "name": "ID/Password",
       "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-      "enable_local": "Enable ID/Password"
+      "enable_local": "Enable ID/Password",
+      "password_reset_by_users": "Password reset by users",
+      "enable_password_reset_by_users": "Enable password reset by users",
+      "password_reset_desc": "when forgot password, users are able to reset it by themselves."
     },
     "ldap": {
       "enable_ldap": "Enable LDAP",
@@ -846,5 +848,20 @@
     "aws_region": "For the region, enter the AWS region name. ex):us-east-1",
     "aws_custom_endpoint":"For the custom endpoint, specify the URL that starts with http(s)://. Also, the trailing slash is not required.",
     "failed_to_send_a_test_email":"Failed to send a test email using SMTP. Please check your settings."
+  },
+  "forgot_password":{
+    "forgot_password": "Forgot Password?",
+    "send": "Send",
+    "return_to_login": "Return to login",
+    "reset_password": "Reset Password",
+    "sign_in_instead": "Sign in instead",
+    "password_reset_request_desc": "You can reset your password here.",
+    "password_reset_excecution_desc": "Enter a new password",
+    "new_password": "New Password",
+    "confirm_new_password": "Confirm the new password",
+    "email_is_required": "Email is required",
+    "success_to_send_email": "Success to send email",
+    "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
+    "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
   }
 }

+ 53 - 16
packages/app/resource/locales/en_US/welcome.md

@@ -1,27 +1,64 @@
-# Welcome to GROWI :anchor:
+# :tada: Welcome to GROWI
 
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 
-<div class="card border-primary">
-  <div class="card-header bg-primary text-light">Tips</div>
-  <div class="card-body"><ul>
-    <li>Ctrl(⌘) + "/" to show quick help</li>
-    <li>You can write HTML with <a href="https://getbootstrap.com/docs/4.5/components/">Bootstrap 4</a>.</li>
-  </ul></div>
+GROWI is a Wiki for Individuals and Corporations | A knowledge base tool.
+Knowledge in companies, university laboratories, and clubs can be easily shared and anyone can edit the page.
+
+We can easily write what we know and edit it together, we can **simplify the tacit knowledge (knowledge which is hard to explain with words) in our team**.  
+Let's increase the information exchange everyday.
+
+### :beginner: How to create a page easily 
+
+- Start from "**Create**" button on the upper right, or the **Pencil Icon** on the lower right.
+    - The page title can be edited again later, don't worry about the title.
+        - On title input field, it's possible to create the page's hierarchy with half-width `/` (slash).
+        - (Example)Try entering `/category1/category2/page-title-we-want-to-create`.
+- We can create a bullet point by adding `-`  at the beginning of the line.
+- We can also copy and paste, drag and drop attachments such as images, PDF, Word/Excel/PowerPoint, etc.
+- Once we finished, press the "**Update**" button to publish the page.
+    - We can also save it by `Ctrl(⌘) +S`.
+
+For more information: [Tutorial#Create New Page](https://docs.growi.org/en/guide/tutorial/create_page.html#create-new-page)
+
+<div class="mt-4 card border-primary">
+  <div class="card-header bg-primary text-light">
+    Tips
+  </div>
+  <div class="card-body">
+    <ul>
+      <li>Ctrl(⌘) + "/" to show quick help.</li>
+      <li>We can write HTML with <a href="https://getbootstrap.com/docs/4.5/components/">Bootstrap 4</a>.</li>
+    </ul>
+  </div>
 </div>
 
-Contents
-=========
+# :anchor: For administrator <small>〜After you construct the site〜</small>
+
+### :arrow_right: Do you will use a Wiki with more than one person?
+- :heavy_check_mark: Let's invite some members.
+    - [Add/invite new members to the Wiki](https://docs.growi.org/en/admin-guide/management-cookbook/user-management.html#temporary-issuance-of-a-new-user)
+### :arrow_right: Work with Slack to receive page and comment notifications.
+- :heavy_check_mark:  [Slack integration](https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#overview)
+### :arrow_right: Are you switching from another system?
+- :heavy_check_mark: It's possible to import data from other GROWI, esa.io, Qiita:Team.
+    -  [Import Data](https://docs.growi.org/en/admin-guide/management-cookbook/import.html)
+
+For more information: [Admin Guide](https://docs.growi.org/en/admin-guide/)
+
+
+# Content List Example
+
+We can display the content list using a table and `$lsx`.
 
-|All Pages|[/Sandbox]|
-| --- | --- |
-| $lsx(/) | <div class="alert alert-success"><span style="font-size: x-large;"><i class="icon-check"></i> [Go to Sandbox](/Sandbox)</span></div> $lsx(/Sandbox)|
+| All page list (First 15 pages)      | [/Sandbox] List of subordinate pages |
+| ----------------------------------- | ------------------------------------ |
+| $lsx(/,num=15)                      | $lsx(/Sandbox)                       |
 
-Slack
-=====
+# Slack
 
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 
-Let's join our Slack channel for all to help make GROWI better.
-In addition to discussing development, we also accept questions at the time of introduction.
+We welcome newcomers joining our slack channel to help improve Growi.
+In addition to discussing development, we are also happy to answer your questions when you join.

+ 18 - 2
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -252,6 +252,12 @@
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
+  "external_notification": {
+    "enabled": "有効",
+    "disabled": "無効",
+    "header_status": "Slack 連携の状態",
+    "caution_enabled": "CAUTION: このページで設定される通知は、Primary として設定された Slack ワークスペースにのみ送信されます。 "
+  },
   "slack_integration": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
@@ -282,6 +288,9 @@
       "cancel": "取消",
       "change": "変更する"
     },
+    "toastr": {
+      "delete_slack_integration_procedure": "Slack 連携手順を削除しました"
+    },
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "access_token_settings": {
       "regenerate": "再発行"
@@ -308,10 +317,13 @@
       "paste_growi_url": "モーダルが表示されるので、<b>GROWI URL</b> には下記のURLを入力します。",
       "enter_access_token_for_growi_and_proxy": "上記で発行した<b>Access Token Proxy to GROWI</b> と <b>Access Token GROWI to Proxy</b>を入れる",
       "set_proxy_url_on_growi": "ProxyのURLをGROWIに登録する",
-      "copy_proxy_url": "1. ②が正常に完了すると、モーダル内で選択したSlack ChannelにProxy URLが表示されるので、コピーします。",
-      "enter_proxy_url_and_update": "2. 連携手順③でコピーしたProxy URLを、このページの<b>Custom bot with proxy 連携</b>の<b>Proxy URL</b>に入力、更新します。",
+      "copy_proxy_url": "上の手順が正常に完了すると、モーダル内で選択したSlack ChannelにProxy URLが表示されるので、コピーします。",
+      "enter_proxy_url_and_update": "コピーしたProxy URLを、このページの<b>Custom bot with proxy 連携</b>の<b>Proxy URL</b>に入力、更新します。",
       "dont_need_update": "※既に値が入っている場合は更新する必要はありません",
       "select_install_your_app": "Install your app をクリックします。",
+      "go-to-manage-distribution": "作成した Slack App の Settings から Manage distribution をクリックします。",
+      "activate-public-distribution": "\"Share Your App with Other Workspaces\" の 4つの項目全てにチェックマークがついたら、\"Activate Public Distribution\" を押します。",
+      "click-add-to-slack-button": "\"Add to Slack\" ボタンをクリックします。",
       "select_install_to_workspace": "Install to Workspace をクリックします。",
       "register_proxy_url": "Proxy の URLをGROWIに登録する",
       "click_allow": "遷移先の画面にて、Allowをクリックします。",
@@ -345,6 +357,10 @@
       "custom_bot_with_proxy_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
     }
   },
+  "slack_integration_legacy": {
+    "alert_disabled": "<a href='/admin/slack-integration'>新しい設定</a>が有効になっているため、この 'Slack連携 (レガシー)' は現在無効になっています。",
+    "alert_deplicated": "この 'Slack連携 (レガシー)' は将来廃止されます。代わりに<a href='/admin/slack-integration'>新しいSlack連携機能</a>を利用してください。"
+  },
   "user_management": {
     "invite_users": "新規ユーザーの仮発行",
     "click_twice_same_checkbox": "少なくとも一つはチェックしてください。",

+ 13 - 0
packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt

@@ -0,0 +1,13 @@
+パスワードリセット
+
+こんにちは、 {{ email }}
+
+{{ appTitle }} からパスワード再設定のリクエストがありましたが、このemailは登録されておりません。
+他のemailアドレスで再度お試しください。
+
+もしこのリクエストに心当たりがない場合は、このメールを無視してください。
+
+-------------------------------------------------------------------------
+
+GROWI: {{ appTitle }}
+URL: {{ url }}

+ 10 - 0
packages/app/resource/locales/ja_JP/notifications/passwordReset.txt

@@ -0,0 +1,10 @@
+パスワード リセット
+
+こんにちは, {{ email }}
+
+あなたのGROWIアカウント {{ appTitle }} から、パスワード再設定のリクエストがありました。
+パスワードをリセットするには、以下のリンクをクリックしてください。
+
+{{ url }}
+
+もしこのリクエストに心当たりがない場合は、このメールを無視してください。

+ 6 - 0
packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt

@@ -0,0 +1,6 @@
+パスワードリセットに成功
+
+こんにちは、 {{ email }}
+
+あなたのパスワードは正常にリセットされました。
+新しいパスワードでログインしてください。

+ 19 - 2
packages/app/resource/locales/ja_JP/translation.json

@@ -440,7 +440,6 @@
     "initialize_successed": "{{target}}を初期化しました",
     "give_user_admin": "{{username}}を管理者に設定しました",
     "remove_user_admin": "{{username}}を管理者から外しました",
-    "delete_slack_integration_procedure": "Slack 連携手順を削除しました",
     "activate_user_success": "{{username}}を有効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
@@ -604,7 +603,10 @@
     "Local": {
       "name": "ID/Password",
       "note for the only env option": "現在LOCAL認証のON/OFFは環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
-      "enable_local": "ID/Password を有効にする"
+      "enable_local": "ID/Password を有効にする",
+      "password_reset_by_users": "ユーザーによるパスワード再設定",
+      "enable_password_reset_by_users": "ユーザーによるパスワード再設定を有効にする",
+      "password_reset_desc": "ログイン時のパスワードを忘れた際に、ユーザー自身がパスワードを再設定できます。"
     },
     "ldap": {
       "enable_ldap": "LDAP を有効にする",
@@ -840,5 +842,20 @@
     "aws_region": "リージョンには、AWSリージョン名を入力してください。例: ap-northeast-1",
     "aws_custom_endpoint": "カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。",
     "failed_to_send_a_test_email":"SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。"
+  },
+  "forgot_password":{
+    "forgot_password": "パスワードをお忘れですか?",
+    "send": "送信",
+    "return_to_login": "ログイン画面に戻る",
+    "reset_password": "パスワード リセット",
+    "sign_in_instead": "ログインする",
+    "password_reset_request_desc": "ここからパスワードリセットできます",
+    "password_reset_excecution_desc": "新しいパスワードを入力してください",
+    "new_password": "新しいパスワード",
+    "confirm_new_password": "新しいパスワードの確認",
+    "email_is_required": "メールを入力してください",
+    "success_to_send_email": "メールを送信しました",
+    "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
+    "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
   }
 }

+ 43 - 10
packages/app/resource/locales/ja_JP/welcome.md

@@ -1,9 +1,27 @@
-# Welcome to GROWI :anchor:
-
+# :tada: GROWI へようこそ
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 
-<div class="card border-primary">
+GROWI は個人・法人向けの Wiki | ナレッジベースツールです。  
+会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。
+
+知っている情報をカジュアルに書き出しみんなで編集することで、**チーム内での暗黙知を減らす**ことができます。  
+当たり前に共有される情報を日々増やしていきましょう。
+
+### :beginner: 簡単なページの作り方
+
+- 右上の "**作成**"ボタンまたは右下の**鉛筆アイコン**のボタンからページを書き始めることができます
+    - ページタイトルは後から変更できますので、適当に入力しても大丈夫です
+        - タイトル入力欄では、半角の `/` (スラッシュ) でページ階層を作れます
+        - (例)`/カテゴリ1/カテゴリ2/作りたいページタイトル` のように入力してみてください
+- `- ` を行頭につけると、この文章のような箇条書きを書くことができます
+- 画像やPDF、Word/Excel/PowerPointなどの添付ファイルも、コピー&ペースト、ドラッグ&ドロップで貼ることができます
+- 書けたら "**更新**" ボタンを押してページを公開しましょう
+    - `Ctrl(⌘) +S` でも保存できます
+
+さらに詳しくはこちら: [チュートリアル#新規ページ作成](https://docs.growi.org/ja/guide/tutorial/create_page.html#新規ページ作成)
+
+<div class="mt-4 card border-primary">
   <div class="card-header bg-primary text-light">Tips</div>
   <div class="card-body"><ul>
     <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
@@ -11,15 +29,30 @@
   </ul></div>
 </div>
 
-Contents
-=========
 
-|All Pages|[/Sandbox]|
-| --- | --- |
-| $lsx(/) | <div class="alert alert-success"><span style="font-size: x-large;"><i class="icon-check"></i> [Sandboxをチェック](/Sandbox)</span></div> $lsx(/Sandbox)|
+# :anchor: 管理者の方へ <small>〜Wikiを作ったら〜</small>
+
+### :arrow_right: 複数人でWikiを使いますか?
+- :heavy_check_mark: メンバーを招待しましょう
+    - [Wikiに新しいメンバーを追加・招待する](https://docs.growi.org/ja/admin-guide/management-cookbook/user-management.html#%E6%96%B0%E8%A6%8F%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E4%BB%AE%E7%99%BA%E8%A1%8C)
+### :arrow_right: Slackと連携してページやコメントの通知を受け取りましょう
+- :heavy_check_mark:  [Slack連携](https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/#%E6%A6%82%E8%A6%81)
+### :arrow_right: 他のシステムからの乗り換えですか?
+- :heavy_check_mark: 他の GROWI、esa. io、Qiita:Team のデータをインポートすることが出来ます
+    -  [データのインポート](https://docs.growi.org/ja/admin-guide/management-cookbook/import.html)
+
+さらに詳しくはこちら: [管理者ガイド](https://docs.growi.org/ja/admin-guide/)
+
+
+# コンテンツリストアップ例
+
+テーブルと `$lsx` を使ってコンテンツリストを表示できます。
+
+| 全てのページリスト (First 15 pages) | [/Sandbox] 配下ページ一覧 |
+| ----------------------------------- | ------------------------- |
+| $lsx(/,num=15)                      | $lsx(/Sandbox)            |
 
-Slack
-=====
+# Slack
 
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 

+ 18 - 2
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -262,6 +262,12 @@
     "download": "下载",
     "delete": "删除"
   },
+  "external_notification": {
+    "enabled": "Enabled",
+    "disabled": "Disabled",
+    "header_status": "Slack整合状态",
+    "caution_enabled": "CAUTION: 目前,在此页面中配置的通知只会通知设置为主要的 Slack 工作区。 "
+  },
   "slack_integration": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
@@ -292,6 +298,9 @@
       "cancel": "取消",
       "change": "改变"
     },
+    "toastr": {
+      "delete_slack_integration_procedure": "删除了 Slack 集成程序"
+    },
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <code>{{variable}}</code> 启用。",
     "access_token_settings": {
       "regenerate": "再生"
@@ -318,10 +327,13 @@
       "paste_growi_url": "由于显示了模式,请在 <b>GROWI URL</b> 中输入以下URL",
       "enter_access_token_for_growi_and_proxy": "插入上面发出的 <b>Access Token Proxy to GROWI</b> 和 <b>Access Token GROWI to Proxy</b>。",
       "set_proxy_url_on_growi": "向GROWI注册Proxy的URL",
-      "copy_proxy_url": "1. 当上述步骤成功完成后,Proxy URL将显示在你在模版中选择的Slack频道中,所以请复制它。",
-      "enter_proxy_url_and_update": "2. 输入并更新你在步骤③中复制的ProxyURL到本页的<b>Custom bot with proxy 一体化</b>的<b>ProxyURL</b>。",
+      "copy_proxy_url": "当上述步骤成功完成后,Proxy URL将显示在你在模版中选择的Slack频道中,所以请复制它。",
+      "enter_proxy_url_and_update": "上述过程中复制的ProxyURL到本页的<b>Custom bot with proxy 一体化</b>的<b>ProxyURL</b>。",
       "dont_need_update": "※如果值已经在里面了,就不需要再更新。",
       "select_install_your_app": "选择 \"Install your app\"。",
+      "go-to-manage-distribution": "一旦你创建了你的Slack应用程序,进入设置并点击管理分发。",
+      "activate-public-distribution": "当 \"Share Your App with Other Workspaces\" 中的所有四个项目都被勾选后,按 \"Activate Public Distribution\"。",
+      "click-add-to-slack-button": "点击 \"Add to Slack\" 按钮。",
       "select_install_to_workspace": "选择 \"Install to Workspace\"。",
       "register_proxy_url": "向 GROWI 注册代理 URL",
       "click_allow": "选择 \"Allow\"。",
@@ -355,6 +367,10 @@
       "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
     }
   },
+  "slack_integration_legacy": {
+    "alert_disabled": "由于<a href='/admin/slack-integration'>新设置</a>已启用,因此该'旧版Slack一体化'目前已被禁用。",
+    "alert_deplicated": "这个 '旧版Slack一体化' 已经过时了,将来会停止使用。使用<a href='/admin/slack-integration'>新的设置</a>来代替。"
+  },
   "user_management": {
     "invite_users": "临时发布新用户",
     "click_twice_same_checkbox": "您应该至少选中一个复选框。",

+ 6 - 0
packages/app/resource/locales/zh_CN/notifications/PasswordResetSuccessful.txt

@@ -0,0 +1,6 @@
+密码重置成功
+
+嗨, {{email}}
+
+您的密码已成功重置。
+请使用您的新密码登录。

+ 13 - 0
packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt

@@ -0,0 +1,13 @@
+重设密码
+
+嗨,{{电子邮件}}
+
+已收到来自 {{appTitle}} 的更改密码请求。
+但是,此电子邮件未注册。请使用其他电子邮件重试。
+
+如果您没有要求重置密码,则可以放心地忽略此电子邮件。
+
+-------------------------------------------------------------------------
+
+GROWI: {{ appTitle }}
+URL: {{ url }}

+ 10 - 0
packages/app/resource/locales/zh_CN/notifications/passwordReset.txt

@@ -0,0 +1,10 @@
+重设密码
+
+嗨,{{ email }}
+
+已收到更改您 GROWI 帐户 {{appTitle}} 密码的请求。
+要重置密码,请单击下面的链接。
+
+{{ url }}
+
+如果您没有要求重置密码,则可以放心地忽略此电子邮件。

+ 20 - 3
packages/app/resource/locales/zh_CN/translation.json

@@ -42,7 +42,7 @@
   "Update": "更新",
 	"Update Page": "更新本页",
 	"Warning": "警告",
-	"Sign in": "登录",
+  "Sign in": "登录",
 	"Sign up is here": "注册",
 	"Sign in is here": "登录",
 	"Sign up": "注册",
@@ -418,7 +418,6 @@
     "initialize_successed": "Succeeded to initialize {{target}}",
 		"give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin ",
-    "delete_slack_integration_procedure": "删除了 Slack 集成程序",
 		"activate_user_success": "Succeeded to activating {{username}}",
 		"deactivate_user_success": "Succeeded to deactivate {{username}}",
 		"remove_user_success": "Succeeded to removing {{username}} ",
@@ -593,7 +592,10 @@
 		"Local": {
 			"name": "ID/Password",
 			"note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-			"enable_local": "Enable ID/Password"
+      "enable_local": "Enable ID/Password",
+      "password_reset_by_users": "用户重置密码",
+      "enable_password_reset_by_users": "启用用户重置密码",
+      "password_reset_desc": "忘记密码时,用户可以自行重置"
 		},
 		"ldap": {
 			"enable_ldap": "Enable LDAP",
@@ -851,5 +853,20 @@
     "aws_region": "关于地区,请输入AWS地区名,例如:ap-east-1",
     "aws_custom_endpoint": "关于自定义端点,请指定以http(s)://开头的URL,链接末尾不需要添加“/”",
     "failed_to_send_a_test_email":"SMTP方式测试邮件发送失败,请检查相关设定。"
+  },
+  "forgot_password":{
+    "forgot_password": "忘记密码?",
+    "send": "发送",
+    "return_to_login": "返回登录",
+    "reset_password": "重设密码",
+    "sign_in_instead": "改为登录",
+    "password_reset_request_desc": "您可以在此处重置密码",
+    "password_reset_excecution_desc": "输入新的密码",
+    "new_password": "新密码",
+    "confirm_new_password": "确认新密码",
+    "email_is_required": "电子邮件是必需的",
+    "success_to_send_email": "我发了一封电子邮件",
+    "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
+    "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
   }
 }

+ 53 - 16
packages/app/resource/locales/zh_CN/welcome.md

@@ -1,27 +1,64 @@
-# 欢迎来到GROWI :anchor:
+# :tada: 欢迎来到GROWI
 
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 
-<div class="card border-primary">
-  <div class="card-header bg-primary text-light">提示</div>
-  <div class="card-body"><ul>
-    <li>(按Ctrl>)+“/”to show quick help</li>
-    <li>>你可以写HTML与</a href=”https://getbootstrap.com docs/4.5 components/“Bootstrap 4</a></li>
-  </ul></div>
+GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
+公司、大学实验室和俱乐部的知识可以轻松共享,任何人都可以编辑页面。
+
+我们可以很容易地写下我们知道的东西,并一起编辑,我们可以**简化我们团队中的隐性知识(难以用语言解释的知识)**。 
+让我们每天增加信息交流。
+
+### :beginner: 如何轻松制作一个页面 
+
+- 从右上方的 "**创建**"按钮,或右下方的**铅笔图标开始。
+    - 页面标题以后可以再编辑,不用担心标题的问题。
+        - 在标题输入栏,可以用半宽的`/`(斜线)创建页面的层次。
+        - 例子)尝试输入`/category1/category2/page-title-we-want-to-create`。
+- 我们可以通过在行首添加`-`来创建一个要点。
+- 我们还可以复制和粘贴,拖放附件,如图片、PDF、Word/Excel/PowerPoint等。
+- 一旦我们完成了,按 "**更新**"按钮来发布页面。
+    - 我们也可以通过`Ctrl(⌘) +S`来保存。
+
+了解更多信息: [Tutorial#Create New Page](https://docs.growi.org/en/guide/tutorial/create_page.html#create-new-page)
+
+<div class="mt-4 card border-primary">
+  <div class="card-header bg-primary text-light">
+    Tips
+  </div>
+  <div class="card-body">
+    <ul>
+      <li>Ctrl(⌘) + "/" 显示快速帮助。</li>
+      <li>你可以用 <a href="https://getbootstrap.com/docs/4.5/components/">Bootstrap 4编写HTML</a>.</li>
+    </ul>
+  </div>
 </div>
 
-Contents
-=========
+# :anchor: 对于管理员来说 <small>〜如果你创建了一个Wiki〜</small>
+
+### :arrow_right: 你会和多个人一起使用Wiki吗?
+- :heavy_check_mark: 让我们邀请一些成员。
+    - [Add/invite new members to the Wiki](https://docs.growi.org/en/admin-guide/management-cookbook/user-management.html#temporary-issuance-of-a-new-user)
+### :arrow_right: 与Slack合作,接收页面和评论通知。
+- :heavy_check_mark:  [Slack integration](https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#overview)
+### :arrow_right: 你是否从另一个系统转换?
+- :heavy_check_mark: 可以从其他GROWI, esa.io, Qiita:Team导入数据。
+    -  [Import Data](https://docs.growi.org/en/admin-guide/management-cookbook/import.html)
+
+了解更多信息: [Admin Guide](https://docs.growi.org/en/admin-guide/)
+
+
+# 内容列表示例
+
+你可以用一个表格和`$lsx`来显示内容列表。
 
-|All Pages|[/Sandbox]|
-| --- | --- |
-| $lsx(/) | <div class="alert alert-success"><span style="font-size: x-large;"><i class="icon-check"></i> [Go to Sandbox](/Sandbox)</span></div> $lsx(/Sandbox)|
+| 所有页面列表(前15页)      | [/Sandbox] 下级页面列表 |
+| ---------------------------| ------------------------|
+| $lsx(/,num=15)             | $lsx(/Sandbox)          |
 
-Slack 
-=====
+# Slack
 
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 
-让我们加入我们所有人的休闲渠道,帮助成长。
-除了讨论发展,我们在介绍时也接受提问。
+我们欢迎新人加入我们的slack频道,帮助改善Growi
+除了讨论发展问题,我们也很乐意在你加入时回答你的问题

+ 5 - 3
packages/app/src/client/admin.jsx

@@ -10,7 +10,7 @@ import ErrorBoundary from '../components/ErrorBoudary';
 import AdminHome from '../components/Admin/AdminHome/AdminHome';
 import UserGroupDetailPage from '../components/Admin/UserGroupDetail/UserGroupDetailPage';
 import NotificationSetting from '../components/Admin/Notification/NotificationSetting';
-import SlackIntegrationNotificationSetting from '../components/Admin/Notification/SlackIntegrationNotificationSetting';
+import LegacySlackIntegration from '../components/Admin/LegacySlackIntegration/LegacySlackIntegration';
 import SlackIntegration from '../components/Admin/SlackIntegration/SlackIntegration';
 import ManageGlobalNotification from '../components/Admin/Notification/ManageGlobalNotification';
 import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
@@ -46,6 +46,7 @@ import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityC
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 
 import { appContainer, componentMappings } from './base';
 
@@ -65,6 +66,7 @@ const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
 const adminUsersContainer = new AdminUsersContainer(appContainer);
 const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
 const adminNotificationContainer = new AdminNotificationContainer(appContainer);
+const adminSlackIntegrationLegacyContainer = new AdminSlackIntegrationLegacyContainer(appContainer);
 const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
 const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appContainer);
 const injectableContainers = [
@@ -78,7 +80,7 @@ const injectableContainers = [
   adminUsersContainer,
   adminExternalAccountsContainer,
   adminNotificationContainer,
-  adminNotificationContainer,
+  adminSlackIntegrationLegacyContainer,
   adminMarkDownContainer,
   adminUserGroupDetailContainer,
 ];
@@ -99,7 +101,7 @@ Object.assign(componentMappings, {
   'admin-export-page': <ExportArchiveDataPage />,
   'admin-notification-setting': <NotificationSetting />,
   'admin-slack-integration': <SlackIntegration />,
-  'admin-slack-integration-notification-setting': <SlackIntegrationNotificationSetting />,
+  'admin-slack-integration-legacy': <LegacySlackIntegration />,
   'admin-global-notification-setting': <ManageGlobalNotification />,
   'admin-user-page': <UserManagement />,
   'admin-external-account-setting': <ManageExternalAccount />,

+ 20 - 1
packages/app/src/client/models/Linker.js

@@ -1,3 +1,8 @@
+
+import { pagePathUtils } from '@growi/core';
+
+const { encodeSpaces } = pagePathUtils;
+
 export default class Linker {
 
   constructor(
@@ -5,10 +10,15 @@ export default class Linker {
       label = '',
       link = '',
   ) {
+
     this.type = type;
     this.label = label;
     this.link = link;
 
+    if (type === Linker.types.markdownLink) {
+      this.initWhenMarkdownLink();
+    }
+
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
   }
 
@@ -25,9 +35,18 @@ export default class Linker {
     markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
   }
 
+  initWhenMarkdownLink() {
+    // fill label with link if empty
+    if (this.label === '') {
+      this.label = this.link;
+    }
+    // encode spaces
+    this.link = encodeSpaces(this.link);
+  }
+
   generateMarkdownText() {
     if (this.type === Linker.types.pukiwikiLink) {
-      if (this.label === this.link) return `[[${this.link}]]`;
+      if (this.label === '') return `[[${this.link}]]`;
       return `[[${this.label}>${this.link}]]`;
     }
     if (this.type === Linker.types.growiLink) {

+ 34 - 0
packages/app/src/client/nologin.jsx

@@ -9,6 +9,8 @@ import AppContainer from '~/client/services/AppContainer';
 
 import InstallerForm from '../components/InstallerForm';
 import LoginForm from '../components/LoginForm';
+import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
+import PasswordResetExecutionForm from '../components/PasswordResetExecutionForm';
 
 const i18n = i18nFactory();
 
@@ -38,6 +40,7 @@ if (loginFormElem) {
   const email = loginFormElem.dataset.email;
   const isRegistrationEnabled = loginFormElem.dataset.isRegistrationEnabled === 'true';
   const registrationMode = loginFormElem.dataset.registrationMode;
+  const isPasswordResetEnabled = loginFormElem.dataset.isPasswordResetEnabled === 'true';
 
 
   let registrationWhiteList = loginFormElem.dataset.registrationWhiteList;
@@ -68,6 +71,7 @@ if (loginFormElem) {
           isRegistrationEnabled={isRegistrationEnabled}
           registrationMode={registrationMode}
           registrationWhiteList={registrationWhiteList}
+          isPasswordResetEnabled={isPasswordResetEnabled}
           isLocalStrategySetup={isLocalStrategySetup}
           isLdapStrategySetup={isLdapStrategySetup}
           objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
@@ -77,3 +81,33 @@ if (loginFormElem) {
     loginFormElem,
   );
 }
+
+// render PasswordResetRequestForm
+const passwordResetRequestFormElem = document.getElementById('password-reset-request-form');
+const appContainer = new AppContainer();
+appContainer.initApp();
+if (passwordResetRequestFormElem) {
+
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <Provider inject={[appContainer]}>
+        <PasswordResetRequestForm />
+      </Provider>
+    </I18nextProvider>,
+    passwordResetRequestFormElem,
+  );
+}
+
+// render PasswordResetExecutionForm
+const passwordResetExecutionFormElem = document.getElementById('password-reset-execution-form');
+if (passwordResetExecutionFormElem) {
+
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <Provider inject={[appContainer]}>
+        <PasswordResetExecutionForm />
+      </Provider>
+    </I18nextProvider>,
+    passwordResetExecutionFormElem,
+  );
+}

+ 4 - 2
packages/app/src/client/plugin.js

@@ -29,15 +29,17 @@ export default class GrowiPlugin {
       const meta = definition.meta;
 
       switch (meta.pluginSchemaVersion) {
-        // v1 is deprecated
+        // v1, v2 and v3 is deprecated
         case 1:
           logger.warn('pluginSchemaVersion 1 is deprecated', definition);
           break;
-        // v2 is deprecated
         case 2:
           logger.warn('pluginSchemaVersion 2 is deprecated', definition);
           break;
         case 3:
+          logger.warn('pluginSchemaVersion 2 is deprecated', definition);
+          break;
+        case 4:
           definition.entries.forEach((entry) => {
             entry(appContainer);
           });

+ 1 - 1
packages/app/src/client/services/AdminGitHubSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminGoogleSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

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

@@ -18,12 +18,19 @@ export default class AdminHomeContainer extends Container {
 
     this.appContainer = appContainer;
 
+    this.copyStateValues = {
+      DEFAULT: 'default',
+      DONE: 'done',
+    };
+    this.timer = null;
+
     this.state = {
       retrieveError: null,
       growiVersion: '',
       nodeVersion: '',
       npmVersion: '',
       yarnVersion: '',
+      copyState: this.copyStateValues.DEFAULT,
       installedPlugins: [],
     };
 
@@ -36,6 +43,10 @@ export default class AdminHomeContainer extends Container {
     return 'AdminHomeContainer';
   }
 
+  componentWillUnmount() {
+    clearTimeout(this.timer);
+  }
+
   /**
    * retrieve admin home data
    */
@@ -44,14 +55,15 @@ export default class AdminHomeContainer extends Container {
       const response = await this.appContainer.apiv3.get('/admin-home/');
       const { adminHomeParams } = response.data;
 
-      this.setState({
+      this.setState(prevState => ({
+        ...prevState,
         growiVersion: adminHomeParams.growiVersion,
         nodeVersion: adminHomeParams.nodeVersion,
         npmVersion: adminHomeParams.npmVersion,
         yarnVersion: adminHomeParams.yarnVersion,
         installedPlugins: adminHomeParams.installedPlugins,
         envVars: adminHomeParams.envVars,
-      });
+      }));
     }
     catch (err) {
       logger.error(err);
@@ -59,4 +71,40 @@ export default class AdminHomeContainer extends Container {
     }
   }
 
+  /**
+   * sets button text when copying system information
+   */
+  onCopyPrefilledHostInformation() {
+    this.setState(prevState => ({
+      ...prevState,
+      copyState: this.copyStateValues.DONE,
+    }));
+
+    this.timer = setTimeout(() => {
+      this.setState(prevState => ({
+        ...prevState,
+        copyState: this.copyStateValues.DEFAULT,
+      }));
+    }, 500);
+  }
+
+  /**
+   * generates prefilled host information as markdown
+   */
+  generatePrefilledHostInformationMarkdown() {
+    return `| item     | version |
+| ---      | --- |
+|OS        ||
+|GROWI     |${this.state.growiVersion}|
+|node.js   |${this.state.nodeVersion}|
+|npm       |${this.state.npmVersion}|
+|yarn      |${this.state.yarnVersion}|
+|Using Docker|yes/no|
+|Using [growi-docker-compose][growi-docker-compose]|yes/no|
+
+[growi-docker-compose]: https://github.com/weseek/growi-docker-compose
+
+*(Accessing https://{GROWI_HOST}/admin helps you to fill in above versions)*`;
+  }
+
 }

+ 12 - 1
packages/app/src/client/services/AdminLocalSecurityContainer.js

@@ -22,6 +22,7 @@ export default class AdminLocalSecurityContainer extends Container {
       registrationMode: this.dummyRegistrationMode,
       registrationWhiteList: [],
       useOnlyEnvVars: false,
+      isPasswordResetEnabled: false,
     };
 
   }
@@ -34,6 +35,7 @@ export default class AdminLocalSecurityContainer extends Container {
         useOnlyEnvVars: localSetting.useOnlyEnvVarsForSomeOptions,
         registrationMode: localSetting.registrationMode,
         registrationWhiteList: localSetting.registrationWhiteList,
+        isPasswordResetEnabled: localSetting.isPasswordResetEnabled,
       });
     }
     catch (err) {
@@ -66,14 +68,22 @@ export default class AdminLocalSecurityContainer extends Container {
     this.setState({ registrationWhiteList: value.split('\n') });
   }
 
+  /**
+   * Switch password reset enabled
+   */
+  switchIsPasswordResetEnabled() {
+    this.setState({ isPasswordResetEnabled: !this.state.isPasswordResetEnabled });
+  }
+
   /**
    * update local security setting
    */
   async updateLocalSecuritySetting() {
-    const { registrationWhiteList } = this.state;
+    const { registrationWhiteList, isPasswordResetEnabled } = this.state;
     const response = await this.appContainer.apiv3.put('/security-setting/local-setting', {
       registrationMode: this.state.registrationMode,
       registrationWhiteList,
+      isPasswordResetEnabled,
     });
 
     const { localSettingParams } = response.data;
@@ -81,6 +91,7 @@ export default class AdminLocalSecurityContainer extends Container {
     this.setState({
       registrationMode: localSettingParams.registrationMode,
       registrationWhiteList: localSettingParams.registrationWhiteList,
+      isPasswordResetEnabled: localSettingParams.isPasswordResetEnabled,
     });
 
     return localSettingParams;

+ 9 - 37
packages/app/src/client/services/AdminNotificationContainer.js

@@ -10,15 +10,14 @@ export default class AdminNotificationContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyWebhookUrl = 0;
-    this.dummyWebhookUrlForError = 1;
 
     this.state = {
       retrieveError: null,
-      selectSlackOption: 'Incoming Webhooks',
-      webhookUrl: this.dummyWebhookUrl,
-      isIncomingWebhookPrioritized: false,
-      slackToken: '',
+
+      isSlackbotConfigured: null,
+      isSlackLegacyConfigured: null,
+      currentBotType: null,
+
       userNotifications: [],
       isNotificationForOwnerPageEnabled: false,
       isNotificationForGroupPageEnabled: false,
@@ -42,9 +41,10 @@ export default class AdminNotificationContainer extends Container {
     const { notificationParams } = response.data;
 
     this.setState({
-      webhookUrl: notificationParams.webhookUrl,
-      isIncomingWebhookPrioritized: notificationParams.isIncomingWebhookPrioritized,
-      slackToken: notificationParams.slackToken,
+      isSlackbotConfigured: notificationParams.isSlackbotConfigured,
+      isSlackLegacyConfigured: notificationParams.isSlackLegacyConfigured,
+      currentBotType: notificationParams.currentBotType,
+
       userNotifications: notificationParams.userNotifications,
       isNotificationForOwnerPageEnabled: notificationParams.isNotificationForOwnerPageEnabled,
       isNotificationForGroupPageEnabled: notificationParams.isNotificationForGroupPageEnabled,
@@ -52,34 +52,6 @@ export default class AdminNotificationContainer extends Container {
     });
   }
 
-  /**
-   * Switch slackOption
-   */
-  switchSlackOption(slackOption) {
-    this.setState({ selectSlackOption: slackOption });
-  }
-
-  /**
-   * Change webhookUrl
-   */
-  changeWebhookUrl(webhookUrl) {
-    this.setState({ webhookUrl });
-  }
-
-  /**
-   * Switch incomingWebhookPrioritized
-   */
-  switchIsIncomingWebhookPrioritized() {
-    this.setState({ isIncomingWebhookPrioritized: !this.state.isIncomingWebhookPrioritized });
-  }
-
-  /**
-   * Change slackToken
-   */
-  changeSlackToken(slackToken) {
-    this.setState({ slackToken });
-  }
-
   /**
    * Update slackAppConfiguration
    * @memberOf SlackAppConfiguration

+ 1 - 1
packages/app/src/client/services/AdminOidcSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminSamlSecurityContainer.js

@@ -1,7 +1,7 @@
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 91 - 0
packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js

@@ -0,0 +1,91 @@
+import { Container } from 'unstated';
+
+/**
+ * Service container for admin LegacySlackIntegration setting page (LegacySlackIntegration.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminSlackIntegrationLegacyContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+    this.dummyWebhookUrl = 0;
+    this.dummyWebhookUrlForError = 1;
+
+    this.state = {
+      isSlackbotConfigured: false,
+      retrieveError: null,
+      selectSlackOption: 'Incoming Webhooks',
+      webhookUrl: this.dummyWebhookUrl,
+      isIncomingWebhookPrioritized: false,
+      slackToken: '',
+    };
+
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminSlackIntegrationLegacyContainer';
+  }
+
+  /**
+   * Retrieve notificationData
+   */
+  async retrieveData() {
+    const response = await this.appContainer.apiv3.get('/slack-integration-legacy-settings/');
+    const { slackIntegrationParams } = response.data;
+
+    this.setState({
+      isSlackbotConfigured: slackIntegrationParams.isSlackbotConfigured,
+      webhookUrl: slackIntegrationParams.webhookUrl,
+      isIncomingWebhookPrioritized: slackIntegrationParams.isIncomingWebhookPrioritized,
+      slackToken: slackIntegrationParams.slackToken,
+    });
+  }
+
+  /**
+   * Switch slackOption
+   */
+  switchSlackOption(slackOption) {
+    this.setState({ selectSlackOption: slackOption });
+  }
+
+  /**
+   * Change webhookUrl
+   */
+  changeWebhookUrl(webhookUrl) {
+    this.setState({ webhookUrl });
+  }
+
+  /**
+   * Switch incomingWebhookPrioritized
+   */
+  switchIsIncomingWebhookPrioritized() {
+    this.setState({ isIncomingWebhookPrioritized: !this.state.isIncomingWebhookPrioritized });
+  }
+
+  /**
+   * Change slackToken
+   */
+  changeSlackToken(slackToken) {
+    this.setState({ slackToken });
+  }
+
+  /**
+   * Update slackAppConfiguration
+   * @memberOf SlackAppConfiguration
+   */
+  async updateSlackAppConfiguration() {
+    const response = await this.appContainer.apiv3.put('/slack-integration-legacy-settings/', {
+      webhookUrl: this.state.webhookUrl,
+      isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
+      slackToken: this.state.slackToken,
+    });
+
+    return response;
+  }
+
+}

+ 1 - 1
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 3 - 1
packages/app/src/client/services/PageContainer.js

@@ -3,8 +3,8 @@ import { Container } from 'unstated';
 
 import * as entities from 'entities';
 import * as toastr from 'toastr';
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
-import { isTrashPage } from '~/utils/path-utils';
 import { toastError } from '../util/apiNotification';
 
 import {
@@ -16,6 +16,8 @@ import {
   DrawioInterceptor,
 } from '../util/interceptor/drawio-interceptor';
 
+const { isTrashPage } = pagePathUtils;
+
 const logger = loggerFactory('growi:services:PageContainer');
 
 /**

+ 5 - 1
packages/app/src/client/util/apiNotification.js

@@ -34,8 +34,12 @@ const toastrOption = {
 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, header, option);
+    toastr.error(err.message || err, header, option);
   }
 };
 

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

@@ -1,6 +1,7 @@
 import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
 import loggerFactory from '~/utils/logger';
 
 import { toastError } from '~/client/util/apiNotification';
@@ -63,6 +64,26 @@ class AdminHome extends React.Component {
             {adminHomeContainer.state.envVars && <EnvVarsTable envVars={adminHomeContainer.state.envVars} />}
           </div>
         </div>
+
+        <div className="row mb-5">
+          <div className="col-md-12">
+            <h2 className="admin-setting-header">{t('admin:admin_top.bug_report')}</h2>
+            <p>
+              <CopyToClipboard
+                text={adminHomeContainer.generatePrefilledHostInformationMarkdown()}
+                onCopy={() => adminHomeContainer.onCopyPrefilledHostInformation()}
+              >
+                <button type="button" className="btn btn-primary">
+                  {adminHomeContainer.state.copyState === adminHomeContainer.copyStateValues.DEFAULT
+                    ? t('admin:admin_top:copy_prefilled_host_information:default')
+                    : t('admin:admin_top:copy_prefilled_host_information:done')}
+                </button>
+              </CopyToClipboard>{' '}
+              {/* eslint-disable-next-line react/no-danger */}
+              <span dangerouslySetInnerHTML={{ __html: t('admin:admin_top:submit_bug_report') }} />
+            </p>
+          </div>
+        </div>
       </Fragment>
     );
   }

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

@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';

+ 71 - 0
packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -0,0 +1,71 @@
+import React, { useMemo, useState } from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
+import loggerFactory from '~/utils/logger';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
+import { withLoadingSppiner } from '../../SuspenseUtils';
+
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
+
+import SlackConfiguration from './SlackConfiguration';
+
+const logger = loggerFactory('growi:NotificationSetting');
+
+let retrieveErrors = null;
+function LegacySlackIntegration(props) {
+  const { t } = useTranslation();
+  const { adminSlackIntegrationLegacyContainer } = props;
+
+  if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrl) {
+    throw (async() => {
+      try {
+        await adminSlackIntegrationLegacyContainer.retrieveData();
+      }
+      catch (err) {
+        const errs = toArrayIfNot(err);
+        toastError(errs);
+        logger.error(errs);
+        retrieveErrors = errs;
+        adminSlackIntegrationLegacyContainer.setState({ webhookUrl: adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError });
+      }
+    })();
+  }
+
+  if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError) {
+    throw new Error(`${retrieveErrors.length} errors occured`);
+  }
+
+  const isDisabled = adminSlackIntegrationLegacyContainer.state.isSlackbotConfigured;
+
+  return (
+    <>
+      { isDisabled && (
+        <div className="alert alert-danger">
+          <i className="icon-minus icon-fw"></i>
+          {/* eslint-disable-next-line react/no-danger */}
+          <span dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_disabled') }}></span>
+        </div>
+      ) }
+
+      <div className="alert alert-warning">
+        <i className="icon-info icon-fw"></i>
+        {/* eslint-disable-next-line react/no-danger */}
+        <span dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_deplicated') }}></span>
+      </div>
+
+      <SlackConfiguration />
+    </>
+  );
+}
+
+const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LegacySlackIntegration), [AdminSlackIntegrationLegacyContainer]);
+
+LegacySlackIntegration.propTypes = {
+  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,
+};
+
+export default LegacySlackIntegrationWithUnstatedContainer;

+ 21 - 21
packages/app/src/components/Admin/Notification/SlackAppConfiguration.jsx → packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -8,12 +8,12 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import AppContainer from '~/client/services/AppContainer';
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:slackAppConfiguration');
 
-class SlackAppConfiguration extends React.Component {
+class SlackConfiguration extends React.Component {
 
   constructor(props) {
     super(props);
@@ -22,10 +22,10 @@ class SlackAppConfiguration extends React.Component {
   }
 
   async onClickSubmit() {
-    const { t, adminNotificationContainer } = this.props;
+    const { t, adminSlackIntegrationLegacyContainer } = this.props;
 
     try {
-      await adminNotificationContainer.updateSlackAppConfiguration();
+      await adminSlackIntegrationLegacyContainer.updateSlackAppConfiguration();
       toastSuccess(t('notification_setting.updated_slackApp'));
     }
     catch (err) {
@@ -35,7 +35,7 @@ class SlackAppConfiguration extends React.Component {
   }
 
   render() {
-    const { t, adminNotificationContainer } = this.props;
+    const { t, adminSlackIntegrationLegacyContainer } = this.props;
 
     return (
       <React.Fragment>
@@ -50,18 +50,18 @@ class SlackAppConfiguration extends React.Component {
                 aria-haspopup="true"
                 aria-expanded="true"
               >
-                {`Slack ${adminNotificationContainer.state.selectSlackOption}`}
+                {`Slack ${adminSlackIntegrationLegacyContainer.state.selectSlackOption}`}
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                <button className="dropdown-item" type="button" onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}>
+                <button className="dropdown-item" type="button" onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}>
                   Slack Incoming Webhooks
                 </button>
-                <button className="dropdown-item" type="button" onClick={() => adminNotificationContainer.switchSlackOption('App')}>Slack App</button>
+                <button className="dropdown-item" type="button" onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('App')}>Slack App</button>
               </div>
             </div>
           </div>
         </div>
-        {adminNotificationContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
+        {adminSlackIntegrationLegacyContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
           <React.Fragment>
             <h2 className="border-bottom mb-5">{t('notification_setting.slack_incoming_configuration')}</h2>
 
@@ -71,8 +71,8 @@ class SlackAppConfiguration extends React.Component {
                 <input
                   className="form-control"
                   type="text"
-                  defaultValue={adminNotificationContainer.state.webhookUrl || ''}
-                  onChange={e => adminNotificationContainer.changeWebhookUrl(e.target.value)}
+                  defaultValue={adminSlackIntegrationLegacyContainer.state.webhookUrl || ''}
+                  onChange={e => adminSlackIntegrationLegacyContainer.changeWebhookUrl(e.target.value)}
                 />
               </div>
             </div>
@@ -84,8 +84,8 @@ class SlackAppConfiguration extends React.Component {
                     type="checkbox"
                     className="custom-control-input"
                     id="cbPrioritizeIWH"
-                    checked={adminNotificationContainer.state.isIncomingWebhookPrioritized || false}
-                    onChange={() => { adminNotificationContainer.switchIsIncomingWebhookPrioritized() }}
+                    checked={adminSlackIntegrationLegacyContainer.state.isIncomingWebhookPrioritized || false}
+                    onChange={() => { adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized() }}
                   />
                   <label className="custom-control-label" htmlFor="cbPrioritizeIWH">
                     {t('notification_setting.prioritize_webhook')}
@@ -111,7 +111,7 @@ class SlackAppConfiguration extends React.Component {
                 <a
                   href="#slack-incoming-webhooks"
                   data-toggle="tab"
-                  onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}
+                  onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}
                 >
                   {t('notification_setting.use_instead')}
                 </a>
@@ -123,8 +123,8 @@ class SlackAppConfiguration extends React.Component {
                   <input
                     className="form-control"
                     type="text"
-                    defaultValue={adminNotificationContainer.state.slackToken || ''}
-                    onChange={e => adminNotificationContainer.changeSlackToken(e.target.value)}
+                    defaultValue={adminSlackIntegrationLegacyContainer.state.slackToken || ''}
+                    onChange={e => adminSlackIntegrationLegacyContainer.changeSlackToken(e.target.value)}
                   />
                 </div>
               </div>
@@ -135,7 +135,7 @@ class SlackAppConfiguration extends React.Component {
 
         <AdminUpdateButtonRow
           onClick={this.onClickSubmit}
-          disabled={adminNotificationContainer.state.retrieveError != null}
+          disabled={adminSlackIntegrationLegacyContainer.state.retrieveError != null}
         />
 
         <hr />
@@ -170,13 +170,13 @@ class SlackAppConfiguration extends React.Component {
 
 }
 
-const SlackAppConfigurationWrapper = withUnstatedContainers(SlackAppConfiguration, [AppContainer, AdminNotificationContainer]);
+const SlackConfigurationWrapper = withUnstatedContainers(SlackConfiguration, [AppContainer, AdminSlackIntegrationLegacyContainer]);
 
-SlackAppConfiguration.propTypes = {
+SlackConfiguration.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,
 
 };
 
-export default withTranslation()(SlackAppConfigurationWrapper);
+export default withTranslation()(SlackConfigurationWrapper);

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

@@ -1,7 +1,15 @@
-import React, { useMemo, useState } from 'react';
+import React, {
+  useCallback, useEffect, useMemo, useState,
+} from 'react';
 import PropTypes from 'prop-types';
 
-import { TabContent, TabPane } from 'reactstrap';
+import {
+  TabContent, TabPane,
+} from 'reactstrap';
+import { useTranslation } from 'react-i18next';
+
+import { SlackbotType } from '@growi/slack';
+
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -19,9 +27,76 @@ import GlobalNotification from './GlobalNotification';
 const logger = loggerFactory('growi:NotificationSetting');
 
 let retrieveErrors = null;
+
+
+// eslint-disable-next-line react/prop-types
+const Badge = ({ isEnabled }) => {
+  const { t } = useTranslation();
+
+  return isEnabled
+    ? <span className="badge badge-success">{t('admin:external_notification.enabled')}</span>
+    : <span className="badge badge-secondary">{t('admin:external_notification.disabled')}</span>;
+};
+
+const SkeltonListItem = () => (
+  <li className="list-group-item">
+    <h4 className="mb-2">
+      <span className="badge badge-secondary">――</span>
+      <span className="ml-2">...</span>
+    </h4>
+  </li>
+);
+
+// eslint-disable-next-line react/prop-types
+const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
+  const { t } = useTranslation();
+
+  const isCautionVisible = currentBotType === SlackbotType.OFFICIAL || currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
+
+  return (
+    <li className="list-group-item">
+      <h4>
+        <Badge isEnabled={isEnabled} />
+        <a href="/admin/slack-integration" className="ml-2">{t('slack_integration')}</a>
+      </h4>
+      { isCautionVisible && (
+        <ul className="mt-2 pl-4">
+          {/* eslint-disable-next-line react/no-danger */}
+          <li dangerouslySetInnerHTML={{ __html: t('admin:external_notification.caution_enabled') }} />
+        </ul>
+      ) }
+    </li>
+  );
+};
+
+// eslint-disable-next-line react/prop-types
+const LegacySlackIntegrationListItem = ({ isEnabled }) => {
+  const { t } = useTranslation();
+
+  return (
+    <li className="list-group-item">
+      <h4>
+        <Badge isEnabled={isEnabled} />
+        <a href="/admin/slack-integration-legacy" className="ml-2">{t('legacy_slack_integration')}</a>
+      </h4>
+      { isEnabled && (
+        <ul className="mt-2 pl-4">
+          <li>
+            {/* eslint-disable-next-line react/no-danger */}
+            <span className="text-danger" dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_deplicated') }}></span>
+          </li>
+        </ul>
+      ) }
+    </li>
+  );
+};
+
 function NotificationSetting(props) {
   const { adminNotificationContainer } = props;
 
+  const { t } = useTranslation();
+
+  const [isMounted, setMounted] = useState(false);
   const [activeTab, setActiveTab] = useState('user_trigger_notification');
   const [activeComponents, setActiveComponents] = useState(new Set(['user_trigger_notification']));
 
@@ -30,24 +105,24 @@ function NotificationSetting(props) {
     setActiveComponents(activeComponents.add(selectedTab));
   };
 
-  if (adminNotificationContainer.state.webhookUrl === adminNotificationContainer.dummyWebhookUrl) {
-    throw (async() => {
-      try {
-        await adminNotificationContainer.retrieveNotificationData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminNotificationContainer.setState({ webhookUrl: adminNotificationContainer.dummyWebhookUrlForError });
-      }
-    })();
-  }
-
-  if (adminNotificationContainer.state.webhookUrl === adminNotificationContainer.dummyWebhookUrlForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+  const fetchData = useCallback(async() => {
+    try {
+      await adminNotificationContainer.retrieveNotificationData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+      retrieveErrors = errs;
+    }
+    finally {
+      setMounted(true);
+    }
+  }, [adminNotificationContainer]);
+
+  useEffect(() => {
+    fetchData();
+  }, [fetchData]);
 
   const navTabMapping = useMemo(() => {
     return {
@@ -64,8 +139,27 @@ function NotificationSetting(props) {
     };
   }, []);
 
+  const { isSlackbotConfigured, isSlackLegacyConfigured, currentBotType } = adminNotificationContainer.state;
+  const isSlackEnabled = isSlackbotConfigured;
+  const isSlackLegacyEnabled = !isSlackbotConfigured && isSlackLegacyConfigured;
+
   return (
     <>
+      <h2 className="admin-setting-header">{t('admin:external_notification.header_status')}</h2>
+      <ul className="list-group">
+        { !isMounted && <SkeltonListItem />}
+        { isMounted && (
+          <>
+            <SlackIntegrationListItem isEnabled={isSlackEnabled} currentBotType={currentBotType} />
+            {/* Legacy Slack Integration become visible only when new Slack Integration is disabled */}
+            { !isSlackEnabled && <LegacySlackIntegrationListItem isEnabled={isSlackLegacyEnabled} /> }
+          </>
+        ) }
+      </ul>
+
+
+      <h2 className="admin-setting-header mt-5">{t('Notification Settings')}</h2>
+
       <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
 
       <TabContent activeTab={activeTab} className="p-5">

+ 0 - 80
packages/app/src/components/Admin/Notification/SlackIntegrationNotificationSetting.jsx

@@ -1,80 +0,0 @@
-import React, { useMemo, useState } from 'react';
-import PropTypes from 'prop-types';
-
-import { TabContent, TabPane } from 'reactstrap';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
-
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-
-import { CustomNavTab } from '../../CustomNavigation/CustomNav';
-
-import SlackAppConfiguration from './SlackAppConfiguration';
-
-const logger = loggerFactory('growi:NotificationSetting');
-
-let retrieveErrors = null;
-function NotificationSetting(props) {
-  const { adminNotificationContainer } = props;
-
-  const [activeTab, setActiveTab] = useState('slack_configuration');
-  const [activeComponents, setActiveComponents] = useState(new Set(['slack_configuration']));
-
-  const switchActiveTab = (selectedTab) => {
-    setActiveTab(selectedTab);
-    setActiveComponents(activeComponents.add(selectedTab));
-  };
-
-  if (adminNotificationContainer.state.webhookUrl === adminNotificationContainer.dummyWebhookUrl) {
-    throw (async() => {
-      try {
-        await adminNotificationContainer.retrieveNotificationData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminNotificationContainer.setState({ webhookUrl: adminNotificationContainer.dummyWebhookUrlForError });
-      }
-    })();
-  }
-
-  if (adminNotificationContainer.state.webhookUrl === adminNotificationContainer.dummyWebhookUrlForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
-
-  const navTabMapping = useMemo(() => {
-    return {
-      slack_configuration: {
-        Icon: () => <i className="icon-settings" />,
-        i18n: 'Slack configuration',
-        index: 0,
-      },
-    };
-  }, []);
-
-  return (
-    <>
-      <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
-
-      <TabContent activeTab={activeTab} className="p-5">
-        <TabPane tabId="slack_configuration">
-          {activeComponents.has('slack_configuration') && <SlackAppConfiguration />}
-        </TabPane>
-      </TabContent>
-    </>
-  );
-}
-
-const NotificationSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(NotificationSetting), [AdminNotificationContainer]);
-
-NotificationSetting.propTypes = {
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
-};
-
-export default NotificationSettingWithUnstatedContainer;

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

@@ -32,7 +32,7 @@ class LocalSecuritySettingContents extends React.Component {
 
   render() {
     const { t, adminGeneralSecurityContainer, adminLocalSecurityContainer } = this.props;
-    const { registrationMode } = adminLocalSecurityContainer.state;
+    const { registrationMode, isPasswordResetEnabled } = adminLocalSecurityContainer.state;
     const { isLocalEnabled } = adminGeneralSecurityContainer.state;
 
     return (
@@ -157,6 +157,27 @@ class LocalSecuritySettingContents extends React.Component {
               </div>
             </div>
 
+            <div className="row">
+              <label className="col-12 col-md-3 text-left text-md-right  col-form-label">{t('security_setting.Local.password_reset_by_users')}</label>
+              <div className="col-12 col-md-6">
+                <div className="custom-control custom-switch custom-checkbox-success">
+                  <input
+                    type="checkbox"
+                    className="custom-control-input"
+                    id="isPasswordResetEnabled"
+                    checked={isPasswordResetEnabled}
+                    onChange={() => adminLocalSecurityContainer.switchIsPasswordResetEnabled()}
+                  />
+                  <label className="custom-control-label" htmlFor="isPasswordResetEnabled">
+                    {t('security_setting.Local.enable_password_reset_by_users')}
+                  </label>
+                </div>
+                <p className="form-text text-muted small">
+                  {t('security_setting.Local.password_reset_desc')}
+                </p>
+              </div>
+            </div>
+
             <div className="row my-3">
               <div className="offset-3 col-6">
                 <button

+ 8 - 5
packages/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx

@@ -2,17 +2,18 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 
+import { SlackbotType } from '@growi/slack';
 
 const botDetails = {
   officialBot: {
-    botType: 'officialBot',
+    botType: SlackbotType.OFFICIAL,
     botTypeCategory: 'official_bot',
     setUp: 'easy',
     multiWSIntegration: 'possible',
     securityControl: 'impossible',
   },
   customBotWithoutProxy: {
-    botType: 'customBotWithoutProxy',
+    botType: SlackbotType.CUSTOM_WITHOUT_PROXY,
     botTypeCategory: 'custom_bot',
     supplementaryBotName: 'without_proxy',
     setUp: 'normal',
@@ -20,7 +21,7 @@ const botDetails = {
     securityControl: 'possible',
   },
   customBotWithProxy: {
-    botType: 'customBotWithProxy',
+    botType: SlackbotType.CUSTOM_WITH_PROXY,
     botTypeCategory: 'custom_bot',
     supplementaryBotName: 'with_proxy',
     setUp: 'hard',
@@ -32,6 +33,8 @@ const botDetails = {
 const BotTypeCard = (props) => {
   const { t } = useTranslation('admin');
 
+  const isBotTypeOfficial = props.botType === SlackbotType.OFFICIAL;
+
   return (
     <div
       className={`card admin-bot-card rounded border-radius-sm shadow ${props.isActive ? 'border-primary' : ''}`}
@@ -41,7 +44,7 @@ const BotTypeCard = (props) => {
     >
       <div>
         <h3 className={`card-header mb-0 py-3
-              ${props.botType === 'officialBot' ? 'd-flex align-items-center justify-content-center' : 'text-center'}
+              ${isBotTypeOfficial ? 'd-flex align-items-center justify-content-center' : 'text-center'}
               ${props.isActive ? 'bg-primary grw-botcard-title-active' : ''}`}
         >
           <span className="mr-2">
@@ -49,7 +52,7 @@ const BotTypeCard = (props) => {
           </span>
 
           {/*  A recommended badge is shown on official bot card, supplementary names are shown on Custom bot cards   */}
-          {props.botType === 'officialBot'
+          { isBotTypeOfficial
             ? (
               <span className="badge badge-info mr-2">
                 {t('admin:slack_integration.selecting_bot_types.recommended')}

+ 39 - 17
packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -1,6 +1,7 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import PropTypes from 'prop-types';
+
 import loggerFactory from '~/utils/logger';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -8,12 +9,15 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import WithProxyAccordions from './WithProxyAccordions';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 
-const logger = loggerFactory('growi:SlackBotSettings');
+const logger = loggerFactory('growi:cli:SlackIntegration:CustomBotWithProxySettings');
 
 const CustomBotWithProxySettings = (props) => {
   const {
-    appContainer, slackAppIntegrations, proxyServerUri, onClickAddSlackWorkspaceBtn, connectionStatuses, onUpdateTokens, onSubmitForm,
+    appContainer, slackAppIntegrations, proxyServerUri,
+    onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
+    connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   const [newProxyServerUri, setNewProxyServerUri] = useState();
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
@@ -31,17 +35,36 @@ const CustomBotWithProxySettings = (props) => {
     }
   };
 
+  const isPrimaryChangedHandler = useCallback(async(slackIntegrationToChange, newValue) => {
+    // do nothing when turning off
+    if (!newValue) {
+      return;
+    }
+
+    try {
+      await appContainer.apiv3.put(`/slack-integration-settings/slack-app-integrations/${slackIntegrationToChange._id}/make-primary`);
+      if (onPrimaryUpdated != null) {
+        onPrimaryUpdated();
+      }
+      toastSuccess(t('toaster.update_successed', { target: 'Primary' }));
+    }
+    catch (err) {
+      toastError(err, 'Failed to change isPrimary');
+      logger.error('Failed to change isPrimary', err);
+    }
+  }, [appContainer.apiv3, t, onPrimaryUpdated]);
+
   const deleteSlackAppIntegrationHandler = async() => {
     try {
-      await appContainer.apiv3.delete('/slack-integration-settings/slack-app-integration', { integrationIdToDelete });
+      await appContainer.apiv3.delete(`/slack-integration-settings/slack-app-integrations/${integrationIdToDelete}`);
       if (props.onDeleteSlackAppIntegration != null) {
         props.onDeleteSlackAppIntegration();
       }
-      toastSuccess(t('toaster.delete_slack_integration_procedure'));
+      toastSuccess(t('admin:slack_integration.toastr.delete_slack_integration_procedure'));
     }
     catch (err) {
-      toastError(err);
-      logger.error(err);
+      toastError(err, 'Failed to delete');
+      logger.error('Failed to delete', err);
     }
   };
 
@@ -53,8 +76,8 @@ const CustomBotWithProxySettings = (props) => {
       toastSuccess(t('toaster.update_successed', { target: 'Proxy URL' }));
     }
     catch (err) {
-      toastError(err);
-      logger.error(err);
+      toastError(err, 'Failed to update');
+      logger.error('Failed to update', err);
     }
   };
 
@@ -113,14 +136,12 @@ const CustomBotWithProxySettings = (props) => {
                 <h2 id={_id || `settings-accordions-${i}`}>
                   {(workspaceName != null) ? `${workspaceName} Work Space` : `Settings #${i}`}
                 </h2>
-                <button
-                  className="btn btn-outline-danger"
-                  type="button"
-                  onClick={() => setIntegrationIdToDelete(slackAppIntegration._id)}
-                >
-                  <i className="icon-trash mr-1" />
-                  {t('admin:slack_integration.delete')}
-                </button>
+                <SlackAppIntegrationControl
+                  slackAppIntegration={slackAppIntegration}
+                  onIsPrimaryChanged={isPrimaryChangedHandler}
+                  // set state to open DeleteSlackBotSettingsModal
+                  onDeleteButtonClicked={saiToDelete => setIntegrationIdToDelete(saiToDelete._id)}
+                />
               </div>
               <WithProxyAccordions
                 botType="customBotWithProxy"
@@ -168,6 +189,7 @@ CustomBotWithProxySettings.propTypes = {
   slackAppIntegrations: PropTypes.array,
   proxyServerUri: PropTypes.string,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
+  onPrimaryUpdated: PropTypes.func,
   onDeleteSlackAppIntegration: PropTypes.func,
   onSubmitForm: PropTypes.func,
   connectionStatuses: PropTypes.object.isRequired,

+ 3 - 3
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx

@@ -67,8 +67,8 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
             readOnly
           />
           <p className="form-text text-muted">
-            {/* eslint-disable-next-line react/no-danger */}
-            <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_SIGNING_SECRET' }) }} />
+            {/* eslint-disable-next-line max-len, react/no-danger */}
+            <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACKBOT_WITHOUT_PROXY_SIGNING_SECRET' }) }} />
           </p>
         </div>
 
@@ -97,7 +97,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
           />
           <p className="form-text text-muted">
             {/* eslint-disable-next-line react/no-danger */}
-            <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_BOT_TOKEN' }) }} />
+            <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACKBOT_WITHOUT_PROXY_BOT_TOKEN' }) }} />
           </p>
         </div>
 

+ 7 - 5
packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx

@@ -51,7 +51,7 @@ const ManageCommandsProcess = ({
 
   const updateCommandsHandler = async() => {
     try {
-      await apiv3Put(`/slack-integration-settings/${slackAppIntegrationId}/supported-commands`, {
+      await apiv3Put(`/slack-integration-settings/slack-app-integrations/${slackAppIntegrationId}/supported-commands`, {
         supportedCommandsForBroadcastUse: Array.from(selectedCommandsForBroadcastUse),
         supportedCommandsForSingleUse: Array.from(selectedCommandsForSingleUse),
       });
@@ -75,18 +75,19 @@ const ManageCommandsProcess = ({
           <div className="custom-control custom-checkbox">
             <div className="row mb-5">
               {defaultSupportedCommandsNameForBroadcastUse.map((commandName) => {
+                const checkboxId = `${commandName}-${slackAppIntegrationId}`;
                 return (
                   <div className="col-sm-6 my-1" key={commandName}>
                     <input
                       type="checkbox"
                       className="custom-control-input"
-                      id={commandName}
+                      id={checkboxId}
                       name={commandName}
                       value={commandName}
                       checked={selectedCommandsForBroadcastUse.has(commandName)}
                       onChange={toggleCheckboxForBroadcast}
                     />
-                    <label className="text-capitalize custom-control-label ml-3" htmlFor={commandName}>
+                    <label className="text-capitalize custom-control-label ml-3" htmlFor={checkboxId}>
                       {commandName}
                     </label>
                   </div>
@@ -100,18 +101,19 @@ const ManageCommandsProcess = ({
           <div className="custom-control custom-checkbox">
             <div className="row mb-5">
               {defaultSupportedCommandsNameForSingleUse.map((commandName) => {
+                const checkboxId = `${commandName}-${slackAppIntegrationId}`;
                 return (
                   <div className="col-sm-6 my-1" key={commandName}>
                     <input
                       type="checkbox"
                       className="custom-control-input"
-                      id={commandName}
+                      id={checkboxId}
                       name={commandName}
                       value={commandName}
                       checked={selectedCommandsForSingleUse.has(commandName)}
                       onChange={toggleCheckboxForSingleUse}
                     />
-                    <label className="text-capitalize custom-control-label ml-3" htmlFor={commandName}>
+                    <label className="text-capitalize custom-control-label ml-3" htmlFor={checkboxId}>
                       {commandName}
                     </label>
                   </div>

+ 40 - 16
packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -1,6 +1,9 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
+
+import { SlackbotType } from '@growi/slack';
+
 import loggerFactory from '~/utils/logger';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -8,12 +11,15 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import WithProxyAccordions from './WithProxyAccordions';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 
-const logger = loggerFactory('growi:SlackBotSettings');
+const logger = loggerFactory('growi:cli:SlackIntegration:OfficialBotSettings');
 
 const OfficialBotSettings = (props) => {
   const {
-    appContainer, slackAppIntegrations, onClickAddSlackWorkspaceBtn, connectionStatuses, onUpdateTokens, onSubmitForm,
+    appContainer, slackAppIntegrations,
+    onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
+    connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   const [siteName, setSiteName] = useState('');
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
@@ -25,17 +31,36 @@ const OfficialBotSettings = (props) => {
     }
   };
 
+  const isPrimaryChangedHandler = useCallback(async(slackIntegrationToChange, newValue) => {
+    // do nothing when turning off
+    if (!newValue) {
+      return;
+    }
+
+    try {
+      await appContainer.apiv3.put(`/slack-integration-settings/slack-app-integrations/${slackIntegrationToChange._id}/make-primary`);
+      if (onPrimaryUpdated != null) {
+        onPrimaryUpdated();
+      }
+      toastSuccess(t('toaster.update_successed', { target: 'Primary' }));
+    }
+    catch (err) {
+      toastError(err, 'Failed to change isPrimary');
+      logger.error('Failed to change isPrimary', err);
+    }
+  }, [appContainer.apiv3, t, onPrimaryUpdated]);
+
   const deleteSlackAppIntegrationHandler = async() => {
-    await appContainer.apiv3.delete('/slack-integration-settings/slack-app-integration', { integrationIdToDelete });
+    await appContainer.apiv3.delete(`/slack-integration-settings/slack-app-integrations/${integrationIdToDelete}`);
     try {
       if (props.onDeleteSlackAppIntegration != null) {
         props.onDeleteSlackAppIntegration();
       }
-      toastSuccess(t('toaster.delete_slack_integration_procedure'));
+      toastSuccess(t('admin:slack_integration.toastr.delete_slack_integration_procedure'));
     }
     catch (err) {
-      toastError(err);
-      logger.error(err);
+      toastError('Failed to delete');
+      logger.error('Failed to delete', err);
     }
   };
 
@@ -79,17 +104,15 @@ const OfficialBotSettings = (props) => {
                 <h2 id={_id || `settings-accordions-${i}`}>
                   {(workspaceName != null) ? `${workspaceName} Work Space` : `Settings #${i}`}
                 </h2>
-                <button
-                  className="btn btn-outline-danger"
-                  type="button"
-                  onClick={() => setIntegrationIdToDelete(slackAppIntegration._id)}
-                >
-                  <i className="icon-trash mr-1" />
-                  {t('admin:slack_integration.delete')}
-                </button>
+                <SlackAppIntegrationControl
+                  slackAppIntegration={slackAppIntegration}
+                  onIsPrimaryChanged={isPrimaryChangedHandler}
+                  // set state to open DeleteSlackBotSettingsModal
+                  onDeleteButtonClicked={saiToDelete => setIntegrationIdToDelete(saiToDelete._id)}
+                />
               </div>
               <WithProxyAccordions
-                botType="officialBot"
+                botType={SlackbotType.OFFICIAL}
                 slackAppIntegrationId={slackAppIntegration._id}
                 tokenGtoP={tokenGtoP}
                 tokenPtoG={tokenPtoG}
@@ -133,6 +156,7 @@ OfficialBotSettings.propTypes = {
 
   slackAppIntegrations: PropTypes.array,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
+  onPrimaryUpdated: PropTypes.func,
   onDeleteSlackAppIntegration: PropTypes.func,
   connectionStatuses: PropTypes.object.isRequired,
   onUpdateTokens: PropTypes.func,

+ 53 - 0
packages/app/src/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx

@@ -0,0 +1,53 @@
+import React, { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type Props = {
+  slackAppIntegration: {
+    _id: string,
+    isPrimary?: boolean,
+  },
+  onIsPrimaryChanged?: (slackAppIntegration: unknown, newValue: boolean) => void,
+  onDeleteButtonClicked?: (slackAppIntegration: unknown) => void,
+}
+
+export const SlackAppIntegrationControl: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
+
+  const { slackAppIntegration, onIsPrimaryChanged, onDeleteButtonClicked } = props;
+  const inputId = `cb-primary-${slackAppIntegration._id}`;
+  const isPrimary = slackAppIntegration.isPrimary === true;
+
+  return (
+    <div className="d-flex align-items-center">
+      <div className="my-1 custom-control custom-switch">
+        <input
+          className="custom-control-input"
+          id={inputId}
+          type="checkbox"
+          checked={isPrimary}
+          disabled={isPrimary}
+          onChange={(e) => {
+            if (onIsPrimaryChanged != null) {
+              onIsPrimaryChanged(slackAppIntegration, e.target.checked);
+            }
+          }}
+        />
+        <label className="custom-control-label" htmlFor={inputId}>
+          Primary
+        </label>
+      </div>
+      <button
+        className="btn btn-outline-danger ml-3"
+        type="button"
+        onClick={() => {
+          if (onDeleteButtonClicked != null) {
+            onDeleteButtonClicked(slackAppIntegration);
+          }
+        }}
+      >
+        <i className="icon-trash mr-1" />
+        {t('admin:slack_integration.delete')}
+      </button>
+    </div>
+  );
+};

+ 10 - 5
packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -1,6 +1,9 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
+
+import { SlackbotType } from '@growi/slack';
+
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
@@ -12,7 +15,7 @@ import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import BotTypeCard from './BotTypeCard';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 
-const botTypes = ['officialBot', 'customBotWithoutProxy', 'customBotWithProxy'];
+const botTypes = Object.values(SlackbotType);
 
 const SlackIntegration = (props) => {
 
@@ -72,7 +75,7 @@ const SlackIntegration = (props) => {
 
   const createSlackIntegrationData = async() => {
     try {
-      await appContainer.apiv3.put('/slack-integration-settings/slack-app-integrations');
+      await appContainer.apiv3.post('/slack-integration-settings/slack-app-integrations');
       fetchSlackIntegrationData();
       toastSuccess(t('admin:slack_integration.adding_slack_ws_integration_settings_successful'));
     }
@@ -125,11 +128,12 @@ const SlackIntegration = (props) => {
   let settingsComponent = null;
 
   switch (currentBotType) {
-    case 'officialBot':
+    case SlackbotType.OFFICIAL:
       settingsComponent = (
         <OfficialBotSettings
           slackAppIntegrations={slackAppIntegrations}
           onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
+          onPrimaryUpdated={fetchSlackIntegrationData}
           onDeleteSlackAppIntegration={fetchSlackIntegrationData}
           connectionStatuses={connectionStatuses}
           onUpdateTokens={fetchSlackIntegrationData}
@@ -137,7 +141,7 @@ const SlackIntegration = (props) => {
         />
       );
       break;
-    case 'customBotWithoutProxy':
+    case SlackbotType.CUSTOM_WITHOUT_PROXY:
       settingsComponent = (
         <CustomBotWithoutProxySettings
           slackBotTokenEnv={slackBotTokenEnv}
@@ -150,12 +154,13 @@ const SlackIntegration = (props) => {
         />
       );
       break;
-    case 'customBotWithProxy':
+    case SlackbotType.CUSTOM_WITH_PROXY:
       settingsComponent = (
         <CustomBotWithProxySettings
           slackAppIntegrations={slackAppIntegrations}
           proxyServerUri={proxyServerUri}
           onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
+          onPrimaryUpdated={fetchSlackIntegrationData}
           onDeleteSlackAppIntegration={fetchSlackIntegrationData}
           connectionStatuses={connectionStatuses}
           onUpdateTokens={fetchSlackIntegrationData}

+ 55 - 41
packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -3,6 +3,9 @@ import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
+
+import { SlackbotType } from '@growi/slack';
+
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -67,15 +70,16 @@ const BotInstallProcessForCustomBotWithProxy = () => {
   const { t } = useTranslation();
   return (
     <div className="container w-75 py-5">
-      <p>1. {t('admin:slack_integration.accordion.select_install_your_app')}</p>
-      <img src="/images/slack-integration/slack-bot-install-your-app-introduction.png" className="border border-light img-fluid mb-5" />
-      <p>2. {t('admin:slack_integration.accordion.select_install_to_workspace')}</p>
-      <img src="/images/slack-integration/slack-bot-install-to-workspace.png" className="border border-light img-fluid mb-5" />
-      <p>3. {t('admin:slack_integration.accordion.click_allow')}</p>
+      <p>1. {t('admin:slack_integration.accordion.go-to-manage-distribution')}</p>
+      <p>2. {t('admin:slack_integration.accordion.activate-public-distribution')}</p>
+      <img src="/images/slack-integration/activate-public-dist.png" className="border border-light img-fluid mb-5" />
+      <p>3. {t('admin:slack_integration.accordion.click-add-to-slack-button')}</p>
+      <img src="/images/slack-integration/click-add-to-slack.png" className="border border-light img-fluid mb-5" />
+      <p>4. {t('admin:slack_integration.accordion.click_allow')}</p>
       <img src="/images/slack-integration/slack-bot-install-your-app-transition-destination.png" className="border border-light img-fluid mb-5" />
-      <p>4. {t('admin:slack_integration.accordion.install_complete_if_checked')}</p>
-      <img src="/images/slack-integration/slack-bot-install-your-app-complete.png" className="border border-light img-fluid mb-5" />
-      <p>5. {t('admin:slack_integration.accordion.invite_bot_to_channel')}</p>
+      <p>5. {t('admin:slack_integration.accordion.install_complete_if_checked')}</p>
+      <img src="/images/slack-integration/basicinfo-all-checked.png" className="border border-light img-fluid mb-5" />
+      <p>6. {t('admin:slack_integration.accordion.invite_bot_to_channel')}</p>
       <img src="/images/slack-integration/slack-bot-install-to-workspace-joined-bot.png" className="border border-light img-fluid mb-1" />
       <img src="/images/slack-integration/slack-bot-install-your-app-introduction-to-channel.png" className="border border-light img-fluid" />
     </div>
@@ -86,17 +90,27 @@ const RegisteringProxyUrlProcess = () => {
   const { t } = useTranslation();
   return (
     <div className="container w-75 py-5">
-      <p
-        // eslint-disable-next-line react/no-danger
-        dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.accordion.copy_proxy_url') }}
-      />
-      <img className="mb-5 border border-light img-fluid" src="/images/slack-integration/growi-register-sentence.png" />
-      <span
-        // eslint-disable-next-line react/no-danger
-        dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.accordion.enter_proxy_url_and_update') }}
-      />
-      <p className="text-danger">{t('admin:slack_integration.accordion.dont_need_update')}</p>
-      <img className="mb-3 border border-light img-fluid" src="/images/slack-integration/growi-set-proxy-url.png" />
+      <ol>
+        <li>
+          <p
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.accordion.copy_proxy_url') }}
+          />
+          <p>
+            <img className="border border-light img-fluid" src="/images/slack-integration/growi-register-sentence.png" />
+          </p>
+        </li>
+        <li>
+          <p
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.accordion.enter_proxy_url_and_update') }}
+          />
+          <p>
+            <img className="border border-light img-fluid" src="/images/slack-integration/growi-set-proxy-url.png" />
+          </p>
+          <p className="text-danger">{t('admin:slack_integration.accordion.dont_need_update')}</p>
+        </li>
+      </ol>
     </div>
   );
 };
@@ -107,7 +121,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
 
   const regenerateTokensHandler = async() => {
     try {
-      await appContainer.apiv3.put('/slack-integration-settings/regenerate-tokens', { slackAppIntegrationId });
+      await appContainer.apiv3.put(`/slack-integration-settings/slack-app-integrations/${slackAppIntegrationId}/regenerate-tokens`);
       if (props.onUpdateTokens != null) {
         props.onUpdateTokens();
       }
@@ -215,7 +229,7 @@ const TestProcess = ({
   const submitForm = async(e) => {
     e.preventDefault();
     try {
-      await apiv3Post('/slack-integration-settings/with-proxy/relation-test', { slackAppIntegrationId, channel: testChannel });
+      await apiv3Post(`/slack-integration-settings/slack-app-integrations/${slackAppIntegrationId}/relation-test`, { channel: testChannel });
       const newLogs = addLogs(logsValue, successMessage, null);
       setLogsValue(newLogs);
 
@@ -309,15 +323,6 @@ const WithProxyAccordions = (props) => {
       />,
     },
     '③': {
-      title: 'manage_commands',
-      content: <ManageCommandsProcess
-        apiv3Put={props.appContainer.apiv3.put}
-        slackAppIntegrationId={props.slackAppIntegrationId}
-        supportedCommandsForBroadcastUse={props.supportedCommandsForBroadcastUse}
-        supportedCommandsForSingleUse={props.supportedCommandsForSingleUse}
-      />,
-    },
-    '④': {
       title: 'test_connection',
       content: <TestProcess
         apiv3Post={props.appContainer.apiv3.post}
@@ -327,6 +332,15 @@ const WithProxyAccordions = (props) => {
         isLatestConnectionSuccess={isLatestConnectionSuccess}
       />,
     },
+    '④': {
+      title: 'manage_commands',
+      content: <ManageCommandsProcess
+        apiv3Put={props.appContainer.apiv3.put}
+        slackAppIntegrationId={props.slackAppIntegrationId}
+        supportedCommandsForBroadcastUse={props.supportedCommandsForBroadcastUse}
+        supportedCommandsForSingleUse={props.supportedCommandsForSingleUse}
+      />,
+    },
   };
 
   const CustomBotIntegrationProcedure = {
@@ -353,15 +367,6 @@ const WithProxyAccordions = (props) => {
       content: <RegisteringProxyUrlProcess />,
     },
     '⑤': {
-      title: 'manage_commands',
-      content: <ManageCommandsProcess
-        apiv3Put={props.appContainer.apiv3.put}
-        slackAppIntegrationId={props.slackAppIntegrationId}
-        supportedCommandsForBroadcastUse={props.supportedCommandsForBroadcastUse}
-        supportedCommandsForSingleUse={props.supportedCommandsForSingleUse}
-      />,
-    },
-    '⑥': {
       title: 'test_connection',
       content: <TestProcess
         apiv3Post={props.appContainer.apiv3.post}
@@ -371,9 +376,18 @@ const WithProxyAccordions = (props) => {
         isLatestConnectionSuccess={isLatestConnectionSuccess}
       />,
     },
+    '⑥': {
+      title: 'manage_commands',
+      content: <ManageCommandsProcess
+        apiv3Put={props.appContainer.apiv3.put}
+        slackAppIntegrationId={props.slackAppIntegrationId}
+        supportedCommandsForBroadcastUse={props.supportedCommandsForBroadcastUse}
+        supportedCommandsForSingleUse={props.supportedCommandsForSingleUse}
+      />,
+    },
   };
 
-  const integrationProcedureMapping = props.botType === 'officialBot' ? officialBotIntegrationProcedure : CustomBotIntegrationProcedure;
+  const integrationProcedureMapping = props.botType === SlackbotType.OFFICIAL ? officialBotIntegrationProcedure : CustomBotIntegrationProcedure;
 
   return (
     <div
@@ -406,7 +420,7 @@ const WithProxyAccordions = (props) => {
 const WithProxyAccordionsWrapper = withUnstatedContainers(WithProxyAccordions, [AppContainer]);
 WithProxyAccordions.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  botType: PropTypes.string.isRequired,
+  botType: PropTypes.oneOf(Object.values(SlackbotType)).isRequired,
   slackAppIntegrationId: PropTypes.string.isRequired,
   tokenPtoG: PropTypes.string,
   tokenGtoP: PropTypes.string,

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -4,11 +4,11 @@ import { withTranslation } from 'react-i18next';
 
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { debounce } from 'throttle-debounce';
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import UserPicture from '../../User/UserPicture';
 
 class UserGroupUserFormByInput extends React.Component {
 

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 
-import UserPicture from '../../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';

+ 1 - 1
packages/app/src/components/Admin/Users/UserTable.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 
-import UserPicture from '../../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import UserMenu from './UserMenu';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';

+ 3 - 1
packages/app/src/components/ComparePathsTable.jsx

@@ -2,10 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 import PageContainer from '~/client/services/PageContainer';
-import { convertToNewAffiliationPath } from '~/utils/path-utils';
+
+const { convertToNewAffiliationPath } = pagePathUtils;
 
 function ComparePathsTable(props) {
   const {

+ 3 - 1
packages/app/src/components/ContentLinkButtons.jsx

@@ -1,7 +1,7 @@
 import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 
-import { isTopPage } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -10,6 +10,8 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
 
+const { isTopPage } = pagePathUtils;
+
 const WIKI_HEADER_LINK = 120;
 
 /**

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

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
 import { withTranslation } from 'react-i18next';
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import { withUnstatedContainers } from './UnstatedUtils';
 

+ 3 - 1
packages/app/src/components/DuplicatedPathsTable.jsx

@@ -2,10 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 import PageContainer from '~/client/services/PageContainer';
-import { convertToNewAffiliationPath } from '~/utils/path-utils';
+
+const { convertToNewAffiliationPath } = pagePathUtils;
 
 function DuplicatedPathsTable(props) {
   const {

+ 1 - 1
packages/app/src/components/Fab.jsx

@@ -62,7 +62,7 @@ const Fab = (props) => {
   }
 
   return (
-    <div className="grw-fab d-none d-md-block">
+    <div className="grw-fab d-none d-md-block d-edit-none">
       {currentUser != null && renderPageCreateButton()}
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
         <button

+ 7 - 0
packages/app/src/components/LoginForm.jsx

@@ -251,6 +251,7 @@ class LoginForm extends React.Component {
       isLocalStrategySetup,
       isLdapStrategySetup,
       isRegistrationEnabled,
+      isPasswordResetEnabled,
       objOfIsExternalAuthEnableds,
     } = this.props;
 
@@ -268,6 +269,11 @@ class LoginForm extends React.Component {
                 {isRegistrationEnabled && (
                   <div className="row">
                     <div className="col-12 text-right py-2">
+                      {isPasswordResetEnabled && (
+                        <a href="/forgot-password" className="d-block link-switch mb-1">
+                          <i className="icon-key"></i> {t('forgot_password.forgot_password')}
+                        </a>
+                      )}
                       <a href="#register" id="register" className="link-switch" onClick={this.switchForm}>
                         <i className="ti-check-box"></i> {t('Sign up is here')}
                       </a>
@@ -307,6 +313,7 @@ LoginForm.propTypes = {
   isRegistrationEnabled: PropTypes.bool,
   registrationMode: PropTypes.string,
   registrationWhiteList: PropTypes.array,
+  isPasswordResetEnabled: PropTypes.bool,
   isLocalStrategySetup: PropTypes.bool,
   isLdapStrategySetup: PropTypes.bool,
   objOfIsExternalAuthEnableds: PropTypes.object,

+ 4 - 2
packages/app/src/components/Navbar/AuthorInfo.jsx

@@ -1,9 +1,11 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { format } from 'date-fns';
-import { userPageRoot } from '~/utils/path-utils';
+import { UserPicture } from '@growi/ui';
+import { pagePathUtils } from '@growi/core';
+
+const { userPageRoot } = pagePathUtils;
 
-import UserPicture from '../User/UserPicture';
 
 const formatType = 'yyyy/MM/dd HH:mm';
 const AuthorInfo = (props) => {

+ 1 - 1
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
+import { DevidedPagePath } from '@growi/core';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
-import DevidedPagePath from '~/models/devided-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 import { withUnstatedContainers } from '../UnstatedUtils';

+ 1 - 1
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -5,6 +5,7 @@ import { withTranslation } from 'react-i18next';
 
 import { UncontrolledTooltip } from 'reactstrap';
 
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
@@ -18,7 +19,6 @@ import {
   updateUserPreferenceWithOsSettings,
 } from '~/client/util/color-scheme';
 
-import UserPicture from '../User/UserPicture';
 
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';

+ 3 - 1
packages/app/src/components/Page/CopyDropdown.jsx

@@ -12,7 +12,9 @@ import {
 
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 
-import { encodeSpaces } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
+
+const { encodeSpaces } = pagePathUtils;
 
 /* eslint-disable react/prop-types */
 const DropdownItemContents = ({ title, contents }) => (

+ 3 - 1
packages/app/src/components/Page/PageManagement.jsx

@@ -4,7 +4,7 @@ import { UncontrolledTooltip } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 
-import { isTopPage } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -15,6 +15,8 @@ import CreateTemplateModal from '../CreateTemplateModal';
 import PagePresentationModal from '../PagePresentationModal';
 import PresentationIcon from '../Icons/PresentationIcon';
 
+const { isTopPage } = pagePathUtils;
+
 
 const PageManagement = (props) => {
   const {

+ 1 - 1
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
-import UserPicture from '../User/UserPicture';
 import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import PageDeleteModal from '../PageDeleteModal';

+ 1 - 1
packages/app/src/components/PageAttachment/DeleteAttachmentModal.jsx

@@ -6,7 +6,7 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import UserPicture from '../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import Username from '../User/Username';
 
 export default class DeleteAttachmentModal extends React.Component {

+ 1 - 1
packages/app/src/components/PageAttachment/PageAttachmentList.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import Attachment from './Attachment';
+import { Attachment } from '@growi/ui';
 
 export default class PageAttachmentList extends React.Component {
 

+ 1 - 1
packages/app/src/components/PageComment/Comment.jsx

@@ -6,6 +6,7 @@ import { format } from 'date-fns';
 
 import { UncontrolledTooltip } from 'reactstrap';
 
+import { UserPicture } from '@growi/ui';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 
@@ -13,7 +14,6 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import RevisionBody from '../Page/RevisionBody';
-import UserPicture from '../User/UserPicture';
 import Username from '../User/Username';
 import CommentEditor from './CommentEditor';
 import CommentControl from './CommentControl';

+ 3 - 3
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -8,6 +8,7 @@ import {
 
 import * as toastr from 'toastr';
 
+import { UserPicture } from '@growi/ui';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import CommentContainer from '~/client/services/CommentContainer';
@@ -15,7 +16,6 @@ import EditorContainer from '~/client/services/EditorContainer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
-import UserPicture from '../User/UserPicture';
 import Editor from '../PageEditor/Editor';
 import SlackNotification from '../SlackNotification';
 
@@ -62,7 +62,7 @@ class CommentEditor extends React.Component {
       isUploadable,
       isUploadableFile,
       errorMessage: undefined,
-      hasSlackConfig: config.hasSlackConfig,
+      isSlackConfigured: config.isSlackConfigured,
     };
 
     this.updateState = this.updateState.bind(this);
@@ -354,7 +354,7 @@ class CommentEditor extends React.Component {
             <span className="flex-grow-1" />
             <span className="d-none d-sm-inline">{ this.state.errorMessage && errorMessage }</span>
 
-            { this.state.hasSlackConfig
+            { this.state.isSlackConfigured
               && (
                 <div className="form-inline align-self-center mr-md-2">
                   <SlackNotification

+ 1 - 1
packages/app/src/components/PageComment/DeleteCommentModal.jsx

@@ -7,7 +7,7 @@ import {
 
 import { format } from 'date-fns';
 
-import UserPicture from '../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import Username from '../User/Username';
 
 export default class DeleteCommentModal extends React.Component {

+ 6 - 4
packages/app/src/components/PageCreateModal.jsx

@@ -7,10 +7,8 @@ import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 
-import { pathUtils } from 'growi-commons';
-import {
-  userPageRoot, isCreatablePage, generateEditorPath,
-} from '~/utils/path-utils';
+import { pagePathUtils, pathUtils } from '@growi/core';
+
 
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
@@ -19,6 +17,10 @@ import { toastError } from '~/client/util/apiNotification';
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 
+const {
+  userPageRoot, isCreatablePage, generateEditorPath,
+} = pagePathUtils;
+
 const PageCreateModal = (props) => {
   const { t, appContainer, navigationContainer } = props;
 

+ 10 - 0
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -801,6 +801,15 @@ export default class CodeMirrorEditor extends AbstractEditor {
       >
         <EditorIcon icon="CheckList" />
       </Button>,
+      <Button
+        key="nav-item-attachment"
+        color={null}
+        size="sm"
+        title="Attachment"
+        onClick={this.props.onAddAttachmentButtonClicked}
+      >
+        <EditorIcon icon="Attachment" />
+      </Button>,
       <Button
         key="nav-item-link"
         color={null}
@@ -947,6 +956,7 @@ CodeMirrorEditor.propTypes = Object.assign({
   emojiStrategy: PropTypes.object,
   lineNumbers: PropTypes.bool,
   onMarkdownHelpButtonClicked: PropTypes.func,
+  onAddAttachmentButtonClicked: PropTypes.func,
 }, AbstractEditor.propTypes);
 CodeMirrorEditor.defaultProps = {
   lineNumbers: true,

+ 7 - 1
packages/app/src/components/PageEditor/Editor.jsx

@@ -39,6 +39,7 @@ export default class Editor extends AbstractEditor {
     this.dropHandler = this.dropHandler.bind(this);
 
     this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
+    this.addAttachmentHandler = this.addAttachmentHandler.bind(this);
 
     this.getAcceptableType = this.getAcceptableType.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
@@ -187,6 +188,10 @@ export default class Editor extends AbstractEditor {
     this.setState({ isCheatsheetModalShown: true });
   }
 
+  addAttachmentHandler() {
+    this.dropzone.open();
+  }
+
   getDropzoneClassName(isDragAccept, isDragReject) {
     let className = 'dropzone';
     if (!this.props.isUploadable) {
@@ -314,6 +319,7 @@ export default class Editor extends AbstractEditor {
                         onPasteFiles={this.pasteFilesHandler}
                         onDragEnter={this.dragEnterHandler}
                         onMarkdownHelpButtonClicked={this.showMarkdownHelp}
+                        onAddAttachmentButtonClicked={this.addAttachmentHandler}
                         {...this.props}
                       />
                     )}
@@ -341,7 +347,7 @@ export default class Editor extends AbstractEditor {
             <button
               type="button"
               className="btn btn-outline-secondary btn-block btn-open-dropzone"
-              onClick={() => { this.dropzone.open() }}
+              onClick={this.addAttachmentHandler}
             >
               <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
               Attach files

+ 7 - 9
packages/app/src/components/PageEditor/EditorIcon.jsx

@@ -118,15 +118,13 @@ const EditorIcon = (props) => {
           <path d="M22.12,17H19.75l-3.12-4H18a1,1,0,0,0,1-1V8a1,1,0,0,0-1-1H12a1,1,0,0,0-1,1v4a1,1,0,0,0,1,1h1.38l-2.92,4H7.88A.94.94,0,0,0,7,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,14,22V18a.94.94,0,0,0-.88-1H11.63l3.13-4h.47l3.13,4H16.88A.94.94,0,0,0,16,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,23,22V18A.94.94,0,0,0,22.12,17ZM13,22H8V18h5ZM12,8h6v4H12ZM22,22H17V18h5Z" />
         </svg>
       );
-    // Unused icon
-    // case 'attachment':
-    //   return (
-    //     <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-    //       <rect fillOpacity="0" width="30" height="30" />
-    //       <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
-    //     </svg>
-    // );
-
+    case 'Attachment':
+      return (
+        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
+          <rect fillOpacity="0" width="30" height="30" />
+          <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
+        </svg>
+      );
   }
 
 

+ 3 - 3
packages/app/src/components/PageEditor/EditorNavbarBottom.jsx

@@ -19,7 +19,7 @@ const EditorNavbarBottom = (props) => {
   const [isExpanded, setExpanded] = useState(false);
 
   const [isSlackExpanded, setSlackExpanded] = useState(false);
-  const hasSlackConfig = props.appContainer.getConfig().hasSlackConfig;
+  const isSlackConfigured = props.appContainer.getConfig().isSlackConfigured;
 
   const {
     navigationContainer,
@@ -61,7 +61,7 @@ const EditorNavbarBottom = (props) => {
   return (
     <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `}>
       {/* Collapsed SlackNotification */}
-      {hasSlackConfig && (
+      {isSlackConfigured && (
         <Collapse isOpen={isSlackExpanded && isDeviceSmallerThanMd}>
           <nav className={`navbar navbar-expand-lg border-top ${additionalClasses.join(' ')}`}>
             <SlackNotification
@@ -84,7 +84,7 @@ const EditorNavbarBottom = (props) => {
         <form className="form-inline flex-nowrap ml-auto">
           {/* Responsive Design for the SlackNotification */}
           {/* Button or the normal Slack banner */}
-          {hasSlackConfig && (isDeviceSmallerThanMd ? (
+          {isSlackConfigured && (isDeviceSmallerThanMd ? (
             <Button
               className="grw-btn-slack border mr-2"
               onClick={() => (setSlackExpanded(!isSlackExpanded))}

Некоторые файлы не были показаны из-за большого количества измененных файлов