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

Merge branch 'master' into copyBugReportBtn

Yuki Takei 4 лет назад
Родитель
Сommit
f4c774126b

+ 2 - 0
.devcontainer/Dockerfile

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

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

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

+ 10 - 0
packages/app/resource/locales/en_US/admin/admin.json

@@ -258,6 +258,12 @@
     "download": "Download",
     "download": "Download",
     "delete": "Delete"
     "delete": "Delete"
   },
   },
+  "external_notification": {
+    "enabled": "Enabled",
+    "disabled": "Disabled",
+    "header_status": "Slack Integration Status",
+    "caution_enabled": "CAUTION: Currently, notifications that are configured in this page will send only to the Slack Workspace set as primary."
+  },
   "slack_integration": {
   "slack_integration": {
     "selecting_bot_types": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
       "slack_bot": "Slack bot",
@@ -355,6 +361,10 @@
       "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
       "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
     }
     }
   },
   },
+  "slack_integration_legacy": {
+    "alert_disabled": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
+    "alert_deplicated": "This 'Legacy Slack Integration' is outdated and will be discontinued in the future. Use <a href='/admin/slack-integration'>the new settings</a> instead. "
+  },
   "user_management": {
   "user_management": {
     "invite_users": "Temporarily issue a new user",
     "invite_users": "Temporarily issue a new user",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
     "click_twice_same_checkbox": "You should check at least one checkbox.",

+ 10 - 0
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -252,6 +252,12 @@
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
   },
+  "external_notification": {
+    "enabled": "有効",
+    "disabled": "無効",
+    "header_status": "Slack 連携の状態",
+    "caution_enabled": "CAUTION: このページで設定される通知は、Primary として設定された Slack ワークスペースにのみ送信されます。 "
+  },
   "slack_integration": {
   "slack_integration": {
     "selecting_bot_types": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
       "slack_bot": "Slack bot",
@@ -348,6 +354,10 @@
       "custom_bot_with_proxy_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
       "custom_bot_with_proxy_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
     }
     }
   },
   },
+  "slack_integration_legacy": {
+    "alert_disabled": "<a href='/admin/slack-integration'>新しい設定</a>が有効になっているため、この 'Slack連携 (レガシー)' は現在無効になっています。",
+    "alert_deplicated": "この 'Slack連携 (レガシー)' は将来廃止されます。代わりに<a href='/admin/slack-integration'>新しいSlack連携機能</a>を利用してください。"
+  },
   "user_management": {
   "user_management": {
     "invite_users": "新規ユーザーの仮発行",
     "invite_users": "新規ユーザーの仮発行",
     "click_twice_same_checkbox": "少なくとも一つはチェックしてください。",
     "click_twice_same_checkbox": "少なくとも一つはチェックしてください。",

+ 10 - 0
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -262,6 +262,12 @@
     "download": "下载",
     "download": "下载",
     "delete": "删除"
     "delete": "删除"
   },
   },
+  "external_notification": {
+    "enabled": "Enabled",
+    "disabled": "Disabled",
+    "header_status": "Slack整合状态",
+    "caution_enabled": "CAUTION: 目前,在此页面中配置的通知只会通知设置为主要的 Slack 工作区。 "
+  },
   "slack_integration": {
   "slack_integration": {
     "selecting_bot_types": {
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
       "slack_bot": "Slack bot",
@@ -358,6 +364,10 @@
       "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
       "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
     }
     }
   },
   },
+  "slack_integration_legacy": {
+    "alert_disabled": "由于<a href='/admin/slack-integration'>新设置</a>已启用,因此该'旧版Slack一体化'目前已被禁用。",
+    "alert_deplicated": "这个 '旧版Slack一体化' 已经过时了,将来会停止使用。使用<a href='/admin/slack-integration'>新的设置</a>来代替。"
+  },
   "user_management": {
   "user_management": {
     "invite_users": "临时发布新用户",
     "invite_users": "临时发布新用户",
     "click_twice_same_checkbox": "您应该至少选中一个复选框。",
     "click_twice_same_checkbox": "您应该至少选中一个复选框。",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 0
packages/app/src/server/routes/apiv3/index.js

@@ -48,6 +48,7 @@ module.exports = (crowi) => {
 
 
   router.use('/slack-integration', require('./slack-integration')(crowi));
   router.use('/slack-integration', require('./slack-integration')(crowi));
   router.use('/slack-integration-settings', require('./slack-integration-settings')(crowi));
   router.use('/slack-integration-settings', require('./slack-integration-settings')(crowi));
+  router.use('/slack-integration-legacy-settings', require('./slack-integration-legacy-settings')(crowi));
   router.use('/staffs', require('./staffs')(crowi));
   router.use('/staffs', require('./staffs')(crowi));
 
 
   return router;
   return router;

+ 5 - 66
packages/app/src/server/routes/apiv3/notification-setting.js

@@ -15,11 +15,6 @@ const { body } = require('express-validator');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 const validator = {
 const validator = {
-  slackConfiguration: [
-    body('webhookUrl').if(value => value != null).isString().trim(),
-    body('isIncomingWebhookPrioritized').isBoolean(),
-    body('slackToken').if(value => value != null).isString().trim(),
-  ],
   userNotification: [
   userNotification: [
     body('pathPattern').isString().trim(),
     body('pathPattern').isString().trim(),
     body('channel').isString().trim(),
     body('channel').isString().trim(),
@@ -52,18 +47,6 @@ const validator = {
  *
  *
  *  components:
  *  components:
  *    schemas:
  *    schemas:
- *      SlackConfigurationParams:
- *        type: object
- *        properties:
- *          webhookUrl:
- *            type: string
- *            description: incoming webhooks url
- *          isIncomingWebhookPrioritized:
- *            type: boolean
- *            description: use incoming webhooks even if Slack App settings are enabled
- *          slackToken:
- *            type: string
- *            description: OAuth access token
  *      UserNotificationParams:
  *      UserNotificationParams:
  *        type: object
  *        type: object
  *        properties:
  *        properties:
@@ -135,9 +118,11 @@ module.exports = (crowi) => {
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
 
 
     const notificationParams = {
     const notificationParams = {
-      webhookUrl: await crowi.configManager.getConfig('notification', 'slack:incomingWebhookUrl'),
-      isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
-      slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
+      // status of slack intagration
+      isSlackbotConfigured: crowi.slackIntegrationService.isSlackbotConfigured,
+      isSlackLegacyConfigured: crowi.slackIntegrationService.isSlackLegacyConfigured,
+      currentBotType: await crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
+
       userNotifications: await UpdatePost.findAll(),
       userNotifications: await UpdatePost.findAll(),
       isNotificationForOwnerPageEnabled: await crowi.configManager.getConfig('notification', 'notification:owner-page:isEnabled'),
       isNotificationForOwnerPageEnabled: await crowi.configManager.getConfig('notification', 'notification:owner-page:isEnabled'),
       isNotificationForGroupPageEnabled: await crowi.configManager.getConfig('notification', 'notification:group-page:isEnabled'),
       isNotificationForGroupPageEnabled: await crowi.configManager.getConfig('notification', 'notification:group-page:isEnabled'),
@@ -146,52 +131,6 @@ module.exports = (crowi) => {
     return res.apiv3({ notificationParams });
     return res.apiv3({ notificationParams });
   });
   });
 
 
-  /**
-   * @swagger
-   *
-   *    /notification-setting/slack-configuration:
-   *      put:
-   *        tags: [NotificationSetting]
-   *        description: Update slack configuration setting
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/SlackConfigurationParams'
-   *        responses:
-   *          200:
-   *            description: Succeeded to update slack configuration setting
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  $ref: '#/components/schemas/SlackConfigurationParams'
-   */
-  router.put('/slack-configuration', loginRequiredStrictly, adminRequired, csrf, validator.slackConfiguration, apiV3FormValidator, async(req, res) => {
-
-    const requestParams = {
-      'slack:incomingWebhookUrl': req.body.webhookUrl,
-      'slack:isIncomingWebhookPrioritized': req.body.isIncomingWebhookPrioritized,
-      'slack:token': req.body.slackToken,
-    };
-
-    try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('notification', requestParams);
-      const responseParams = {
-        webhookUrl: await crowi.configManager.getConfig('notification', 'slack:incomingWebhookUrl'),
-        isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
-        slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
-      };
-      return res.apiv3({ responseParams });
-    }
-    catch (err) {
-      const msg = 'Error occurred in updating slack configuration';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-slackConfiguration-failed'));
-    }
-
-  });
-
   /**
   /**
   * @swagger
   * @swagger
   *
   *

+ 128 - 0
packages/app/src/server/routes/apiv3/slack-integration-legacy-settings.js

@@ -0,0 +1,128 @@
+import loggerFactory from '~/utils/logger';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:routes:apiv3:slack-integration-legacy-setting');
+
+const express = require('express');
+
+const router = express.Router();
+
+const { body } = require('express-validator');
+
+const ErrorV3 = require('../../models/vo/error-apiv3');
+
+const validator = {
+  slackConfiguration: [
+    body('webhookUrl').if(value => value != null).isString().trim(),
+    body('isIncomingWebhookPrioritized').isBoolean(),
+    body('slackToken').if(value => value != null).isString().trim(),
+  ],
+};
+
+/**
+ * @swagger
+ *  tags:
+ *    name: SlackIntegrationLegacySetting
+ */
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      SlackConfigurationParams:
+ *        type: object
+ *        properties:
+ *          webhookUrl:
+ *            type: string
+ *            description: incoming webhooks url
+ *          isIncomingWebhookPrioritized:
+ *            type: boolean
+ *            description: use incoming webhooks even if Slack App settings are enabled
+ *          slackToken:
+ *            type: string
+ *            description: OAuth access token
+ */
+module.exports = (crowi) => {
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const adminRequired = require('../../middlewares/admin-required')(crowi);
+  const csrf = require('../../middlewares/csrf')(crowi);
+  const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration-legacy-setting/:
+   *      get:
+   *        tags: [SlackIntegrationLegacySetting]
+   *        description: Get slack configuration setting
+   *        responses:
+   *          200:
+   *            description: params of slack configuration setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    notificationParams:
+   *                      type: object
+   *                      description: slack configuration setting params
+   */
+  router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
+
+    const slackIntegrationParams = {
+      isSlackbotConfigured: crowi.slackIntegrationService.isSlackbotConfigured,
+      webhookUrl: await crowi.configManager.getConfig('notification', 'slack:incomingWebhookUrl'),
+      isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
+      slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
+    };
+    return res.apiv3({ slackIntegrationParams });
+  });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration-legacy-setting/:
+   *      put:
+   *        tags: [SlackIntegrationLegacySetting]
+   *        description: Update slack configuration setting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/SlackConfigurationParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update slack configuration setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/SlackConfigurationParams'
+   */
+  router.put('/', loginRequiredStrictly, adminRequired, csrf, validator.slackConfiguration, apiV3FormValidator, async(req, res) => {
+
+    const requestParams = {
+      'slack:incomingWebhookUrl': req.body.webhookUrl,
+      'slack:isIncomingWebhookPrioritized': req.body.isIncomingWebhookPrioritized,
+      'slack:token': req.body.slackToken,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('notification', requestParams);
+      const responseParams = {
+        webhookUrl: await crowi.configManager.getConfig('notification', 'slack:incomingWebhookUrl'),
+        isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
+        slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
+      };
+      return res.apiv3({ responseParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating slack configuration';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-slackConfiguration-failed'));
+    }
+
+  });
+
+  return router;
+};

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

@@ -111,7 +111,11 @@ module.exports = (crowi) => {
 
 
     // Send response immediately to avoid opelation_timeout error
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
-    res.send();
+    res.json({
+      response_type: 'ephemeral',
+      text: 'Processing your request ...',
+    });
+
 
 
     const args = body.text.split(' ');
     const args = body.text.split(' ');
     const command = args[0];
     const command = args[0];

+ 0 - 4
packages/app/src/server/service/page.js

@@ -105,10 +105,6 @@ class PageService {
       await Page.create(path, body, user, { redirectTo: newPagePath });
       await Page.create(path, body, user, { redirectTo: newPagePath });
     }
     }
 
 
-    if (isRecursively) {
-      await this.renameDescendantsWithStream(page, newPagePath, user, options);
-    }
-
     this.pageEvent.emit('delete', page, user, socketClientId);
     this.pageEvent.emit('delete', page, user, socketClientId);
     this.pageEvent.emit('create', renamedPage, user, socketClientId);
     this.pageEvent.emit('create', renamedPage, user, socketClientId);
 
 

+ 1 - 1
packages/app/src/server/views/admin/slack-integration-legacy.html

@@ -7,6 +7,6 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}
-<div id="admin-slack-integration-notification-setting" class="admin-slack-integration-notification-setting"></div>
+<div id="admin-slack-integration-legacy" class="admin-slack-integration-legacy"></div>
 {% endblock content_main %}
 {% endblock content_main %}
 
 

+ 12 - 13
packages/slackbot-proxy/src/controllers/slack.ts

@@ -126,10 +126,6 @@ export class SlackCtrl {
 
 
     // register
     // register
     if (growiCommand.growiCommandType === 'register') {
     if (growiCommand.growiCommandType === 'register') {
-      // 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();
-
       return this.registerService.process(growiCommand, authorizeResult, body as {[key:string]:string});
       return this.registerService.process(growiCommand, authorizeResult, body as {[key:string]:string});
     }
     }
 
 
@@ -142,10 +138,6 @@ export class SlackCtrl {
         return 'GROWI Urls must be urls.';
         return 'GROWI Urls must be urls.';
       }
       }
 
 
-      // 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();
-
       return this.unregisterService.process(growiCommand, authorizeResult, body as {[key:string]:string});
       return this.unregisterService.process(growiCommand, authorizeResult, body as {[key:string]:string});
     }
     }
 
 
@@ -178,7 +170,10 @@ export class SlackCtrl {
 
 
     // Send response immediately to avoid opelation_timeout error
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
-    res.send();
+    res.json({
+      response_type: 'ephemeral',
+      text: 'Processing your request ...',
+    });
 
 
     const baseDate = new Date();
     const baseDate = new Date();
 
 
@@ -259,10 +254,6 @@ export class SlackCtrl {
 
 
     const { body, authorizeResult } = req;
     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
     // pass
     if (body.ssl_check != null) {
     if (body.ssl_check != null) {
       return;
       return;
@@ -300,10 +291,18 @@ export class SlackCtrl {
 
 
     // forward to GROWI server
     // forward to GROWI server
     if (callBackId === 'select_growi') {
     if (callBackId === 'select_growi') {
+      // 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();
+
       const selectedGrowiInformation = await this.selectGrowiService.handleSelectInteraction(installation, payload);
       const selectedGrowiInformation = await this.selectGrowiService.handleSelectInteraction(installation, payload);
       return this.sendCommand(selectedGrowiInformation.growiCommand, [selectedGrowiInformation.relation], selectedGrowiInformation.sendCommandBody);
       return this.sendCommand(selectedGrowiInformation.growiCommand, [selectedGrowiInformation.relation], selectedGrowiInformation.sendCommandBody);
     }
     }
 
 
+    // 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();
+
     /*
     /*
     * forward to GROWI server
     * forward to GROWI server
     */
     */