فهرست منبع

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

zahmis 4 سال پیش
والد
کامیت
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
 ARG USER_GID=$USER_UID
 
 
 RUN mkdir -p /workspace/growi/node_modules
 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
 # [Optional] Update UID/GID if needed
 RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
 RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \

+ 1 - 0
.devcontainer/devcontainer.json

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

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

@@ -24,8 +24,9 @@ services:
     volumes:
     volumes:
       - ..:/workspace/growi:delegated
       - ..:/workspace/growi:delegated
       - node_modules:/workspace/growi/node_modules
       - 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
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
-      - ../../node_modules:/workspace/node_modules:delegated
 
 
     tty: true
     tty: true
 
 
@@ -81,3 +82,5 @@ services:
       - /files/sqlite
       - /files/sqlite
 volumes:
 volumes:
   node_modules:
   node_modules:
+  node_modules_app:
+  node_modules_slackbot-proxy:

+ 4 - 0
.eslintrc.js

@@ -26,5 +26,9 @@ module.exports = {
         FunctionExpression: { body: 1, parameters: 2 },
         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
     - name: yarn dev:ci
       working-directory: ./packages/slackbot-proxy
       working-directory: ./packages/slackbot-proxy
       run: |
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.development.local
         yarn dev:ci
         yarn dev:ci
       env:
       env:
         TYPEORM_CONNECTION: mysql
         TYPEORM_CONNECTION: mysql
@@ -211,7 +211,7 @@ jobs:
     - name: yarn start:prod:ci
     - name: yarn start:prod:ci
       working-directory: ./packages/slackbot-proxy
       working-directory: ./packages/slackbot-proxy
       run: |
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn start:prod:ci
         yarn start:prod:ci
       env:
       env:
         TYPEORM_CONNECTION: mysql
         TYPEORM_CONNECTION: mysql

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

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

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

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

+ 4 - 0
.markdownlint.yml

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

+ 20 - 0
.vscode/launch.json

@@ -44,6 +44,26 @@
         "url": "http://localhost:3000",
         "url": "http://localhost:3000",
         "webRoot": "${workspaceFolder}/packages/app/public",
         "webRoot": "${workspaceFolder}/packages/app/public",
         "pathMappings": [
         "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",
             "url": "webpack:///src",
             "path": "${workspaceFolder}/packages/app/src"
             "path": "${workspaceFolder}/packages/app/src"

+ 30 - 2
CHANGES.md

@@ -1,11 +1,39 @@
 # CHANGES
 # 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
 * Support: Upgrade libs
     * @slack/web-api
     * @slack/web-api
-    * escape-string-regexp
+    * date-fns
+    * helmet
     * morgan
     * 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
 ## v4.3.2
 
 

+ 23 - 5
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.3.3-RC",
+  "version": "4.4.0-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -21,13 +21,17 @@
   },
   },
   "private": true,
   "private": true,
   "workspaces": {
   "workspaces": {
-    "packages": ["packages/*"],
-    "nohoist": ["**/slackbot-proxy/bootstrap"]
+    "packages": [
+      "packages/*"
+    ],
+    "nohoist": [
+      "**/slackbot-proxy/bootstrap"
+    ]
   },
   },
   "scripts": {
   "scripts": {
     "start": "yarn app:server",
     "start": "yarn app:server",
     "prestart": "yarn app:build",
     "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",
     "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:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",
     "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"
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
   },
   },
   "dependencies": {
   "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": {
   "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/eslint-plugin": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "eslint": "^7.31.0",
     "eslint": "^7.31.0",
@@ -48,7 +61,12 @@
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react-hooks": "^4.2.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": {
   "engines": {
     "node": "^12 || ^14",
     "node": "^12 || ^14",

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

@@ -3,13 +3,14 @@
 ## https://nextjs.org/docs/basic-features/environment-variables
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 ##
 FILE_UPLOAD=mongodb
 FILE_UPLOAD=mongodb
-# MONGO_GRIDFS_TOTAL_LIMIT=10485760 # 10MB
+# MONGO_GRIDFS_TOTAL_LIMIT=10485760
 MATHJAX=1
 MATHJAX=1
 # NO_CDN=true
 # NO_CDN=true
 MONGO_URI="mongodb://mongo:27017/growi"
 MONGO_URI="mongodb://mongo:27017/growi"
 # REDIS_URI="http://redis:6379"
 # REDIS_URI="http://redis:6379"
 # NCHAN_URI="http://nchan"
 # NCHAN_URI="http://nchan"
 ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
 ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
+ELASTICSEARCH_REQUEST_TIMEOUT=15000
 HACKMD_URI="http://localhost:3010"
 HACKMD_URI="http://localhost:3010"
 HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
@@ -17,11 +18,9 @@ HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 # PUBLISH_OPEN_API=true
 # PUBLISH_OPEN_API=true
 # USER_UPPER_LIMIT=0
 # USER_UPPER_LIMIT=0
 # DEV_HTTPS=true
 # DEV_HTTPS=true
-# FORCE_WIKI_MODE=private # 'public', 'private', undefined
+# FORCE_WIKI_MODE=private
 # PROMSTER_ENABLED=true
 # 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_CLOUD_URI='http://growi.cloud'
 # GROWI_APP_ID_FOR_GROWI_CLOUD=012345
 # 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
 ## Handled by Next.js with dotenv or dotenv-flow
 ## https://nextjs.org/docs/basic-features/environment-variables
 ## 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/**
 /public/**
 /src/client/legacy/thirdparty-js/**
 /src/client/legacy/thirdparty-js/**
 /src/client/util/reveal/plugins/markdown.js
 /src/client/util/reveal/plugins/markdown.js

+ 1 - 1
packages/app/.gitignore

@@ -13,7 +13,7 @@
 
 
 # dist (for GROWI v4.x and below)
 # dist (for GROWI v4.x and below)
 /public/*.chunk.js
 /public/*.chunk.js
-/public/*.chunk.js.LICENSE
+/public/*.chunk.js.LICENSE.txt
 /public/*.bundle.js
 /public/*.bundle.js
 /public/manifest.json
 /public/manifest.json
 /public/dll
 /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 normalize from 'normalize-path';
 import swig from 'swig-templates';
 import swig from 'swig-templates';
 
 
+import { PluginDefinitionV4 } from '@growi/core';
+
 import PluginUtils from '../src/server/plugins/plugin-utils';
 import PluginUtils from '../src/server/plugins/plugin-utils';
 import loggerFactory from '../src/utils/logger';
 import loggerFactory from '../src/utils/logger';
 import { resolveFromRoot } from '../src/utils/project-dir-utils';
 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();
 const pluginNames: string[] = pluginUtils.listPluginNames();
 logger.info('Detected plugins: ', pluginNames);
 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
     // convert backslash to slash
     definition.entries = definition.entries.map((entryPath) => {
     definition.entries = definition.entries.map((entryPath) => {
       return normalize(entryPath);
       return normalize(entryPath);
     });
     });
     return definition;
     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
 # disable Elasticsearch
 ELASTICSEARCH_URI=
 ELASTICSEARCH_URI=

+ 34 - 24
packages/app/docker/Dockerfile

@@ -17,16 +17,21 @@ COPY ./package.json .
 COPY ./yarn.lock .
 COPY ./yarn.lock .
 COPY ./lerna.json .
 COPY ./lerna.json .
 COPY ./packages/app/package.json packages/app/
 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/slack/package.json packages/slack/
+COPY ./packages/ui/package.json packages/ui/
 
 
 # setup
 # setup
 RUN yarn config set network-timeout 300000
 RUN yarn config set network-timeout 300000
 RUN npx lerna bootstrap
 RUN npx lerna bootstrap
 
 
 # make artifacts
 # 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
 RUN yarn install --production
 
 
 # make artifacts
 # 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 ./yarn.lock ./
 COPY ./lerna.json ./
 COPY ./lerna.json ./
 COPY ./tsconfig.base.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 all related packages
-COPY packages/slack packages/slack
 COPY packages/app packages/app
 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
 # build
 RUN yarn lerna run build
 RUN yarn lerna run build
@@ -106,15 +109,18 @@ RUN yarn lerna run build
 RUN tar cf packages.tar \
 RUN tar cf packages.tar \
   package.json \
   package.json \
   yarn.lock \
   yarn.lock \
-  config \
-  public \
-  resource \
-  src \
-  tmp \
+  tsconfig.base.json \
   packages/app/package.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
 FROM node:14-slim
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
 
+ENV NODE_ENV production
+
 ENV appDir /opt/growi
 ENV appDir /opt/growi
 
 
 # Add gosu
 # Add gosu
@@ -154,12 +162,14 @@ RUN rm node_modules.tar packages.tar
 
 
 USER root
 USER root
 
 
-COPY docker/docker-entrypoint.sh /
+COPY packages/app/docker/docker-entrypoint.sh /
 RUN chmod 700 /docker-entrypoint.sh
 RUN chmod 700 /docker-entrypoint.sh
 RUN chown node:node ${appDir}
 RUN chown node:node ${appDir}
 
 
+WORKDIR ${appDir}/packages/app
+
 VOLUME /data
 VOLUME /data
 EXPOSE 3000
 EXPOSE 3000
 
 
 ENTRYPOINT ["/tini", "-e", "143", "--", "/docker-entrypoint.sh"]
 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`
 # Support `FILE_UPLOAD=local`
 mkdir -p /data/uploads
 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
 fi
 
 
 chown -R node:node /data/uploads
 chown -R node:node /data/uploads
-chown -h node:node $appDir/public/uploads
+chown -h node:node ./public/uploads
 
 
 gosu node $@
 gosu node $@

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

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

+ 28 - 36
packages/app/package.json

@@ -1,25 +1,26 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "4.3.3-RC",
+  "version": "4.4.0-RC",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
     "start": "yarn build && yarn server",
     "start": "yarn build && yarn server",
     "build": "run-p build:*",
     "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",
     "server:ci": "yarn server --ci",
-    "preserver": "yarn migrate",
+    "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
     "//// for development": "",
     "//// for development": "",
     "dev": "run-p dev:client dev:server",
     "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": "",
     "//// for CI": "",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "predev:ci": "run-p resources:*",
     "predev:ci": "run-p resources:*",
@@ -32,10 +33,10 @@
     "prelint:eslint": "yarn resources:plugin",
     "prelint:eslint": "yarn resources:plugin",
     "prelint:swagger2openapi": "yarn openapi:v3",
     "prelint:swagger2openapi": "yarn openapi:v3",
     "//// misc": "",
     "//// 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",
     "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:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
     "resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
     "resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
     "migrate": "yarn migrate:up",
     "migrate": "yarn migrate:up",
@@ -44,7 +45,7 @@
     "migrate:up": "yarn ts-node node_modules/.bin/migrate-mongo up -f config/migrate.js",
     "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",
     "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": "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": {
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
@@ -54,8 +55,10 @@
   "dependencies": {
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@google-cloud/storage": "^5.8.5",
     "@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",
     "@kobalab/socket.io-session": "^1.0.3",
     "@promster/express": "^5.0.1",
     "@promster/express": "^5.0.1",
     "@promster/server": "^6.0.0",
     "@promster/server": "^6.0.0",
@@ -76,12 +79,10 @@
     "connect-mongo": "^4.4.1",
     "connect-mongo": "^4.4.1",
     "connect-redis": "^4.0.4",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "cookie-parser": "^1.4.5",
-    "cross-env": "^7.0.0",
     "csrf": "^3.1.0",
     "csrf": "^3.1.0",
-    "date-fns": "^2.0.0",
+    "date-fns": "^2.23.0",
     "detect-indent": "^6.0.0",
     "detect-indent": "^6.0.0",
     "diff": "^5.0.0",
     "diff": "^5.0.0",
-    "dotenv-flow": "^3.2.0",
     "elasticsearch": "^16.0.0",
     "elasticsearch": "^16.0.0",
     "entities": "^2.0.0",
     "entities": "^2.0.0",
     "esa-nodejs": "^0.0.7",
     "esa-nodejs": "^0.0.7",
@@ -90,14 +91,14 @@
     "express-bunyan-logger": "^1.3.3",
     "express-bunyan-logger": "^1.3.3",
     "express-form": "~0.12.0",
     "express-form": "~0.12.0",
     "express-mongo-sanitize": "^2.1.0",
     "express-mongo-sanitize": "^2.1.0",
+    "express-rate-limit": "^5.3.0",
     "express-session": "^1.16.1",
     "express-session": "^1.16.1",
     "express-validator": "^6.1.1",
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
     "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",
     "http-errors": "~1.6.2",
     "i18next": "^20.3.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
     "i18next-express-middleware": "^2.0.0",
@@ -117,7 +118,6 @@
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "nodemailer": "^6.6.2",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
-    "npm-run-all": "^4.1.2",
     "openid-client": "=2.5.0",
     "openid-client": "=2.5.0",
     "passport": "^0.4.0",
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
@@ -133,14 +133,11 @@
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
-    "socket.io": "^2.3.0",
+    "socket.io": "^4.0.0",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "string-width": "=4.2.2",
     "swagger-jsdoc": "^3.4.0",
     "swagger-jsdoc": "^3.4.0",
     "swig-templates": "^2.0.2",
     "swig-templates": "^2.0.2",
-    "ts-node": "^9.1.1",
-    "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "unzipper": "^0.10.5",
@@ -158,11 +155,11 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
     "@atlaskit/navigation-next": "^8.0.5",
+    "@growi/ui": "^4.4.0-RC",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
     "@types/multer": "^1.4.5",
     "@types/multer": "^1.4.5",
-    "@types/node": "^14.14.35",
     "@types/react-dom": "^17.0.9",
     "@types/react-dom": "^17.0.9",
     "autoprefixer": "^9.0.0",
     "autoprefixer": "^9.0.0",
     "bootstrap": "^4.5.0",
     "bootstrap": "^4.5.0",
@@ -182,8 +179,6 @@
     "hard-source-webpack-plugin": "^0.13.1",
     "hard-source-webpack-plugin": "^0.13.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "imports-loader": "^0.8.0",
     "imports-loader": "^0.8.0",
-    "jest": "^27.0.6",
-    "jest-date-mock": "^1.0.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jquery.cookie": "~1.4.1",
@@ -221,15 +216,13 @@
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
     "react-i18next": "^11.1.0",
     "react-i18next": "^11.1.0",
-    "react-images": "1.0.0",
-    "react-motion": "^0.5.2",
     "react-waypoint": "^10.1.0",
     "react-waypoint": "^10.1.0",
     "reactstrap": "^8.9.0",
     "reactstrap": "^8.9.0",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
     "sass-loader": "^8.0.0",
     "sass-loader": "^8.0.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
-    "socket.io-client": "^2.3.0",
+    "socket.io-client": "^4.0.0",
     "sticky-events": "^3.1.3",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
     "styled-components": "^5.0.1",
@@ -239,7 +232,6 @@
     "terser-webpack-plugin": "^4.1.0",
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",
-    "ts-jest": "^27.0.4",
     "ts-loader": "^8.3.0",
     "ts-loader": "^8.3.0",
     "ts-node-dev": "^1.1.6",
     "ts-node-dev": "^1.1.6",
     "tsc-alias": "^1.2.9",
     "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",
     "installed_version": "Installed version",
     "list_of_env_vars":"List of environment variables",
     "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.",
     "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": {
   "app_setting": {
     "site_name": "Site name",
     "site_name": "Site name",
@@ -252,6 +258,12 @@
     "download": "Download",
     "download": "Download",
     "delete": "Delete"
     "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": {
   "slack_integration": {
     "selecting_bot_types": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
       "slack_bot": "Slack bot",
@@ -282,6 +294,9 @@
       "cancel": "Cancel",
       "cancel": "Cancel",
       "change": "Change"
       "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.",
     "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": {
     "access_token_settings": {
       "regenerate": "Regenerate"
       "regenerate": "Regenerate"
@@ -309,10 +324,13 @@
       "paste_growi_url": "Since a modal is displayed, enter the following URL in <b>GROWI URL</b>.",
       "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>",
       "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",
       "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.",
       "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\".",
       "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\".",
       "select_install_to_workspace": "Select \"Install to Workspace\".",
       "register_proxy_url": "Register Proxy URL with GROWI",
       "register_proxy_url": "Register Proxy URL with GROWI",
       "click_allow": "Select \"Allow\".",
       "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"
       "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": {
   "user_management": {
     "invite_users": "Temporarily issue a new user",
     "invite_users": "Temporarily issue a new user",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
     "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
 ## 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.
 A paragraph can be created by pressing Enter at the end of the previous paragraph.
 
 
 ```
 ```
@@ -53,17 +53,17 @@ paragraph2
 ## Br new line
 ## Br new line
 
 
 Add two spaces before break.
 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
 ## Blockquotes
 
 
@@ -84,7 +84,7 @@ Add one `>` per level at the start of the line
 Wrap code with three back quotes or tildes.
 Wrap code with three back quotes or tildes.
 
 
 ```
 ```
-print 'hoge'
+print 'foo'
 ```
 ```
 
 
 ### Syntax highlight and file name
 ### Syntax highlight and file name
@@ -131,16 +131,16 @@ This is  `Inline Code`.
 Code blocks should be preceded by four spaces or one tab.
 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
     end
     end
 ```
 ```
 
 
-    class Hoge
-        def hoge
-            print 'hoge'
+    class Foo
+        def foo
+            print 'foo'
         end
         end
     end
     end
 
 
@@ -166,7 +166,7 @@ ___
 
 
 ### Italic
 ### 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* .
 This is *Italic* .
@@ -178,7 +178,7 @@ This is _Italic_ .
 
 
 ### Bold
 ### 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**.
 This is **bold**.
@@ -263,7 +263,7 @@ Example of Bootstrap4 is[[here>./Bootstrap4]]
 
 
 ## Ul Bulleted list
 ## 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.
 Items can be nested using indentation.
 
 
 ```
 ```
@@ -286,7 +286,7 @@ Items can be nested using indentation.
 
 
 ## Ol Numbered List
 ## 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.
 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..
 # :heavy_plus_sign: More..
 
 
-- Try to attach Bootstrap4 Tags?
+- Want to attach Bootstrap4 Tags?
     - :arrow_right: [/Sandbox/Bootstrap4]
     - :arrow_right: [/Sandbox/Bootstrap4]
-- Try to draw Diagrams?
+- Want to draw Diagrams?
     - :arrow_right: [/Sandbox/Diagrams]
     - :arrow_right: [/Sandbox/Diagrams]
-- Try to write Math Formulas?
+- Want to write Math Formulas?
     - :arrow_right: [/Sandbox/Math]
     - :arrow_right: [/Sandbox/Math]

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

@@ -438,7 +438,6 @@
     "initialize_successed": "Succeeded to initialize {{target}}",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "give_user_admin": "Succeeded to give {{username}} admin",
     "give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{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}}",
     "activate_user_success": "Succeeded to activating {{username}}",
     "deactivate_user_success": "Succeeded to deactivate {{username}}",
     "deactivate_user_success": "Succeeded to deactivate {{username}}",
     "remove_user_success": "Succeeded to removing {{username}}",
     "remove_user_success": "Succeeded to removing {{username}}",
@@ -605,7 +604,10 @@
     "Local": {
     "Local": {
       "name": "ID/Password",
       "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> .",
       "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": {
     "ldap": {
       "enable_ldap": "Enable LDAP",
       "enable_ldap": "Enable LDAP",
@@ -846,5 +848,20 @@
     "aws_region": "For the region, enter the AWS region name. ex):us-east-1",
     "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.",
     "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."
     "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)
 [![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)
 [![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>
 </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>
 <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 側に同名のページが存在する場合、そのページはスキップされます",
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
   },
+  "external_notification": {
+    "enabled": "有効",
+    "disabled": "無効",
+    "header_status": "Slack 連携の状態",
+    "caution_enabled": "CAUTION: このページで設定される通知は、Primary として設定された Slack ワークスペースにのみ送信されます。 "
+  },
   "slack_integration": {
   "slack_integration": {
     "selecting_bot_types": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
       "slack_bot": "Slack bot",
@@ -282,6 +288,9 @@
       "cancel": "取消",
       "cancel": "取消",
       "change": "変更する"
       "change": "変更する"
     },
     },
+    "toastr": {
+      "delete_slack_integration_procedure": "Slack 連携手順を削除しました"
+    },
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "access_token_settings": {
     "access_token_settings": {
       "regenerate": "再発行"
       "regenerate": "再発行"
@@ -308,10 +317,13 @@
       "paste_growi_url": "モーダルが表示されるので、<b>GROWI URL</b> には下記のURLを入力します。",
       "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>を入れる",
       "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に登録する",
       "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": "※既に値が入っている場合は更新する必要はありません",
       "dont_need_update": "※既に値が入っている場合は更新する必要はありません",
       "select_install_your_app": "Install your app をクリックします。",
       "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 をクリックします。",
       "select_install_to_workspace": "Install to Workspace をクリックします。",
       "register_proxy_url": "Proxy の URLをGROWIに登録する",
       "register_proxy_url": "Proxy の URLをGROWIに登録する",
       "click_allow": "遷移先の画面にて、Allowをクリックします。",
       "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"
       "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": {
   "user_management": {
     "invite_users": "新規ユーザーの仮発行",
     "invite_users": "新規ユーザーの仮発行",
     "click_twice_same_checkbox": "少なくとも一つはチェックしてください。",
     "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}}を初期化しました",
     "initialize_successed": "{{target}}を初期化しました",
     "give_user_admin": "{{username}}を管理者に設定しました",
     "give_user_admin": "{{username}}を管理者に設定しました",
     "remove_user_admin": "{{username}}を管理者から外しました",
     "remove_user_admin": "{{username}}を管理者から外しました",
-    "delete_slack_integration_procedure": "Slack 連携手順を削除しました",
     "activate_user_success": "{{username}}を有効化しました",
     "activate_user_success": "{{username}}を有効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
     "remove_user_success": "{{username}}を削除しました",
@@ -604,7 +603,10 @@
     "Local": {
     "Local": {
       "name": "ID/Password",
       "name": "ID/Password",
       "note for the only env option": "現在LOCAL認証のON/OFFは環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
       "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": {
     "ldap": {
       "enable_ldap": "LDAP を有効にする",
       "enable_ldap": "LDAP を有効にする",
@@ -840,5 +842,20 @@
     "aws_region": "リージョンには、AWSリージョン名を入力してください。例: ap-northeast-1",
     "aws_region": "リージョンには、AWSリージョン名を入力してください。例: ap-northeast-1",
     "aws_custom_endpoint": "カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。",
     "aws_custom_endpoint": "カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。",
     "failed_to_send_a_test_email":"SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。"
     "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)
 [![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)
 [![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-header bg-primary text-light">Tips</div>
   <div class="card-body"><ul>
   <div class="card-body"><ul>
     <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
     <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
@@ -11,15 +29,30 @@
   </ul></div>
   </ul></div>
 </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>
 <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": "下载",
     "download": "下载",
     "delete": "删除"
     "delete": "删除"
   },
   },
+  "external_notification": {
+    "enabled": "Enabled",
+    "disabled": "Disabled",
+    "header_status": "Slack整合状态",
+    "caution_enabled": "CAUTION: 目前,在此页面中配置的通知只会通知设置为主要的 Slack 工作区。 "
+  },
   "slack_integration": {
   "slack_integration": {
     "selecting_bot_types": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
       "slack_bot": "Slack bot",
@@ -292,6 +298,9 @@
       "cancel": "取消",
       "cancel": "取消",
       "change": "改变"
       "change": "改变"
     },
     },
+    "toastr": {
+      "delete_slack_integration_procedure": "删除了 Slack 集成程序"
+    },
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <code>{{variable}}</code> 启用。",
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <code>{{variable}}</code> 启用。",
     "access_token_settings": {
     "access_token_settings": {
       "regenerate": "再生"
       "regenerate": "再生"
@@ -318,10 +327,13 @@
       "paste_growi_url": "由于显示了模式,请在 <b>GROWI URL</b> 中输入以下URL",
       "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>。",
       "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",
       "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": "※如果值已经在里面了,就不需要再更新。",
       "dont_need_update": "※如果值已经在里面了,就不需要再更新。",
       "select_install_your_app": "选择 \"Install your app\"。",
       "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\"。",
       "select_install_to_workspace": "选择 \"Install to Workspace\"。",
       "register_proxy_url": "向 GROWI 注册代理 URL",
       "register_proxy_url": "向 GROWI 注册代理 URL",
       "click_allow": "选择 \"Allow\"。",
       "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"
       "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": {
   "user_management": {
     "invite_users": "临时发布新用户",
     "invite_users": "临时发布新用户",
     "click_twice_same_checkbox": "您应该至少选中一个复选框。",
     "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": "更新",
 	"Update Page": "更新本页",
 	"Update Page": "更新本页",
 	"Warning": "警告",
 	"Warning": "警告",
-	"Sign in": "登录",
+  "Sign in": "登录",
 	"Sign up is here": "注册",
 	"Sign up is here": "注册",
 	"Sign in is here": "登录",
 	"Sign in is here": "登录",
 	"Sign up": "注册",
 	"Sign up": "注册",
@@ -418,7 +418,6 @@
     "initialize_successed": "Succeeded to initialize {{target}}",
     "initialize_successed": "Succeeded to initialize {{target}}",
 		"give_user_admin": "Succeeded to give {{username}} admin",
 		"give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin ",
     "remove_user_admin": "Succeeded to remove {{username}} admin ",
-    "delete_slack_integration_procedure": "删除了 Slack 集成程序",
 		"activate_user_success": "Succeeded to activating {{username}}",
 		"activate_user_success": "Succeeded to activating {{username}}",
 		"deactivate_user_success": "Succeeded to deactivate {{username}}",
 		"deactivate_user_success": "Succeeded to deactivate {{username}}",
 		"remove_user_success": "Succeeded to removing {{username}} ",
 		"remove_user_success": "Succeeded to removing {{username}} ",
@@ -593,7 +592,10 @@
 		"Local": {
 		"Local": {
 			"name": "ID/Password",
 			"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> .",
 			"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": {
 		"ldap": {
 			"enable_ldap": "Enable LDAP",
 			"enable_ldap": "Enable LDAP",
@@ -851,5 +853,20 @@
     "aws_region": "关于地区,请输入AWS地区名,例如:ap-east-1",
     "aws_region": "关于地区,请输入AWS地区名,例如:ap-east-1",
     "aws_custom_endpoint": "关于自定义端点,请指定以http(s)://开头的URL,链接末尾不需要添加“/”",
     "aws_custom_endpoint": "关于自定义端点,请指定以http(s)://开头的URL,链接末尾不需要添加“/”",
     "failed_to_send_a_test_email":"SMTP方式测试邮件发送失败,请检查相关设定。"
     "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)
 [![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)
 [![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>
 </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>
 <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 AdminHome from '../components/Admin/AdminHome/AdminHome';
 import UserGroupDetailPage from '../components/Admin/UserGroupDetail/UserGroupDetailPage';
 import UserGroupDetailPage from '../components/Admin/UserGroupDetail/UserGroupDetailPage';
 import NotificationSetting from '../components/Admin/Notification/NotificationSetting';
 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 SlackIntegration from '../components/Admin/SlackIntegration/SlackIntegration';
 import ManageGlobalNotification from '../components/Admin/Notification/ManageGlobalNotification';
 import ManageGlobalNotification from '../components/Admin/Notification/ManageGlobalNotification';
 import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
 import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
@@ -46,6 +46,7 @@ import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityC
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
 import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 
 
 import { appContainer, componentMappings } from './base';
 import { appContainer, componentMappings } from './base';
 
 
@@ -65,6 +66,7 @@ const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
 const adminUsersContainer = new AdminUsersContainer(appContainer);
 const adminUsersContainer = new AdminUsersContainer(appContainer);
 const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
 const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
 const adminNotificationContainer = new AdminNotificationContainer(appContainer);
 const adminNotificationContainer = new AdminNotificationContainer(appContainer);
+const adminSlackIntegrationLegacyContainer = new AdminSlackIntegrationLegacyContainer(appContainer);
 const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
 const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
 const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appContainer);
 const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appContainer);
 const injectableContainers = [
 const injectableContainers = [
@@ -78,7 +80,7 @@ const injectableContainers = [
   adminUsersContainer,
   adminUsersContainer,
   adminExternalAccountsContainer,
   adminExternalAccountsContainer,
   adminNotificationContainer,
   adminNotificationContainer,
-  adminNotificationContainer,
+  adminSlackIntegrationLegacyContainer,
   adminMarkDownContainer,
   adminMarkDownContainer,
   adminUserGroupDetailContainer,
   adminUserGroupDetailContainer,
 ];
 ];
@@ -99,7 +101,7 @@ Object.assign(componentMappings, {
   'admin-export-page': <ExportArchiveDataPage />,
   'admin-export-page': <ExportArchiveDataPage />,
   'admin-notification-setting': <NotificationSetting />,
   'admin-notification-setting': <NotificationSetting />,
   'admin-slack-integration': <SlackIntegration />,
   'admin-slack-integration': <SlackIntegration />,
-  'admin-slack-integration-notification-setting': <SlackIntegrationNotificationSetting />,
+  'admin-slack-integration-legacy': <LegacySlackIntegration />,
   'admin-global-notification-setting': <ManageGlobalNotification />,
   'admin-global-notification-setting': <ManageGlobalNotification />,
   'admin-user-page': <UserManagement />,
   'admin-user-page': <UserManagement />,
   'admin-external-account-setting': <ManageExternalAccount />,
   '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 {
 export default class Linker {
 
 
   constructor(
   constructor(
@@ -5,10 +10,15 @@ export default class Linker {
       label = '',
       label = '',
       link = '',
       link = '',
   ) {
   ) {
+
     this.type = type;
     this.type = type;
     this.label = label;
     this.label = label;
     this.link = link;
     this.link = link;
 
 
+    if (type === Linker.types.markdownLink) {
+      this.initWhenMarkdownLink();
+    }
+
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
   }
   }
 
 
@@ -25,9 +35,18 @@ export default class Linker {
     markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
     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() {
   generateMarkdownText() {
     if (this.type === Linker.types.pukiwikiLink) {
     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}]]`;
       return `[[${this.label}>${this.link}]]`;
     }
     }
     if (this.type === Linker.types.growiLink) {
     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 InstallerForm from '../components/InstallerForm';
 import LoginForm from '../components/LoginForm';
 import LoginForm from '../components/LoginForm';
+import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
+import PasswordResetExecutionForm from '../components/PasswordResetExecutionForm';
 
 
 const i18n = i18nFactory();
 const i18n = i18nFactory();
 
 
@@ -38,6 +40,7 @@ if (loginFormElem) {
   const email = loginFormElem.dataset.email;
   const email = loginFormElem.dataset.email;
   const isRegistrationEnabled = loginFormElem.dataset.isRegistrationEnabled === 'true';
   const isRegistrationEnabled = loginFormElem.dataset.isRegistrationEnabled === 'true';
   const registrationMode = loginFormElem.dataset.registrationMode;
   const registrationMode = loginFormElem.dataset.registrationMode;
+  const isPasswordResetEnabled = loginFormElem.dataset.isPasswordResetEnabled === 'true';
 
 
 
 
   let registrationWhiteList = loginFormElem.dataset.registrationWhiteList;
   let registrationWhiteList = loginFormElem.dataset.registrationWhiteList;
@@ -68,6 +71,7 @@ if (loginFormElem) {
           isRegistrationEnabled={isRegistrationEnabled}
           isRegistrationEnabled={isRegistrationEnabled}
           registrationMode={registrationMode}
           registrationMode={registrationMode}
           registrationWhiteList={registrationWhiteList}
           registrationWhiteList={registrationWhiteList}
+          isPasswordResetEnabled={isPasswordResetEnabled}
           isLocalStrategySetup={isLocalStrategySetup}
           isLocalStrategySetup={isLocalStrategySetup}
           isLdapStrategySetup={isLdapStrategySetup}
           isLdapStrategySetup={isLdapStrategySetup}
           objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
           objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
@@ -77,3 +81,33 @@ if (loginFormElem) {
     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;
       const meta = definition.meta;
 
 
       switch (meta.pluginSchemaVersion) {
       switch (meta.pluginSchemaVersion) {
-        // v1 is deprecated
+        // v1, v2 and v3 is deprecated
         case 1:
         case 1:
           logger.warn('pluginSchemaVersion 1 is deprecated', definition);
           logger.warn('pluginSchemaVersion 1 is deprecated', definition);
           break;
           break;
-        // v2 is deprecated
         case 2:
         case 2:
           logger.warn('pluginSchemaVersion 2 is deprecated', definition);
           logger.warn('pluginSchemaVersion 2 is deprecated', definition);
           break;
           break;
         case 3:
         case 3:
+          logger.warn('pluginSchemaVersion 2 is deprecated', definition);
+          break;
+        case 4:
           definition.entries.forEach((entry) => {
           definition.entries.forEach((entry) => {
             entry(appContainer);
             entry(appContainer);
           });
           });

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

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

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

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 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.appContainer = appContainer;
 
 
+    this.copyStateValues = {
+      DEFAULT: 'default',
+      DONE: 'done',
+    };
+    this.timer = null;
+
     this.state = {
     this.state = {
       retrieveError: null,
       retrieveError: null,
       growiVersion: '',
       growiVersion: '',
       nodeVersion: '',
       nodeVersion: '',
       npmVersion: '',
       npmVersion: '',
       yarnVersion: '',
       yarnVersion: '',
+      copyState: this.copyStateValues.DEFAULT,
       installedPlugins: [],
       installedPlugins: [],
     };
     };
 
 
@@ -36,6 +43,10 @@ export default class AdminHomeContainer extends Container {
     return 'AdminHomeContainer';
     return 'AdminHomeContainer';
   }
   }
 
 
+  componentWillUnmount() {
+    clearTimeout(this.timer);
+  }
+
   /**
   /**
    * retrieve admin home data
    * retrieve admin home data
    */
    */
@@ -44,14 +55,15 @@ export default class AdminHomeContainer extends Container {
       const response = await this.appContainer.apiv3.get('/admin-home/');
       const response = await this.appContainer.apiv3.get('/admin-home/');
       const { adminHomeParams } = response.data;
       const { adminHomeParams } = response.data;
 
 
-      this.setState({
+      this.setState(prevState => ({
+        ...prevState,
         growiVersion: adminHomeParams.growiVersion,
         growiVersion: adminHomeParams.growiVersion,
         nodeVersion: adminHomeParams.nodeVersion,
         nodeVersion: adminHomeParams.nodeVersion,
         npmVersion: adminHomeParams.npmVersion,
         npmVersion: adminHomeParams.npmVersion,
         yarnVersion: adminHomeParams.yarnVersion,
         yarnVersion: adminHomeParams.yarnVersion,
         installedPlugins: adminHomeParams.installedPlugins,
         installedPlugins: adminHomeParams.installedPlugins,
         envVars: adminHomeParams.envVars,
         envVars: adminHomeParams.envVars,
-      });
+      }));
     }
     }
     catch (err) {
     catch (err) {
       logger.error(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,
       registrationMode: this.dummyRegistrationMode,
       registrationWhiteList: [],
       registrationWhiteList: [],
       useOnlyEnvVars: false,
       useOnlyEnvVars: false,
+      isPasswordResetEnabled: false,
     };
     };
 
 
   }
   }
@@ -34,6 +35,7 @@ export default class AdminLocalSecurityContainer extends Container {
         useOnlyEnvVars: localSetting.useOnlyEnvVarsForSomeOptions,
         useOnlyEnvVars: localSetting.useOnlyEnvVarsForSomeOptions,
         registrationMode: localSetting.registrationMode,
         registrationMode: localSetting.registrationMode,
         registrationWhiteList: localSetting.registrationWhiteList,
         registrationWhiteList: localSetting.registrationWhiteList,
+        isPasswordResetEnabled: localSetting.isPasswordResetEnabled,
       });
       });
     }
     }
     catch (err) {
     catch (err) {
@@ -66,14 +68,22 @@ export default class AdminLocalSecurityContainer extends Container {
     this.setState({ registrationWhiteList: value.split('\n') });
     this.setState({ registrationWhiteList: value.split('\n') });
   }
   }
 
 
+  /**
+   * Switch password reset enabled
+   */
+  switchIsPasswordResetEnabled() {
+    this.setState({ isPasswordResetEnabled: !this.state.isPasswordResetEnabled });
+  }
+
   /**
   /**
    * update local security setting
    * update local security setting
    */
    */
   async updateLocalSecuritySetting() {
   async updateLocalSecuritySetting() {
-    const { registrationWhiteList } = this.state;
+    const { registrationWhiteList, isPasswordResetEnabled } = this.state;
     const response = await this.appContainer.apiv3.put('/security-setting/local-setting', {
     const response = await this.appContainer.apiv3.put('/security-setting/local-setting', {
       registrationMode: this.state.registrationMode,
       registrationMode: this.state.registrationMode,
       registrationWhiteList,
       registrationWhiteList,
+      isPasswordResetEnabled,
     });
     });
 
 
     const { localSettingParams } = response.data;
     const { localSettingParams } = response.data;
@@ -81,6 +91,7 @@ export default class AdminLocalSecurityContainer extends Container {
     this.setState({
     this.setState({
       registrationMode: localSettingParams.registrationMode,
       registrationMode: localSettingParams.registrationMode,
       registrationWhiteList: localSettingParams.registrationWhiteList,
       registrationWhiteList: localSettingParams.registrationWhiteList,
+      isPasswordResetEnabled: localSettingParams.isPasswordResetEnabled,
     });
     });
 
 
     return localSettingParams;
     return localSettingParams;

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

@@ -10,15 +10,14 @@ export default class AdminNotificationContainer extends Container {
     super();
     super();
 
 
     this.appContainer = appContainer;
     this.appContainer = appContainer;
-    this.dummyWebhookUrl = 0;
-    this.dummyWebhookUrlForError = 1;
 
 
     this.state = {
     this.state = {
       retrieveError: null,
       retrieveError: null,
-      selectSlackOption: 'Incoming Webhooks',
-      webhookUrl: this.dummyWebhookUrl,
-      isIncomingWebhookPrioritized: false,
-      slackToken: '',
+
+      isSlackbotConfigured: null,
+      isSlackLegacyConfigured: null,
+      currentBotType: null,
+
       userNotifications: [],
       userNotifications: [],
       isNotificationForOwnerPageEnabled: false,
       isNotificationForOwnerPageEnabled: false,
       isNotificationForGroupPageEnabled: false,
       isNotificationForGroupPageEnabled: false,
@@ -42,9 +41,10 @@ export default class AdminNotificationContainer extends Container {
     const { notificationParams } = response.data;
     const { notificationParams } = response.data;
 
 
     this.setState({
     this.setState({
-      webhookUrl: notificationParams.webhookUrl,
-      isIncomingWebhookPrioritized: notificationParams.isIncomingWebhookPrioritized,
-      slackToken: notificationParams.slackToken,
+      isSlackbotConfigured: notificationParams.isSlackbotConfigured,
+      isSlackLegacyConfigured: notificationParams.isSlackLegacyConfigured,
+      currentBotType: notificationParams.currentBotType,
+
       userNotifications: notificationParams.userNotifications,
       userNotifications: notificationParams.userNotifications,
       isNotificationForOwnerPageEnabled: notificationParams.isNotificationForOwnerPageEnabled,
       isNotificationForOwnerPageEnabled: notificationParams.isNotificationForOwnerPageEnabled,
       isNotificationForGroupPageEnabled: notificationParams.isNotificationForGroupPageEnabled,
       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
    * Update slackAppConfiguration
    * @memberOf SlackAppConfiguration
    * @memberOf SlackAppConfiguration

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

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

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

@@ -1,7 +1,7 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 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 { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 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 entities from 'entities';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { isTrashPage } from '~/utils/path-utils';
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
 
 
 import {
 import {
@@ -16,6 +16,8 @@ import {
   DrawioInterceptor,
   DrawioInterceptor,
 } from '../util/interceptor/drawio-interceptor';
 } from '../util/interceptor/drawio-interceptor';
 
 
+const { isTrashPage } = pagePathUtils;
+
 const logger = loggerFactory('growi:services:PageContainer');
 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) => {
 export const toastError = (err, header = 'Error', option = toastrOption.error) => {
   const errs = toArrayIfNot(err);
   const errs = toArrayIfNot(err);
 
 
+  if (err.length === 0) {
+    toastr.error('', header);
+  }
+
   for (const err of errs) {
   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 React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
@@ -63,6 +64,26 @@ class AdminHome extends React.Component {
             {adminHomeContainer.state.envVars && <EnvVarsTable envVars={adminHomeContainer.state.envVars} />}
             {adminHomeContainer.state.envVars && <EnvVarsTable envVars={adminHomeContainer.state.envVars} />}
           </div>
           </div>
         </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>
       </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 { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 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 { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 const logger = loggerFactory('growi:slackAppConfiguration');
 const logger = loggerFactory('growi:slackAppConfiguration');
 
 
-class SlackAppConfiguration extends React.Component {
+class SlackConfiguration extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
@@ -22,10 +22,10 @@ class SlackAppConfiguration extends React.Component {
   }
   }
 
 
   async onClickSubmit() {
   async onClickSubmit() {
-    const { t, adminNotificationContainer } = this.props;
+    const { t, adminSlackIntegrationLegacyContainer } = this.props;
 
 
     try {
     try {
-      await adminNotificationContainer.updateSlackAppConfiguration();
+      await adminSlackIntegrationLegacyContainer.updateSlackAppConfiguration();
       toastSuccess(t('notification_setting.updated_slackApp'));
       toastSuccess(t('notification_setting.updated_slackApp'));
     }
     }
     catch (err) {
     catch (err) {
@@ -35,7 +35,7 @@ class SlackAppConfiguration extends React.Component {
   }
   }
 
 
   render() {
   render() {
-    const { t, adminNotificationContainer } = this.props;
+    const { t, adminSlackIntegrationLegacyContainer } = this.props;
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
@@ -50,18 +50,18 @@ class SlackAppConfiguration extends React.Component {
                 aria-haspopup="true"
                 aria-haspopup="true"
                 aria-expanded="true"
                 aria-expanded="true"
               >
               >
-                {`Slack ${adminNotificationContainer.state.selectSlackOption}`}
+                {`Slack ${adminSlackIntegrationLegacyContainer.state.selectSlackOption}`}
               </button>
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
               <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
                   Slack Incoming Webhooks
                 </button>
                 </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>
           </div>
           </div>
         </div>
         </div>
-        {adminNotificationContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
+        {adminSlackIntegrationLegacyContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
           <React.Fragment>
           <React.Fragment>
             <h2 className="border-bottom mb-5">{t('notification_setting.slack_incoming_configuration')}</h2>
             <h2 className="border-bottom mb-5">{t('notification_setting.slack_incoming_configuration')}</h2>
 
 
@@ -71,8 +71,8 @@ class SlackAppConfiguration extends React.Component {
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   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>
             </div>
             </div>
@@ -84,8 +84,8 @@ class SlackAppConfiguration extends React.Component {
                     type="checkbox"
                     type="checkbox"
                     className="custom-control-input"
                     className="custom-control-input"
                     id="cbPrioritizeIWH"
                     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">
                   <label className="custom-control-label" htmlFor="cbPrioritizeIWH">
                     {t('notification_setting.prioritize_webhook')}
                     {t('notification_setting.prioritize_webhook')}
@@ -111,7 +111,7 @@ class SlackAppConfiguration extends React.Component {
                 <a
                 <a
                   href="#slack-incoming-webhooks"
                   href="#slack-incoming-webhooks"
                   data-toggle="tab"
                   data-toggle="tab"
-                  onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}
+                  onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}
                 >
                 >
                   {t('notification_setting.use_instead')}
                   {t('notification_setting.use_instead')}
                 </a>
                 </a>
@@ -123,8 +123,8 @@ class SlackAppConfiguration extends React.Component {
                   <input
                   <input
                     className="form-control"
                     className="form-control"
                     type="text"
                     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>
               </div>
               </div>
@@ -135,7 +135,7 @@ class SlackAppConfiguration extends React.Component {
 
 
         <AdminUpdateButtonRow
         <AdminUpdateButtonRow
           onClick={this.onClickSubmit}
           onClick={this.onClickSubmit}
-          disabled={adminNotificationContainer.state.retrieveError != null}
+          disabled={adminSlackIntegrationLegacyContainer.state.retrieveError != null}
         />
         />
 
 
         <hr />
         <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
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   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 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 loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -19,9 +27,76 @@ import GlobalNotification from './GlobalNotification';
 const logger = loggerFactory('growi:NotificationSetting');
 const logger = loggerFactory('growi:NotificationSetting');
 
 
 let retrieveErrors = null;
 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) {
 function NotificationSetting(props) {
   const { adminNotificationContainer } = props;
   const { adminNotificationContainer } = props;
 
 
+  const { t } = useTranslation();
+
+  const [isMounted, setMounted] = useState(false);
   const [activeTab, setActiveTab] = useState('user_trigger_notification');
   const [activeTab, setActiveTab] = useState('user_trigger_notification');
   const [activeComponents, setActiveComponents] = useState(new Set(['user_trigger_notification']));
   const [activeComponents, setActiveComponents] = useState(new Set(['user_trigger_notification']));
 
 
@@ -30,24 +105,24 @@ function NotificationSetting(props) {
     setActiveComponents(activeComponents.add(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 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(() => {
   const navTabMapping = useMemo(() => {
     return {
     return {
@@ -64,8 +139,27 @@ function NotificationSetting(props) {
     };
     };
   }, []);
   }, []);
 
 
+  const { isSlackbotConfigured, isSlackLegacyConfigured, currentBotType } = adminNotificationContainer.state;
+  const isSlackEnabled = isSlackbotConfigured;
+  const isSlackLegacyEnabled = !isSlackbotConfigured && isSlackLegacyConfigured;
+
   return (
   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 />
       <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
 
 
       <TabContent activeTab={activeTab} className="p-5">
       <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() {
   render() {
     const { t, adminGeneralSecurityContainer, adminLocalSecurityContainer } = this.props;
     const { t, adminGeneralSecurityContainer, adminLocalSecurityContainer } = this.props;
-    const { registrationMode } = adminLocalSecurityContainer.state;
+    const { registrationMode, isPasswordResetEnabled } = adminLocalSecurityContainer.state;
     const { isLocalEnabled } = adminGeneralSecurityContainer.state;
     const { isLocalEnabled } = adminGeneralSecurityContainer.state;
 
 
     return (
     return (
@@ -157,6 +157,27 @@ class LocalSecuritySettingContents extends React.Component {
               </div>
               </div>
             </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="row my-3">
               <div className="offset-3 col-6">
               <div className="offset-3 col-6">
                 <button
                 <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 PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
+import { SlackbotType } from '@growi/slack';
 
 
 const botDetails = {
 const botDetails = {
   officialBot: {
   officialBot: {
-    botType: 'officialBot',
+    botType: SlackbotType.OFFICIAL,
     botTypeCategory: 'official_bot',
     botTypeCategory: 'official_bot',
     setUp: 'easy',
     setUp: 'easy',
     multiWSIntegration: 'possible',
     multiWSIntegration: 'possible',
     securityControl: 'impossible',
     securityControl: 'impossible',
   },
   },
   customBotWithoutProxy: {
   customBotWithoutProxy: {
-    botType: 'customBotWithoutProxy',
+    botType: SlackbotType.CUSTOM_WITHOUT_PROXY,
     botTypeCategory: 'custom_bot',
     botTypeCategory: 'custom_bot',
     supplementaryBotName: 'without_proxy',
     supplementaryBotName: 'without_proxy',
     setUp: 'normal',
     setUp: 'normal',
@@ -20,7 +21,7 @@ const botDetails = {
     securityControl: 'possible',
     securityControl: 'possible',
   },
   },
   customBotWithProxy: {
   customBotWithProxy: {
-    botType: 'customBotWithProxy',
+    botType: SlackbotType.CUSTOM_WITH_PROXY,
     botTypeCategory: 'custom_bot',
     botTypeCategory: 'custom_bot',
     supplementaryBotName: 'with_proxy',
     supplementaryBotName: 'with_proxy',
     setUp: 'hard',
     setUp: 'hard',
@@ -32,6 +33,8 @@ const botDetails = {
 const BotTypeCard = (props) => {
 const BotTypeCard = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
+  const isBotTypeOfficial = props.botType === SlackbotType.OFFICIAL;
+
   return (
   return (
     <div
     <div
       className={`card admin-bot-card rounded border-radius-sm shadow ${props.isActive ? 'border-primary' : ''}`}
       className={`card admin-bot-card rounded border-radius-sm shadow ${props.isActive ? 'border-primary' : ''}`}
@@ -41,7 +44,7 @@ const BotTypeCard = (props) => {
     >
     >
       <div>
       <div>
         <h3 className={`card-header mb-0 py-3
         <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' : ''}`}
               ${props.isActive ? 'bg-primary grw-botcard-title-active' : ''}`}
         >
         >
           <span className="mr-2">
           <span className="mr-2">
@@ -49,7 +52,7 @@ const BotTypeCard = (props) => {
           </span>
           </span>
 
 
           {/*  A recommended badge is shown on official bot card, supplementary names are shown on Custom bot cards   */}
           {/*  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">
               <span className="badge badge-info mr-2">
                 {t('admin:slack_integration.selecting_bot_types.recommended')}
                 {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 { useTranslation } from 'react-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -8,12 +9,15 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import WithProxyAccordions from './WithProxyAccordions';
 import WithProxyAccordions from './WithProxyAccordions';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 
 
-const logger = loggerFactory('growi:SlackBotSettings');
+const logger = loggerFactory('growi:cli:SlackIntegration:CustomBotWithProxySettings');
 
 
 const CustomBotWithProxySettings = (props) => {
 const CustomBotWithProxySettings = (props) => {
   const {
   const {
-    appContainer, slackAppIntegrations, proxyServerUri, onClickAddSlackWorkspaceBtn, connectionStatuses, onUpdateTokens, onSubmitForm,
+    appContainer, slackAppIntegrations, proxyServerUri,
+    onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
+    connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   } = props;
   const [newProxyServerUri, setNewProxyServerUri] = useState();
   const [newProxyServerUri, setNewProxyServerUri] = useState();
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   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() => {
   const deleteSlackAppIntegrationHandler = async() => {
     try {
     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) {
       if (props.onDeleteSlackAppIntegration != null) {
         props.onDeleteSlackAppIntegration();
         props.onDeleteSlackAppIntegration();
       }
       }
-      toastSuccess(t('toaster.delete_slack_integration_procedure'));
+      toastSuccess(t('admin:slack_integration.toastr.delete_slack_integration_procedure'));
     }
     }
     catch (err) {
     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' }));
       toastSuccess(t('toaster.update_successed', { target: 'Proxy URL' }));
     }
     }
     catch (err) {
     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}`}>
                 <h2 id={_id || `settings-accordions-${i}`}>
                   {(workspaceName != null) ? `${workspaceName} Work Space` : `Settings #${i}`}
                   {(workspaceName != null) ? `${workspaceName} Work Space` : `Settings #${i}`}
                 </h2>
                 </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>
               </div>
               <WithProxyAccordions
               <WithProxyAccordions
                 botType="customBotWithProxy"
                 botType="customBotWithProxy"
@@ -168,6 +189,7 @@ CustomBotWithProxySettings.propTypes = {
   slackAppIntegrations: PropTypes.array,
   slackAppIntegrations: PropTypes.array,
   proxyServerUri: PropTypes.string,
   proxyServerUri: PropTypes.string,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
+  onPrimaryUpdated: PropTypes.func,
   onDeleteSlackAppIntegration: PropTypes.func,
   onDeleteSlackAppIntegration: PropTypes.func,
   onSubmitForm: PropTypes.func,
   onSubmitForm: PropTypes.func,
   connectionStatuses: PropTypes.object.isRequired,
   connectionStatuses: PropTypes.object.isRequired,

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

@@ -67,8 +67,8 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
             readOnly
             readOnly
           />
           />
           <p className="form-text text-muted">
           <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>
           </p>
         </div>
         </div>
 
 
@@ -97,7 +97,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
           />
           />
           <p className="form-text text-muted">
           <p className="form-text text-muted">
             {/* eslint-disable-next-line react/no-danger */}
             {/* 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>
           </p>
         </div>
         </div>
 
 

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

@@ -51,7 +51,7 @@ const ManageCommandsProcess = ({
 
 
   const updateCommandsHandler = async() => {
   const updateCommandsHandler = async() => {
     try {
     try {
-      await apiv3Put(`/slack-integration-settings/${slackAppIntegrationId}/supported-commands`, {
+      await apiv3Put(`/slack-integration-settings/slack-app-integrations/${slackAppIntegrationId}/supported-commands`, {
         supportedCommandsForBroadcastUse: Array.from(selectedCommandsForBroadcastUse),
         supportedCommandsForBroadcastUse: Array.from(selectedCommandsForBroadcastUse),
         supportedCommandsForSingleUse: Array.from(selectedCommandsForSingleUse),
         supportedCommandsForSingleUse: Array.from(selectedCommandsForSingleUse),
       });
       });
@@ -75,18 +75,19 @@ const ManageCommandsProcess = ({
           <div className="custom-control custom-checkbox">
           <div className="custom-control custom-checkbox">
             <div className="row mb-5">
             <div className="row mb-5">
               {defaultSupportedCommandsNameForBroadcastUse.map((commandName) => {
               {defaultSupportedCommandsNameForBroadcastUse.map((commandName) => {
+                const checkboxId = `${commandName}-${slackAppIntegrationId}`;
                 return (
                 return (
                   <div className="col-sm-6 my-1" key={commandName}>
                   <div className="col-sm-6 my-1" key={commandName}>
                     <input
                     <input
                       type="checkbox"
                       type="checkbox"
                       className="custom-control-input"
                       className="custom-control-input"
-                      id={commandName}
+                      id={checkboxId}
                       name={commandName}
                       name={commandName}
                       value={commandName}
                       value={commandName}
                       checked={selectedCommandsForBroadcastUse.has(commandName)}
                       checked={selectedCommandsForBroadcastUse.has(commandName)}
                       onChange={toggleCheckboxForBroadcast}
                       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}
                       {commandName}
                     </label>
                     </label>
                   </div>
                   </div>
@@ -100,18 +101,19 @@ const ManageCommandsProcess = ({
           <div className="custom-control custom-checkbox">
           <div className="custom-control custom-checkbox">
             <div className="row mb-5">
             <div className="row mb-5">
               {defaultSupportedCommandsNameForSingleUse.map((commandName) => {
               {defaultSupportedCommandsNameForSingleUse.map((commandName) => {
+                const checkboxId = `${commandName}-${slackAppIntegrationId}`;
                 return (
                 return (
                   <div className="col-sm-6 my-1" key={commandName}>
                   <div className="col-sm-6 my-1" key={commandName}>
                     <input
                     <input
                       type="checkbox"
                       type="checkbox"
                       className="custom-control-input"
                       className="custom-control-input"
-                      id={commandName}
+                      id={checkboxId}
                       name={commandName}
                       name={commandName}
                       value={commandName}
                       value={commandName}
                       checked={selectedCommandsForSingleUse.has(commandName)}
                       checked={selectedCommandsForSingleUse.has(commandName)}
                       onChange={toggleCheckboxForSingleUse}
                       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}
                       {commandName}
                     </label>
                     </label>
                   </div>
                   </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 PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+
+import { SlackbotType } from '@growi/slack';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -8,12 +11,15 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import WithProxyAccordions from './WithProxyAccordions';
 import WithProxyAccordions from './WithProxyAccordions';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 
 
-const logger = loggerFactory('growi:SlackBotSettings');
+const logger = loggerFactory('growi:cli:SlackIntegration:OfficialBotSettings');
 
 
 const OfficialBotSettings = (props) => {
 const OfficialBotSettings = (props) => {
   const {
   const {
-    appContainer, slackAppIntegrations, onClickAddSlackWorkspaceBtn, connectionStatuses, onUpdateTokens, onSubmitForm,
+    appContainer, slackAppIntegrations,
+    onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
+    connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   } = props;
   const [siteName, setSiteName] = useState('');
   const [siteName, setSiteName] = useState('');
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   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() => {
   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 {
     try {
       if (props.onDeleteSlackAppIntegration != null) {
       if (props.onDeleteSlackAppIntegration != null) {
         props.onDeleteSlackAppIntegration();
         props.onDeleteSlackAppIntegration();
       }
       }
-      toastSuccess(t('toaster.delete_slack_integration_procedure'));
+      toastSuccess(t('admin:slack_integration.toastr.delete_slack_integration_procedure'));
     }
     }
     catch (err) {
     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}`}>
                 <h2 id={_id || `settings-accordions-${i}`}>
                   {(workspaceName != null) ? `${workspaceName} Work Space` : `Settings #${i}`}
                   {(workspaceName != null) ? `${workspaceName} Work Space` : `Settings #${i}`}
                 </h2>
                 </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>
               </div>
               <WithProxyAccordions
               <WithProxyAccordions
-                botType="officialBot"
+                botType={SlackbotType.OFFICIAL}
                 slackAppIntegrationId={slackAppIntegration._id}
                 slackAppIntegrationId={slackAppIntegration._id}
                 tokenGtoP={tokenGtoP}
                 tokenGtoP={tokenGtoP}
                 tokenPtoG={tokenPtoG}
                 tokenPtoG={tokenPtoG}
@@ -133,6 +156,7 @@ OfficialBotSettings.propTypes = {
 
 
   slackAppIntegrations: PropTypes.array,
   slackAppIntegrations: PropTypes.array,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
+  onPrimaryUpdated: PropTypes.func,
   onDeleteSlackAppIntegration: PropTypes.func,
   onDeleteSlackAppIntegration: PropTypes.func,
   connectionStatuses: PropTypes.object.isRequired,
   connectionStatuses: PropTypes.object.isRequired,
   onUpdateTokens: PropTypes.func,
   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 React, { useState, useEffect, useCallback } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+
+import { SlackbotType } from '@growi/slack';
+
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
@@ -12,7 +15,7 @@ import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import BotTypeCard from './BotTypeCard';
 import BotTypeCard from './BotTypeCard';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 
 
-const botTypes = ['officialBot', 'customBotWithoutProxy', 'customBotWithProxy'];
+const botTypes = Object.values(SlackbotType);
 
 
 const SlackIntegration = (props) => {
 const SlackIntegration = (props) => {
 
 
@@ -72,7 +75,7 @@ const SlackIntegration = (props) => {
 
 
   const createSlackIntegrationData = async() => {
   const createSlackIntegrationData = async() => {
     try {
     try {
-      await appContainer.apiv3.put('/slack-integration-settings/slack-app-integrations');
+      await appContainer.apiv3.post('/slack-integration-settings/slack-app-integrations');
       fetchSlackIntegrationData();
       fetchSlackIntegrationData();
       toastSuccess(t('admin:slack_integration.adding_slack_ws_integration_settings_successful'));
       toastSuccess(t('admin:slack_integration.adding_slack_ws_integration_settings_successful'));
     }
     }
@@ -125,11 +128,12 @@ const SlackIntegration = (props) => {
   let settingsComponent = null;
   let settingsComponent = null;
 
 
   switch (currentBotType) {
   switch (currentBotType) {
-    case 'officialBot':
+    case SlackbotType.OFFICIAL:
       settingsComponent = (
       settingsComponent = (
         <OfficialBotSettings
         <OfficialBotSettings
           slackAppIntegrations={slackAppIntegrations}
           slackAppIntegrations={slackAppIntegrations}
           onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
           onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
+          onPrimaryUpdated={fetchSlackIntegrationData}
           onDeleteSlackAppIntegration={fetchSlackIntegrationData}
           onDeleteSlackAppIntegration={fetchSlackIntegrationData}
           connectionStatuses={connectionStatuses}
           connectionStatuses={connectionStatuses}
           onUpdateTokens={fetchSlackIntegrationData}
           onUpdateTokens={fetchSlackIntegrationData}
@@ -137,7 +141,7 @@ const SlackIntegration = (props) => {
         />
         />
       );
       );
       break;
       break;
-    case 'customBotWithoutProxy':
+    case SlackbotType.CUSTOM_WITHOUT_PROXY:
       settingsComponent = (
       settingsComponent = (
         <CustomBotWithoutProxySettings
         <CustomBotWithoutProxySettings
           slackBotTokenEnv={slackBotTokenEnv}
           slackBotTokenEnv={slackBotTokenEnv}
@@ -150,12 +154,13 @@ const SlackIntegration = (props) => {
         />
         />
       );
       );
       break;
       break;
-    case 'customBotWithProxy':
+    case SlackbotType.CUSTOM_WITH_PROXY:
       settingsComponent = (
       settingsComponent = (
         <CustomBotWithProxySettings
         <CustomBotWithProxySettings
           slackAppIntegrations={slackAppIntegrations}
           slackAppIntegrations={slackAppIntegrations}
           proxyServerUri={proxyServerUri}
           proxyServerUri={proxyServerUri}
           onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
           onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
+          onPrimaryUpdated={fetchSlackIntegrationData}
           onDeleteSlackAppIntegration={fetchSlackIntegrationData}
           onDeleteSlackAppIntegration={fetchSlackIntegrationData}
           connectionStatuses={connectionStatuses}
           connectionStatuses={connectionStatuses}
           onUpdateTokens={fetchSlackIntegrationData}
           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 PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
+
+import { SlackbotType } from '@growi/slack';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -67,15 +70,16 @@ const BotInstallProcessForCustomBotWithProxy = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   return (
   return (
     <div className="container w-75 py-5">
     <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" />
       <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-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" />
       <img src="/images/slack-integration/slack-bot-install-your-app-introduction-to-channel.png" className="border border-light img-fluid" />
     </div>
     </div>
@@ -86,17 +90,27 @@ const RegisteringProxyUrlProcess = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   return (
   return (
     <div className="container w-75 py-5">
     <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>
     </div>
   );
   );
 };
 };
@@ -107,7 +121,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
 
 
   const regenerateTokensHandler = async() => {
   const regenerateTokensHandler = async() => {
     try {
     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) {
       if (props.onUpdateTokens != null) {
         props.onUpdateTokens();
         props.onUpdateTokens();
       }
       }
@@ -215,7 +229,7 @@ const TestProcess = ({
   const submitForm = async(e) => {
   const submitForm = async(e) => {
     e.preventDefault();
     e.preventDefault();
     try {
     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);
       const newLogs = addLogs(logsValue, successMessage, null);
       setLogsValue(newLogs);
       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',
       title: 'test_connection',
       content: <TestProcess
       content: <TestProcess
         apiv3Post={props.appContainer.apiv3.post}
         apiv3Post={props.appContainer.apiv3.post}
@@ -327,6 +332,15 @@ const WithProxyAccordions = (props) => {
         isLatestConnectionSuccess={isLatestConnectionSuccess}
         isLatestConnectionSuccess={isLatestConnectionSuccess}
       />,
       />,
     },
     },
+    '④': {
+      title: 'manage_commands',
+      content: <ManageCommandsProcess
+        apiv3Put={props.appContainer.apiv3.put}
+        slackAppIntegrationId={props.slackAppIntegrationId}
+        supportedCommandsForBroadcastUse={props.supportedCommandsForBroadcastUse}
+        supportedCommandsForSingleUse={props.supportedCommandsForSingleUse}
+      />,
+    },
   };
   };
 
 
   const CustomBotIntegrationProcedure = {
   const CustomBotIntegrationProcedure = {
@@ -353,15 +367,6 @@ const WithProxyAccordions = (props) => {
       content: <RegisteringProxyUrlProcess />,
       content: <RegisteringProxyUrlProcess />,
     },
     },
     '⑤': {
     '⑤': {
-      title: 'manage_commands',
-      content: <ManageCommandsProcess
-        apiv3Put={props.appContainer.apiv3.put}
-        slackAppIntegrationId={props.slackAppIntegrationId}
-        supportedCommandsForBroadcastUse={props.supportedCommandsForBroadcastUse}
-        supportedCommandsForSingleUse={props.supportedCommandsForSingleUse}
-      />,
-    },
-    '⑥': {
       title: 'test_connection',
       title: 'test_connection',
       content: <TestProcess
       content: <TestProcess
         apiv3Post={props.appContainer.apiv3.post}
         apiv3Post={props.appContainer.apiv3.post}
@@ -371,9 +376,18 @@ const WithProxyAccordions = (props) => {
         isLatestConnectionSuccess={isLatestConnectionSuccess}
         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 (
   return (
     <div
     <div
@@ -406,7 +420,7 @@ const WithProxyAccordions = (props) => {
 const WithProxyAccordionsWrapper = withUnstatedContainers(WithProxyAccordions, [AppContainer]);
 const WithProxyAccordionsWrapper = withUnstatedContainers(WithProxyAccordions, [AppContainer]);
 WithProxyAccordions.propTypes = {
 WithProxyAccordions.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  botType: PropTypes.string.isRequired,
+  botType: PropTypes.oneOf(Object.values(SlackbotType)).isRequired,
   slackAppIntegrationId: PropTypes.string.isRequired,
   slackAppIntegrationId: PropTypes.string.isRequired,
   tokenPtoG: PropTypes.string,
   tokenPtoG: PropTypes.string,
   tokenGtoP: 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 { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import UserPicture from '../../User/UserPicture';
 
 
 class UserGroupUserFormByInput extends React.Component {
 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 { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 
 
-import UserPicture from '../../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 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 { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 
 
-import UserPicture from '../../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import UserMenu from './UserMenu';
 import UserMenu from './UserMenu';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 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 PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { convertToNewAffiliationPath } from '~/utils/path-utils';
+
+const { convertToNewAffiliationPath } = pagePathUtils;
 
 
 function ComparePathsTable(props) {
 function ComparePathsTable(props) {
   const {
   const {

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

@@ -1,7 +1,7 @@
 import React, { useMemo } from 'react';
 import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { isTopPage } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -10,6 +10,8 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
 
 
+const { isTopPage } = pagePathUtils;
+
 const WIKI_HEADER_LINK = 120;
 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 { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import { withUnstatedContainers } from './UnstatedUtils';
 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 PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { convertToNewAffiliationPath } from '~/utils/path-utils';
+
+const { convertToNewAffiliationPath } = pagePathUtils;
 
 
 function DuplicatedPathsTable(props) {
 function DuplicatedPathsTable(props) {
   const {
   const {

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

@@ -62,7 +62,7 @@ const Fab = (props) => {
   }
   }
 
 
   return (
   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()}
       {currentUser != null && renderPageCreateButton()}
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
         <button
         <button

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

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

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

@@ -1,9 +1,11 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { format } from 'date-fns';
 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 formatType = 'yyyy/MM/dd HH:mm';
 const AuthorInfo = (props) => {
 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 { withTranslation } from 'react-i18next';
 
 
+import { DevidedPagePath } from '@growi/core';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
-import DevidedPagePath from '~/models/devided-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 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 { UncontrolledTooltip } from 'reactstrap';
 
 
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
@@ -18,7 +19,6 @@ import {
   updateUserPreferenceWithOsSettings,
   updateUserPreferenceWithOsSettings,
 } from '~/client/util/color-scheme';
 } from '~/client/util/color-scheme';
 
 
-import UserPicture from '../User/UserPicture';
 
 
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';
 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 { 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 */
 /* eslint-disable react/prop-types */
 const DropdownItemContents = ({ title, contents }) => (
 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 { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { isTopPage } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -15,6 +15,8 @@ import CreateTemplateModal from '../CreateTemplateModal';
 import PagePresentationModal from '../PagePresentationModal';
 import PagePresentationModal from '../PagePresentationModal';
 import PresentationIcon from '../Icons/PresentationIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 
 
+const { isTopPage } = pagePathUtils;
+
 
 
 const PageManagement = (props) => {
 const PageManagement = (props) => {
   const {
   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 { withTranslation } from 'react-i18next';
 
 
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import UserPicture from '../User/UserPicture';
 import PutbackPageModal from '../PutbackPageModal';
 import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import PageDeleteModal from '../PageDeleteModal';
 import PageDeleteModal from '../PageDeleteModal';

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

@@ -6,7 +6,7 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import UserPicture from '../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import Username from '../User/Username';
 import Username from '../User/Username';
 
 
 export default class DeleteAttachmentModal extends React.Component {
 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 React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import Attachment from './Attachment';
+import { Attachment } from '@growi/ui';
 
 
 export default class PageAttachmentList extends React.Component {
 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 { UncontrolledTooltip } from 'reactstrap';
 
 
+import { UserPicture } from '@growi/ui';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 
 
@@ -13,7 +14,6 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import RevisionBody from '../Page/RevisionBody';
 import RevisionBody from '../Page/RevisionBody';
-import UserPicture from '../User/UserPicture';
 import Username from '../User/Username';
 import Username from '../User/Username';
 import CommentEditor from './CommentEditor';
 import CommentEditor from './CommentEditor';
 import CommentControl from './CommentControl';
 import CommentControl from './CommentControl';

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

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

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

@@ -7,7 +7,7 @@ import {
 
 
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 
 
-import UserPicture from '../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import Username from '../User/Username';
 import Username from '../User/Username';
 
 
 export default class DeleteCommentModal extends React.Component {
 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 { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 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 AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
@@ -19,6 +17,10 @@ import { toastError } from '~/client/util/apiNotification';
 
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 
+const {
+  userPageRoot, isCreatablePage, generateEditorPath,
+} = pagePathUtils;
+
 const PageCreateModal = (props) => {
 const PageCreateModal = (props) => {
   const { t, appContainer, navigationContainer } = 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" />
         <EditorIcon icon="CheckList" />
       </Button>,
       </Button>,
+      <Button
+        key="nav-item-attachment"
+        color={null}
+        size="sm"
+        title="Attachment"
+        onClick={this.props.onAddAttachmentButtonClicked}
+      >
+        <EditorIcon icon="Attachment" />
+      </Button>,
       <Button
       <Button
         key="nav-item-link"
         key="nav-item-link"
         color={null}
         color={null}
@@ -947,6 +956,7 @@ CodeMirrorEditor.propTypes = Object.assign({
   emojiStrategy: PropTypes.object,
   emojiStrategy: PropTypes.object,
   lineNumbers: PropTypes.bool,
   lineNumbers: PropTypes.bool,
   onMarkdownHelpButtonClicked: PropTypes.func,
   onMarkdownHelpButtonClicked: PropTypes.func,
+  onAddAttachmentButtonClicked: PropTypes.func,
 }, AbstractEditor.propTypes);
 }, AbstractEditor.propTypes);
 CodeMirrorEditor.defaultProps = {
 CodeMirrorEditor.defaultProps = {
   lineNumbers: true,
   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.dropHandler = this.dropHandler.bind(this);
 
 
     this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
     this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
+    this.addAttachmentHandler = this.addAttachmentHandler.bind(this);
 
 
     this.getAcceptableType = this.getAcceptableType.bind(this);
     this.getAcceptableType = this.getAcceptableType.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
@@ -187,6 +188,10 @@ export default class Editor extends AbstractEditor {
     this.setState({ isCheatsheetModalShown: true });
     this.setState({ isCheatsheetModalShown: true });
   }
   }
 
 
+  addAttachmentHandler() {
+    this.dropzone.open();
+  }
+
   getDropzoneClassName(isDragAccept, isDragReject) {
   getDropzoneClassName(isDragAccept, isDragReject) {
     let className = 'dropzone';
     let className = 'dropzone';
     if (!this.props.isUploadable) {
     if (!this.props.isUploadable) {
@@ -314,6 +319,7 @@ export default class Editor extends AbstractEditor {
                         onPasteFiles={this.pasteFilesHandler}
                         onPasteFiles={this.pasteFilesHandler}
                         onDragEnter={this.dragEnterHandler}
                         onDragEnter={this.dragEnterHandler}
                         onMarkdownHelpButtonClicked={this.showMarkdownHelp}
                         onMarkdownHelpButtonClicked={this.showMarkdownHelp}
+                        onAddAttachmentButtonClicked={this.addAttachmentHandler}
                         {...this.props}
                         {...this.props}
                       />
                       />
                     )}
                     )}
@@ -341,7 +347,7 @@ export default class Editor extends AbstractEditor {
             <button
             <button
               type="button"
               type="button"
               className="btn btn-outline-secondary btn-block btn-open-dropzone"
               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;
               <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
               Attach files
               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" />
           <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>
         </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 [isExpanded, setExpanded] = useState(false);
 
 
   const [isSlackExpanded, setSlackExpanded] = useState(false);
   const [isSlackExpanded, setSlackExpanded] = useState(false);
-  const hasSlackConfig = props.appContainer.getConfig().hasSlackConfig;
+  const isSlackConfigured = props.appContainer.getConfig().isSlackConfigured;
 
 
   const {
   const {
     navigationContainer,
     navigationContainer,
@@ -61,7 +61,7 @@ const EditorNavbarBottom = (props) => {
   return (
   return (
     <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `}>
     <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `}>
       {/* Collapsed SlackNotification */}
       {/* Collapsed SlackNotification */}
-      {hasSlackConfig && (
+      {isSlackConfigured && (
         <Collapse isOpen={isSlackExpanded && isDeviceSmallerThanMd}>
         <Collapse isOpen={isSlackExpanded && isDeviceSmallerThanMd}>
           <nav className={`navbar navbar-expand-lg border-top ${additionalClasses.join(' ')}`}>
           <nav className={`navbar navbar-expand-lg border-top ${additionalClasses.join(' ')}`}>
             <SlackNotification
             <SlackNotification
@@ -84,7 +84,7 @@ const EditorNavbarBottom = (props) => {
         <form className="form-inline flex-nowrap ml-auto">
         <form className="form-inline flex-nowrap ml-auto">
           {/* Responsive Design for the SlackNotification */}
           {/* Responsive Design for the SlackNotification */}
           {/* Button or the normal Slack banner */}
           {/* Button or the normal Slack banner */}
-          {hasSlackConfig && (isDeviceSmallerThanMd ? (
+          {isSlackConfigured && (isDeviceSmallerThanMd ? (
             <Button
             <Button
               className="grw-btn-slack border mr-2"
               className="grw-btn-slack border mr-2"
               onClick={() => (setSlackExpanded(!isSlackExpanded))}
               onClick={() => (setSlackExpanded(!isSlackExpanded))}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است