Sfoglia il codice sorgente

Merge branch 'master' into support/110444-next-ConflictDiffModal

Yuken Tezuka 3 anni fa
parent
commit
15e04b4f3d
100 ha cambiato i file con 543 aggiunte e 1067 eliminazioni
  1. 6 3
      .github/workflows/ci-app.yml
  2. 2 1
      package.json
  3. 29 0
      packages/app/cypress.config.ts
  4. 0 18
      packages/app/cypress.json
  5. 1 0
      packages/app/docker/Dockerfile
  6. 2 1
      packages/app/package.json
  7. 35 30
      packages/app/public/static/locales/en_US/admin.json
  8. 0 1
      packages/app/public/static/locales/en_US/translation.json
  9. 27 20
      packages/app/public/static/locales/ja_JP/admin.json
  10. 6 14
      packages/app/public/static/locales/zh_CN/admin.json
  11. 0 1
      packages/app/public/static/locales/zh_CN/translation.json
  12. 1 1
      packages/app/src/client/util/toastr.ts
  13. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  14. 5 7
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  15. 26 25
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  16. 1 1
      packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx
  17. 1 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  18. 2 2
      packages/app/src/components/InAppNotification/InAppNotificationList.tsx
  19. 3 6
      packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  20. 5 13
      packages/app/src/components/Layout/RawLayout.tsx
  21. 4 6
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  22. 7 4
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  23. 1 1
      packages/app/src/components/Page/CopyDropdown.jsx
  24. 9 11
      packages/app/src/components/Page/RenderTagLabels.tsx
  25. 1 1
      packages/app/src/components/Page/TagLabels.tsx
  26. 13 11
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  27. 4 0
      packages/app/src/components/PageCreateModal.jsx
  28. 9 2
      packages/app/src/components/PageEditorByHackmd/HackmdEditor.jsx
  29. 7 5
      packages/app/src/components/SlackNotification.module.scss
  30. 3 1
      packages/app/src/components/SlackNotification.tsx
  31. 0 32
      packages/app/src/components/Theme/ThemeAntarctic.tsx
  32. 0 8
      packages/app/src/components/Theme/ThemeBlackboard.tsx
  33. 0 32
      packages/app/src/components/Theme/ThemeChristmas.tsx
  34. 0 8
      packages/app/src/components/Theme/ThemeDefault.tsx
  35. 0 8
      packages/app/src/components/Theme/ThemeFireRed.tsx
  36. 0 8
      packages/app/src/components/Theme/ThemeFuture.tsx
  37. 0 32
      packages/app/src/components/Theme/ThemeHalloween.tsx
  38. 0 36
      packages/app/src/components/Theme/ThemeHufflepuff.tsx
  39. 0 32
      packages/app/src/components/Theme/ThemeIsland.tsx
  40. 0 8
      packages/app/src/components/Theme/ThemeJadeGreen.tsx
  41. 0 8
      packages/app/src/components/Theme/ThemeKibela.tsx
  42. 0 8
      packages/app/src/components/Theme/ThemeMonoBlue.tsx
  43. 0 8
      packages/app/src/components/Theme/ThemeNature.tsx
  44. 0 32
      packages/app/src/components/Theme/ThemeSpring.tsx
  45. 0 32
      packages/app/src/components/Theme/ThemeWood.tsx
  46. 0 36
      packages/app/src/components/Theme/utils/ThemeInjector.tsx
  47. 0 66
      packages/app/src/components/Theme/utils/ThemeProvider.tsx
  48. 2 2
      packages/app/src/components/User/SeenUserInfo.module.scss
  49. 4 0
      packages/app/src/interfaces/customize.ts
  50. 3 0
      packages/app/src/interfaces/in-app-notification.ts
  51. 2 2
      packages/app/src/models/serializers/in-app-notification-snapshot/page.ts
  52. 2 4
      packages/app/src/pages/_app.page.tsx
  53. 46 5
      packages/app/src/pages/_document.page.tsx
  54. 1 1
      packages/app/src/pages/admin/plugins.page.tsx
  55. 0 3
      packages/app/src/pages/utils/commons.ts
  56. 5 0
      packages/app/src/server/crowi/express-init.js
  57. 12 40
      packages/app/src/server/routes/apiv3/customize-setting.js
  58. 1 1
      packages/app/src/server/routes/apiv3/index.js
  59. 0 0
      packages/app/src/server/routes/apiv3/plugins-extension.ts
  60. 12 12
      packages/app/src/server/routes/login.js
  61. 31 4
      packages/app/src/stores/admin/customize.tsx
  62. 0 4
      packages/app/src/stores/context.tsx
  63. 21 2
      packages/app/src/stores/in-app-notification.ts
  64. 0 59
      packages/app/src/styles/_mixins.scss
  65. 0 43
      packages/app/src/styles/_page.scss
  66. 0 32
      packages/app/src/styles/_vendor.scss
  67. 60 0
      packages/app/src/styles/atoms/mixins/_buttons.scss
  68. 34 3
      packages/app/src/styles/bootstrap/_apply.scss
  69. 36 58
      packages/app/src/styles/style-app.scss
  70. 0 149
      packages/app/src/styles/style-next.scss
  71. 0 15
      packages/app/src/styles/style-themes.scss
  72. 2 2
      packages/app/src/styles/theme/_apply-colors-dark.scss
  73. 2 2
      packages/app/src/styles/theme/_apply-colors-light.scss
  74. 1 1
      packages/app/src/styles/theme/_apply-colors.scss
  75. 1 1
      packages/app/src/styles/theme/_reboot-bootstrap-colors.scss
  76. 3 3
      packages/app/src/styles/theme/_reboot-bootstrap-theme-colors.scss
  77. 0 0
      packages/app/test/cypress/integration/10-install/10-install--install.spec.ts
  78. 0 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts
  79. 0 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-pagelist.spec.ts
  80. 0 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--click-page-icons.spec.ts
  81. 0 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts
  82. 0 0
      packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts
  83. 0 0
      packages/app/test/cypress/integration/30-search/30-search--search.spec.ts
  84. 0 0
      packages/app/test/cypress/integration/40-admin/40-admin--access-to-admin-page.spec.ts
  85. 0 0
      packages/app/test/cypress/integration/50-sidebar/50-sidebar--access-to-side-bar.spec.ts
  86. 0 0
      packages/app/test/cypress/integration/50-sidebar/50-sidebar--switching-sidebar-mode.spec.ts
  87. 0 0
      packages/app/test/cypress/integration/60-home/60-home--home.spec.ts
  88. 0 33
      packages/app/test/cypress/plugins/index.ts
  89. 1 2
      packages/hackmd/package.json
  90. 0 1
      packages/hackmd/src/hackmd-styles.js
  91. 1 1
      packages/hackmd/src/index.js
  92. 2 0
      packages/preset-themes/.eslintignore
  93. 18 0
      packages/preset-themes/.eslintrc.js
  94. 1 0
      packages/preset-themes/.gitignore
  95. 1 0
      packages/preset-themes/README.md
  96. 27 0
      packages/preset-themes/package.json
  97. 0 0
      packages/preset-themes/public/images/antarctic/bg.svg
  98. 0 0
      packages/preset-themes/public/images/antarctic/topimage.svg
  99. 0 0
      packages/preset-themes/public/images/christmas/christmas-navbar.jpg
  100. 0 0
      packages/preset-themes/public/images/christmas/christmas.jpg

+ 6 - 3
.github/workflows/ci-app.yml

@@ -52,12 +52,15 @@ jobs:
         run: |
           npx lerna bootstrap -- --frozen-lockfile
 
-      - name: lerna run lint for plugins
+      - name: lerna run lint for dependent packages
         run: |
-          yarn lerna run lint --scope @growi/remark-*
+          yarn lerna run lint --scope @growi/codemirror-textlint --scope @growi/core --scope @growi/hackmd --scope @growi/preset-themes --scope @growi/remark-* --scope @growi/slack --scope @growi/ui
+      - name: build dependent packages
+        run: |
+          yarn lerna run build --scope @growi/preset-themes
       - name: lerna run lint for app
         run: |
-          yarn lerna run lint --scope @growi/app --scope @growi/codemirror-textlint --scope @growi/core --scope @growi/slack --scope @growi/ui
+          yarn lerna run lint --scope @growi/app
 
       - name: Slack Notification
         uses: weseek/ghaction-slack-notification@master

+ 2 - 1
package.json

@@ -61,7 +61,7 @@
     "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
-    "cypress": "^9.2.0",
+    "cypress": "^12.0.1",
     "eslint": "^8.18.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.0",
@@ -89,6 +89,7 @@
     "ts-node": "^10.9.1",
     "tsconfig-paths": "^3.9.0",
     "typescript": "~4.7",
+    "vite": "^3.2.5",
     "yargs": "^17.3.1"
   },
   "engines": {

+ 29 - 0
packages/app/cypress.config.ts

@@ -0,0 +1,29 @@
+import { defineConfig } from 'cypress';
+
+export default defineConfig({
+  e2e: {
+    baseUrl: 'http://localhost:3000',
+    specPattern: 'test/cypress/integration/',
+    supportFile: 'test/cypress/support/index.ts',
+    setupNodeEvents: (on) => {
+      // change screen size
+      // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
+      on('before:browser:launch', (browser, launchOptions) => {
+        if (browser.name === 'chrome' && browser.isHeadless) {
+          launchOptions.args.push('--window-size=1400,1024');
+          launchOptions.args.push('--force-device-scale-factor=1');
+        }
+        return launchOptions;
+      });
+    },
+  },
+  fileServerFolder: 'test/cypress',
+  fixturesFolder: 'test/cypress/fixtures',
+  screenshotsFolder: 'test/cypress/screenshots',
+  videosFolder: 'test/cypress/videos',
+
+  viewportWidth: 1400,
+  viewportHeight: 1024,
+
+  defaultCommandTimeout: 30000,
+});

+ 0 - 18
packages/app/cypress.json

@@ -1,18 +0,0 @@
-{
-  "baseUrl": "http://localhost:3000",
-
-  "fileServerFolder": "test/cypress",
-  "fixturesFolder": "test/cypress/fixtures",
-  "integrationFolder": "test/cypress/integration",
-  "screenshotsFolder": "test/cypress/screenshots",
-  "videosFolder": "test/cypress/videos",
-  "supportFile": "test/cypress/support/index.ts",
-  "pluginsFile": "test/cypress/plugins/index.ts",
-  "testFiles": "**/*.spec.ts",
-
-  "viewportWidth": 1400,
-  "viewportHeight": 1024,
-
-  "experimentalSessionSupport": true,
-  "defaultCommandTimeout": 30000
-}

+ 1 - 0
packages/app/docker/Dockerfile

@@ -111,6 +111,7 @@ COPY packages/remark-drawio-plugin packages/remark-drawio-plugin
 COPY packages/remark-growi-plugin packages/remark-growi-plugin
 COPY packages/remark-lsx packages/remark-lsx
 COPY packages/hackmd packages/hackmd
+COPY packages/preset-themes packages/preset-themes
 
 # build
 RUN yarn lerna run build

+ 2 - 1
packages/app/package.json

@@ -46,7 +46,7 @@
     "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "resources:hackmd": "yarn lerna run build --scope=@growi/hackmd",
-    "resources:dummy": "true",
+    "resources:preset-themes": "yarn lerna run build --scope=@growi/preset-themes",
     "// resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
     "ts-node": "node -r ts-node/register -r tsconfig-paths/register -r dotenv-flow/config"
   },
@@ -67,6 +67,7 @@
     "@growi/codemirror-textlint": "^6.0.0-RC.9",
     "@growi/core": "^6.0.0-RC.9",
     "@growi/hackmd": "^6.0.0-RC.9",
+    "@growi/preset-themes": "^6.0.0-RC.9",
     "@growi/remark-drawio-plugin": "^6.0.0-RC.9",
     "@growi/remark-growi-plugin": "^6.0.0-RC.9",
     "@growi/remark-lsx": "^6.0.0-RC.9",

+ 35 - 30
packages/app/public/static/locales/en_US/admin.json

@@ -2,11 +2,13 @@
   "meta": {
     "display_name": "English"
   },
-  "wiki_management_home_page": "Wiki Management Home Page",
   "last_login": "Last login",
-  "anyone_with_the_link": "anyone with the link",
-  "only_me": "only me",
-  "only_inside_the_group": "only inside the group",
+  "wiki_management_home_page": "Wiki Management Home Page",
+  "public": "Public",
+  "anyone_with_the_link": "Anyone with the link",
+  "specified_users": "Specified users",
+  "only_me": "Only me",
+  "only_inside_the_group": "Only inside the group",
   "security_settings": {
     "security_settings": "Security Settings",
     "scope_of_page_disclosure": "Scope of page disclosure",
@@ -203,7 +205,7 @@
         "username_detail": "Specification of mappings for <code>username</code> when creating new users",
         "name_detail": "Specification of mappings for <code>name</code> when creating new users",
         "mapping_detail": "Specification of mappings for %s when creating new users",
-        "register_1": "Contant to OIDC IdP Administrator",
+        "register_1": "Contact to OIDC IdP Administrator",
         "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
         "register_3": "Copy and paste your ClientID and Client Secret above",
         "updated_oidc": "Succeeded to update OpenID Connect",
@@ -279,6 +281,29 @@
     "toggle_notification": "Updated setting of {{path}}",
     "not_found_global_notification_triggerid": "Not found the global notification id"
   },
+  "full_text_search_management": {
+    "full_text_search_management": "Full Text Search Management",
+    "elasticsearch_management": "Elasticsearch management",
+    "connection_status": "Connection status",
+    "connection_status_label_unconfigured": "UNCONFIGURED",
+    "connection_status_label_connected": "CONNECTED",
+    "connection_status_label_disconnected": "DISCONNECTED",
+    "connection_status_label_erroroccured": "ERROR OCCURED ON SEARCH SERVICE",
+    "indices_status": "Indices Status",
+    "indices_status_label_normalized": "NORMALIZED",
+    "indices_status_label_unnormalized": "REBUILDING or BROKEN",
+    "indices_summary": "Indices summary",
+    "reconnect": "Reconnect",
+    "reconnect_button": "Try to reconnect",
+    "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
+    "normalize": "Normalize",
+    "normalize_button": "Normalize indices",
+    "normalize_description": "Click the button to repair broken indices.",
+    "rebuild": "Rebuild",
+    "rebuild_button": "Rebuild index",
+    "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
+    "rebuild_description_2": "This may take a while."
+  },
   "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
     "management_wiki": "Management Wiki",
@@ -497,6 +522,11 @@
   },
   "importer_management": {
     "import_data": "Import Data",
+    "article": "Article",
+    "category": "Category",
+    "tag": "Tag",
+    "page": "Page",
+    "page_path": "Page Path",
     "beta_warning": "This function is Beta.",
     "import_from": "Import from {{from}}",
     "import_growi_archive": "Import GROWI archive",
@@ -635,7 +665,6 @@
     "integration_procedure": "Integration Procedure",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
     "integration_failed":"Integration failed",
-    "official_bot_settings": "Official bot Settings",
     "reset": "Reset",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Delete Slack Bot settings",
@@ -708,7 +737,6 @@
   "slack_integration_legacy": {
     "slack_integration_legacy": "Legacy Slack Integration",
     "alert_disabled": "This 'Slack Legacy Intenfation' has been currently disabled since <a href='/admin/slack-integration'>New settings</a> are enabled",
-    "alert_Pd": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
     "alert_deplicated": "This 'Legacy Slack Integration' is outdated and will be discontinued in the future. Use <a href='/admin/slack-integration'>the new settings</a> instead. "
   },
   "user_management": {
@@ -818,29 +846,6 @@
       "force_update_parents_description": "Enable this option to force the addition of missing users to the ancestor groups if they exist after changing a parent group."
     }
   },
-  "full_text_search_management": {
-    "full_text_search_management": "Full Text Search Management",
-    "elasticsearch_management": "Elasticsearch management",
-    "connection_status": "Connection status",
-    "connection_status_label_unconfigured": "UNCONFIGURED",
-    "connection_status_label_connected": "CONNECTED",
-    "connection_status_label_disconnected": "DISCONNECTED",
-    "connection_status_label_erroroccured": "ERROR OCCURED ON SEARCH SERVICE",
-    "indices_status": "Indices Status",
-    "indices_status_label_normalized": "NORMALIZED",
-    "indices_status_label_unnormalized": "REBUILDING or BROKEN",
-    "indices_summary": "Indices summary",
-    "reconnect": "Reconnect",
-    "reconnect_button": "Try to reconnect",
-    "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
-    "normalize": "Normalize",
-    "normalize_button": "Normalize indices",
-    "normalize_description": "Click the button to repair broken indices.",
-    "rebuild": "Rebuild",
-    "rebuild_button": "Rebuild index",
-    "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
-    "rebuild_description_2": "This may take a while."
-  },
   "audit_log_management": {
     "audit_log": "Audit Log",
     "audit_log_settings": "Audit Log Settings",

+ 0 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -35,7 +35,6 @@
   "add": "Add",
   "Undo": "Undo",
   "Article": "Article",
-  "Page": "Page",
   "Page Path": "Page path",
   "Category": "Category",
   "User": "User",

+ 27 - 20
packages/app/public/static/locales/ja_JP/admin.json

@@ -64,6 +64,9 @@
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
+    "providerName": "プロバイダ名",
+    "issuerHost": "発行ホスト",
+    "scope": "範囲",
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "authorization_endpoint": "認可エンドポイント",
     "token_endpoint": "トークンエンドポイント",
@@ -210,13 +213,17 @@
         "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
         "name_detail": "新規ユーザー名(<code>name</code>)に関連付ける属性",
         "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
+        "register_1": "OIDC IdP Administrator へ接続します。",
+        "register_2": "OIDCアプリの認証コールバックURLを<code>%s</code>として登録します。",
+        "register_3": "上記のClientIDとClient Secretをコピー&ペーストしてください。",
         "updated_oidc": "OpenID Connect を更新しました",
         "Use discovered URL if empty": "データベース側の値が空の場合、\"Issuer Host\"から検出した値を利用します。"
       },
       "how_to": {
         "google": "Google OAuth の設定方法",
         "github": "GitHub OAuth の設定方法",
-        "twitter": "Twitter OAuth の設定方法"
+        "twitter": "Twitter OAuth の設定方法",
+        "oidc": "OIDC の設定方法"
       }
     },
     "form_item_name": {
@@ -521,25 +528,6 @@
     "upload_new_logo": "新しいロゴをアップロードする",
     "delete_logo": "ロゴを削除"
   },
-  "export_management": {
-    "export_archive_data": "データアーカイブ",
-    "exporting_collection_list": "エクスポート中のコレクション",
-    "exported_data_list": "エクスポートされたアーカイブリスト",
-    "export_collections": "コレクションのエクスポート",
-    "check_all": "全てにチェックを付ける",
-    "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
-    "create_new_archive_data": "アーカイブデータの新規作成",
-    "export": "エクスポート",
-    "cancel": "キャンセル",
-    "file": "ファイル名",
-    "growi_version": "GROWI バージョン",
-    "collections": "コレクション",
-    "exported_at": "エクスポートされた時間",
-    "export_menu": "エクスポートメニュー",
-    "download": "ダウンロード",
-    "delete": "削除"
-  },
   "importer_management": {
     "import_data": "データインポート",
     "article": "記事",
@@ -617,6 +605,25 @@
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
+  "export_management": {
+    "export_archive_data": "データアーカイブ",
+    "exporting_collection_list": "エクスポート中のコレクション",
+    "exported_data_list": "エクスポートされたアーカイブリスト",
+    "export_collections": "コレクションのエクスポート",
+    "check_all": "全てにチェックを付ける",
+    "uncheck_all": "全てからチェックを外す",
+    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
+    "create_new_archive_data": "アーカイブデータの新規作成",
+    "export": "エクスポート",
+    "cancel": "キャンセル",
+    "file": "ファイル名",
+    "growi_version": "GROWI バージョン",
+    "collections": "コレクション",
+    "exported_at": "エクスポートされた時間",
+    "export_menu": "エクスポートメニュー",
+    "download": "ダウンロード",
+    "delete": "削除"
+  },
   "external_notification": {
     "external_notification": "外部ツールへの通知",
     "enabled": "有効",

+ 6 - 14
packages/app/public/static/locales/zh_CN/admin.json

@@ -7,6 +7,7 @@
   "User": "用户",
   "Name": "姓名",
   "Created": "创建",
+  "Page": "页面",
   "Edit": "编辑",
   "Description": "描述",
   "last_login": "上次登录",
@@ -79,7 +80,7 @@
 		"client_secret": "客户机密",
 		"updated_general_security_setting": "更新安全设置成功",
 		"setup_not_completed_yet": "安装尚未完成",
-		"guest_mode": {
+    "guest_mode": {
 			"deny": "拒绝(仅限注册用户)",
 			"readonly": "接受(来宾可以只读)"
 		},
@@ -212,7 +213,7 @@
 				"username_detail": "Specification of mappings for <code>username</code> when creating new users",
 				"name_detail": "Specification of mappings for <code>name</code> when creating new users",
 				"mapping_detail": "Specification of mappings for %s when creating new users",
-				"register_1": "Contant to OIDC IdP Administrator",
+				"register_1": "Contact to OIDC IdP Administrator",
 				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
 				"register_3": "Copy and paste your ClientID and Client Secret above",
 				"updated_oidc": "Succeeded to update OpenID Connect",
@@ -238,6 +239,7 @@
 		}
   },
   "notification_settings": {
+    "notification_settings": "通知设置",
 		"slack_incoming_configuration": "Slack Incoming Webhooks configuration",
 		"prioritize_webhook": "Prioritize incoming webhook than Slack App",
 		"prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
@@ -399,9 +401,10 @@
     "aws_label": "AWS(S3)",
     "local_label": "Local",
     "gridfs_label": "MongoDB(GridFS)",
+    "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
+    "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "ses_settings": "SES设置",
     "test_connection": "测试邮件服务器连接",
-    "": "如果您没有SMTP设置,电子邮件将通过SES发送。您需要从电子邮件地址和生产设置进行验证。",
     "change_setting": "注意:如果你更改此设置未完成,您将无法访问迄今为止上传的文件。",
     "region": "Region",
     "bucket_name": "Bucket name",
@@ -474,17 +477,6 @@
       "expanded": "内容宽度100% "
     },
     "theme": "主体",
-    "behavior": "行为",
-    "behavior_desc": {
-      "growi_text1": "<code>/page</code> and <code>/page/</code> 都显示同一页。",
-      "growi_text2": "<code>/nonexistent_page</code> 显示编辑表单",
-      "growi_text3": "如果使用GROWI增强布局,则所有页面都显示子页面列表",
-      "crowi_text1": "<code>/page</code> 显示页面",
-      "crowi_text2": "<code>/page/</code> 显示子页列表",
-      "crowi_text3": "如果portal应用于<code>/page/</code>,则会显示portal和子页面列表",
-      "crowi_text4": "<code>/nonexistent_page</code> 显示编辑表单<",
-      "crowi_text5": "<code>/nonexistent_page/</code> 子页列表"
-    },
     "theme_desc": {
       "light_and_dark": "明暗模式",
       "unique": "只有一种模式"

+ 0 - 1
packages/app/public/static/locales/zh_CN/translation.json

@@ -117,7 +117,6 @@
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <i class='icon-share-alt'></i> ",
 	"Markdown Settings": "Markdown设置",
-	"Notification Settings": "通知设置",
 	"external_account_management": "外部账户管理",
   "UserGroup": "用户组",
   "ChildUserGroup": "儿童用户组",

+ 1 - 1
packages/app/src/client/util/toastr.ts

@@ -5,7 +5,7 @@ import { toArrayIfNot } from '~/utils/array-utils';
 
 
 export const toastErrorOption: ToastOptions = {
-  autoClose: 0,
+  autoClose: false,
   closeButton: true,
 };
 export const toastError = (err: string | Error | Error[], option: ToastOptions = toastErrorOption): void => {

+ 1 - 1
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -36,7 +36,7 @@ const AdminNavigation = (props) => {
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
       case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-      case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{           'Plugins Extention'}</>;
+      case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{           'Plugins Extension'}</>;
       case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
       default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
       /* eslint-enable no-multi-spaces, max-len */

+ 5 - 7
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -49,14 +49,13 @@ const uniqueTheme = [{
 
 const CustomizeThemeOptions = (props) => {
 
-  const { adminCustomizeContainer, currentTheme } = props;
-  const { currentLayout } = adminCustomizeContainer.state;
+  const { selectedTheme } = props;
 
   const { t } = useTranslation('admin');
 
 
   return (
-    <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
+    <div id="themeOptions">
       {/* Light and Dark Themes */}
       <div>
         <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
@@ -65,7 +64,7 @@ const CustomizeThemeOptions = (props) => {
             return (
               <ThemeColorBox
                 key={theme.name}
-                isSelected={currentTheme === theme.name}
+                isSelected={selectedTheme === theme.name}
                 onSelected={() => props.onSelected(theme.name)}
                 {...theme}
               />
@@ -81,7 +80,7 @@ const CustomizeThemeOptions = (props) => {
             return (
               <ThemeColorBox
                 key={theme.name}
-                isSelected={currentTheme === theme.name}
+                isSelected={selectedTheme === theme.name}
                 onSelected={() => props.onSelected(theme.name)}
                 {...theme}
               />
@@ -97,9 +96,8 @@ const CustomizeThemeOptions = (props) => {
 const CustomizeThemeOptionsWrapper = withUnstatedContainers(CustomizeThemeOptions, [AdminCustomizeContainer]);
 
 CustomizeThemeOptions.propTypes = {
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
   onSelected: PropTypes.func,
-  currentTheme: PropTypes.string,
+  selectedTheme: PropTypes.string,
 };
 
 export default CustomizeThemeOptionsWrapper;

+ 26 - 25
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -1,60 +1,61 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { useGrowiTheme } from '~/stores/context';
+import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
+import { useSWRxGrowiTheme } from '~/stores/admin/customize';
 
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 
+// eslint-disable-next-line @typescript-eslint/ban-types
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
 }
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
-
-  const { adminCustomizeContainer } = props;
-  const { data: currentTheme } = useGrowiTheme();
   const { t } = useTranslation();
 
-  const selectedHandler = useCallback((themeName) => {
-    // TODO: preview without using mutate of useGrowiTheme
-    // https://github.com/weseek/growi/pull/6860
-    // mutateGrowiTheme(themeName);
+  const { data: currentTheme, error } = useSWRxGrowiTheme();
+  const [selectedTheme, setSelectedTheme] = useState(currentTheme);
+
+  useEffect(() => {
+    setSelectedTheme(currentTheme);
+  }, [currentTheme]);
+
+  const selectedHandler = useCallback((themeName: string) => {
+    setSelectedTheme(themeName);
   }, []);
 
   const submitHandler = useCallback(async() => {
+    if (selectedTheme == null) {
+      toastWarning('The selected theme is undefined');
+      return;
+    }
+
     try {
-      if (currentTheme != null) {
-        await apiv3Put('/customize-setting/theme', {
-          themeType: currentTheme,
-        });
-      }
+      await apiv3Put('/customize-setting/theme', {
+        theme: selectedTheme,
+      });
 
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
     }
-  }, [currentTheme, t]);
+  }, [selectedTheme, t]);
 
   return (
     <div className="row">
       <div className="col-12">
         <h2 className="admin-setting-header">{t('admin:customize_settings.theme')}</h2>
-        <CustomizeThemeOptions onSelected={selectedHandler} currentTheme={currentTheme} />
-        <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        <CustomizeThemeOptions onSelected={selectedHandler} selectedTheme={selectedTheme} />
+        <AdminUpdateButtonRow onClick={submitHandler} disabled={error != null} />
       </div>
     </div>
   );
 };
 
-const CustomizeThemeSettingWrapper = withUnstatedContainers(CustomizeThemeSetting, [AdminCustomizeContainer]);
-
-export default CustomizeThemeSettingWrapper;
+export default CustomizeThemeSetting;

+ 1 - 1
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -30,7 +30,7 @@ export const PluginInstallerForm = (): JSX.Element => {
     };
 
     try {
-      await apiv3Post('/plugins-extention', { pluginInstallerForm });
+      await apiv3Post('/plugins-extension', { pluginInstallerForm });
       toastSuccess('Plugin Install Successed!');
     }
     catch (err) {

+ 1 - 1
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -248,7 +248,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
     <DropdownMenu
       data-testid="page-item-control-menu"
       positionFixed
-      modifiers={{ preventOverflow: { boundariesElement: undefined } }}
+      modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}
       right={alignRight}
     >
       {contents}

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

@@ -2,7 +2,7 @@ import React, { FC } from 'react';
 
 import { HasObjectId } from '@growi/core';
 
-import { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+import type { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 
 import InAppNotificationElm from './InAppNotificationElm';
 
@@ -26,7 +26,7 @@ const InAppNotificationList: FC<Props> = (props: Props) => {
     );
   }
 
-  const notifications = inAppNotificationData.docs;
+  const notifications = inAppNotificationData.docs.filter((notification) => { return notification.parsedSnapshot != null });
 
   return (
     <>

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

@@ -6,10 +6,9 @@ import { HasObjectId } from '@growi/core';
 import { PagePathLabel } from '@growi/ui';
 import { useRouter } from 'next/router';
 
-import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
-import { IInAppNotification } from '~/interfaces/in-app-notification';
+import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
-import { parseSnapshot } from '../../../models/serializers/in-app-notification-snapshot/page';
 import FormattedDistanceDate from '../../FormattedDistanceDate';
 
 interface Props {
@@ -27,8 +26,6 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
 
   const router = useRouter();
 
-  const snapshot = parseSnapshot(notification.snapshot);
-
   // publish open()
   useImperativeHandle(ref, () => ({
     open() {
@@ -45,7 +42,7 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
   return (
     <div className="p-2 overflow-hidden">
       <div className="text-truncate">
-        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={snapshot.path} />
+        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
       </div>
       <i className={`${actionIcon} mr-2`} />
       <FormattedDistanceDate

+ 5 - 13
packages/app/src/components/Layout/RawLayout.tsx

@@ -4,14 +4,10 @@ import Head from 'next/head';
 import { ToastContainer } from 'react-toastify';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
-import { useGrowiTheme } from '~/stores/context';
 import { ColorScheme, useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
 
-import { ThemeProvider as GrowiThemeProvider } from '../Theme/utils/ThemeProvider';
-
-
 const logger = loggerFactory('growi:cli:RawLayout');
 
 
@@ -26,8 +22,6 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
   if (className != null) {
     classNames.push(className);
   }
-  const { data: growiTheme } = useGrowiTheme();
-
   // get color scheme from next-themes
   const { resolvedTheme, resolvedThemeByAttributes } = useNextThemes();
 
@@ -36,7 +30,7 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
   // set colorScheme in CSR
   useIsomorphicLayoutEffect(() => {
     setColorScheme(resolvedTheme ?? resolvedThemeByAttributes);
-  }, [resolvedTheme]);
+  }, [resolvedTheme, resolvedThemeByAttributes]);
 
   return (
     <>
@@ -46,12 +40,10 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
         <meta name="viewport" content="initial-scale=1.0, width=device-width" />
       </Head>
       <NextThemesProvider>
-        <GrowiThemeProvider theme={growiTheme} colorScheme={colorScheme}>
-          <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
-            {children}
-            <ToastContainer theme={colorScheme} />
-          </div>
-        </GrowiThemeProvider>
+        <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
+          {children}
+          <ToastContainer theme={colorScheme} />
+        </div>
       </NextThemesProvider>
     </>
   );

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

@@ -68,6 +68,8 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
     </>
   );
 
+  const dropdownDivider = <div className="dropdown-divider"></div>;
+
   const renderSidebarModeSwitch = useCallback((isEditMode: boolean) => {
     return (
       <>
@@ -111,14 +113,10 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
       <div className="dropdown-menu dropdown-menu-right">
 
         {/* sidebar mode */}
-        {renderSidebarModeSwitch(false)}
-
-        <div className="dropdown-divider"></div>
+        {[renderSidebarModeSwitch(false), dropdownDivider]}
 
         {/* side bar mode on editor */}
-        {isAuthenticated && renderSidebarModeSwitch(true)}
-
-        <div className="dropdown-divider"></div>
+        {isAuthenticated && [renderSidebarModeSwitch(true), dropdownDivider]}
 
         {/* color mode */}
         <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>

+ 7 - 4
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,6 +1,6 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
-import { isPopulated, IUser } from '@growi/core';
+import { isPopulated, IUser, pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
@@ -43,6 +43,7 @@ import type { SubNavButtonsProps } from './SubNavButtons';
 import AuthorInfoStyles from './AuthorInfo.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
+const { isUsersHomePage } = pagePathUtils;
 
 const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
 
@@ -304,11 +305,13 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
         router.push(path);
       }
       else {
-        reload();
+        // Do not use "router.push(currentPathname)" to avoid `Error: Invariant: attempted to hard navigate to the same URL`
+        // See: https://github.com/weseek/growi/pull/7061
+        router.reload();
       }
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
-  }, [openDeleteModal, reload, router]);
+  }, [openDeleteModal, router]);
 
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
     await updateContentWidth(pageId, value);
@@ -378,7 +381,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
               />
             )}
           </div>
-          { (isAbleToShowPageAuthors && !isCompactMode) && (
+          { (isAbleToShowPageAuthors && !isCompactMode && !isUsersHomePage(path ?? '')) && (
             <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
               <li className="pb-1">
                 { currentPage != null

+ 1 - 1
packages/app/src/components/Page/CopyDropdown.jsx

@@ -118,7 +118,7 @@ const CopyDropdown = (props) => {
           <span id={dropdownToggleId}>{children}</span>
         </DropdownToggle>
 
-        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
+        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}>
 
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">

+ 9 - 11
packages/app/src/components/Page/RenderTagLabels.tsx

@@ -21,25 +21,23 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
   }
 
   const isTagsEmpty = tags.length === 0;
-  const tagElements = tags.map((tag) => {
-    return (
-      <a key={tag} href={`/_search?q=tag:${tag}`} className="grw-tag-label badge badge-secondary mr-2">
-        {tag}
-      </a>
-    );
-  });
 
   return (
     <>
-      {tagElements}
-
+      {tags.map((tag) => {
+        return (
+          <a key={tag} href={`/_search?q=tag:${tag}`} className="grw-tag-label badge badge-secondary mr-2">
+            {tag}
+          </a>
+        );
+      })}
       <div id="edit-tags-btn-wrapper-for-tooltip">
         <a
-          className={`btn btn-link btn-edit-tags p-0 text-muted d-flex ${isTagsEmpty ? 'no-tags' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
           onClick={openEditorHandler}
         >
           { isTagsEmpty && <>{ t('Add tags for this page') }</>}
-          <i className="ml-1 icon-plus"></i>
+          <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
         </a>
       </div>
       {isGuestUser && (

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

@@ -37,7 +37,7 @@ export const TagLabels:FC<Props> = (props: Props) => {
   return (
     <>
       <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
-        <i className="tag-icon icon-tag mr-2"></i>
+        <i className="tag-icon icon-tag mr-2"/>
         <RenderTagLabels
           tags={tags}
           openEditorModal={openEditorModal}

+ 13 - 11
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 import { UserPicture } from '@growi/ui';
 import { format } from 'date-fns';
@@ -32,27 +32,25 @@ export const TrashPageAlert = (): JSX.Element => {
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
 
-  if (!isTrashPage) {
-    return <></>;
-  }
-
 
   const deleteUser = pageData?.deleteUser;
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const revisionId = pageData?.revision?._id;
 
 
-  function openPutbackPageModalHandler() {
+  const openPutbackPageModalHandler = useCallback(() => {
     if (pageId === undefined || pagePath === undefined) {
       return;
     }
     const putBackedHandler = () => {
-      router.push(`/${pageId}`);
+      // Do not use "router.push(`/${pageId}`)" to avoid `Error: Invariant: attempted to hard navigate to the same URL`
+      // See: https://github.com/weseek/growi/pull/7054
+      router.reload();
     };
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
-  }
+  }, [openPutBackPageModal, pageId, pagePath, router]);
 
-  function openPageDeleteModalHandler() {
+  const openPageDeleteModalHandler = useCallback(() => {
     if (pageId === undefined || revisionId === undefined || pagePath === undefined) {
       return;
     }
@@ -65,9 +63,9 @@ export const TrashPageAlert = (): JSX.Element => {
       meta: pageInfo,
     };
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
-  }
+  }, [openDeleteModal, pageId, pageInfo, pagePath, revisionId]);
 
-  function renderTrashPageManagementButtons() {
+  const renderTrashPageManagementButtons = useCallback(() => {
     return (
       <>
         <button
@@ -88,6 +86,10 @@ export const TrashPageAlert = (): JSX.Element => {
         </button>
       </>
     );
+  }, [openPageDeleteModalHandler, openPutbackPageModalHandler, pageInfo?.isAbleToDeleteCompletely, t]);
+
+  if (!isTrashPage) {
+    return <></>;
   }
 
   return (

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

@@ -55,6 +55,10 @@ const PageCreateModal = () => {
     }
   }, [isOpened, pathname, isCreatable]);
 
+  useEffect(() => {
+    setTodayInput1(t('Memo'));
+  }, [t]);
+
   const checkIsUsersHomePageDebounce = useMemo(() => {
     const checkIsUsersHomePage = () => {
       setIsMatchedWithUserHomePagePath(isUsersHomePage(pageNameInput));

+ 9 - 2
packages/app/src/components/PageEditorByHackmd/HackmdEditor.jsx

@@ -1,6 +1,8 @@
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import connectToChild from 'penpal/lib/connectToChild';
+import PropTypes from 'prop-types';
+
 import loggerFactory from '~/utils/logger';
 
 
@@ -27,11 +29,16 @@ export default class HackmdEditor extends React.PureComponent {
   }
 
   async initHackmdWithPenpal() {
+    const shouldInit = document.getElementById('iframe-hackmd') != null;
+    if (shouldInit) {
+      return;
+    }
+
     // eslint-disable-next-line @typescript-eslint/no-this-alias
     const _this = this; // for in methods scope
-
     const iframe = document.createElement('iframe');
     iframe.src = `${this.props.hackmdUri}/${this.props.pageIdOnHackmd}?both`;
+    iframe.id = 'iframe-hackmd';
     this.iframeContainer.appendChild(iframe);
 
     const connection = connectToChild({

+ 7 - 5
packages/app/src/styles/molecules/slack-notification.scss → packages/app/src/components/SlackNotification.module.scss

@@ -1,6 +1,8 @@
-.grw-slack-notification {
-  $input-height-slack: $custom-control-indicator-size * 1.5;
-  border-color: $gray-200;
+@use '~/styles/bootstrap/init' as bs;
+
+.grw-slack-notification :global {
+  $input-height-slack: bs.$custom-control-indicator-size * 1.5;
+  border-color: bs.$gray-200;
 
   border-style: solid;
   border-width: 1px;
@@ -9,10 +11,10 @@
   .form-control {
     height: $input-height-slack;
     border: transparent;
-    @include media-breakpoint-up(sm) {
+    @include bs.media-breakpoint-up(sm) {
       width: 130px;
     }
-    @include media-breakpoint-up(md) {
+    @include bs.media-breakpoint-up(md) {
       width: 180px;
     }
   }

+ 3 - 1
packages/app/src/components/SlackNotification.tsx

@@ -4,6 +4,8 @@ import React, { FC } from 'react';
 import { useTranslation } from 'next-i18next';
 import { PopoverBody, PopoverHeader, UncontrolledPopover } from 'reactstrap';
 
+import styles from './SlackNotification.module.scss';
+
 
 type SlackNotificationProps = {
   id: string;
@@ -35,7 +37,7 @@ export const SlackNotification: FC<SlackNotificationProps> = ({
 
 
   return (
-    <div className="grw-slack-notification w-100">
+    <div className={`grw-slack-notification ${styles['grw-slack-notification']} w-100`}>
       <div className="grw-input-group-slack-notification input-group extended-setting">
         <label className="input-group-addon">
           <div className="custom-control custom-switch custom-switch-lg custom-switch-slack">

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

@@ -1,32 +0,0 @@
-import Image from 'next/image';
-
-import { Themes } from '~/stores/use-next-themes';
-
-import { ThemeInjector } from './utils/ThemeInjector';
-
-// import styles from './ThemeAntarctic.module.scss';
-
-export const getBackgroundImageSrc = (colorScheme: Themes): string => {
-  switch (colorScheme) {
-    default:
-      return '/images/themes/antarctic/bg.svg';
-  }
-};
-
-type Props = {
-  children: JSX.Element,
-  colorScheme?: Themes,
-}
-
-const ThemeAntarctic = ({ children, colorScheme }: Props): JSX.Element => {
-  const bgImageNode = (
-    <>
-      {colorScheme != null && (
-        <Image alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
-      )}
-    </>
-  );
-  return <ThemeInjector className="theme-antarctic" bgImageNode={bgImageNode}>{children}</ThemeInjector>;
-};
-
-export default ThemeAntarctic;

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

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

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

@@ -1,32 +0,0 @@
-import Image from 'next/image';
-
-import { Themes } from '~/stores/use-next-themes';
-
-import { ThemeInjector } from './utils/ThemeInjector';
-
-// import styles from './ThemeChristmas.module.scss';
-
-export const getBackgroundImageSrc = (colorScheme: Themes): string => {
-  switch (colorScheme) {
-    default:
-      return '/images/themes/christmas/christmas.jpg';
-  }
-};
-
-type Props = {
-  children: JSX.Element,
-  colorScheme?: Themes,
-}
-
-const ThemeChristmas = ({ children, colorScheme }: Props): JSX.Element => {
-  const bgImageNode = (
-    <>
-      {colorScheme != null && (
-        <Image alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
-      )}
-    </>
-  );
-  return <ThemeInjector className="theme-christmas" bgImageNode={bgImageNode}>{children}</ThemeInjector>;
-};
-
-export default ThemeChristmas;

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

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

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

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

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

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

+ 0 - 32
packages/app/src/components/Theme/ThemeHalloween.tsx

@@ -1,32 +0,0 @@
-import Image from 'next/image';
-
-import { Themes } from '~/stores/use-next-themes';
-
-import { ThemeInjector } from './utils/ThemeInjector';
-
-// import styles from './ThemeHalloween.module.scss';
-
-export const getBackgroundImageSrc = (colorScheme: Themes): string => {
-  switch (colorScheme) {
-    default:
-      return '/images/themes/halloween/halloween.jpg';
-  }
-};
-
-type Props = {
-  children: JSX.Element,
-  colorScheme?: Themes,
-}
-
-const ThemeHalloween = ({ children, colorScheme }: Props): JSX.Element => {
-  const bgImageNode = (
-    <>
-      {colorScheme != null && (
-        <Image alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
-      )}
-    </>
-  );
-  return <ThemeInjector className="theme-halloween" bgImageNode={bgImageNode}>{children}</ThemeInjector>;
-};
-
-export default ThemeHalloween;

+ 0 - 36
packages/app/src/components/Theme/ThemeHufflepuff.tsx

@@ -1,36 +0,0 @@
-import Image from 'next/image';
-
-import { Themes } from '~/stores/use-next-themes';
-
-import { ThemeInjector } from './utils/ThemeInjector';
-
-// import styles from './ThemeHufflepuff.module.scss';
-
-export const getBackgroundImageSrc = (colorScheme: Themes): string => {
-  switch (colorScheme) {
-    case Themes.light:
-      return '/images/themes/hufflepuff/badger-light3.png';
-    case Themes.dark:
-      return '/images/themes/hufflepuff/badger-dark.jpg';
-    default:
-      return '/images/themes/hufflepuff/badger-light3.png';
-  }
-};
-
-type Props = {
-  children: JSX.Element,
-  colorScheme?: Themes,
-}
-
-const ThemeHufflepuff = ({ children, colorScheme }: Props): JSX.Element => {
-  const bgImageNode = (
-    <>
-      {colorScheme != null && (
-        <Image alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
-      )}
-    </>
-  );
-  return <ThemeInjector className="theme-hufflepuff" bgImageNode={bgImageNode}>{children}</ThemeInjector>;
-};
-
-export default ThemeHufflepuff;

+ 0 - 32
packages/app/src/components/Theme/ThemeIsland.tsx

@@ -1,32 +0,0 @@
-import Image from 'next/image';
-
-import { Themes } from '~/stores/use-next-themes';
-
-import { ThemeInjector } from './utils/ThemeInjector';
-
-// import styles from './ThemeIsland.module.scss';
-
-export const getBackgroundImageSrc = (colorScheme: Themes): string => {
-  switch (colorScheme) {
-    default:
-      return '/images/themes/island/island.png';
-  }
-};
-
-type Props = {
-  children: JSX.Element,
-  colorScheme?: Themes,
-}
-
-const ThemeIsland = ({ children, colorScheme }: Props): JSX.Element => {
-  const bgImageNode = (
-    <>
-      {colorScheme != null && (
-        <Image alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
-      )}
-    </>
-  );
-  return <ThemeInjector className="theme-island" bgImageNode={bgImageNode}>{children}</ThemeInjector>;
-};
-
-export default ThemeIsland;

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

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

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

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

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

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

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

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

+ 0 - 32
packages/app/src/components/Theme/ThemeSpring.tsx

@@ -1,32 +0,0 @@
-import Image from 'next/image';
-
-import { Themes } from '~/stores/use-next-themes';
-
-import { ThemeInjector } from './utils/ThemeInjector';
-
-// import styles from './ThemeSpring.module.scss';
-
-export const getBackgroundImageSrc = (colorScheme: Themes): string => {
-  switch (colorScheme) {
-    default:
-      return '/images/themes/spring/spring02.svg';
-  }
-};
-
-type Props = {
-  children: JSX.Element,
-  colorScheme?: Themes,
-}
-
-const ThemeSpring = ({ children, colorScheme }: Props): JSX.Element => {
-  const bgImageNode = (
-    <>
-      {colorScheme != null && (
-        <Image alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
-      )}
-    </>
-  );
-  return <ThemeInjector className="theme-spring" bgImageNode={bgImageNode}>{children}</ThemeInjector>;
-};
-
-export default ThemeSpring;

+ 0 - 32
packages/app/src/components/Theme/ThemeWood.tsx

@@ -1,32 +0,0 @@
-import Image from 'next/image';
-
-import { Themes } from '~/stores/use-next-themes';
-
-import { ThemeInjector } from './utils/ThemeInjector';
-
-// import styles from './ThemeWood.module.scss';
-
-export const getBackgroundImageSrc = (colorScheme: Themes): string => {
-  switch (colorScheme) {
-    default:
-      return '/images/themes/wood/wood.jpg';
-  }
-};
-
-type Props = {
-  children: JSX.Element,
-  colorScheme?: Themes,
-}
-
-const ThemeWood = ({ children, colorScheme }: Props): JSX.Element => {
-  const bgImageNode = (
-    <>
-      {colorScheme != null && (
-        <Image alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
-      )}
-    </>
-  );
-  return <ThemeInjector className="theme-wood" bgImageNode={bgImageNode}>{children}</ThemeInjector>;
-};
-
-export default ThemeWood;

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

@@ -1,36 +0,0 @@
-
-import React from 'react';
-
-import { useIsomorphicLayoutEffect } from 'usehooks-ts';
-
-type Props = {
-  children: JSX.Element,
-  bodyTagClassName?: string,
-  className?: string,
-  bgImageNode?: React.ReactNode,
-}
-
-export const ThemeInjector = ({
-  children, bodyTagClassName, className: childrenClassName, bgImageNode,
-}: Props): JSX.Element => {
-  const className = `${children.props.className ?? ''} ${childrenClassName ?? ''}`;
-
-  // add class name to <body>
-  useIsomorphicLayoutEffect(() => {
-    if (bodyTagClassName != null) {
-      document.body.classList.add(bodyTagClassName);
-    }
-
-    // clean up
-    return () => {
-      if (bodyTagClassName != null) {
-        document.body.classList.remove(bodyTagClassName);
-      }
-    };
-  });
-
-  return React.cloneElement(children, { className }, [
-    <div key="grw-bg-image-wrapper" className="grw-bg-image-wrapper">{bgImageNode}</div>,
-    children.props.children,
-  ]);
-};

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

@@ -1,66 +0,0 @@
-
-import React from 'react';
-
-import dynamic from 'next/dynamic';
-
-import { GrowiThemes } from '~/interfaces/theme';
-import { Themes } from '~/stores/use-next-themes';
-
-
-// const ThemeAntarctic = dynamic(() => import('../ThemeAntarctic'));
-// const ThemeBlackboard = dynamic(() => import('../ThemeBlackboard'));
-// const ThemeChristmas = dynamic(() => import('../ThemeChristmas'));
-const ThemeDefault = dynamic(() => import('../ThemeDefault'));
-// const ThemeFireRed = dynamic(() => import('../ThemeFireRed'));
-// const ThemeFuture = dynamic(() => import('../ThemeFuture'));
-// const ThemeHalloween = dynamic(() => import('../ThemeHalloween'));
-// const ThemeHufflepuff = dynamic(() => import('../ThemeHufflepuff'));
-// const ThemeIsland = dynamic(() => import('../ThemeIsland'));
-// const ThemeJadeGreen = dynamic(() => import('../ThemeJadeGreen'));
-// const ThemeKibela = dynamic(() => import('../ThemeKibela'));
-// const ThemeMonoBlue = dynamic(() => import('../ThemeMonoBlue'));
-// const ThemeNature = dynamic(() => import('../ThemeNature'));
-// const ThemeSpring = dynamic(() => import('../ThemeSpring'));
-// const ThemeWood = dynamic(() => import('../ThemeWood'));
-
-
-type Props = {
-  children: JSX.Element,
-  theme?: GrowiThemes,
-  colorScheme?: Themes,
-}
-
-export const ThemeProvider = ({ theme, children, colorScheme }: Props): JSX.Element => {
-  switch (theme) {
-    // case GrowiThemes.ANTARCTIC:
-    //   return <ThemeAntarctic colorScheme={colorScheme}>{children}</ThemeAntarctic>;
-    // case GrowiThemes.BLACKBOARD:
-    //   return <ThemeBlackboard>{children}</ThemeBlackboard>;
-    // case GrowiThemes.CHRISTMAS:
-    //   return <ThemeChristmas colorScheme={colorScheme}>{children}</ThemeChristmas>;
-    // case GrowiThemes.FIRE_RED:
-    //   return <ThemeFireRed>{children}</ThemeFireRed>;
-    // case GrowiThemes.FUTURE:
-    //   return <ThemeFuture>{children}</ThemeFuture>;
-    // case GrowiThemes.HALLOWEEN:
-    //   return <ThemeHalloween colorScheme={colorScheme}>{children}</ThemeHalloween>;
-    // case GrowiThemes.HUFFLEPUFF:
-    //   return <ThemeHufflepuff colorScheme={colorScheme}>{children}</ThemeHufflepuff>;
-    // case GrowiThemes.ISLAND:
-    //   return <ThemeIsland colorScheme={colorScheme}>{children}</ThemeIsland>;
-    // case GrowiThemes.JADE_GREEN:
-    //   return <ThemeJadeGreen>{children}</ThemeJadeGreen>;
-    // case GrowiThemes.KIBELA:
-    //   return <ThemeKibela>{children}</ThemeKibela>;
-    // case GrowiThemes.MONO_BLUE:
-    //   return <ThemeMonoBlue>{children}</ThemeMonoBlue>;
-    // case GrowiThemes.NATURE:
-    //   return <ThemeNature>{children}</ThemeNature>;
-    // case GrowiThemes.SPRING:
-    //   return <ThemeSpring colorScheme={colorScheme}>{children}</ThemeSpring>;
-    // case GrowiThemes.WOOD:
-    //   return <ThemeWood colorScheme={colorScheme}>{children}</ThemeWood>;
-    default:
-      return <ThemeDefault>{children}</ThemeDefault>;
-  }
-};

+ 2 - 2
packages/app/src/components/User/SeenUserInfo.module.scss

@@ -1,12 +1,12 @@
 @use '~/styles/bootstrap/init' as bs;
-@use '~/styles/mixins';
+@use '~/styles/atoms/mixins/buttons' as mixins-buttons;
 
 .grw-seen-user-info :global {
   .btn.btn-seen-user {
     $color-seen-user: #549c79;
 
     @include bs.button-outline-variant($color-seen-user, $color-seen-user, rgba(lighten($color-seen-user, 10%), 0.15), rgba(lighten($color-seen-user, 10%), 0.5));
-    @include mixins.button-outline-svg-icon-variant($color-seen-user, $color-seen-user);
+    @include mixins-buttons.button-outline-svg-icon-variant($color-seen-user, $color-seen-user);
 
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled).active {

+ 4 - 0
packages/app/src/interfaces/customize.ts

@@ -1,3 +1,7 @@
 export type IResLayoutSetting = {
   isContainerFluid: boolean,
 };
+
+export type IResGrowiTheme = {
+  theme: string,
+}

+ 3 - 0
packages/app/src/interfaces/in-app-notification.ts

@@ -1,3 +1,5 @@
+import type { IPageSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+
 import { IPage } from './page';
 import { IUser } from './user';
 
@@ -16,6 +18,7 @@ export interface IInAppNotification {
   actionUsers: IUser[]
   createdAt: Date
   snapshot: string
+  parsedSnapshot?: IPageSnapshot
 }
 
 /*

+ 2 - 2
packages/app/src/models/serializers/in-app-notification-snapshot/page.ts

@@ -1,5 +1,5 @@
-import { IUser } from '~/interfaces/user';
-import { IPage } from '~/interfaces/page';
+import type { IPage } from '~/interfaces/page';
+import type { IUser } from '~/interfaces/user';
 
 export interface IPageSnapshot {
   path: string

+ 2 - 4
packages/app/src/pages/_app.page.tsx

@@ -10,7 +10,7 @@ import * as nextI18nConfig from '^/config/next-i18next.config';
 import { ActivatePluginService } from '~/client/services/activate-plugin';
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
-  useAppTitle, useConfidential, useGrowiTheme, useGrowiVersion, useSiteUrl, useCustomizedLogoSrc,
+  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useCustomizedLogoSrc,
 } from '~/stores/context';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
@@ -18,8 +18,7 @@ import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 import { CommonProps } from './utils/commons';
 import { registerTransformerForObjectId } from './utils/objectid-transformer';
 
-import '~/styles/style-next.scss';
-import '~/styles/style-themes.scss';
+import '~/styles/style-app.scss';
 
 
 const isDev = process.env.NODE_ENV === 'development';
@@ -56,7 +55,6 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useAppTitle(commonPageProps.appTitle);
   useSiteUrl(commonPageProps.siteUrl);
   useConfidential(commonPageProps.confidential);
-  useGrowiTheme(commonPageProps.theme);
   useGrowiVersion(commonPageProps.growiVersion);
   useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
 

+ 46 - 5
packages/app/src/pages/_document.page.tsx

@@ -1,6 +1,8 @@
 /* eslint-disable @next/next/google-font-display */
 import React from 'react';
 
+import type { PresetThemesManifest } from '@growi/preset-themes';
+import { getManifestKeyFromTheme } from '@growi/preset-themes';
 import mongoose from 'mongoose';
 import Document, {
   DocumentContext, DocumentInitialProps,
@@ -8,13 +10,39 @@ import Document, {
 } from 'next/document';
 
 import { ActivatePluginService, GrowiPluginManifestEntries } from '~/client/services/activate-plugin';
-import { CrowiRequest } from '~/interfaces/crowi-request';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { GrowiPlugin, GrowiPluginResourceType } from '~/interfaces/plugin';
+import type { GrowiThemes } from '~/interfaces/theme';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:page:_document');
+
+type HeadersForPresetThemesProps = {
+  theme: GrowiThemes,
+  manifest: PresetThemesManifest,
+}
+const HeadersForPresetThemes = (props: HeadersForPresetThemesProps): JSX.Element => {
+  const { theme, manifest } = props;
+
+  let themeKey = getManifestKeyFromTheme(theme);
+  if (!(themeKey in manifest)) {
+    logger.warn(`The key for '${theme} does not exist in preset-themes manifest`);
+    themeKey = getManifestKeyFromTheme('default');
+  }
+  const href = `/static/preset-themes/${manifest[themeKey].file}`; // configured by express.static
+
+  const elements: JSX.Element[] = [];
+
+  elements.push(
+    <link rel="stylesheet" key={`link_preset-themes-${theme}`} href={href} />,
+  );
+
+  return <>{elements}</>;
+};
 
 type HeadersForGrowiPluginProps = {
   pluginManifestEntries: GrowiPluginManifestEntries;
 }
-
 const HeadersForGrowiPlugin = (props: HeadersForGrowiPluginProps): JSX.Element => {
   const { pluginManifestEntries } = props;
 
@@ -48,7 +76,9 @@ const HeadersForGrowiPlugin = (props: HeadersForGrowiPluginProps): JSX.Element =
 };
 
 interface GrowiDocumentProps {
+  theme: GrowiThemes,
   customCss: string;
+  presetThemesManifest: PresetThemesManifest,
   pluginManifestEntries: GrowiPluginManifestEntries;
 }
 declare type GrowiDocumentInitialProps = DocumentInitialProps & GrowiDocumentProps;
@@ -58,18 +88,28 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
   static override async getInitialProps(ctx: DocumentContext): Promise<GrowiDocumentInitialProps> {
     const initialProps: DocumentInitialProps = await Document.getInitialProps(ctx);
     const { crowi } = ctx.req as CrowiRequest<any>;
-    const { customizeService } = crowi;
+    const { configManager, customizeService } = crowi;
+
+    const theme = configManager.getConfig('crowi', 'customize:theme');
     const customCss: string = customizeService.getCustomCss();
 
+    // import preset-themes manifest
+    const presetThemesManifest = await import('@growi/preset-themes/dist/themes/manifest.json').then(imported => imported.default);
+
+    // retrieve plugin manifests
     const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
     const growiPlugins = await GrowiPlugin.find({ isEnabled: true });
     const pluginManifestEntries: GrowiPluginManifestEntries = await ActivatePluginService.retrievePluginManifests(growiPlugins);
 
-    return { ...initialProps, customCss, pluginManifestEntries };
+    return {
+      ...initialProps, theme, customCss, presetThemesManifest, pluginManifestEntries,
+    };
   }
 
   override render(): JSX.Element {
-    const { customCss, pluginManifestEntries } = this.props;
+    const {
+      customCss, theme, presetThemesManifest, pluginManifestEntries,
+    } = this.props;
 
     return (
       <Html>
@@ -87,6 +127,7 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
           <link rel='preload' href="/static/fonts/Lato-Regular-latin-ext.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/Lato-Bold-latin.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/Lato-Bold-latin-ext.woff2" as="font" type="font/woff2" />
+          <HeadersForPresetThemes theme={theme} manifest={presetThemesManifest} />
           <HeadersForGrowiPlugin pluginManifestEntries={pluginManifestEntries} />
         </Head>
         <body>

+ 1 - 1
packages/app/src/pages/admin/plugins.page.tsx

@@ -27,7 +27,7 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   useIsMaintenanceMode(props.isMaintenanceMode);
   useCurrentUser(props.currentUser ?? null);
 
-  const title = 'Plugins Extention';
+  const title = 'Plugins Extension';
   const injectableContainers: Container<any>[] = [];
 
   if (isClient()) {

+ 0 - 3
packages/app/src/pages/utils/commons.ts

@@ -7,7 +7,6 @@ import { SSRConfig, UserConfig } from 'next-i18next';
 import * as nextI18NextConfig from '^/config/next-i18next.config';
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { GrowiThemes } from '~/interfaces/theme';
 
 export type CommonProps = {
   namespacesRequired: string[], // i18next
@@ -15,7 +14,6 @@ export type CommonProps = {
   appTitle: string,
   siteUrl: string,
   confidential: string,
-  theme: GrowiThemes,
   customTitleTemplate: string,
   csrfToken: string,
   isContainerFluid: boolean,
@@ -55,7 +53,6 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     appTitle: appService.getAppTitle(),
     siteUrl: configManager.getConfig('crowi', 'app:siteUrl'), // DON'T USE appService.getSiteUrl()
     confidential: appService.getAppConfidential() || '',
-    theme: configManager.getConfig('crowi', 'customize:theme'),
     customTitleTemplate: customizeService.customTitleTemplate,
     csrfToken: req.csrfToken(),
     isContainerFluid: configManager.getConfig('crowi', 'customize:isContainerFluid') ?? false,

+ 5 - 0
packages/app/src/server/crowi/express-init.js

@@ -1,9 +1,11 @@
+import { manifestPath as presetThemesManifestPath } from '@growi/preset-themes';
 import csrf from 'csurf';
 import mongoose from 'mongoose';
 
 import { i18n, localePath } from '^/config/next-i18next.config';
 
 import loggerFactory from '~/utils/logger';
+import { resolveFromRoot } from '~/utils/project-dir-utils';
 
 const logger = loggerFactory('growi:crowi:express-init');
 
@@ -115,6 +117,9 @@ module.exports = function(crowi, app) {
 
   const staticOption = (crowi.node_env === 'production') ? { maxAge: '30d' } : {};
   app.use(express.static(crowi.publicDir, staticOption));
+  app.use('/static/preset-themes', express.static(
+    resolveFromRoot(`../../node_modules/@growi/preset-themes/${path.dirname(presetThemesManifestPath)}`),
+  ));
   app.use('/plugins', express.static(path.resolve(__dirname, '../../../tmp/plugins')));
 
   app.engine('html', swig.renderFile);

+ 12 - 40
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -108,11 +108,8 @@ module.exports = (crowi) => {
     layout: [
       body('isContainerFluid').isBoolean(),
     ],
-    themeAssetPath: [
-      query('themeName').isString(),
-    ],
     theme: [
-      body('themeType').isString(),
+      body('theme').isString(),
     ],
     sidebar: [
       body('isSidebarDrawerMode').isBoolean(),
@@ -175,7 +172,6 @@ module.exports = (crowi) => {
    */
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
     const customizeParams = {
-      themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
       isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
       pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
@@ -272,41 +268,17 @@ module.exports = (crowi) => {
     }
   });
 
-  /**
-   * @swagger
-   *
-   *    /customize-setting/theme/asset-path:
-   *      put:
-   *        tags: [CustomizeSetting]
-   *        operationId: getThemeAssetPath
-   *        summary: /customize-setting/theme/asset-path
-   *        description: Get theme asset path
-   *        parameters:
-   *          - name: themeName
-   *            in: query
-   *            required: true
-   *            schema:
-   *              type: string
-   *        responses:
-   *          200:
-   *            description: Succeeded to get theme asset path
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    assetPath:
-   *                      type: string
-   */
-  router.get('/theme/asset-path', loginRequiredStrictly, adminRequired, validator.themeAssetPath, apiV3FormValidator, async(req, res) => {
-    const { themeName } = req.query;
-
-    const webpackAssetKey = `styles/theme-${themeName}.css`;
-    const assetPath = res.locals.webpack_asset(webpackAssetKey);
+  router.get('/theme', loginRequiredStrictly, adminRequired, async(req, res) => {
 
-    if (assetPath == null) {
-      return res.apiv3Err(new ErrorV3(`The asset for '${webpackAssetKey}' is undefined.`, 'invalid-asset'));
+    try {
+      const theme = await crowi.configManager.getConfig('crowi', 'customize:theme');
+      return res.apiv3({ theme });
+    }
+    catch (err) {
+      const msg = 'Error occurred in getting theme';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'get-theme-failed'));
     }
-    return res.apiv3({ assetPath });
   });
 
   /**
@@ -334,13 +306,13 @@ module.exports = (crowi) => {
    */
   router.put('/theme', loginRequiredStrictly, adminRequired, addActivity, validator.theme, apiV3FormValidator, async(req, res) => {
     const requestParams = {
-      'customize:theme': req.body.themeType,
+      'customize:theme': req.body.theme,
     };
 
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
-        themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
+        theme: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_THEME_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);

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

@@ -104,7 +104,7 @@ module.exports = (crowi, app) => {
     userActivation.validateCompleteRegistration,
     userActivation.completeRegistrationAction(crowi));
 
-  router.use('/plugins-extention', require('./plugins-extention')(crowi));
+  router.use('/plugins-extension', require('./plugins-extension')(crowi));
 
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 

+ 0 - 0
packages/app/src/server/routes/apiv3/plugins-extention.ts → packages/app/src/server/routes/apiv3/plugins-extension.ts


+ 12 - 12
packages/app/src/server/routes/login.js

@@ -102,18 +102,18 @@ module.exports = function(crowi, app) {
 
   actions.preLogin = function(req, res, next) {
     // user has already logged in
-    // const { user } = req;
-    // if (user != null && user.status === User.STATUS_ACTIVE) {
-    //   const { redirectTo } = req.session;
-    //   // remove session.redirectTo
-    //   delete req.session.redirectTo;
-    //   return res.safeRedirect(redirectTo);
-    // }
-
-    // // set referer to 'redirectTo'
-    // if (req.session.redirectTo == null && req.headers.referer != null) {
-    //   req.session.redirectTo = req.headers.referer;
-    // }
+    const { user } = req;
+    if (user != null && user.status === User.STATUS_ACTIVE) {
+      const { redirectTo } = req.session;
+      // remove session.redirectTo
+      delete req.session.redirectTo;
+      return res.safeRedirect(redirectTo);
+    }
+
+    // set referer to 'redirectTo'
+    if (req.session.redirectTo == null && req.headers.referer != null) {
+      req.session.redirectTo = req.headers.referer;
+    }
 
     next();
   };

+ 31 - 4
packages/app/src/stores/admin/customize.tsx

@@ -1,10 +1,11 @@
 import { useCallback } from 'react';
 
-import useSWR, { SWRResponse } from 'swr';
+import { SWRResponse } from 'swr';
+import useSWRImmutable from 'swr/immutable';
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
-import { updateConfigMethodForAdmin } from '~/interfaces/admin';
-import { IResLayoutSetting } from '~/interfaces/customize';
+import type { updateConfigMethodForAdmin } from '~/interfaces/admin';
+import type { IResLayoutSetting, IResGrowiTheme } from '~/interfaces/customize';
 
 export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> & updateConfigMethodForAdmin<IResLayoutSetting> => {
 
@@ -13,7 +14,7 @@ export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> &
     return res.data;
   }, []);
 
-  const swrResponse = useSWR('/customize-setting/layout', fetcher);
+  const swrResponse = useSWRImmutable('/customize-setting/layout', fetcher);
 
   const update = useCallback(async(layoutSetting: IResLayoutSetting) => {
     await apiv3Put('/customize-setting/layout', layoutSetting);
@@ -25,3 +26,29 @@ export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> &
     update,
   };
 };
+
+export const useSWRxGrowiTheme = (): SWRResponse<string, Error> => {
+
+  const fetcher = useCallback(async() => {
+    const res = await apiv3Get<IResGrowiTheme>('/customize-setting/theme');
+    return res.data.theme;
+  }, []);
+
+  const swrResponse = useSWRImmutable('/customize-setting/theme', fetcher);
+
+  const update = async(theme: string) => {
+    await apiv3Put('/customize-setting/layout', { theme });
+    await swrResponse.mutate();
+    // The updateFn should be a promise or asynchronous function to handle the remote mutation
+    // it should return updated data. see: https://swr.vercel.app/docs/mutation#optimistic-updates
+    // Moreover, `async() => false` does not work since it's too fast to be calculated.
+    await swrResponse.mutate(new Promise(r => setTimeout(() => r(theme), 10)), { optimisticData: () => theme });
+  };
+
+  return Object.assign(
+    swrResponse,
+    {
+      update,
+    },
+  );
+};

+ 0 - 4
packages/app/src/stores/context.tsx

@@ -37,10 +37,6 @@ export const useConfidential = (initialData?: string): SWRResponse<string, Error
   return useContextSWR('confidential', initialData);
 };
 
-export const useGrowiTheme = (initialData?: GrowiThemes): SWRResponse<GrowiThemes, Error> => {
-  return useContextSWR('theme', initialData);
-};
-
 export const useCurrentUser = (initialData?: Nullable<IUser>): SWRResponse<Nullable<IUser>, Error> => {
   return useContextSWR<Nullable<IUser>, Error>('currentUser', initialData);
 };

+ 21 - 2
packages/app/src/stores/in-app-notification.ts

@@ -1,7 +1,15 @@
 import useSWR, { SWRResponse } from 'swr';
-import { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+
+import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+import { parseSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+import loggerFactory from '~/utils/logger';
+
 import { apiv3Get } from '../client/util/apiv3-client';
 
+const logger = loggerFactory('growi:cli:InAppNotification');
+
+type inAppNotificationPaginateResult = PaginateResult<IInAppNotification>
+
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxInAppNotifications = <Data, Error>(
   limit: number,
@@ -10,7 +18,18 @@ export const useSWRxInAppNotifications = <Data, Error>(
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
     ['/in-app-notification/list', limit, offset, status],
-    endpoint => apiv3Get(endpoint, { limit, offset, status }).then(response => response.data),
+    endpoint => apiv3Get(endpoint, { limit, offset, status }).then((response) => {
+      const inAppNotificationPaginateResult = response.data as inAppNotificationPaginateResult;
+      inAppNotificationPaginateResult.docs.forEach((doc) => {
+        try {
+          doc.parsedSnapshot = parseSnapshot(doc.snapshot as string);
+        }
+        catch (err) {
+          logger.warn('Failed to parse snapshot', err);
+        }
+      });
+      return inAppNotificationPaginateResult;
+    }),
   );
 };
 

+ 0 - 59
packages/app/src/styles/_mixins.scss

@@ -141,65 +141,6 @@
   }
 }
 
-@mixin button-svg-icon-variant($background, $hover-background: darken($background, 7.5%), $active-background: darken($background, 10%)) {
-  svg {
-    fill: color-yiq($background);
-  }
-
-  @include bs.hover() {
-    svg {
-      fill: color-yiq($hover-background);
-    }
-  }
-
-  &:focus,
-  &.focus {
-    svg {
-      fill: color-yiq($hover-background);
-    }
-  }
-
-  // Disabled comes first so active can properly restyle
-  &.disabled,
-  &:disabled {
-    svg {
-      fill: color-yiq($background);
-    }
-  }
-
-  &:not(:disabled):not(.disabled):active,
-  &:not(:disabled):not(.disabled).active,
-  .show > &.dropdown-toggle {
-    svg {
-      fill: color-yiq($active-background);
-    }
-  }
-}
-
-@mixin button-outline-svg-icon-variant($value, $color-hover: $value) {
-  svg {
-    fill: $value;
-  }
-  @include bs.hover() {
-    svg {
-      fill: $color-hover;
-    }
-  }
-  &.disabled,
-  &:disabled {
-    svg {
-      fill: $value;
-    }
-  }
-  &:not(:disabled):not(.disabled):active,
-  &:not(:disabled):not(.disabled).active,
-  .show > &.dropdown-toggle {
-    svg {
-      fill: $color-hover;
-    }
-  }
-}
-
 @mixin overlay-processing-style($additionalSelector, $contentFontSize: inherit, $contentPadding: inherit) {
   .overlay.#{$additionalSelector} {
     background: rgba(255, 255, 255, 0.5);

+ 0 - 43
packages/app/src/styles/_page.scss

@@ -1,43 +0,0 @@
-// // import diff2html styles
-// @import '~/diff2html/bundles/css/diff2html.min.css';
-
-/**
- * for table with handsontable modal button
- */
-.editable-with-handsontable {
-  position: relative;
-
-  .handsontable-modal-trigger {
-    position: absolute;
-    top: 11px;
-    right: 10px;
-    padding: 0;
-    font-size: 16px;
-    line-height: 1;
-    vertical-align: bottom;
-    background-color: transparent;
-    border: none;
-    opacity: 0;
-  }
-
-  .page-mobile & .handsontable-modal-trigger {
-    opacity: 0.3;
-  }
-
-  &:hover .handsontable-modal-trigger {
-    opacity: 1;
-  }
-}
-
-/**
- * for drawio with drawio iframe button
- */
-.editable-with-drawio {
-  .drawio-iframe-trigger {
-    top: 11px;
-    right: 10px;
-    z-index: 14;
-    font-size: 12px;
-    line-height: 1;
-  }
-}

+ 0 - 32
packages/app/src/styles/_vendor.scss

@@ -1,32 +0,0 @@
-// import bootstrap configurations
-@import '~bootstrap/scss/functions';
-@import '~bootstrap/scss/variables';
-@import '~bootstrap/scss/mixins';
-@import '~bootstrap/scss/utilities';
-@import '~bootstrap/scss/root';
-
-// increase specificity with ':root' for GROWI theming
-:root {
-  // import bootstrap
-  @import '~bootstrap/scss/bootstrap';
-  // import toastr styles
-  @import '~toastr/build/toastr';
-}
-
-// import react-bootstrap-typeahead
-@import '~react-bootstrap-typeahead/css/Typeahead';
-
-// import CodeMirror styles
-@import '~codemirror/lib/codemirror.css';
-@import '~codemirror/addon/hint/show-hint.css';
-@import '~codemirror/theme/elegant.css';
-@import '~codemirror/theme/eclipse.css';
-
-// import Handsontable styles
-@import '~handsontable/dist/handsontable.full.css';
-
-// import SimpleBar styles
-@import '~simplebar/dist/simplebar.min.css';
-
-// Emoji-mart style
-@import '~emoji-mart/css/emoji-mart.css';

+ 60 - 0
packages/app/src/styles/atoms/mixins/_buttons.scss

@@ -0,0 +1,60 @@
+@use '../../bootstrap/init' as bs;
+
+@mixin button-svg-icon-variant($background, $hover-background: darken($background, 7.5%), $active-background: darken($background, 10%)) {
+  svg {
+    fill: color-yiq($background);
+  }
+
+  @include bs.hover() {
+    svg {
+      fill: color-yiq($hover-background);
+    }
+  }
+
+  &:focus,
+  &.focus {
+    svg {
+      fill: color-yiq($hover-background);
+    }
+  }
+
+  // Disabled comes first so active can properly restyle
+  &.disabled,
+  &:disabled {
+    svg {
+      fill: color-yiq($background);
+    }
+  }
+
+  &:not(:disabled):not(.disabled):active,
+  &:not(:disabled):not(.disabled).active,
+  .show > &.dropdown-toggle {
+    svg {
+      fill: color-yiq($active-background);
+    }
+  }
+}
+
+@mixin button-outline-svg-icon-variant($value, $color-hover: $value) {
+  svg {
+    fill: $value;
+  }
+  @include bs.hover() {
+    svg {
+      fill: $color-hover;
+    }
+  }
+  &.disabled,
+  &:disabled {
+    svg {
+      fill: $value;
+    }
+  }
+  &:not(:disabled):not(.disabled):active,
+  &:not(:disabled):not(.disabled).active,
+  .show > &.dropdown-toggle {
+    svg {
+      fill: $color-hover;
+    }
+  }
+}

+ 34 - 3
packages/app/src/styles/bootstrap/_apply.scss

@@ -1,9 +1,40 @@
 @use 'init' as *;
 
-@import '~bootstrap/scss/utilities';
+// apply bootstrap
 @import '~bootstrap/scss/root';
+@import '~bootstrap/scss/reboot';
+@import '~bootstrap/scss/type';
+@import '~bootstrap/scss/images';
+@import '~bootstrap/scss/code';
+@import '~bootstrap/scss/grid';
+@import '~bootstrap/scss/tables';
+@import '~bootstrap/scss/forms';
+@import '~bootstrap/scss/buttons';
+@import '~bootstrap/scss/transitions';
+@import '~bootstrap/scss/dropdown';
+@import '~bootstrap/scss/button-group';
+@import '~bootstrap/scss/input-group';
+@import '~bootstrap/scss/custom-forms';
+@import '~bootstrap/scss/nav';
+@import '~bootstrap/scss/navbar';
+@import '~bootstrap/scss/card';
+@import '~bootstrap/scss/breadcrumb';
+@import '~bootstrap/scss/pagination';
+@import '~bootstrap/scss/badge';
+@import '~bootstrap/scss/jumbotron';
+@import '~bootstrap/scss/alert';
+@import '~bootstrap/scss/progress';
+@import '~bootstrap/scss/media';
+@import '~bootstrap/scss/list-group';
+@import '~bootstrap/scss/close';
+@import '~bootstrap/scss/toasts';
+@import '~bootstrap/scss/modal';
+@import '~bootstrap/scss/tooltip';
+@import '~bootstrap/scss/popover';
+@import '~bootstrap/scss/carousel';
+@import '~bootstrap/scss/spinners';
+@import '~bootstrap/scss/utilities';
+@import '~bootstrap/scss/print';
 
-// import bootstrap
-@import '~bootstrap/scss/bootstrap';
 // override
 @import './override';

+ 36 - 58
packages/app/src/styles/style-app.scss

@@ -1,81 +1,59 @@
-// import variables
-@import 'variables';
+@import './bootstrap/apply';
 
 @import 'mixins';
-@import 'override-bootstrap-variables';
 
-// vendor
-@import 'vendor';
+// react-bootstrap-typeahead
+@import '~react-bootstrap-typeahead/css/Typeahead';
+@import 'override-rbt';
 
-// override bootstrap
-@import 'override-bootstrap';
+// SimpleBar
+@import '~simplebar/dist/simplebar.min.css';
+@import 'override-simplebar';
 
-// override codemirror
-@import 'override-codemirror';
+// KaTeX
+@import '~katex/dist/katex.min';
 
-// override react-bootstrap-typeahead styles
-@import 'override-rbt';
+// icons
 
-// override simplebar-react styles
-@import 'override-simplebar';
+// DO NOT CHANGE THER OERDER OF font-awesome AND simple-line-icons.
+// font-familiy used in simple-line-icons has to be prioritized than the one used in font-awesome.
+@import '~font-awesome';
+@import '~simple-line-icons';
+@import '~material-icons/iconfont/filled';
+@import '~@icon/themify-icons/themify-icons';
 
 // atoms
 @import 'atoms/buttons';
-@import 'atoms/code';
-@import 'atoms/nav';
-@import 'atoms/pre';
 @import 'atoms/spinners';
 @import 'atoms/custom_control';
+@import 'atoms/code';
 
 // molecules
-@import 'molecules/copy-dropdown';
-@import 'molecules/page-editor-mode-manager';
-@import 'molecules/slack-notification';
-@import 'molecules/duplicated-paths-table.scss';
-
-// growi component
-@import 'admin';
-@import 'attachments';
-@import 'comment';
-@import 'comment_growi';
-@import 'drawio';
-@import 'create-page';
-@import 'draft';
-@import 'editor-attachment';
-@import 'editor-navbar';
-@import 'page-content-footer';
-// @import 'handsontable';
+@import 'molecules/toastr';
+// @import 'molecules/slack-notification';
+// @import 'molecules/duplicated-paths-table.scss';
+
+// organisms
+@import 'organisms/wiki';
+
+// // growi component
+// @import 'attachments';
+// @import 'comment';
+// @import 'comment_growi';
+// @import 'draft';
+@import 'editor';
+@import 'fonts';
 @import 'layout';
-@import 'login';
-@import 'me';
+// @import 'me';
 @import 'mirror_mode';
 @import 'modal';
-@import 'navbar';
-@import 'old-ios';
-@import 'on-edit';
-@import 'page-duplicate-modal';
-@import 'page_list';
-@import 'page-accessories-control';
-@import 'page-accessories-modal';
+// @import 'old-ios';
+// @import 'page-duplicate-modal';
 @import 'page-path';
-@import 'page-tree';
-@import 'page';
-@import 'page-presentation';
-@import 'page-history';
-@import 'recent-changes';
 @import 'search';
-@import 'shortcuts';
-@import 'sidebar';
-@import 'sidebar-wiki';
-@import 'subnav';
 @import 'tag';
-@import 'toc';
-@import 'user';
-@import 'staff_credit';
-@import 'waves';
-@import 'wiki';
-@import 'sharelink';
-@import 'linkedit-preview';
+// @import 'user';
+
 
 /*
  * for Guest User Mode

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

@@ -1,149 +0,0 @@
-@import './bootstrap/apply';
-
-@import 'mixins';
-
-// // override codemirror
-// @import 'override-codemirror';
-
-// react-bootstrap-typeahead
-@import '~react-bootstrap-typeahead/css/Typeahead';
-@import 'override-rbt';
-
-// SimpleBar
-@import '~simplebar/dist/simplebar.min.css';
-@import 'override-simplebar';
-
-// KaTeX
-@import '~katex/dist/katex.min';
-
-// icons
-
-// DO NOT CHANGE THER OERDER OF font-awesome AND simple-line-icons.
-// font-familiy used in simple-line-icons has to be prioritized than the one used in font-awesome.
-@import '~font-awesome';
-@import '~simple-line-icons';
-@import '~material-icons/iconfont/filled';
-@import '~@icon/themify-icons/themify-icons';
-
-// atoms
-@import 'atoms/buttons';
-@import 'atoms/spinners';
-@import 'atoms/custom_control';
-@import 'atoms/code';
-
-// molecules
-@import 'molecules/toastr';
-// @import 'molecules/copy-dropdown';
-// @import 'molecules/slack-notification';
-// @import 'molecules/duplicated-paths-table.scss';
-
-// organisms
-@import 'organisms/wiki';
-
-// // growi component
-// @import 'attachments';
-// @import 'comment';
-// @import 'comment_growi';
-// @import 'create-page';
-// @import 'draft';
-@import 'editor';
-@import 'fonts';
-// @import 'handsontable';
-@import 'layout';
-// @import 'me';
-@import 'mirror_mode';
-@import 'modal';
-// @import 'old-ios';
-// @import 'page-duplicate-modal';
-@import 'page-path';
-// @import 'page';
-@import 'search';
-@import 'tag';
-// @import 'user';
-
-
-/*
- * for Guest User Mode
- */
-// TODO: reactify and replace with `grw-not-available-for-guest`
-.dropdown-toggle.dropdown-toggle-disabled {
-  cursor: not-allowed;
-}
-
-// TODO: reactify and replace with `grw-not-available-for-guest`
-.edit-button.edit-button-disabled {
-  cursor: not-allowed;
-}
-
-.grw-not-available-for-guest {
-  cursor: not-allowed !important;
-}
-
-/*
- * Helper Classes
- */
-
-.mw-0 {
-  min-width: 0;
-}
-
-.flex-basis-0 {
-  flex-basis: 0;
-}
-
-.picture {
-  width: 24px;
-  height: 24px;
-
-  // size list
-  &.picture-lg {
-    width: 48px;
-    height: 48px;
-  }
-
-  &.picture-md {
-    width: 24px;
-    height: 24px;
-  }
-
-  &.picture-sm {
-    width: 18px;
-    height: 18px;
-  }
-
-  &.picture-xs {
-    width: 14px;
-    height: 14px;
-  }
-}
-
-// transplant from FontAwesome
-.icon-fw {
-  display: inline-block;
-  width: 1.4em;
-  text-align: left;
-}
-
-.cmd-key.mac {
-  &:after {
-    content: '⌘';
-  }
-}
-
-.cmd-key.win {
-  &:after {
-    content: 'Ctrl';
-  }
-}
-
-.grw-page-control-dropdown-item {
-  display: flex !important;
-  align-items: center;
-
-  .grw-page-control-dropdown-icon {
-    display: flex;
-    justify-content: center;
-    width: 25px;
-  }
-
-}

+ 0 - 15
packages/app/src/styles/style-themes.scss

@@ -1,15 +0,0 @@
-// @import '~/components/Theme/ThemeAntarctic.global.scss';
-// @import '~/components/Theme/ThemeBlackboard.global.scss';
-// @import '~/components/Theme/ThemeChristmas.global.scss';
-@import '~/components/Theme/ThemeDefault.global.scss';
-// @import '~/components/Theme/ThemeFireRed.global.scss';
-// @import '~/components/Theme/ThemeFuture.global.scss';
-// @import '~/components/Theme/ThemeHalloween.global.scss';
-// @import '~/components/Theme/ThemeHufflepuff.global.scss';
-// @import '~/components/Theme/ThemeIsland.global.scss';
-// @import '~/components/Theme/ThemeJadeGreen.global.scss';
-// @import '~/components/Theme/ThemeKibela.global.scss';
-// @import '~/components/Theme/ThemeMonoBlue.global.scss';
-// @import '~/components/Theme/ThemeNature.global.scss';
-// @import '~/components/Theme/ThemeSpring.global.scss';
-// @import '~/components/Theme/ThemeWood.global.scss';

+ 2 - 2
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -1,6 +1,6 @@
 @use '../variables' as var;
 @use '../bootstrap/init' as *;
-@use '../mixins';
+@use '../atoms/mixins/buttons' as mixins-buttons;
 @use './mixins/count-badge';
 
 // determine optional variables
@@ -301,7 +301,7 @@ ul.pagination {
       lighten($bgcolor-sidebar-context, 24%)
     );
     .grw-pagetree-triangle-btn {
-      @include mixins.button-outline-svg-icon-variant($secondary, $gray-200);
+      @include mixins-buttons.button-outline-svg-icon-variant($secondary, $gray-200);
     }
     .btn-page-item-control {
       @include button-outline-variant($gray-500, $gray-500, $secondary, transparent);

+ 2 - 2
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -1,6 +1,6 @@
 @use '../variables' as var;
 @use '../bootstrap/init' as *;
-@use '../mixins';
+@use '../atoms/mixins/buttons' as mixins-buttons;
 @use './mixins/count-badge';
 
 // determine optional variables
@@ -201,7 +201,7 @@ $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
       darken($bgcolor-sidebar-context, 24%)
     );
     .grw-pagetree-triangle-btn {
-      @include mixins.button-outline-svg-icon-variant($gray-400, $primary);
+      @include mixins-buttons.button-outline-svg-icon-variant($gray-400, $primary);
     }
   }
   .private-legacy-pages-link {

+ 1 - 1
packages/app/src/styles/theme/_apply-colors.scss

@@ -570,7 +570,7 @@ body.editing-sidebar {
 .page-comment-meta .page-comment-revision svg {
   fill: $color-link;
 
-  &:hover() {
+  &:hover {
     fill: $color-link-hover;
   }
 }

+ 1 - 1
packages/app/src/styles/theme/_reboot-bootstrap-colors.scss

@@ -14,7 +14,7 @@
 // 3. Set an explicit initial text-align value so that we can later use
 //    the `inherit` value on things like `<th>` elements.
 
-& {
+&, body {
   color: $body-color;
   background-color: $body-bg; // 2
 

+ 3 - 3
packages/app/src/styles/theme/_reboot-bootstrap-theme-colors.scss

@@ -1,5 +1,5 @@
 @use '../bootstrap/init' as *;
-@use '../mixins';
+@use '../atoms/mixins/buttons' as mixins-buttons;
 
 @each $color, $value in $theme-colors {
   @include bg-variant('.bg-#{$color}', $value);
@@ -18,7 +18,7 @@
 @each $color, $value in $theme-colors {
   .btn-#{$color} {
     @include button-variant($value, $value);
-    @include mixins.button-svg-icon-variant($value, $value);
+    @include mixins-buttons.button-svg-icon-variant($value, $value);
     box-shadow: none !important;
   }
 }
@@ -26,7 +26,7 @@
 @each $color, $value in $theme-colors {
   .btn-outline-#{$color} {
     @include button-outline-variant($value, $color-hover: $value, $active-background: rgba($value, 0.1), $active-border: $value);
-    @include mixins.button-outline-svg-icon-variant($value, $color-hover: $value);
+    @include mixins-buttons.button-outline-svg-icon-variant($value, $color-hover: $value);
 
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled).active {

+ 0 - 0
packages/app/test/cypress/integration/10-install/install.spec.ts → packages/app/test/cypress/integration/10-install/10-install--install.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts → packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/20-basic-features/access-to-pagelist.spec.ts → packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-pagelist.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/20-basic-features/click-page-icons.spec.ts → packages/app/test/cypress/integration/20-basic-features/20-basic-features--click-page-icons.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts → packages/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts → packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/30-search/search.spec.ts → packages/app/test/cypress/integration/30-search/30-search--search.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/40-admin/access-to-admin-page.spec.ts → packages/app/test/cypress/integration/40-admin/40-admin--access-to-admin-page.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts → packages/app/test/cypress/integration/50-sidebar/50-sidebar--access-to-side-bar.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/50-sidebar/switching-sidebar-mode.spec.ts → packages/app/test/cypress/integration/50-sidebar/50-sidebar--switching-sidebar-mode.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/60-home/home.spec.ts → packages/app/test/cypress/integration/60-home/60-home--home.spec.ts


+ 0 - 33
packages/app/test/cypress/plugins/index.ts

@@ -1,33 +0,0 @@
-/// <reference types="cypress" />
-// ***********************************************************
-// This example plugins/index.js can be used to load plugins
-//
-// You can change the location of this file or turn off loading
-// the plugins file with the 'pluginsFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/plugins-guide
-// ***********************************************************
-
-// This function is called when a project is opened or re-opened (e.g. due to
-// the project's config changing)
-
-/**
- * @type {Cypress.PluginConfig}
- */
-// eslint-disable-next-line no-unused-vars
-module.exports = (on, config) => {
-  // `on` is used to hook into various events Cypress emits
-  // `config` is the resolved Cypress config
-
-  // change screen size
-  // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
-  on('before:browser:launch', (browser, launchOptions) => {
-    if (browser.name === 'chrome' && browser.isHeadless) {
-      launchOptions.args.push('--window-size=1400,1024')
-      launchOptions.args.push('--force-device-scale-factor=1')
-    }
-
-    return launchOptions
-  })
-}

+ 1 - 2
packages/hackmd/package.json

@@ -10,7 +10,6 @@
   "dependencies": {},
   "devDependencies": {
     "penpal": "^4.0.0",
-    "throttle-debounce": "^3.0.1",
-    "vite": "^3.1.0"
+    "throttle-debounce": "^3.0.1"
   }
 }

+ 0 - 1
packages/hackmd/src/hackmd-styles.js

@@ -19,7 +19,6 @@ const styles = '<%= styles %>'; // will be replaced by ejs
  */
 function insertStyle() {
   const element = document.createElement('style');
-  element.type = 'text/css';
   element.appendChild(document.createTextNode(unescape(styles)));
   document.getElementsByTagName('head')[0].appendChild(element);
 }

+ 1 - 1
packages/hackmd/src/index.js

@@ -11,5 +11,5 @@ const stylesCSSFile = fs.readFileSync(path.resolve(__dirname, `${dirPath}/styles
 module.exports = {
   stylesJS: stylesJSFile.toString(),
   agentJS: agentJSFile.toString(),
-  stylesCSS: stylesCSSFile.toString(),
+  stylesCSS: stylesCSSFile.toString().replace(/(\r\n|\n|\r)/gm, ''), // https://stackoverflow.com/questions/10805125/how-to-remove-all-line-breaks-from-a-string
 };

+ 2 - 0
packages/preset-themes/.eslintignore

@@ -0,0 +1,2 @@
+/dist/**
+*.d.ts

+ 18 - 0
packages/preset-themes/.eslintrc.js

@@ -0,0 +1,18 @@
+module.exports = {
+  extends: [
+    'weseek/react',
+    'weseek/typescript',
+  ],
+  env: {
+  },
+  globals: {
+  },
+  settings: {
+    // resolve path aliases by eslint-import-resolver-typescript
+    'import/resolver': {
+      typescript: {},
+    },
+  },
+  rules: {
+  },
+};

+ 1 - 0
packages/preset-themes/.gitignore

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

+ 1 - 0
packages/preset-themes/README.md

@@ -0,0 +1 @@
+# GROWI Preset Themes

+ 27 - 0
packages/preset-themes/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "@growi/preset-themes",
+  "description": "GROWI preset themes",
+  "version": "6.0.0-RC.9",
+  "license": "MIT",
+  "main": "dist/libs/index.js",
+  "files": ["dist"],
+  "scripts": {
+    "build": "yarn build:libs & yarn build:themes",
+    "build:w": "yarn build:libs -w & yarn build:themes -w",
+    "build:libs": "vite -c vite.libs.config.ts build",
+    "build:themes": "vite -c vite.themes.config.ts build",
+    "lint:eslint": "eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
+    "lint:styles": "stylelint src/**/*.scss",
+    "lint": "run-p lint:*",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-innertext": "^1.1.5"
+  },
+  "devDependencies": {
+    "bootstrap": "^4.6.1",
+    "sass": "^1.55.0"
+  }
+}

+ 0 - 0
packages/app/public/images/themes/antarctic/bg.svg → packages/preset-themes/public/images/antarctic/bg.svg


+ 0 - 0
packages/app/public/images/themes/antarctic/topimage.svg → packages/preset-themes/public/images/antarctic/topimage.svg


+ 0 - 0
packages/app/public/images/themes/christmas/christmas-navbar.jpg → packages/preset-themes/public/images/christmas/christmas-navbar.jpg


+ 0 - 0
packages/app/public/images/themes/christmas/christmas.jpg → packages/preset-themes/public/images/christmas/christmas.jpg


Some files were not shown because too many files changed in this diff