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

Merge branch 'support/apply-nextjs-2' into imprv/1000059-show-customize-page

kaori 3 лет назад
Родитель
Сommit
a773222c9d
100 измененных файлов с 1163 добавлено и 741 удалено
  1. 5 0
      packages/app/next.config.js
  2. 4 0
      packages/app/package.json
  3. 167 0
      packages/app/public/static/locales/en_US/admin/admin.json
  4. 167 0
      packages/app/public/static/locales/ja_JP/admin/admin.json
  5. 167 0
      packages/app/public/static/locales/zh_CN/admin/admin.json
  6. 1 1
      packages/app/resource/locales/en_US/welcome.md
  7. 1 1
      packages/app/resource/locales/ja_JP/welcome.md
  8. 1 1
      packages/app/resource/locales/zh_CN/welcome.md
  9. 0 5
      packages/app/src/client/base.jsx
  10. 0 4
      packages/app/src/client/boot.js
  11. 1 2
      packages/app/src/client/services/page-operation.ts
  12. 0 73
      packages/app/src/client/util/color-scheme.js
  13. 1 1
      packages/app/src/components/Admin/AuditLog/ActivityTable.tsx
  14. 1 1
      packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx
  15. 2 2
      packages/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx
  16. 5 6
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  17. 4 5
      packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  18. 17 16
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  19. 0 1
      packages/app/src/components/Admin/ManageExternalAccount.jsx
  20. 23 24
      packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  21. 1 1
      packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  22. 1 1
      packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  23. 7 11
      packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  24. 2 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  25. 2 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  26. 0 49
      packages/app/src/components/BasicLayout.tsx
  27. 30 7
      packages/app/src/components/Drawio.tsx
  28. 1 1
      packages/app/src/components/InAppNotification/InAppNotificationElm.tsx
  29. 2 1
      packages/app/src/components/InAppNotification/InAppNotificationList.tsx
  30. 1 1
      packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  31. 23 23
      packages/app/src/components/InstallerForm.jsx
  32. 5 1
      packages/app/src/components/Layout/AdminLayout.tsx
  33. 56 0
      packages/app/src/components/Layout/BasicLayout.tsx
  34. 48 0
      packages/app/src/components/Layout/RawLayout.tsx
  35. 12 28
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  36. 4 6
      packages/app/src/components/Navbar/GlobalSearch.tsx
  37. 1 2
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  38. 7 13
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  39. 1 2
      packages/app/src/components/Page/DisplaySwitcher.tsx
  40. 1 1
      packages/app/src/components/Page/RevisionRenderer.tsx
  41. 5 3
      packages/app/src/components/PageAlert/PageStaleAlert.tsx
  42. 1 1
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  43. 24 22
      packages/app/src/components/PageComment/CommentEditor.tsx
  44. 2 1
      packages/app/src/components/PageContentFooter.tsx
  45. 4 18
      packages/app/src/components/PageCreateModal.jsx
  46. 1 2
      packages/app/src/components/PageDeleteModal.tsx
  47. 4 3
      packages/app/src/components/PageEditor/EmojiPicker.tsx
  48. 15 12
      packages/app/src/components/PageEditor/Preview.tsx
  49. 4 4
      packages/app/src/components/PageList/PageList.tsx
  50. 3 3
      packages/app/src/components/PageList/PageListItemL.tsx
  51. 0 30
      packages/app/src/components/RawLayout.tsx
  52. 18 0
      packages/app/src/components/ReactMarkdownComponents/Header.module.scss
  53. 4 1
      packages/app/src/components/ReactMarkdownComponents/Header.tsx
  54. 1 2
      packages/app/src/components/SavePageControls/GrantSelector.tsx
  55. 2 3
      packages/app/src/components/SearchForm.tsx
  56. 2 2
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  57. 4 4
      packages/app/src/components/SearchPage/SearchResultList.tsx
  58. 3 4
      packages/app/src/components/SearchPage2/SearchPageBase.tsx
  59. 5 6
      packages/app/src/components/SearchTypeahead.tsx
  60. 10 10
      packages/app/src/components/Sidebar.module.scss
  61. 50 48
      packages/app/src/components/Sidebar.tsx
  62. 1 2
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  63. 1 1
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  64. 1 4
      packages/app/src/components/Sidebar/RecentChanges.tsx
  65. 1 2
      packages/app/src/components/SubscribeButton.tsx
  66. 12 4
      packages/app/src/components/SystemVersion.tsx
  67. 7 7
      packages/app/src/components/Theme/ThemeAntarctic.module.scss
  68. 8 0
      packages/app/src/components/Theme/ThemeAntarctic.tsx
  69. 7 7
      packages/app/src/components/Theme/ThemeBlackboard.module.scss
  70. 8 0
      packages/app/src/components/Theme/ThemeBlackboard.tsx
  71. 7 7
      packages/app/src/components/Theme/ThemeChristmas.module.scss
  72. 8 0
      packages/app/src/components/Theme/ThemeChristmas.tsx
  73. 9 9
      packages/app/src/components/Theme/ThemeDefault.module.scss
  74. 8 0
      packages/app/src/components/Theme/ThemeDefault.tsx
  75. 0 0
      packages/app/src/components/Theme/ThemeFireRed.module.scss
  76. 0 0
      packages/app/src/components/Theme/ThemeFuture.module.scss
  77. 0 0
      packages/app/src/components/Theme/ThemeHalloween.module.scss
  78. 0 0
      packages/app/src/components/Theme/ThemeHufflepuff.module.scss
  79. 0 0
      packages/app/src/components/Theme/ThemeIsland.module.scss
  80. 0 0
      packages/app/src/components/Theme/ThemeJadeGreen.module.scss
  81. 0 0
      packages/app/src/components/Theme/ThemeKibela.module.scss
  82. 0 0
      packages/app/src/components/Theme/ThemeMonoBlue.module.scss
  83. 0 0
      packages/app/src/components/Theme/ThemeNature.module.scss
  84. 0 0
      packages/app/src/components/Theme/ThemeSpring.module.scss
  85. 0 0
      packages/app/src/components/Theme/ThemeWood.module.scss
  86. 12 0
      packages/app/src/components/Theme/utils/ThemeInjector.tsx
  87. 31 0
      packages/app/src/components/Theme/utils/ThemeProvider.tsx
  88. 66 27
      packages/app/src/interfaces/activity.ts
  89. 1 11
      packages/app/src/interfaces/attachment.ts
  90. 2 2
      packages/app/src/interfaces/comment.ts
  91. 0 19
      packages/app/src/interfaces/common.ts
  92. 2 1
      packages/app/src/interfaces/external-account.ts
  93. 1 6
      packages/app/src/interfaces/global.ts
  94. 8 0
      packages/app/src/interfaces/graph-viewer.ts
  95. 1 1
      packages/app/src/interfaces/page-grant.ts
  96. 10 116
      packages/app/src/interfaces/page.ts
  97. 3 25
      packages/app/src/interfaces/revision.ts
  98. 10 6
      packages/app/src/interfaces/search.ts
  99. 1 6
      packages/app/src/interfaces/subscription.ts
  100. 3 6
      packages/app/src/interfaces/tag.ts

+ 5 - 0
packages/app/next.config.js

@@ -22,8 +22,13 @@ const setupWithTM = () => {
     'unified',
     'comma-separated-tokens',
     'decode-named-character-reference',
+    'html-void-elements',
+    'property-information',
     'space-separated-tokens',
     'trim-lines',
+    'web-namespaces',
+    'vfile',
+    'zwitch',
     'emoticon',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
   ];

+ 4 - 0
packages/app/package.json

@@ -126,6 +126,7 @@
     "multer-autoreap": "^1.0.3",
     "next": "^12.1.6",
     "next-i18next": "^11.0.0",
+    "next-themes": "^0.2.0",
     "next-transpile-modules": "^9.0.0",
     "nocache": "^3.0.1",
     "nodemailer": "^6.6.2",
@@ -153,6 +154,8 @@
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
+    "rehype-raw": "^6.1.1",
+    "rehype-sanitize": "^5.0.1",
     "rehype-slug": "^5.0.1",
     "rehype-toc": "^3.0.2",
     "remark-breaks": "^3.0.2",
@@ -200,6 +203,7 @@
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-regex": "^1.8.0",
+    "font-awesome": "^4.7.0",
     "handsontable": "=6.2.2",
     "i18next-hmr": "^1.7.7",
     "jquery-slimscroll": "^1.3.8",

+ 167 - 0
packages/app/public/static/locales/en_US/admin/admin.json

@@ -537,5 +537,172 @@
     "available_action_list_explain": "List of actions that can be search / view in the Audit Log",
     "action_list": "Action List",
     "disable_mode_explain": "Audit log is currently disabled. To enable it, set the environment variable <code>AUDIT_LOG_ENABLED</code> to true."
+  },
+  "audit_log_action_category": {
+    "Page": "Page",
+    "Comment": "Comment",
+    "Tag": "Tag",
+    "Attachment": "Attachment",
+    "ShareLink": "ShareLink",
+    "Search": "Search",
+    "User": "User",
+    "Admin": "Admin"
+  },
+  "audit_log_action": {
+    "USER_REGISTRATION_SUCCESS": "User Creation",
+    "USER_LOGIN_WITH_LOCAL": "Login with ID/Password",
+    "USER_LOGIN_WITH_LDAP": "Login with LDAP",
+    "USER_LOGIN_WITH_GOOGLE": "Login with Google",
+    "USER_LOGIN_WITH_GITHUB": "Login with GitHub",
+    "USER_LOGIN_WITH_TWITTER": "Login with Twitter",
+    "USER_LOGIN_WITH_OIDC": "Login with OIDC",
+    "USER_LOGIN_WITH_SAML": "Login with SAML",
+    "USER_LOGIN_WITH_BASIC": "Login with BASIC",
+    "USER_LOGIN_FAILURE": "Login failure",
+    "USER_LOGOUT": "Logout",
+    "USER_FOGOT_PASSWORD": "Request password reset",
+    "USER_RESET_PASSWORD": "Reset password",
+    "USER_PERSONAL_SETTINGS_UPDATE": "User personal settings update",
+    "USER_IMAGE_TYPE_UPDATE": "User image type update",
+    "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP account associate",
+    "USER_LDAP_ACCOUNT_DISCONNECT": "LDAP account disconnect",
+    "USER_PASSWORD_UPDATE": "Password update",
+    "USER_API_TOKEN_UPDATE": "API Token update",
+    "USER_EDITOR_SETTINGS_UPDATE": "Editor settings update",
+    "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "In-App Notification settings update",
+    "PAGE_VIEW": "Page view",
+    "PAGE_USER_HOME_VIEW": "Page view (User home)",
+    "PAGE_FORBIDDEN": "Page view (Fobidden page)",
+    "PAGE_NOT_FOUND": "Page view (Not found page)",
+    "PAGE_NOT_CREATABLE": "Page view(Not Creatable page)",
+    "PAGE_LIKE": "Like",
+    "PAGE_UNLIKE": "Unlike",
+    "PAGE_BOOKMARK": "Bookmark",
+    "PAGE_UNBOOKMARK": "Unbookmark",
+    "PAGE_CREATE": "Create page",
+    "PAGE_UPDATE": "Update page",
+    "PAGE_RENAME": "Rename page",
+    "PAGE_DUPLICATE": "Duplicate page",
+    "PAGE_DELETE": "Delete page",
+    "PAGE_DELETE_COMPLETELY": "Delete completely page",
+    "PAGE_REVERT": "Revert page",
+    "PAGE_EMPTY_TRASH": "Empty trash",
+    "PAGE_SUBSCRIBE": "Subscribe page",
+    "PAGE_UNSUBSCRIBE": "Unsubscribe page",
+    "PAGE_EXPORT": "Export page",
+    "TAG_UPDATE": "Update tags",
+    "IN_APP_NOTIFICATION_ALL_STATUSES_OPEN": "Read all In-App notifications",
+    "COMMENT_CREATE": "Create comment",
+    "COMMENT_UPDATE": "Update comment",
+    "COMMENT_REMOVE": "Delete comment",
+    "SHARE_LINK_CREATE": "Create Share link",
+    "SHARE_LINK_DELETE": "Delete Share link",
+    "SHARE_LINK_DELETE_BY_PAGE": "Remove all shared links on the page",
+    "SHARE_LINK_ALL_DELETE": "Delete all share link",
+    "SHARE_LINK_PAGE_VIEW": "Page view(Share link)",
+    "SHARE_LINK_EXPIRED_PAGE_VIEW": "Page view(Expired share link)",
+    "SHARE_LINK_NOT_FOUND": "Page view (Not found share link)",
+    "ATTACHMENT_ADD": "Add Attachment",
+    "ATTACHMENT_REMOVE": "Delete Attachment",
+    "ACTION_ATTACHMENT_DOWNLOAD": "Download Attachment",
+    "SEARCH_PAGE": "Page Search",
+    "SEARCH_PAGE_VIEW": "Page view(Search results page)",
+    "ADMIN_APP_SETTING_UPDATE": "Update App Settings",
+    "ADMIN_SITE_URL_UPDATE": "Update Site URL Settings",
+    "ADMIN_MAIL_SMTP_UPDATE": "Update E-mail(SMTP) Settings",
+    "ADMIN_MAIL_SES_UPDATE": "Update E-mail(SES) Settings",
+    "ADMIN_MAIL_TEST_SUBMIT" : "Send test mail",
+    "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "Update File Upload Settings",
+    "ADMIN_PLUGIN_UPDATE": "Update Plugin Settings",
+    "ADMIN_MAINTENANCEMODE_ENABLED": "Enable Maintenance Mode",
+    "ADMIN_MAINTENANCEMODE_DISABLED": "Disabled Maintenance Mode",
+    "ADMIN_SECURITY_SETTINGS_UPDATE": "Update Security Settings",
+    "ADMIN_PERMIT_SHARE_LINK": "Enable Share Link",
+    "ADMIN_REJECT_SHARE_LINK": "Disable Share Link",
+    "ADMIN_AUTH_ID_PASS_ENABLED": "Enable ID/Password auth",
+    "ADMIN_AUTH_ID_PASS_DISABLED": "Disable ID/Password auth",
+    "ADMIN_AUTH_ID_PASS_UPDATE": "Update ID/Password auth settings",
+    "ADMIN_AUTH_LDAP_ENABLED": "Enable LDAP auth",
+    "ADMIN_AUTH_LDAP_DISABLED": "Disable LDAP auth",
+    "ADMIN_AUTH_LDAP_UPDATE": "Update LDAP auth settings",
+    "ADMIN_AUTH_SAML_ENABLED": "Enable SAML auth",
+    "ADMIN_AUTH_SAML_DISABLED": "Disable SAML auth",
+    "ADMIN_AUTH_SAML_UPDATE": "Update SAML auth settings",
+    "ADMIN_AUTH_OIDC_ENABLED": "Enable OIDC auth",
+    "ADMIN_AUTH_OIDC_DISABLED": "Disable OIDC auth",
+    "ADMIN_AUTH_OIDC_UPDATE": "Update OIDC settings",
+    "ADMIN_AUTH_BASIC_ENABLED": "Enable BASIC auth",
+    "ADMIN_AUTH_BASIC_DISABLED": "Disable BASIC auth",
+    "ADMIN_AUTH_BASIC_UPDATE": "Update BASIC auth settings",
+    "ADMIN_AUTH_GOOGLE_ENABLED": "Enable Google auth",
+    "ADMIN_AUTH_GOOGLE_DISABLED": "Disable Google auth",
+    "ADMIN_AUTH_GOOGLE_UPDATE": "Update Google auth settings",
+    "ADMIN_AUTH_GITHUB_ENABLED": "Enable GitHub auth",
+    "ADMIN_AUTH_GITHUB_DISABLED": "Disable GitHub auth",
+    "ADMIN_AUTH_GITHUB_UPDATE": "Update GitHub auth settings",
+    "ADMIN_AUTH_TWITTER_ENABLED": "Enable Twitter auth",
+    "ADMIN_AUTH_TWITTER_DISABLED": "Disable Twitter auth",
+    "ADMIN_AUTH_TWITTER_UPDATE": "Update Twitter auth settings",
+    "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "Update Link Break settings",
+    "ADMIN_MARKDOWN_INDENT_UPDATE": "Update Indent settings",
+    "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "Update Presentation setting",
+    "ADMIN_MARKDOWN_XSS_UPDATE": "Update prevent XSS settings",
+    "ADMIN_LAYOUT_UPDATE": "Update Layout",
+    "ADMIN_THEME_UPDATE": "Update Theme",
+    "ADMIN_SIDEBAR_UPDATE": "Update Default Sidebar mode",
+    "ADMIN_FUNCTION_UPDATE": "Update Function",
+    "ADMIN_CODE_HIGHLIGHT_UPDATE": "Update Code Highlight",
+    "ADMIN_CUSTOM_TITLE_UPDATE": "Update Custom Title",
+    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "Update Custom HTML header",
+    "ADMIN_CUSTOM_CSS_UPDATE": "Update Custom CSS",
+    "ADMIN_CUSTOM_SCRIPT_UPDATE": "Update Custom script",
+    "ADMIN_ARCHIVE_DATA_UPLOAD": "Upload Archived Data",
+    "ADMIN_GROWI_DATA_IMPORTED": "Import Archived Data",
+    "ADMIN_UPLOADED_GROWI_DATA_DISCARDED": "Discard Archived Data",
+    "ADMIN_ESA_DATA_IMPORTED": "Import from esa.io",
+    "ADMIN_ESA_DATA_UPDATED": "Update esa.io import settings",
+    "ADMIN_CONNECTION_TEST_OF_ESA_DATA": "Test connection to esa",
+    "ADMIN_QIITA_DATA_IMPORTED": "Import from Qiita:Team",
+    "ADMIN_QIITA_DATA_UPDATED": "Update Qiita:Team import settings",
+    "ADMIN_CONNECTION_TEST_OF_QIITA_DATA": "Test connection to Qiita:Team",
+    "ADMIN_ARCHIVE_DATA_CREATE": "Create Archived Data",
+    "ADMIN_ARCHIVE_DATA_DOWNLOAD": "Download Archive Data",
+    "ADMIN_ARCHIVE_DATA_DELETE": "Delete Archive Data",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_ADD": "Add User trigger notification notification settings",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_DELETE": "Delete User trigger notification notification settings",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD": "Add Grobal notification settings",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE": "Update Grobal notification settings",
+    "ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE": "Update Grobal notification permissions",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED": "Enable Grobal notification settings",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED": "Disable Grobal notification settings",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE": "Delete Grobal notification settings",
+    "ADMIN_SLACK_WORKSPACE_CREATE": "Add Slack Workspace",
+    "ADMIN_SLACK_WORKSPACE_DELETE": "Delete Slack Workspace",
+    "ADMIN_SLACK_BOT_TYPE_UPDATE": "Change Slack bot type",
+    "ADMIN_SLACK_BOT_TYPE_DELETE": "Delete Slack bot type",
+    "ADMIN_SLACK_ACCESS_TOKEN_REGENERATE": "Regenerate Slack access token",
+    "ADMIN_SLACK_MAKE_APP_PRIMARY": "Make the Slack bot primary",
+    "ADMIN_SLACK_PERMISSION_UPDATE": "Update Slack bot permissions",
+    "ADMIN_SLACK_PROXY_URI_UPDATE": "Update Proxy URL for Custom bot with proxy",
+    "ADMIN_SLACK_RELATION_TEST": "Test connection to slack bot",
+    "ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE": "Update Slack bot without proxy settings",
+    "ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE": "Update Slack bot without proxy permissions",
+    "ADMIN_SLACK_WITHOUT_PROXY_TEST": "Test connection to Slack bot without proxy",
+    "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "Update Slack Incoming Webhooks configuration",
+    "ADMIN_USERS_INVITE": "User Invitation",
+    "ADMIN_USERS_PASSWORD_RESET": "Reset user password",
+    "ADMIN_USERS_ACTIVATE": "Activate user",
+    "ADMIN_USERS_DEACTIVATE": "Deactivate user",
+    "ADMIN_USERS_GIVE_ADMIN": "Give admin access",
+    "ADMIN_USERS_REMOVE_ADMIN": "Remove admin access",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "Resend invitation email",
+    "ADMIN_USERS_REMOVE": "Remove user",
+    "ADMIN_USER_GROUP_CREATE": "Create User Group",
+    "ADMIN_USER_GROUP_UPDATE": "Update User Group",
+    "ADMIN_USER_GROUP_DELETE": "Delete User Group",
+    "ADMIN_USER_GROUP_ADD_USER": "Add User to User Group",
+    "ADMIN_SEARCH_CONNECTION": "Attempting to reconnect to Elasticsearch",
+    "ADMIN_SEARCH_INDICES_NORMALIZE": "Normalize of Elasticsearch indexes",
+    "ADMIN_SEARCH_INDICES_REBUILD": "Rebuild Elasticsearch indexes"
   }
 }

+ 167 - 0
packages/app/public/static/locales/ja_JP/admin/admin.json

@@ -536,5 +536,172 @@
     "available_action_list_explain": "監査ログで 検索 / 表示 可能なアクション一覧です",
     "action_list": "アクション一覧",
     "disable_mode_explain": "現在、監査ログは無効になっています。有効にする場合は環境変数 <code>AUDIT_LOG_ENABLED</code> を true に設定してください。"
+  },
+  "audit_log_action_category": {
+    "Page": "ページ",
+    "Comment": "コメント",
+    "Tag": "タグ",
+    "Attachment": "添付ファイル",
+    "ShareLink": "シェアリンク",
+    "Search": "検索",
+    "User": "ユーザー",
+    "Admin": "管理者ユーザー"
+  },
+  "audit_log_action": {
+    "USER_REGISTRATION_SUCCESS": "ユーザー作成",
+    "USER_LOGIN_WITH_LOCAL": "ID/Password 認証でログイン",
+    "USER_LOGIN_WITH_LDAP": "LDAP 認証でログイン",
+    "USER_LOGIN_WITH_GOOGLE": "Google 認証でログイン",
+    "USER_LOGIN_WITH_GITHUB": "GitHub 認証でログイン",
+    "USER_LOGIN_WITH_TWITTER": "Twitter 認証でログイン",
+    "USER_LOGIN_WITH_OIDC": "OIDC 認証でログイン",
+    "USER_LOGIN_WITH_SAML": "SAML 認証でログイン",
+    "USER_LOGIN_WITH_BASIC": "BASIC 認証でログイン",
+    "USER_LOGIN_FAILURE": "ログイン失敗",
+    "USER_LOGOUT": "ログアウト",
+    "USER_FOGOT_PASSWORD": "パスワードリセットのリクエスト",
+    "USER_RESET_PASSWORD": "パスワードのリセット",
+    "USER_PERSONAL_SETTINGS_UPDATE": "ユーザーの基本情報の更新",
+    "USER_IMAGE_TYPE_UPDATE": "プロフィール画像の変更",
+    "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP アカウントの追加",
+    "USER_LDAP_ACCOUNT_DISCONNECT": "LDAP アカウントの切断",
+    "USER_PASSWORD_UPDATE": "パスワードの変更",
+    "USER_API_TOKEN_UPDATE": "API トークンの更新",
+    "USER_EDITOR_SETTINGS_UPDATE": "エディター設定の更新",
+    "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "アプリ内通知設定の更新",
+    "PAGE_VIEW": "ページ閲覧",
+    "PAGE_USER_HOME_VIEW": "ページ閲覧(ユーザーホーム)",
+    "PAGE_FORBIDDEN": "ページ閲覧(fobiddenページ)",
+    "PAGE_NOT_FOUND": "ページ閲覧(Not Found ページ)",
+    "PAGE_NOT_CREATABLE": "ページ閲覧(Not Creatable ページ)",
+    "PAGE_LIKE": "いいね",
+    "PAGE_UNLIKE": "いいねの解除",
+    "PAGE_BOOKMARK": "ブックマーク",
+    "PAGE_UNBOOKMARK": "ブックマークの解除",
+    "PAGE_CREATE": "ページの作成",
+    "PAGE_UPDATE": "ページの更新",
+    "PAGE_RENAME": "ページのリネーム",
+    "PAGE_DUPLICATE": "ページの複製",
+    "PAGE_DELETE": "ページの削除",
+    "PAGE_DELETE_COMPLETELY": "ページの完全削除",
+    "PAGE_REVERT": "ページを元に戻す",
+    "PAGE_EMPTY_TRASH": "ゴミ箱を空にする",
+    "PAGE_SUBSCRIBE": "ページをサブスクライブ",
+    "PAGE_UNSUBSCRIBE": "ページをアンサブスクライブ",
+    "PAGE_EXPORT": "マークダウン形式でページをエクスポート",
+    "TAG_UPDATE": "タグの更新",
+    "IN_APP_NOTIFICATION_ALL_STATUSES_OPEN": "アプリ内通知を全て既読",
+    "COMMENT_CREATE": "コメントの作成",
+    "COMMENT_UPDATE": "コメントの更新",
+    "COMMENT_REMOVE": "コメントの削除",
+    "SHARE_LINK_CREATE": "共有リンクの作成",
+    "SHARE_LINK_DELETE": "共有リンクの削除",
+    "SHARE_LINK_DELETE_BY_PAGE": "ページ内の共有リンクを全て削除",
+    "SHARE_LINK_ALL_DELETE": "共有リンクを全て削除",
+    "SHARE_LINK_PAGE_VIEW": "ページ閲覧(共有リンク)",
+    "SHARE_LINK_EXPIRED_PAGE_VIEW": "ページ閲覧(期限切れの共有リンク)",
+    "SHARE_LINK_NOT_FOUND": "ページ閲覧(存在しない共有リンク)",
+    "ATTACHMENT_ADD": "添付データの追加",
+    "ATTACHMENT_REMOVE": "添付データの削除",
+    "ACTION_ATTACHMENT_DOWNLOAD": "添付データのダウンロード",
+    "SEARCH_PAGE": "ページの検索",
+    "SEARCH_PAGE_VIEW": "ページ閲覧(検索結果ページ)",
+    "ADMIN_APP_SETTING_UPDATE": "アプリ設定の更新",
+    "ADMIN_SITE_URL_UPDATE": "サイトURL設定の更新",
+    "ADMIN_MAIL_SMTP_UPDATE": "メール設定(SMTP)の更新",
+    "ADMIN_MAIL_SES_UPDATE": "メール設定(SES)の更新",
+    "ADMIN_MAIL_TEST_SUBMIT" : "テストメールの送信",
+    "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "ファイルアップロード設定の更新",
+    "ADMIN_PLUGIN_UPDATE": "プラグイン設定の更新",
+    "ADMIN_MAINTENANCEMODE_ENABLED": "メンテナンスモードの開始",
+    "ADMIN_MAINTENANCEMODE_DISABLED": "メンテナンスモードの終了",
+    "ADMIN_SECURITY_SETTINGS_UPDATE": "セキュリティ設定の更新",
+    "ADMIN_PERMIT_SHARE_LINK": "共有リンクの有効化",
+    "ADMIN_REJECT_SHARE_LINK": "共有リンクの無効化",
+    "ADMIN_AUTH_ID_PASS_ENABLED": "ID/Password 認証を有効",
+    "ADMIN_AUTH_ID_PASS_DISABLED": "ID/Password 認証を無効",
+    "ADMIN_AUTH_ID_PASS_UPDATE": "ID/Password 認証設定を更新",
+    "ADMIN_AUTH_LDAP_ENABLED": "LDAP 認証を有効",
+    "ADMIN_AUTH_LDAP_DISABLED": "LDAP 認証を無効",
+    "ADMIN_AUTH_LDAP_UPDATE": "LDAP 認証設定を更新",
+    "ADMIN_AUTH_SAML_ENABLED": "SAML 認証を有効",
+    "ADMIN_AUTH_SAML_DISABLED": "SAML 認証を無効",
+    "ADMIN_AUTH_SAML_UPDATE": "SAML 認証設定を更新",
+    "ADMIN_AUTH_OIDC_ENABLED": "OIDC 認証を有効",
+    "ADMIN_AUTH_OIDC_DISABLED": "OIDC 認証を無効",
+    "ADMIN_AUTH_OIDC_UPDATE": "OIDC 認証設定の更新",
+    "ADMIN_AUTH_BASIC_ENABLED": "BASIC 認証の有効",
+    "ADMIN_AUTH_BASIC_DISABLED": "BASIC 認証の無効",
+    "ADMIN_AUTH_BASIC_UPDATE": "BASIC 認証設定の更新",
+    "ADMIN_AUTH_GOOGLE_ENABLED": "Google 認証の有効",
+    "ADMIN_AUTH_GOOGLE_DISABLED": "Google 認証の無効",
+    "ADMIN_AUTH_GOOGLE_UPDATE": "Google 認証設定の更新",
+    "ADMIN_AUTH_GITHUB_ENABLED": "GitHub 認証の有効",
+    "ADMIN_AUTH_GITHUB_DISABLED": "GitHub 認証の無効",
+    "ADMIN_AUTH_GITHUB_UPDATE": "GitHub 認証設定の更新",
+    "ADMIN_AUTH_TWITTER_ENABLED": "Twitter 認証の有効",
+    "ADMIN_AUTH_TWITTER_DISABLED": "Twitter 認証の無効",
+    "ADMIN_AUTH_TWITTER_UPDATE": "Twitter 認証設定の更新",
+    "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "Line Break 設定の更新",
+    "ADMIN_MARKDOWN_INDENT_UPDATE": "インデント設定の更新",
+    "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "プレゼンテーション設定の更新",
+    "ADMIN_MARKDOWN_XSS_UPDATE": "XSS 対策設定の更新",
+    "ADMIN_LAYOUT_UPDATE": "レイアウト設定の更新",
+    "ADMIN_THEME_UPDATE": "テーマ設定の更新",
+    "ADMIN_SIDEBAR_UPDATE": "デフォルトのサイドバーモードの設定の更新",
+    "ADMIN_FUNCTION_UPDATE": "機能設定の更新",
+    "ADMIN_CODE_HIGHLIGHT_UPDATE": "コードハイライト設定の更新",
+    "ADMIN_CUSTOM_TITLE_UPDATE": "カスタムタイトル設定の更新",
+    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "カスタム HTML Header 設定の更新",
+    "ADMIN_CUSTOM_CSS_UPDATE": "カスタム CSS 設定の更新",
+    "ADMIN_CUSTOM_SCRIPT_UPDATE": "カスタムスクリプト設定の更新",
+    "ADMIN_ARCHIVE_DATA_UPLOAD": "アーカイブデータのアップロード",
+    "ADMIN_GROWI_DATA_IMPORTED": "アーカイブデータのインポート",
+    "ADMIN_UPLOADED_GROWI_DATA_DISCARDED": "アーカイブデータの破棄",
+    "ADMIN_ESA_DATA_IMPORTED": "esa.io からインポート",
+    "ADMIN_ESA_DATA_UPDATED": "esa.io のインポート設定の更新",
+    "ADMIN_CONNECTION_TEST_OF_ESA_DATA": "esa.io の接続テスト",
+    "ADMIN_QIITA_DATA_IMPORTED": "Qiita:Team からのインポート",
+    "ADMIN_QIITA_DATA_UPDATED": "Qiita:Team のインポート設定の更新",
+    "ADMIN_CONNECTION_TEST_OF_QIITA_DATA": "Qiita:Team の接続テスト",
+    "ADMIN_ARCHIVE_DATA_CREATE": "アーカイブデータの作成",
+    "ADMIN_ARCHIVE_DATA_DOWNLOAD": "アーカイブデータのダウンロード",
+    "ADMIN_ARCHIVE_DATA_DELETE": "アーカイブデータの削除",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_ADD": "User trigger notification の通知設定の追加",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_DELETE": "User trigger notification の通知設定の削除",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD": "Grobal notification の設定の追加",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE": "Grobal notification の設定の更新",
+    "ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE": "Grobal notification の権限の更新",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED": "Grobal notification の設定の有効",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED": "Grobal notification の設定の無効",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE": "Grobal notification の設定の削除",
+    "ADMIN_SLACK_WORKSPACE_CREATE": "Slack ワークスペースの追加",
+    "ADMIN_SLACK_WORKSPACE_DELETE": "Slack ワークスペースの削除",
+    "ADMIN_SLACK_BOT_TYPE_UPDATE": "Slack bot タイプの変更",
+    "ADMIN_SLACK_BOT_TYPE_DELETE": "Slack bot タイプの削除",
+    "ADMIN_SLACK_ACCESS_TOKEN_REGENERATE": "Slack アクセストークンの再発行",
+    "ADMIN_SLACK_MAKE_APP_PRIMARY": "Slack bot をプライマリーにする",
+    "ADMIN_SLACK_PERMISSION_UPDATE": "Slack bot の権限の更新",
+    "ADMIN_SLACK_PROXY_URI_UPDATE": "Custom bot with proxy の Proxy URL の更新",
+    "ADMIN_SLACK_RELATION_TEST": "Slack bot の接続テスト",
+    "ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE": "Slack bot without proxy の設定の更新",
+    "ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE": "Slack bot without proxy の権限の更新",
+    "ADMIN_SLACK_WITHOUT_PROXY_TEST": "Slack bot without proxy の接続テスト",
+    "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "Slack Incoming Webhooks の設定の更新",
+    "ADMIN_USERS_INVITE": "ユーザーの招待",
+    "ADMIN_USERS_PASSWORD_RESET": "ユーザーのパスワードをリセット",
+    "ADMIN_USERS_ACTIVATE": "ユーザーを承認する",
+    "ADMIN_USERS_DEACTIVATE": "ユーザーを停止する",
+    "ADMIN_USERS_GIVE_ADMIN": "管理者にする",
+    "ADMIN_USERS_REMOVE_ADMIN": "管理者から外す",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "招待メールの再送信",
+    "ADMIN_USERS_REMOVE": "ユーザーの削除",
+    "ADMIN_USER_GROUP_CREATE": "ユーザーグループの作成",
+    "ADMIN_USER_GROUP_UPDATE": "ユーザーグループの更新",
+    "ADMIN_USER_GROUP_DELETE": "ユーザーグループの削除",
+    "ADMIN_USER_GROUP_ADD_USER": "ユーザーグループにユーザーを追加",
+    "ADMIN_SEARCH_CONNECTION": "Elasticsearch の再接続の試行",
+    "ADMIN_SEARCH_INDICES_NORMALIZE": "Elasticsearch のインデックスの正規化",
+    "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド"
   }
 }

+ 167 - 0
packages/app/public/static/locales/zh_CN/admin/admin.json

@@ -546,5 +546,172 @@
     "available_action_list_explain": "可以在审计日志中 搜索/查看 的行动列表",
     "action_list": "行动清单",
     "disable_mode_explain": "审计日志当前已禁用。 要启用它,请将环境变量 <code>AUDIT_LOG_ENABLED</code> 设置为 true。"
+  },
+  "audit_log_action_category": {
+    "Page": "页面",
+    "Comment": "评论",
+    "Tag": "标签",
+    "Attachment": "附件",
+    "ShareLink": "分享链接",
+    "Search": "搜索",
+    "User": "用户",
+    "Admin": "管理"
+  },
+  "audit_log_action": {
+    "USER_REGISTRATION_SUCCESS": "用户创建",
+    "USER_LOGIN_WITH_LOCAL": "用ID/密码登录",
+    "USER_LOGIN_WITH_LDAP": "使用 LDAP 登录",
+    "USER_LOGIN_WITH_GOOGLE": "用谷歌登录",
+    "USER_LOGIN_WITH_GITHUB": "使用 GitHub 登录",
+    "USER_LOGIN_WITH_TWITTER": "使用 Twitter 登录",
+    "USER_LOGIN_WITH_OIDC": "使用 OIDC 登录",
+    "USER_LOGIN_WITH_SAML": "使用 SAML 登录",
+    "USER_LOGIN_WITH_BASIC": "使用 BASIC 登录",
+    "USER_LOGIN_FAILURE": "登录失败",
+    "USER_LOGOUT": "注销",
+    "USER_FOGOT_PASSWORD": "要求重置密码",
+    "USER_RESET_PASSWORD": "重置密码",
+    "USER_PERSONAL_SETTINGS_UPDATE": "用户个人设置更新",
+    "USER_IMAGE_TYPE_UPDATE": "用户图片类型更新",
+    "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP 帐户关联",
+    "USER_LDAP_ACCOUNT_DISCONNECT": "LDAP 账户断开连接",
+    "USER_PASSWORD_UPDATE": "密码更新",
+    "USER_API_TOKEN_UPDATE": "API 令牌更新",
+    "USER_EDITOR_SETTINGS_UPDATE": "编辑器设置更新",
+    "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "应用内通知设置更新",
+    "PAGE_VIEW": "页面浏览量",
+    "PAGE_USER_HOME_VIEW": "页面浏览量(用户主页)",
+    "PAGE_FORBIDDEN": "页面浏览量(禁止页面)",
+    "PAGE_NOT_FOUND": "页面查看(未找到页面)",
+    "PAGE_NOT_CREATABLE": "页面浏览量(不可创建页面)",
+    "PAGE_LIKE": "喜欢",
+    "PAGE_UNLIKE": "不喜欢",
+    "PAGE_BOOKMARK": "书签",
+    "PAGE_UNBOOKMARK": "取消书签",
+    "PAGE_CREATE": "创建页面",
+    "PAGE_UPDATE": "更新页面",
+    "PAGE_RENAME": "重命名页面",
+    "PAGE_DUPLICATE": "重复页面",
+    "PAGE_DELETE": "删除页面",
+    "PAGE_DELETE_COMPLETELY": "彻底删除页面",
+    "PAGE_REVERT": "还原页面",
+    "PAGE_EMPTY_TRASH": "清空垃圾箱",
+    "PAGE_SUBSCRIBE": "订阅页面",
+    "PAGE_UNSUBSCRIBE": "退订页面",
+    "PAGE_EXPORT": "导出页面",
+    "TAG_UPDATE": "更新标签",
+    "IN_APP_NOTIFICATION_ALL_STATUSES_OPEN": "读取所有应用内通知",
+    "COMMENT_CREATE": "创建评论",
+    "COMMENT_UPDATE": "更新评论",
+    "COMMENT_REMOVE": "删除评论",
+    "SHARE_LINK_CREATE": "创建分享链接",
+    "SHARE_LINK_DELETE": "删除分享链接",
+    "SHARE_LINK_DELETE_BY_PAGE": "删除页面上的所有共享链接",
+    "SHARE_LINK_ALL_DELETE": "删除所有分享链接",
+    "SHARE_LINK_PAGE_VIEW": "页面浏览量(分享链接)",
+    "SHARE_LINK_EXPIRED_PAGE_VIEW": "页面浏览量(已过期的分享链接)",
+    "SHARE_LINK_NOT_FOUND": "页面浏览量(未找到分享链接)",
+    "ATTACHMENT_ADD": "添加附件",
+    "ATTACHMENT_REMOVE": "删除附件",
+    "ACTION_ATTACHMENT_DOWNLOAD": "下载附件",
+    "SEARCH_PAGE": "页面搜索",
+    "SEARCH_PAGE_VIEW": "页面浏览量(搜索结果页面)",
+    "ADMIN_APP_SETTING_UPDATE": "更新应用设置",
+    "ADMIN_SITE_URL_UPDATE": "更新站点 URL 设置",
+    "ADMIN_MAIL_SMTP_UPDATE": "更新电子邮件(SMTP)设置",
+    "ADMIN_MAIL_SES_UPDATE": "更新电子邮件(SES)设置",
+    "ADMIN_MAIL_TEST_SUBMIT" : "发送测试邮件",
+    "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "更新文件上传设置",
+    "ADMIN_PLUGIN_UPDATE": "更新插件设置",
+    "ADMIN_MAINTENANCEMODE_ENABLED": "启用维护模式",
+    "ADMIN_MAINTENANCEMODE_DISABLED": "禁用维护模式",
+    "ADMIN_SECURITY_SETTINGS_UPDATE": "更新安全设置",
+    "ADMIN_PERMIT_SHARE_LINK": "启用分享链接",
+    "ADMIN_REJECT_SHARE_LINK": "禁用分享链接",
+    "ADMIN_AUTH_ID_PASS_ENABLED": "启用 ID/密码验证",
+    "ADMIN_AUTH_ID_PASS_DISABLED": "禁用 ID/密码验证",
+    "ADMIN_AUTH_ID_PASS_UPDATE": "更新 ID/密码验证设置",
+    "ADMIN_AUTH_LDAP_ENABLED": "启用 LDAP 身份验证",
+    "ADMIN_AUTH_LDAP_DISABLED": "禁用 LDAP 身份验证",
+    "ADMIN_AUTH_LDAP_UPDATE": "更新 LDAP 身份验证设置",
+    "ADMIN_AUTH_SAML_ENABLED": "启用 SAML 身份验证",
+    "ADMIN_AUTH_SAML_DISABLED": "禁用 SAML 身份验证",
+    "ADMIN_AUTH_SAML_UPDATE": "更新 SAML 身份验证设置",
+    "ADMIN_AUTH_OIDC_ENABLED": "启用 OIDC 身份验证",
+    "ADMIN_AUTH_OIDC_DISABLED": "禁用 OIDC 身份验证",
+    "ADMIN_AUTH_OIDC_UPDATE": "更新 OIDC 设置",
+    "ADMIN_AUTH_BASIC_ENABLED": "启用基本身份验证",
+    "ADMIN_AUTH_BASIC_DISABLED": "禁用基本身份验证",
+    "ADMIN_AUTH_BASIC_UPDATE": "更新基本认证设置",
+    "ADMIN_AUTH_GOOGLE_ENABLED": "启用谷歌身份验证",
+    "ADMIN_AUTH_GOOGLE_DISABLED": "禁用谷歌身份验证",
+    "ADMIN_AUTH_GOOGLE_UPDATE": "更新谷歌授权设置",
+    "ADMIN_AUTH_GITHUB_ENABLED": "启用 GitHub 身份验证",
+    "ADMIN_AUTH_GITHUB_DISABLED": "禁用 GitHub 身份验证",
+    "ADMIN_AUTH_GITHUB_UPDATE": "更新 GitHub 授权设置",
+    "ADMIN_AUTH_TWITTER_ENABLED": "启用 Twitter 身份验证",
+    "ADMIN_AUTH_TWITTER_DISABLED": "禁用 Twitter 身份验证",
+    "ADMIN_AUTH_TWITTER_UPDATE": "更新 Twitter 授权设置",
+    "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "更新链接中断设置",
+    "ADMIN_MARKDOWN_INDENT_UPDATE": "更新缩进设置",
+    "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "更新演示设置",
+    "ADMIN_MARKDOWN_XSS_UPDATE": "更新阻止 XSS 设置",
+    "ADMIN_LAYOUT_UPDATE": "更新布局",
+    "ADMIN_THEME_UPDATE": "更新主题",
+    "ADMIN_SIDEBAR_UPDATE": "更新默认的侧边栏模式",
+    "ADMIN_FUNCTION_UPDATE": "更新函数",
+    "ADMIN_CODE_HIGHLIGHT_UPDATE": "更新代码高亮",
+    "ADMIN_CUSTOM_TITLE_UPDATE": "更新自定义标题",
+    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "更新自定义 HTML 标头",
+    "ADMIN_CUSTOM_CSS_UPDATE": "更新自定义 CSS",
+    "ADMIN_CUSTOM_SCRIPT_UPDATE": "更新自定义脚本",
+    "ADMIN_ARCHIVE_DATA_UPLOAD": "上传存档数据",
+    "ADMIN_GROWI_DATA_IMPORTED": "导入存档数据",
+    "ADMIN_UPLOADED_GROWI_DATA_DISCARDED": "丢弃存档数据",
+    "ADMIN_ESA_DATA_IMPORTED": "从 esa.io 导入",
+    "ADMIN_ESA_DATA_UPDATED": "更新 esa.io 导入设置",
+    "ADMIN_CONNECTION_TEST_OF_ESA_DATA": "测试与 esa 的连接",
+    "ADMIN_QIITA_DATA_IMPORTED": "从 Qiita:Team 导入",
+    "ADMIN_QIITA_DATA_UPDATED": "更新 Qiita:团队导入设置",
+    "ADMIN_CONNECTION_TEST_OF_QIITA_DATA": "测试与 Qiita:Team 的连接",
+    "ADMIN_ARCHIVE_DATA_CREATE": "创建归档数据",
+    "ADMIN_ARCHIVE_DATA_DOWNLOAD": "下载存档数据",
+    "ADMIN_ARCHIVE_DATA_DELETE": "删除存档数据",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_ADD": "添加用户触发通知通知设置",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_DELETE": "删除用户触发通知通知设置",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD": "添加全局通知设置",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE": "更新 Grobal 通知设置",
+    "ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE": "更新 Grobal 通知权限",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED": "启用 Grobal 通知设置",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED": "禁用 Grobal 通知设置",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE": "删除 Grobal 通知设置",
+    "ADMIN_SLACK_WORKSPACE_CREATE": "添加松弛工作区",
+    "ADMIN_SLACK_WORKSPACE_DELETE": "删除 Slack 工作区",
+    "ADMIN_SLACK_BOT_TYPE_UPDATE": "更改 Slack 机器人类型",
+    "ADMIN_SLACK_BOT_TYPE_DELETE": "删除 Slack 机器人类型",
+    "ADMIN_SLACK_ACCESS_TOKEN_REGENERATE": "重新生成 Slack 访问令牌",
+    "ADMIN_SLACK_MAKE_APP_PRIMARY": "将 Slack 机器人设为主要",
+    "ADMIN_SLACK_PERMISSION_UPDATE": "更新 Slack 机器人权限",
+    "ADMIN_SLACK_PROXY_URI_UPDATE": "使用代理更新自定义机器人的代理 URL",
+    "ADMIN_SLACK_RELATION_TEST": "测试与 slack 机器人的连接",
+    "ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE": "在没有代理设置的情况下更新 Slack 机器人",
+    "ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE": "更新没有代理权限的 Slack 机器人",
+    "ADMIN_SLACK_WITHOUT_PROXY_TEST": "在没有代理的情况下测试与 Slack 机器人的连接",
+    "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "更新 Slack Incoming Webhooks 配置",
+    "ADMIN_USERS_INVITE": "用户邀请",
+    "ADMIN_USERS_PASSWORD_RESET": "重置用户密码",
+    "ADMIN_USERS_ACTIVATE": "激活用户",
+    "ADMIN_USERS_DEACTIVATE": "停用用户",
+    "ADMIN_USERS_GIVE_ADMIN": "授予管理员访问权限",
+    "ADMIN_USERS_REMOVE_ADMIN": "删除管理员访问权限",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "重发邀请函",
+    "ADMIN_USERS_REMOVE": "删除用户",
+    "ADMIN_USER_GROUP_CREATE": "创建用户组",
+    "ADMIN_USER_GROUP_UPDATE": "更新用户组",
+    "ADMIN_USER_GROUP_DELETE": "删除用户组",
+    "ADMIN_USER_GROUP_ADD_USER": "添加用户到用户组",
+    "ADMIN_SEARCH_CONNECTION": "重试Elasticsearch连接",
+    "ADMIN_SEARCH_INDICES_NORMALIZE": "试图重新连接Elasticsearch",
+    "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引"
   }
 }

+ 1 - 1
packages/app/resource/locales/en_US/welcome.md

@@ -1,7 +1,7 @@
 # :tada: Welcome to GROWI
 
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 GROWI is a Wiki for Individuals and Corporations | A knowledge base tool.
 Knowledge in companies, university laboratories, and clubs can be easily shared and anyone can edit the page.

+ 1 - 1
packages/app/resource/locales/ja_JP/welcome.md

@@ -1,6 +1,6 @@
 # :tada: GROWI へようこそ
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 GROWI は個人・法人向けの Wiki | ナレッジベースツールです。  
 会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。

+ 1 - 1
packages/app/resource/locales/zh_CN/welcome.md

@@ -1,7 +1,7 @@
 # :tada: 欢迎来到GROWI
 
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 公司、大学实验室和俱乐部的知识可以轻松共享,任何人都可以编辑页面。

+ 0 - 5
packages/app/src/client/base.jsx

@@ -8,7 +8,6 @@ import PutbackPageModal from '~/components/PutbackPageModal';
 import ShortcutsModal from '~/components/ShortcutsModal';
 import SystemVersion from '~/components/SystemVersion';
 import InterceptorManager from '~/services/interceptor-manager';
-import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 
 import EmptyTrashModal from '../components/EmptyTrashModal';
@@ -30,10 +29,6 @@ if (!window) {
   window = {};
 }
 
-// setup xss library
-const xss = new Xss();
-window.xss = xss;
-
 window.globalEmitter = new EventEmitter();
 window.interceptorManager = new InterceptorManager();
 

+ 0 - 4
packages/app/src/client/boot.js

@@ -1,9 +1,5 @@
-import {
-  applyColorScheme,
-} from './util/color-scheme';
 import {
   applyOldIos,
 } from './util/old-ios';
 
-applyColorScheme();
 applyOldIos();

+ 1 - 2
packages/app/src/client/services/page-operation.ts

@@ -1,7 +1,6 @@
+import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 
-import { SubscriptionStatusType } from '~/interfaces/subscription';
-
 import { toastError } from '../util/apiNotification';
 import { apiv3Post, apiv3Put } from '../util/apiv3-client';
 

+ 0 - 73
packages/app/src/client/util/color-scheme.js

@@ -1,73 +0,0 @@
-const mediaQueryListForDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
-
-function isUserPreferenceExists() {
-  return localStorage.preferDarkModeByUser != null;
-}
-
-function isPreferedDarkModeByUser() {
-  return localStorage.preferDarkModeByUser === 'true';
-}
-
-function isDarkMode() {
-  if (isUserPreferenceExists()) {
-    return isPreferedDarkModeByUser();
-  }
-  return mediaQueryListForDarkMode.matches;
-}
-
-/**
- * Apply color scheme as 'dark' attribute of <html></html>
- */
-function applyColorScheme() {
-  let isDarkMode = mediaQueryListForDarkMode.matches;
-  if (isUserPreferenceExists()) {
-    isDarkMode = isPreferedDarkModeByUser();
-  }
-
-  // switch to dark mode
-  if (isDarkMode) {
-    document.documentElement.removeAttribute('light');
-    document.documentElement.setAttribute('dark', 'true');
-  }
-  // switch to light mode
-  else {
-    document.documentElement.setAttribute('light', 'true');
-    document.documentElement.removeAttribute('dark');
-  }
-}
-
-/**
- * Remove color scheme preference
- */
-function removeUserPreference() {
-  if (isUserPreferenceExists()) {
-    delete localStorage.removeItem('preferDarkModeByUser');
-  }
-}
-
-/**
- * Set color scheme preference
- * @param {boolean} isDarkMode
- */
-function updateUserPreference(isDarkMode) {
-  // store settings to localStorage
-  localStorage.preferDarkModeByUser = isDarkMode;
-}
-
-/**
- * Set color scheme preference with OS settings
- */
-function updateUserPreferenceWithOsSettings() {
-  localStorage.preferDarkModeByUser = mediaQueryListForDarkMode.matches;
-}
-
-export {
-  mediaQueryListForDarkMode,
-  isUserPreferenceExists,
-  isPreferedDarkModeByUser,
-  isDarkMode,
-  applyColorScheme,
-  removeUserPreference,
-  updateUserPreference,
-  updateUserPreferenceWithOsSettings,
-};

+ 1 - 1
packages/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -34,7 +34,7 @@ export const ActivityTable : FC<Props> = (props: Props) => {
               <tr data-testid="activity-table" key={activity._id}>
                 <td>{activity.snapshot?.username}</td>
                 <td>{formatDate(activity.createdAt)}</td>
-                <td>{activity.action}</td>
+                <td>{t(`admin:audit_log_action.${activity.action}`)}</td>
                 <td>{activity.ip}</td>
                 <td>{activity.endpoint}</td>
               </tr>

+ 1 - 1
packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -45,7 +45,7 @@ export const AuditLogSettings: FC = () => {
       <Collapse isOpen={isExpandActionList}>
         <ul className="list-group">
           { availableActions.map(action => (
-            <li key={action} className="list-group-item">{ action }</li>
+            <li key={action} className="list-group-item">{t(`admin:audit_log_action.${action}`)}</li>
           )) }
         </ul>
       </Collapse>

+ 2 - 2
packages/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx

@@ -91,7 +91,7 @@ export const SelectActionDropdown: FC<Props> = (props: Props) => {
                   defaultChecked
                   onChange={(e) => { multipleActionCheckboxChangedHandler(item.actions, e.target.checked) }}
                 />
-                <label className="form-check-label">{item.actionCategory}</label>
+                <label className="form-check-label">{t(`admin:audit_log_action_category.${item.actionCategory}`)}</label>
               </div>
             </div>
             {
@@ -109,7 +109,7 @@ export const SelectActionDropdown: FC<Props> = (props: Props) => {
                       className="form-check-label"
                       htmlFor={`checkbox${action}`}
                     >
-                      {action}
+                      {t(`admin:audit_log_action.${action}`)}
                     </label>
                   </div>
                 </div>

+ 5 - 6
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -4,14 +4,13 @@ import { useTranslation } from 'next-i18next';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
-import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
-
-const isDarkMode = isDarkModeByUtil();
-const colorText = isDarkMode ? 'dark' : 'light';
+import { useNextThemes } from '~/stores/use-next-themes';
 
 const CustomizeLayoutSetting = (): JSX.Element => {
   const { t } = useTranslation();
 
+  const { resolvedTheme } = useNextThemes();
+
   const [isContainerFluid, setIsContainerFluid] = useState(false);
   const [retrieveError, setRetrieveError] = useState();
 
@@ -54,7 +53,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
                 onClick={() => setIsContainerFluid(false)}
                 role="button"
               >
-                <img src={`/images/customize-settings/default-${colorText}.svg`} />
+                <img src={`/images/customize-settings/default-${resolvedTheme}.svg`} />
                 <div className="card-body text-center">
                   {t('admin:customize_setting.layout_options.default')}
                 </div>
@@ -64,7 +63,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
                 onClick={() => setIsContainerFluid(true)}
                 role="button"
               >
-                <img src={`/images/customize-settings/fluid-${colorText}.svg`} />
+                <img src={`/images/customize-settings/fluid-${resolvedTheme}.svg`} />
                 <div className="card-body  text-center">
                   {t('admin:customize_setting.layout_options.expanded')}
                 </div>

+ 4 - 5
packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -4,8 +4,8 @@ import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
 import { useSWRxSidebarConfig } from '~/stores/ui';
+import { useNextThemes } from '~/stores/use-next-themes';
 
 const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation();
@@ -13,10 +13,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
     update, isSidebarDrawerMode, isSidebarClosedAtDockMode, setIsSidebarDrawerMode, setIsSidebarClosedAtDockMode,
   } = useSWRxSidebarConfig();
 
-  const isDarkMode = isDarkModeByUtil();
-  const colorText = isDarkMode ? 'dark' : 'light';
-  const drawerIconFileName = `/images/customize-settings/drawer-${colorText}.svg`;
-  const dockIconFileName = `/images/customize-settings/dock-${colorText}.svg`;
+  const { resolvedTheme } = useNextThemes();
+  const drawerIconFileName = `/images/customize-settings/drawer-${resolvedTheme}.svg`;
+  const dockIconFileName = `/images/customize-settings/dock-${resolvedTheme}.svg`;
 
   const onClickSubmit = useCallback(async() => {
     try {

+ 17 - 16
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -1,10 +1,11 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import { GrowiThemes } from '~/interfaces/theme';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -12,37 +13,37 @@ import ThemeColorBox from './ThemeColorBox';
 
 /* eslint-disable no-multi-spaces */
 const lightNDarkTheme = [{
-  name: 'default',    bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
+  name: GrowiThemes.DEFAULT,      bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
 }, {
-  name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
+  name: GrowiThemes.MONO_BLUE,    bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
 }, {
-  name: 'hufflepuff',  bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
+  name: GrowiThemes.HUFFLEPUFF,   bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
 }, {
-  name: 'fire-red',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
+  name: GrowiThemes.FIRE_RED,     bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
 }, {
-  name: 'jade-green',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
+  name: GrowiThemes.JADE_GREEN,   bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
 }];
 
 const uniqueTheme = [{
-  name: 'nature',     bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
+  name: GrowiThemes.NATURE,       bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
 }, {
-  name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
+  name: GrowiThemes.WOOD,         bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
 }, {
-  name: 'island',     bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
+  name: GrowiThemes.ISLAND,       bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
 }, {
-  name: 'christmas',  bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
+  name: GrowiThemes.CHRISTMAS,    bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
 }, {
-  name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
+  name: GrowiThemes.ANTARCTIC,    bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
 }, {
-  name: 'spring',     bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
+  name: GrowiThemes.SPRING,       bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
 }, {
-  name: 'future',     bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
+  name: GrowiThemes.FUTURE,       bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
 }, {
-  name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
+  name: GrowiThemes.HALLOWEEN,    bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
 }, {
-  name: 'kibela',  bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
+  name: GrowiThemes.KIBELA,       bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
 }, {
-  name: 'blackboard',  bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
+  name: GrowiThemes.BLACKBOARD,   bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
 }];
 
 

+ 0 - 1
packages/app/src/components/Admin/ManageExternalAccount.jsx

@@ -17,7 +17,6 @@ class ManageExternalAccount extends React.Component {
 
   constructor(props) {
     super(props);
-    this.xss = window.xss;
     this.handleExternalAccountPage = this.handleExternalAccountPage.bind(this);
   }
 

+ 23 - 24
packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -9,7 +9,7 @@ import {
 } from 'reactstrap';
 
 import { IUserGroupHasId } from '~/interfaces/user';
-import { useXss } from '~/stores/xss';
+
 /**
  * Delete User Group Select component
  *
@@ -41,10 +41,13 @@ const actionForPages = {
 };
 
 const UserGroupDeleteModal: FC<Props> = (props: Props) => {
-  const { data: xss } = useXss();
 
   const { t } = useTranslation();
 
+  const {
+    onHide, onDelete, userGroups, deleteUserGroup,
+  } = props;
+
   const availableOptions = useMemo<AvailableOption[]>(() => {
     return [
       {
@@ -69,7 +72,7 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         label: t('admin:user_group_management.delete_modal.transfer_pages'),
       },
     ];
-  }, []);
+  }, [t]);
 
   /*
    * State
@@ -85,14 +88,14 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     setTransferToUserGroupId('');
   }, []);
 
-  const onHide = useCallback(() => {
-    if (props.onHide == null) {
+  const toggleHandler = useCallback(() => {
+    if (onHide == null) {
       return;
     }
 
     resetStates();
-    props.onHide();
-  }, [props.onHide]);
+    onHide();
+  }, [onHide, resetStates]);
 
   const handleActionChange = useCallback((e) => {
     const actionName = e.target.value;
@@ -105,23 +108,22 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   }, []);
 
   const handleSubmit = useCallback((e) => {
-    if (props.onDelete == null || props.deleteUserGroup == null) {
+    if (onDelete == null || deleteUserGroup == null) {
       return;
     }
 
     e.preventDefault();
 
-    props.onDelete(
-      props.deleteUserGroup._id,
+    onDelete(
+      deleteUserGroup._id,
       actionName,
       transferToUserGroupId,
     );
-  }, [props.onDelete, props.deleteUserGroup, actionName, transferToUserGroupId]);
+  }, [onDelete, deleteUserGroup, actionName, transferToUserGroupId]);
 
   const renderPageActionSelector = useCallback(() => {
     const options = availableOptions.map((opt) => {
-      const dataContent = `<i class="icon icon-fw ${opt.iconClass} ${opt.styleClass}"></i> <span class="action-name ${opt.styleClass}">${opt.label}</span>`;
-      return <option key={opt.id} value={opt.actionForPages} data-content={dataContent}>{opt.label}</option>;
+      return <option key={opt.id} value={opt.actionForPages}>{opt.label}</option>;
     });
 
     return (
@@ -133,25 +135,22 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleActionChange}
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
-        {options}
+        {...options}
       </select>
     );
-  }, [handleActionChange, actionName, availableOptions]);
+  }, [availableOptions, actionName, handleActionChange, t]);
 
   const renderGroupSelector = useCallback(() => {
-    const { deleteUserGroup } = props;
-
     if (deleteUserGroup == null) {
       return;
     }
 
-    const groups = props.userGroups.filter((group) => {
+    const groups = userGroups.filter((group) => {
       return group._id !== deleteUserGroup._id;
     });
 
     const options = groups.map((group) => {
-      const dataContent = `<i class="icon icon-fw icon-organization"></i> ${xss.process(group.name)}`;
-      return <option key={group._id} value={group._id} data-content={dataContent}>{xss.process(group.name)}</option>;
+      return <option key={group._id} value={group._id}>{group.name}</option>;
     });
 
     const defaultOptionText = groups.length === 0 ? t('admin:user_group_management.delete_modal.no_groups')
@@ -165,10 +164,10 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleGroupChange}
       >
         <option value="" disabled>{defaultOptionText}</option>
-        {options}
+        {...options}
       </select>
     );
-  }, [actionName, transferToUserGroupId, props.userGroups, props.deleteUserGroup]);
+  }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroupId, handleGroupChange]);
 
   const validateForm = useCallback(() => {
     let isValid = true;
@@ -184,8 +183,8 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   }, [actionName, transferToUserGroupId]);
 
   return (
-    <Modal className="modal-md" isOpen={props.isShow} toggle={onHide}>
-      <ModalHeader tag="h4" toggle={onHide} className="bg-danger text-light">
+    <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>
+      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light">
         <i className="icon icon-fire"></i> {t('admin:user_group_management.delete_modal.header')}
       </ModalHeader>
       <ModalBody>

+ 1 - 1
packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -2,13 +2,13 @@ import React, {
   FC, useState, useEffect, useCallback,
 } from 'react';
 
+import { Ref } from '@growi/core';
 import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import { Ref } from '~/interfaces/common';
 import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 
 type Props = {

+ 1 - 1
packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -1,6 +1,6 @@
 import React, { FC, useState, useCallback } from 'react';
 
-import { useTranslation } from 'next-i18next';
+import { useTranslation } from 'react-i18next';
 
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';

+ 7 - 11
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -1,15 +1,12 @@
 import React, {
-  FC, useState, useCallback, useEffect,
+  FC, useState, useEffect,
 } from 'react';
 
 import dateFnsFormat from 'date-fns/format';
 import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 
-import { CustomWindow } from '~/interfaces/global';
 import { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '~/interfaces/user';
-import Xss from '~/services/xss';
-import { useXss } from '~/stores/xss';
 
 type Props = {
   headerLabel?: TFunctionResult,
@@ -57,7 +54,6 @@ const generateGroupIdToChildGroupsMap = (childUserGroups: IUserGroupHasId[]): Re
 
 
 const UserGroupTable: FC<Props> = (props: Props) => {
-  const { data: xss } = useXss();
   const { t } = useTranslation();
 
   /*
@@ -152,17 +148,17 @@ const UserGroupTable: FC<Props> = (props: Props) => {
               <tr key={group._id}>
                 {props.isAclEnabled
                   ? (
-                    <td><a href={`/admin/user-group-detail/${group._id}`}>{xss?.process(group.name)}</a></td>
+                    <td><a href={`/admin/user-group-detail/${group._id}`}>{group.name}</a></td>
                   )
                   : (
-                    <td>{xss?.process(group.name)}</td>
+                    <td>{group.name}</td>
                   )
                 }
-                <td>{xss?.process(group.description)}</td>
+                <td>{group.description}</td>
                 <td>
                   <ul className="list-inline">
                     {users != null && users.map((user) => {
-                      return <li key={user._id} className="list-inline-item badge badge-pill badge-warning">{xss?.process(user.username)}</li>;
+                      return <li key={user._id} className="list-inline-item badge badge-pill badge-warning">{user.username}</li>;
                     })}
                   </ul>
                 </td>
@@ -173,10 +169,10 @@ const UserGroupTable: FC<Props> = (props: Props) => {
                         <li key={group._id} className="list-inline-item badge badge-success">
                           {props.isAclEnabled
                             ? (
-                              <a href={`/admin/user-group-detail/${group._id}`}>{xss?.process(group.name)}</a>
+                              <a href={`/admin/user-group-detail/${group._id}`}>{group.name}</a>
                             )
                             : (
-                              <p>{xss?.process(group.name)}</p>
+                              <p>{group.name}</p>
                             )
                           }
                         </li>

+ 2 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -9,6 +9,7 @@ import { debounce } from 'throttle-debounce';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import Xss from '~/services/xss';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -25,7 +26,7 @@ class UserGroupUserFormByInput extends React.Component {
       searchError: null,
     };
 
-    this.xss = window.xss;
+    this.xss = new Xss();
 
     this.addUserBySubmit = this.addUserBySubmit.bind(this);
     this.validateForm = this.validateForm.bind(this);

+ 2 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -8,6 +8,7 @@ import { useTranslation } from 'next-i18next';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import Xss from '~/services/xss';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -16,7 +17,7 @@ class UserGroupUserTable extends React.Component {
   constructor(props) {
     super(props);
 
-    this.xss = window.xss;
+    this.xss = new Xss();
 
     this.removeUser = this.removeUser.bind(this);
   }

+ 0 - 49
packages/app/src/components/BasicLayout.tsx

@@ -1,49 +0,0 @@
-import React, { ReactNode } from 'react';
-
-import dynamic from 'next/dynamic';
-
-import { GrowiNavbar } from './Navbar/GrowiNavbar';
-import { RawLayout } from './RawLayout';
-import Sidebar from './Sidebar';
-
-
-type Props = {
-  title: string
-  className?: string,
-  children?: ReactNode
-}
-
-export const BasicLayout = ({ children, title, className }: Props): JSX.Element => {
-
-  // const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
-  // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
-  const GrowiNavbarBottom = dynamic(() => import('./Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
-  const ShortcutsModal = dynamic(() => import('./ShortcutsModal'), { ssr: false });
-  const SystemVersion = dynamic(() => import('./SystemVersion'), { ssr: false });
-
-  return (
-    <>
-      <RawLayout title={title} className={className}>
-        <GrowiNavbar />
-
-        <div className="page-wrapper d-flex d-print-block">
-          <div className="grw-sidebar-wrapper">
-            <Sidebar />
-          </div>
-
-          <div className="flex-fill mw-0">
-            {children}
-          </div>
-        </div>
-
-        <GrowiNavbarBottom />
-      </RawLayout>
-
-      {/* <PageCreateModal /> */}
-      {/* <HotkeysManager /> */}
-
-      <ShortcutsModal />
-      <SystemVersion />
-    </>
-  );
-};

+ 30 - 7
packages/app/src/components/Drawio.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useCallback, useEffect, useMemo, useRef,
+  useCallback, useEffect, useMemo, useRef, useState,
 } from 'react';
 
 import EventEmitter from 'events';
@@ -8,35 +8,55 @@ import { useTranslation } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 
 import { CustomWindow } from '~/interfaces/global';
-import { IGraphViewer } from '~/interfaces/graph-viewer';
+import { IGraphViewer, isGraphViewer } from '~/interfaces/graph-viewer';
 
 import NotAvailableForGuest from './NotAvailableForGuest';
 
 type Props = {
+  GraphViewer: IGraphViewer,
   drawioContent: string,
   rangeLineNumberOfMarkdown: { beginLineNumber: number, endLineNumber: number },
   isPreview?: boolean,
 }
 
+// It calls callback when GraphViewer is not null.
+// eslint-disable-next-line @typescript-eslint/ban-types
+const waitForGraphViewer = async(callback: Function) => {
+  const MAX_WAIT_COUNT = 10; // no reason for 10
+
+  for (let i = 0; i < MAX_WAIT_COUNT; i++) {
+    if (isGraphViewer((window as CustomWindow).GraphViewer)) {
+      callback((window as CustomWindow).GraphViewer);
+      break;
+    }
+    // Sleep 500 ms
+    // eslint-disable-next-line no-await-in-loop
+    await new Promise<void>(r => setTimeout(() => r(), 500));
+  }
+};
+
 const Drawio = (props: Props): JSX.Element => {
 
   const { t } = useTranslation();
 
+  // Wrap with a function since GraphViewer is a function.
+  // This applies when call setGraphViewer as well.
+  const [GraphViewer, setGraphViewer] = useState<IGraphViewer | undefined>(() => (window as CustomWindow).GraphViewer);
+
   const { drawioContent, rangeLineNumberOfMarkdown, isPreview } = props;
 
   // const { open: openDrawioModal } = useDrawioModalForPage();
 
   const drawioContainerRef = useRef<HTMLDivElement>(null);
 
-  const globalEmitter: EventEmitter = useMemo(() => (window as CustomWindow).globalEmitter, []);
-  const GraphViewer: IGraphViewer = useMemo(() => (window as CustomWindow).GraphViewer, []);
+  const globalEmitter: EventEmitter = (window as CustomWindow).globalEmitter;
 
   const editButtonClickHandler = useCallback(() => {
     const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
     globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
   }, [rangeLineNumberOfMarkdown, globalEmitter]);
 
-  const renderDrawio = useCallback(() => {
+  const renderDrawio = useCallback((GraphViewer: IGraphViewer) => {
     if (drawioContainerRef.current == null) {
       return;
     }
@@ -51,16 +71,19 @@ const Drawio = (props: Props): JSX.Element => {
         GraphViewer.createViewerForElement(div);
       }
     }
-  }, [GraphViewer]);
+  }, [drawioContainerRef]);
 
   const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
 
   useEffect(() => {
     if (GraphViewer == null) {
+      waitForGraphViewer((gv: IGraphViewer) => {
+        setGraphViewer(() => gv);
+      });
       return;
     }
 
-    renderDrawioWithDebounce();
+    renderDrawioWithDebounce(GraphViewer);
   }, [renderDrawioWithDebounce, GraphViewer]);
 
   return (

+ 1 - 1
packages/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -2,12 +2,12 @@ import React, {
   FC, useRef,
 } from 'react';
 
+import { HasObjectId } from '@growi/core';
 import { UserPicture } from '@growi/ui';
 import { DropdownItem } from 'reactstrap';
 
 import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { HasObjectId } from '~/interfaces/has-object-id';
 import { IInAppNotification, InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 
 // Change the display for each targetmodel

+ 2 - 1
packages/app/src/components/InAppNotification/InAppNotificationList.tsx

@@ -1,7 +1,8 @@
 import React, { FC } from 'react';
 
+import { HasObjectId } from '@growi/core';
+
 import { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
-import { HasObjectId } from '~/interfaces/has-object-id';
 
 import InAppNotificationElm from './InAppNotificationElm';
 

+ 1 - 1
packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -2,10 +2,10 @@ import React, {
   forwardRef, ForwardRefRenderFunction, useImperativeHandle,
 } from 'react';
 
+import { HasObjectId } from '@growi/core';
 import { PagePathLabel } from '@growi/ui';
 
 import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
-import { HasObjectId } from '~/interfaces/has-object-id';
 import { IInAppNotification } from '~/interfaces/in-app-notification';
 
 import { parseSnapshot } from '../../../models/serializers/in-app-notification-snapshot/page';

+ 23 - 23
packages/app/src/components/InstallerForm.jsx

@@ -1,10 +1,10 @@
 import React from 'react';
 
 import i18next from 'i18next';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import { localeMetadatas } from '~/client/util/i18n';
+// import { localeMetadatas } from '~/client/util/i18n';
 import { useCsrfToken } from '~/stores/context';
 
 class InstallerForm extends React.Component {
@@ -17,31 +17,31 @@ class InstallerForm extends React.Component {
       isSubmittingDisabled: false,
       selectedLang: {},
     };
-    // this.checkUserName = this.checkUserName.bind(this);
+    this.checkUserName = this.checkUserName.bind(this);
 
     this.submitHandler = this.submitHandler.bind(this);
   }
 
-  UNSAFE_componentWillMount() {
-    const meta = localeMetadatas.find(v => v.id === i18next.language);
-    if (meta == null) {
-      return this.setState({ selectedLang: localeMetadatas[0] });
-    }
-    this.setState({ selectedLang: meta });
-  }
-
-  // checkUserName(event) {
-  //   const axios = require('axios').create({
-  //     headers: {
-  //       'Content-Type': 'application/json',
-  //       'X-Requested-With': 'XMLHttpRequest',
-  //     },
-  //     responseType: 'json',
-  //   });
-  //   axios.get('/_api/v3/check-username', { params: { username: event.target.value } })
-  //     .then((res) => { return this.setState({ isValidUserName: res.data.valid }) });
+  // UNSAFE_componentWillMount() {
+  //   const meta = localeMetadatas.find(v => v.id === i18next.language);
+  //   if (meta == null) {
+  //     return this.setState({ selectedLang: localeMetadatas[0] });
+  //   }
+  //   this.setState({ selectedLang: meta });
   // }
 
+  checkUserName(event) {
+    const axios = require('axios').create({
+      headers: {
+        'Content-Type': 'application/json',
+        'X-Requested-With': 'XMLHttpRequest',
+      },
+      responseType: 'json',
+    });
+    axios.get('/_api/v3/check-username', { params: { username: event.target.value } })
+      .then((res) => { return this.setState({ isValidUserName: res.data.valid }) });
+  }
+
   changeLanguage(meta) {
     i18next.changeLanguage(meta.id);
     this.setState({ selectedLang: meta });
@@ -97,7 +97,7 @@ class InstallerForm extends React.Component {
                   value={this.state.selectedLang.id}
                   name="registerForm[app:globalLang]"
                 />
-                <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
+                {/* <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
                   {
                     localeMetadatas.map(meta => (
                       <button
@@ -111,7 +111,7 @@ class InstallerForm extends React.Component {
                       </button>
                     ))
                   }
-                </div>
+                </div> */}
               </div>
             </div>
 

+ 5 - 1
packages/app/src/components/AdminLayout.tsx → packages/app/src/components/Layout/AdminLayout.tsx

@@ -3,7 +3,8 @@ import React, { ReactNode } from 'react';
 import dynamic from 'next/dynamic';
 import { Provider } from 'unstated';
 
-import { GrowiNavbar } from './Navbar/GrowiNavbar';
+import { GrowiNavbar } from '../Navbar/GrowiNavbar';
+
 import { RawLayout } from './RawLayout';
 
 // import { injectableContainers } from '~/client/admin';
@@ -25,6 +26,7 @@ const AdminLayout = ({
 }: Props): JSX.Element => {
 
   const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
+  const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
 
   return (
     <RawLayout title={title}>
@@ -48,6 +50,8 @@ const AdminLayout = ({
           </div>
         </div>
       </div>
+
+      <SystemVersion />
     </RawLayout>
   );
 };

+ 56 - 0
packages/app/src/components/Layout/BasicLayout.tsx

@@ -0,0 +1,56 @@
+import React, { ReactNode } from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { GrowiNavbar } from '../Navbar/GrowiNavbar';
+import Sidebar from '../Sidebar';
+
+import { RawLayout } from './RawLayout';
+
+
+type Props = {
+  title: string
+  className?: string,
+  children?: ReactNode
+}
+
+export const BasicLayout = ({ children, title, className }: Props): JSX.Element => {
+
+  // const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
+  // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
+  const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
+  const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
+  const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
+  // Page modals
+  const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
+  const PageDuplicateModal = dynamic(() => import('../PageDuplicateModal'), { ssr: false });
+  const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
+  const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
+
+  return (
+    <RawLayout title={title} className={className}>
+      <GrowiNavbar />
+
+      <div className="page-wrapper d-flex d-print-block">
+        <div className="grw-sidebar-wrapper">
+          <Sidebar />
+        </div>
+
+        <div className="flex-fill mw-0">
+          {children}
+        </div>
+      </div>
+
+      <GrowiNavbarBottom />
+
+      <PageCreateModal />
+      <PageDuplicateModal />
+      <PageDeleteModal />
+      <PageRenameModal />
+      {/* <HotkeysManager /> */}
+
+      <ShortcutsModal />
+      <SystemVersion showShortcutsButton />
+    </RawLayout>
+  );
+};

+ 48 - 0
packages/app/src/components/Layout/RawLayout.tsx

@@ -0,0 +1,48 @@
+import React, { ReactNode, useEffect, useState } from 'react';
+
+import Head from 'next/head';
+
+import { useGrowiTheme } from '~/stores/context';
+import { Themes, useNextThemes } from '~/stores/use-next-themes';
+
+import { ThemeProvider } from '../Theme/utils/ThemeProvider';
+
+type Props = {
+  title: string,
+  className?: string,
+  children?: ReactNode,
+}
+
+export const RawLayout = ({ children, title, className }: Props): JSX.Element => {
+
+  const classNames: string[] = ['wrapper'];
+  if (className != null) {
+    classNames.push(className);
+  }
+  const { data: growiTheme } = useGrowiTheme();
+
+  // get color scheme from next-themes
+  const { resolvedTheme } = useNextThemes();
+
+  const [colorScheme, setColorScheme] = useState<Themes|undefined>(undefined);
+
+  // set colorScheme in CSR
+  useEffect(() => {
+    setColorScheme(resolvedTheme as Themes);
+  }, [resolvedTheme]);
+
+  return (
+    <>
+      <Head>
+        <title>{title}</title>
+        <meta charSet="utf-8" />
+        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
+      </Head>
+      <ThemeProvider theme={growiTheme}>
+        <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
+          {children}
+        </div>
+      </ThemeProvider>
+    </>
+  );
+};

+ 12 - 28
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -1,5 +1,5 @@
 import React, {
-  FC, useState, useCallback, useRef,
+  FC, useCallback, useRef,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
@@ -7,15 +7,8 @@ import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 
 import { useUserUISettings } from '~/client/services/user-ui-settings';
-import {
-  isUserPreferenceExists,
-  isDarkMode as isDarkModeByUtil,
-  applyColorScheme,
-  removeUserPreference,
-  updateUserPreference,
-  updateUserPreferenceWithOsSettings,
-} from '~/client/util/color-scheme';
 import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
+import { Themes, useNextThemes } from '~/stores/use-next-themes';
 
 import MoonIcon from '../Icons/MoonIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';
@@ -31,9 +24,9 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
 
   const { isAuthenticated } = props;
 
-  const [useOsSettings, setOsSettings] = useState(!isUserPreferenceExists());
-  const [isDarkMode, setIsDarkMode] = useState(isDarkModeByUtil());
-
+  const {
+    setTheme, resolvedTheme, useOsSettings, isDarkMode,
+  } = useNextThemes();
   const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
   const { scheduleToPut } = useUserUISettings();
@@ -52,27 +45,18 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
     }
   }, [updatePreferDrawerMode, mutatePreferDrawerModeOnEdit, scheduleToPut]);
 
-  const followOsCheckboxModifiedHandler = useCallback((useOsSettings: boolean) => {
-    if (useOsSettings) {
-      removeUserPreference();
+  const followOsCheckboxModifiedHandler = useCallback((isChecked: boolean) => {
+    if (isChecked) {
+      setTheme(Themes.system);
     }
     else {
-      updateUserPreferenceWithOsSettings();
+      setTheme(resolvedTheme ?? Themes.light);
     }
-    applyColorScheme();
-
-    // update states
-    setOsSettings(useOsSettings);
-    setIsDarkMode(isDarkModeByUtil());
-  }, []);
+  }, [resolvedTheme, setTheme]);
 
   const userPreferenceSwitchModifiedHandler = useCallback((isDarkMode: boolean) => {
-    updateUserPreference(isDarkMode);
-    applyColorScheme();
-
-    // update state
-    setIsDarkMode(isDarkModeByUtil());
-  }, []);
+    setTheme(isDarkMode ? 'dark' : 'light');
+  }, [setTheme]);
 
   /* eslint-disable react/prop-types */
   const IconWithTooltip = ({

+ 4 - 6
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -5,8 +5,7 @@ import assert from 'assert';
 import { useTranslation } from 'next-i18next';
 
 import { IFocusable } from '~/client/interfaces/focusable';
-import { IPageWithMeta } from '~/interfaces/page';
-import { IPageSearchMeta } from '~/interfaces/search';
+import { IPageWithSearchMeta } from '~/interfaces/search';
 import {
   useCurrentPagePath, useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
 } from '~/stores/context';
@@ -14,15 +13,14 @@ import { useGlobalSearchFormRef } from '~/stores/ui';
 
 import SearchForm from '../SearchForm';
 
-
 import styles from './GlobalSearch.module.scss';
 
 
-type Props = {
+export type GlobalSearchProps = {
   dropup?: boolean,
 }
 
-export const GlobalSearch = (props: Props): JSX.Element => {
+export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
   const { t } = useTranslation();
 
   const { dropup } = props;
@@ -40,7 +38,7 @@ export const GlobalSearch = (props: Props): JSX.Element => {
   const [isFocused, setFocused] = useState<boolean>(false);
 
 
-  const gotoPage = useCallback((data: IPageWithMeta<IPageSearchMeta>[]) => {
+  const gotoPage = useCallback((data: IPageWithSearchMeta[]) => {
     assert(data.length > 0);
 
     const page = data[0].data; // should be single page selected

+ 1 - 2
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,13 +1,12 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
+import { isPopulated } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import { DropdownItem } from 'reactstrap';
 
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
-import { isPopulated } from '~/interfaces/common';
 import {
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
 } from '~/interfaces/page';

+ 7 - 13
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -9,27 +9,21 @@ import Link from 'next/link';
 import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 
-import { HasChildren } from '~/interfaces/common';
 import {
   useIsSearchPage, useCurrentPagePath, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential,
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 
+import { HasChildren } from '../../interfaces/common';
 import GrowiLogo from '../Icons/GrowiLogo';
 
+import { GlobalSearchProps } from './GlobalSearch';
 import PersonalDropdown from './PersonalDropdown';
 
 import styles from './GrowiNavbar.module.scss';
 
 
-const ShowSkeltonInSSR = memo(({ children }: HasChildren): JSX.Element => {
-  return isServer()
-    ? <></>
-    : <>{children}</>;
-});
-ShowSkeltonInSSR.displayName = 'ShowSkeltonInSSR';
-
 const NavbarRight = memo((): JSX.Element => {
   const { t } = useTranslation();
 
@@ -52,7 +46,7 @@ const NavbarRight = memo((): JSX.Element => {
     return (
       <>
         <li className="nav-item">
-          <ShowSkeltonInSSR><InAppNotificationDropdown /></ShowSkeltonInSSR>
+          <InAppNotificationDropdown />
         </li>
 
         <li className="nav-item d-none d-md-block">
@@ -69,11 +63,11 @@ const NavbarRight = memo((): JSX.Element => {
         </li>
 
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
-          <ShowSkeltonInSSR><AppearanceModeDropdown isAuthenticated={isAuthenticated} /></ShowSkeltonInSSR>
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </li>
 
         <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret" data-testid="grw-personal-dropdown">
-          <ShowSkeltonInSSR><PersonalDropdown /></ShowSkeltonInSSR>
+          <PersonalDropdown />
         </li>
       </>
     );
@@ -83,7 +77,7 @@ const NavbarRight = memo((): JSX.Element => {
     return (
       <>
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
-          <ShowSkeltonInSSR><AppearanceModeDropdown isAuthenticated={isAuthenticated} /></ShowSkeltonInSSR>
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </li>
 
         <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;
@@ -130,7 +124,7 @@ Confidential.displayName = 'Confidential';
 
 export const GrowiNavbar = (): JSX.Element => {
 
-  const GlobalSearch = dynamic(() => import('./GlobalSearch').then(mod => mod.GlobalSearch), { ssr: false });
+  const GlobalSearch = dynamic<GlobalSearchProps>(() => import('./GlobalSearch').then(mod => mod.GlobalSearch), { ssr: false });
 
   const { data: appTitle } = useAppTitle();
   const { data: confidential } = useConfidential();

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

@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react';
+import React from 'react';
 
 import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
@@ -6,7 +6,6 @@ import dynamic from 'next/dynamic';
 import { TabContent, TabPane } from 'reactstrap';
 
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
-import { isPopulated } from '~/interfaces/common';
 import {
   useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound, useIsNotCreatable,
 } from '~/stores/context';

+ 1 - 1
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -47,7 +47,7 @@ const logger = loggerFactory('components:Page:RevisionRenderer');
 
 //   // for non-chrome browsers compatibility
 //   try {
-//     // eslint-disable-next-line regex/invalid
+// eslint-disable-next-line regex/invalid, max-len
 //     keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
 //   }
 //   catch (err) {

+ 5 - 3
packages/app/src/components/PageAlert/PageStaleAlert.tsx

@@ -1,4 +1,6 @@
-import { useTranslation } from 'react-i18next';
+import { useTranslation } from 'next-i18next';
+
+import { isIPageInfoForEntity } from '~/interfaces/page';
 
 import { useIsEnabledStaleNotification } from '../../stores/context';
 import { useSWRxCurrentPage, useSWRxPageInfo } from '../../stores/page';
@@ -11,7 +13,7 @@ export const PageStaleAlert = ():JSX.Element => {
   const { data: pageData } = useSWRxCurrentPage();
   const { data: pageInfo } = useSWRxPageInfo(isEnabledStaleNotification ? pageData?._id : null);
 
-  const contentAge = pageInfo?.contentAge;
+  const contentAge = isIPageInfoForEntity(pageInfo) ? pageInfo.contentAge : null;
 
   if (!isEnabledStaleNotification) {
     return <></>;
@@ -36,7 +38,7 @@ export const PageStaleAlert = ():JSX.Element => {
   return (
     <div className={`alert ${alertClass}`}>
       <i className="icon-fw icon-hourglass"></i>
-      <strong>{ t('page_page.notice.stale', { count: pageInfo.contentAge }) }</strong>
+      <strong>{ t('page_page.notice.stale', { count: contentAge }) }</strong>
     </div>
   );
 };

+ 1 - 1
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -36,7 +36,7 @@ export const TrashPageAlert = (): JSX.Element => {
 
   const lastUpdateUserName = pageData?.lastUpdateUser.name;
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
-  const revisionId = pageData?.revision._id;
+  const revisionId = pageData?.revision?._id;
 
   if (!isTrashPage) {
     return <></>;

+ 24 - 22
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -100,28 +100,30 @@ const CommentEditor = (props: PropsType): JSX.Element => {
       parsedHTML: '',
     };
 
-    const interceptorManager: IInterceptorManager = (window as CustomWindow).interceptorManager;
-    interceptorManager.process('preRenderCommnetPreview', context)
-      .then(() => { return interceptorManager.process('prePreProcess', context) })
-      .then(() => {
-        context.markdown = rendererOptions.preProcess(context.markdown, context);
-      })
-      .then(() => { return interceptorManager.process('postPreProcess', context) })
-      .then(() => {
-        const parsedHTML = rendererOptions.process(context.markdown, context);
-        context.parsedHTML = parsedHTML;
-      })
-      .then(() => { return interceptorManager.process('prePostProcess', context) })
-      .then(() => {
-        context.parsedHTML = rendererOptions.postProcess(context.parsedHTML, context);
-      })
-      .then(() => { return interceptorManager.process('postPostProcess', context) })
-      .then(() => { return interceptorManager.process('preRenderCommentPreviewHtml', context) })
-      .then(() => {
-        setHtml(context.parsedHTML);
-      })
-      // process interceptors for post rendering
-      .then(() => { return interceptorManager.process('postRenderCommentPreviewHtml', context) });
+    // TODO: use ReactMarkdown
+
+    // const interceptorManager: IInterceptorManager = (window as CustomWindow).interceptorManager;
+    // interceptorManager.process('preRenderCommnetPreview', context)
+    //   .then(() => { return interceptorManager.process('prePreProcess', context) })
+    //   .then(() => {
+    //     context.markdown = rendererOptions.preProcess(context.markdown, context);
+    //   })
+    //   .then(() => { return interceptorManager.process('postPreProcess', context) })
+    //   .then(() => {
+    //     const parsedHTML = rendererOptions.process(context.markdown, context);
+    //     context.parsedHTML = parsedHTML;
+    //   })
+    //   .then(() => { return interceptorManager.process('prePostProcess', context) })
+    //   .then(() => {
+    //     context.parsedHTML = rendererOptions.postProcess(context.parsedHTML, context);
+    //   })
+    //   .then(() => { return interceptorManager.process('postPostProcess', context) })
+    //   .then(() => { return interceptorManager.process('preRenderCommentPreviewHtml', context) })
+    //   .then(() => {
+    //     setHtml(context.parsedHTML);
+    //   })
+    //   // process interceptors for post rendering
+    //   .then(() => { return interceptorManager.process('postRenderCommentPreviewHtml', context) });
   }, [rendererOptions]);
 
   const handleSelect = useCallback((activeTab: string) => {

+ 2 - 1
packages/app/src/components/PageContentFooter.tsx

@@ -1,6 +1,7 @@
 import React, { FC, memo } from 'react';
 
-import { Ref } from '../interfaces/common';
+import { Ref } from '@growi/core';
+
 import { IUser } from '../interfaces/user';
 
 import AuthorInfo from './Navbar/AuthorInfo';

+ 4 - 18
packages/app/src/components/PageCreateModal.jsx

@@ -1,23 +1,19 @@
 import React, {
-  useEffect, useState, useMemo, useCallback,
+  useEffect, useState, useMemo,
 } from 'react';
 
 import { pagePathUtils, pathUtils } from '@growi/core';
 import { format } from 'date-fns';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser, useIsSearchServiceReachable } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 
 import PagePathAutoComplete from './PagePathAutoComplete';
-import { withUnstatedContainers } from './UnstatedUtils';
-
 
 const {
   userPageRoot, isCreatablePage, generateEditorPath, isUsersHomePage,
@@ -25,15 +21,13 @@ const {
 
 const PageCreateModal = (props) => {
   const { t } = useTranslation();
-  const { appContainer } = props;
 
   const { data: currentUser } = useCurrentUser();
 
   const { data: pageCreateModalData, close: closeCreateModal } = usePageCreateModal();
   const { isOpened, path } = pageCreateModalData;
 
-  const config = appContainer.getConfig();
-  const isReachable = config.isSearchServiceReachable;
+  const { data: isReachable } = useIsSearchServiceReachable();
   const pathname = path || '';
   const userPageRootPath = userPageRoot(currentUser);
   const isCreatable = isCreatablePage(pathname) || isUsersHomePage(pathname);
@@ -311,13 +305,5 @@ const PageCreateModal = (props) => {
   );
 };
 
-PageCreateModal.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const PageCreateModalWrapper = withUnstatedContainers(PageCreateModal, [AppContainer]);
 
-export default PageCreateModalWrapper;
+export default PageCreateModal;

+ 1 - 2
packages/app/src/components/PageDeleteModal.tsx

@@ -2,7 +2,7 @@ import React, {
   useState, FC, useMemo, useEffect,
 } from 'react';
 
-import { pagePathUtils } from '@growi/core';
+import { HasObjectId, pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -10,7 +10,6 @@ import {
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { HasObjectId } from '~/interfaces/has-object-id';
 import {
   IDeleteSinglePageApiv1Result, IDeleteManyPageApiv3Result, IPageToDeleteWithMeta, IDataWithMeta, isIPageInfoForEntity, IPageInfoForEntity,
 } from '~/interfaces/page';

+ 4 - 3
packages/app/src/components/PageEditor/EmojiPicker.tsx

@@ -3,7 +3,7 @@ import React, { FC } from 'react';
 import { Picker } from 'emoji-mart';
 import { Modal } from 'reactstrap';
 
-import { isDarkMode } from '~/client/util/color-scheme';
+import { useNextThemes } from '~/stores/use-next-themes';
 
 import EmojiPickerHelper, { getEmojiTranslation } from './EmojiPickerHelper';
 
@@ -20,6 +20,8 @@ const EmojiPicker: FC<Props> = (props: Props) => {
     onClose, emojiSearchText, emojiPickerHelper, isOpen,
   } = props;
 
+  const { resolvedTheme } = useNextThemes();
+
   // Set search emoji input and trigger search
   const searchEmoji = () => {
     const input = window.document.querySelector('[id^="emoji-mart-search"]') as HTMLInputElement;
@@ -42,7 +44,6 @@ const EmojiPicker: FC<Props> = (props: Props) => {
 
 
   const translation = getEmojiTranslation();
-  const theme = isDarkMode() ? 'dark' : 'light';
 
   return (
     <Modal isOpen={isOpen} toggle={onClose} onOpened={searchEmoji} backdropClassName="emoji-picker-modal" fade={false}>
@@ -52,7 +53,7 @@ const EmojiPicker: FC<Props> = (props: Props) => {
         title={translation.title}
         emojiTooltip
         style={emojiPickerHelper.setStyle()}
-        theme={theme}
+        theme={resolvedTheme}
       />
     </Modal>
   );

+ 15 - 12
packages/app/src/components/PageEditor/Preview.tsx

@@ -47,19 +47,22 @@ const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivEl
   }, [markdown, pagePath, editorSettings?.renderDrawioInRealtime]);
 
   const renderPreview = useCallback(async() => {
-    if (interceptorManager != null) {
-      await interceptorManager.process('preRenderPreview', context);
-      await interceptorManager.process('prePreProcess', context);
-      context.markdown = rendererOptions.preProcess(context.markdown, context);
-      await interceptorManager.process('postPreProcess', context);
-      context.parsedHTML = rendererOptions.process(context.markdown, context);
-      await interceptorManager.process('prePostProcess', context);
-      context.parsedHTML = rendererOptions.postProcess(context.parsedHTML, context);
-      await interceptorManager.process('postPostProcess', context);
-      await interceptorManager.process('preRenderPreviewHtml', context);
-    }
 
-    setHtml(context.parsedHTML ?? '');
+    // TODO: use ReactMarkdown
+
+    // if (interceptorManager != null) {
+    //   await interceptorManager.process('preRenderPreview', context);
+    //   await interceptorManager.process('prePreProcess', context);
+    //   context.markdown = rendererOptions.preProcess(context.markdown, context);
+    //   await interceptorManager.process('postPreProcess', context);
+    //   context.parsedHTML = rendererOptions.process(context.markdown, context);
+    //   await interceptorManager.process('prePostProcess', context);
+    //   context.parsedHTML = rendererOptions.postProcess(context.parsedHTML, context);
+    //   await interceptorManager.process('postPostProcess', context);
+    //   await interceptorManager.process('preRenderPreviewHtml', context);
+    // }
+
+    // setHtml(context.parsedHTML ?? '');
   }, [context, rendererOptions]);
 
   useEffect(() => {

+ 4 - 4
packages/app/src/components/PageList/PageList.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import { IPageWithMeta } from '~/interfaces/page';
+import { IPageInfoForEntity, IPageWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
@@ -10,15 +10,15 @@ import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { PageListItemL } from './PageListItemL';
 
 
-type Props = {
-  pages: IPageWithMeta[],
+type Props<M extends IPageInfoForEntity> = {
+  pages: IPageWithMeta<M>[],
   isEnableActions?: boolean,
   forceHideMenuItems?: ForceHideMenuItems,
   onPagesDeleted?: OnDeletedFunction,
   onPagePutBacked?: OnPutBackedFunction,
 }
 
-const PageList = (props: Props): JSX.Element => {
+const PageList = (props: Props<IPageInfoForEntity>): JSX.Element => {
   const { t } = useTranslation();
   const {
     pages, isEnableActions, forceHideMenuItems, onPagesDeleted, onPagePutBacked,

+ 3 - 3
packages/app/src/components/PageList/PageListItemL.tsx

@@ -16,9 +16,9 @@ import urljoin from 'url-join';
 import { ISelectable } from '~/client/interfaces/selectable-all';
 import { bookmark, unbookmark } from '~/client/services/page-operation';
 import {
-  IPageInfoAll, IPageInfoForEntity, IPageInfoForListing, IPageWithMeta, isIPageInfoForListing, isIPageInfoForEntity,
+  IPageInfoAll, isIPageInfoForListing, isIPageInfoForEntity, IPageWithMeta, IPageInfoForListing,
 } from '~/interfaces/page';
-import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
+import { IPageSearchMeta, IPageWithSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
 import {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
 } from '~/interfaces/ui';
@@ -33,7 +33,7 @@ import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItem
 import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
 
 type Props = {
-  page: IPageWithMeta<IPageInfoForEntity> | IPageWithMeta<IPageSearchMeta> | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,
+  page: IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,
   isSelected?: boolean, // is item selected(focused)
   isEnableActions?: boolean,
   forceHideMenuItems?: ForceHideMenuItems,

+ 0 - 30
packages/app/src/components/RawLayout.tsx

@@ -1,30 +0,0 @@
-import React, { ReactNode } from 'react';
-
-import Head from 'next/head';
-
-type Props = {
-  title: string,
-  className?: string,
-  children?: ReactNode,
-}
-
-export const RawLayout = ({ children, title, className }: Props): JSX.Element => {
-
-  const classNames: string[] = ['wrapper'];
-  if (className != null) {
-    classNames.push(className);
-  }
-
-  return (
-    <>
-      <Head>
-        <title>{title}</title>
-        <meta charSet="utf-8" />
-        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
-      </Head>
-      <div className={classNames.join(' ')}>
-        {children}
-      </div>
-    </>
-  );
-};

+ 18 - 0
packages/app/src/components/ReactMarkdownComponents/Header.module.scss

@@ -0,0 +1,18 @@
+.revision-head :global {
+  a {
+    text-decoration: none;
+  }
+
+  .revision-head-link,
+  .revision-head-edit-button {
+    margin-left: 0.5em;
+    font-size: 0.6em;
+    opacity: 0;
+  }
+}
+
+.revision-head:hover :global {
+  .revision-head-link, .revision-head-edit-button {
+    opacity: 1 !important;
+  }
+}

+ 4 - 1
packages/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -3,6 +3,9 @@ import { Element } from 'react-markdown/lib/rehype-filter';
 import { NextLink } from './NextLink';
 
 
+import styles from './Header.module.scss';
+
+
 type EditLinkProps = {
   line?: number,
 }
@@ -38,7 +41,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
   const CustomTag = `h${level}` as keyof JSX.IntrinsicElements;
 
   return (
-    <CustomTag id={id} className="revision-head">
+    <CustomTag id={id} className={`revision-head ${styles['revision-head']} ${styles.hoge}`}>
       {children}
       <NextLink href={`#${id}`} className="revision-head-link">
         <span className="icon-link"></span>

+ 1 - 2
packages/app/src/components/SavePageControls/GrantSelector.tsx

@@ -1,5 +1,6 @@
 import React, { useCallback, useState } from 'react';
 
+import { isPopulated } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   UncontrolledDropdown,
@@ -8,8 +9,6 @@ import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
-
-import { isPopulated } from '~/interfaces/common';
 import { IUserGroupHasId } from '~/interfaces/user';
 import { useCurrentUser } from '~/stores/context';
 import { useSWRxMyUserGroupRelations } from '~/stores/user-group';

+ 2 - 3
packages/app/src/components/SearchForm.tsx

@@ -7,8 +7,7 @@ import { useTranslation } from 'next-i18next';
 
 import { IFocusable } from '~/client/interfaces/focusable';
 import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
-import { IPageWithMeta } from '~/interfaces/page';
-import { IPageSearchMeta } from '~/interfaces/search';
+import { IPageWithSearchMeta } from '~/interfaces/search';
 
 import SearchTypeahead from './SearchTypeahead';
 
@@ -84,7 +83,7 @@ type Props = TypeaheadProps & {
 
   keywordOnInit?: string,
   disableIncrementalSearch?: boolean,
-  onChange?: (data: IPageWithMeta<IPageSearchMeta>[]) => void,
+  onChange?: (data: IPageWithSearchMeta[]) => void,
   onSubmit?: (input: string) => void,
 };
 

+ 2 - 2
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -9,7 +9,7 @@ import { exportAsMarkdown } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/apiNotification';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { IPageToDeleteWithMeta, IPageToRenameWithMeta, IPageWithMeta } from '~/interfaces/page';
-import { IPageSearchMeta } from '~/interfaces/search';
+import { IPageWithSearchMeta } from '~/interfaces/search';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
@@ -55,7 +55,7 @@ const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true };
 
 type Props ={
   appContainer: AppContainer,
-  pageWithMeta : IPageWithMeta<IPageSearchMeta>,
+  pageWithMeta : IPageWithSearchMeta,
   highlightKeywords?: string[],
   showPageControlDropdown?: boolean,
   forceHideMenuItems?: ForceHideMenuItems,

+ 4 - 4
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -10,7 +10,7 @@ import { toastSuccess } from '~/client/util/apiNotification';
 import {
   IPageInfoForListing, IPageWithMeta, isIPageInfoForListing,
 } from '~/interfaces/page';
-import { IPageSearchMeta } from '~/interfaces/search';
+import { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useIsGuestUser } from '~/stores/context';
 import { useSWRxPageInfoForList, usePageTreeTermManager } from '~/stores/page-listing';
@@ -21,10 +21,10 @@ import { PageListItemL } from '../PageList/PageListItemL';
 
 
 type Props = {
-  pages: IPageWithMeta<IPageSearchMeta>[],
+  pages: IPageWithSearchMeta[],
   selectedPageId?: string,
   forceHideMenuItems?: ForceHideMenuItems,
-  onPageSelected?: (page?: IPageWithMeta<IPageSearchMeta>) => void,
+  onPageSelected?: (page?: IPageWithSearchMeta) => void,
   onCheckboxChanged?: (isChecked: boolean, pageId: string) => void,
 }
 
@@ -73,7 +73,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     }
   }, [onPageSelected, pages]);
 
-  let injectedPages: (IPageWithMeta<IPageSearchMeta> | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>)[] | undefined;
+  let injectedPages: (IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>)[] | undefined;
   // inject data to list
   if (idToPageInfo != null) {
     injectedPages = pages.map((page) => {

+ 3 - 4
packages/app/src/components/SearchPage2/SearchPageBase.tsx

@@ -7,8 +7,7 @@ import { useTranslation } from 'next-i18next';
 import { ISelectableAll } from '~/client/interfaces/selectable-all';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess } from '~/client/util/apiNotification';
-import { IPageWithMeta } from '~/interfaces/page';
-import { IFormattedSearchResult, IPageSearchMeta } from '~/interfaces/search';
+import { IFormattedSearchResult, IPageWithSearchMeta } from '~/interfaces/search';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsSearchServiceConfigured, useIsSearchServiceReachable } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
@@ -31,7 +30,7 @@ export interface IReturnSelectedPageIds {
 type Props = {
   appContainer: AppContainer,
 
-  pages?: IPageWithMeta<IPageSearchMeta>[],
+  pages?: IPageWithSearchMeta[],
   searchingKeyword?: string,
 
   forceHideMenuItems?: ForceHideMenuItems,
@@ -61,7 +60,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
 
   const [selectedPageIdsByCheckboxes] = useState<Set<string>>(new Set());
   // const [allPageIds] = useState<Set<string>>(new Set());
-  const [selectedPageWithMeta, setSelectedPageWithMeta] = useState<IPageWithMeta<IPageSearchMeta> | undefined>();
+  const [selectedPageWithMeta, setSelectedPageWithMeta] = useState<IPageWithSearchMeta | undefined>();
 
   // publish selectAll()
   useImperativeHandle(ref, () => ({

+ 5 - 6
packages/app/src/components/SearchTypeahead.tsx

@@ -8,8 +8,7 @@ import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 
 import { IFocusable } from '~/client/interfaces/focusable';
 import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
-import { IPageWithMeta } from '~/interfaces/page';
-import { IPageSearchMeta } from '~/interfaces/search';
+import { IPageWithSearchMeta } from '~/interfaces/search';
 import { useSWRxSearch } from '~/stores/search';
 
 
@@ -49,7 +48,7 @@ type TypeaheadInstance = {
   clear: () => void,
   focus: () => void,
   toggleMenu: () => void,
-  state: { selected: IPageWithMeta<IPageSearchMeta>[] }
+  state: { selected: IPageWithSearchMeta[] }
 }
 
 const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
@@ -132,7 +131,7 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
   const DELAY_FOR_SUBMISSION = 100;
   const timeoutIdRef = useRef<NodeJS.Timeout>();
 
-  const changeHandler = useCallback((selectedItems: IPageWithMeta<IPageSearchMeta>[]) => {
+  const changeHandler = useCallback((selectedItems: IPageWithSearchMeta[]) => {
     // cancel schedule to submit
     if (timeoutIdRef.current != null) {
       clearTimeout(timeoutIdRef.current);
@@ -165,11 +164,11 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
     }
   }, [onSearchError, searchError]);
 
-  const labelKey = useCallback((option?: IPageWithMeta<IPageSearchMeta>) => {
+  const labelKey = useCallback((option?: IPageWithSearchMeta) => {
     return option?.data.path ?? '';
   }, []);
 
-  const renderMenu = useCallback((options: IPageWithMeta<IPageSearchMeta>[], menuProps) => {
+  const renderMenu = useCallback((options: IPageWithSearchMeta[], menuProps) => {
     if (!isForcused) {
       return <></>;
     }

+ 10 - 10
packages/app/src/components/Sidebar.module.scss

@@ -240,18 +240,18 @@
   }
 }
 
-// '&' could not be set after :global
-// workaround from https://github.com/css-modules/css-modules/issues/295#issuecomment-404873976
-.grw-sidebar :global {
-  .grw-sidebar-drawer {
-    @include drawer();
-  }
-  .grw-sidebar-dock {
-    @include bs.media-breakpoint-down(sm) {
+.grw-sidebar {
+  &:global {
+    &.grw-sidebar-drawer {
       @include drawer();
     }
-    @include bs.media-breakpoint-up(md) {
-      @include dock();
+    &.grw-sidebar-dock {
+      @include bs.media-breakpoint-down(sm) {
+        @include drawer();
+      }
+      @include bs.media-breakpoint-up(md) {
+        @include dock();
+      }
     }
   }
 }

+ 50 - 48
packages/app/src/components/Sidebar.tsx

@@ -88,8 +88,7 @@ const SidebarContentsWrapper = () => {
 
 const Sidebar = (): JSX.Element => {
 
-  // const { data: isDrawerMode } = useDrawerMode(); Todo Universalize
-  const isDrawerMode = false; // dummy
+  const { data: isDrawerMode } = useDrawerMode();
   const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
   const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
   const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
@@ -287,58 +286,61 @@ const Sidebar = (): JSX.Element => {
 
   const showContents = isDrawerMode || isHover || !isCollapsed;
 
+
+  // css styles
+  const grwSidebarClass = `grw-sidebar ${styles['grw-sidebar']}`;
+  const sidebarModeClass = `${isDrawerMode ? 'grw-sidebar-drawer' : 'grw-sidebar-dock'}`;
+  const isOpenClass = `${isDrawerOpened ? 'open' : ''}`;
   return (
     <>
-      <div className={`grw-sidebar ${styles['grw-sidebar']}`}>
-        <div className={`d-print-none ${isDrawerMode ? 'grw-sidebar-drawer' : 'grw-sidebar-dock'} ${isDrawerOpened ? 'open' : ''}`}>
-          <div className="data-layout-container">
-            <div
-              className='navigation transition-enabled'
-              onMouseEnter={hoverOnHandler}
-              onMouseLeave={hoverOutHandler}
-            >
-              <div className="grw-navigation-wrap">
-                <div className="grw-global-navigation">
-                  <GlobalNavigation></GlobalNavigation>
-                </div>
-                <div
-                  ref={resizableContainer}
-                  className="grw-contextual-navigation"
-                  onMouseEnter={hoverOnResizableContainerHandler}
-                  onMouseLeave={hoverOutResizableContainerHandler}
-                  style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
-                >
-                  <div className="grw-contextual-navigation-child">
-                    <div role="group" data-testid="grw-contextual-navigation-sub" className={`grw-contextual-navigation-sub ${showContents ? '' : 'd-none'}`}>
-                      <SidebarContentsWrapper></SidebarContentsWrapper>
-                    </div>
+      <div className={`${grwSidebarClass} ${sidebarModeClass} ${isOpenClass} d-print-none`}>
+        <div className="data-layout-container">
+          <div
+            className='navigation transition-enabled'
+            onMouseEnter={hoverOnHandler}
+            onMouseLeave={hoverOutHandler}
+          >
+            <div className="grw-navigation-wrap">
+              <div className="grw-global-navigation">
+                <GlobalNavigation></GlobalNavigation>
+              </div>
+              <div
+                ref={resizableContainer}
+                className="grw-contextual-navigation"
+                onMouseEnter={hoverOnResizableContainerHandler}
+                onMouseLeave={hoverOutResizableContainerHandler}
+                style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
+              >
+                <div className="grw-contextual-navigation-child">
+                  <div role="group" data-testid="grw-contextual-navigation-sub" className={`grw-contextual-navigation-sub ${showContents ? '' : 'd-none'}`}>
+                    <SidebarContentsWrapper></SidebarContentsWrapper>
                   </div>
                 </div>
               </div>
-              <div className="grw-navigation-draggable">
-                { isResizableByDrag && (
-                  <div
-                    className="grw-navigation-draggable-hitarea"
-                    onMouseDown={dragableAreaMouseDownHandler}
-                  >
-                    <div className="grw-navigation-draggable-hitarea-child"></div>
-                  </div>
-                ) }
-                <button
-                  data-testid="grw-navigation-resize-button"
-                  className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapsed' : ''} `}
-                  type="button"
-                  aria-expanded="true"
-                  aria-label="Toggle navigation"
-                  disabled={isDrawerMode}
-                  onClick={toggleNavigationBtnClickHandler}
+            </div>
+            <div className="grw-navigation-draggable">
+              { isResizableByDrag && (
+                <div
+                  className="grw-navigation-draggable-hitarea"
+                  onMouseDown={dragableAreaMouseDownHandler}
                 >
-                  <span className="hexagon-container" role="presentation">
-                    <NavigationResizeHexagon />
-                  </span>
-                  <span className="hitarea" role="presentation"></span>
-                </button>
-              </div>
+                  <div className="grw-navigation-draggable-hitarea-child"></div>
+                </div>
+              ) }
+              <button
+                data-testid="grw-navigation-resize-button"
+                className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapsed' : ''} `}
+                type="button"
+                aria-expanded="true"
+                aria-label="Toggle navigation"
+                disabled={isDrawerMode}
+                onClick={toggleNavigationBtnClickHandler}
+              >
+                <span className="hexagon-container" role="presentation">
+                  <NavigationResizeHexagon />
+                </span>
+                <span className="hitarea" role="presentation"></span>
+              </button>
             </div>
           </div>
         </div>

+ 1 - 2
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -4,7 +4,7 @@ import React, {
 
 import nodePath from 'path';
 
-import { pathUtils, pagePathUtils } from '@growi/core';
+import { pathUtils, pagePathUtils, Nullable } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import { useDrag, useDrop } from 'react-dnd';
@@ -14,7 +14,6 @@ import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/p
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
-import { Nullable } from '~/interfaces/common';
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';

+ 1 - 1
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -2,11 +2,11 @@ import React, {
   useEffect, useRef, useState, useMemo, useCallback,
 } from 'react';
 
+import { Nullable } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
-import { Nullable } from '~/interfaces/common';
 import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { AncestorsChildrenResult, RootPageResult, TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';

+ 1 - 4
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -2,15 +2,12 @@ import React, {
   memo, useCallback, useEffect, useState,
 } from 'react';
 
-import { DevidedPagePath } from '@growi/core';
+import { DevidedPagePath, isPopulated } from '@growi/core';
 import { UserPicture, FootstampIcon } from '@growi/ui';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
-import PropTypes from 'prop-types';
-
 
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
-import { isPopulated } from '~/interfaces/common';
 import { IPageHasId } from '~/interfaces/page';
 import LinkedPagePath from '~/models/linked-page-path';
 import { useSWRInifinitexRecentlyUpdated } from '~/stores/page-listing';

+ 1 - 2
packages/app/src/components/SubscribeButton.tsx

@@ -1,10 +1,9 @@
 import React, { FC, useCallback } from 'react';
 
+import { SubscriptionStatusType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 
-import { SubscriptionStatusType } from '~/interfaces/subscription';
-
 import styles from './SubscribeButton.module.scss';
 
 

+ 12 - 4
packages/app/src/components/SystemVersion.tsx

@@ -5,7 +5,13 @@ import { useShortcutsModal } from '~/stores/modal';
 
 import styles from './SystemVersion.module.scss';
 
-const SystemVersion = (): JSX.Element => {
+
+type Props = {
+  showShortcutsButton?: boolean,
+}
+
+const SystemVersion = (props: Props): JSX.Element => {
+  const { showShortcutsButton } = props;
 
   const { open: openShortcutsModal } = useShortcutsModal();
 
@@ -21,9 +27,11 @@ const SystemVersion = (): JSX.Element => {
         <span>
           <a href="https://growi.org">GROWI</a> {growiVersion}
         </span>
-        <button type="button" className="btn btn-link ml-2 p-0" onClick={() => openShortcutsModal()}>
-          <i className="fa fa-keyboard-o"></i>&nbsp;<span className={`cmd-key ${os}`}></span>-/
-        </button>
+        { showShortcutsButton && (
+          <button type="button" className="btn btn-link ml-2 p-0" onClick={() => openShortcutsModal()}>
+            <i className="fa fa-keyboard-o"></i>&nbsp;<span className={`cmd-key ${os}`}></span>-/
+          </button>
+        ) }
       </div>
 
     </>

+ 7 - 7
packages/app/src/styles/theme/antarctic.scss → packages/app/src/components/Theme/ThemeAntarctic.module.scss

@@ -1,5 +1,6 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 // == Define Bootstrap theme colors
 //
@@ -43,8 +44,7 @@ $accentcolor: #ffd700;
 
 //== Light Mode
 //
-html[light],
-html[dark] {
+.theme :global {
   $primary: $themecolor;
 
   // Background colors
@@ -110,13 +110,13 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: lighten($themecolor, 20%);
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   //Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(darken($primary, 10%), lighten($primary, 55%), lighten($primary, 60%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 10%), lighten($primary, 55%), lighten($primary, 60%));
     }
   }
 

+ 8 - 0
packages/app/src/components/Theme/ThemeAntarctic.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeAntarctic.module.scss';
+
+const ThemeAntarctic = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeAntarctic;

+ 7 - 7
packages/app/src/styles/theme/blackboard.scss → packages/app/src/components/Theme/ThemeBlackboard.module.scss

@@ -1,8 +1,8 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
-html[light],
-html[dark] {
+.theme :global {
   // Theme colors
   $themecolor: #da8506;
   $themelight: #223729;
@@ -79,8 +79,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
   // Navs
   .nav-tabs {
@@ -108,7 +108,7 @@ html[dark] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
     }
   }
 }

+ 8 - 0
packages/app/src/components/Theme/ThemeBlackboard.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeBlackboard.module.scss';
+
+const ThemeBlackboard = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeBlackboard;

+ 7 - 7
packages/app/src/styles/theme/christmas.scss → packages/app/src/components/Theme/ThemeChristmas.module.scss

@@ -1,5 +1,6 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 // == Define Bootstrap theme colors
 //
@@ -37,8 +38,7 @@ $color-link-wiki-hover: lighten($color-link-wiki, 15%);
 
 //== Light Mode
 //
-html[light],
-html[dark] {
+.theme :global {
   $primary: #d3c665;
   // Background colors
   $bgcolor-card: $gray-50;
@@ -102,8 +102,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: lighten($themecolor, 20%);
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   // change color of highlighted header in wiki (default: orange)
 
@@ -176,7 +176,7 @@ html[dark] {
   // Button
   .grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(darken($subthemecolor, 15%), lighten($subthemecolor, 35%), lighten($subthemecolor, 45%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($subthemecolor, 15%), lighten($subthemecolor, 35%), lighten($subthemecolor, 45%));
     }
   }
 }

+ 8 - 0
packages/app/src/components/Theme/ThemeChristmas.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeChristmas.module.scss';
+
+const ThemeChristmas = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeChristmas;

+ 9 - 9
packages/app/src/styles/theme/default.scss → packages/app/src/components/Theme/ThemeDefault.module.scss

@@ -1,6 +1,6 @@
-@use '../variables' as *;
-@use '../bootstrap/variables' as *;
-@use './mixins/page-editor-mode-manager';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 // == Define Bootstrap theme colors
 //
@@ -16,7 +16,7 @@
 
 //== Light Mode
 //
-html[light] {
+.theme[data-color-scheme='light'] :global {
   $primary: #122c55;
   $accent: #209fd8;
 
@@ -103,8 +103,8 @@ html[light] {
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   // Button
   .btn-group.grw-page-editor-mode-manager {
@@ -116,7 +116,7 @@ html[light] {
 
 //== Dark Mode
 //
-html[dark] {
+.theme[data-color-scheme='dark'] :global {
   $primary: #115cd3;
   $accent: #db00c2;
 
@@ -200,8 +200,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
   //Button
   .btn-group.grw-page-editor-mode-manager {

+ 8 - 0
packages/app/src/components/Theme/ThemeDefault.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeDefault.module.scss';
+
+const ThemeDefault = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeDefault;

+ 0 - 0
packages/app/src/styles/theme/fire-red.scss → packages/app/src/components/Theme/ThemeFireRed.module.scss


+ 0 - 0
packages/app/src/styles/theme/future.scss → packages/app/src/components/Theme/ThemeFuture.module.scss


+ 0 - 0
packages/app/src/styles/theme/halloween.scss → packages/app/src/components/Theme/ThemeHalloween.module.scss


+ 0 - 0
packages/app/src/styles/theme/hufflepuff.scss → packages/app/src/components/Theme/ThemeHufflepuff.module.scss


+ 0 - 0
packages/app/src/styles/theme/island.scss → packages/app/src/components/Theme/ThemeIsland.module.scss


+ 0 - 0
packages/app/src/styles/theme/jade-green.scss → packages/app/src/components/Theme/ThemeJadeGreen.module.scss


+ 0 - 0
packages/app/src/styles/theme/kibela.scss → packages/app/src/components/Theme/ThemeKibela.module.scss


+ 0 - 0
packages/app/src/styles/theme/mono-blue.scss → packages/app/src/components/Theme/ThemeMonoBlue.module.scss


+ 0 - 0
packages/app/src/styles/theme/nature.scss → packages/app/src/components/Theme/ThemeNature.module.scss


+ 0 - 0
packages/app/src/styles/theme/spring.scss → packages/app/src/components/Theme/ThemeSpring.module.scss


+ 0 - 0
packages/app/src/styles/theme/wood.scss → packages/app/src/components/Theme/ThemeWood.module.scss


+ 12 - 0
packages/app/src/components/Theme/utils/ThemeInjector.tsx

@@ -0,0 +1,12 @@
+
+import React from 'react';
+
+type Props = {
+  children: JSX.Element,
+  className: string,
+}
+
+export const ThemeInjector = ({ children, className: themeClassName }: Props): JSX.Element => {
+  const className = `${children.props.className ?? ''} ${themeClassName}`;
+  return React.cloneElement(children, { className });
+};

+ 31 - 0
packages/app/src/components/Theme/utils/ThemeProvider.tsx

@@ -0,0 +1,31 @@
+
+import React from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { GrowiThemes } from '~/interfaces/theme';
+
+
+const ThemeAntarctic = dynamic(() => import('../ThemeAntarctic'));
+const ThemeBlackboard = dynamic(() => import('../ThemeBlackboard'));
+const ThemeChristmas = dynamic(() => import('../ThemeChristmas'));
+const ThemeDefault = dynamic(() => import('../ThemeDefault'));
+
+
+type Props = {
+  children: JSX.Element,
+  theme?: GrowiThemes,
+}
+
+export const ThemeProvider = ({ theme, children }: Props): JSX.Element => {
+  switch (theme) {
+    case GrowiThemes.ANTARCTIC:
+      return <ThemeAntarctic>{children}</ThemeAntarctic>;
+    case GrowiThemes.BLACKBOARD:
+      return <ThemeBlackboard>{children}</ThemeBlackboard>;
+    case GrowiThemes.CHRISTMAS:
+      return <ThemeChristmas>{children}</ThemeChristmas>;
+    default:
+      return <ThemeDefault>{children}</ThemeDefault>;
+  }
+};

+ 66 - 27
packages/app/src/interfaces/activity.ts

@@ -1,5 +1,5 @@
-import { Ref } from './common';
-import { HasObjectId } from './has-object-id';
+import { Ref, HasObjectId } from '@growi/core';
+
 import { IUser } from './user';
 
 // Model
@@ -19,6 +19,8 @@ const ACTION_USER_LOGIN_WITH_SAML = 'USER_LOGIN_WITH_SAML';
 const ACTION_USER_LOGIN_WITH_BASIC = 'USER_LOGIN_WITH_BASIC';
 const ACTION_USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
 const ACTION_USER_LOGOUT = 'USER_LOGOUT';
+const ACTION_USER_FOGOT_PASSWORD = 'USER_FOGOT_PASSWORD';
+const ACTION_USER_RESET_PASSWORD = 'USER_RESET_PASSWORD';
 const ACTION_USER_PERSONAL_SETTINGS_UPDATE = 'USER_PERSONAL_SETTINGS_UPDATE';
 const ACTION_USER_IMAGE_TYPE_UPDATE = 'USER_IMAGE_TYPE_UPDATE';
 const ACTION_USER_LDAP_ACCOUNT_ASSOCIATE = 'USER_LDAP_ACCOUNT_ASSOCIATE';
@@ -68,7 +70,7 @@ const ACTION_ADMIN_APP_SETTINGS_UPDATE = 'ADMIN_APP_SETTING_UPDATE';
 const ACTION_ADMIN_SITE_URL_UPDATE = 'ADMIN_SITE_URL_UPDATE';
 const ACTION_ADMIN_MAIL_SMTP_UPDATE = 'ADMIN_MAIL_SMTP_UPDATE';
 const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
-const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT ';
+const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT';
 const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE = 'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
 const ACTION_ADMIN_PLUGIN_UPDATE = 'ADMIN_PLUGIN_UPDATE';
 const ACTION_ADMIN_MAINTENANCEMODE_ENABLED = 'ADMIN_MAINTENANCEMODE_ENABLED';
@@ -106,6 +108,7 @@ const ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE = 'ADMIN_MARKDOWN_PRESENTATION_U
 const ACTION_ADMIN_MARKDOWN_XSS_UPDATE = 'ADMIN_MARKDOWN_XSS_UPDATE';
 const ACTION_ADMIN_LAYOUT_UPDATE = 'ADMIN_LAYOUT_UPDATE';
 const ACTION_ADMIN_THEME_UPDATE = 'ADMIN_THEME_UPDATE';
+const ACTION_ADMIN_SIDEBAR_UPDATE = 'ADMIN_SIDEBAR_UPDATE';
 const ACTION_ADMIN_FUNCTION_UPDATE = 'ADMIN_FUNCTION_UPDATE';
 const ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE = 'ADMIN_CODE_HIGHLIGHT_UPDATE';
 const ACTION_ADMIN_CUSTOM_TITLE_UPDATE = 'ADMIN_CUSTOM_TITLE_UPDATE';
@@ -113,7 +116,17 @@ const ACTION_ADMIN_CUSTOM_HTML_HEADER_UPDATE = 'ADMIN_CUSTOM_HTML_HEADER_UPDATE'
 const ACTION_ADMIN_CUSTOM_CSS_UPDATE = 'ADMIN_CUSTOM_CSS_UPDATE';
 const ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE = 'ADMIN_CUSTOM_SCRIPT_UPDATE';
 const ACTION_ADMIN_ARCHIVE_DATA_UPLOAD = 'ADMIN_ARCHIVE_DATA_UPLOAD';
+const ACTION_ADMIN_GROWI_DATA_IMPORTED = 'ADMIN_GROWI_DATA_IMPORTED';
+const ACTION_ADMIN_UPLOADED_GROWI_DATA_DISCARDED = 'ADMIN_UPLOADED_GROWI_DATA_DISCARDED';
+const ACTION_ADMIN_ESA_DATA_IMPORTED = 'ADMIN_ESA_DATA_IMPORTED';
+const ACTION_ADMIN_ESA_DATA_UPDATED = 'ADMIN_ESA_DATA_UPDATED';
+const ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA = 'ADMIN_CONNECTION_TEST_OF_ESA_DATA';
+const ACTION_ADMIN_QIITA_DATA_IMPORTED = 'ADMIN_QIITA_DATA_IMPORTED';
+const ACTION_ADMIN_QIITA_DATA_UPDATED = 'ADMIN_QIITA_DATA_UPDATED';
+const ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA = 'ADMIN_CONNECTION_TEST_OF_QIITA_DATA';
 const ACTION_ADMIN_ARCHIVE_DATA_CREATE = 'ADMIN_ARCHIVE_DATA_CREATE';
+const ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD = 'ADMIN_ARCHIVE_DATA_DOWNLOAD';
+const ACTION_ADMIN_ARCHIVE_DATA_DELETE = 'ADMIN_ARCHIVE_DATA_DELETE';
 const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD = 'ADMIN_USER_NOTIFICATION_SETTINGS_ADD';
 const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE = 'ADMIN_USER_NOTIFICATION_SETTINGS_DELETE';
 const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD';
@@ -136,20 +149,20 @@ const ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE = 'ADMIN_SLACK_WITHOUT_
 const ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST = 'ADMIN_SLACK_WITHOUT_PROXY_TEST';
 const ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE = 'ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE';
 const ACTION_ADMIN_USERS_INVITE = 'ADMIN_USERS_INVITE';
+const ACTION_ADMIN_USERS_PASSWORD_RESET = 'ADMIN_USERS_PASSWORD_RESET';
+const ACTION_ADMIN_USERS_ACTIVATE = 'ADMIN_USERS_ACTIVATE';
+const ACTION_ADMIN_USERS_GIVE_ADMIN = 'ADMIN_USERS_GIVE_ADMIN';
+const ACTION_ADMIN_USERS_REMOVE_ADMIN = 'ADMIN_USERS_REMOVE_ADMIN';
+const ACTION_ADMIN_USERS_DEACTIVATE = 'ADMIN_USERS_DEACTIVATE';
+const ACTION_ADMIN_USERS_SEND_INVITATION_EMAIL = 'ADMIN_USERS_SEND_INVITATION_EMAIL';
+const ACTION_ADMIN_USERS_REMOVE = 'ADMIN_USERS_REMOVE';
 const ACTION_ADMIN_USER_GROUP_CREATE = 'ADMIN_USER_GROUP_CREATE';
 const ACTION_ADMIN_USER_GROUP_UPDATE = 'ADMIN_USER_GROUP_UPDATE';
 const ACTION_ADMIN_USER_GROUP_DELETE = 'ADMIN_USER_GROUP_DELETE';
 const ACTION_ADMIN_USER_GROUP_ADD_USER = 'ADMIN_USER_GROUP_ADD_USER';
+const ACTION_ADMIN_SEARCH_CONNECTION = 'ADMIN_SEARCH_CONNECTION';
 const ACTION_ADMIN_SEARCH_INDICES_NORMALIZE = 'ADMIN_SEARCH_INDICES_NORMALIZE';
 const ACTION_ADMIN_SEARCH_INDICES_REBUILD = 'ADMIN_SEARCH_INDICES_REBUILD';
-const ACTION_ADMIN_GROWI_DATA_IMPORTED = 'ADMIN_GROWI_DATA_IMPORTED';
-const ACTION_ADMIN_ESA_DATA_IMPORTED = 'ADMIN_ESA_DATA_IMPORTED';
-const ACTION_ADMIN_QIITA_DATA_IMPORTED = 'ADMIN_QIITA_DATA_IMPORTED';
-const ACTION_ADMIN_UPLOADED_GROWI_DATA_DISCARDED = 'ADMIN_UPLOADED_GROWI_DATA_DISCARDED';
-const ACTION_ADMIN_ESA_DATA_UPDATED = 'ADMIN_ESA_DATA_UPDATED';
-const ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA = 'ADMIN_CONNECTION_TEST_OF_ESA_DATA';
-const ACTION_ADMIN_QIITA_DATA_UPDATED = 'ADMIN_QIITA_DATA_UPDATED';
-const ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA = 'ADMIN_CONNECTION_TEST_OF_QIITA_DATA';
 
 
 export const SupportedTargetModel = {
@@ -185,6 +198,8 @@ export const SupportedAction = {
   ACTION_USER_LOGIN_WITH_BASIC,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGOUT,
+  ACTION_USER_FOGOT_PASSWORD,
+  ACTION_USER_RESET_PASSWORD,
   ACTION_USER_PERSONAL_SETTINGS_UPDATE,
   ACTION_USER_IMAGE_TYPE_UPDATE,
   ACTION_USER_LDAP_ACCOUNT_ASSOCIATE,
@@ -272,6 +287,7 @@ export const SupportedAction = {
   ACTION_ADMIN_MARKDOWN_XSS_UPDATE,
   ACTION_ADMIN_LAYOUT_UPDATE,
   ACTION_ADMIN_THEME_UPDATE,
+  ACTION_ADMIN_SIDEBAR_UPDATE,
   ACTION_ADMIN_FUNCTION_UPDATE,
   ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE,
   ACTION_ADMIN_CUSTOM_TITLE_UPDATE,
@@ -279,7 +295,17 @@ export const SupportedAction = {
   ACTION_ADMIN_CUSTOM_CSS_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
+  ACTION_ADMIN_GROWI_DATA_IMPORTED,
+  ACTION_ADMIN_ESA_DATA_IMPORTED,
+  ACTION_ADMIN_QIITA_DATA_IMPORTED,
+  ACTION_ADMIN_UPLOADED_GROWI_DATA_DISCARDED,
+  ACTION_ADMIN_ESA_DATA_UPDATED,
+  ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA,
+  ACTION_ADMIN_QIITA_DATA_UPDATED,
+  ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA,
   ACTION_ADMIN_ARCHIVE_DATA_CREATE,
+  ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD,
+  ACTION_ADMIN_ARCHIVE_DATA_DELETE,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD,
@@ -302,20 +328,20 @@ export const SupportedAction = {
   ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST,
   ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE,
   ACTION_ADMIN_USERS_INVITE,
+  ACTION_ADMIN_USERS_PASSWORD_RESET,
+  ACTION_ADMIN_USERS_ACTIVATE,
+  ACTION_ADMIN_USERS_DEACTIVATE,
+  ACTION_ADMIN_USERS_GIVE_ADMIN,
+  ACTION_ADMIN_USERS_REMOVE_ADMIN,
+  ACTION_ADMIN_USERS_SEND_INVITATION_EMAIL,
+  ACTION_ADMIN_USERS_REMOVE,
   ACTION_ADMIN_USER_GROUP_CREATE,
   ACTION_ADMIN_USER_GROUP_UPDATE,
   ACTION_ADMIN_USER_GROUP_DELETE,
   ACTION_ADMIN_USER_GROUP_ADD_USER,
+  ACTION_ADMIN_SEARCH_CONNECTION,
   ACTION_ADMIN_SEARCH_INDICES_NORMALIZE,
   ACTION_ADMIN_SEARCH_INDICES_REBUILD,
-  ACTION_ADMIN_GROWI_DATA_IMPORTED,
-  ACTION_ADMIN_ESA_DATA_IMPORTED,
-  ACTION_ADMIN_QIITA_DATA_IMPORTED,
-  ACTION_ADMIN_UPLOADED_GROWI_DATA_DISCARDED,
-  ACTION_ADMIN_ESA_DATA_UPDATED,
-  ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA,
-  ACTION_ADMIN_QIITA_DATA_UPDATED,
-  ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA,
 } as const;
 
 // Action required for notification
@@ -356,6 +382,8 @@ export const SmallActionGroup = {
 export const MediumActionGroup = {
   ...SmallActionGroup,
   ACTION_USER_REGISTRATION_SUCCESS,
+  ACTION_USER_FOGOT_PASSWORD,
+  ACTION_USER_RESET_PASSWORD,
   ACTION_USER_PERSONAL_SETTINGS_UPDATE,
   ACTION_USER_IMAGE_TYPE_UPDATE,
   ACTION_USER_LDAP_ACCOUNT_ASSOCIATE,
@@ -440,6 +468,7 @@ export const LargeActionGroup = {
   ACTION_ADMIN_MARKDOWN_XSS_UPDATE,
   ACTION_ADMIN_LAYOUT_UPDATE,
   ACTION_ADMIN_THEME_UPDATE,
+  ACTION_ADMIN_SIDEBAR_UPDATE,
   ACTION_ADMIN_FUNCTION_UPDATE,
   ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE,
   ACTION_ADMIN_CUSTOM_TITLE_UPDATE,
@@ -447,7 +476,17 @@ export const LargeActionGroup = {
   ACTION_ADMIN_CUSTOM_CSS_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
+  ACTION_ADMIN_GROWI_DATA_IMPORTED,
+  ACTION_ADMIN_ESA_DATA_IMPORTED,
+  ACTION_ADMIN_QIITA_DATA_IMPORTED,
+  ACTION_ADMIN_UPLOADED_GROWI_DATA_DISCARDED,
+  ACTION_ADMIN_ESA_DATA_UPDATED,
+  ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA,
+  ACTION_ADMIN_QIITA_DATA_UPDATED,
+  ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA,
   ACTION_ADMIN_ARCHIVE_DATA_CREATE,
+  ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD,
+  ACTION_ADMIN_ARCHIVE_DATA_DELETE,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD,
@@ -470,20 +509,20 @@ export const LargeActionGroup = {
   ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST,
   ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE,
   ACTION_ADMIN_USERS_INVITE,
+  ACTION_ADMIN_USERS_PASSWORD_RESET,
+  ACTION_ADMIN_USERS_ACTIVATE,
+  ACTION_ADMIN_USERS_DEACTIVATE,
+  ACTION_ADMIN_USERS_GIVE_ADMIN,
+  ACTION_ADMIN_USERS_REMOVE_ADMIN,
+  ACTION_ADMIN_USERS_SEND_INVITATION_EMAIL,
+  ACTION_ADMIN_USERS_REMOVE,
   ACTION_ADMIN_USER_GROUP_CREATE,
   ACTION_ADMIN_USER_GROUP_UPDATE,
   ACTION_ADMIN_USER_GROUP_DELETE,
   ACTION_ADMIN_USER_GROUP_ADD_USER,
+  ACTION_ADMIN_SEARCH_CONNECTION,
   ACTION_ADMIN_SEARCH_INDICES_NORMALIZE,
   ACTION_ADMIN_SEARCH_INDICES_REBUILD,
-  ACTION_ADMIN_GROWI_DATA_IMPORTED,
-  ACTION_ADMIN_ESA_DATA_IMPORTED,
-  ACTION_ADMIN_QIITA_DATA_IMPORTED,
-  ACTION_ADMIN_UPLOADED_GROWI_DATA_DISCARDED,
-  ACTION_ADMIN_ESA_DATA_UPDATED,
-  ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA,
-  ACTION_ADMIN_QIITA_DATA_UPDATED,
-  ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA,
 } as const;
 
 

+ 1 - 11
packages/app/src/interfaces/attachment.ts

@@ -1,11 +1 @@
-import { Ref } from './common';
-import { IPage } from './page';
-import { IUser } from './user';
-
-export type IAttachment = {
-  page?: Ref<IPage>,
-  creator?: Ref<IUser>,
-
-  // virtual property
-  filePathProxied: string,
-};
+export type { IAttachment } from '@growi/core';

+ 2 - 2
packages/app/src/interfaces/comment.ts

@@ -1,5 +1,5 @@
-import { Nullable, Ref } from './common';
-import { HasObjectId } from './has-object-id';
+import { Nullable, Ref, HasObjectId } from '@growi/core';
+
 import { IPage } from './page';
 import { IRevision } from './revision';
 import { IUser } from './user';

+ 0 - 19
packages/app/src/interfaces/common.ts

@@ -4,25 +4,6 @@
 
 import { ReactNode } from 'react';
 
-import { HasObjectId } from './has-object-id';
-
-
-// Foreign key field
-export type Ref<T> = string | T & HasObjectId;
-
-export type Nullable<T> = T | null | undefined;
-
-export const isPopulated = <T>(ref: Ref<T>): ref is T & HasObjectId => {
-  return !(typeof ref === 'string');
-};
-
-export const getIdForRef = <T>(ref: Ref<T>): string => {
-  return isPopulated(ref)
-    ? ref._id
-    : ref;
-};
-
-
 export type HasChildren<T = ReactNode> = {
   children?: T
 }

+ 2 - 1
packages/app/src/interfaces/external-account.ts

@@ -1,4 +1,5 @@
-import { Ref } from '~/interfaces/common';
+import { Ref } from '@growi/core';
+
 import { IUser } from '~/interfaces/user';
 
 

+ 1 - 6
packages/app/src/interfaces/global.ts

@@ -1,13 +1,8 @@
 import EventEmitter from 'events';
 
-import GrowiRenderer from '~/services/renderer/growi-renderer';
-import Xss from '~/services/xss';
-
 import { IGraphViewer } from './graph-viewer';
 
 export type CustomWindow = Window
                          & typeof globalThis
                          & { globalEmitter: EventEmitter }
-                         & { GraphViewer: IGraphViewer }
-                         & { growiRenderer: GrowiRenderer }
-                         & { previewRenderer: GrowiRenderer }; // TODO: Remove this code when reveal.js is omitted. see: https://github.com/weseek/growi/pull/6223
+                         & { GraphViewer: IGraphViewer };

+ 8 - 0
packages/app/src/interfaces/graph-viewer.ts

@@ -1,3 +1,11 @@
 export interface IGraphViewer {
   createViewerForElement: (Element) => void,
 }
+
+export const isGraphViewer = (val: any): val is IGraphViewer => {
+  if (typeof val === 'function' && typeof val.createViewerForElement === 'function') {
+    return true;
+  }
+
+  return false;
+};

+ 1 - 1
packages/app/src/interfaces/page-grant.ts

@@ -5,7 +5,7 @@ export type IDataApplicableGroup = {
 }
 
 export type IDataApplicableGrant = null | IDataApplicableGroup;
-export type IRecordApplicableGrant = Record<PageGrant, IDataApplicableGrant>
+export type IRecordApplicableGrant = Partial<Record<PageGrant, IDataApplicableGrant>>
 export type IResApplicableGrant = {
   data?: IRecordApplicableGrant
 }

+ 10 - 116
packages/app/src/interfaces/page.ts

@@ -1,125 +1,19 @@
-import { Ref, Nullable } from './common';
-import { HasObjectId } from './has-object-id';
-import { IPageOperationProcessData } from './page-operation';
-import { IRevision, HasRevisionShortbody } from './revision';
-import { SubscriptionStatusType } from './subscription';
-import { ITag } from './tag';
-import { IUser } from './user';
-
+import { IPageHasId, Nullable } from '@growi/core';
 
-export interface IPage {
-  path: string,
-  status: string,
-  revision: Ref<IRevision>,
-  tags: Ref<ITag>[],
-  creator: any,
-  createdAt: Date,
-  updatedAt: Date,
-  seenUsers: Ref<IUser>[],
-  parent: Ref<IPage> | null,
-  descendantCount: number,
-  isEmpty: boolean,
-  grant: PageGrant,
-  grantedUsers: Ref<IUser>[],
-  grantedGroup: Ref<any>,
-  lastUpdateUser: Ref<IUser>,
-  liker: Ref<IUser>[],
-  commentCount: number
-  slackChannels: string,
-  pageIdOnHackmd: string,
-  revisionHackmdSynced: Ref<IRevision>,
-  hasDraftOnHackmd: boolean,
-  deleteUser: Ref<IUser>,
-  deletedAt: Date,
-  latestRevision?: Ref<IRevision>,
-}
+import { IPageOperationProcessData } from './page-operation';
 
-export const PageGrant = {
-  GRANT_PUBLIC: 1,
-  GRANT_RESTRICTED: 2,
-  GRANT_SPECIFIED: 3, // DEPRECATED
-  GRANT_OWNER: 4,
-  GRANT_USER_GROUP: 5,
-};
-export type PageGrant = typeof PageGrant[keyof typeof PageGrant];
+export { PageGrant } from '@growi/core';
+export type {
+  IPage, IPageHasId, IPageInfo, IPageInfoForEntity, IPageInfoForOperation, IPageInfoForListing, IPageInfoAll,
+  IDataWithMeta, IPageWithMeta, IPageToDeleteWithMeta, IPageToRenameWithMeta,
+} from '@growi/core';
 
-export type IPageHasId = IPage & HasObjectId;
+export {
+  isIPageInfoForEntity, isIPageInfoForOperation, isIPageInfoForListing,
+} from '@growi/core';
 
 export type IPageForItem = Partial<IPageHasId & {isTarget?: boolean, processData?: IPageOperationProcessData}>;
 
-export type IPageInfo = {
-  isV5Compatible: boolean,
-  isEmpty: boolean,
-  isMovable: boolean,
-  isDeletable: boolean,
-  isAbleToDeleteCompletely: boolean,
-  isRevertible: boolean,
-  contentAge?: number,
-}
-
-export type IPageInfoForEntity = IPageInfo & {
-  bookmarkCount?: number,
-  sumOfLikers?: number,
-  likerIds?: string[],
-  sumOfSeenUsers?: number,
-  seenUserIds?: string[],
-}
-
-export type IPageInfoForOperation = IPageInfoForEntity & {
-  isBookmarked?: boolean,
-  isLiked?: boolean,
-  subscriptionStatus?: SubscriptionStatusType,
-}
-
-export type IPageInfoForListing = IPageInfoForEntity & HasRevisionShortbody;
-
-export type IPageInfoAll = IPageInfo | IPageInfoForEntity | IPageInfoForOperation | IPageInfoForListing;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const isIPageInfoForEntity = (pageInfo: any | undefined): pageInfo is IPageInfoForEntity => {
-  return pageInfo != null;
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const isIPageInfoForOperation = (pageInfo: any | undefined): pageInfo is IPageInfoForOperation => {
-  return pageInfo != null
-    && isIPageInfoForEntity(pageInfo)
-    && ('isBookmarked' in pageInfo || 'isLiked' in pageInfo || 'subscriptionStatus' in pageInfo);
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const isIPageInfoForListing = (pageInfo: any | undefined): pageInfo is IPageInfoForListing => {
-  return pageInfo != null
-    && isIPageInfoForEntity(pageInfo)
-    && 'revisionShortBody' in pageInfo;
-};
-
-// export type IPageInfoTypeResolver<T extends IPageInfo> =
-//   T extends HasRevisionShortbody ? IPageInfoForListing :
-//   T extends { isBookmarked?: boolean } | { isLiked?: boolean } | { subscriptionStatus?: SubscriptionStatusType } ? IPageInfoForOperation :
-//   T extends { bookmarkCount: number } ? IPageInfoForEntity :
-//   T extends { isEmpty: number } ? IPageInfo :
-//   T;
-
-/**
- * Union Distribution
- * @param pageInfo
- * @returns
- */
-// export const resolvePageInfo = <T extends IPageInfo>(pageInfo: T | undefined): IPageInfoTypeResolver<T> => {
-//   return <IPageInfoTypeResolver<T>>pageInfo;
-// };
-
-export type IDataWithMeta<D = unknown, M = unknown> = {
-  data: D,
-  meta?: M,
-}
-
-export type IPageWithMeta<M = IPageInfoAll> = IDataWithMeta<IPageHasId, M>;
-
-export type IPageToDeleteWithMeta<T = IPageInfoForEntity | unknown> = IDataWithMeta<HasObjectId & (IPage | { path: string, revision: string | null}), T>;
-export type IPageToRenameWithMeta<T = IPageInfoForEntity | unknown> = IPageToDeleteWithMeta<T>;
-
 export type IPageGrantData = {
   grant: number,
   grantedGroup?: {

+ 3 - 25
packages/app/src/interfaces/revision.ts

@@ -1,25 +1,3 @@
-import { IUser } from './user';
-
-export type IRevision = {
-  body: string,
-  author: IUser,
-  hasDiffToPrev: boolean;
-  createdAt: Date,
-  updatedAt: Date,
-}
-
-export type IRevisionsForPagination = {
-  revisions: IRevision[], // revisions in one pagination
-  totalCounts: number // total counts
-}
-
-export type IRevisionOnConflict = {
-  revisionId: string,
-  revisionBody: string,
-  createdAt: Date,
-  user: IUser
-}
-
-export type HasRevisionShortbody = {
-  revisionShortBody?: string,
-}
+export type {
+  IRevision, IRevisionsForPagination, IRevisionOnConflict, HasRevisionShortbody,
+} from '@growi/core';

+ 10 - 6
packages/app/src/interfaces/search.ts

@@ -1,4 +1,4 @@
-import { IPageWithMeta } from './page';
+import { IDataWithMeta, IPageHasId } from './page';
 
 export type IPageSearchMeta = {
   bookmarkCount?: number,
@@ -14,10 +14,6 @@ export const isIPageSearchMeta = (meta: any): meta is IPageSearchMeta => {
   return meta != null && 'elasticSearchResult' in meta;
 };
 
-export type ISearchResult<T > = ISearchResultMeta & {
-  data: T[],
-}
-
 export type ISearchResultMeta = {
   meta: {
     took?: number
@@ -26,7 +22,15 @@ export type ISearchResultMeta = {
   },
 }
 
-export type IFormattedSearchResult = ISearchResult<IPageWithMeta<IPageSearchMeta>>;
+export type ISearchResult<T> = ISearchResultMeta & {
+  data: T[],
+}
+
+export type IPageWithSearchMeta = IDataWithMeta<IPageHasId, IPageSearchMeta>;
+
+export type IFormattedSearchResult = ISearchResultMeta & {
+  data: IPageWithSearchMeta[],
+}
 
 export const SORT_AXIS = {
   RELATION_SCORE: 'relationScore',

+ 1 - 6
packages/app/src/interfaces/subscription.ts

@@ -1,6 +1 @@
-export const SubscriptionStatusType = {
-  SUBSCRIBE: 'SUBSCRIBE',
-  UNSUBSCRIBE: 'UNSUBSCRIBE',
-} as const;
-export const AllSubscriptionStatusType = Object.values(SubscriptionStatusType);
-export type SubscriptionStatusType = typeof SubscriptionStatusType[keyof typeof SubscriptionStatusType];
+export { SubscriptionStatusType, AllSubscriptionStatusType } from '@growi/core';

+ 3 - 6
packages/app/src/interfaces/tag.ts

@@ -1,20 +1,17 @@
+import { ITag } from '@growi/core';
+
 import { IPageHasId } from './page';
 
-export type ITag<ID = string> = {
-  _id: ID
-  name: string,
-}
+export type { ITag } from '@growi/core';
 
 export type IDataTagCount = ITag & {count: number}
 
-
 export type IPageTagsInfo = {
   tags : string[],
 }
 
 export type IListTagNamesByPage = string[];
 
-
 export type IResTagsUpdateApiv1 = {
   ok: boolean,
   savedPage: IPageHasId,

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