ソースを参照

Merge branch 'feat/growi-bot' into imprv/remain-red-if-not-correct-scope

zahmis 5 年 前
コミット
ce1f7ea1be
28 ファイル変更419 行追加131 行削除
  1. 7 1
      CHANGES.md
  2. 1 1
      package.json
  3. 2 1
      packages/slack/src/index.ts
  4. 7 0
      packages/slack/src/utils/webclient-factory.ts
  5. 81 3
      packages/slackbot-proxy/src/controllers/slack.ts
  6. 53 12
      packages/slackbot-proxy/src/middlewares/authorizer.ts
  7. 5 2
      packages/slackbot-proxy/src/services/RegisterService.ts
  8. 5 2
      resource/locales/en_US/admin/admin.json
  9. 4 2
      resource/locales/ja_JP/admin/admin.json
  10. 5 3
      resource/locales/zh_CN/admin/admin.json
  11. 2 2
      src/client/js/components/Admin/SlackIntegration/BotTypeCard.jsx
  12. 5 2
      src/client/js/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx
  13. 52 0
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxyIntegrationCard.jsx
  14. 5 38
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  15. 32 0
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettingsAccordion.jsx
  16. 37 38
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxyIntegrationCard.jsx
  17. 15 14
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  18. 11 3
      src/client/js/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  19. 38 0
      src/client/js/components/Admin/SlackIntegration/OfficialbotSettingsAccordion.jsx
  20. 1 2
      src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx
  21. 12 2
      src/client/js/components/PageEditor.jsx
  22. 0 1
      src/client/js/components/SavePageControls/GrantSelector.jsx
  23. 12 1
      src/client/styles/scss/_admin.scss
  24. 6 0
      src/client/styles/scss/theme/_apply-colors.scss
  25. 7 0
      src/client/styles/scss/theme/island.scss
  26. 9 0
      src/client/styles/scss/theme/spring.scss
  27. 4 0
      src/server/routes/apiv3/slack-bot.js
  28. 1 1
      src/server/routes/apiv3/slack-integration.js

+ 7 - 1
CHANGES.md

@@ -1,10 +1,16 @@
 # CHANGES
 
-## v4.2.16-RC
+## v4.2.17-RC
 
+* Fix: No unsaved alert is displayed without difference the latest markdown and editor value
 * Support: Update libs
     * eslint-config-weseek
 
+## v4.2.16
+
+* Fix: "Only inside the group" causes an error
+    * Introduced by v4.2.15
+
 ## v4.2.15
 
 * Improvement: toastr location for editing

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.2.16-RC",
+  "version": "4.2.17-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

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

@@ -9,6 +9,7 @@ export const supportedGrowiCommands: string[] = [
 
 export * from './interfaces/growi-command';
 export * from './models/errors';
-export * from './utils/slash-command-parser';
 export * from './middlewares/verification-slack-request';
 export * from './utils/block-creater';
+export * from './utils/slash-command-parser';
+export * from './utils/webclient-factory';

+ 7 - 0
packages/slack/src/utils/webclient-factory.ts

@@ -0,0 +1,7 @@
+import { LogLevel, WebClient } from '@slack/web-api';
+
+const isProduction = process.env.NODE_ENV === 'production';
+
+export const generateWebClient = (botToken: string): WebClient => {
+  return new WebClient(botToken, { logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
+};

+ 81 - 3
packages/slackbot-proxy/src/controllers/slack.ts

@@ -4,7 +4,7 @@ import {
 
 import axios from 'axios';
 
-import { parseSlashCommand } from '@growi/slack';
+import { generateMarkdownSectionBlock, generateWebClient, parseSlashCommand } from '@growi/slack';
 import { Installation } from '~/entities/installation';
 
 import { InstallationRepository } from '~/repositories/installation';
@@ -106,7 +106,7 @@ export class SlackCtrl {
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
     const relations = await this.relationRepository.find({ installation: installation?.id });
 
-    await relations.map((relation: Relation) => {
+    const promises = relations.map((relation: Relation) => {
       // generate API URL
       const url = new URL('/_api/v3/slack-bot/commands', relation.growiUri);
       return axios.post(url.toString(), {
@@ -115,6 +115,32 @@ export class SlackCtrl {
         growiCommand,
       });
     });
+
+    // pickup PromiseRejectedResult only
+    const results = await Promise.allSettled(promises);
+    const rejectedResults: PromiseRejectedResult[] = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
+
+    if (rejectedResults.length > 0) {
+      const botToken = installation?.data.bot?.token;
+
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      const client = generateWebClient(botToken!);
+
+      try {
+        client.chat.postEphemeral({
+          text: 'Error occured.',
+          channel: body.channel_id,
+          user: body.user_id,
+          blocks: [
+            generateMarkdownSectionBlock('*Error occured:*'),
+            ...rejectedResults.map(result => generateMarkdownSectionBlock(result.reason.toString())),
+          ],
+        });
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    }
   }
 
   @Post('/interactions')
@@ -122,7 +148,59 @@ export class SlackCtrl {
   async handleInteraction(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string> {
     logger.info('receive interaction', req.body);
     logger.info('receive interaction', req.authorizeResult);
-    return;
+
+    const { body, authorizeResult } = req;
+
+    // Send response immediately to avoid opelation_timeout error
+    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+    res.send();
+
+    // pass
+    if (body.ssl_check != null) {
+      return;
+    }
+
+    const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
+
+    const handleViewSubmission = async(inputValues) => {
+
+      const inputGrowiUrl = inputValues.growiDomain.contents_input.value;
+      const inputGrowiAccessToken = inputValues.growiAccessToken.contents_input.value;
+      const inputProxyAccessToken = inputValues.proxyToken.contents_input.value;
+
+      const order = await this.orderRepository.findOne({ installation: installation?.id, growiUrl: inputGrowiUrl });
+      if (order != null) {
+        this.orderRepository.update(
+          { installation: installation?.id, growiUrl: inputGrowiUrl },
+          { growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken },
+        );
+      }
+      else {
+        this.orderRepository.save({
+          installation: installation?.id, growiUrl: inputGrowiUrl, growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken,
+        });
+      }
+    };
+
+    const payload = JSON.parse(body.payload);
+    const { type } = payload;
+    const inputValues = payload.view.state.values;
+
+    try {
+      switch (type) {
+        case 'view_submission':
+          await handleViewSubmission(inputValues);
+          break;
+        default:
+          break;
+      }
+    }
+    catch (error) {
+      logger.error(error);
+    }
+
   }
 
   @Post('/events')

+ 53 - 12
packages/slackbot-proxy/src/middlewares/authorizer.ts

@@ -1,11 +1,14 @@
-import { InstallationQuery } from '@slack/oauth';
+import { AuthorizeResult, InstallationQuery } from '@slack/oauth';
 import {
   IMiddleware, Inject, Middleware, Req, Res,
 } from '@tsed/common';
 
+import Logger from 'bunyan';
+
 import { AuthedReq } from '~/interfaces/authorized-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { InstallerService } from '~/services/InstallerService';
+import loggerFactory from '~/utils/logger';
 
 @Middleware()
 export class AuthorizeCommandMiddleware implements IMiddleware {
@@ -16,15 +19,22 @@ export class AuthorizeCommandMiddleware implements IMiddleware {
   @Inject()
   installationRepository: InstallationRepository;
 
+  private logger: Logger;
+
+  constructor() {
+    this.logger = loggerFactory('slackbot-proxy:middlewares:AuthorizeCommandMiddleware');
+  }
+
   async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
     const { body } = req;
 
     // extract id from body
     const teamId = body.team_id;
     const enterpriseId = body.enterprize_id;
+    const isEnterpriseInstall = body.is_enterprise_install === 'true';
 
     if (teamId == null && enterpriseId == null) {
-      res.writeHead(400);
+      res.writeHead(400, 'No installation found');
       return res.end();
     }
 
@@ -32,13 +42,22 @@ export class AuthorizeCommandMiddleware implements IMiddleware {
     const query: InstallationQuery<boolean> = {
       teamId,
       enterpriseId,
-      isEnterpriseInstall: body.is_enterprise_install === 'true',
+      isEnterpriseInstall,
     };
 
-    const result = await this.installerService.installer.authorize(query);
+    let result: AuthorizeResult;
+    try {
+      result = await this.installerService.installer.authorize(query);
 
-    if (result.botToken == null) {
-      res.writeHead(403);
+      if (result.botToken == null) {
+        res.writeHead(403, `The installation for the team(${teamId || enterpriseId}) has no botToken`);
+        return res.end();
+      }
+    }
+    catch (e) {
+      this.logger.error(e.message);
+
+      res.writeHead(500, e.message);
       return res.end();
     }
 
@@ -58,17 +77,30 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
   @Inject()
   installationRepository: InstallationRepository;
 
+  private logger: Logger;
+
+  constructor() {
+    this.logger = loggerFactory('slackbot-proxy:middlewares:AuthorizeInteractionMiddleware');
+  }
+
   async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
     const { body } = req;
 
+    if (body.payload == null) {
+      // do nothing
+      this.logger.info('body does not have payload');
+      return;
+    }
+
     const payload = JSON.parse(body.payload);
 
     // extract id from body
     const teamId = payload.team?.id;
-    const enterpriseId = body.enterprise?.id;
+    const enterpriseId = payload.enterprise?.id;
+    const isEnterpriseInstall = payload.is_enterprise_install === 'true';
 
     if (teamId == null && enterpriseId == null) {
-      res.writeHead(400);
+      res.writeHead(400, 'No installation found');
       return res.end();
     }
 
@@ -76,13 +108,22 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
     const query: InstallationQuery<boolean> = {
       teamId,
       enterpriseId,
-      isEnterpriseInstall: body.is_enterprise_install === 'true',
+      isEnterpriseInstall,
     };
 
-    const result = await this.installerService.installer.authorize(query);
+    let result: AuthorizeResult;
+    try {
+      result = await this.installerService.installer.authorize(query);
+
+      if (result.botToken == null) {
+        res.writeHead(403, `The installation for the team(${teamId || enterpriseId}) has no botToken`);
+        return res.end();
+      }
+    }
+    catch (e) {
+      this.logger.error(e.message);
 
-    if (result.botToken == null) {
-      res.writeHead(403);
+      res.writeHead(500, e.message);
       return res.end();
     }
 

+ 5 - 2
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -5,6 +5,9 @@ import { AuthorizeResult } from '@slack/oauth';
 
 import { GrowiCommandProcessor } from '~/interfaces/growi-command-processor';
 
+
+const isProduction = process.env.NODE_ENV === 'production';
+
 @Service()
 export class RegisterService implements GrowiCommandProcessor {
 
@@ -13,7 +16,7 @@ export class RegisterService implements GrowiCommandProcessor {
     const { botToken } = authorizeResult;
 
     // tmp use process.env
-    const client = new WebClient(botToken, { logLevel: LogLevel.DEBUG });
+    const client = new WebClient(botToken, { logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
     await client.views.open({
       trigger_id: body.trigger_id,
       view: {
@@ -33,7 +36,7 @@ export class RegisterService implements GrowiCommandProcessor {
         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.....'),
+          generateInputSectionBlock('proxyToken', 'PROXY ACCESS_TOKEN', 'contents_input', false, 'jBMZvpk.....'),
         ],
       },
     });

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

@@ -288,13 +288,16 @@
       "discard": "Discard",
       "generate": "Generate"
     },
-    "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) Settings",
-    "without_proxy": {
+    "official_bot_settings": "Official bot Settings",
+    "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
+    "accordion": {
       "create_bot": "Create Bot",
       "how_to_create_a_bot": "How to create a bot",
       "install_bot_to_slack": "Install Bot To Slack",
       "select_install_your_app": "Select \"Install your app\".",
       "select_install_to_workspace": "Select \"Install to Workspace\".",
+      "register_official_bot_proxy_service": "Issue Access Token / Register GROWI Official Bot Proxy Service",
+      "register_proxy_url": "Register Proxy URL with GROWI",
       "click_allow": "Select \"Allow\".",
       "install_complete_if_checked": "Confirm that \"Install your app\" is checked.",
       "invite_bot_to_channel": "Invite GROWI bot to channel by calling @example.",

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

@@ -287,12 +287,14 @@
       "generate": "発行"
     },
     "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 設定",
-    "without_proxy": {
+    "accordion": {
       "create_bot": "Bot を作成する",
       "how_to_create_a_bot": "作成方法はこちら",
-      "install_bot_to_slack": "Bot を Slackにインストールする",
+      "install_bot_to_slack": "Bot を Slack にインストールする",
       "select_install_your_app": "Install your app をクリックします。",
       "select_install_to_workspace": "Install to Workspace をクリックします。",
+      "register_official_bot_proxy_service": "アクセストークンの発行 / GROWI Official Bot Proxy サービスへの登録",
+      "register_proxy_url": "Proxy の URLをGROWIに登録する",
       "click_allow": "遷移先の画面にて、Allowをクリックします。",
       "install_complete_if_checked": "Install your app の右側に緑色のチェックがつけばワークスペースへのインストール完了です。",
       "invite_bot_to_channel": "GROWI bot を使いたいチャンネルに @example を使用して招待します。",

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

@@ -297,12 +297,14 @@
       "generate": "生成"
     },
     "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 设置",
-    "without_proxy": {
+    "accordion": {
       "create_bot": "创建 Bot",
-      "how_to_create_a_bot": "如何创建一个BOT",
-      "install_bot_to_slack": "将Bot安装到Slack",
+      "how_to_create_a_bot": "如何创建一个 Bot",
+      "install_bot_to_slack": "将 Bot 安装到 Slack",
       "select_install_your_app": "选择 \"Install your app\"。",
       "select_install_to_workspace": "选择 \"Install to Workspace\"。",
+      "register_official_bot_proxy_service": "发行访问令牌 / 注册 GROWI 官方 Bot 代理服务",
+      "register_proxy_url": "向 GROWI 注册代理 URL",
       "click_allow": "选择 \"Allow\"。",
       "install_complete_if_checked": "确认已选中 \"Install your app\"。",
       "invite_bot_to_channel": "通过调用 @example 邀请 GROWI Bot 进行频道。",

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

@@ -42,7 +42,7 @@ const BotTypeCard = (props) => {
       <div>
         <h3 className={`card-header mb-0 py-3
               ${props.botType === 'officialBot' ? 'd-flex align-items-center justify-content-center' : 'text-center'}
-              ${props.isActive ? 'bg-primary text-light' : ''}`}
+              ${props.isActive ? 'bg-primary grw-botcard-title-active' : ''}`}
         >
           <span className="mr-2">
             {t(`admin:slack_integration.selecting_bot_types.${botDetails[props.botType].botTypeCategory}`)}
@@ -61,7 +61,7 @@ const BotTypeCard = (props) => {
           )}
 
           {/* 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>
+          <i className={`fa fa-external-link btn-link ${props.isActive ? 'grw-botcard-title-active' : ''}`} aria-hidden="true"></i>
         </h3>
       </div>
       <div className="card-body p-4">

+ 5 - 2
src/client/js/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx

@@ -22,7 +22,10 @@ const ConfirmBotChangeModal = (props) => {
 
   return (
     <Modal isOpen={props.isOpen} centered>
-      <ModalHeader toggle={handleCancelButton}>
+      <ModalHeader
+        toggle={handleCancelButton}
+        className="bg-danger"
+      >
         {t('slack_integration.modal.warning')}
       </ModalHeader>
       <ModalBody>
@@ -37,7 +40,7 @@ const ConfirmBotChangeModal = (props) => {
         <button type="button" className="btn btn-secondary" onClick={handleCancelButton}>
           {t('slack_integration.modal.cancel')}
         </button>
-        <button type="button" className="btn btn-primary" onClick={handleChangeButton}>
+        <button type="button" className="btn btn-danger" onClick={handleChangeButton}>
           {t('slack_integration.modal.change')}
         </button>
       </ModalFooter>

+ 52 - 0
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxyIntegrationCard.jsx

@@ -0,0 +1,52 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+const CustomBotWithProxyIntegrationCard = () => {
+
+  const { t } = useTranslation();
+
+  return (
+    <>
+
+      <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">
+          <small
+            className="text-secondary"
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+          />
+          <div className="pt-2">
+            <div className="position-relative mt-5">
+              <div className="circle position-absolute bg-primary border-light">
+                <p className="circle-inner text-light font-weight-bold">Proxy Server</p>
+              </div>
+              <hr className="align-self-center admin-border-danger border-danger"></hr>
+            </div>
+          </div>
+        </div>
+
+        <div className="card rounded-lg shadow border-0 w-50 admin-bot-card">
+          <div className="row">
+            <h5 className="card-title font-weight-bold mt-3 ml-4 col">GROWI App</h5>
+            <div className="pull-right mt-3 mr-3">
+              <a className="icon-fw fa fa-repeat fa-2x"></a>
+            </div>
+          </div>
+          <div className="card-body p-4 mb-5 text-center">
+            <a className="btn btn-primary">WESEEK Inner Wiki</a>
+          </div>
+        </div>
+
+      </div>
+
+    </>
+  );
+};
+
+export default CustomBotWithProxyIntegrationCard;

+ 5 - 38
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
 import AppContainer from '../../../services/AppContainer';
 import AdminAppContainer from '../../../services/AdminAppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
+import CustomBotWithProxyIntegrationCard from './CustomBotWithProxyIntegrationCard';
+import CustomBotWithProxySettingsAccordion from './CustomBotWithProxySettingsAccordion';
 
 const CustomBotWithProxySettings = (props) => {
   // eslint-disable-next-line no-unused-vars
@@ -16,50 +18,15 @@ const CustomBotWithProxySettings = (props) => {
       {/* TODO: GW-5768 */}
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_with_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 mb-0">
-          <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 mb-5">
-          <div className="mt-4">
-            <small
-              className="text-secondary m-0"
-              // eslint-disable-next-line react/no-danger
-              dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
-            />
-            <div className="row m-0">
-              <hr className="border-danger align-self-center admin-border col"></hr>
-              <div className="circle text-center bg-primary border-light">
-                <p className="text-light font-weight-bold m-0 pt-3">Proxy</p>
-                <p className="text-light font-weight-bold">Server</p>
-              </div>
-              <hr className="border-danger align-self-center admin-border col"></hr>
-            </div>
-          </div>
-        </div>
-
-        <div className="card rounded-lg shadow border-0 w-50 admin-bot-card mb-0">
-          <div className="row m-0">
-            <h5 className="card-title font-weight-bold mt-3 ml-4 col">GROWI App</h5>
-            <div className="pull-right mt-3">
-              <a className="icon-fw fa fa-repeat fa-2x"></a>
-            </div>
-          </div>
-          <div className="card-body p-4 text-center">
-            <a className="btn btn-primary mt-3">WESEEK Inner Wiki</a>
-          </div>
-        </div>
+      <CustomBotWithProxyIntegrationCard />
 
+      <div className="my-5 mx-3">
+        <CustomBotWithProxySettingsAccordion />
       </div>
-
     </>
   );
 };
 
-
 const CustomBotWithProxySettingsWrapper = withUnstatedContainers(CustomBotWithProxySettings, [AppContainer, AdminAppContainer]);
 
 CustomBotWithProxySettings.propTypes = {

+ 32 - 0
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettingsAccordion.jsx

@@ -0,0 +1,32 @@
+import React from 'react';
+import Accordion from '../Common/Accordion';
+
+const CustomBotWithProxySettingsAccordion = () => {
+
+  return (
+    <div className="card border-0 rounded-lg shadow overflow-hidden">
+      <Accordion
+        title={<><span className="mr-2">①</span>First Accordion</>}
+      >
+        1
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">②</span>Second Accordion</>}
+      >
+        2
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">③</span>Third Accordion</>}
+      >
+        3
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">④</span>Fourth Accordion</>}
+      >
+        4
+      </Accordion>
+    </div>
+  );
+};
+
+export default CustomBotWithProxySettingsAccordion;

+ 37 - 38
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxyIntegrationCard.jsx

@@ -7,58 +7,57 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
   const { t } = useTranslation();
 
   return (
-    <>
-
-      <div className="d-flex justify-content-center my-5 bot-integration">
-        <div className="card rounded shadow border-0 w-50 admin-bot-card mb-0">
-          <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
-          <div className="card-body p-2 w-50 mx-auto">
-            {props.slackWSNameInWithoutProxy && (
-              <div className="card slack-work-space-name-card">
-                <div className="m-2 text-center">
-                  <h5 className="font-weight-bold">{ props.slackWSNameInWithoutProxy }</h5>
-                  <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
-                </div>
-              </div>
-            )}
+    <div className="d-flex justify-content-center my-5 bot-integration">
+      <div className="card rounded shadow border-0 w-50 admin-bot-card mb-0">
+        <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
+        <div className="card-body p-2 w-50 mx-auto">
+          {props.slackWSNameInWithoutProxy != null && (
+          <div className="card slack-work-space-name-card">
+            <div className="m-2 text-center">
+              <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
+              <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
+            </div>
           </div>
+            )}
         </div>
+      </div>
 
-        <div className="text-center w-25">
-          {props.isSlackScopeSet && (
-            <div className="mt-5">
-              <p className="text-success"><small className="fa fa-check"> {t('admin:slack_integration.integration_sentence.integration_successful')}</small></p>
-              <hr className="align-self-center admin-border-success border-success"></hr>
-            </div>
+      <div className="text-center w-25">
+        {props.isSetupSlackBot && (
+        <div className="mt-5">
+          <p className="text-success small">
+            <i className="fa fa-check mr-1" />
+            {t('admin:slack_integration.integration_sentence.integration_successful')}
+          </p>
+          <hr className="align-self-center admin-border-success border-success"></hr>
+        </div>
           )}
-          {!props.isSlackScopeSet && (
-            <div className="mt-4">
-              <small
-                className="text-secondary m-0"
+        {!props.isSetupSlackBot && (
+        <div className="mt-4">
+          <small
+            className="text-secondary m-0"
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
-              />
-              <hr className="align-self-center admin-border-danger border-danger"></hr>
-            </div>
-          )}
+            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+          />
+          <hr className="align-self-center admin-border-danger border-danger"></hr>
         </div>
+          )}
+      </div>
 
-        <div className="card rounded-lg shadow border-0 w-50 admin-bot-card mb-0">
-          <h5 className="card-title font-weight-bold mt-3 ml-4">GROWI App</h5>
-          <div className="card-body p-4 mb-5 text-center">
-            <div className="btn btn-primary">{ props.siteName }</div>
-          </div>
+      <div className="card rounded-lg shadow border-0 w-50 admin-bot-card mb-0">
+        <h5 className="card-title font-weight-bold mt-3 ml-4">GROWI App</h5>
+        <div className="card-body p-4 mb-5 text-center">
+          <div className="btn btn-primary">{ props.siteName }</div>
         </div>
       </div>
-
-    </>
+    </div>
   );
 };
 
 CustomBotWithoutProxyIntegrationCard.propTypes = {
   siteName: PropTypes.string.isRequired,
-  slackWSNameInWithoutProxy: PropTypes,
-  isSlackScopeSet: PropTypes.bool.isRequired,
+  slackWSNameInWithoutProxy: PropTypes.string,
+  isSetupSlackBot: PropTypes.bool.isRequired,
 };
 
 export default CustomBotWithoutProxyIntegrationCard;

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

@@ -102,13 +102,13 @@ const CustomBotWithoutProxySettingsAccordion = ({
     <div className="card border-0 rounded-lg shadow overflow-hidden">
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CREATE_BOT)}
-        title={<><span className="mr-2">①</span>{t('admin:slack_integration.without_proxy.create_bot')}</>}
+        title={<><span className="mr-2">①</span>{t('admin:slack_integration.accordion.create_bot')}</>}
       >
         <div className="row my-5">
           <div className="mx-auto">
             <div>
               <button type="button" className="btn btn-primary text-nowrap mx-1" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
-                {t('admin:slack_integration.without_proxy.create_bot')}
+                {t('admin:slack_integration.accordion.create_bot')}
                 <i className="fa fa-external-link ml-2" aria-hidden="true" />
               </button>
             </div>
@@ -116,7 +116,7 @@ const CustomBotWithoutProxySettingsAccordion = ({
             <a href="#">
               <p className="text-center mt-1">
                 <small>
-                  {t('admin:slack_integration.without_proxy.how_to_create_a_bot')}
+                  {t('admin:slack_integration.accordion.how_to_create_a_bot')}
                   <i className="fa fa-external-link ml-2" aria-hidden="true" />
                 </small>
               </p>
@@ -126,18 +126,18 @@ const CustomBotWithoutProxySettingsAccordion = ({
       </Accordion>
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.INSTALL_BOT)}
-        title={<><span className="mr-2">②</span>{t('admin:slack_integration.without_proxy.install_bot_to_slack')}</>}
+        title={<><span className="mr-2">②</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
       >
         <div className="container w-75 py-5">
-          <p>1. {t('admin:slack_integration.without_proxy.select_install_your_app')}</p>
+          <p>1. {t('admin:slack_integration.accordion.select_install_your_app')}</p>
           <img src="/images/slack-integration/slack-bot-install-your-app-introduction.png" className="border border-light img-fluid mb-5" />
-          <p>2. {t('admin:slack_integration.without_proxy.select_install_to_workspace')}</p>
+          <p>2. {t('admin:slack_integration.accordion.select_install_to_workspace')}</p>
           <img src="/images/slack-integration/slack-bot-install-to-workspace.png" className="border border-light img-fluid mb-5" />
-          <p>3. {t('admin:slack_integration.without_proxy.click_allow')}</p>
+          <p>3. {t('admin:slack_integration.accordion.click_allow')}</p>
           <img src="/images/slack-integration/slack-bot-install-your-app-transition-destination.png" className="border border-light img-fluid mb-5" />
-          <p>4. {t('admin:slack_integration.without_proxy.install_complete_if_checked')}</p>
+          <p>4. {t('admin:slack_integration.accordion.install_complete_if_checked')}</p>
           <img src="/images/slack-integration/slack-bot-install-your-app-complete.png" className="border border-light img-fluid mb-5" />
-          <p>5. {t('admin:slack_integration.without_proxy.invite_bot_to_channel')}</p>
+          <p>5. {t('admin:slack_integration.accordion.invite_bot_to_channel')}</p>
           <img src="/images/slack-integration/slack-bot-install-to-workspace-joined-bot.png" className="border border-light img-fluid mb-1" />
           <img src="/images/slack-integration/slack-bot-install-your-app-introduction-to-channel.png" className="border border-light img-fluid" />
         </div>
@@ -145,7 +145,7 @@ const CustomBotWithoutProxySettingsAccordion = ({
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.REGISTER_SLACK_CONFIGURATION)}
         // eslint-disable-next-line max-len
-        title={<><span className="mr-2">③</span>{t('admin:slack_integration.without_proxy.register_secret_and_token')}{isRegisterSlackCredentials && <i className="ml-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="mr-2">③</span>{t('admin:slack_integration.accordion.register_secret_and_token')}{isRegisterSlackCredentials && <i className="ml-3 text-success fa fa-check"></i>}</>}
       >
         <CustomBotWithoutProxySecretTokenSection
           updateSecretTokenHandler={updateSecretTokenHandler}
@@ -160,9 +160,9 @@ const CustomBotWithoutProxySettingsAccordion = ({
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         // eslint-disable-next-line max-len
-        title={<><span className="mr-2">④</span>{t('admin:slack_integration.without_proxy.test_connection')}{isSendTestMessage && <i className="ml-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}{isSendTestMessage && <i className="ml-3 text-success fa fa-check"></i>}</>}
       >
-        <p className="text-center m-4">{t('admin:slack_integration.without_proxy.test_connection_by_pressing_button')}</p>
+        <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <div className="d-flex justify-content-center">
           <form className="form-row align-items-center w-25" onSubmit={e => submitForm(e)}>
             <div className="col-8 input-group-prepend">
@@ -186,9 +186,9 @@ const CustomBotWithoutProxySettingsAccordion = ({
           </form>
         </div>
         {connectionErrorMessage != null
-        && <p className="text-danger text-center my-4">{t('admin:slack_integration.without_proxy.error_check_logs_below')}</p>}
+        && <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>}
         {connectionSuccessMessage != null
-         && <p className="text-info text-center my-4">{t('admin:slack_integration.without_proxy.send_message_to_slack_work_space')}</p>}
+        && <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>}
         <form>
           <div className="row my-3 justify-content-center">
             <div className="form-group slack-connection-log w-25">
@@ -196,6 +196,7 @@ const CustomBotWithoutProxySettingsAccordion = ({
               <textarea
                 className="form-control card border-info slack-connection-log-body rounded-lg"
                 value={value}
+                readOnly
               />
             </div>
           </div>

+ 11 - 3
src/client/js/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -1,11 +1,19 @@
 import React from 'react';
+import { useTranslation } from 'react-i18next';
+import OfficialBotSettingsAccordion from './OfficialbotSettingsAccordion';
 
 const OfficialBotSettings = () => {
+  const { t } = useTranslation();
 
   return (
-    <div className="row my-5">
-      <h1>Official Bot Settings Component</h1>
-    </div>
+    <>
+      <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_settings')}</h2>
+
+      <div className="my-5 mx-3">
+        <OfficialBotSettingsAccordion />
+      </div>
+    </>
+
   );
 };
 

+ 38 - 0
src/client/js/components/Admin/SlackIntegration/OfficialbotSettingsAccordion.jsx

@@ -0,0 +1,38 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import Accordion from '../Common/Accordion';
+
+const OfficialBotSettingsAccordion = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="card border-0 rounded-lg shadow overflow-hidden">
+      <Accordion
+        title={<><span className="mr-2">①</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
+      >
+        {/* TODO: GW-5824 add accordion contents  */}
+        hoge
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">②</span>{t('admin:slack_integration.accordion.register_official_bot_proxy_service')}</>}
+      >
+        {/* TODO: GW-5824 add accordion contents  */}
+        hoge
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">③</span>{t('admin:slack_integration.accordion.register_proxy_url')}</>}
+      >
+        {/* TODO: GW-5824 add accordion contents  */}
+        hoge
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}</>}
+      >
+        {/* TODO: GW-5824 add accordion contents  */}
+        hoge
+      </Accordion>
+    </div>
+  );
+};
+
+export default OfficialBotSettingsAccordion;

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

@@ -178,9 +178,8 @@ const SlackIntegration = (props) => {
         <div className="row my-5 flex-wrap-reverse justify-content-center">
           {botTypes.map((botType) => {
             return (
-              <div className="m-3">
+              <div key={botType} className="m-3">
                 <BotTypeCard
-                  key={botType}
                   botType={botType}
                   isActive={currentBotType === botType}
                   handleBotTypeSelect={handleBotTypeSelect}

+ 12 - 2
src/client/js/components/PageEditor.jsx

@@ -101,13 +101,23 @@ class PageEditor extends React.Component {
    * @param {string} value
    */
   onMarkdownChanged(value) {
-    const { pageContainer, editorContainer } = this.props;
+    const { pageContainer } = this.props;
     this.setMarkdownStateWithDebounce(value);
     // only when the first time to edit
     if (!pageContainer.state.revisionId) {
       this.saveDraftWithDebounce();
     }
-    editorContainer.enableUnsavedWarning();
+  }
+
+  // Displays an alert if there is a difference with pageContainer's markdown
+  componentDidUpdate(prevProps, prevState) {
+    const { pageContainer, editorContainer } = this.props;
+
+    if (this.state.markdown !== prevState.markdown) {
+      if (pageContainer.state.markdown !== this.state.markdown) {
+        editorContainer.enableUnsavedWarning();
+      }
+    }
   }
 
   /**

+ 0 - 1
src/client/js/components/SavePageControls/GrantSelector.jsx

@@ -203,7 +203,6 @@ class GrantSelector extends React.Component {
     return (
       <Modal
         className="select-grant-group"
-        container={this}
         isOpen={this.state.isSelectGroupModalShown}
         toggle={this.hideSelectGroupModal}
       >

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

@@ -131,11 +131,22 @@ $slack-work-space-name-card-border: #efc1f6;
       border-width: 3px;
     }
     .circle {
+      top: 50%;
+      left: 50%;
       width: 100px;
       height: 100px;
-      // background: #092c58;
       border: 13px solid;
       border-radius: 50%;
+      -webkit-transform: translate(-50%, -50%);
+      -ms-transform: translate(-50%, -50%);
+      transform: translate(-50%, -50%);
+    }
+    .circle-inner {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      -webkit-transform: translate(-50%, -50%);
+      transform: translate(-50%, -50%);
     }
     .slack-work-space-name-card {
       background-color: $slack-work-space-name-card-background;

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

@@ -364,6 +364,12 @@ ul.pagination {
   box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
 }
 
+.admin-bot-card {
+  .grw-botcard-title-active {
+    color: $gray-200;
+  }
+}
+
 /*
  * Form Slider
  */

+ 7 - 0
src/client/styles/scss/theme/island.scss

@@ -110,4 +110,11 @@ html[dark] {
       @include btn-page-editor-mode-manager(darken($primary, 50%), lighten($primary, 5%), darken($primary, 5%));
     }
   }
+
+  // Cards
+  .admin-bot-card {
+    .grw-botcard-title-active {
+      color: $color-reversal;
+    }
+  }
 }

+ 9 - 0
src/client/styles/scss/theme/spring.scss

@@ -144,10 +144,19 @@ html[dark] {
     background-color: $bgcolor-global;
   }
 
+  /*
+    Cards
+  */
   .card-timeline > .card-header {
     background-color: $third-main-color;
   }
 
+  .admin-bot-card {
+    .grw-botcard-title-active {
+      color: $color-reversal;
+    }
+  }
+
   h1,
   h2 {
     color: $subthemecolor;

+ 4 - 0
src/server/routes/apiv3/slack-bot.js

@@ -12,6 +12,10 @@ module.exports = (crowi) => {
 
   // Check if the access token is correct
   function verificationAccessToken(req, res, next) {
+    const botType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
+    if (botType === 'customBotWithoutProxy') {
+      return next();
+    }
     const slackBotAccessToken = req.body.slack_bot_access_token || null;
 
     if (slackBotAccessToken == null || slackBotAccessToken !== this.crowi.configManager.getConfig('crowi', 'slackbot:access-token')) {

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

@@ -232,7 +232,7 @@ module.exports = (crowi) => {
     }
     catch (error) {
       let msg = 'Error occured in slack_bot_token';
-      if (error.data.ok === false && error.data.error === 'missing_scope') {
+      if (error.data.error === 'missing_scope') {
         msg = 'missing_scope';
       }
       logger.error('Error', error);