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

Merge branch 'feat/growi-bot' into feat/5569-without-proxy-accordion

Steven Fukase 5 лет назад
Родитель
Сommit
ea6238a9d6
59 измененных файлов с 2192 добавлено и 136 удалено
  1. 1 0
      .eslintignore
  2. 241 0
      .github/workflows/ci-slackbot-proxy.yml
  3. 20 10
      .github/workflows/ci.yml
  4. 2 2
      .gitignore
  5. 2 6
      .vscode/settings.json
  6. 18 2
      CHANGES.md
  7. 2 2
      docker/Dockerfile
  8. 1 0
      docker/Dockerfile.dockerignore
  9. 11 2
      package.json
  10. 2 0
      packages/slack/.eslintignore
  11. 31 0
      packages/slack/.eslintrc.js
  12. 1 0
      packages/slack/.gitignore
  13. 61 0
      packages/slack/jest.config.js
  14. 29 0
      packages/slack/package.json
  15. 8 0
      packages/slack/src/index.ts
  16. 5 0
      packages/slack/src/interfaces/growi-command.ts
  17. 9 0
      packages/slack/src/models/errors.ts
  18. 80 0
      packages/slack/src/utils/slash-command-parser.test.ts
  19. 17 0
      packages/slack/src/utils/slash-command-parser.ts
  20. 18 0
      packages/slack/tsconfig.build.json
  21. 12 0
      packages/slack/tsconfig.json
  22. 1 0
      packages/slackbot-proxy/.dockerignore
  23. 1 0
      packages/slackbot-proxy/.env
  24. 5 0
      packages/slackbot-proxy/.env.development
  25. 2 0
      packages/slackbot-proxy/.eslintignore
  26. 31 0
      packages/slackbot-proxy/.eslintrc.js
  27. 1 0
      packages/slackbot-proxy/.gitignore
  28. 3 0
      packages/slackbot-proxy/config/ci/.env.local.for-ci
  29. 32 0
      packages/slackbot-proxy/docker-compose.dev.yml
  30. 48 0
      packages/slackbot-proxy/package.json
  31. 94 0
      packages/slackbot-proxy/src/Server.ts
  32. 118 0
      packages/slackbot-proxy/src/controllers/slack.ts
  33. 44 0
      packages/slackbot-proxy/src/entities/installation.ts
  34. 38 0
      packages/slackbot-proxy/src/entities/order.ts
  35. 28 0
      packages/slackbot-proxy/src/index.ts
  36. 14 0
      packages/slackbot-proxy/src/repositories/installation.ts
  37. 10 0
      packages/slackbot-proxy/src/repositories/order.ts
  38. 60 0
      packages/slackbot-proxy/src/services/InstallerService.ts
  39. 16 0
      packages/slackbot-proxy/src/services/RecieveService.ts
  40. 10 0
      packages/slackbot-proxy/tsconfig.base.json
  41. 21 0
      packages/slackbot-proxy/tsconfig.build.json
  42. 10 0
      packages/slackbot-proxy/tsconfig.json
  43. 30 3
      resource/locales/en_US/admin/admin.json
  44. 31 4
      resource/locales/ja_JP/admin/admin.json
  45. 30 3
      resource/locales/zh_CN/admin/admin.json
  46. 35 0
      src/client/js/components/Admin/App/AppSetting.jsx
  47. 6 1
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  48. 145 30
      src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx
  49. 25 6
      src/client/js/components/Admin/Users/UserInviteModal.jsx
  50. 10 0
      src/client/js/services/AdminAppContainer.js
  51. 22 0
      src/client/styles/scss/_admin.scss
  52. 21 0
      src/client/styles/scss/theme/_apply-colors.scss
  53. 1 1
      src/linter-checker/test.scss
  54. 1 0
      src/server/models/config.js
  55. 5 0
      src/server/models/user.js
  56. 7 0
      src/server/routes/apiv3/app-settings.js
  57. 6 0
      src/server/service/config-loader.js
  58. 38 0
      tsconfig.base.json
  59. 621 64
      yarn.lock

+ 1 - 0
.eslintignore

@@ -1,6 +1,7 @@
 /.github/**
 /.vscode/**
 /node_modules/**
+/packages/**
 /public/**
 /src/client/js/legacy/thirdparty-js/**
 /src/client/js/util/reveal/plugins/markdown.js

+ 241 - 0
.github/workflows/ci-slackbot-proxy.yml

@@ -0,0 +1,241 @@
+name: Node CI for slackbot-proxy
+
+on:
+  push:
+    branches-ignore:
+      - release/**
+      - tmp/**
+    paths:
+      - .github/workflows/ci-slackbot-proxy.yml
+      - packages/slack/*
+      - packages/slackbot-proxy/*
+      - package.json
+      - yarn.lock
+
+defaults:
+  run:
+    working-directory: packages/slackbot-proxy
+
+jobs:
+
+  test:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [14.x]
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        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'
+      id: cache-yarn
+      run: echo "::set-output name=dir::$(yarn cache dir)"
+    - name: Cache/Restore yarn cache
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      uses: actions/cache@v2
+      with:
+        path: ${{ steps.cache-yarn.outputs.dir }}
+        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('./yarn.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
+    - name: Install dependencies
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      run: |
+        yarn --frozen-lockfile
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --depth=0
+    - name: yarn lint
+      run: |
+        yarn test
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-bot-proxy - test (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  launch-dev:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [14.x]
+
+    services:
+      mysql:
+        image: mysql:8.0
+        ports:
+          - 3306
+        options: --health-cmd "mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 3
+        env:
+          MYSQL_ALLOW_EMPTY_PASSWORD: yes
+          MYSQL_DATABASE: growi-slackbot-proxy
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        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'
+      id: cache-yarn
+      run: echo "::set-output name=dir::$(yarn cache dir)"
+    - name: Cache/Restore yarn cache
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      uses: actions/cache@v2
+      with:
+        path: ${{ steps.cache-yarn.outputs.dir }}
+        key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-yarn-
+    - name: Install dependencies
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      run: |
+        yarn --frozen-lockfile
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --depth=0
+    - name: yarn dev:ci
+      run: |
+        cp config/ci/.env.local.for-ci .env.local
+        yarn dev:ci
+      env:
+        TYPEORM_CONNECTION: mysql
+        TYPEORM_HOST: localhost
+        TYPEORM_PORT: ${{ job.services.mysql.ports[3306] }}
+        TYPEORM_DATABASE: growi-slackbot-proxy
+        TYPEORM_USERNAME: root
+        TYPEORM_PASSWORD:
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-bot-proxy - launch-dev (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  launch-prod:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [14.x]
+
+    services:
+      mysql:
+        image: mysql:8.0
+        ports:
+          - 3306
+        options: --health-cmd "mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 3
+        env:
+          MYSQL_ALLOW_EMPTY_PASSWORD: yes
+          MYSQL_DATABASE: growi-slackbot-proxy
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Get Date
+      id: date
+      run: |
+        echo ::set-output name=YmdH::$(date '+%Y%m%d%H')
+        echo ::set-output name=Ymd::$(date '+%Y%m%d')
+        echo ::set-output name=Ym::$(date '+%Y%m')
+        echo ::set-output name=Y::$(date '+%Y')
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v2
+      with:
+        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 }}
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ym }}
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Y }}
+    - name: Get yarn cache dir
+      id: cache-yarn
+      run: echo "::set-output name=dir::$(yarn cache dir)"
+    - name: Cache/Restore yarn cache
+      uses: actions/cache@v2
+      with:
+        path: ${{ steps.cache-yarn.outputs.dir }}
+        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('./yarn.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-yarn-
+    - name: Install dependencies
+      run: |
+        yarn --frozen-lockfile
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --depth=0
+    - name: yarn build
+      run: |
+        yarn build
+    - name: yarn install --production
+      run: |
+        yarn install --production
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --production --depth=0
+    - name: yarn start:prod:ci
+      run: |
+        cp config/ci/.env.local.for-ci .env.local
+        yarn start:prod:ci
+      env:
+        TYPEORM_CONNECTION: mysql
+        TYPEORM_HOST: localhost
+        TYPEORM_PORT: ${{ job.services.mysql.ports[3306] }}
+        TYPEORM_DATABASE: growi-slackbot-proxy
+        TYPEORM_USERNAME: root
+        TYPEORM_PASSWORD:
+    - name: Upload report as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Report
+        path: report
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-bot-proxy - launch-prod (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 20 - 10
.github/workflows/ci.yml

@@ -1,10 +1,20 @@
-name: Node CI
+name: Node CI for growi
 
 on:
   push:
     branches-ignore:
       - release/**
       - tmp/**
+    paths:
+      - .github/workflows/ci.yml
+      - .eslint*
+      - .prettier*
+      - .stylelint*
+      - config
+      - resource
+      - src
+      - package.json
+      - yarn.lock
 
 jobs:
 
@@ -42,7 +52,7 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -57,7 +67,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*test (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - lint (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -107,7 +117,7 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -129,7 +139,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*test (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - test (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -185,7 +195,7 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -200,7 +210,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*build-dev (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - build-dev (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -257,8 +267,8 @@ jobs:
           ${{ runner.os }}-yarn-
     - name: Install dependencies
       run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
-        yarn add -D react-images@1.0.0 react-motion
+        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
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -300,7 +310,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*build-prod (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - build-prod (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 2 - 2
.gitignore

@@ -1,12 +1,12 @@
 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 
 # dependencies
-/node_modules
+node_modules
 /.pnp
 .pnp.js
 
 # testing
-/coverage
+coverage
 
 # next.js
 /.next/

+ 2 - 6
.vscode/settings.json

@@ -1,10 +1,7 @@
 {
-  // 既定の改行文字。LF の場合には \n を CRLF の場合には \r\n を使用してください。
   "files.eol": "\n",
-  // 指定した構文に対してプロファイルを定義するか、特定の規則がある独自のプロファイルをご使用ください。
-  "emmet.syntaxProfiles": {
-    "javascript": "jsx"
-  },
+
+  "eslint.workingDirectories": [{ "mode": "auto" }],
 
   // use stylelint-plus
   // see https://qiita.com/y-w/items/bd7f11013fe34b69f0df#vs-code%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B
@@ -16,7 +13,6 @@
   "[scss]": {
     "editor.formatOnSave": true
   },
-  "stylelint.autoFixOnSave": true,
 
   // for vscode-eslint
   "[javascript]": {

+ 18 - 2
CHANGES.md

@@ -1,8 +1,24 @@
 # CHANGES
 
-## v4.2.14-RC
+## v4.2.15-EC
 
-* 
+* Support: Update libs
+    * eslint-config-weseek
+
+## v4.2.14
+
+* Feature: Add an option to restrict publishing email property for new users
+* Improvement: Invite modal in admin page without email server settings
+* Improvement: Global notification settings in admin page without email server settings
+* Fix: Can create pages on the share route
+    * Introduced by v4.2.8
+* Fix: Pages restrected by group are excluded for recurrence operation
+    * Introduced by v4.2.8
+* Fix: Rename and duplicate to descendants path does not work correctly
+    * Introduced by v4.2.8
+* Support: Update libs
+    * bunyan
+    * browser-bunyan
 
 ## v4.2.13
 

+ 2 - 2
docker/Dockerfile

@@ -20,9 +20,9 @@ COPY ./yarn.lock ./
 RUN yarn config set network-timeout 300000
 RUN yarn
 # install official plugins
-RUN yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
+RUN yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
 # install peerDependencies
-RUN yarn add -D react-images@1.0.0 react-motion
+RUN yarn add -W -D react-images@1.0.0 react-motion
 
 
 

+ 1 - 0
docker/Dockerfile.dockerignore

@@ -2,6 +2,7 @@
 .github
 .vscode
 node_modules
+packages
 src/linter-checker
 src/test
 .editorconfig

+ 11 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.2.14-RC",
+  "version": "4.2.15-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -19,6 +19,11 @@
   "bugs": {
     "url": "https://github.com/weseek/growi/issues"
   },
+  "private": true,
+  "workspaces": {
+    "packages": ["packages/*"],
+    "nohoist": []
+  },
   "scripts": {
     "build:api:jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
     "build:apiv3:jsdoc": "cross-env API_VERSION=3 npm run build:api:jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
@@ -177,6 +182,10 @@
     "@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",
@@ -196,7 +205,7 @@
     "diff2html": "^3.1.2",
     "eazy-logger": "^3.0.2",
     "eslint": "^6.0.1",
-    "eslint-config-weseek": "^1.0.4",
+    "eslint-config-weseek": "^1.0.8",
     "eslint-plugin-import": "^2.18.0",
     "eslint-plugin-jest": "^23.0.3",
     "eslint-plugin-react": "^7.14.2",

+ 2 - 0
packages/slack/.eslintignore

@@ -0,0 +1,2 @@
+/lib/**
+/node_modules/**

+ 31 - 0
packages/slack/.eslintrc.js

@@ -0,0 +1,31 @@
+module.exports = {
+  parser: '@typescript-eslint/parser',
+  extends: [
+    'weseek',
+    'weseek/typescript',
+    'plugin:jest/recommended',
+  ],
+  env: {
+    jquery: true,
+    'jest/globals': true,
+  },
+  globals: {
+  },
+  plugins: [
+    'jest',
+  ],
+  rules: {
+    'import/prefer-default-export': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    indent: [
+      'error',
+      2,
+      {
+        SwitchCase: 1,
+        ArrayExpression: 'first',
+        FunctionDeclaration: { body: 1, parameters: 2 },
+        FunctionExpression: { body: 1, parameters: 2 },
+      },
+    ],
+  },
+};

+ 1 - 0
packages/slack/.gitignore

@@ -0,0 +1 @@
+/lib/

+ 61 - 0
packages/slack/jest.config.js

@@ -0,0 +1,61 @@
+// For a detailed explanation regarding each configuration property, visit:
+// https://jestjs.io/docs/en/configuration.html
+
+const MODULE_NAME_MAPPING = {
+  '^\\^/(.+)$': '<rootDir>/$1',
+  '^~/(.+)$': '<rootDir>/src/$1',
+};
+
+module.exports = {
+
+  preset: 'ts-jest/presets/js-with-ts',
+
+  moduleNameMapper: MODULE_NAME_MAPPING,
+
+  // Automatically clear mock calls and instances between every test
+  clearMocks: true,
+
+  // Indicates whether the coverage information should be collected while executing the test
+  collectCoverage: true,
+
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
+  // collectCoverageFrom: undefined,
+
+  // The directory where Jest should output its coverage files
+  coverageDirectory: 'coverage',
+
+  // An array of regexp pattern strings used to skip coverage collection
+  coveragePathIgnorePatterns: [
+    '/node_modules/',
+  ],
+
+  // An object that configures minimum threshold enforcement for coverage results
+  // TODO: activate -- 2020.03.24 Yuki Takei
+  // coverageThreshold: {
+  //   global: {
+  //     branches: 70,
+  //     functions: 70,
+  //     lines: 70,
+  //     statements: 70,
+  //   },
+  // },
+
+  // An array of file extensions your modules use
+  moduleFileExtensions: [
+    'js',
+    'json',
+    'jsx',
+    'ts',
+    'tsx',
+    'node',
+  ],
+
+  // The test environment that will be used for testing
+  testEnvironment: 'node',
+
+  // The glob patterns Jest uses to detect test files
+  testMatch: [
+    '**/src/**/__tests__/**/*.[jt]s?(x)',
+    '**/src/**/?(*.)+(spec|test).[tj]s?(x)',
+  ],
+};

+ 29 - 0
packages/slack/package.json

@@ -0,0 +1,29 @@
+{
+  "name": "@growi/slack",
+  "version": "0.9.0-RC",
+  "license": "MIT",
+  "scripts": {
+    "test": "yarn test:lint && yarn test:coverage",
+    "test:unit": "cross-env NODE_ENV=test jest --passWithNoTests",
+    "test:coverage": "yarn test:unit",
+    "test:lint": "eslint src --ext .ts",
+    "test:lint:fix": "eslint src --ext .ts --fix"
+  },
+  "dependencies": {
+    "dotenv-flow": "^3.2.0"
+  },
+  "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",
+    "eslint-import-resolver-typescript": "^2.4.0",
+    "eslint-plugin-jest": "^24.3.2",
+    "ts-jest": "^26.5.4",
+    "typescript": "^4.2.3"
+  }
+}

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

@@ -0,0 +1,8 @@
+export const supportedSlackCommands: string[] = [
+  '/growi',
+];
+
+export const supportedGrowiCommands: string[] = [
+  'search',
+  'create',
+];

+ 5 - 0
packages/slack/src/interfaces/growi-command.ts

@@ -0,0 +1,5 @@
+export type GrowiCommand = {
+  text: string,
+  growiCommandType: string,
+  growiCommandArgs: string[],
+};

+ 9 - 0
packages/slack/src/models/errors.ts

@@ -0,0 +1,9 @@
+export class InvalidGrowiCommandError extends Error {
+
+  constructor(e?: string) {
+    super(e);
+    this.name = new.target.name;
+    Object.setPrototypeOf(this, new.target.prototype);
+  }
+
+}

+ 80 - 0
packages/slack/src/utils/slash-command-parser.test.ts

@@ -0,0 +1,80 @@
+import { SlashCommand } from '@slack/bolt';
+import { InvalidGrowiCommandError } from '../models/errors';
+
+import { parse } from './slash-command-parser';
+
+const SlashCommandMock = jest.fn<SlashCommand, [string]>().mockImplementation((text) => {
+  return { text } as SlashCommand;
+});
+
+describe('parse SlashCommand', () => {
+
+  describe('without growiCommandType', () => {
+    test('throws InvalidGrowiCommandError', () => {
+      // setup
+      const slashCommandText = '';
+      const slashCommand = new SlashCommandMock(slashCommandText);
+
+      // when/then
+      expect(() => {
+        parse(slashCommand);
+      }).toThrowError(InvalidGrowiCommandError);
+    });
+  });
+
+  test('returns a GrowiCommand instance with empty growiCommandArgs', () => {
+    // setup
+    const slashCommandText = 'search';
+    const slashCommand = new SlashCommandMock(slashCommandText);
+
+    // when
+    const result = parse(slashCommand);
+
+    // then
+    expect(result.text).toBe(slashCommandText);
+    expect(result.growiCommandType).toBe('search');
+    expect(result.growiCommandArgs).toStrictEqual([]);
+  });
+
+  test('returns a GrowiCommand instance with space growiCommandType', () => {
+    // setup
+    const slashCommandText = '   search   ';
+    const slashCommand = new SlashCommandMock(slashCommandText);
+
+    // when
+    const result = parse(slashCommand);
+
+    // then
+    expect(result.text).toBe(slashCommandText);
+    expect(result.growiCommandType).toBe('search');
+    expect(result.growiCommandArgs).toStrictEqual([]);
+  });
+
+  test('returns a GrowiCommand instance with space growiCommandArgs', () => {
+    // setup
+    const slashCommandText = '   search hoge   ';
+    const slashCommand = new SlashCommandMock(slashCommandText);
+
+    // when
+    const result = parse(slashCommand);
+
+    // then
+    expect(result.text).toBe(slashCommandText);
+    expect(result.growiCommandType).toBe('search');
+    expect(result.growiCommandArgs).toStrictEqual(['hoge']);
+  });
+
+  test('returns a GrowiCommand instance', () => {
+    // setup
+    const slashCommandText = 'search keyword1 keyword2';
+    const slashCommand = new SlashCommandMock(slashCommandText);
+
+    // when
+    const result = parse(slashCommand);
+
+    // then
+    expect(result.text).toBe(slashCommandText);
+    expect(result.growiCommandType).toBe('search');
+    expect(result.growiCommandArgs).toStrictEqual(['keyword1', 'keyword2']);
+  });
+});

+ 17 - 0
packages/slack/src/utils/slash-command-parser.ts

@@ -0,0 +1,17 @@
+import { GrowiCommand } from '../interfaces/growi-command';
+import { InvalidGrowiCommandError } from '../models/errors';
+
+export const parse = (slashCommand:{[key:string]:string}): GrowiCommand => {
+  const trimmedText = slashCommand.text.trim();
+  const splitted = trimmedText.split(' ');
+
+  if (splitted[0] === '') {
+    throw new InvalidGrowiCommandError('The SlashCommand.text does not specify GrowiCommand type');
+  }
+
+  return {
+    text: slashCommand.text,
+    growiCommandType: splitted[0],
+    growiCommandArgs: splitted.slice(1),
+  };
+};

+ 18 - 0
packages/slack/tsconfig.build.json

@@ -0,0 +1,18 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "rootDir": "./src",
+    "outDir": "../../dist/slack",
+    "composite": true,
+    "declaration": true,
+    "noResolve": false,
+    "preserveConstEnums": true,
+    "sourceMap": true,
+    "noEmit": false,
+    "inlineSources": true,
+
+    "baseUrl": ".",
+    "paths": {
+    }
+  }
+}

+ 12 - 0
packages/slack/tsconfig.json

@@ -0,0 +1,12 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+    }
+  },
+  "exclude": [
+    "node_modules",
+    "**/*.test.ts"
+  ]
+}

+ 1 - 0
packages/slackbot-proxy/.dockerignore

@@ -0,0 +1 @@
+node_modules

+ 1 - 0
packages/slackbot-proxy/.env

@@ -0,0 +1 @@
+SLACK_INSTALLPROVIDER_STATE_SECRET=change-it

+ 5 - 0
packages/slackbot-proxy/.env.development

@@ -0,0 +1,5 @@
+TYPEORM_CONNECTION=mysql
+TYPEORM_HOST=mysql
+TYPEORM_DATABASE=growi-slackbot-proxy
+TYPEORM_USERNAME=growi-slackbot-proxy
+TYPEORM_PASSWORD=YrkUi7rCW46Z2N6EXSFUBwaQTUR8biCU

+ 2 - 0
packages/slackbot-proxy/.eslintignore

@@ -0,0 +1,2 @@
+/dist/**
+/node_modules/**

+ 31 - 0
packages/slackbot-proxy/.eslintrc.js

@@ -0,0 +1,31 @@
+module.exports = {
+  parser: '@typescript-eslint/parser',
+  extends: [
+    'weseek',
+    'weseek/typescript',
+    'plugin:jest/recommended',
+  ],
+  env: {
+    jquery: true,
+    'jest/globals': true,
+  },
+  globals: {
+  },
+  plugins: [
+    'jest',
+  ],
+  rules: {
+    'import/prefer-default-export': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    indent: [
+      'error',
+      2,
+      {
+        SwitchCase: 1,
+        ArrayExpression: 'first',
+        FunctionDeclaration: { body: 1, parameters: 2 },
+        FunctionExpression: { body: 1, parameters: 2 },
+      },
+    ],
+  },
+};

+ 1 - 0
packages/slackbot-proxy/.gitignore

@@ -0,0 +1 @@
+/dist/

+ 3 - 0
packages/slackbot-proxy/config/ci/.env.local.for-ci

@@ -0,0 +1,3 @@
+SLACK_CLIENT_ID=dummy
+SLACK_CLIENT_SECRET=dummy
+SLACK_SIGNING_SECRET=dummy

+ 32 - 0
packages/slackbot-proxy/docker-compose.dev.yml

@@ -0,0 +1,32 @@
+version: '3'
+services:
+
+  mysql:
+    image: mysql:8.0
+    restart: unless-stopped
+    ports:
+      - 3306:3306
+    environment:
+      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
+      - MYSQL_USER=growi-slackbot-proxy
+      - MYSQL_PASSWORD=YrkUi7rCW46Z2N6EXSFUBwaQTUR8biCU
+      - MYSQL_DATABASE=growi-slackbot-proxy
+    volumes:
+      - /data/db
+
+  phpmyadmin:
+    depends_on:
+      - mysql
+    image: phpmyadmin
+    restart: unless-stopped
+    ports:
+      - 13306:80
+    environment:
+      - PMA_HOST=mysql
+      - PMA_USER=growi-slackbot-proxy
+      - PMA_PASSWORD=YrkUi7rCW46Z2N6EXSFUBwaQTUR8biCU
+
+networks:
+  default:
+    external:
+      name: growi_devcontainer_default

+ 48 - 0
packages/slackbot-proxy/package.json

@@ -0,0 +1,48 @@
+{
+  "name": "@growi/slackbot-proxy",
+  "version": "0.9.0-RC",
+  "license": "MIT",
+  "scripts": {
+    "build": "yarn tsc",
+    "tsc": "tsc -b --verbose tsconfig.build.json && tsc-alias -p tsconfig.build.json",
+    "tsc:w": "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",
+    "start:prod": "cross-env NODE_ENV=production node -r dotenv-flow/config ../../dist/slackbot-proxy/index.js",
+    "test": "yarn test:lint && yarn test:coverage",
+    "test:unit": "cross-env NODE_ENV=test jest --passWithNoTests",
+    "test:coverage": "yarn test:unit",
+    "test:lint": "eslint src --ext .ts",
+    "test:lint:fix": "eslint src --ext .ts --fix"
+  },
+  "dependencies": {
+    "@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",
+    "compression": "^1.7.4",
+    "dotenv-flow": "^3.2.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",
+    "@typescript-eslint/eslint-plugin": "^4.18.0",
+    "@typescript-eslint/parser": "^4.18.0",
+    "eslint-import-resolver-typescript": "^2.4.0",
+    "ts-jest": "^26.5.4",
+    "ts-node": "^9.1.1",
+    "ts-node-dev": "^1.1.6",
+    "tsc-alias": "1.2.6",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
+  }
+}

+ 94 - 0
packages/slackbot-proxy/src/Server.ts

@@ -0,0 +1,94 @@
+import { Configuration, Inject, InjectorService } from '@tsed/di';
+import { PlatformApplication } from '@tsed/common';
+import '@tsed/platform-express'; // /!\ keep this import
+import bodyParser from 'body-parser';
+import compress from 'compression';
+import cookieParser from 'cookie-parser';
+import methodOverride from 'method-override';
+import '@tsed/swagger';
+import { TypeORMService } from '@tsed/typeorm';
+import { ConnectionOptions } from 'typeorm';
+
+
+export const rootDir = __dirname;
+
+const connectionOptions: ConnectionOptions = {
+  // The 'name' property must be set. Otherwise, the 'name' will be '0' and won't work well. -- 2021.04.05 Yuki Takei
+  // see: https://github.com/TypedProject/tsed/blob/7630cda20a1f6fa3a692ecc3e6cd51d37bc3c45f/packages/typeorm/src/utils/createConnection.ts#L10
+  name: 'default',
+  type: process.env.TYPEORM_CONNECTION,
+  host: process.env.TYPEORM_HOST,
+  port: process.env.TYPEORM_PORT,
+  database: process.env.TYPEORM_DATABASE,
+  username: process.env.TYPEORM_USERNAME,
+  password: process.env.TYPEORM_PASSWORD,
+  synchronize: true,
+} as ConnectionOptions;
+
+
+@Configuration({
+  rootDir,
+  acceptMimes: ['application/json'],
+  httpPort: process.env.PORT || 8080,
+  httpsPort: false, // CHANGE
+  mount: {
+    '/': [
+      `${rootDir}/controllers/*.ts`,
+      `${rootDir}/middlewares/*.ts`,
+    ],
+  },
+  componentsScan: [
+    `${rootDir}/services/*.ts`,
+  ],
+  typeorm: [
+    {
+      ...connectionOptions,
+      entities: [
+        `${rootDir}/entities/*{.ts,.js}`,
+      ],
+      migrations: [
+        `${rootDir}/migrations/*{.ts,.js}`,
+      ],
+      subscribers: [
+        `${rootDir}/subscribers/*{.ts,.js}`,
+      ],
+    } as ConnectionOptions,
+  ],
+  swagger: [
+    {
+      path: '/docs',
+      specVersion: '3.0.1',
+    },
+  ],
+  exclude: [
+    '**/*.spec.ts',
+  ],
+})
+export class Server {
+
+  @Inject()
+  app: PlatformApplication;
+
+  @Configuration()
+  settings: Configuration;
+
+  @Inject()
+  injector: InjectorService;
+
+  $beforeRoutesInit(): void {
+    this.app
+      .use(cookieParser())
+      .use(compress({}))
+      .use(methodOverride())
+      .use(bodyParser.json())
+      .use(bodyParser.urlencoded({
+        extended: true,
+      }));
+  }
+
+  async $onReady(): Promise<void> {
+    // for synchromizing when boot
+    this.injector.get<TypeORMService>(TypeORMService);
+  }
+
+}

+ 118 - 0
packages/slackbot-proxy/src/controllers/slack.ts

@@ -0,0 +1,118 @@
+import {
+  BodyParams, Controller, Get, Inject, Post, Req, Res,
+} from '@tsed/common';
+
+import { Installation } from '~/entities/installation';
+import { Order } from '~/entities/order';
+
+import { InstallationRepository } from '~/repositories/installation';
+import { OrderRepository } from '~/repositories/order';
+import { InstallerService } from '~/services/InstallerService';
+import { ReceiveService } from '~/services/RecieveService';
+
+
+@Controller('/slack')
+export class SlackCtrl {
+
+  @Inject()
+  installerService: InstallerService;
+
+  @Inject()
+  installationRepository: InstallationRepository;
+
+  @Inject()
+  orderRepository: OrderRepository;
+
+  @Inject()
+  receiveService: ReceiveService;
+
+  @Get('/testsave')
+  testsave(): void {
+    const installation = new Installation();
+    installation.data = {
+      team: undefined,
+      enterprise: undefined,
+      user: {
+        id: '',
+        token: undefined,
+        scopes: undefined,
+      },
+    };
+
+    // const installationRepository = getRepository(Installation);
+
+    this.installationRepository.save(installation);
+  }
+
+
+  @Get('/install')
+  async install(): Promise<string> {
+    const url = await this.installerService.installer.generateInstallUrl({
+      // Add the scopes your app needs
+      scopes: [
+        'channels:history',
+        'commands',
+        'groups:history',
+        'im:history',
+        'mpim:history',
+        'chat:write',
+      ],
+    });
+
+    return `<a href="${url}">`
+      // eslint-disable-next-line max-len
+      + '<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" />'
+      + '</a>';
+  }
+
+  @Post('/events')
+  async handleEvent(@BodyParams() body:{[key:string]:string}, @Res() res: Res): Promise<string> {
+    // 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);
+    res.send();
+
+    const installation = await this.installationRepository.findByID('1');
+    if (installation == null) {
+      throw new Error('installation is reqiured');
+    }
+
+    // Find the latest order by installationId
+    let order = await this.orderRepository.findOne({
+      installation: installation.id,
+    }, {
+      order: {
+        createdAt: 'DESC',
+      },
+    });
+
+    if (order == null || order.isExpired()) {
+      order = await this.orderRepository.save({ installation: installation.id });
+    }
+
+    console.log('body', body);
+    console.log('order', order);
+
+    return 'This action will be handled by bolt service.';
+  }
+
+  @Get('/oauth_redirect')
+  async handleOauthRedirect(@Req() req: Req, @Res() res: Res): Promise<void> {
+    // illegal state
+    // TODO: https://youtrack.weseek.co.jp/issue/GW-5543
+    if (req.query.state === '') {
+      throw new Error('illegal state');
+    }
+
+    return this.installerService.installer.handleCallback(req, res);
+
+    // TODO: https://youtrack.weseek.co.jp/issue/GW-5543
+    // this.installer.handleCallback(req, res, {
+    //   success: (installation, metadata, req, res) => {},
+    //   failure: (error, installOptions, req, res) => {},
+    // });
+  }
+
+}

+ 44 - 0
packages/slackbot-proxy/src/entities/installation.ts

@@ -0,0 +1,44 @@
+import {
+  Required,
+} from '@tsed/schema';
+import {
+  Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, OneToMany,
+} from 'typeorm';
+
+import { Installation as SlackInstallation } from '@slack/oauth';
+import { Order } from './order';
+
+@Entity()
+export class Installation {
+
+  @PrimaryGeneratedColumn()
+  readonly id: number;
+
+  @Column({ type: 'json' })
+  @Required()
+  data: SlackInstallation;
+
+  @CreateDateColumn()
+  readonly createdAt: Date;
+
+  @UpdateDateColumn()
+  readonly updatedAt: Date;
+
+  @Column({ nullable: true })
+  isEnterpriseInstall?: boolean;
+
+  @Column({ nullable: true, unique: true })
+  teamId?: string;
+
+  @Column({ nullable: true, unique: true })
+  enterpriseId?: string;
+
+  setData(slackInstallation: SlackInstallation): void {
+    this.data = slackInstallation;
+
+    this.isEnterpriseInstall = slackInstallation.isEnterpriseInstall;
+    this.teamId = slackInstallation.team?.id;
+    this.enterpriseId = slackInstallation.enterprise?.id;
+  }
+
+}

+ 38 - 0
packages/slackbot-proxy/src/entities/order.ts

@@ -0,0 +1,38 @@
+import {
+  Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, ManyToOne,
+} from 'typeorm';
+import { Installation } from './installation';
+
+@Entity()
+export class Order {
+
+  @PrimaryGeneratedColumn()
+  readonly id: number;
+
+  @CreateDateColumn()
+  readonly createdAt: Date;
+
+  @UpdateDateColumn()
+  readonly updatedAt: Date;
+
+  @ManyToOne(() => Installation)
+  readonly installation: number;
+
+  @Column({ nullable: true, default: false })
+  isCompleted?: boolean;
+
+  @Column({ nullable: true })
+  growiUrl?: string;
+
+  @Column({ nullable: true })
+  growiAccessToken?: string;
+
+  @Column({ nullable: true })
+  proxyAccessToken?: string;
+
+  isExpired():boolean {
+    // TODO GW-5555 implement this
+    return false;
+  }
+
+}

+ 28 - 0
packages/slackbot-proxy/src/index.ts

@@ -0,0 +1,28 @@
+import { $log } from '@tsed/common';
+import { PlatformExpress } from '@tsed/platform-express';
+
+import { Server } from './Server';
+
+function hasProcessFlag(flag: string): boolean {
+  return process.argv.join('').indexOf(flag) > -1;
+}
+
+async function bootstrap() {
+  try {
+    $log.debug('Start server...');
+    const platform = await PlatformExpress.bootstrap(Server);
+
+    await platform.listen();
+    $log.debug('Server initialized');
+
+    if (hasProcessFlag('ci')) {
+      $log.info('"--ci" flag is detected. Exit process.');
+      process.exit();
+    }
+  }
+  catch (er) {
+    $log.error(er);
+  }
+}
+
+bootstrap();

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

@@ -0,0 +1,14 @@
+import {
+  Repository, EntityRepository,
+} from 'typeorm';
+
+import { Installation } from '~/entities/installation';
+
+@EntityRepository(Installation)
+export class InstallationRepository extends Repository<Installation> {
+
+  findByID(id: string): Promise<Installation | undefined> {
+    return this.findOne(id);
+  }
+
+}

+ 10 - 0
packages/slackbot-proxy/src/repositories/order.ts

@@ -0,0 +1,10 @@
+import {
+  Repository, EntityRepository,
+} from 'typeorm';
+
+import { Order } from '~/entities/order';
+
+@EntityRepository(Order)
+export class OrderRepository extends Repository<Order> {
+
+}

+ 60 - 0
packages/slackbot-proxy/src/services/InstallerService.ts

@@ -0,0 +1,60 @@
+import {
+  Installation as SlackInstallation, InstallationQuery, InstallProvider,
+} from '@slack/oauth';
+import { Inject, Service } from '@tsed/di';
+
+import { Installation } from '~/entities/installation';
+import { InstallationRepository } from '~/repositories/installation';
+
+@Service()
+export class InstallerService {
+
+  installer: InstallProvider;
+
+  @Inject()
+  private readonly repository: InstallationRepository;
+
+  $onInit(): Promise<any> | void {
+    const clientId = process.env.SLACK_CLIENT_ID;
+    const clientSecret = process.env.SLACK_CLIENT_SECRET;
+    const stateSecret = process.env.SLACK_INSTALLPROVIDER_STATE_SECRET;
+
+    if (clientId === undefined) {
+      throw new Error('The environment variable \'SLACK_CLIENT_ID\' must be defined.');
+    }
+    if (clientSecret === undefined) {
+      throw new Error('The environment variable \'SLACK_CLIENT_SECRET\' must be defined.');
+    }
+
+    const { repository } = this;
+
+    this.installer = new InstallProvider({
+      clientId,
+      clientSecret,
+      stateSecret,
+      installationStore: {
+        storeInstallation: async(slackInstallation: SlackInstallation<'v1' | 'v2', boolean>) => {
+          const installation = new Installation();
+          installation.setData(slackInstallation);
+
+          await repository.save(installation);
+
+          return;
+        },
+        fetchInstallation: async(installQuery: InstallationQuery<boolean>) => {
+          const installation: SlackInstallation<'v1' | 'v2', boolean> = {
+            team: undefined,
+            enterprise: undefined,
+            user: {
+              id: '',
+              token: undefined,
+              scopes: undefined,
+            },
+          };
+          return installation;
+        },
+      },
+    });
+  }
+
+}

+ 16 - 0
packages/slackbot-proxy/src/services/RecieveService.ts

@@ -0,0 +1,16 @@
+import { Service } from '@tsed/di';
+import { parse } from '@growi/slack/utils/slash-command-parser';
+
+@Service()
+export class ReceiveService {
+
+  receiveContentsFromSlack(body:{[key:string]:string}) : string {
+    const parseBody = parse(body);
+    if (parseBody.growiCommandType === 'register') {
+      console.log('register action occured');
+      return 'register action occurd';
+    }
+    return 'return receiveContentsFromSlack';
+  }
+
+}

+ 10 - 0
packages/slackbot-proxy/tsconfig.base.json

@@ -0,0 +1,10 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+  },
+  "exclude": [
+    "node_modules",
+    "config",
+    "**/*.test.ts"
+  ]
+}

+ 21 - 0
packages/slackbot-proxy/tsconfig.build.json

@@ -0,0 +1,21 @@
+{
+  "extends": "./tsconfig.base.json",
+  "compilerOptions": {
+    "rootDir": "./src",
+    "outDir": "../../dist/slackbot-proxy",
+    "declaration": true,
+    "noResolve": false,
+    "preserveConstEnums": true,
+    "sourceMap": true,
+    "noEmit": false,
+    "inlineSources": true,
+    "baseUrl": "./src",
+    "paths": {
+      "~/*": ["./*"],
+      "@growi/*": ["../../../dist/*"]
+    }
+  },
+  "references": [
+    { "path": "../slack/tsconfig.build.json" }
+  ]
+}

+ 10 - 0
packages/slackbot-proxy/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"],
+      "@growi/slack/*": ["../slack/src/*"],
+    }
+  }
+}

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

@@ -1,4 +1,5 @@
 {
+  "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
     "management_wiki": "Management Wiki",
     "system_information": "System information",
@@ -22,6 +23,7 @@
     "confidential_name": "Confidential name",
     "confidential_example": "ex): internal use only",
     "default_language": "Default language for new users",
+    "default_mail_visibility": "Disclose e-mail for new users",
     "file_uploading": "File uploading",
     "enable_files_except_image": "Enabling this option will allow upload of any file type. Without this option, only image file upload is supported.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
@@ -253,6 +255,27 @@
     "delete": "Delete"
   },
   "slack_integration": {
+    "selecting_bot_types": {
+      "slack_bot": "Slack bot",
+      "detailed_explanation": "Detailed explanation",
+      "selecting_bot_type": "・Select bot type",
+      "official_bot": "Official bot",
+      "custom_bot": "Custom bot",
+      "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",
+      "easy": "Easy",
+      "normal": "Normal",
+      "hard": "Hard",
+      "possible": "Possible",
+      "impossible": "Impossible"
+    },
     "bot_reset_successful": "Bot settings have been reset.",
     "copied_to_clipboard": "Copied to clipboard",
     "modal": {
@@ -277,16 +300,20 @@
     }
   },
   "user_management": {
-    "invite_users": "Invite new users",
+    "invite_users": "Temporarily issue a new user",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
     "invite_modal": {
-      "emails": "Emails (Possible to invite multiple people with new lines)",
+      "emails": "Emails (Possible to issue multiple people with new lines)",
+      "description1":"Temporarily issue new users by email addresses.",
+      "description2":"A temporary password will be generated for the first login.",
       "invite_thru_email": "Send invitation email",
+      "mail_setting_link":"<i class='icon-settings mr-2'></i><a href='/admin/app'>Email settings</a>",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
       "send_temporary_password": "Be sure to copy the temporary password ON THIS SCREEN and send it to the user.",
-      "existing_email": "The following emails already exist"
+      "existing_email": "The following emails already exist",
+      "issue": "Issue"
     },
     "user_table": {
       "administrator": "Administrator",

+ 31 - 4
resource/locales/ja_JP/admin/admin.json

@@ -1,4 +1,5 @@
 {
+  "mailer_setup_required":"送信するには <a href='/admin/app'>メールの設定</a> が必要です。",
   "admin_top": {
     "management_wiki": "Wiki管理",
     "system_information": "システム情報",
@@ -22,6 +23,7 @@
     "confidential_name": "コンフィデンシャル表示",
     "confidential_example": "例: 社外秘",
     "default_language": "新規ユーザーのデフォルト設定言語",
+    "default_mail_visibility": "新規ユーザーの初期メール公開設定",
     "file_uploading": "ファイルアップロード",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
     "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
@@ -251,6 +253,27 @@
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
   "slack_integration": {
+    "selecting_bot_types": {
+      "slack_bot": "Slack bot",
+      "detailed_explanation": "詳しい説明はこちら",
+      "selecting_bot_type": "・Botタイプを選択する",
+      "official_bot": "Official bot",
+      "custom_bot": "Custom bot",
+      "without_proxy": "without proxy",
+      "with_proxy": "with proxy",
+      "recommended": "おすすめ",
+      "for_beginners": "- 初心者向け -",
+      "for_intermediate": "- 中級者向け -",
+      "for_advanced": "- 上級者向け -",
+      "set_up": "セットアップ",
+      "multiple_workspaces_integration": "複数ワークスペースとの連携",
+      "security_control": "セキュリティコントロール",
+      "easy": "かんたん",
+      "normal": "ふつう",
+      "hard": "むずかしい",
+      "possible": "可能",
+      "impossible": "不可"
+    },
     "bot_reset_successful": "Botの設定を消去しました。",
     "copied_to_clipboard": "クリップボードにコピーされました。",
     "modal": {
@@ -275,16 +298,20 @@
     }
   },
   "user_management": {
-    "invite_users": "新規ユーザーの招待",
+    "invite_users": "新規ユーザーの仮発行",
     "click_twice_same_checkbox": "少なくとも一つはチェックしてください。",
     "invite_modal": {
-      "emails": "メールアドレス (複数行入力で複数人招待可能)",
-      "invite_thru_email": "招待をメールで送信",
+      "emails": "メールアドレス (複数行入力で複数人発行可能)",
+      "description1":"メールアドレスを使用して新規ユーザーを仮発行します。",
+      "description2":"初回のログイン時に使用する仮パスワードが生成されます。",
+      "invite_thru_email": "招待メールを送信する",
+      "mail_setting_link":"<i class='icon-settings mr-2'></i><a href='/admin/app'>メールの設定</a>",
       "valid_email": "メールアドレスを入力してください。",
       "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
       "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
       "send_temporary_password": "招待メールを送っていない場合、この画面で必ず仮パスワードをコピーし、招待者へ連絡してください。",
-      "existing_email": "以下のEmailはすでに存在しています。"
+      "existing_email": "以下のEmailはすでに存在しています。",
+      "issue": "発行"
     },
     "user_table": {
       "administrator": "管理者",

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

@@ -1,4 +1,5 @@
 {
+  "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
 	"admin_top": {
 		"management_wiki": "管理Wiki",
 		"system_information": "系统信息",
@@ -22,6 +23,7 @@
 		"confidential_name": "内部名称",
 		"confidential_example": "ex):仅供内部使用",
 		"default_language": "新用户的默认语言",
+		"default_mail_visibility": "新用户的默认电子邮件可见性",
 		"file_uploading": "文件上传",
 		"enable_files_except_image": "启用此选项将允许上传任何文件类型。如果没有此选项,则仅支持图像文件上载。",
 		"attach_enable": "如果启用此选项,则可以附加图像文件以外的文件。",
@@ -261,6 +263,27 @@
 		"delete": "删除"
   },
   "slack_integration": {
+    "selecting_bot_types": {
+      "slack_bot": "Slack bot",
+      "detailed_explanation": "详细说明",
+      "selecting_bot_type": "・选择机器人类型",
+      "official_bot": "Official bot",
+      "custom_bot": "Custom bot",
+      "without_proxy": "without proxy",
+      "with_proxy": "with proxy",
+      "recommended": "受到推崇的",
+      "for_beginners": "- 对于初学者 -",
+      "for_intermediate": "- 对于中级 -",
+      "for_advanced": "- 对于高级 -",
+      "set_up": "设置",
+      "multiple_workspaces_integration": "集成到多个工作区",
+      "security_control": "安全控制",
+      "easy": "简单",
+      "normal": "通常",
+      "hard": "难的",
+      "possible": "可能的",
+      "impossible": "不可能"
+    },
     "bot_reset_successful": "删除了BOT设置。",
     "copied_to_clipboard": "它已复制到剪贴板。",
     "modal": {
@@ -285,16 +308,20 @@
     }
   },
 	"user_management": {
-		"invite_users": "邀请新用户",
+		"invite_users": "临时发布新用户",
 		"click_twice_same_checkbox": "您应该至少选中一个复选框。",
 		"invite_modal": {
 			"emails": "电子邮件",
-			"invite_thru_email": "发送邀请电子邮件",
+      "description1":"通过电子邮件地址临时发布新用户。",
+      "description2":"将为首次登录生成一个临时密码。",
+      "mail_setting_link":"<i class='icon-settings mr-2'></i><a href='/admin/app'>Email settings</a>",
 			"valid_email": "需要有效的电子邮件地址",
+			"invite_thru_email": "发送邀请电子邮件",
 			"temporary_password": "创建的用户具有临时密码",
 			"send_new_password": "请将新密码发送给用户。",
 			"send_temporary_password": "请确保复制此屏幕上的临时密码并将其发送给用户。",
-			"existing_email": "以下电子邮件已存在"
+			"existing_email": "以下电子邮件已存在",
+      "issue": "Issue"
 		},
 		"user_table": {
 			"administrator": "管理员",

+ 35 - 0
src/client/js/components/Admin/App/AppSetting.jsx

@@ -103,6 +103,41 @@ class AppSetting extends React.Component {
           </div>
         </div>
 
+        <div className="row form-group mb-5">
+          <label
+            className="text-left text-md-right col-md-3 col-form-label"
+          >
+            {t('admin:app_setting.default_mail_visibility')}
+          </label>
+          <div className="col-md-6 py-2">
+
+            <div className="custom-control custom-radio custom-control-inline">
+              <input
+                type="radio"
+                id="radio-email-show"
+                className="custom-control-input"
+                name="mailVisibility"
+                checked={adminAppContainer.state.isEmailPublishedForNewUser === true}
+                onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(true) }}
+              />
+              <label className="custom-control-label" htmlFor="radio-email-show">{t('Show')}</label>
+            </div>
+
+            <div className="custom-control custom-radio custom-control-inline">
+              <input
+                type="radio"
+                id="radio-email-hide"
+                className="custom-control-input"
+                name="mailVisibility"
+                checked={adminAppContainer.state.isEmailPublishedForNewUser === false}
+                onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(false) }}
+              />
+              <label className="custom-control-label" htmlFor="radio-email-hide">{t('Hide')}</label>
+            </div>
+
+          </div>
+        </div>
+
         <div className="row form-group mb-5">
           <label
             className="text-left text-md-right col-md-3 col-form-label"

+ 6 - 1
src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -96,7 +96,9 @@ class ManageGlobalNotification extends React.Component {
 
 
   render() {
-    const { t } = this.props;
+    const { t, appContainer } = this.props;
+    const { isMailerSetup } = appContainer.config;
+
     return (
       <React.Fragment>
 
@@ -179,7 +181,10 @@ class ManageGlobalNotification extends React.Component {
                     />
 
                   </div>
+
                   <p className="p-2">
+                    {/* eslint-disable-next-line react/no-danger */}
+                    {!isMailerSetup && <span className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:mailer_setup_required') }} />}
                     <b>Hint: </b>
                     <a href="https://ifttt.com/create" target="blank">{t('notification_setting.email.ifttt_link')}
                       <i className="icon-share-alt" />

+ 145 - 30
src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -103,6 +103,16 @@ const SlackIntegration = (props) => {
       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 (
     <>
       <ConfirmBotChangeModal
@@ -117,42 +127,147 @@ const SlackIntegration = (props) => {
         onClickGenerateToken={generateTokenHandler}
       />
 
-      <div className="row my-5">
-        <div className="card-deck mx-auto">
-
-          <div
-            className={`card mx-3 py-5 rounded ${currentBotType === 'official-bot' ? 'border-info' : ''}`}
-            role="button"
-            onClick={() => handleBotTypeSelect('official-bot')}
-          >
-            <div className="card-body">
-              <h5 className="card-title">Official Bot</h5>
-              <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content.</p>
+      <div className="selecting-bot-type my-5">
+        <h2 className="admin-setting-header mb-4">
+          {t('admin:slack_integration.selecting_bot_types.slack_bot')}
+          <span className="ml-2 btn-link">
+            <span className="mr-1">{t('admin:slack_integration.selecting_bot_types.detailed_explanation')}</span>
+            {/* TODO: add an appropriate link by GW-5614 */}
+            <i className="fa fa-external-link" aria-hidden="true"></i>
+          </span>
+
+        </h2>
+
+        {t('admin:slack_integration.selecting_bot_types.selecting_bot_type')}
+
+        <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>
 
-          <div
-            className={`card mx-3 py-5 rounded ${currentBotType === 'custom-bot-without-proxy' ? 'border-info' : ''}`}
-            role="button"
-            onClick={() => handleBotTypeSelect('custom-bot-without-proxy')}
-          >
-            <div className="card-body">
-              <h5 className="card-title">Custom Bot (Without Proxy)</h5>
-              <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. </p>
+            <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>
 
-          <div
-            className={`card mx-3 py-5 rounded ${currentBotType === 'custom-bot-with-proxy' ? 'border-info' : ''}`}
-            role="button"
-            onClick={() => handleBotTypeSelect('custom-bot-with-proxy')}
-          >
-            <div className="card-body">
-              <h5 className="card-title">Custom Bot (With Proxy)</h5>
-              <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content.</p>
+            <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>
-          </div>
 
+          </div>
         </div>
       </div>
 

+ 25 - 6
src/client/js/components/Admin/Users/UserInviteModal.jsx

@@ -46,7 +46,12 @@ class UserInviteModal extends React.Component {
 
     return (
       <>
-        <label> {t('admin:user_management.invite_modal.emails')}</label>
+        <label>{t('admin:user_management.invite_modal.emails')}</label>
+        <p>
+          {t('admin:user_management.invite_modal.description1')}
+          <br />
+          {t('admin:user_management.invite_modal.description2')}
+        </p>
         <textarea
           className="form-control"
           placeholder="e.g.&#13;&#10;user1@growi.org&#13;&#10;user2@growi.org"
@@ -74,15 +79,29 @@ class UserInviteModal extends React.Component {
   }
 
   renderModalFooter() {
-    const { t } = this.props;
+    const { t, appContainer } = this.props;
+    const { isMailerSetup } = appContainer.config;
 
     return (
       <>
         <div className="col text-left custom-control custom-checkbox custom-checkbox-info text-left" onChange={this.handleCheckBox}>
-          <input type="checkbox" id="sendEmail" className="custom-control-input" name="sendEmail" defaultChecked={this.state.sendEmail} />
+          <input
+            type="checkbox"
+            id="sendEmail"
+            className="custom-control-input"
+            name="sendEmail"
+            defaultChecked={this.state.sendEmail}
+            disabled={!isMailerSetup}
+          />
           <label className="custom-control-label" htmlFor="sendEmail">
             {t('admin:user_management.invite_modal.invite_thru_email')}
           </label>
+          {isMailerSetup
+            // eslint-disable-next-line react/no-danger
+            ? <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:user_management.invite_modal.mail_setting_link') }} />
+            // eslint-disable-next-line react/no-danger
+            : <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:mailer_setup_required') }} />
+          }
         </div>
         <div>
           <button
@@ -90,7 +109,7 @@ class UserInviteModal extends React.Component {
             className="btn btn-outline-secondary mr-2"
             onClick={this.onToggleModal}
           >
-            Cancel
+            {t('Cancel')}
           </button>
 
           <button
@@ -99,7 +118,7 @@ class UserInviteModal extends React.Component {
             onClick={this.handleSubmit}
             disabled={!this.validEmail()}
           >
-            Invite
+            {t('admin:user_management.invite_modal.issue')}
           </button>
         </div>
       </>
@@ -119,7 +138,7 @@ class UserInviteModal extends React.Component {
           className="btn btn-outline-secondary"
           onClick={this.onToggleModal}
         >
-          Close
+          {t('Close')}
         </button>
       </>
     );

+ 10 - 0
src/client/js/services/AdminAppContainer.js

@@ -19,6 +19,7 @@ export default class AdminAppContainer extends Container {
       title: this.dummyTitle,
       confidential: '',
       globalLang: '',
+      isEmailPublishedForNewUser: true,
       fileUpload: '',
 
       siteUrl: '',
@@ -78,6 +79,7 @@ export default class AdminAppContainer extends Container {
       title: appSettingsParams.title,
       confidential: appSettingsParams.confidential,
       globalLang: appSettingsParams.globalLang,
+      isEmailPublishedForNewUser: appSettingsParams.isEmailPublishedForNewUser,
       fileUpload: appSettingsParams.fileUpload,
       siteUrl: appSettingsParams.siteUrl,
       envSiteUrl: appSettingsParams.envSiteUrl,
@@ -144,6 +146,13 @@ export default class AdminAppContainer extends Container {
     this.setState({ globalLang });
   }
 
+  /**
+   * Change isEmailPublishedForNewUser
+   */
+  changeIsEmailPublishedForNewUserShow(isEmailPublishedForNewUser) {
+    this.setState({ isEmailPublishedForNewUser });
+  }
+
   /**
    * Change fileUpload
    */
@@ -309,6 +318,7 @@ export default class AdminAppContainer extends Container {
       title: this.state.title,
       confidential: this.state.confidential,
       globalLang: this.state.globalLang,
+      isEmailPublishedForNewUser: this.state.isEmailPublishedForNewUser,
       fileUpload: this.state.fileUpload,
     });
     const { appSettingParams } = response.data;

+ 22 - 0
src/client/styles/scss/_admin.scss

@@ -83,6 +83,28 @@
     }
   }
 
+  /*
+  Slack Integration
+  */
+  .selecting-bot-type {
+    .btn-link {
+      font-size: 1rem;
+    }
+    .supplementary-desc {
+      font-size: 1rem;
+    }
+    .badge-info {
+      font-size: 0.6rem;
+    }
+    .admin-bot-card {
+      min-width: 280px;
+      border-radius: 8px !important;
+    }
+    .border-primary {
+      border-width: 2px;
+    }
+  }
+
   //// TODO: migrate to Bootstrap 4
   //// omit all .btn-toggle and use Switches
   //// https://getbootstrap.com/docs/4.2/components/forms/#switches

+ 21 - 0
src/client/styles/scss/theme/_apply-colors.scss

@@ -591,3 +591,24 @@ mark.rbt-highlight-text {
 .grw-btn-page-management:focus {
   background-color: rgba($color-link, 0.15);
 }
+
+/*
+  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;
+  }
+}

+ 1 - 1
src/linter-checker/test.scss

@@ -19,7 +19,7 @@
   background: $gray-300;
   color: $gray-900;
 
-  ul > li {
+  ul>li {
     margin-left: 0;
   }
 }

+ 1 - 0
src/server/models/config.js

@@ -229,6 +229,7 @@ module.exports = function(crowi) {
       isAclEnabled: crowi.aclService.isAclEnabled(),
       isSearchServiceConfigured: crowi.searchService.isConfigured,
       isSearchServiceReachable: crowi.searchService.isReachable,
+      isMailerSetup: crowi.mailService.isMailerSetup,
       globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     };
 

+ 5 - 0
src/server/models/user.js

@@ -268,6 +268,7 @@ module.exports = function(crowi) {
     this.name = name;
     this.username = username;
     this.status = STATUS_ACTIVE;
+    this.isEmailPublished = crowi.configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser');
 
     this.save((err, userData) => {
       userEvent.emit('activated', userData);
@@ -651,6 +652,10 @@ module.exports = function(crowi) {
     }
 
     const configManager = crowi.configManager;
+
+    // Default email show/hide is up to the administrator
+    newUser.isEmailPublished = configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser');
+
     const globalLang = configManager.getConfig('crowi', 'app:globalLang');
     if (globalLang != null) {
       newUser.lang = globalLang;

+ 7 - 0
src/server/routes/apiv3/app-settings.js

@@ -38,6 +38,9 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          globalLang:
  *            type: string
  *            description: language set when create user
+ *          isEmailPublishedForNewUser:
+ *            type: boolean
+ *            description: default email show/hide setting when create user
  *          fileUpload:
  *            type: boolean
  *            description: enable upload file except image file
@@ -154,6 +157,7 @@ module.exports = (crowi) => {
       body('title').trim(),
       body('confidential'),
       body('globalLang').isIn(listLocaleIds()),
+      body('isEmailPublishedForNewUser').isBoolean(),
       body('fileUpload').isBoolean(),
     ],
     siteUrlSetting: [
@@ -219,6 +223,7 @@ module.exports = (crowi) => {
       title: crowi.configManager.getConfig('crowi', 'app:title'),
       confidential: crowi.configManager.getConfig('crowi', 'app:confidential'),
       globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
+      isEmailPublishedForNewUser: crowi.configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser'),
       fileUpload: crowi.configManager.getConfig('crowi', 'app:fileUpload'),
       siteUrl: crowi.configManager.getConfig('crowi', 'app:siteUrl'),
       envSiteUrl: crowi.configManager.getConfigFromEnvVars('crowi', 'app:siteUrl'),
@@ -289,6 +294,7 @@ module.exports = (crowi) => {
       'app:title': req.body.title,
       'app:confidential': req.body.confidential,
       'app:globalLang': req.body.globalLang,
+      'customize:isEmailPublishedForNewUser': req.body.isEmailPublishedForNewUser,
       'app:fileUpload': req.body.fileUpload,
     };
 
@@ -298,6 +304,7 @@ module.exports = (crowi) => {
         title: crowi.configManager.getConfig('crowi', 'app:title'),
         confidential: crowi.configManager.getConfig('crowi', 'app:confidential'),
         globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
+        isEmailPublishedForNewUser: crowi.configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser'),
         fileUpload: crowi.configManager.getConfig('crowi', 'app:fileUpload'),
       };
       return res.apiv3({ appSettingParams });

+ 6 - 0
src/server/service/config-loader.js

@@ -422,6 +422,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     default: null,
   },
+  DEFAULT_EMAIL_PUBLISHED: {
+    ns:      'crowi',
+    key:     'customize:isEmailPublishedForNewUser',
+    type:    TYPES.BOOLEAN,
+    default: true,
+  },
 };
 
 class ConfigLoader {

+ 38 - 0
tsconfig.base.json

@@ -0,0 +1,38 @@
+{
+  "compilerOptions": {
+    "target": "es2019",
+    "module": "commonjs",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "sourceMap": true,
+    "noEmit": true,
+    "removeComments": false,
+    "importHelpers": true,
+    "isolatedModules": true,
+
+    /* Strict Type-Checking Options */
+    // "strict": true,
+    "strictNullChecks": true,
+    "noImplicitAny": false,
+
+    /* Additional Checks */
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+
+    /* Module Resolution Options */
+    "moduleResolution": "node",
+    "typeRoots": [
+      "./node_modules/@types"
+    ],
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+
+    /* Misc */
+    "preserveConstEnums": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+
+    /* Experimental Options */
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 621 - 64
yarn.lock


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