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

Merge branch 'feat/imprv/5613-accordion-component-v3' into feat/5630-accordion-translation

Steven Fukase 5 лет назад
Родитель
Сommit
4f80831c78
32 измененных файлов с 918 добавлено и 583 удалено
  1. 8 8
      .github/workflows/ci-slackbot-proxy.yml
  2. 13 11
      .github/workflows/ci.yml
  3. 1 200
      package.json
  4. 211 0
      packages/core/package.json
  5. 4 6
      packages/slack/package.json
  6. 1 0
      packages/slack/src/index.ts
  7. 31 0
      packages/slack/src/utils/block-creater.ts
  8. 101 0
      packages/slackbot-proxy/docker/Dockerfile
  9. 66 0
      packages/slackbot-proxy/docker/README.md
  10. 15 12
      packages/slackbot-proxy/package.json
  11. 10 5
      packages/slackbot-proxy/src/controllers/slack.ts
  12. 3 0
      packages/slackbot-proxy/src/interfaces/growi-commands-mappings.ts
  13. 9 0
      packages/slackbot-proxy/src/repositories/installation.ts
  14. 15 2
      packages/slackbot-proxy/src/services/InstallerService.ts
  15. 37 0
      packages/slackbot-proxy/src/services/RegisterService.ts
  16. BIN
      public/images/slack-integration/impossible.png
  17. BIN
      public/images/slack-integration/possible.png
  18. BIN
      public/images/slack-integration/triangle-basic-gray.png
  19. 5 3
      resource/locales/en_US/admin/admin.json
  20. 5 3
      resource/locales/ja_JP/admin/admin.json
  21. 5 3
      resource/locales/zh_CN/admin/admin.json
  22. 35 0
      src/client/js/components/Admin/Common/Accordion.jsx
  23. 99 0
      src/client/js/components/Admin/SlackIntegration/BotTypeCard.jsx
  24. 29 55
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  25. 26 2
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  26. 99 50
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  27. 15 137
      src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx
  28. 11 1
      src/client/styles/scss/_admin.scss
  29. 2 14
      src/client/styles/scss/theme/_apply-colors.scss
  30. 1 1
      src/server/routes/apiv3/slack-integration.js
  31. 1 1
      src/server/service/config-loader.js
  32. 60 69
      yarn.lock

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

@@ -31,7 +31,7 @@ jobs:
       id: cache-dependencies
       uses: actions/cache@v2
       with:
-        path: node_modules
+        path: '**/node_modules'
         key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
     - name: Get yarn cache dir
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
@@ -96,7 +96,7 @@ jobs:
       id: cache-dependencies
       uses: actions/cache@v2
       with:
-        path: node_modules
+        path: '**/node_modules'
         key: ${{ runner.OS }}-node_modules_dev-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
     - name: Get yarn cache dir
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
@@ -110,7 +110,7 @@ jobs:
         key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
           ${{ runner.os }}-yarn-
-    - name: Install dependencies
+    - name: lerna bootstrap
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
         npx lerna bootstrap
@@ -175,7 +175,7 @@ jobs:
     - name: Cache/Restore node_modules
       uses: actions/cache@v2
       with:
-        path: node_modules
+        path: '**/node_modules'
         key: ${{ runner.OS }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.YmdH }}
         restore-keys: |
           ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ymd }}
@@ -191,7 +191,7 @@ jobs:
         key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
           ${{ runner.os }}-yarn-
-    - name: Install dependencies
+    - name: lerna bootstrap
       run: |
         npx lerna bootstrap
     - name: Print dependencies
@@ -199,12 +199,12 @@ jobs:
         echo -n "node " && node -v
         echo -n "npm " && npm -v
         yarn list --depth=0
-    - name: yarn build
+    - name: lerna run build
       run: |
         yarn lerna run build
-    - name: yarn install --production
+    - name: lerna bootstrap --production
       run: |
-        yarn lerna exec "yarn install --production"
+        npx lerna bootstrap -- --production
     - name: Print dependencies
       run: |
         echo -n "node " && node -v

+ 13 - 11
.github/workflows/ci.yml

@@ -35,7 +35,7 @@ jobs:
       id: cache-dependencies
       uses: actions/cache@v2
       with:
-        path: node_modules
+        path: '**/node_modules'
         key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
     - name: Get yarn cache dir
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
@@ -52,7 +52,7 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        npx lerna bootstrap
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -100,7 +100,7 @@ jobs:
       id: cache-dependencies
       uses: actions/cache@v2
       with:
-        path: node_modules
+        path: '**/node_modules'
         key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
     - name: Get yarn cache dir
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
@@ -117,7 +117,7 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        npx lerna bootstrap
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -162,7 +162,7 @@ jobs:
       id: cache-dependencies
       uses: actions/cache@v2
       with:
-        path: node_modules
+        path: '**/node_modules'
         key: ${{ runner.OS }}-node_modules_dev-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
     - name: Get Date
       id: date
@@ -195,7 +195,8 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        npx lerna bootstrap
+        yarn lerna add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion --scope @growi/core
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -249,7 +250,7 @@ jobs:
     - name: Cache/Restore node_modules
       uses: actions/cache@v2
       with:
-        path: node_modules
+        path: '**/node_modules'
         key: ${{ runner.OS }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.YmdH }}
         restore-keys: |
           ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ymd }}
@@ -267,8 +268,9 @@ jobs:
           ${{ runner.os }}-yarn-
     - name: Install dependencies
       run: |
-        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
-        yarn add -W -D react-images@1.0.0 react-motion
+        npx lerna bootstrap
+        yarn lerna add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs --scope @growi/core
+        yarn lerna add -D react-images@1.0.0 react-motion --scope @growi/core
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -277,9 +279,9 @@ jobs:
     - name: yarn build:prod:analyze
       run: |
         yarn build:prod:analyze
-    - name: yarn install --production
+    - name: lerna bootstrap --production
       run: |
-        yarn install --production
+        npx lerna bootstrap -- --production
     - name: Print dependencies
       run: |
         echo -n "node " && node -v

+ 1 - 200
package.json

@@ -78,208 +78,9 @@
     "webpack": "webpack"
   },
   "dependencies": {
-    "//": [
-      "check-node-version: see https://github.com/parshap/check-node-version/issues/35",
-      "openid-client: Node.js 12 or higher is required for openid-client@3 and above."
-    ],
-    "@google-cloud/storage": "^3.3.0",
-    "@kobalab/socket.io-session": "^1.0.3",
-    "@promster/express": "^5.0.1",
-    "@promster/server": "^6.0.0",
-    "@slack/events-api": "^3.0.0",
-    "@slack/web-api": "^6.1.0",
-    "JSONStream": "^1.3.5",
-    "archiver": "^3.1.1",
-    "array.prototype.flatmap": "^1.2.2",
-    "async-canvas-to-blob": "^1.0.3",
-    "aws-sdk": "^2.88.0",
-    "axios": "^0.21.1",
-    "body-parser": "^1.18.2",
-    "bunyan": "^1.8.12",
-    "bunyan-format": "^0.2.1",
-    "check-node-version": "^4.0.2",
-    "connect-flash": "~0.1.1",
-    "connect-mongo": "^3.2.0",
-    "connect-redis": "^4.0.4",
-    "cookie-parser": "^1.4.3",
-    "cross-env": "^7.0.0",
-    "csrf": "^3.1.0",
-    "date-fns": "^2.0.0",
-    "detect-indent": "^6.0.0",
-    "diff": "^4.0.1",
-    "elasticsearch": "^16.0.0",
-    "entities": "^2.0.0",
-    "env-cmd": "^10.0.1",
-    "esa-nodejs": "^0.0.7",
-    "escape-string-regexp": "^2.0.0",
-    "express": "^4.16.1",
-    "express-bunyan-logger": "^1.3.3",
-    "express-form": "~0.12.0",
-    "express-session": "^1.16.1",
-    "express-validator": "^6.1.1",
-    "express-webpack-assets": "^0.1.0",
-    "graceful-fs": "^4.1.11",
-    "growi-commons": "^5.0.3",
-    "helmet": "^3.13.0",
-    "i18next": "^19.0.0",
-    "i18next-express-middleware": "^1.4.1",
-    "i18next-node-fs-backend": "^2.1.0",
-    "i18next-sprintf-postprocessor": "^0.2.2",
-    "is-iso-date": "^0.0.1",
-    "lucene-query-parser": "^1.2.0",
-    "md5": "^2.2.1",
-    "method-override": "^3.0.0",
-    "migrate-mongo": "^8.1.4",
-    "mkdirp": "^1.0.3",
-    "module-alias": "^2.0.6",
-    "mongoose": "5.10.11",
-    "mongoose-gridfs": "^1.2.42",
-    "mongoose-paginate-v2": "^1.3.9",
-    "mongoose-unique-validator": "^2.0.3",
-    "multer": "~1.4.0",
-    "multer-autoreap": "^1.0.3",
-    "nodemailer": "^6.0.0",
-    "nodemailer-ses-transport": "~1.5.0",
-    "npm-run-all": "^4.1.2",
-    "openid-client": "=2.5.0",
-    "package-installed-version-sync": "^2.1.0",
-    "passport": "^0.4.0",
-    "passport-github": "^1.1.0",
-    "passport-google-oauth20": "^2.0.0",
-    "passport-http": "^0.3.0",
-    "passport-ldapauth": "^2.0.0",
-    "passport-local": "^1.0.0",
-    "passport-saml": "^1.0.0",
-    "passport-twitter": "^1.0.4",
-    "prom-client": "^13.0.0",
-    "react-card-flip": "^1.0.10",
-    "react-image-crop": "^8.3.0",
-    "reconnecting-websocket": "^4.4.0",
-    "redis": "^3.0.2",
-    "rimraf": "^3.0.0",
-    "slack-node": "^0.1.8",
-    "socket.io": "^2.3.0",
-    "stream-to-promise": "^2.2.0",
-    "string-width": "^4.1.0",
-    "swig-templates": "^2.0.2",
-    "uglifycss": "^0.0.29",
-    "unzipper": "^0.10.5",
-    "url-join": "^4.0.0",
-    "validator": "^12.0.0",
-    "ws": "^7.3.1",
-    "xss": "^1.0.6"
   },
   "devDependencies": {
-    "//": [
-      "@handsontable/react: v3 requires handsontable >= 7.0.0.",
-      "handsontable: v7.0.0 or above is no loger MIT lisence."
-    ],
-    "@alienfast/i18next-loader": "^1.0.16",
-    "@atlaskit/drawer": "^5.3.7",
-    "@atlaskit/navigation-next": "^8.0.5",
-    "@babel/core": "^7.4.5",
-    "@babel/plugin-proposal-class-properties": "^7.8.3",
-    "@babel/plugin-proposal-optional-chaining": "^7.9.0",
-    "@babel/polyfill": "^7.4.4",
-    "@babel/preset-env": "^7.4.5",
-    "@babel/preset-react": "^7.0.0",
-    "@handsontable/react": "=2.1.0",
-    "@types/compression": "^1.7.0",
-    "@types/express": "^4.17.11",
-    "@types/multer": "^1.4.5",
-    "@types/node": "^14.14.35",
-    "autoprefixer": "^9.0.0",
-    "babel-eslint": "^10.0.1",
-    "babel-loader": "^8.0.6",
-    "babel-plugin-lodash": "^3.3.4",
-    "babel-plugin-transform-imports": "^2.0.0",
-    "bootstrap": "^4.5.0",
-    "browser-bunyan": "^1.3.0",
-    "browser-sync": "^2.26.3",
-    "bunyan-debug": "^2.0.0",
-    "cli": "~1.0.1",
-    "codemirror": "^5.48.4",
-    "colors": "^1.2.5",
-    "connect-browser-sync": "^2.1.0",
-    "core-js": "=2.6.9",
-    "css-loader": "^3.0.0",
-    "csv-to-markdown-table": "^1.0.1",
-    "diff2html": "^3.1.2",
-    "eazy-logger": "^3.0.2",
-    "eslint": "^6.0.1",
-    "eslint-config-weseek": "^1.0.8",
-    "eslint-plugin-import": "^2.18.0",
-    "eslint-plugin-jest": "^23.0.3",
-    "eslint-plugin-react": "^7.14.2",
-    "eslint-plugin-react-hooks": "^4.0.4",
-    "file-loader": "^5.0.2",
-    "handsontable": "=6.2.2",
-    "hard-source-webpack-plugin": "^0.13.1",
-    "i18next-browser-languagedetector": "^4.0.1",
-    "imports-loader": "^0.8.0",
-    "jest": "^25.1.0",
-    "jquery-slimscroll": "^1.3.8",
-    "jquery-ui": "^1.12.1",
-    "jquery.cookie": "~1.4.1",
-    "lerna": "^4.0.0",
-    "load-css-file": "^1.0.0",
-    "lodash-webpack-plugin": "^0.11.5",
-    "markdown-it": "^10.0.0",
-    "markdown-it-blockdiag": "^1.1.1",
-    "markdown-it-drawio-viewer": "^1.3.1",
-    "markdown-it-emoji": "^1.4.0",
-    "markdown-it-footnote": "^3.0.1",
-    "markdown-it-mathjax": "^2.0.0",
-    "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^1.3.0",
-    "markdown-it-task-checkbox": "^1.0.6",
-    "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
-    "markdown-table": "^1.1.1",
-    "mini-css-extract-plugin": "^0.9.0",
-    "morgan": "^1.9.0",
-    "node-dev": "^4.0.0",
-    "node-sass": "^4.14.1",
-    "normalize-path": "^3.0.0",
-    "null-loader": "^3.0.0",
-    "on-headers": "^1.0.1",
-    "optimize-css-assets-webpack-plugin": "^5.0.3",
-    "penpal": "^4.0.0",
-    "plantuml-encoder": "^1.2.5",
-    "postcss-loader": "^3.0.0",
-    "prettier": "^1.19.1",
-    "react": "^16.8.3",
-    "react-bootstrap-typeahead": "^3.4.7",
-    "react-codemirror2": "^6.0.0",
-    "react-copy-to-clipboard": "^5.0.1",
-    "react-dom": "^16.8.3",
-    "react-dropzone": "^11.2.4",
-    "react-frame-component": "^4.0.0",
-    "react-hotkeys": "^2.0.0",
-    "react-i18next": "^11.1.0",
-    "react-waypoint": "^9.0.0",
-    "reactstrap": "^8.0.1",
-    "replacestream": "^4.0.3",
-    "reveal.js": "^3.5.0",
-    "rs-i18n": "^0.0.9",
-    "sass-loader": "^8.0.0",
-    "simple-load-script": "^1.0.2",
-    "socket.io-client": "^2.3.0",
-    "sticky-events": "^3.1.3",
-    "style-loader": "^1.0.0",
-    "styled-components": "^5.0.1",
-    "stylelint": "^13.2.0",
-    "stylelint-config-recess-order": "^2.0.1",
-    "swagger-jsdoc": "^3.4.0",
-    "swagger2openapi": "^5.3.1",
-    "terser-webpack-plugin": "^4.1.0",
-    "throttle-debounce": "^2.0.0",
-    "toastr": "^2.1.2",
-    "unstated": "^2.1.1",
-    "webpack": "^4.39.3",
-    "webpack-assets-manifest": "^3.1.1",
-    "webpack-bundle-analyzer": "^3.0.2",
-    "webpack-cli": "^3.3.7",
-    "webpack-merge": "^4.2.2"
+    "lerna": "^4.0.0"
   },
   "_moduleAliases": {
     "@root": ".",

+ 211 - 0
packages/core/package.json

@@ -0,0 +1,211 @@
+{
+  "name": "@growi/core",
+  "version": "0.9.0-RC",
+  "license": "MIT",
+  "main": "dist/index.js",
+  "typings": "dist/index.d.ts",
+  "scripts": {
+  },
+  "// comments for dependencies": {
+    "openid-client": "Node.js 12 or higher is required for openid-client@3 and above."
+  },
+  "dependencies": {
+    "@google-cloud/storage": "^3.3.0",
+    "@kobalab/socket.io-session": "^1.0.3",
+    "@promster/express": "^5.0.1",
+    "@promster/server": "^6.0.0",
+    "@slack/events-api": "^3.0.0",
+    "@slack/web-api": "^6.1.0",
+    "JSONStream": "^1.3.5",
+    "archiver": "^3.1.1",
+    "array.prototype.flatmap": "^1.2.2",
+    "async-canvas-to-blob": "^1.0.3",
+    "aws-sdk": "^2.88.0",
+    "axios": "^0.21.1",
+    "body-parser": "^1.18.2",
+    "bunyan": "^1.8.12",
+    "bunyan-format": "^0.2.1",
+    "check-node-version": "^4.1.0",
+    "connect-flash": "~0.1.1",
+    "connect-mongo": "^3.2.0",
+    "connect-redis": "^4.0.4",
+    "cookie-parser": "^1.4.5",
+    "cross-env": "^7.0.0",
+    "csrf": "^3.1.0",
+    "date-fns": "^2.0.0",
+    "detect-indent": "^6.0.0",
+    "diff": "^4.0.1",
+    "elasticsearch": "^16.0.0",
+    "entities": "^2.0.0",
+    "env-cmd": "^10.0.1",
+    "esa-nodejs": "^0.0.7",
+    "escape-string-regexp": "^2.0.0",
+    "express": "^4.16.1",
+    "express-bunyan-logger": "^1.3.3",
+    "express-form": "~0.12.0",
+    "express-session": "^1.16.1",
+    "express-validator": "^6.1.1",
+    "express-webpack-assets": "^0.1.0",
+    "graceful-fs": "^4.1.11",
+    "growi-commons": "^5.0.3",
+    "helmet": "^3.13.0",
+    "i18next": "^19.0.0",
+    "i18next-express-middleware": "^1.4.1",
+    "i18next-node-fs-backend": "^2.1.0",
+    "i18next-sprintf-postprocessor": "^0.2.2",
+    "is-iso-date": "^0.0.1",
+    "lucene-query-parser": "^1.2.0",
+    "md5": "^2.2.1",
+    "method-override": "^3.0.0",
+    "migrate-mongo": "^8.1.4",
+    "mkdirp": "^1.0.3",
+    "module-alias": "^2.0.6",
+    "mongoose": "5.10.11",
+    "mongoose-gridfs": "^1.2.42",
+    "mongoose-paginate-v2": "^1.3.9",
+    "mongoose-unique-validator": "^2.0.3",
+    "multer": "~1.4.0",
+    "multer-autoreap": "^1.0.3",
+    "nodemailer": "^6.0.0",
+    "nodemailer-ses-transport": "~1.5.0",
+    "npm-run-all": "^4.1.2",
+    "openid-client": "=2.5.0",
+    "package-installed-version-sync": "^2.1.0",
+    "passport": "^0.4.0",
+    "passport-github": "^1.1.0",
+    "passport-google-oauth20": "^2.0.0",
+    "passport-http": "^0.3.0",
+    "passport-ldapauth": "^2.0.0",
+    "passport-local": "^1.0.0",
+    "passport-saml": "^1.0.0",
+    "passport-twitter": "^1.0.4",
+    "prom-client": "^13.0.0",
+    "react-card-flip": "^1.0.10",
+    "react-image-crop": "^8.3.0",
+    "reconnecting-websocket": "^4.4.0",
+    "redis": "^3.0.2",
+    "rimraf": "^3.0.0",
+    "slack-node": "^0.1.8",
+    "socket.io": "^2.3.0",
+    "stream-to-promise": "^2.2.0",
+    "string-width": "^4.1.0",
+    "swig-templates": "^2.0.2",
+    "uglifycss": "^0.0.29",
+    "unzipper": "^0.10.5",
+    "url-join": "^4.0.0",
+    "validator": "^12.0.0",
+    "ws": "^7.3.1",
+    "xss": "^1.0.6"
+  },
+  "// comments for defDependencies": {
+    "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
+    "handsontable": "v7.0.0 or above is no loger MIT lisence."
+  },
+  "devDependencies": {
+    "@alienfast/i18next-loader": "^1.0.16",
+    "@atlaskit/drawer": "^5.3.7",
+    "@atlaskit/navigation-next": "^8.0.5",
+    "@babel/core": "^7.4.5",
+    "@babel/plugin-proposal-class-properties": "^7.8.3",
+    "@babel/plugin-proposal-optional-chaining": "^7.9.0",
+    "@babel/polyfill": "^7.4.4",
+    "@babel/preset-env": "^7.4.5",
+    "@babel/preset-react": "^7.0.0",
+    "@handsontable/react": "=2.1.0",
+    "@types/compression": "^1.7.0",
+    "@types/express": "^4.17.11",
+    "@types/multer": "^1.4.5",
+    "@types/node": "^14.14.35",
+    "autoprefixer": "^9.0.0",
+    "babel-eslint": "^10.0.1",
+    "babel-loader": "^8.0.6",
+    "babel-plugin-lodash": "^3.3.4",
+    "babel-plugin-transform-imports": "^2.0.0",
+    "bootstrap": "^4.5.0",
+    "browser-bunyan": "^1.3.0",
+    "browser-sync": "^2.26.3",
+    "bunyan-debug": "^2.0.0",
+    "cli": "~1.0.1",
+    "codemirror": "^5.48.4",
+    "colors": "^1.2.5",
+    "connect-browser-sync": "^2.1.0",
+    "core-js": "=2.6.9",
+    "css-loader": "^3.0.0",
+    "csv-to-markdown-table": "^1.0.1",
+    "diff2html": "^3.1.2",
+    "eazy-logger": "^3.0.2",
+    "eslint": "^6.0.1",
+    "eslint-config-weseek": "^1.0.8",
+    "eslint-plugin-import": "^2.18.0",
+    "eslint-plugin-jest": "^23.0.3",
+    "eslint-plugin-react": "^7.14.2",
+    "eslint-plugin-react-hooks": "^4.0.4",
+    "file-loader": "^5.0.2",
+    "handsontable": "=6.2.2",
+    "hard-source-webpack-plugin": "^0.13.1",
+    "i18next-browser-languagedetector": "^4.0.1",
+    "imports-loader": "^0.8.0",
+    "jest": "^25.1.0",
+    "jquery-slimscroll": "^1.3.8",
+    "jquery-ui": "^1.12.1",
+    "jquery.cookie": "~1.4.1",
+    "load-css-file": "^1.0.0",
+    "lodash-webpack-plugin": "^0.11.5",
+    "markdown-it": "^10.0.0",
+    "markdown-it-blockdiag": "^1.1.1",
+    "markdown-it-drawio-viewer": "^1.3.1",
+    "markdown-it-emoji": "^1.4.0",
+    "markdown-it-footnote": "^3.0.1",
+    "markdown-it-mathjax": "^2.0.0",
+    "markdown-it-named-headers": "^0.0.4",
+    "markdown-it-plantuml": "^1.3.0",
+    "markdown-it-task-checkbox": "^1.0.6",
+    "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
+    "markdown-table": "^1.1.1",
+    "mini-css-extract-plugin": "^0.9.0",
+    "morgan": "^1.9.0",
+    "node-dev": "^4.0.0",
+    "node-sass": "^4.14.1",
+    "normalize-path": "^3.0.0",
+    "null-loader": "^3.0.0",
+    "on-headers": "^1.0.1",
+    "optimize-css-assets-webpack-plugin": "^5.0.3",
+    "penpal": "^4.0.0",
+    "plantuml-encoder": "^1.2.5",
+    "postcss-loader": "^3.0.0",
+    "prettier": "^1.19.1",
+    "react": "^16.8.3",
+    "react-bootstrap-typeahead": "^3.4.7",
+    "react-codemirror2": "^6.0.0",
+    "react-copy-to-clipboard": "^5.0.1",
+    "react-dom": "^16.8.3",
+    "react-dropzone": "^11.2.4",
+    "react-frame-component": "^4.0.0",
+    "react-hotkeys": "^2.0.0",
+    "react-i18next": "^11.1.0",
+    "react-waypoint": "^9.0.0",
+    "reactstrap": "^8.0.1",
+    "replacestream": "^4.0.3",
+    "reveal.js": "^3.5.0",
+    "rs-i18n": "^0.0.9",
+    "sass-loader": "^8.0.0",
+    "simple-load-script": "^1.0.2",
+    "socket.io-client": "^2.3.0",
+    "sticky-events": "^3.1.3",
+    "style-loader": "^1.0.0",
+    "styled-components": "^5.0.1",
+    "stylelint": "^13.2.0",
+    "stylelint-config-recess-order": "^2.0.1",
+    "swagger-jsdoc": "^3.4.0",
+    "swagger2openapi": "^5.3.1",
+    "terser-webpack-plugin": "^4.1.0",
+    "throttle-debounce": "^2.0.0",
+    "toastr": "^2.1.2",
+    "unstated": "^2.1.1",
+    "webpack": "^4.39.3",
+    "webpack-assets-manifest": "^3.1.1",
+    "webpack-bundle-analyzer": "^3.0.2",
+    "webpack-cli": "^3.3.7",
+    "webpack-merge": "^4.2.2"
+  }
+}

+ 4 - 6
packages/slack/package.json

@@ -5,8 +5,9 @@
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "scripts": {
-    "build": "yarn tsc",
-    "tsc": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
+    "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
+    "tsc": "tsc -p tsconfig.build.json",
+    "tsc:w": "yarn tsc -w",
     "test": "yarn test:lint && yarn test:coverage",
     "test:unit": "cross-env NODE_ENV=test jest --passWithNoTests",
     "test:coverage": "yarn test:unit",
@@ -18,13 +19,10 @@
   },
   "devDependencies": {
     "@slack/bolt": "^3.3.0",
-    "@tsed/core": "^6.34.3",
-    "@tsed/exceptions": "^6.34.3",
-    "@tsed/json-mapper": "^6.34.3",
-    "@tsed/schema": "^6.34.3",
     "@types/jest": "^26.0.22",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",
+    "cross-env": "^7.0.0",
     "eslint-import-resolver-typescript": "^2.4.0",
     "eslint-plugin-jest": "^24.3.2",
     "ts-jest": "^26.5.4",

+ 1 - 0
packages/slack/src/index.ts

@@ -10,3 +10,4 @@ export const supportedGrowiCommands: string[] = [
 export * from './interfaces/growi-command';
 export * from './models/errors';
 export * from './utils/slash-command-parser';
+export * from './utils/block-creater';

+ 31 - 0
packages/slack/src/utils/block-creater.ts

@@ -0,0 +1,31 @@
+import { SectionBlock, InputBlock } from '@slack/types';
+
+export const generateMarkdownSectionBlock = (blocks:string):SectionBlock => {
+  return {
+    type: 'section',
+    text: {
+      type: 'mrkdwn',
+      text: blocks,
+    },
+  };
+};
+
+export const generateInputSectionBlock = (blockId:string, labelText:string, actionId:string, isMultiline:boolean, placeholder:string):InputBlock => {
+  return {
+    type: 'input',
+    block_id: blockId,
+    label: {
+      type: 'plain_text',
+      text: labelText,
+    },
+    element: {
+      type: 'plain_text_input',
+      action_id: actionId,
+      multiline: isMultiline,
+      placeholder: {
+        type: 'plain_text',
+        text: placeholder,
+      },
+    },
+  };
+};

+ 101 - 0
packages/slackbot-proxy/docker/Dockerfile

@@ -0,0 +1,101 @@
+# syntax = docker/dockerfile:1.2
+
+##
+## deps-resolver-base
+##
+FROM node:14-slim AS deps-resolver-base
+
+ENV appDir /opt
+
+WORKDIR ${appDir}
+COPY ./package.json ./
+COPY ./yarn.lock ./
+COPY ./lerna.json ./
+COPY ./packages/slack/package.json ./packages/slack/package.json
+COPY ./packages/slackbot-proxy/package.json ./packages/slackbot-proxy/package.json
+
+# setup
+RUN yarn config set network-timeout 300000
+
+
+
+##
+## deps-resolver-dev
+##
+FROM deps-resolver-base AS deps-resolver-dev
+RUN npx lerna bootstrap
+
+
+
+##
+## deps-resolver-prod
+##
+FROM deps-resolver-base AS deps-resolver-prod
+RUN npx lerna bootstrap -- --production
+# make artifacts
+RUN tar cf node_modules.tar node_modules
+
+
+##
+## builder
+##
+FROM node:14-slim AS builder
+
+ENV appDir /opt
+
+WORKDIR ${appDir}
+
+COPY --from=deps-resolver-dev ${appDir}/node_modules node_modules
+
+# copy all related packages
+COPY packages/slack packages/slack
+COPY packages/slackbot-proxy packages/slackbot-proxy
+
+COPY ./package.json ./
+COPY ./lerna.json ./
+COPY ./tsconfig.base.json ./
+COPY ./packages/slack ./packages/slack
+COPY ./packages/slackbot-proxy ./packages/slackbot-proxy
+
+# build
+RUN yarn lerna run build
+
+# make artifacts
+RUN tar cf packages.tar \
+  packages/slack/package.json \
+  packages/slack/dist \
+  packages/slackbot-proxy/package.json \
+  packages/slackbot-proxy/dist
+
+
+
+##
+## release
+##
+FROM node:14-slim
+LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
+
+ENV NODE_ENV production
+
+ENV appDir /opt
+
+COPY --from=deps-resolver-prod --chown=node:node \
+  ${appDir}/node_modules.tar ${appDir}/
+COPY --from=builder --chown=node:node \
+  ${appDir}/packages.tar ${appDir}/
+
+RUN chown node:node ${appDir}
+
+USER node
+
+# extract node_modules.tar
+WORKDIR ${appDir}
+RUN tar xf node_modules.tar
+RUN tar xf packages.tar
+RUN rm node_modules.tar packages.tar
+
+WORKDIR ${appDir}/packages/slackbot-proxy
+
+EXPOSE 8080
+
+CMD ["node", "-r", "dotenv-flow/config", "dist/index.js"]

+ 66 - 0
packages/slackbot-proxy/docker/README.md

@@ -0,0 +1,66 @@
+
+GROWI Slackbot Proxy Server Official docker image
+==============================================
+
+[![Node CI for slackbot-proxy](https://github.com/weseek/growi/actions/workflows/ci-slackbot-proxy.yml/badge.svg)](https://github.com/weseek/growi/actions/workflows/ci-slackbot-proxy.yml) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi-slackbot-proxy.svg)](https://hub.docker.com/r/weseek/growi-slackbot-proxy/)
+
+
+Supported tags and respective Dockerfile links
+------------------------------------------------
+
+* [`1.0.0`, `1.0`, `1`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/master/docker/Dockerfile)
+
+
+What is GROWI Slackbot Proxy Server?
+----------------------------------
+
+The proxy server produced by GROWI Development Team, provides the backend for Slack App (Bot) to integrate GROWI Apps and Slack workspaces.
+
+see: (TBD)
+
+
+Requirements
+-------------
+
+* MySQL (>= 8.0)
+
+### Optional Dependencies
+
+* 
+
+
+Usage
+-----
+
+Create `.env.production.local`
+
+```
+```
+
+```bash
+docker run -d \
+    -e TYPEORM_CONNECTION=mysql \
+    -e TYPEORM_HOST=mysqlserver \
+    -e TYPEORM_DATABASE=growi-slackbot-proxy \
+    -e TYPEORM_USERNAME=growi-slackbot-proxy \
+    -e TYPEORM_PASSWORD=CHANGE-IT \
+    -e SLACK_CLIENT_ID=000000000000.0000000000000 \
+    -e SLACK_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
+    -e SLACK_SIGNING_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
+    weseek/growi-slackbot-proxy
+```
+
+and go to `http://localhost:8080/` .
+
+### docker-compose
+
+(TBD)
+
+Configuration
+-----------
+
+(TBD)
+
+### Environment Variables
+
+(TBD)

+ 15 - 12
packages/slackbot-proxy/package.json

@@ -3,9 +3,9 @@
   "version": "0.9.0-RC",
   "license": "MIT",
   "scripts": {
-    "build": "yarn tsc",
-    "tsc": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
-    "tsc:w": "tsc -w",
+    "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
+    "tsc": "tsc -p tsconfig.build.json",
+    "tsc:w": "yarn tsc -w",
     "dev:ci": "yarn dev --ci",
     "dev": "cross-env NODE_ENV=development ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config src/index.ts",
     "start:prod:ci": "yarn start:prod --ci",
@@ -20,21 +20,24 @@
     "@growi/slack": "0.9.0-RC",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.1.0",
-    "@tsed/common": "^6.34.3",
-    "@tsed/di": "^6.34.3",
-    "@tsed/platform-express": "^6.34.3",
-    "@tsed/swagger": "^6.34.3",
-    "@tsed/typeorm": "^6.34.3",
+    "@tsed/common": "^6.43.0",
+    "@tsed/di": "^6.43.0",
+    "@tsed/platform-express": "^6.43.0",
+    "@tsed/swagger": "^6.43.0",
+    "@tsed/typeorm": "^6.43.0",
     "compression": "^1.7.4",
+    "cookie-parser": "^1.4.5",
+    "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
+    "method-override": "^3.0.0",
     "mysql2": "^2.2.5",
     "typeorm": "^0.2.31"
   },
   "devDependencies": {
-    "@tsed/core": "^6.34.3",
-    "@tsed/exceptions": "^6.34.3",
-    "@tsed/json-mapper": "^6.34.3",
-    "@tsed/schema": "^6.34.3",
+    "@tsed/core": "^6.43.0",
+    "@tsed/exceptions": "^6.43.0",
+    "@tsed/json-mapper": "^6.43.0",
+    "@tsed/schema": "^6.43.0",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",
     "eslint-import-resolver-typescript": "^2.4.0",

+ 10 - 5
packages/slackbot-proxy/src/controllers/slack.ts

@@ -1,7 +1,7 @@
 import {
   BodyParams, Controller, Get, Inject, Post, Req, Res,
 } from '@tsed/common';
-
+import { parseSlashCommand } from '@growi/slack';
 import { Installation } from '~/entities/installation';
 import { Relation } from '~/entities/relation';
 import { Order } from '~/entities/order';
@@ -10,7 +10,7 @@ import { InstallationRepository } from '~/repositories/installation';
 import { RelationRepository } from '~/repositories/relation';
 import { OrderRepository } from '~/repositories/order';
 import { InstallerService } from '~/services/InstallerService';
-import { ReceiveService } from '~/services/RecieveService';
+import { RegisterService } from '~/services/RegisterService';
 
 
 @Controller('/slack')
@@ -29,7 +29,11 @@ export class SlackCtrl {
   orderRepository: OrderRepository;
 
   @Inject()
-  receiveService: ReceiveService;
+  registerService: RegisterService;
+
+  growiCommandsMappings = {
+    register: async(body:{[key:string]:string}):Promise<void> => this.registerService.execSlashCommand(body),
+  };
 
   @Get('/testsave')
   testsave(): void {
@@ -75,8 +79,9 @@ export class SlackCtrl {
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
 
-    const slackInput = this.receiveService.receiveContentsFromSlack(body);
-    console.log('Controller/events', slackInput);
+    const parsedBody = parseSlashCommand(body);
+    const executeGrowiCommand = this.growiCommandsMappings[parsedBody.growiCommandType];
+    await executeGrowiCommand(body);
     res.send();
 
     const installation = await this.installationRepository.findByID('1');

+ 3 - 0
packages/slackbot-proxy/src/interfaces/growi-commands-mappings.ts

@@ -0,0 +1,3 @@
+export interface GrowiCommandsMappings{
+  execSlashCommand(body:{[key:string]:string}):Promise<void>
+}

+ 9 - 0
packages/slackbot-proxy/src/repositories/installation.ts

@@ -11,4 +11,13 @@ export class InstallationRepository extends Repository<Installation> {
     return this.findOne(id);
   }
 
+  async findByTeamIdOrEnterpriseId(teamIdOrEnterpriseId:string): Promise<Installation|undefined> {
+    return this.findOne({
+      where: [
+        { teamId: teamIdOrEnterpriseId },
+        { enterpriseId: teamIdOrEnterpriseId, isEnterpriseInstall: true },
+      ],
+    });
+  }
+
 }

+ 15 - 2
packages/slackbot-proxy/src/services/InstallerService.ts

@@ -33,12 +33,25 @@ export class InstallerService {
       clientSecret,
       stateSecret,
       installationStore: {
+        // upsert
         storeInstallation: async(slackInstallation: SlackInstallation<'v1' | 'v2', boolean>) => {
+          const teamIdOrEnterpriseId = slackInstallation.team?.id || slackInstallation.enterprise?.id;
+
+          if (teamIdOrEnterpriseId == null) {
+            throw new Error('teamId or enterpriseId is required.');
+          }
+
+          const existedInstallation = await repository.findByTeamIdOrEnterpriseId(teamIdOrEnterpriseId);
+
+          if (existedInstallation != null) {
+            existedInstallation.setData(slackInstallation);
+            await repository.save(existedInstallation);
+            return;
+          }
+
           const installation = new Installation();
           installation.setData(slackInstallation);
-
           await repository.save(installation);
-
           return;
         },
         fetchInstallation: async(installQuery: InstallationQuery<boolean>) => {

+ 37 - 0
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -0,0 +1,37 @@
+import { Service } from '@tsed/di';
+import { WebClient, LogLevel } from '@slack/web-api';
+import { generateInputSectionBlock } from '@growi/slack';
+import { GrowiCommandsMappings } from '../interfaces/growi-commands-mappings';
+
+@Service()
+export class RegisterService implements GrowiCommandsMappings {
+
+  async execSlashCommand(body:{[key:string]:string}):Promise<void> {
+    // tmp use process.env
+    const client = new WebClient(process.env.SLACK_BOT_USER_OAUTH_TOKEN, { logLevel: LogLevel.DEBUG });
+    await client.views.open({
+      trigger_id: body.trigger_id,
+      view: {
+        type: 'modal',
+        title: {
+          type: 'plain_text',
+          text: 'Register Credentials',
+        },
+        submit: {
+          type: 'plain_text',
+          text: 'Submit',
+        },
+        close: {
+          type: 'plain_text',
+          text: 'Close',
+        },
+        blocks: [
+          generateInputSectionBlock('growiDomain', 'GROWI domain', 'contents_input', false, 'https://example.com'),
+          generateInputSectionBlock('growiAccessToken', 'GROWI ACCESS_TOKEN', 'contents_input', false, 'jBMZvpk.....'),
+          generateInputSectionBlock('proxyToken', 'PROXY ACCESS_TOKEM', 'contents_input', false, 'jBMZvpk.....'),
+        ],
+      },
+    });
+  }
+
+}

BIN
public/images/slack-integration/impossible.png


BIN
public/images/slack-integration/possible.png


BIN
public/images/slack-integration/triangle-basic-gray.png


+ 5 - 3
resource/locales/en_US/admin/admin.json

@@ -264,9 +264,6 @@
       "without_proxy": "without proxy",
       "with_proxy": "with proxy",
       "recommended": "Recommended",
-      "for_beginners": "- For beginners -",
-      "for_intermediate": "- For intermediates -",
-      "for_advanced": "- For advanced -",
       "set_up": "Set up",
       "multiple_workspaces_integration": "Multiple workspaces integration",
       "security_control": "Security control",
@@ -304,6 +301,11 @@
       "test_connection": "Test Connection",
       "test_connection_by_pressing_button": "Press the button to test the connection",
       "error_check_logs_below": "An error has occurred. Please check the logs below."
+    },
+    "custom_bot_without_proxy_integration": "Custom bot without proxy integration",
+    "integration_sentence": {
+      "integration_is_not_complete": "Integration is not complete.",
+      "proceed_with_the_following_integration_procedure": "Proceed with the following integration procedure."
     }
   },
   "user_management": {

+ 5 - 3
resource/locales/ja_JP/admin/admin.json

@@ -262,9 +262,6 @@
       "without_proxy": "without proxy",
       "with_proxy": "with proxy",
       "recommended": "おすすめ",
-      "for_beginners": "- 初心者向け -",
-      "for_intermediate": "- 中級者向け -",
-      "for_advanced": "- 上級者向け -",
       "set_up": "セットアップ",
       "multiple_workspaces_integration": "複数ワークスペースとの連携",
       "security_control": "セキュリティコントロール",
@@ -302,6 +299,11 @@
       "test_connection": "連携状況のテストをする",
       "test_connection_by_pressing_button": "以下のテストボタンを押して、Slack連携が完了しているかの確認をしましょう",
       "error_check_logs_below": "エラーが発生しました。下記のログを確認してください。"
+    },
+    "custom_bot_without_proxy_integration": "Custom bot without proxy 連携",
+    "integration_sentence": {
+      "integration_is_not_complete": "連携は完了していません。",
+      "proceed_with_the_following_integration_procedure": "下記の連携手順を進めてください。"
     }
   },
   "user_management": {

+ 5 - 3
resource/locales/zh_CN/admin/admin.json

@@ -272,9 +272,6 @@
       "without_proxy": "without proxy",
       "with_proxy": "with proxy",
       "recommended": "受到推崇的",
-      "for_beginners": "- 对于初学者 -",
-      "for_intermediate": "- 对于中级 -",
-      "for_advanced": "- 对于高级 -",
       "set_up": "设置",
       "multiple_workspaces_integration": "集成到多个工作区",
       "security_control": "安全控制",
@@ -312,6 +309,11 @@
       "test_connection": "测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
       "error_check_logs_below": "发生了错误。请检查以下日志。"
+    },
+    "custom_bot_without_proxy_integration": "Custom bot without proxy 一体化",
+    "integration_sentence": {
+      "integration_is_not_complete": "一体化未完成。",
+      "proceed_with_the_following_integration_procedure": "进行以下一体化程序。"
     }
   },
 	"user_management": {

+ 35 - 0
src/client/js/components/Admin/Common/Accordion.jsx

@@ -0,0 +1,35 @@
+import React, { useState } from 'react';
+import { Collapse } from 'reactstrap';
+import PropTypes from 'prop-types';
+
+const Accordion = (props) => {
+  const [isOpen, setIsOpen] = useState(props.isOpenDefault);
+  return (
+    <div className="card border-0 rounded-lg mb-0">
+      <div
+        className="card-header font-weight-normal py-3 d-flex justify-content-between"
+        role="button"
+        onClick={() => setIsOpen(prevState => !prevState)}
+      >
+        <p className="mb-0 text-primary">{props.title}</p>
+        {isOpen
+          ? <i className="fa fa-chevron-up" />
+          : <i className="fa fa-chevron-down" />
+        }
+      </div>
+      <Collapse isOpen={isOpen}>
+        <div className="card-body">
+          {props.children}
+        </div>
+      </Collapse>
+    </div>
+  );
+};
+
+Accordion.propTypes = {
+  title: PropTypes.node.isRequired,
+  children: PropTypes.node.isRequired,
+  isOpenDefault: PropTypes.bool.isRequired,
+};
+
+export default Accordion;

+ 99 - 0
src/client/js/components/Admin/SlackIntegration/BotTypeCard.jsx

@@ -0,0 +1,99 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
+
+const botDetails = {
+  officialBot: {
+    botType: 'officialBot',
+    botTypeCategory: 'official_bot',
+    setUp: 'easy',
+    multiWSIntegration: 'possible',
+    securityControl: 'impossible',
+  },
+  customBotWithoutProxy: {
+    botType: 'customBotWithoutProxy',
+    botTypeCategory: 'custom_bot',
+    supplementaryBotName: 'without_proxy',
+    setUp: 'normal',
+    multiWSIntegration: 'impossible',
+    securityControl: 'possible',
+  },
+  customBotWithProxy: {
+    botType: 'customBotWithProxy',
+    botTypeCategory: 'custom_bot',
+    supplementaryBotName: 'with_proxy',
+    setUp: 'hard',
+    multiWSIntegration: 'possible',
+    securityControl: 'possible',
+  },
+};
+
+const BotTypeCard = (props) => {
+  const { t } = useTranslation('admin');
+
+  return (
+    <div
+      className={`card admin-bot-card rounded border-radius-sm shadow ${props.isActive ? 'border-primary' : ''}`}
+      onClick={() => props.handleBotTypeSelect(botDetails[props.botType].botType)}
+      role="button"
+      key={props.botType}
+    >
+      <div>
+        <h3 className={`card-header mb-0 py-3
+              ${props.botType === 'officialBot' ? 'd-flex align-items-center justify-content-center' : 'text-center'}
+              ${props.isActive ? 'bg-primary text-light' : ''}`}
+        >
+          <span className="mr-2">
+            {t(`admin:slack_integration.selecting_bot_types.${botDetails[props.botType].botTypeCategory}`)}
+          </span>
+
+          {/*  A recommended badge is shown on official bot card, supplementary names are shown on Custom bot cards   */}
+          {props.botType === 'officialBot'
+          ? (
+            <span className="badge badge-info mr-2">
+              {t('admin:slack_integration.selecting_bot_types.recommended')}
+            </span>
+          ) : (
+            <span className="supplementary-bot-name mr-2">
+              {t(`admin:slack_integration.selecting_bot_types.${botDetails[props.botType].supplementaryBotName}`)}
+            </span>
+          )}
+
+          {/* TODO: add an appropriate links by GW-5614 */}
+          <i className={`fa fa-external-link btn-link ${props.isActive ? 'bg-primary text-light' : ''}`} aria-hidden="true"></i>
+        </h3>
+      </div>
+      <div className="card-body p-4">
+        <div className="card-text">
+          <div className="my-2">
+            <div className="d-flex justify-content-between mb-3">
+              {/* TODO add image of difficulties by GW-5638
+               <span>{t('admin:slack_integration.selecting_bot_types.set_up')}</span>
+               <span className={`bot-type-disc-${value.setUp}`}>{t(`admin:slack_integration.selecting_bot_types.${value.setUp}`)}</span>  */}
+
+
+            </div>
+            <div className="d-flex justify-content-between mb-3">
+              <span>{t('admin:slack_integration.selecting_bot_types.multiple_workspaces_integration')}</span>
+              <img className="bot-type-disc" src={`/images/slack-integration/${botDetails[props.botType].multiWSIntegration}.png`} alt="" />
+            </div>
+            <div className="d-flex justify-content-between">
+              <span>{t('admin:slack_integration.selecting_bot_types.security_control')}</span>
+              <img className="bot-type-disc" src={`/images/slack-integration/${botDetails[props.botType].securityControl}.png`} alt="" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+
+};
+
+BotTypeCard.propTypes = {
+  isActive: PropTypes.bool.isRequired,
+  botType: PropTypes.string.isRequired,
+  handleBotTypeSelect: PropTypes.func.isRequired,
+};
+
+export default BotTypeCard;

+ 29 - 55
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx

@@ -1,57 +1,28 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React from 'react';
 import { useTranslation } from 'react-i18next';
 import PropTypes from 'prop-types';
-import AppContainer from '../../../services/AppContainer';
-import AdminAppContainer from '../../../services/AdminAppContainer';
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const CustomBotWithoutProxySecretTokenSection = (props) => {
-  const { appContainer, adminAppContainer } = props;
   const { t } = useTranslation();
 
-  const [slackSigningSecret, setSlackSigningSecret] = useState('');
-  const [slackBotToken, setSlackBotToken] = useState('');
-  const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
-  const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
-  const currentBotType = 'custom-bot-without-proxy';
-
-  const fetchData = useCallback(async() => {
-    try {
-      await adminAppContainer.retrieveAppSettingsData();
-      const res = await appContainer.apiv3.get('/slack-integration/');
-      const {
-        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars,
-      } = res.data.slackBotSettingParams.customBotWithoutProxySettings;
-      setSlackSigningSecret(slackSigningSecret);
-      setSlackBotToken(slackBotToken);
-      setSlackSigningSecretEnv(slackSigningSecretEnvVars);
-      setSlackBotTokenEnv(slackBotTokenEnvVars);
-    }
-    catch (err) {
-      toastError(err);
+  const onChangeSigningSecretHandler = (signingSecretInput) => {
+    if (props.onChangeSigningSecretHandler != null) {
+      props.onChangeSigningSecretHandler(signingSecretInput);
     }
-  }, [appContainer, adminAppContainer]);
-
-  useEffect(() => {
-    fetchData();
-  }, [fetchData]);
+  };
 
-  async function updateHandler() {
-    try {
-      await appContainer.apiv3.put('/slack-integration/custom-bot-without-proxy', {
-        slackSigningSecret,
-        slackBotToken,
-        currentBotType,
-      });
-      fetchData();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:slack_integration.custom_bot_without_proxy_settings') }));
+  const onChangeBotTokenHandler = (botTokenInput) => {
+    if (props.onChangeBotTokenHandler != null) {
+      props.onChangeBotTokenHandler(botTokenInput);
     }
-    catch (err) {
-      toastError(err);
+  };
+
+  const updateSecretTokenHandler = () => {
+    if (props.updateSecretTokenHandler != null) {
+      props.updateSecretTokenHandler();
     }
-  }
+  };
 
   return (
     <div className="card-body">
@@ -71,15 +42,15 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
               <input
                 className="form-control"
                 type="text"
-                value={slackSigningSecret || ''}
-                onChange={e => setSlackSigningSecret(e.target.value)}
+                value={props.slackSigningSecret || ''}
+                onChange={e => onChangeSigningSecretHandler(e.target.value)}
               />
             </td>
             <td>
               <input
                 className="form-control"
                 type="text"
-                value={slackSigningSecretEnv || ''}
+                value={props.slackSigningSecretEnv || ''}
                 readOnly
               />
               <p className="form-text text-muted">
@@ -94,15 +65,15 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
               <input
                 className="form-control"
                 type="text"
-                value={slackBotToken || ''}
-                onChange={e => setSlackBotToken(e.target.value)}
+                value={props.slackBotToken || ''}
+                onChange={e => onChangeBotTokenHandler(e.target.value)}
               />
             </td>
             <td>
               <input
                 className="form-control"
                 type="text"
-                value={slackBotTokenEnv || ''}
+                value={props.slackBotTokenEnv || ''}
                 readOnly
               />
               <p className="form-text text-muted">
@@ -114,16 +85,19 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
         </tbody>
       </table>
 
-      <AdminUpdateButtonRow onClick={updateHandler} disabled={false} />
+      <AdminUpdateButtonRow onClick={updateSecretTokenHandler} disabled={false} />
     </div>
   );
 };
 
-const CustomBotWithoutProxySecretTokenSectionWrapper = withUnstatedContainers(CustomBotWithoutProxySecretTokenSection, [AppContainer, AdminAppContainer]);
-
 CustomBotWithoutProxySecretTokenSection.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
+  updateSecretTokenHandler: PropTypes.func,
+  onChangeSigningSecretHandler: PropTypes.func,
+  onChangeBotTokenHandler: PropTypes.func,
+  slackSigningSecret: PropTypes.string,
+  slackSigningSecretEnv: PropTypes.string,
+  slackBotToken: PropTypes.string,
+  slackBotTokenEnv: PropTypes.string,
 };
 
-export default CustomBotWithoutProxySecretTokenSectionWrapper;
+export default CustomBotWithoutProxySecretTokenSection;

+ 26 - 2
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -6,7 +6,7 @@ import AdminAppContainer from '../../../services/AdminAppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import SlackGrowiBridging from './SlackGrowiBridging';
-import CustomBotWithoutProxySettingsAccordion from './CustomBotWithoutProxySettingsAccordion';
+import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 
 const CustomBotWithoutProxySettings = (props) => {
   const { appContainer, adminAppContainer } = props;
@@ -56,6 +56,29 @@ const CustomBotWithoutProxySettings = (props) => {
 
   return (
     <>
+
+      <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}</h2>
+
+      <div className="d-flex justify-content-center my-5 bot-integration">
+        <div className="card rounded shadow border-0 w-50 admin-bot-card">
+          <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
+          <div className="card-body p-4"></div>
+        </div>
+
+        <div className="text-center w-25 mt-4">
+          <p className="text-secondary m-0"><small>{t('admin:slack_integration.integration_sentence.integration_is_not_complete')}</small></p>
+          <p className="text-secondary"><small>{t('admin:slack_integration.integration_sentence.proceed_with_the_following_integration_procedure')}</small></p>
+          <hr className="border-danger align-self-center admin-border"></hr>
+        </div>
+
+        <div className="card rounded-lg shadow border-0 w-50 admin-bot-card">
+          <h5 className="card-title font-weight-bold mt-3 ml-4">GROWI App</h5>
+          <div className="card-body p-4 text-center">
+            <a className="btn btn-primary mb-5">WESEEK Inner Wiki</a>
+          </div>
+        </div>
+      </div>
+
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_settings')}</h2>
       {/* temporarily put bellow component */}
       <SlackGrowiBridging
@@ -64,7 +87,8 @@ const CustomBotWithoutProxySettings = (props) => {
       />
 
       <div className="my-5 mx-3">
-        <CustomBotWithoutProxySettingsAccordion />
+        {/* TODO GW-5644 active create bot step temporary */}
+        <CustomBotWithoutProxySettingsAccordion activeStep={botInstallationStep.CREATE_BOT} />
       </div>
 
     </>

+ 99 - 50
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -1,30 +1,80 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import AppContainer from '../../../services/AppContainer';
+import AdminAppContainer from '../../../services/AdminAppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import BotSettingsAccordion from './BotSettingsAccordion';
+import Accordion from '../Common/Accordion';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
 
-const CustomBotWithoutProxySettingsAccordion = (props) => {
-  const { appContainer } = props;
+export const botInstallationStep = {
+  CREATE_BOT: 'create-bot',
+  INSTALL_BOT: 'install-bot',
+  REGISTER_SLACK_CONFIGURATION: 'register-slack-configuration',
+  CONNECTION_TEST: 'connection-test',
+};
+
+const CustomBotWithoutProxySettingsAccordion = ({ appContainer, adminAppContainer, activeStep }) => {
   const { t } = useTranslation('admin');
-  const [openAccordionIndexes, setOpenAccordionIndexes] = useState(new Set());
-  const [connectionErrorLog, setConnectionErrorLog] = useState({});
+  // TODO: GW-5644 Store default open accordion
+  // eslint-disable-next-line no-unused-vars
+  const [defaultOpenAccordionKeys, setDefaultOpenAccordionKeys] = useState(new Set([activeStep]));
+  const [connectionErrorCode, setConnectionErrorCode] = useState(null);
+  const [connectionErrorMessage, setConnectionErrorMessage] = useState(null);
+  const [slackSigningSecret, setSlackSigningSecret] = useState('');
+  const [slackBotToken, setSlackBotToken] = useState('');
+  const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
+  const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
+  const currentBotType = 'custom-bot-without-proxy';
 
-  const onToggleAccordionHandler = (i) => {
-    const accordionIndexes = new Set(openAccordionIndexes);
-    if (accordionIndexes.has(i)) {
-      accordionIndexes.delete(i);
+  const fetchData = useCallback(async() => {
+    try {
+      await adminAppContainer.retrieveAppSettingsData();
+      const res = await appContainer.apiv3.get('/slack-integration/');
+      const {
+        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars,
+      } = res.data.slackBotSettingParams.customBotWithoutProxySettings;
+      setSlackSigningSecret(slackSigningSecret);
+      setSlackBotToken(slackBotToken);
+      setSlackSigningSecretEnv(slackSigningSecretEnvVars);
+      setSlackBotTokenEnv(slackBotTokenEnvVars);
     }
-    else {
-      accordionIndexes.add(i);
+    catch (err) {
+      toastError(err);
     }
-    setOpenAccordionIndexes(accordionIndexes);
+  }, [appContainer, adminAppContainer]);
+
+  useEffect(() => {
+    fetchData();
+  }, [fetchData]);
+
+  const updateSecretTokenHandler = async() => {
+    try {
+      await appContainer.apiv3.put('/slack-integration/custom-bot-without-proxy', {
+        slackSigningSecret,
+        slackBotToken,
+        currentBotType,
+      });
+      fetchData();
+      toastSuccess(t('toaster.update_successed', { target: t('slack_integration.custom_bot_without_proxy_settings') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  const onChangeSigningSecretHandler = (signingSecretInput) => {
+    setSlackSigningSecret(signingSecretInput);
+  };
+
+  const onChangeBotTokenHandler = (botTokenInput) => {
+    setSlackBotToken(botTokenInput);
   };
 
   const onTestConnectionHandler = async() => {
-    setConnectionErrorLog({ connectionErrorCode: null, connectionErrorMessage: null });
+    setConnectionErrorCode(null);
+    setConnectionErrorMessage(null);
     try {
       await appContainer.apiv3.post('slack-integration/notification-test-to-slack-work-space', {
         // TODO put proper request
@@ -32,21 +82,16 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       });
     }
     catch (err) {
-      setConnectionErrorLog(prevState => ({
-        ...prevState,
-        connectionErrorCode: err[0].code,
-        connectionErrorMessage: err[0].message,
-      }));
+      setConnectionErrorCode(err[0].code);
+      setConnectionErrorMessage(err[0].message);
     }
   };
 
   return (
-    <BotSettingsAccordion>
-      <BotSettingsAccordion.Item
-        isActive={openAccordionIndexes.has(0)}
-        itemNumber="①"
-        title={t('slack_integration.without_proxy.create_bot')}
-        onToggleAccordionHandler={() => onToggleAccordionHandler(0)}
+    <div className="card border-0 rounded-lg shadow overflow-hidden">
+      <Accordion
+        defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CREATE_BOT)}
+        title={<><span className="mr-2">①</span>{t('slack_integration.without_proxy.create_bot')}</>}
       >
         <div className="row my-5">
           <div className="mx-auto">
@@ -67,12 +112,10 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
             </a>
           </div>
         </div>
-      </BotSettingsAccordion.Item>
-      <BotSettingsAccordion.Item
-        isActive={openAccordionIndexes.has(1)}
-        itemNumber="②"
-        title={t('slack_integration.without_proxy.install_bot_to_slack')}
-        onToggleAccordionHandler={() => onToggleAccordionHandler(1)}
+      </Accordion>
+      <Accordion
+        defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.INSTALL_BOT)}
+        title={<><span className="mr-2">②</span>{t('slack_integration.without_proxy.install_bot_to_slack')}</>}
       >
         <div className="container w-75 py-5">
           <p>1. {t('slack_integration.without_proxy.select_install_your_app')}</p>
@@ -87,46 +130,52 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
           <img src="/images/slack-integration/slack-bot-install-to-workspace-joined-bot.png" className="border border-light img-fluid mb-1" />
           <img src="/images/slack-integration/slack-bot-install-your-app-introduction-to-channel.png" className="border border-light img-fluid" />
         </div>
-      </BotSettingsAccordion.Item>
-      <BotSettingsAccordion.Item
-        isActive={openAccordionIndexes.has(2)}
-        itemNumber="③"
-        title={t('slack_integration.without_proxy.register_secret_and_token')}
-        onToggleAccordionHandler={() => onToggleAccordionHandler(2)}
+      </Accordion>
+      <Accordion
+        defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.REGISTER_SLACK_CONFIGURATION)}
+        title={<><span className="mr-2">③</span>{t('slack_integration.without_proxy.register_secret_and_token')}</>}
       >
-        <CustomBotWithoutProxySecretTokenSection />
-      </BotSettingsAccordion.Item>
-      <BotSettingsAccordion.Item
-        isActive={openAccordionIndexes.has(3)}
-        itemNumber="④"
-        title={t('slack_integration.without_proxy.test_connection')}
-        onToggleAccordionHandler={() => onToggleAccordionHandler(3)}
+        <CustomBotWithoutProxySecretTokenSection
+          updateSecretTokenHandler={updateSecretTokenHandler}
+          onChangeSigningSecretHandler={onChangeSigningSecretHandler}
+          onChangeBotTokenHandler={onChangeBotTokenHandler}
+          slackSigningSecret={slackSigningSecret}
+          slackSigningSecretEnv={slackSigningSecretEnv}
+          slackBotToken={slackBotToken}
+          slackBotTokenEnv={slackBotTokenEnv}
+        />
+      </Accordion>
+      <Accordion
+        defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
+        title={<><span className="mr-2">④</span>{t('slack_integration.without_proxy.test_connection')}</>}
       >
         <p className="text-center m-4">{t('slack_integration.without_proxy.test_connection_by_pressing_button')}</p>
         <div className="d-flex justify-content-center">
           <button type="button" className="btn btn-info m-3 px-5 font-weight-bold" onClick={onTestConnectionHandler}>Test</button>
         </div>
-        {connectionErrorLog.connectionErrorMessage != null
+        {connectionErrorMessage != null
           && <p className="text-danger text-center m-4">{t('slack_integration.without_proxy.error_check_logs_below')}</p>
         }
         <div className="row m-3 justify-content-center">
           <div className="col-sm-5 slack-connection-error-log">
             <p className="border-info slack-connection-error-log-title mb-1 pl-2">Logs</p>
             <div className="card border-info slack-connection-error-log-body rounded-lg px-5 py-4">
-              <p className="m-0">{connectionErrorLog.connectionErrorCode}</p>
-              <p className="m-0">{connectionErrorLog.connectionErrorMessage}</p>
+              <p className="m-0">{connectionErrorCode}</p>
+              <p className="m-0">{connectionErrorMessage}</p>
             </div>
           </div>
         </div>
-      </BotSettingsAccordion.Item>
-    </BotSettingsAccordion>
+      </Accordion>
+    </div>
   );
 };
 
-const CustomBotWithoutProxySettingsAccordionWrapper = withUnstatedContainers(CustomBotWithoutProxySettingsAccordion, [AppContainer]);
+const CustomBotWithoutProxySettingsAccordionWrapper = withUnstatedContainers(CustomBotWithoutProxySettingsAccordion, [AppContainer, AdminAppContainer]);
 
 CustomBotWithoutProxySettingsAccordion.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
+  activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
 };
 
 export default CustomBotWithoutProxySettingsAccordionWrapper;

+ 15 - 137
src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -10,7 +10,9 @@ import OfficialBotSettings from './OfficialBotSettings';
 import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
+import BotTypeCard from './BotTypeCard';
 
+const botTypes = ['officialBot', 'customBotWithoutProxy', 'customBotWithProxy'];
 
 const SlackIntegration = (props) => {
   const { appContainer } = props;
@@ -90,28 +92,19 @@ const SlackIntegration = (props) => {
   let settingsComponent = null;
 
   switch (currentBotType) {
-    case 'official-bot':
+    case 'officialBot':
       settingsComponent = <OfficialBotSettings />;
       break;
-    case 'custom-bot-without-proxy':
+    case 'customBotWithoutProxy':
       settingsComponent = (
         <CustomBotWithoutProxySettings />
       );
       break;
-    case 'custom-bot-with-proxy':
+    case 'customBotWithProxy':
       settingsComponent = <CustomBotWithProxySettings />;
       break;
   }
 
-  const showBotTypeLevel = (level) => {
-    return <span>{t(`admin:slack_integration.selecting_bot_types.${level}`)}</span>;
-  };
-  const showBotTypeLabel = (label) => {
-    return <span>{t(`admin:slack_integration.selecting_bot_types.${label}`)}</span>;
-  };
-  const showBotTypeDiscription = (desc) => {
-    return <span className={`bot-type-disc-${desc}`}>{t(`admin:slack_integration.selecting_bot_types.${desc}`)}</span>;
-  };
 
   return (
     <>
@@ -142,131 +135,16 @@ const SlackIntegration = (props) => {
 
         <div className="row my-4">
           <div className="card-deck mx-auto">
-
-            <div
-              className={`card admin-bot-card mx-3 rounded border-radius-sm shadow ${currentBotType === 'official-bot' ? 'border-primary' : ''}`}
-              onClick={() => handleBotTypeSelect('official-bot')}
-              role="button"
-            >
-              <div>
-                <h3 className={`card-header mb-0 py-3 text-center ${currentBotType === 'official-bot' ? 'bg-primary text-light' : ''}`}>
-                  <span className="mr-2">
-                    {t('admin:slack_integration.selecting_bot_types.official_bot')}
-                  </span>
-                  <span className="badge badge-info mr-2">
-                    {t('admin:slack_integration.selecting_bot_types.recommended')}
-                  </span>
-                  {/* TODO: add an appropriate link by GW-5614 */}
-                  <i className={`fa fa-external-link btn-link ${currentBotType === 'official-bot' ? 'bg-primary text-light' : ''}`} aria-hidden="true"></i>
-                </h3>
-              </div>
-              <div className="card-body p-4">
-                <p className="card-text">
-                  <div className="text-center">
-                    {showBotTypeLevel('for_beginners')}
-                  </div>
-                  <div className="my-4">
-                    <div className="d-flex justify-content-between mb-2">
-                      {showBotTypeLabel('set_up')}
-                      {showBotTypeDiscription('easy')}
-                    </div>
-                    <div className="d-flex justify-content-between mb-2">
-                      {showBotTypeLabel('multiple_workspaces_integration')}
-                      {showBotTypeDiscription('possible')}
-                    </div>
-                    <div className="d-flex justify-content-between">
-                      {showBotTypeLabel('security_control')}
-                      {showBotTypeDiscription('impossible')}
-                    </div>
-                  </div>
-                </p>
-              </div>
-            </div>
-
-            <div
-              className={`card admin-bot-card mx-3 rounded shadow ${currentBotType === 'custom-bot-without-proxy' ? 'border-primary' : ''}`}
-              onClick={() => handleBotTypeSelect('custom-bot-without-proxy')}
-              role="button"
-            >
-              <h3 className={`card-header mb-0 py-3 text-center text-nowrap  ${currentBotType === 'custom-bot-without-proxy' ? 'bg-primary text-light' : ''}`}>
-                <span className="mr-2">
-                  {t('admin:slack_integration.selecting_bot_types.custom_bot')}
-                </span>
-                <span className="supplementary-desc mr-2">
-                  {t('admin:slack_integration.selecting_bot_types.without_proxy')}
-                </span>
-                {/* TODO: add an appropriate link by GW-5614 */}
-                <i
-                  className={`fa fa-external-link btn-link ${currentBotType === 'custom-bot-without-proxy' ? 'bg-primary text-light' : ''}`}
-                  aria-hidden="true"
-                >
-                </i>
-              </h3>
-              <div className="card-body p-4">
-                <p className="card-text">
-                  <div className="text-center">
-                    {showBotTypeLevel('for_intermediate')}
-                  </div>
-                  <div className="my-4">
-                    <div className="d-flex justify-content-between mb-2">
-                      {showBotTypeLabel('set_up')}
-                      {showBotTypeDiscription('normal')}
-                    </div>
-                    <div className="d-flex justify-content-between mb-2">
-                      {showBotTypeLabel('multiple_workspaces_integration')}
-                      {showBotTypeDiscription('impossible')}
-                    </div>
-                    <div className="d-flex justify-content-between">
-                      {showBotTypeLabel('security_control')}
-                      {showBotTypeDiscription('possible')}
-                    </div>
-                  </div>
-                </p>
-              </div>
-            </div>
-
-            <div
-              className={`card admin-bot-card mx-3 rounded shadow ${currentBotType === 'custom-bot-with-proxy' ? 'border-primary' : ''}`}
-              onClick={() => handleBotTypeSelect('custom-bot-with-proxy')}
-              role="button"
-            >
-              <h3 className={`card-header mb-0 py-3 text-center text-nowrap ${currentBotType === 'custom-bot-with-proxy' ? 'bg-primary text-light' : ''}`}>
-                <span className="mr-2">
-                  {t('admin:slack_integration.selecting_bot_types.custom_bot')}
-                </span>
-                <span className="supplementary-desc mr-2">
-                  {t('admin:slack_integration.selecting_bot_types.with_proxy')}
-                </span>
-                {/* TODO: add an appropriate link by GW-5614 */}
-                <i
-                  className={`fa fa-external-link btn-link ${currentBotType === 'custom-bot-with-proxy' ? 'bg-primary text-light' : ''}`}
-                  aria-hidden="true"
-                >
-                </i>
-              </h3>
-              <div className="card-body p-4">
-                <p className="card-text">
-                  <div className="text-center">
-                    {showBotTypeLevel('for_advanced')}
-                  </div>
-                  <div className="my-4">
-                    <div className="d-flex justify-content-between mb-2">
-                      {showBotTypeLabel('set_up')}
-                      {showBotTypeDiscription('hard')}
-                    </div>
-                    <div className="d-flex justify-content-between mb-2">
-                      {showBotTypeLabel('multiple_workspaces_integration')}
-                      {showBotTypeDiscription('possible')}
-                    </div>
-                    <div className="d-flex justify-content-between">
-                      {showBotTypeLabel('security_control')}
-                      {showBotTypeDiscription('impossible')}
-                    </div>
-                  </div>
-                </p>
-              </div>
-            </div>
-
+            {botTypes.map((botType) => {
+              return (
+                <BotTypeCard
+                  key={botType}
+                  botType={botType}
+                  isActive={currentBotType === botType}
+                  handleBotTypeSelect={handleBotTypeSelect}
+                />
+              );
+            })}
           </div>
         </div>
       </div>

+ 11 - 1
src/client/styles/scss/_admin.scss

@@ -90,7 +90,7 @@
     .btn-link {
       font-size: 1rem;
     }
-    .supplementary-desc {
+    .supplementary-bot-name {
       font-size: 1rem;
     }
     .badge-info {
@@ -115,6 +115,16 @@
     }
   }
 
+  .bot-integration {
+    .admin-bot-card {
+      border-radius: 8px !important;
+    }
+    .admin-border {
+      border-style : dashed;
+      border-width : 2px;
+    }
+  }
+
   //// TODO: migrate to Bootstrap 4
   //// omit all .btn-toggle and use Switches
   //// https://getbootstrap.com/docs/4.2/components/forms/#switches

+ 2 - 14
src/client/styles/scss/theme/_apply-colors.scss

@@ -596,19 +596,7 @@ mark.rbt-highlight-text {
   Slack Integration
 */
 .selecting-bot-type {
-  .bot-type-disc-easy {
-    color: #33d541;
-  }
-  .bot-type-disc-normal {
-    color: #e6a63c;
-  }
-  .bot-type-disc-hard {
-    color: #ff5757;
-  }
-  .bot-type-disc-possible {
-    color: $info;
-  }
-  .bot-type-disc-impossible {
-    color: $gray-500;
+  .bot-type-disc {
+    width: 20px;
   }
 }

+ 1 - 1
src/server/routes/apiv3/slack-integration.js

@@ -54,7 +54,7 @@ module.exports = (crowi) => {
     ],
     SlackIntegration: [
       body('currentBotType')
-        .isIn(['official-bot', 'custom-bot-without-proxy', 'custom-bot-with-proxy']),
+        .isIn(['officialBot', 'customBotWithoutProxy', 'customBotWithProxy']),
     ],
     NotificationTestToSlackWorkSpace: [
       body('channel').isString(),

+ 1 - 1
src/server/service/config-loader.js

@@ -418,7 +418,7 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   },
   SLACK_BOT_TYPE: {
     ns:      'crowi',
-    key:     'slackbot:currentBotType', // 'official-bot' || 'custom-bot-without-proxy' || 'custom-bot-with-proxy'
+    key:     'slackbot:currentBotType', // 'officialBot' || 'customBotWithoutProxy' || 'customBotWithProxy'
     type:    TYPES.STRING,
     default: null,
   },

+ 60 - 69
yarn.lock

@@ -2804,17 +2804,17 @@
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
   integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
 
-"@tsed/common@^6.34.3":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/common/-/common-6.40.0.tgz#a871cb3428b9a0286241c58091b5eaa7497ba5f7"
-  integrity sha512-7qkib4JBeY5v4NTIDInC4mLUVg4jGF6eKjaxmiJMJ+AtkN1bk42Ugrv2/Qt+62GSuKOTVqOnNFuOCzxTZgU86Q==
-  dependencies:
-    "@tsed/core" "6.40.0"
-    "@tsed/di" "6.40.0"
-    "@tsed/exceptions" "6.40.0"
-    "@tsed/json-mapper" "6.40.0"
+"@tsed/common@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/common/-/common-6.43.0.tgz#43d55a414c3ef24cd4e8f2adbf91935e08749f16"
+  integrity sha512-/tSElT1SVH7xbw+h89PLzm0Lhm3RzmVb04al/391wQX5W9rCGPevgFB1HN5zVNQqKFfvKPoTrvzLEHjNZj1jRQ==
+  dependencies:
+    "@tsed/core" "6.43.0"
+    "@tsed/di" "6.43.0"
+    "@tsed/exceptions" "6.43.0"
+    "@tsed/json-mapper" "6.43.0"
     "@tsed/logger" "^5.12.1"
-    "@tsed/schema" "6.40.0"
+    "@tsed/schema" "6.43.0"
     "@types/cache-manager" "^3.4.0"
     "@types/json-schema" "7.0.6"
     cache-manager "^3.4.1"
@@ -2825,50 +2825,40 @@
     tslib "2.1.0"
     uuid "8.3.2"
 
-"@tsed/core@6.40.0", "@tsed/core@^6.34.3":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/core/-/core-6.40.0.tgz#8eae4b1f821c39097398006da76cbd676e1869a6"
-  integrity sha512-nijyNb5EXzlZI4ck71t9k8R9FtQH5kaHOxekLYkLE04nmKRT//Zok18v9upT4rUMVsim2kjSpRZ2h8nuasgd+g==
+"@tsed/core@6.43.0", "@tsed/core@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/core/-/core-6.43.0.tgz#63db73fb99a7f56791d58d1e557d10a670b5bc2e"
+  integrity sha512-6e3QWTCVla/BtI0bg0/ALvZf0i2MhfQwm32UHr4tHECmJ28AcStD6tc/zF9ffFEJr7xjHrFcMuvdEb3fhPLR4w==
   dependencies:
     normalize-path "3.0.0"
     reflect-metadata "^0.1.13"
     source-map-support "0.5.19"
     tslib "2.1.0"
 
-"@tsed/di@6.40.0":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/di/-/di-6.40.0.tgz#587685649c8336509cce2b9e6b804e6d31a44b3d"
-  integrity sha512-FUsQeXwfmGmGUI2OZyOIrAc/HxMZv+ki5m6CRYBGA+lWjWylKjKWuzQCvH/Bl4oDHb7QOtgSbbphf+WSsto/nQ==
+"@tsed/di@6.43.0", "@tsed/di@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/di/-/di-6.43.0.tgz#c3c55c90f048f78a07e96d19fb413053aba52ecf"
+  integrity sha512-F1dM4KfhwOOmv9dKVaoi65wnLW6hPacVpiXYREqPhBHnAMSsQcpdgbvc7cFU3rO5ymPA8IHGLpDsfOM1aj8EcA==
   dependencies:
-    "@tsed/core" "6.40.0"
+    "@tsed/core" "6.43.0"
     chalk "4.1.0"
     globby "11.0.1"
     tslib "2.1.0"
 
-"@tsed/di@^6.34.3":
-  version "6.34.3"
-  resolved "https://registry.yarnpkg.com/@tsed/di/-/di-6.34.3.tgz#57f4a72c7cf2046ecfbb8879cb951e198b535daa"
-  integrity sha512-DU26ykSzgreEDPiRifXF3vqoYqasA4sGCCeGF08b19Wee1P4K0DHnrnjOvCUfr7mKq1dmielVNxdYWzWp3nwvg==
+"@tsed/exceptions@6.43.0", "@tsed/exceptions@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/exceptions/-/exceptions-6.43.0.tgz#327aefa42235c8da955366fe932bc11864104541"
+  integrity sha512-Sz0Zt51xsoXbF36dWm1vl74pVmAmHAhbykl3pIH5Cnbr4vqp3K5OpPPD/ZVxzfKJrTfiIN15zmvs5eXbyjXu5g==
   dependencies:
-    "@tsed/core" "6.34.3"
-    chalk "4.1.0"
-    globby "11.0.1"
-    tslib "2.1.0"
-
-"@tsed/exceptions@6.40.0", "@tsed/exceptions@^6.34.3":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/exceptions/-/exceptions-6.40.0.tgz#66342dd392c32720b5eb5b5f7839665ab62482fb"
-  integrity sha512-gqMkHMar0nH9XkyBg3mKA1TuLhORAcI29E69tumoZ3akZmNPn7WWzhEuireE16asHk2mjkzpzFq7wuuWJrllEg==
-  dependencies:
-    "@tsed/schema" "6.40.0"
+    "@tsed/schema" "6.43.0"
     tslib "2.1.0"
 
-"@tsed/json-mapper@6.40.0", "@tsed/json-mapper@^6.34.3":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/json-mapper/-/json-mapper-6.40.0.tgz#6959af6329ba19956c857994b351bf01e1055258"
-  integrity sha512-2Zjy1nm2XGLR7yTc+1WoRkSco5dkcD77ymga3Ung06jb0gvACsnRJLSmci98YMHnkZJ8jfuPonndIKGqX69EFw==
+"@tsed/json-mapper@6.43.0", "@tsed/json-mapper@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/json-mapper/-/json-mapper-6.43.0.tgz#6b804b7e56a2c4bd1ad20b5721bb3b460c2ea5e2"
+  integrity sha512-Sy4w5VzSHsERdfBUAxZmkSCf6pC7Ri7qjCNBX9RaZQteqePPOBxugUzOqQCaOBoadawfpn9KDljo+gZbCjqJ9w==
   dependencies:
-    "@tsed/core" "6.40.0"
+    "@tsed/core" "6.43.0"
     tslib "2.1.0"
 
 "@tsed/logger@^5.12.1":
@@ -2883,42 +2873,42 @@
     streamroller "^1.0.3"
     tslib "2.1.0"
 
-"@tsed/openspec@6.40.0":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/openspec/-/openspec-6.40.0.tgz#5348d8c371d65813df711768dbe443b9d81f91fb"
-  integrity sha512-8Z23mQKyvJi+Xsl/1iO54Op5kOYXIzCtmUNOai3+K6FvTLJiSgmK8rYmIljD1Mv62WZ4AgL68QO7OcYYpDXo5Q==
+"@tsed/openspec@6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/openspec/-/openspec-6.43.0.tgz#defa3fa4dd4c5b978e14042c720f416b81abceec"
+  integrity sha512-vX25uMQ9DZj9F8ZUE4ztsLJDcC9Wjb7OPUqd97Ve+SQoVt0iK5cxw103hBfEjG1Jag2J6IEbeX5SbXjepTZdXg==
 
-"@tsed/platform-express@^6.34.3":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/platform-express/-/platform-express-6.40.0.tgz#eff9377a8d76a2e5824c32621de4f062e5c52dd4"
-  integrity sha512-Th+uTbPuohiIez64AAGKrmKonIX40iZrUi+ckyvDyS8c4a7QqKa8o929zlLC9lklqkykkdUW/CwwFprvqDM55g==
+"@tsed/platform-express@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/platform-express/-/platform-express-6.43.0.tgz#b8a6abbde54c8ad47e4cb414e102c94990bb656b"
+  integrity sha512-FsBJfR3wT3qwON8TS5cTITGG/si2KiW+vYXUXCbcgsyRgTGi83K2rNlVcRsZPqCwIJ/pH1fEWjQfMpLYbTIpSQ==
   dependencies:
     express "^4.17.1"
     multer "^1.4.2"
 
-"@tsed/schema@6.40.0", "@tsed/schema@^6.34.3":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/schema/-/schema-6.40.0.tgz#34af6d52daead4d1773d59b4f8c1a08e0c38b341"
-  integrity sha512-QfygeDCJFSE3xiwjN5nBl9RXWhsxM3dGi23kdoAecm+U+JUEn6NySccSSj8NcMh81/1FQKsLEkXa9iZ4ERIZZA==
+"@tsed/schema@6.43.0", "@tsed/schema@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/schema/-/schema-6.43.0.tgz#b4bb7b8d97eeeeeeb58f9f6d3bc6304792e09f82"
+  integrity sha512-fEP2ieYENPHLXba7ZT7Atys85A//g1f2LSvfWVVdyRPd+fZ/QKA61pDhFV/chWhNGrymqBRNczXcUVnq16l3dA==
   dependencies:
-    "@tsed/core" "6.40.0"
-    "@tsed/openspec" "6.40.0"
+    "@tsed/core" "6.43.0"
+    "@tsed/openspec" "6.43.0"
     change-case "4.1.2"
     micromatch "4.0.2"
     tslib "2.1.0"
 
-"@tsed/swagger@^6.34.3":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/swagger/-/swagger-6.40.0.tgz#6af7449d4ec99ac7edf8cff5216d727f105597d3"
-  integrity sha512-2cVWElVEpuVVZmrWyjOTVIB1yOD2c566dYxTaaGsegXvKJCZ2CVbX//V0a46YzueElJAd4Q3esbktKp2IkoM1A==
+"@tsed/swagger@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/swagger/-/swagger-6.43.0.tgz#a91fa9d7bf654a07f8d15c2aecab70ea80828016"
+  integrity sha512-wuL5UdlYEpV9mj3smx9Sxq7MhAC5ra32Sc0GL3n+3M2JeWQi2eELNhcsZBDVioh49yfcsI3S1c1/tTHNZPvLdw==
   dependencies:
     swagger-ui-dist "^3.46.0"
     tslib "2.1.0"
 
-"@tsed/typeorm@^6.34.3":
-  version "6.40.0"
-  resolved "https://registry.yarnpkg.com/@tsed/typeorm/-/typeorm-6.40.0.tgz#86a070f32dfef5101ac550958f96a90a79d23048"
-  integrity sha512-LTsI5xqPJdIzR6uksHp6tQwX9wnStR8+x/jhNfHplaFWIe/4gzKZo/3kVi3L7GeQ8EhZYsB2sqyc/aeG6THhXw==
+"@tsed/typeorm@^6.43.0":
+  version "6.43.0"
+  resolved "https://registry.yarnpkg.com/@tsed/typeorm/-/typeorm-6.43.0.tgz#f064d08d733fddf647e2db567235771b24409440"
+  integrity sha512-B2xU8fsKQYaAW+DNDoAOvawmqaqmKLia17p9AdTcc1aaxsJ51KTdpOxn9CkwcvWExMqyJNrJldELiaH5LuBqOw==
   dependencies:
     tslib "2.1.0"
 
@@ -5303,10 +5293,10 @@ charenc@~0.0.1:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
 
-check-node-version@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/check-node-version/-/check-node-version-4.0.2.tgz#0385880c42425651ce4aaee8de8008e4a640f5dc"
-  integrity sha512-PsIRqtX9i4oWuScZrBf7I3fDfGfo8aS5uU7F1jJ771X0lNwW6hd+SYgIfs9w8Cw9mY4bF2QlU8ZZ1KdYdCobFA==
+check-node-version@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/check-node-version/-/check-node-version-4.1.0.tgz#12ff45bfeb8dd591700a0ab848c21b2d8ceeeb94"
+  integrity sha512-TSXGsyfW5/xY2QseuJn8/hleO2AU7HxVCdkc900jp1vcfzF840GkjvRT7CHl8sRtWn23n3X3k0cwH9RXeRwhfw==
   dependencies:
     chalk "^3.0.0"
     map-values "^1.0.1"
@@ -6077,11 +6067,12 @@ convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
   dependencies:
     safe-buffer "~5.1.1"
 
-cookie-parser@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5"
+cookie-parser@^1.4.5:
+  version "1.4.5"
+  resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49"
+  integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==
   dependencies:
-    cookie "0.3.1"
+    cookie "0.4.0"
     cookie-signature "1.0.6"
 
 cookie-signature@1.0.6: