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

Merge branch 'master' into feat/107958/enable-email-sending-for-email-authentication

Shun Miyazawa 3 лет назад
Родитель
Сommit
adf445d021
59 измененных файлов с 514 добавлено и 913 удалено
  1. 0 173
      packages/app/_obsolete/src/client/admin.jsx
  2. 11 4
      packages/app/public/static/locales/en_US/admin.json
  3. 11 1
      packages/app/public/static/locales/en_US/commons.json
  4. 3 16
      packages/app/public/static/locales/en_US/translation.json
  5. 13 5
      packages/app/public/static/locales/ja_JP/admin.json
  6. 11 1
      packages/app/public/static/locales/ja_JP/commons.json
  7. 3 16
      packages/app/public/static/locales/ja_JP/translation.json
  8. 61 4
      packages/app/public/static/locales/zh_CN/admin.json
  9. 11 1
      packages/app/public/static/locales/zh_CN/commons.json
  10. 3 62
      packages/app/public/static/locales/zh_CN/translation.json
  11. 1 1
      packages/app/src/components/Admin/App/AppSetting.jsx
  12. 60 19
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  13. 3 3
      packages/app/src/components/Admin/Security/ShareLinkSetting.tsx
  14. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  15. 2 2
      packages/app/src/components/Admin/Users/GiveAdminButton.tsx
  16. 9 9
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  17. 4 4
      packages/app/src/components/Admin/Users/RemoveAdminButton.tsx
  18. 2 2
      packages/app/src/components/Admin/Users/RemoveAdminMenuItem.tsx
  19. 2 2
      packages/app/src/components/Admin/Users/StatusActivateButton.jsx
  20. 2 2
      packages/app/src/components/Admin/Users/StatusSuspendMenuItem.tsx
  21. 1 1
      packages/app/src/components/Admin/Users/UserRemoveButton.jsx
  22. 2 2
      packages/app/src/components/Page.tsx
  23. 4 4
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  24. 9 1
      packages/app/src/components/PageEditor/HandsontableModal.jsx
  25. 24 0
      packages/app/src/components/PageEditor/HandsontableModal.module.scss
  26. 4 3
      packages/app/src/components/SavePageControls/GrantSelector.tsx
  27. 2 2
      packages/app/src/components/ShareLink/ShareLink.tsx
  28. 5 5
      packages/app/src/components/ShareLink/ShareLinkList.tsx
  29. 73 0
      packages/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx
  30. 45 0
      packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx
  31. 0 212
      packages/app/src/server/routes/admin.js
  32. 1 0
      packages/app/src/server/routes/apiv3/index.js
  33. 30 12
      packages/app/src/server/routes/apiv3/notification-setting.js
  34. 33 20
      packages/app/src/server/routes/apiv3/user-activation.ts
  35. 2 41
      packages/app/src/server/routes/index.js
  36. 9 1
      packages/app/src/server/routes/login.js
  37. 0 15
      packages/app/src/server/views/admin/app.html
  38. 0 11
      packages/app/src/server/views/admin/audit-log.html
  39. 0 14
      packages/app/src/server/views/admin/customize.html
  40. 0 11
      packages/app/src/server/views/admin/export.html
  41. 0 11
      packages/app/src/server/views/admin/external-accounts.html
  42. 0 12
      packages/app/src/server/views/admin/global-notification-detail.html
  43. 0 11
      packages/app/src/server/views/admin/importer.html
  44. 0 11
      packages/app/src/server/views/admin/index.html
  45. 0 11
      packages/app/src/server/views/admin/markdown.html
  46. 0 8
      packages/app/src/server/views/admin/not_found.html
  47. 0 11
      packages/app/src/server/views/admin/notification.html
  48. 0 11
      packages/app/src/server/views/admin/search.html
  49. 0 11
      packages/app/src/server/views/admin/security.html
  50. 0 12
      packages/app/src/server/views/admin/slack-integration-legacy.html
  51. 0 11
      packages/app/src/server/views/admin/slack-integration.html
  52. 0 15
      packages/app/src/server/views/admin/user-group-detail.html
  53. 0 11
      packages/app/src/server/views/admin/user-groups.html
  54. 0 11
      packages/app/src/server/views/admin/users.html
  55. 0 37
      packages/app/src/server/views/layout/admin.html
  56. 37 0
      packages/app/src/stores/global-notification.ts
  57. 0 35
      packages/app/src/styles/_handsontable.scss
  58. 1 1
      packages/app/src/styles/style-app.scss
  59. 19 0
      packages/app/src/styles/style-next.scss

+ 0 - 173
packages/app/_obsolete/src/client/admin.jsx

@@ -1,173 +0,0 @@
-import React from 'react';
-
-import ReactDOM from 'react-dom';
-import { I18nextProvider } from 'react-i18next';
-import { SWRConfig } from 'swr';
-import { Provider } from 'unstated';
-
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
-import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
-import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-import AdminImportContainer from '~/client/services/AdminImportContainer';
-import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
-import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
-import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
-import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
-import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
-// import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
-import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import ContextExtractor from '~/client/services/ContextExtractor';
-import loggerFactory from '~/utils/logger';
-import { swrGlobalConfiguration } from '~/utils/swr-utils';
-
-import AdminHome from '../components/Admin/AdminHome/AdminHome';
-// import AppSettingsPage from '../components/Admin/App/AppSettingsPage';
-import { AuditLogManagement } from '../components/Admin/AuditLogManagement';
-import AdminNavigation from '../components/Admin/Common/AdminNavigation';
-import Customize from '../components/Admin/Customize/Customize';
-import ExportArchiveDataPage from '../components/Admin/ExportArchiveDataPage';
-import FullTextSearchManagement from '../components/Admin/FullTextSearchManagement';
-// import ImportDataPage from '../components/Admin/ImportDataPage';
-import LegacySlackIntegration from '../components/Admin/LegacySlackIntegration/LegacySlackIntegration';
-import ManageExternalAccount from '../components/Admin/ManageExternalAccount';
-// import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
-import ManageGlobalNotification from '../components/Admin/Notification/ManageGlobalNotification';
-import NotificationSetting from '../components/Admin/Notification/NotificationSetting';
-import SecurityManagement from '../components/Admin/Security/SecurityManagement';
-import SlackIntegration from '../components/Admin/SlackIntegration/SlackIntegration';
-import UserGroupPage from '../components/Admin/UserGroup/UserGroupPage';
-import UserGroupDetailPage from '../components/Admin/UserGroupDetail/UserGroupDetailPage';
-import UserManagement from '../components/Admin/UserManagement';
-import ErrorBoundary from '../components/ErrorBoudary';
-
-import { appContainer, componentMappings } from './base';
-
-const logger = loggerFactory('growi:admin');
-
-appContainer.initContents();
-
-const { i18n } = appContainer;
-// create unstated container instance
-const adminAppContainer = new AdminAppContainer(appContainer);
-const adminImportContainer = new AdminImportContainer(appContainer);
-const adminSocketIoContainer = new AdminSocketIoContainer(appContainer);
-const adminHomeContainer = new AdminHomeContainer(appContainer);
-const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
-const adminUsersContainer = new AdminUsersContainer(appContainer);
-const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
-const adminNotificationContainer = new AdminNotificationContainer(appContainer);
-const adminSlackIntegrationLegacyContainer = new AdminSlackIntegrationLegacyContainer(appContainer);
-const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
-// const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appContainer);
-const socketIoContainer = appContainer.getContainer('SocketIoContainer');
-const injectableContainers = [
-  appContainer,
-  adminAppContainer,
-  adminImportContainer,
-  adminSocketIoContainer,
-  adminHomeContainer,
-  adminCustomizeContainer,
-  adminUsersContainer,
-  adminExternalAccountsContainer,
-  adminNotificationContainer,
-  adminSlackIntegrationLegacyContainer,
-  adminMarkDownContainer,
-  // adminUserGroupDetailContainer,
-  socketIoContainer,
-];
-
-logger.info('unstated containers have been initialized');
-
-/**
- * define components
- *  key: id of element
- *  value: React Element
- */
-Object.assign(componentMappings, {
-  'admin-home': <AdminHome />,
-  // 'admin-app': <AppSettingsPage />,
-  // 'admin-markdown-setting': <MarkdownSetting />,
-  'admin-customize': <Customize />,
-  // 'admin-importer': <ImportDataPage />,
-  'admin-export-page': <ExportArchiveDataPage />,
-  'admin-notification-setting': <NotificationSetting />,
-  'admin-slack-integration': <SlackIntegration />,
-  'admin-slack-integration-legacy': <LegacySlackIntegration />,
-  'admin-global-notification-setting': <ManageGlobalNotification />,
-  'admin-user-page': <UserManagement />,
-  'admin-external-account-setting': <ManageExternalAccount />,
-  'admin-user-group-detail': <UserGroupDetailPage />,
-  'admin-full-text-search-management': <FullTextSearchManagement />,
-  'admin-user-group-page': <UserGroupPage />,
-  'admin-audit-log': <AuditLogManagement />,
-  'admin-navigation': <AdminNavigation />,
-});
-
-const renderMainComponents = () => {
-  Object.keys(componentMappings).forEach((key) => {
-    const elem = document.getElementById(key);
-    if (elem) {
-      ReactDOM.render(
-        <I18nextProvider i18n={i18n}>
-          <ErrorBoundary>
-            <Provider inject={injectableContainers}>
-              {componentMappings[key]}
-            </Provider>
-          </ErrorBoundary>
-        </I18nextProvider>,
-        elem,
-      );
-    }
-  });
-};
-
-// extract context before rendering main components
-const elem = document.getElementById('growi-context-extractor');
-if (elem != null) {
-  ReactDOM.render(
-    <SWRConfig value={swrGlobalConfiguration}>
-      <ContextExtractor></ContextExtractor>
-    </SWRConfig>,
-    elem,
-    renderMainComponents,
-  );
-}
-else {
-  renderMainComponents();
-}
-
-const adminSecuritySettingElem = document.getElementById('admin-security-setting');
-if (adminSecuritySettingElem != null) {
-  const adminGeneralSecurityContainer = new AdminGeneralSecurityContainer(appContainer);
-  const adminLocalSecurityContainer = new AdminLocalSecurityContainer(appContainer);
-  const adminLdapSecurityContainer = new AdminLdapSecurityContainer(appContainer);
-  const adminSamlSecurityContainer = new AdminSamlSecurityContainer(appContainer);
-  const adminOidcSecurityContainer = new AdminOidcSecurityContainer(appContainer);
-  const adminBasicSecurityContainer = new AdminBasicSecurityContainer(appContainer);
-  const adminGoogleSecurityContainer = new AdminGoogleSecurityContainer(appContainer);
-  const adminGitHubSecurityContainer = new AdminGitHubSecurityContainer(appContainer);
-  const adminTwitterSecurityContainer = new AdminTwitterSecurityContainer(appContainer);
-  const adminSecurityContainers = [
-    adminGeneralSecurityContainer, adminLocalSecurityContainer, adminLdapSecurityContainer, adminSamlSecurityContainer,
-    adminOidcSecurityContainer, adminBasicSecurityContainer, adminGoogleSecurityContainer, adminGitHubSecurityContainer, adminTwitterSecurityContainer,
-  ];
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <ErrorBoundary>
-        <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
-          <SecurityManagement />
-        </Provider>
-      </ErrorBoundary>
-    </I18nextProvider>,
-    adminSecuritySettingElem,
-  );
-}

+ 11 - 4
packages/app/public/static/locales/en_US/admin.json

@@ -80,12 +80,9 @@
       "closed": "Closed (Invitation Only)"
     },
     "share_link_management": "Share Link Management",
+    "No_share_links":"No share links",
     "share_link_notice":"remove all share links",
     "delete_all_share_links":"Delete all share links",
-    "Share Link": "Share Link",
-    "Page Path": "Page Path",
-    "expire": "Expiration",
-    "description": "Description",
     "share_link_rights": "Share link rights",
     "enable_link_sharing": "Enable link sharing",
     "all_share_links": "All share links",
@@ -1035,5 +1032,15 @@
     "ADMIN_SEARCH_CONNECTION": "Attempting to reconnect to Elasticsearch",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Normalize of Elasticsearch indexes",
     "ADMIN_SEARCH_INDICES_REBUILD": "Rebuild Elasticsearch indexes"
+  },
+  "toaster": {
+    "give_user_admin": "Succeeded to give {{username}} admin",
+    "remove_user_admin": "Succeeded to remove {{username}} admin",
+    "activate_user_success": "Succeeded to activating {{username}}",
+    "deactivate_user_success": "Succeeded to deactivate {{username}}",
+    "remove_user_success": "Succeeded to removing {{username}}",
+    "remove_external_user_success": "Succeeded to remove {{accountId}}",
+    "switch_disable_link_sharing_success": "Succeeded to update share link setting",
+    "failed_to_reset_password":"Failed to reset password"
   }
 }

+ 11 - 1
packages/app/public/static/locales/en_US/commons.json

@@ -6,12 +6,22 @@
     "create_succeeded": "Succeeded to create {{target}}",
     "create_failed": "Failed to create {{target}}",
     "update_successed": "Succeeded to update {{target}}",
-    "update_failed": "Failed to update {{target}}"
+    "update_failed": "Failed to update {{target}}",
+
+    "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
+    "remove_share_link": "Succeeded to remove {{count}} share links"
   },
   "alert": {
     "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}"
   },
   "headers": {
     "app_settings": "App Settings"
+  },
+
+  "share_links": {
+    "Share Link": "Share Link",
+    "Page Path": "Page Path",
+    "expire": "Expiration",
+    "description": "Description"
   }
 }

+ 3 - 16
packages/app/public/static/locales/en_US/translation.json

@@ -237,9 +237,6 @@
     "Shere this page link to public": "Shere this page link to public",
     "share_link_list": "Share link list",
     "share_link_management": "Share Link Management",
-    "No_share_links":"No share links",
-    "Share Link": "Share Link",
-    "Page Path": "Page Path",
     "delete_all_share_links":"Delete all share links",
     "expire": "Expiration",
     "Days": "Days",
@@ -522,19 +519,8 @@
   "toaster": {
     "file_upload_succeeded": "File upload succeeded.",
     "file_upload_failed": "File upload failed.",
-    "initialize_successed": "Succeeded to initialize {{target}}",
-    "give_user_admin": "Succeeded to give {{username}} admin",
-    "remove_user_admin": "Succeeded to remove {{username}} admin",
-    "activate_user_success": "Succeeded to activating {{username}}",
-    "deactivate_user_success": "Succeeded to deactivate {{username}}",
-    "remove_user_success": "Succeeded to removing {{username}}",
-    "remove_external_user_success": "Succeeded to remove {{accountId}}",
-    "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
-    "issue_share_link": "Succeeded to issue new share link",
-    "remove_share_link": "Succeeded to remove {{count}} share links",
-    "switch_disable_link_sharing_success": "Succeeded to update share link setting",
-    "failed_to_reset_password":"Failed to reset password",
-    "save_succeeded": "Saved successfully"
+    "save_succeeded": "Saved successfully",
+    "issue_share_link": "Succeeded to issue new share link"
   },
   "template": {
     "modal_label": {
@@ -699,6 +685,7 @@
     "username_should_not_be_null":"Username should not be null. Please check Authentication Mechanism Settings on admin page",
     "email_address_is_already_registered":"This email address is already registered.",
     "can_not_register_maximum_number_of_users":"Can not register more than the maximum number of users.",
+    "email_settings_is_not_setup":"E-mail settings is not set up. Please ask the administrator.",
     "failed_to_register":"Failed to register.",
     "successfully_created":"The user {{username}} is successfully created.",
     "can_not_activate_maximum_number_of_users":"Can not activate more than the maximum number of users.",

+ 13 - 5
packages/app/public/static/locales/ja_JP/admin.json

@@ -87,12 +87,9 @@
       "closed": "非公開 (登録には管理者による招待が必要)"
     },
     "share_link_management": "共有リンク管理",
+    "No_share_links":"共有リンクが存在しません",
     "share_link_notice":"共有リンクを全て削除します",
     "delete_all_share_links":"全ての共有リンクを削除します",
-    "Share Link": "共有用リンク",
-    "Page Path": "ページパス",
-    "expire": "有効期限",
-    "description": "概要",
     "share_link_rights": "シェアリンクの権限",
     "enable_link_sharing": "リンクのシェアを許可",
     "all_share_links": "全てのシェアリンク",
@@ -283,7 +280,8 @@
     "delete_notification_pattern": "通知パターンを削除しました。",
     "delete_notification_pattern_desc1": "Path: {{path}} を削除します。",
     "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
-    "toggle_notification": "{{path}}の通知設定を変更しました"
+    "toggle_notification": "{{path}}の通知設定を変更しました",
+    "not_found_global_notification_triggerid": "アクセス先の通知設定は見つかりませんでした"
   },
   "full_text_search_management": {
     "full_text_search_management": "全文検索管理",
@@ -1041,5 +1039,15 @@
     "ADMIN_SEARCH_CONNECTION": "Elasticsearch の再接続の試行",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Elasticsearch のインデックスの正規化",
     "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド"
+  },
+  "toaster": {
+    "give_user_admin": "{{username}}を管理者に設定しました",
+    "remove_user_admin": "{{username}}を管理者から外しました",
+    "activate_user_success": "{{username}}を有効化しました",
+    "deactivate_user_success": "{{username}}を無効化しました",
+    "remove_user_success": "{{username}}を削除しました",
+    "remove_external_user_success": "{{accountId}}を削除しました",
+    "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
+    "failed_to_reset_password":"パスワードのリセットに失敗しました"
   }
 }

+ 11 - 1
packages/app/public/static/locales/ja_JP/commons.json

@@ -6,12 +6,22 @@
     "create_succeeded": "新しい{{target}}が作成されました",
     "create_failed": "{{target}}の作成に失敗しました",
     "update_successed": "{{target}}を更新しました",
-    "update_failed": "{{target}}の更新に失敗しました"
+    "update_failed": "{{target}}の更新に失敗しました",
+
+    "remove_share_link_success": "{{shareLinkId}}を削除しました",
+    "remove_share_link": "共有リンクを{{count}}件削除しました"
   },
   "alert": {
     "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。"
   },
   "headers": {
     "app_settings": "アプリ設定"
+  },
+
+  "share_links": {
+    "Share Link": "共有用リンク",
+    "Page Path": "ページパス",
+    "expire": "有効期限",
+    "description": "概要"
   }
 }

+ 3 - 16
packages/app/public/static/locales/ja_JP/translation.json

@@ -233,9 +233,6 @@
     "Shere this page link to public": "外部に共有するリンクを発行する",
     "share_link_list": "共有リンクリスト",
     "share_link_management": "共有リンク管理",
-    "No_share_links":"共有リンクが存在しません",
-    "Share Link": "共有用リンク",
-    "Page Path": "ページパス",
     "delete_all_share_links":"全ての共有リンクを削除します",
     "expire": "有効期限",
     "Days": "日間",
@@ -516,19 +513,8 @@
   "toaster": {
     "file_upload_succeeded": "ファイルをアップロードしました",
     "file_upload_failed": "ファイルのアップロードに失敗しました",
-    "initialize_successed": "{{target}}を初期化しました",
-    "give_user_admin": "{{username}}を管理者に設定しました",
-    "remove_user_admin": "{{username}}を管理者から外しました",
-    "activate_user_success": "{{username}}を有効化しました",
-    "deactivate_user_success": "{{username}}を無効化しました",
-    "remove_user_success": "{{username}}を削除しました",
-    "remove_external_user_success": "{{accountId}}を削除しました",
-    "remove_share_link_success": "{{shareLinkId}}を削除しました",
-    "issue_share_link": "共有リンクを作成しました",
-    "remove_share_link": "共有リンクを{{count}}件削除しました",
-    "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
-    "failed_to_reset_password":"パスワードのリセットに失敗しました",
-    "save_succeeded": "保存に成功しました"
+    "save_succeeded": "保存に成功しました",
+    "issue_share_link": "共有リンクを作成しました"
   },
   "template": {
     "modal_label": {
@@ -693,6 +679,7 @@
     "username_should_not_be_null":"Username が null になっています 管理画面の認証機構設定にて設定の確認をしてください",
     "email_address_is_already_registered":"このメールアドレスは既に登録されています。",
     "can_not_register_maximum_number_of_users":"ユーザー数が上限を超えたため登録できません。",
+    "email_settings_is_not_setup":"E-mail 設定が完了していません。管理者に問い合わせてください。",
     "failed_to_register":"登録に失敗しました。",
     "successfully_created":"{{username}} が作成されました。",
     "can_not_activate_maximum_number_of_users":"ユーザーが上限に達したためアクティベートできません。",

+ 61 - 4
packages/app/public/static/locales/zh_CN/admin.json

@@ -89,12 +89,9 @@
 			"closed": "已关闭(仅限邀请)"
 		},
     "share_link_management": "Share Link Management",
+    "No_share_links":"No share links",
     "share_link_notice":"remove all share links",
     "delete_all_share_links":"Delete all share links",
-    "Share Link": "Share Link",
-    "Page Path": "Page Path",
-    "expire": "Expiration",
-    "description": "Description",
     "share_link_rights": "分享链接权",
     "enable_link_sharing": "启用链接共享",
     "all_share_links": "所有共享链接",
@@ -241,6 +238,56 @@
 			"ABLCRule": "Rule"
 		}
   },
+  "notification_settings": {
+		"slack_incoming_configuration": "Slack Incoming Webhooks configuration",
+		"prioritize_webhook": "Prioritize incoming webhook than Slack App",
+		"prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
+		"slack_app_configuration": "Slack app configuration",
+		"slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
+		"use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
+		"how_to": {
+			"header": "How to configure Incoming Webhooks?",
+			"workspace": "(At Workspace) Add a hook",
+			"workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
+			"workspace_desc2": "Choose the default channel to post.",
+			"workspace_desc3": "Add.",
+			"at_growi": "(At GROWI admin page) Set Webhook URL",
+			"at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
+		},
+		"user_trigger_notification_header": "Default notification settings for patterns",
+		"pattern": "Pattern",
+		"channel": "Channel",
+		"pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
+		"channel_desc": "Slack channel name. Without <code>#</code>.",
+		"valid_page": "启用/禁用通知",
+		"link_notification_help": "<strong>只有那些知道“链接的任何人”链接的人才能查看的页面并不总是得到通知。</strong> ",
+		"just_me_notification_help": "<strong>被“仅限我”限制的页在编辑时被通知。</strong>",
+		"group_notification_help": "<strong>被“用户组”限制的页面在编辑时被通知。</strong>",
+		"notification_list": "List of notification settings",
+		"add_notification": "Add new",
+		"trigger_path": "Trigger path",
+		"trigger_path_help": "(expression with <code>*</code> is supported)",
+		"trigger_events": "Trigger events",
+		"notify_to": "Notify to",
+		"back_to_list": "Go back to list",
+		"notification_detail": "Notification Setting Details",
+		"event_pageCreate": "When new page is \"CREATED\"",
+		"event_pageEdit": "When page is \"EDITED\"",
+		"event_pageDelete": "When page is \"DELETED\"",
+		"event_pageMove": "When page is \"MOVED\" (renamed)",
+		"event_pageLike": "When someone \"LIKES\" page",
+		"event_comment": "When someone \"COMMENTS\" on page",
+		"email": {
+			"ifttt_link": "Create a new IFTTT applet with Email trigger"
+		},
+		"updated_slackApp": "Succeeded to update Slack App Configuration setting",
+		"add_notification_pattern": "Add user trigger notification patterns",
+		"delete_notification_pattern": "Delete notification pattern",
+		"delete_notification_pattern_desc1": "Delete Path: {{path}}",
+		"delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
+		"toggle_notification": "Updated setting of {{path}}",
+    "not_found_global_notification_triggerid": "未找到全局通知 ID"
+	},
   "full_text_search_management": {
     "full_text_search_management": "全文搜索管理",
 		"elasticsearch_management": "Elasticsearch管理",
@@ -1008,5 +1055,15 @@
     "ADMIN_SEARCH_CONNECTION": "重试Elasticsearch连接",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "试图重新连接Elasticsearch",
     "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引"
+  },
+  "toaster": {
+    "give_user_admin": "Succeeded to give {{username}} admin",
+    "remove_user_admin": "Succeeded to remove {{username}} admin ",
+		"activate_user_success": "Succeeded to activating {{username}}",
+		"deactivate_user_success": "Succeeded to deactivate {{username}}",
+    "remove_user_success": "Succeeded to removing {{username}}",
+    "remove_external_user_success": "Succeeded to remove {{accountId}}",
+    "switch_disable_link_sharing_success": "成功更新分享链接设置",
+    "failed_to_reset_password":"Failed to reset password"
   }
 }

+ 11 - 1
packages/app/public/static/locales/zh_CN/commons.json

@@ -6,12 +6,22 @@
     "create_succeeded": "Succeeded to create {{target}}",
     "create_failed": "Failed to create {{target}}",
     "update_successed": "Succeeded to update {{target}}",
-    "update_failed": "Failed to update {{target}}"
+    "update_failed": "Failed to update {{target}}",
+
+    "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
+    "remove_share_link": "Succeeded to remove {{count}} share links"
   },
   "alert": {
     "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置"
   },
   "headers": {
     "app_settings": "系统设置"
+  },
+
+  "share_links": {
+    "Share Link": "Share Link",
+    "Page Path": "Page Path",
+    "expire": "Expiration",
+    "description": "Description"
   }
 }

+ 3 - 62
packages/app/public/static/locales/zh_CN/translation.json

@@ -497,16 +497,8 @@
 	"toaster": {
     "file_upload_succeeded": "文件上传成功",
     "file_upload_failed": "文件上传失败",
-    "initialize_successed": "Succeeded to initialize {{target}}",
-    "give_user_admin": "Succeeded to give {{username}} admin",
-    "remove_user_admin": "Succeeded to remove {{username}} admin ",
-		"activate_user_success": "Succeeded to activating {{username}}",
-		"deactivate_user_success": "Succeeded to deactivate {{username}}",
-		"remove_user_success": "Succeeded to removing {{username}} ",
-    "remove_external_user_success": "Succeeded to remove {{accountId}} ",
-    "switch_disable_link_sharing_success": "成功更新分享链接设置",
-    "failed_to_reset_password":"Failed to reset password",
-    "save_succeeded": "已成功保存"
+    "save_succeeded": "已成功保存",
+    "issue_share_link": "Succeeded to issue new share link"
   },
 	"template": {
 		"modal_label": {
@@ -586,9 +578,6 @@
     "Shere this page link to public": "Shere this page link to public",
     "share_link_list": "Share link list",
     "share_link_management": "Share Link Management",
-    "No_share_links":"No share links",
-    "Share Link": "Share Link",
-    "Page Path": "Page Path",
     "delete_all_share_links":"Delete all share links",
     "expire": "Expiration",
     "Days": "Days",
@@ -601,55 +590,6 @@
     "Invalid_Number_of_Date" : "You entered invalid value",
     "link_sharing_is_disabled": "链接共享已被禁用"
   },
-	"notification_setting": {
-		"slack_incoming_configuration": "Slack Incoming Webhooks configuration",
-		"prioritize_webhook": "Prioritize incoming webhook than Slack App",
-		"prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
-		"slack_app_configuration": "Slack app configuration",
-		"slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
-		"use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
-		"how_to": {
-			"header": "How to configure Incoming Webhooks?",
-			"workspace": "(At Workspace) Add a hook",
-			"workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
-			"workspace_desc2": "Choose the default channel to post.",
-			"workspace_desc3": "Add.",
-			"at_growi": "(At GROWI admin page) Set Webhook URL",
-			"at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
-		},
-		"user_trigger_notification_header": "Default notification settings for patterns",
-		"pattern": "Pattern",
-		"channel": "Channel",
-		"pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
-		"channel_desc": "Slack channel name. Without <code>#</code>.",
-		"valid_page": "启用/禁用通知",
-		"link_notification_help": "<strong>只有那些知道“链接的任何人”链接的人才能查看的页面并不总是得到通知。</strong> ",
-		"just_me_notification_help": "<strong>被“仅限我”限制的页在编辑时被通知。</strong>",
-		"group_notification_help": "<strong>被“用户组”限制的页面在编辑时被通知。</strong>",
-		"notification_list": "List of notification settings",
-		"add_notification": "Add new",
-		"trigger_path": "Trigger path",
-		"trigger_path_help": "(expression with <code>*</code> is supported)",
-		"trigger_events": "Trigger events",
-		"notify_to": "Notify to",
-		"back_to_list": "Go back to list",
-		"notification_detail": "Notification Setting Details",
-		"event_pageCreate": "When new page is \"CREATED\"",
-		"event_pageEdit": "When page is \"EDITED\"",
-		"event_pageDelete": "When page is \"DELETED\"",
-		"event_pageMove": "When page is \"MOVED\" (renamed)",
-		"event_pageLike": "When someone \"LIKES\" page",
-		"event_comment": "When someone \"COMMENTS\" on page",
-		"email": {
-			"ifttt_link": "Create a new IFTTT applet with Email trigger"
-		},
-		"updated_slackApp": "Succeeded to update Slack App Configuration setting",
-		"add_notification_pattern": "Add user trigger notification patterns",
-		"delete_notification_pattern": "Delete notification pattern",
-		"delete_notification_pattern_desc1": "Delete Path: {{path}}",
-		"delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
-		"toggle_notification": "Updated setting of {{path}}"
-	},
 	"personal_dropdown": {
 		"home": "家",
 		"settings": "设置",
@@ -747,6 +687,7 @@
     "username_should_not_be_null":"用户名不应为空。请检查管理页面上的身份验证机制设置",
 		"email_address_is_already_registered": "此电子邮件地址已注册。",
 		"can_not_register_maximum_number_of_users": "注册的用户数不能超过最大值。",
+    "email_settings_is_not_setup":"邮箱设置未设置,请询问管理员。",
 		"failed_to_register": "注册失败。",
 		"successfully_created": "已成功创建用户{{username}。",
 		"can_not_activate_maximum_number_of_users": "无法激活超过最大用户数的用户。",

+ 1 - 1
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -23,7 +23,7 @@ const AppSetting = (props) => {
   const submitHandler = useCallback(async() => {
     try {
       await adminAppContainer.updateAppSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('commons:headers.app_settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('commons:headers.app_settings'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);

+ 60 - 19
packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -1,12 +1,16 @@
-import React, { useCallback, useState } from 'react';
+import React, {
+  useCallback, useMemo, useEffect, useState,
+} from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { useIsMailerSetup } from '~/stores/context';
+import { useSWRxGlobalNotification } from '~/stores/global-notification';
 import loggerFactory from '~/utils/logger';
 
 
@@ -18,18 +22,48 @@ import TriggerEventCheckBox from './TriggerEventCheckBox';
 
 const logger = loggerFactory('growi:manageGlobalNotification');
 
-const ManageGlobalNotification = (props) => {
 
-  let globalNotification;
-  // TODO: securely fetch the data of globalNotification variable without using swig. URL https://redmine.weseek.co.jp/issues/103901
-  // globalNotification = JSON.parse(document.getElementById('admin-global-notification-setting').getAttribute('data-global-notification'));
+const ManageGlobalNotification = (props) => {
 
-  const [globalNotificationId, setGlobalNotificationId] = useState(null);
   const [triggerPath, setTriggerPath] = useState('');
-  const [notifyToType, setNotifyToType] = useState('mail');
+  const [notifyType, setNotifyType] = useState('mail');
   const [emailToSend, setEmailToSend] = useState('');
   const [slackChannelToSend, setSlackChannelToSend] = useState('');
-  const [triggerEvents, setTriggerEvents] = useState(new Set(globalNotification?.triggerEvents));
+  const [triggerEvents, setTriggerEvents] = useState(new Set());
+  const { data: globalNotificationData, update: updateGlobalNotification } = useSWRxGlobalNotification(props.globalNotificationId);
+  const globalNotification = useMemo(() => globalNotificationData?.globalNotification, [globalNotificationData?.globalNotification]);
+
+  const router = useRouter();
+
+
+  useEffect(() => {
+    if (globalNotification != null) {
+      const notifyType = globalNotification.__t;
+      setNotifyType(notifyType);
+
+      setTriggerPath(globalNotification.triggerPath);
+      setTriggerEvents(new Set(globalNotification.triggerEvents));
+
+      if (notifyType === 'mail') {
+        setEmailToSend(globalNotification.toEmail);
+      }
+      else {
+        setSlackChannelToSend(globalNotification.slackChannels);
+      }
+    }
+
+
+  }, [globalNotification]);
+
+  const isLoading = globalNotificationData === undefined;
+  const notExistsGlobalNotification = !isLoading && globalNotificationData == null;
+
+  useEffect(() => {
+    if (notExistsGlobalNotification) {
+      router.push('/admin/notification');
+    }
+  }, [notExistsGlobalNotification, router]);
+
 
   const onChangeTriggerEvents = useCallback((triggerEvent) => {
     let newTriggerEvents;
@@ -44,29 +78,35 @@ const ManageGlobalNotification = (props) => {
     }
   }, [triggerEvents]);
 
-  const updateButtonClickedHandler = useCallback(async() => {
 
+  const updateButtonClickedHandler = useCallback(async() => {
     const requestParams = {
       triggerPath,
-      notifyToType,
+      notifyType,
       toEmail: emailToSend,
       slackChannels: slackChannelToSend,
       triggerEvents: [...triggerEvents],
     };
 
+    const { _id: globalNotificationId } = globalNotification;
+
     try {
       if (globalNotificationId != null) {
-        await apiv3Put(`/notification-setting/global-notification/${globalNotificationId}`, requestParams);
+        await updateGlobalNotification(requestParams);
+        router.push('/admin/notification');
+        // await apiv3Put(`/notification-setting/global-notification/${globalNotificationId}`, requestParams);
       }
       else {
         await apiv3Post('/notification-setting/global-notification', requestParams);
+        router.push('/admin/notification');
       }
     }
     catch (err) {
       toastError(err);
       logger.error(err);
     }
-  }, [emailToSend, globalNotificationId, notifyToType, slackChannelToSend, triggerEvents, triggerPath]);
+  }, [emailToSend, globalNotification, notifyType, router, slackChannelToSend, triggerEvents, triggerPath, updateGlobalNotification]);
+
 
   const { data: isMailerSetup } = useIsMailerSetup();
   const { adminNotificationContainer } = props;
@@ -110,10 +150,10 @@ const ManageGlobalNotification = (props) => {
                 className="custom-control-input"
                 type="radio"
                 id="mail"
-                name="notifyToType"
+                name="notifyType"
                 value="mail"
-                checked={notifyToType === 'mail'}
-                onChange={() => { setNotifyToType('mail') }}
+                checked={notifyType === 'mail'}
+                onChange={() => { setNotifyType('mail') }}
               />
               <label className="custom-control-label" htmlFor="mail">
                 <p className="font-weight-bold">Email</p>
@@ -124,10 +164,10 @@ const ManageGlobalNotification = (props) => {
                 className="custom-control-input"
                 type="radio"
                 id="slack"
-                name="notifyToType"
+                name="notifyType"
                 value="slack"
-                checked={notifyToType === 'slack'}
-                onChange={() => { setNotifyToType('slack') }}
+                checked={notifyType === 'slack'}
+                onChange={() => { setNotifyType('slack') }}
               />
               <label className="custom-control-label" htmlFor="slack">
                 <p className="font-weight-bold">Slack</p>
@@ -135,7 +175,7 @@ const ManageGlobalNotification = (props) => {
             </div>
           </div>
 
-          {notifyToType === 'mail'
+          {notifyType === 'mail'
             ? (
               <>
                 <div className="input-group notify-to-option" id="mail-input">
@@ -278,6 +318,7 @@ const ManageGlobalNotification = (props) => {
 
 ManageGlobalNotification.propTypes = {
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+  globalNotificationId: PropTypes.string,
 };
 
 const ManageGlobalNotificationWrapper = withUnstatedContainers(ManageGlobalNotification, [AdminNotificationContainer]);

+ 3 - 3
packages/app/src/components/Admin/Security/ShareLinkSetting.tsx

@@ -70,7 +70,7 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
     try {
       const res = await apiv3Delete('/share-links/all');
       const { deletedCount } = res.data;
-      toastSuccess(t('toaster.remove_share_link', { count: deletedCount }));
+      toastSuccess(t('toaster.remove_share_link', { count: deletedCount, ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
@@ -82,7 +82,7 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
     try {
       const res = await apiv3Delete(`/share-links/${shareLinkId}`);
       const { deletedShareLink } = res.data;
-      toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
+      toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id, ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
@@ -148,7 +148,7 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
           isAdmin
         />
       )
-        : (<p className="text-center">{t('share_links.No_share_links')}</p>
+        : (<p className="text-center">{t('security_settings.No_share_links')}</p>
         )
       }
 

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -37,7 +37,7 @@ const UpdateParentConfirmModal = dynamic(() => import('./UpdateParentConfirmModa
 
 
 type Props = {
-  userGroupId?: string,
+  userGroupId: string,
 }
 
 const UserGroupDetailPage = (props: Props): JSX.Element => {

+ 2 - 2
packages/app/src/components/Admin/Users/GiveAdminButton.tsx

@@ -15,7 +15,7 @@ type GiveAdminButtonProps = {
 
 const GiveAdminButton = (props: GiveAdminButtonProps): JSX.Element => {
 
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { adminUsersContainer, user } = props;
 
   const onClickGiveAdminBtnHandler = useCallback(async() => {
@@ -30,7 +30,7 @@ const GiveAdminButton = (props: GiveAdminButtonProps): JSX.Element => {
 
   return (
     <button className="dropdown-item" type="button" onClick={() => onClickGiveAdminBtnHandler()}>
-      <i className="icon-fw icon-user-following"></i> {t('admin:user_management.user_table.give_admin_access')}
+      <i className="icon-fw icon-user-following"></i> {t('user_management.user_table.give_admin_access')}
     </button>
   );
 

+ 9 - 9
packages/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -42,11 +42,11 @@ class PasswordResetModal extends React.Component {
     return (
       <>
         <p>
-          {t('admin:user_management.reset_password_modal.password_never_seen')}<br />
-          <span className="text-danger">{t('admin:user_management.reset_password_modal.send_new_password')}</span>
+          {t('user_management.reset_password_modal.password_never_seen')}<br />
+          <span className="text-danger">{t('user_management.reset_password_modal.send_new_password')}</span>
         </p>
         <p>
-          {t('admin:user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
+          {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
         </p>
       </>
     );
@@ -57,12 +57,12 @@ class PasswordResetModal extends React.Component {
 
     return (
       <>
-        <p className="alert alert-danger">{t('admin:user_management.reset_password_modal.password_reset_message')}</p>
+        <p className="alert alert-danger">{t('user_management.reset_password_modal.password_reset_message')}</p>
         <p>
-          {t('admin:user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
+          {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
         </p>
         <p>
-          {t('admin:user_management.reset_password_modal.new_password')}: <code>{this.state.temporaryPassword}</code>
+          {t('user_management.reset_password_modal.new_password')}: <code>{this.state.temporaryPassword}</code>
         </p>
       </>
     );
@@ -72,7 +72,7 @@ class PasswordResetModal extends React.Component {
     const { t } = this.props;
     return (
       <button type="submit" className="btn btn-danger" onClick={this.resetPassword}>
-        {t('admin:user_management.reset_password')}
+        {t('user_management.reset_password')}
       </button>
     );
   }
@@ -94,7 +94,7 @@ class PasswordResetModal extends React.Component {
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
         <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-warning text-light">
-          {t('admin:user_management.reset_password') }
+          {t('user_management.reset_password') }
         </ModalHeader>
         <ModalBody>
           {this.state.isPasswordResetDone ? this.returnModalBodyAfterReset() : this.renderModalBodyBeforeReset()}
@@ -109,7 +109,7 @@ class PasswordResetModal extends React.Component {
 }
 
 const PasswordResetModalWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   return <PasswordResetModal t={t} {...props} />;
 };
 

+ 4 - 4
packages/app/src/components/Admin/Users/RemoveAdminButton.tsx

@@ -16,7 +16,7 @@ type RemoveAdminButtonProps = {
 
 const RemoveAdminButton = (props: RemoveAdminButtonProps): JSX.Element => {
 
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { data: currentUser } = useCurrentUser();
   const { adminUsersContainer, user } = props;
 
@@ -33,7 +33,7 @@ const RemoveAdminButton = (props: RemoveAdminButtonProps): JSX.Element => {
   const renderRemoveAdminBtn = () => {
     return (
       <button className="dropdown-item" type="button" onClick={() => onClickRemoveAdminBtnHandler()}>
-        <i className="icon-fw icon-user-unfollow"></i>{t('admin:user_management.user_table.remove_admin_access')}
+        <i className="icon-fw icon-user-unfollow"></i>{t('user_management.user_table.remove_admin_access')}
       </button>
     );
   };
@@ -41,8 +41,8 @@ const RemoveAdminButton = (props: RemoveAdminButtonProps): JSX.Element => {
   const renderRemoveAdminAlert = () => {
     return (
       <div className="px-4">
-        <i className="icon-fw icon-user-unfollow mb-2"></i>{t('admin:user_management.user_table.remove_admin_access')}
-        <p className="alert alert-danger">{t('admin:user_management.user_table.cannot_remove')}</p>
+        <i className="icon-fw icon-user-unfollow mb-2"></i>{t('user_management.user_table.remove_admin_access')}
+        <p className="alert alert-danger">{t('user_management.user_table.cannot_remove')}</p>
       </div>
     );
   };

+ 2 - 2
packages/app/src/components/Admin/Users/RemoveAdminMenuItem.tsx

@@ -29,7 +29,7 @@ type Props = {
 }
 
 const RemoveAdminMenuItem = (props: Props): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const { adminUsersContainer, user } = props;
 
@@ -49,7 +49,7 @@ const RemoveAdminMenuItem = (props: Props): JSX.Element => {
   return user.username !== currentUser?.username
     ? (
       <button className="dropdown-item" type="button" onClick={clickRemoveAdminBtnHandler}>
-        <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_management.user_table.remove_admin_access')}
+        <i className="icon-fw icon-user-unfollow"></i> {t('user_management.user_table.remove_admin_access')}
       </button>
     )
     : <RemoveAdminAlert />;

+ 2 - 2
packages/app/src/components/Admin/Users/StatusActivateButton.jsx

@@ -33,7 +33,7 @@ class StatusActivateButton extends React.Component {
 
     return (
       <button className="dropdown-item" type="button" onClick={() => { this.onClickAcceptBtn() }}>
-        <i className="icon-fw icon-user-following"></i> {t('admin:user_management.user_table.accept')}
+        <i className="icon-fw icon-user-following"></i> {t('user_management.user_table.accept')}
       </button>
     );
   }
@@ -41,7 +41,7 @@ class StatusActivateButton extends React.Component {
 }
 
 const StatusActivateFormWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   return <StatusActivateButton t={t} {...props} />;
 };
 

+ 2 - 2
packages/app/src/components/Admin/Users/StatusSuspendMenuItem.tsx

@@ -28,7 +28,7 @@ type Props = {
 }
 
 const StatusSuspendMenuItem = (props: Props): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const { adminUsersContainer, user } = props;
 
@@ -47,7 +47,7 @@ const StatusSuspendMenuItem = (props: Props): JSX.Element => {
   return user.username !== currentUser?.username
     ? (
       <button className="dropdown-item" type="button" onClick={clickDeactiveBtnHandler}>
-        <i className="icon-fw icon-ban"></i> {t('admin:user_management.user_table.deactivate_account')}
+        <i className="icon-fw icon-ban"></i> {t('user_management.user_table.deactivate_account')}
       </button>
     )
     : <SuspendAlert />;

+ 1 - 1
packages/app/src/components/Admin/Users/UserRemoveButton.jsx

@@ -49,7 +49,7 @@ UserRemoveButton.propTypes = {
 };
 
 const UserRemoveButtonWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   return <UserRemoveButton t={t} {...props} />;
 };
 

+ 2 - 2
packages/app/src/components/Page.tsx

@@ -36,7 +36,7 @@ declare const globalEmitter: EventEmitter;
 
 // const DrawioModal = dynamic(() => import('./PageEditor/DrawioModal'), { ssr: false });
 const GridEditModal = dynamic(() => import('./PageEditor/GridEditModal'), { ssr: false });
-// const HandsontableModal = dynamic(() => import('./PageEditor/HandsontableModal'), { ssr: false });
+const HandsontableModal = dynamic(() => import('./PageEditor/HandsontableModal'), { ssr: false });
 const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr: false });
 
 const logger = loggerFactory('growi:Page');
@@ -188,7 +188,7 @@ class PageSubstance extends React.Component<PageSubstanceProps> {
           <>
             <GridEditModal ref={this.gridEditModal} />
             <LinkEditModal ref={this.linkEditModal} />
-            {/* <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} /> */}
+            <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} />
             {/* TODO: use global DrawioModal https://redmine.weseek.co.jp/issues/105981 */}
             {/* <DrawioModal
               ref={this.drawioModal}

+ 4 - 4
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -25,7 +25,7 @@ import EmojiPickerHelper from './EmojiPickerHelper';
 import GridEditModal from './GridEditModal';
 // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
 // import geu from './GridEditorUtil';
-// import HandsontableModal from './HandsontableModal';
+import HandsontableModal from './HandsontableModal';
 import LinkEditModal from './LinkEditModal';
 import mdu from './MarkdownDrawioUtil';
 import markdownLinkUtil from './MarkdownLinkUtil';
@@ -870,7 +870,7 @@ class CodeMirrorEditor extends AbstractEditor {
   }
 
   showHandsonTableHandler() {
-    // this.handsontableModal.current.show(mtu.getMarkdownTable(this.getCodeMirror()));
+    this.handsontableModal.current.show(mtu.getMarkdownTable(this.getCodeMirror()));
   }
 
 
@@ -1135,11 +1135,11 @@ class CodeMirrorEditor extends AbstractEditor {
           ref={this.linkEditModal}
           onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
         />
-        {/* <HandsontableModal
+        <HandsontableModal
           ref={this.handsontableModal}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
           autoFormatMarkdownTable={this.props.editorSettings.autoFormatMarkdownTable}
-        /> */}
+        />
       </div>
     );
   }

+ 9 - 1
packages/app/src/components/PageEditor/HandsontableModal.jsx

@@ -16,6 +16,9 @@ import ExpandOrContractButton from '../ExpandOrContractButton';
 
 import MarkdownTableDataImportForm from './MarkdownTableDataImportForm';
 
+import styles from './HandsontableModal.module.scss';
+import 'handsontable/dist/handsontable.full.min.css';
+
 const DEFAULT_HOT_HEIGHT = 300;
 const MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING = {
   r: 'htRight',
@@ -332,6 +335,10 @@ export default class HandsontableModal extends React.PureComponent {
     const align = this.state.markdownTable.options.align;
     const hotInstance = this.hotTable.hotInstance;
 
+    if (hotInstance.isDestroyed === true) {
+      return;
+    }
+
     for (let i = 0; i < align.length; i++) {
       for (let j = 0; j < hotInstance.countRows(); j++) {
         hotInstance.setCellMeta(j, i, 'className', MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING[align[i]]);
@@ -435,7 +442,8 @@ export default class HandsontableModal extends React.PureComponent {
         backdrop="static"
         keyboard={false}
         size="lg"
-        className={`handsontable-modal ${this.state.isWindowExpanded && 'grw-modal-expanded'}`}
+        className={`handsontable-modal ${styles['grw-handsontable']}
+          ${this.state.isWindowExpanded && `grw-modal-expanded ${styles['grw-modal-expanded']}`}`}
       >
         <ModalHeader tag="h4" toggle={this.cancel} close={buttons} className="bg-primary text-light">
           Edit Table

+ 24 - 0
packages/app/src/components/PageEditor/HandsontableModal.module.scss

@@ -0,0 +1,24 @@
+.grw-handsontable :global {
+  .handsontable {
+    position: relative;
+
+    .handsontableInput {
+      max-width: 290px !important;
+    }
+
+    td {
+      word-break: break-all;
+    }
+
+    th {
+      text-align: inherit;
+    }
+  }
+}
+
+// expand .hot-table-container (with flexbox)
+.grw-modal-expanded :global {
+  .hot-table-container {
+    flex: 1;
+  }
+}

+ 4 - 3
packages/app/src/components/SavePageControls/GrantSelector.tsx

@@ -9,6 +9,7 @@ import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
+import { IPageGrantData } from '~/interfaces/page';
 import { IUserGroupHasId } from '~/interfaces/user';
 import { useCurrentUser } from '~/stores/context';
 import { useSWRxMyUserGroupRelations } from '~/stores/user-group';
@@ -37,7 +38,7 @@ type Props = {
   grantGroupId?: string,
   grantGroupName?: string,
 
-  onUpdateGrant?: (args: { grant: number, grantGroupId?: string | null, grantGroupName?: string | null }) => void,
+  onUpdateGrant?: (grantData: IPageGrantData) => void,
 }
 
 /**
@@ -78,13 +79,13 @@ const GrantSelector = (props: Props): JSX.Element => {
     }
 
     if (onUpdateGrant != null) {
-      onUpdateGrant({ grant, grantGroupId: null, grantGroupName: null });
+      onUpdateGrant({ grant, grantedGroup: undefined });
     }
   }, [onUpdateGrant, showSelectGroupModal]);
 
   const groupListItemClickHandler = useCallback((grantGroup: IUserGroupHasId) => {
     if (onUpdateGrant != null) {
-      onUpdateGrant({ grant: 5, grantGroupId: grantGroup._id, grantGroupName: grantGroup.name });
+      onUpdateGrant({ grant: 5, grantedGroup: { id: grantGroup._id, name: grantGroup.name } });
     }
 
     // hide modal

+ 2 - 2
packages/app/src/components/ShareLink/ShareLink.tsx

@@ -29,7 +29,7 @@ const ShareLink = (): JSX.Element => {
     try {
       const res = await apiv3Delete('/share-links/', { relatedPage: currentPageId });
       const count = res.data.n;
-      toastSuccess(t('toaster.remove_share_link', { count }));
+      toastSuccess(t('toaster.remove_share_link', { count, ns: 'commons' }));
       mutate();
     }
     catch (err) {
@@ -41,7 +41,7 @@ const ShareLink = (): JSX.Element => {
     try {
       const res = await apiv3Delete(`/share-links/${shareLinkId}`);
       const { deletedShareLink } = res.data;
-      toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
+      toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id, ns: 'commons' }));
       mutate();
     }
     catch (err) {

+ 5 - 5
packages/app/src/components/ShareLink/ShareLinkList.tsx

@@ -69,7 +69,7 @@ type Props = {
 
 const ShareLinkList = (props: Props): JSX.Element => {
 
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation('commons');
 
   function renderShareLinks() {
     return (
@@ -96,10 +96,10 @@ const ShareLinkList = (props: Props): JSX.Element => {
       <table className="table table-bordered">
         <thead>
           <tr>
-            <th>{t('security_settings.Share Link')}</th>
-            {props.isAdmin && <th>{t('security_settings.Page Path')}</th>}
-            <th>{t('security_settings.expire')}</th>
-            <th>{t('security_settings.description')}</th>
+            <th>{t('share_links.Share Link', { ns: 'commons' })}</th>
+            {props.isAdmin && <th>{t('share_links.Page Path', { ns: 'commons' })}</th>}
+            <th>{t('share_links.expire', { ns: 'commons' })}</th>
+            <th>{t('share_links.description', { ns: 'commons' })}</th>
             <th></th>
           </tr>
         </thead>

+ 73 - 0
packages/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx

@@ -0,0 +1,73 @@
+import { useEffect } from 'react';
+
+import { isClient, objectIdUtils } from '@growi/core';
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
+import { Container, Provider } from 'unstated';
+
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+
+import { retrieveServerSideProps } from '../../../utils/admin-page-util';
+
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
+const ManageGlobalNotification = dynamic(() => import('~/components/Admin/Notification/ManageGlobalNotification'), { ssr: false });
+
+
+const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
+  const { t } = useTranslation('admin');
+  const router = useRouter();
+  const { globalNotificationId } = router.query;
+  const currentGlobalNotificationId = Array.isArray(globalNotificationId) ? globalNotificationId[0] : globalNotificationId;
+
+
+  useEffect(() => {
+    if (globalNotificationId == null) {
+      router.push('/admin/notification');
+    }
+    if ((currentGlobalNotificationId != null && !objectIdUtils.isValidObjectId(currentGlobalNotificationId))) {
+      toastError(t('notification_settings.not_found_global_notification_triggerid'));
+      router.push('/admin/global-notification/new');
+      return;
+    }
+  }, [currentGlobalNotificationId, globalNotificationId, router, t]);
+
+
+  const title = t('external_notification.external_notification');
+  const customTitle = useCustomTitle(props, title);
+
+
+  const injectableContainers: Container<any>[] = [];
+
+  if (isClient()) {
+    const adminNotificationContainer = new AdminNotificationContainer();
+    injectableContainers.push(adminNotificationContainer);
+  }
+
+
+  return (
+    <Provider inject={[...injectableContainers]}>
+      <AdminLayout title={customTitle} componentTitle={title} >
+        {
+          currentGlobalNotificationId != null && router.isReady
+      && <ManageGlobalNotification globalNotificationId={currentGlobalNotificationId} />
+        }
+      </AdminLayout>
+    </Provider>
+  );
+
+};
+
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const props = await retrieveServerSideProps(context);
+  return props;
+};
+
+
+export default AdminGlobalNotificationNewPage;

+ 45 - 0
packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx

@@ -0,0 +1,45 @@
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
+
+import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
+
+import { retrieveServerSideProps } from '../../../utils/admin-page-util';
+
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
+const UserGroupDetailPage = dynamic(() => import('~/components/Admin/UserGroupDetail/UserGroupDetailPage'), { ssr: false });
+
+
+const AdminUserGroupDetailPage: NextPage<CommonProps> = (props) => {
+  const { t } = useTranslation('admin');
+  useIsMaintenanceMode(props.isMaintenanceMode);
+  const router = useRouter();
+  const { userGroupId } = router.query;
+
+  const title = t('user_group_management.user_group_management');
+  const customTitle = useCustomTitle(props, title);
+
+
+  const currentUserGroupId = Array.isArray(userGroupId) ? userGroupId[0] : userGroupId;
+
+  return (
+    <AdminLayout title={customTitle} componentTitle={title} >
+      {
+        currentUserGroupId != null && router.isReady
+      && <UserGroupDetailPage userGroupId={currentUserGroupId} />
+      }
+    </AdminLayout>
+  );
+};
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const props = await retrieveServerSideProps(context);
+  return props;
+};
+
+
+export default AdminUserGroupDetailPage;

+ 0 - 212
packages/app/src/server/routes/admin.js

@@ -88,212 +88,12 @@ module.exports = function(crowi, app) {
     return pager;
   }
 
-  actions.index = function(req, res) {
-    return res.render('admin/index');
-  };
-
-  // app.get('/admin/app'                  , admin.app.index);
-  actions.app = {};
-  actions.app.index = function(req, res) {
-    return res.render('admin/app');
-  };
-
-  actions.app.settingUpdate = function(req, res) {
-  };
-
-  // app.get('/admin/security'                  , admin.security.index);
-  actions.security = {};
-  actions.security.index = function(req, res) {
-
-    return res.render('admin/security');
-  };
-
-  // app.get('/admin/markdown'                  , admin.markdown.index);
-  actions.markdown = {};
-  actions.markdown.index = function(req, res) {
-    const markdownSetting = configManager.getConfigByPrefix('markdown', 'markdown:');
-
-    return res.render('admin/markdown', {
-      markdownSetting,
-      recommendedWhitelist,
-    });
-  };
-
-  // app.get('/admin/customize' , admin.customize.index);
-  actions.customize = {};
-  actions.customize.index = function(req, res) {
-    const settingForm = configManager.getConfigByPrefix('crowi', 'customize:');
-
-    return res.render('admin/customize', {
-      settingForm,
-    });
-  };
-
-  // app.get('/admin/notification'               , admin.notification.index);
-  actions.notification = {};
-  actions.notification.index = async(req, res) => {
-
-    return res.render('admin/notification');
-  };
-
-  // app.get('/admin/notification/slackAuth'     , admin.notification.slackauth);
-  actions.notification.slackAuth = function(req, res) {
-    const code = req.query.code;
-    const { t } = req;
-
-    if (!code || !slackIntegrationService.isSlackConfigured()) {
-      return res.redirect('/admin/notification');
-    }
-
-    const slack = crowi.slack;
-    slack.getOauthAccessToken(code)
-      .then(async(data) => {
-        debug('oauth response', data);
-
-        try {
-          await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': data.access_token });
-          req.flash('successMessage', [t('message.successfully_connected')]);
-        }
-        catch (err) {
-          req.flash('errorMessage', [t('message.fail_to_save_access_token')]);
-        }
-
-        return res.redirect('/admin/notification');
-      })
-      .catch((err) => {
-        debug('oauth response ERROR', err);
-        req.flash('errorMessage', [t('message.fail_to_fetch_access_token')]);
-        return res.redirect('/admin/notification');
-      });
-  };
-
-  // app.post('/admin/notification/slackSetting/disconnect' , admin.notification.disconnectFromSlack);
-  actions.notification.disconnectFromSlack = async function(req, res) {
-    await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': '' });
-    req.flash('successMessage', [req.t('successfully_disconnected')]);
-
-    return res.redirect('/admin/notification');
-  };
-
-  actions.globalNotification = {};
-  actions.globalNotification.detail = async(req, res) => {
-    const notificationSettingId = req.params.id;
-    let globalNotification;
-
-    if (notificationSettingId) {
-      try {
-        globalNotification = await GlobalNotificationSetting.findOne({ _id: notificationSettingId });
-      }
-      catch (err) {
-        logger.error(`Error in finding a global notification setting with {_id: ${notificationSettingId}}`);
-      }
-    }
-
-    return res.render('admin/global-notification-detail', { globalNotification });
-  };
-
-  actions.search = {};
-  actions.search.index = function(req, res) {
-    return res.render('admin/search', {});
-  };
-
-  actions.user = {};
-  actions.user.index = async function(req, res) {
-    return res.render('admin/users');
-  };
-
-  actions.externalAccount = {};
-  actions.externalAccount.index = function(req, res) {
-    return res.render('admin/external-accounts');
-  };
-
-  actions.slackIntegrationLegacy = {};
-  actions.slackIntegrationLegacy = function(req, res) {
-    return res.render('admin/slack-integration-legacy');
-  };
-
-  actions.slackIntegration = {};
-  actions.slackIntegration = function(req, res) {
-    return res.render('admin/slack-integration');
-  };
-
-  actions.userGroup = {};
-  actions.userGroup.index = function(req, res) {
-    const page = parseInt(req.query.page) || 1;
-    const renderVar = {
-      userGroups: [],
-      userGroupRelations: new Map(),
-      pager: null,
-    };
-
-    UserGroup.findUserGroupsWithPagination({ page })
-      .then((result) => {
-        const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
-        const userGroups = result.docs;
-        renderVar.userGroups = userGroups;
-        renderVar.pager = pager;
-        return userGroups.map((userGroup) => {
-          return new Promise((resolve, reject) => {
-            UserGroupRelation.findAllRelationForUserGroup(userGroup)
-              .then((relations) => {
-                return resolve({
-                  id: userGroup._id,
-                  relatedUsers: relations.map((relation) => {
-                    return relation.relatedUser;
-                  }),
-                });
-              });
-          });
-        });
-      })
-      .then((allRelationsPromise) => {
-        return Promise.all(allRelationsPromise);
-      })
-      .then((relations) => {
-        for (const relation of relations) {
-          renderVar.userGroupRelations[relation.id] = relation.relatedUsers;
-        }
-        debug('in findUserGroupsWithPagination findAllRelationForUserGroupResult', renderVar.userGroupRelations);
-        return res.render('admin/user-groups', renderVar);
-      })
-      .catch((err) => {
-        debug('Error on find all relations', err);
-        return res.json(ApiResponse.error('Error'));
-      });
-  };
-
-  // グループ詳細
-  actions.userGroup.detail = async function(req, res) {
-    const userGroupId = req.params.id;
-    const userGroup = await UserGroup.findOne({ _id: userGroupId }).populate('parent');
-
-    if (userGroup == null) {
-      logger.error('no userGroup is exists. ', userGroupId);
-      return res.redirect('/admin/user-groups');
-    }
-
-    return res.render('admin/user-group-detail', { userGroup });
-  };
-
-  // AuditLog
-  actions.auditLog = {};
-  actions.auditLog.index = (req, res) => {
-    return res.render('admin/audit-log');
-  };
-
   // Importer management
   actions.importer = {};
   actions.importer.api = api;
   api.validators = {};
   api.validators.importer = {};
 
-  actions.importer.index = function(req, res) {
-    const settingForm = configManager.getConfigByPrefix('crowi', 'importer:');
-    return res.render('admin/importer', {
-      settingForm,
-    });
-  };
-
   api.validators.importer.esa = function() {
     const validator = [
       check('importer:esa:team_name').not().isEmpty().withMessage('Error. Empty esa:team_name'),
@@ -316,10 +116,6 @@ module.exports = function(crowi, app) {
   actions.export.api = api;
   api.validators.export = {};
 
-  actions.export.index = (req, res) => {
-    return res.render('admin/export');
-  };
-
   api.validators.export.download = function() {
     const validator = [
       // https://regex101.com/r/mD4eZs/6
@@ -507,13 +303,5 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success());
   };
 
-  /*
-  * Use AdminNotFoundPage component instead
-  */
-  // actions.notFound = {};
-  // actions.notFound.index = function(req, res) {
-  //   return res.render('admin/not_found');
-  // };
-
   return actions;
 };

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

@@ -68,6 +68,7 @@ module.exports = (crowi, app, isInstalled) => {
 
   router.use('/personal-setting', require('./personal-setting')(crowi));
 
+  router.use('/user-group-relations', require('./user-group-relation')(crowi));
   router.use('/user-group-relations', require('./user-group-relation')(crowi));
 
   router.use('/statistics', require('./statistics')(crowi));

+ 30 - 12
packages/app/src/server/routes/apiv3/notification-setting.js

@@ -26,12 +26,12 @@ const validator = {
   globalNotification: [
     body('triggerPath').isString().trim().not()
       .isEmpty(),
-    body('notifyToType').isString().trim().isIn(['mail', 'slack']),
+    body('notifyType').isString().trim().isIn(['mail', 'slack']),
     body('toEmail').trim().custom((value, { req }) => {
-      return (req.body.notifyToType === 'mail') ? (!!value && value.match(/.+@.+\..+/)) : true;
+      return (req.body.notifyType === 'mail') ? (!!value && value.match(/.+@.+\..+/)) : true;
     }),
     body('slackChannels').trim().custom((value, { req }) => {
-      return (req.body.notifyToType === 'slack') ? !!value : true;
+      return (req.body.notifyType === 'slack') ? !!value : true;
     }),
   ],
   notifyForPageGrant: [
@@ -72,7 +72,7 @@ const validator = {
  *      GlobalNotificationParams:
  *        type: object
  *        properties:
- *          notifyToType:
+ *          notifyType:
  *            type: string
  *            description: What is type for notify
  *          toEmail:
@@ -232,6 +232,24 @@ module.exports = (crowi) => {
 
   });
 
+
+  router.get('/global-notification/:id', loginRequiredStrictly, adminRequired, validator.globalNotification, async(req, res) => {
+
+    const notificationSettingId = req.params.id;
+    let globalNotification;
+
+    if (notificationSettingId) {
+      try {
+        globalNotification = await GlobalNotificationSetting.findOne({ _id: notificationSettingId });
+      }
+      catch (err) {
+        logger.error(`Error in finding a global notification setting with {_id: ${notificationSettingId}}`);
+      }
+    }
+
+    return res.apiv3({ globalNotification });
+  });
+
   /**
    * @swagger
    *
@@ -260,16 +278,16 @@ module.exports = (crowi) => {
   router.post('/global-notification', loginRequiredStrictly, adminRequired, addActivity, validator.globalNotification, apiV3FormValidator, async(req, res) => {
 
     const {
-      notifyToType, toEmail, slackChannels, triggerPath, triggerEvents,
+      notifyType, toEmail, slackChannels, triggerPath, triggerEvents,
     } = req.body;
 
     let notification;
 
-    if (notifyToType === GlobalNotificationSetting.TYPE.MAIL) {
+    if (notifyType === GlobalNotificationSetting.TYPE.MAIL) {
       notification = new GlobalNotificationMailSetting(crowi);
       notification.toEmail = toEmail;
     }
-    if (notifyToType === GlobalNotificationSetting.TYPE.SLACK) {
+    if (notifyType === GlobalNotificationSetting.TYPE.SLACK) {
       notification = new GlobalNotificationSlackSetting(crowi);
       notification.slackChannels = slackChannels;
     }
@@ -328,7 +346,7 @@ module.exports = (crowi) => {
   router.put('/global-notification/:id', loginRequiredStrictly, adminRequired, addActivity, validator.globalNotification, apiV3FormValidator, async(req, res) => {
     const { id } = req.params;
     const {
-      notifyToType, toEmail, slackChannels, triggerPath, triggerEvents,
+      notifyType, toEmail, slackChannels, triggerPath, triggerEvents,
     } = req.body;
 
     const models = {
@@ -342,7 +360,7 @@ module.exports = (crowi) => {
 
       // when switching from one type to another,
       // remove toEmail from slack setting and slackChannels from mail setting
-      if (setting.__t !== notifyToType) {
+      if (setting.__t !== notifyType) {
         setting = models[setting.__t].hydrate(setting);
         setting.toEmail = undefined;
         setting.slackChannels = undefined;
@@ -350,16 +368,16 @@ module.exports = (crowi) => {
         setting = setting.toObject();
       }
 
-      if (notifyToType === GlobalNotificationSetting.TYPE.MAIL) {
+      if (notifyType === GlobalNotificationSetting.TYPE.MAIL) {
         setting = GlobalNotificationMailSetting.hydrate(setting);
         setting.toEmail = toEmail;
       }
-      if (notifyToType === GlobalNotificationSetting.TYPE.SLACK) {
+      if (notifyType === GlobalNotificationSetting.TYPE.SLACK) {
         setting = GlobalNotificationSlackSetting.hydrate(setting);
         setting.slackChannels = slackChannels;
       }
 
-      setting.__t = notifyToType;
+      setting.__t = notifyType;
       setting.triggerPath = triggerPath;
       setting.triggerEvents = triggerEvents || [];
 

+ 33 - 20
packages/app/src/server/routes/apiv3/user-activation.ts

@@ -5,8 +5,12 @@ import { format, subSeconds } from 'date-fns';
 import { body, validationResult } from 'express-validator';
 
 import UserRegistrationOrder from '~/server/models/user-registration-order';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:routes:apiv3:user-activation');
 
 const PASSOWRD_MINIMUM_NUMBER = 8;
+
 // validation rules for complete registration form
 export const completeRegistrationRules = () => {
   return [
@@ -72,11 +76,16 @@ export const completeRegistrationAction = (crowi) => {
       return res.apiv3Err(new ErrorV3('You have been logged in', 'registration-failed'), 403);
     }
 
-    // config で closed ならさよなら
+    // error when registration is not allowed
     if (configManager.getConfig('crowi', 'security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED) {
       return res.apiv3Err(new ErrorV3('Registration closed', 'registration-failed'), 403);
     }
 
+    // error when email authentication is disabled
+    if (configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled') !== true) {
+      return res.apiv3Err(new ErrorV3('Email authentication configuration is disabled', 'registration-failed'), 403);
+    }
+
     const { userRegistrationOrder } = req;
     const registerForm = req.body;
 
@@ -107,21 +116,23 @@ export const completeRegistrationAction = (crowi) => {
         return res.apiv3Err(new ErrorV3(errorMessage, 'registration-failed'), 403);
       }
 
-      if (configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled') === true) {
-        User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
-          if (err) {
-            if (err.name === 'UserUpperLimitException') {
-              errorMessage = req.t('message.can_not_register_maximum_number_of_users');
-            }
-            else {
-              errorMessage = req.t('message.failed_to_register');
-            }
-            return res.apiv3Err(new ErrorV3(errorMessage, 'registration-failed'), 403);
+      User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
+        if (err) {
+          if (err.name === 'UserUpperLimitException') {
+            errorMessage = req.t('message.can_not_register_maximum_number_of_users');
+          }
+          else {
+            errorMessage = req.t('message.failed_to_register');
           }
+          return res.apiv3Err(new ErrorV3(errorMessage, 'registration-failed'), 403);
+        }
 
-          userRegistrationOrder.revokeOneTimeToken();
+        userRegistrationOrder.revokeOneTimeToken();
 
-          if (configManager.getConfig('crowi', 'security:registrationMode') !== aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
+        if (configManager.getConfig('crowi', 'security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
+          const isMailerSetup = mailService.isMailerSetup ?? false;
+
+          if (isMailerSetup) {
             const admins = await User.findAdmins();
             const appTitle = appService.getAppTitle();
             const template = path.join(crowi.localeDir, 'en_US/admin/userWaitingActivation.txt');
@@ -129,14 +140,16 @@ export const completeRegistrationAction = (crowi) => {
 
             sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url);
           }
+          // This 'completeRegistrationAction' should not be able to be called if the email settings is not set up in the first place.
+          // So this method dows not stop processing as an error, but only displays a warning. -- 2022.11.01 Yuki Takei
+          else {
+            logger.warn('E-mail Settings must be set up.');
+          }
+        }
 
-          req.flash('successMessage', req.t('message.successfully_created', { username }));
-          res.apiv3({ status: 'ok' });
-        });
-      }
-      else {
-        return res.apiv3Err(new ErrorV3('Email authentication configuration is disabled', 'registration-failed'), 403);
-      }
+        req.flash('successMessage', req.t('message.successfully_created', { username }));
+        res.apiv3({ status: 'ok' });
+      });
     });
   };
 };

+ 2 - 41
packages/app/src/server/routes/index.js

@@ -84,12 +84,11 @@ module.exports = function(crowi, app) {
 
   app.get('/register'                 , applicationInstalled, login.preLogin, login.register);
 
-  // load before "/admin/*"
-  app.get('/admin/export/:fileName'             , loginRequiredStrictly , adminRequired ,admin.export.api.validators.export.download(), admin.export.download);
+  // NOTE: get method "/admin/export/:fileName" should be loaded before "/admin/*"
+  app.get('/admin/export/:fileName'   , loginRequiredStrictly , adminRequired ,admin.export.api.validators.export.download(), admin.export.download);
 
   app.get('/admin/*'                    , applicationInstalled, loginRequiredStrictly , adminRequired , next.delegateToNext);
   app.get('/admin'                    , applicationInstalled, loginRequiredStrictly , adminRequired , next.delegateToNext);
-  // app.get('/admin/app'                , applicationInstalled, loginRequiredStrictly , adminRequired , admin.app.index);
 
   // installer
   if (!isInstalled) {
@@ -112,40 +111,7 @@ module.exports = function(crowi, app) {
 
   app.post('/_api/login/testLdap'    , loginRequiredStrictly , loginFormValidator.loginRules() , loginFormValidator.loginValidation , loginPassport.testLdapCredentials);
 
-  // security admin
-  // app.get('/admin/security'          , loginRequiredStrictly , adminRequired , admin.security.index);
-
-  // markdown admin
-  // app.get('/admin/markdown'          , loginRequiredStrictly , adminRequired , admin.markdown.index);
-
-  // customize admin
-  // app.get('/admin/customize'         , loginRequiredStrictly , adminRequired , admin.customize.index);
-
-  // search admin
-  // app.get('/admin/search'            , loginRequiredStrictly , adminRequired , admin.search.index);
-
-  // notification admin
-  // app.get('/admin/notification'                         , loginRequiredStrictly , adminRequired , admin.notification.index);
-  // app.get('/admin/notification/slackAuth'               , loginRequiredStrictly , adminRequired , admin.notification.slackAuth);
-  // app.get('/admin/notification/slackSetting/disconnect' , loginRequiredStrictly , adminRequired , admin.notification.disconnectFromSlack);
-  // app.get('/admin/global-notification/new'              , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
-  // app.get('/admin/global-notification/:id'              , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
-  // app.get('/admin/slack-integration-legacy'             , loginRequiredStrictly , adminRequired,  admin.slackIntegrationLegacy);
-  // app.get('/admin/slack-integration'                    , loginRequiredStrictly , adminRequired,  admin.slackIntegration);
-
-  // app.get('/admin/users'                                , loginRequiredStrictly , adminRequired , admin.user.index);
-
-  // app.get('/admin/users/external-accounts'              , loginRequiredStrictly , adminRequired , admin.externalAccount.index);
-
-  // user-groups admin
-  // app.get('/admin/user-groups'                          , loginRequiredStrictly, adminRequired, admin.userGroup.index);
-  // app.get('/admin/user-group-detail/:id'                , loginRequiredStrictly, adminRequired, admin.userGroup.detail);
-
-  // auditLog admin
-  // app.get('/admin/audit-log'                            , loginRequiredStrictly, adminRequired, admin.auditLog.index);
-
   // importer management for admin
-  // app.get('/admin/importer'                     , loginRequiredStrictly , adminRequired , admin.importer.index);
   app.post('/_api/admin/settings/importerEsa'   , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.importer.api.validators.importer.esa(),admin.api.importerSettingEsa);
   app.post('/_api/admin/settings/importerQiita' , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.importer.api.validators.importer.qiita(), admin.api.importerSettingQiita);
   app.post('/_api/admin/import/esa'             , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.importDataFromEsa);
@@ -153,11 +119,6 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/import/qiita'           , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.importDataFromQiita);
   app.post('/_api/admin/import/testQiitaAPI'    , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.testQiitaAPI);
 
-  // export management for admin
-  // app.get('/admin/export'                       , loginRequiredStrictly , adminRequired ,admin.export.index);
-
-  // app.get('/admin/*'                            , loginRequiredStrictly ,adminRequired, admin.notFound.index);
-
   /*
    * Routes below are unavailable when maintenance mode
    */

+ 9 - 1
packages/app/src/server/routes/login.js

@@ -1,5 +1,6 @@
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
+
 // disable all of linting
 // because this file is a deprecated legacy of Crowi
 
@@ -147,6 +148,13 @@ module.exports = function(crowi, app) {
         return res.apiv3Err(errors, 400);
       }
 
+      const registrationMode = configManager.getConfig('crowi', 'security:registrationMode');
+      const isMailerSetup = mailService.isMailerSetup ?? false;
+
+      if (!isMailerSetup && registrationMode === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
+        return res.apiv3Err(['email_settings_is_not_setup'], 403);
+      }
+
       User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
         if (err) {
           const errors = [];
@@ -159,7 +167,7 @@ module.exports = function(crowi, app) {
           return res.apiv3Err(errors, 405);
         }
 
-        if (configManager.getConfig('crowi', 'security:registrationMode') !== aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
+        if (registrationMode === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
           // send mail asynchronous
           sendEmailToAllAdmins(userData);
         }

+ 0 - 15
packages/app/src/server/views/admin/app.html

@@ -1,15 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('app_settings')) }}{% endblock %}
-
-{% block head_warn_alert_siteurl_undefined %} {# remove including block for './widget/alert_siteurl_undefined.html' #}
-{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('app_settings') }}</h1>
-{% endblock %}
-
-
-{% block content_main %}
-  <div id="admin-app"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/audit-log.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('audit_log_management.audit_log')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('audit_log_management.audit_log') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id ="admin-audit-log"></div>
-{% endblock content_main %}

+ 0 - 14
packages/app/src/server/views/admin/customize.html

@@ -1,14 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('admin:customize')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('admin:customize') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="grw-hljs-container-for-demo">
-  {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}
-</div>
-<div id="admin-customize" class="admin-customize"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/export.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('export_archive_data')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('export_archive_data') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-export-page" class="admin-export"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/external-accounts.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('external_account_management')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('user_management.user_management') }} / {{ t('external_account_management') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-external-account-setting"></div>
-{% endblock content_main %}

+ 0 - 12
packages/app/src/server/views/admin/global-notification-detail.html

@@ -1,12 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('external_notification.external_notification')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('external_notification.external_notification') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-global-notification-setting"
-    data-global-notification="{{ globalNotification|json }}"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/importer.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('importer_management.import_data')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('importer_management.import_data') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-importer" class="admin-importer"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/index.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('Wiki Management Home Page')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title"> {{ t('Wiki Management Home Page') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-home"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/markdown.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('markdown_settings.markdown_settings')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('markdown_settings.markdown_settings') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-markdown-setting"></div>
-{% endblock content_main %}

+ 0 - 8
packages/app/src/server/views/admin/not_found.html

@@ -1,8 +0,0 @@
-<!-- Use AdminNotFoundPage component instead -->
-<!-- {% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('not_found_page.page_not_exist')) }}{% endblock %}
-
-{% block content_main %}
-<h1 class="title">{{ t('not_found_page.page_not_exist') }}</h1>
-{% endblock content_main %} -->

+ 0 - 11
packages/app/src/server/views/admin/notification.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('external_notification.external_notification')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('external_notification.external_notification') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-notification-setting" class="admin-notification"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/search.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('full_text_search_management.full_text_search_management')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('full_text_search_management.full_text_search_management') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-  <div id ="admin-full-text-search-management"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/security.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('security_settings.security_settings')) }} · {% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('security_settings.security_settings') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-security-setting" class="admin-security"></div>
-{% endblock content_main %}

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

@@ -1,12 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('slack_integration_legacy.slack_integration_legacy')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('slack_integration_legacy.slack_integration_legacy') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-slack-integration-legacy" class="admin-slack-integration-legacy"></div>
-{% endblock content_main %}
-

+ 0 - 11
packages/app/src/server/views/admin/slack-integration.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('slack_integration.slack_integration')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('slack_integration.slack_integration') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id="admin-slack-integration" class="admin-slack-integration"></div>
-{% endblock content_main %}

+ 0 - 15
packages/app/src/server/views/admin/user-group-detail.html

@@ -1,15 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('user_group_management.user_group_management') + '/' + userGroup.name) | preventXss }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('user_group_management.user_group_management') + '/' + userGroup.name | preventXss }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div
-  id="admin-user-group-detail"
-  data-user-group="{{ userGroup|json }}"
->
-</div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/user-groups.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('user_group_management.user_group_management')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('user_group_management.user_group_management') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id ="admin-user-group-page"></div>
-{% endblock content_main %}

+ 0 - 11
packages/app/src/server/views/admin/users.html

@@ -1,11 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('user_management.user_management')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('user_management.user_management') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div id ="admin-user-page" class="admin-user-page"></div>
-{% endblock content_main %}

+ 0 - 37
packages/app/src/server/views/layout/admin.html

@@ -1,37 +0,0 @@
-{% extends './layout.html' %}
-
-{% block html_base_css %}admin-page{% endblock %}
-
-{% block html_head_loading_app %}
-<script src="{{ webpack_asset('js/admin.js') }}" defer></script>
-{% endblock %}
-
-{% block layout_main %}
-
-{% block content_header_wrapper %}
-<header class="container-fluid py-0">
-  {% block content_header %}
-    <div id="grw-subnav-container"></div>
-  {% endblock %}
-</header>
-{% endblock %}
-
-<div id="main" class="main">
-
-  <div class="container-fluid">
-    <div class="row">
-      <div class="col-lg-3" id="admin-navigation"></div>
-      <div class="col-lg-9">
-        {% block content_main_before %}
-        {% endblock %}
-
-        {% block content_main %}
-        {% endblock content_main %}
-
-        {% block content_main_after %}
-        {% endblock %}
-      </div>
-    </div>
-  </div>
-</div><!-- /.main -->
-{% endblock %} {# layout_main #}

+ 37 - 0
packages/app/src/stores/global-notification.ts

@@ -0,0 +1,37 @@
+import { SWRResponseWithUtils, withUtils } from '@growi/core';
+import useSWRImmutable from 'swr/immutable';
+
+
+import { apiv3Get, apiv3Put } from '../client/util/apiv3-client';
+
+
+type Util = {
+  update(updateData: any): Promise<void>
+};
+
+
+// TODO: typescriptize
+export const useSWRxGlobalNotification = (globalNotificationId: string): SWRResponseWithUtils<Util, any, Error> => {
+  const swrResult = useSWRImmutable(
+    globalNotificationId != null ? `/notification-setting/global-notification/${globalNotificationId}` : null,
+    endpoint => apiv3Get(endpoint).then((response) => {
+      return {
+        globalNotification: response.data.globalNotification,
+      };
+    }),
+  );
+
+
+  const update = async(updateData) => {
+    const { data } = swrResult;
+
+    if (data == null) {
+      return;
+    }
+
+    // invoke API
+    await apiv3Put(`/notification-setting/global-notification/${globalNotificationId}`, updateData);
+  };
+
+  return withUtils<Util, any, Error>(swrResult, { update });
+};

+ 0 - 35
packages/app/src/styles/_handsontable.scss

@@ -1,35 +0,0 @@
-.handsontable {
-  .handsontableInput {
-    max-width: 290px !important;
-  }
-
-  td {
-    word-break: break-all;
-  }
-}
-
-.handsontable-modal.grw-modal-expanded {
-  // expand .hot-table-container (with flexbox)
-  .hot-table-container {
-    flex: 1;
-  }
-}
-
-// Prevent handsontable/handsontable #2937 (Manual column resize does not work when handsontable is loaded inside Bootstrap 3.0 Modal)
-// see https://github.com/handsontable/handsontable/issues/2937#issuecomment-287390111
-.modal.in .modal-dialog.handsontable-modal {
-  transform: none;
-
-  .data-import-button {
-    position: relative;
-    padding-right: 35px;
-    padding-left: 10px;
-
-    i:before {
-      position: absolute;
-      top: 6px;
-      right: 8px;
-      font-size: 20px;
-    }
-  }
-}

+ 1 - 1
packages/app/src/styles/style-app.scss

@@ -44,7 +44,7 @@
 @import 'editor-attachment';
 @import 'editor-navbar';
 @import 'page-content-footer';
-@import 'handsontable';
+// @import 'handsontable';
 @import 'layout';
 @import 'login';
 @import 'me';

+ 19 - 0
packages/app/src/styles/style-next.scss

@@ -147,3 +147,22 @@
   }
 
 }
+
+// Prevent handsontable/handsontable #2937 (Manual column resize does not work when handsontable is loaded inside Bootstrap 3.0 Modal)
+// see https://github.com/handsontable/handsontable/issues/2937#issuecomment-287390111
+.modal.in .modal-dialog.handsontable-modal {
+  transform: none;
+
+  .data-import-button {
+    position: relative;
+    padding-right: 35px;
+    padding-left: 10px;
+
+    i:before {
+      position: absolute;
+      top: 6px;
+      right: 8px;
+      font-size: 20px;
+    }
+  }
+}