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

Merge branch 'master' into feat/180264-180289-add-growi-cloud-link-to-ai-integration

Shun Miyazawa 1 день назад
Родитель
Сommit
2ab935b371
66 измененных файлов с 1023 добавлено и 573 удалено
  1. 0 5
      .changeset/eight-zebras-design.md
  2. 21 0
      .claude/rules/coding-style.md
  3. 0 2
      .github/workflows/ci-app-prod.yml
  4. 0 88
      .github/workflows/reusable-app-reg-suit.yml
  5. 4 0
      apps/app/bin/openapi/definition-apiv3.js
  6. 1 0
      apps/app/bin/openapi/generate-spec-apiv3.sh
  7. 31 0
      apps/app/bin/postbuild-server.ts
  8. 2 2
      apps/app/package.json
  9. 6 2
      apps/app/public/static/locales/en_US/admin.json
  10. 17 0
      apps/app/public/static/locales/en_US/translation.json
  11. 6 2
      apps/app/public/static/locales/fr_FR/admin.json
  12. 17 0
      apps/app/public/static/locales/fr_FR/translation.json
  13. 6 2
      apps/app/public/static/locales/ja_JP/admin.json
  14. 17 0
      apps/app/public/static/locales/ja_JP/translation.json
  15. 6 2
      apps/app/public/static/locales/ko_KR/admin.json
  16. 17 0
      apps/app/public/static/locales/ko_KR/translation.json
  17. 6 2
      apps/app/public/static/locales/zh_CN/admin.json
  18. 17 0
      apps/app/public/static/locales/zh_CN/translation.json
  19. 0 26
      apps/app/regconfig.json
  20. 21 11
      apps/app/src/client/components/Admin/App/AzureSetting.tsx
  21. 64 39
      apps/app/src/client/components/Admin/App/FileUploadSetting.tsx
  22. 18 11
      apps/app/src/client/components/Admin/App/GcsSetting.tsx
  23. 42 10
      apps/app/src/client/components/Admin/AuditLog/AuditLogExportModal.tsx
  24. 12 1
      apps/app/src/client/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  25. 4 0
      apps/app/src/client/components/Admin/AuditLogManagement.tsx
  26. 1 1
      apps/app/src/client/components/PageComment/Comment.module.scss
  27. 20 2
      apps/app/src/client/components/PageCreateModal.tsx
  28. 8 0
      apps/app/src/client/components/Sidebar/PageCreateButton/CreateButton.module.scss
  29. 4 2
      apps/app/src/client/components/Sidebar/PageCreateButton/DropendToggle.module.scss
  30. 5 6
      apps/app/src/client/components/Sidebar/PageCreateButton/Hexagon.tsx
  31. 78 0
      apps/app/src/features/ai-tools/suggest-path/server/routes/apiv3/index.ts
  32. 1 1
      apps/app/src/server/crowi/index.ts
  33. 1 1
      apps/app/src/server/middlewares/exclude-read-only-user.spec.ts
  34. 1 3
      apps/app/src/server/service/in-app-notification.ts
  35. 14 2
      apps/app/vitest.workspace.mts
  36. 1 1
      apps/slackbot-proxy/package.json
  37. 12 16
      package.json
  38. 6 0
      packages/core/CHANGELOG.md
  39. 2 2
      packages/core/package.json
  40. 2 1
      packages/editor/package.json
  41. 12 1
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/AttachmentsDropup.tsx
  42. 21 9
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/DiagramButton.tsx
  43. 15 2
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/EmojiButton.tsx
  44. 20 8
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TableButton.tsx
  45. 21 9
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TemplateButton.tsx
  46. 83 9
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TextFormatTools.tsx
  47. 1 0
      packages/editor/vite.config.ts
  48. 7 0
      packages/pluginkit/CHANGELOG.md
  49. 2 2
      packages/pluginkit/package.json
  50. 1 1
      packages/presentation/package.json
  51. 1 0
      packages/presentation/vite.config.ts
  52. 1 1
      packages/preset-themes/package.json
  53. 2 2
      packages/remark-attachment-refs/package.json
  54. 1 0
      packages/remark-attachment-refs/vite.client.config.ts
  55. 1 1
      packages/remark-drawio/package.json
  56. 1 0
      packages/remark-drawio/vite.config.ts
  57. 3 3
      packages/remark-growi-directive/package.json
  58. 1 1
      packages/remark-lsx/package.json
  59. 50 0
      packages/remark-lsx/src/client/services/renderer/lsx.spec.ts
  60. 11 1
      packages/remark-lsx/src/client/services/renderer/lsx.ts
  61. 1 3
      packages/remark-lsx/src/server/routes/list-pages/add-depth-condition.spec.ts
  62. 1 0
      packages/remark-lsx/vite.client.config.ts
  63. 1 1
      packages/slack/package.json
  64. 2 1
      packages/ui/package.json
  65. 1 0
      packages/ui/vite.config.ts
  66. 271 275
      pnpm-lock.yaml

+ 0 - 5
.changeset/eight-zebras-design.md

@@ -1,5 +0,0 @@
----
-'@growi/core': minor
----
-
-add YJS_WEBSOCKET_BASE_PATH

+ 21 - 0
.claude/rules/coding-style.md

@@ -201,6 +201,27 @@ Implemented react-window for virtualizing page tree
 to improve performance with 10k+ pages.
 ```
 
+## Cross-Platform Compatibility
+
+GROWI must work on Windows, macOS, and Linux. Never use platform-specific shell commands in npm scripts.
+
+```json
+// ❌ WRONG: Unix-only commands in npm scripts
+"clean": "rm -rf dist",
+"copy": "cp src/foo.ts dist/foo.ts",
+"move": "mv src dist"
+
+// ✅ CORRECT: Cross-platform tools
+"clean": "rimraf dist",
+"copy": "node -e \"require('fs').cpSync('src/foo.ts','dist/foo.ts')\"",
+"move": "node -e \"require('fs').renameSync('src','dist')\""
+```
+
+**Rules**:
+- Use `rimraf` instead of `rm -rf`
+- Use Node.js one-liners or cross-platform tools (`cpy-cli`, `cpx2`) instead of `cp`, `mv`, `echo`, `ls`
+- Never assume a POSIX shell in npm scripts
+
 ## Code Quality Checklist
 
 Before marking work complete:

+ 0 - 2
.github/workflows/ci-app-prod.yml

@@ -9,7 +9,6 @@ on:
       - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
-      - .github/workflows/reusable-app-reg-suit.yml
       - tsconfig.base.json
       - turbo.json
       - pnpm-lock.yaml
@@ -23,7 +22,6 @@ on:
       - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
-      - .github/workflows/reusable-app-reg-suit.yml
       - tsconfig.base.json
       - pnpm-lock.yaml
       - turbo.json

+ 0 - 88
.github/workflows/reusable-app-reg-suit.yml

@@ -1,88 +0,0 @@
-name: Reusable VRT reporting workflow for production
-
-on:
-  workflow_call:
-    inputs:
-      node-version:
-        required: true
-        type: string
-      checkout-ref:
-        type: string
-        default: ${{ github.head_ref }}
-      skip-reg-suit:
-        type: boolean
-      cypress-report-artifact-name-pattern:
-        required: true
-        type: string
-    secrets:
-      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID:
-        required: true
-      AWS_ACCESS_KEY_ID:
-        required: true
-      AWS_SECRET_ACCESS_KEY:
-        required: true
-      SLACK_WEBHOOK_URL:
-        required: true
-    outputs:
-      EXPECTED_IMAGES_EXIST:
-        value: ${{ jobs.run-reg-suit.outputs.EXPECTED_IMAGES_EXIST }}
-
-
-jobs:
-
-  run-reg-suit:
-    # use secrets for "VRT" environment
-    # https://github.com/growilabs/growi/settings/environments/376165508/edit
-    environment: VRT
-
-    if: ${{ !inputs.skip-reg-suit }}
-
-    env:
-      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
-      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
-      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
-
-    runs-on: ubuntu-latest
-
-    outputs:
-      EXPECTED_IMAGES_EXIST: ${{ steps.check-expected-images.outputs.EXPECTED_IMAGES_EXIST }}
-
-    steps:
-    - uses: actions/checkout@v4
-      with:
-        ref: ${{ inputs.checkout-ref }}
-        fetch-depth: 0
-
-    - uses: pnpm/action-setup@v4
-
-    - uses: actions/setup-node@v4
-      with:
-        node-version: ${{ inputs.node-version }}
-        cache: 'pnpm'
-
-    - name: Install dependencies
-      run: |
-        pnpm install --frozen-lockfile
-
-    - name: Download screenshots taken by cypress
-      uses: actions/download-artifact@v4
-      with:
-        path: apps/app/test/cypress
-        pattern: ${{ inputs.cypress-report-artifact-name-pattern }}
-        merge-multiple: true
-
-    - name: Run reg-suit
-      working-directory: ./apps/app
-      run: |
-        pnpm run reg:run
-
-    - name: Slack Notification
-      uses: weseek/ghaction-slack-notification@master
-      if: failure()
-      with:
-        type: ${{ job.status }}
-        job_name: '*Node CI for growi - run-reg-suit (${{ inputs.node-version }})*'
-        channel: '#ci'
-        isCompactMode: true
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 4 - 0
apps/app/bin/openapi/definition-apiv3.js

@@ -116,6 +116,10 @@ module.exports = {
         'Install',
       ],
     },
+    {
+      name: 'AI API',
+      tags: ['AI Tools'],
+    },
     {
       name: 'Public API',
       tags: ['Healthcheck', 'Statistics', '', '', '', '', '', ''],

+ 1 - 0
apps/app/bin/openapi/generate-spec-apiv3.sh

@@ -12,6 +12,7 @@ swagger-jsdoc \
   -d "${APP_PATH}/bin/openapi/definition-apiv3.js" \
   "${APP_PATH}/src/features/external-user-group/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/templates/server/routes/apiv3/*.ts" \
+  "${APP_PATH}/src/features/ai-tools/**/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/growi-plugin/server/routes/apiv3/**/*.ts" \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \
   "${APP_PATH}/src/server/routes/login.js" \

+ 31 - 0
apps/app/bin/postbuild-server.ts

@@ -0,0 +1,31 @@
+/**
+ * Post-build script for server compilation.
+ *
+ * tspc compiles both `src/` and `config/` (which will be migrated to TypeScript),
+ * so the output directory (`transpiled/`) mirrors the source tree structure
+ * (e.g. `transpiled/src/`, `transpiled/config/`).
+ *
+ * Setting `rootDir: "src"` and `outDir: "dist"` in tsconfig would eliminate this script,
+ * but that would break once `config/` is included in the compilation.
+ * Instead, this script extracts only `transpiled/src/` into `dist/` and discards the rest.
+ */
+import { readdirSync, renameSync, rmSync } from 'node:fs';
+
+const TRANSPILED_DIR = 'transpiled';
+const DIST_DIR = 'dist';
+const SRC_SUBDIR = `${TRANSPILED_DIR}/src`;
+
+// List transpiled contents for debugging
+// biome-ignore lint/suspicious/noConsole: This is a build script, console output is expected.
+console.log('Listing files under transpiled:');
+// biome-ignore lint/suspicious/noConsole: This is a build script, console output is expected.
+console.log(readdirSync(TRANSPILED_DIR).join('\n'));
+
+// Remove old dist
+rmSync(DIST_DIR, { recursive: true, force: true });
+
+// Move transpiled/src -> dist
+renameSync(SRC_SUBDIR, DIST_DIR);
+
+// Remove leftover transpiled directory
+rmSync(TRANSPILED_DIR, { recursive: true, force: true });

+ 2 - 2
apps/app/package.json

@@ -9,8 +9,8 @@
     "start": "next start",
     "build:client": "next build",
     "build:server": "cross-env NODE_ENV=production tspc -p tsconfig.build.server.json",
-    "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
-    "clean": "shx rm -rf dist transpiled .next next.config.js",
+    "postbuild:server": "node bin/postbuild-server.ts",
+    "clean": "rimraf dist transpiled .next next.config.js",
     "server": "cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
     "server:ci": "pnpm run server --ci",
     "preserver": "cross-env NODE_ENV=production pnpm run migrate",

+ 6 - 2
apps/app/public/static/locales/en_US/admin.json

@@ -413,6 +413,7 @@
     "azure_storage_account_name": "Storage Account Name",
     "azure_storage_container_name": "Container Name",
     "azure_note_for_the_only_env_option": "The Azure Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+    "azure_note_for_the_only_env_option_cloud": "Azure settings can be changed from the GROWI.cloud admin panel.",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "test_connection": "Test connection to mail",
     "change_setting": "Caution:if you change this setting not completed, you will not be able to access files you have uploaded so far.",
@@ -425,7 +426,8 @@
     "enable": "Enable",
     "disable": "Disable",
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
-    "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
+    "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+    "note_for_the_only_env_option_cloud": "GCS settings can be changed from the GROWI.cloud admin panel."
   },
   "markdown_settings": {
     "markdown_settings": "Markdown Settings",
@@ -915,7 +917,9 @@
     "confirm": "Delete plugin?"
   },
   "cloud_setting_management": {
-    "to_cloud_settings": "Open GROWI.cloud Settings"
+    "to_cloud_settings": "Open GROWI.cloud Settings",
+    "change_from_cloud": "You can change the settings from the GROWI.cloud admin panel.",
+    "storage_change_from_cloud": "The current file upload method is {{fileUploadType}}. Settings can be changed from the GROWI.cloud admin panel."
   },
   "audit_log_action_category": {
     "Page": "Page",

+ 17 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -1084,5 +1084,22 @@
     "success-toaster": "Latest text synchronized",
     "skipped-toaster": "Skipped synchronizing since the editor is not activated. Please open the editor and try again.",
     "error-toaster": "Synchronization of the latest text failed"
+  },
+  "toolbar": {
+    "attachments": "Attachments",
+    "bold": "Bold",
+    "bullet_list": "Bullet List",
+    "checklist": "Checklist",
+    "code": "Code",
+    "diagram": "Diagram",
+    "emoji": "Emoji",
+    "heading": "Heading",
+    "italic": "Italic",
+    "numbered_list": "Numbered List",
+    "quote": "Quote",
+    "strikethrough": "Strikethrough",
+    "table": "Table",
+    "template": "Template",
+    "text_formatting": "Text Formatting"
   }
 }

+ 6 - 2
apps/app/public/static/locales/fr_FR/admin.json

@@ -413,6 +413,7 @@
     "azure_storage_account_name": "Nom du compte de stockage",
     "azure_storage_container_name": "Nom du conteneur",
     "azure_note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> .",
+    "azure_note_for_the_only_env_option_cloud": "Les paramètres Azure peuvent être modifiés depuis le panneau d'administration GROWI.cloud.",
     "file_upload": "Téléversement de fichiers",
     "test_connection": "Essai de la connection e-mail",
     "change_setting": "Si ce paramètre n'est pas complètement configuré, les fichiers existants seront inaccessibles.",
@@ -425,7 +426,8 @@
     "enable": "Activer",
     "disable": "Désactiver",
     "use_env_var_if_empty": "Si la valeur dans la base de données est vide, la valeur de variable d'environnement <code>{{variable}}</code> est utilisé.",
-    "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> ."
+    "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> .",
+    "note_for_the_only_env_option_cloud": "Les paramètres GCS peuvent être modifiés depuis le panneau d'administration GROWI.cloud."
   },
   "markdown_settings": {
     "markdown_settings": "Markdown",
@@ -914,7 +916,9 @@
     "confirm": "Supprimer le plugin?"
   },
   "cloud_setting_management": {
-    "to_cloud_settings": "Ouvrir paramètres GROWI.cloud"
+    "to_cloud_settings": "Ouvrir paramètres GROWI.cloud",
+    "change_from_cloud": "Vous pouvez modifier les paramètres depuis le panneau d'administration GROWI.cloud.",
+    "storage_change_from_cloud": "La méthode de téléversement actuelle est {{fileUploadType}}. Les paramètres peuvent être modifiés depuis le panneau d'administration GROWI.cloud."
   },
   "audit_log_action_category": {
     "Page": "Page",

+ 17 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -1076,5 +1076,22 @@
     "success-toaster": "Dernière révision synchronisée",
     "skipped-toaster": "Le mode édition doit être activé pour déclencher la synchronisation. Synchronisation annulée.",
     "error-toaster": "Synchronisation échouée"
+  },
+  "toolbar": {
+    "attachments": "Pièces jointes",
+    "bold": "Gras",
+    "bullet_list": "Liste à puces",
+    "checklist": "Liste de contrôle",
+    "code": "Code",
+    "diagram": "Diagramme",
+    "emoji": "Emoji",
+    "heading": "Titre",
+    "italic": "Italique",
+    "numbered_list": "Liste numérotée",
+    "quote": "Citation",
+    "strikethrough": "Barré",
+    "table": "Tableau",
+    "template": "Modèle",
+    "text_formatting": "Mise en forme du texte"
   }
 }

+ 6 - 2
apps/app/public/static/locales/ja_JP/admin.json

@@ -421,6 +421,7 @@
     "azure_storage_account_name": "ストレージアカウント名",
     "azure_storage_container_name": "コンテナ名",
     "azure_note_for_the_only_env_option": "現在Azure設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
+    "azure_note_for_the_only_env_option_cloud": "Azure の設定は GROWI.cloud の管理画面から変更できます",
     "fixed_by_env_var": "環境変数 <code>{{envKey}}={{envVar}}</code> により固定されています。",
     "file_upload": "ファイルをアップロードするための設定を行います。ファイルアップロードの設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
     "test_connection": "接続テスト",
@@ -434,7 +435,8 @@
     "enable": "有効",
     "disable": "無効",
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
-    "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください"
+    "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
+    "note_for_the_only_env_option_cloud": "GCS の設定は GROWI.cloud の管理画面から変更できます"
   },
   "markdown_settings": {
     "markdown_settings": "マークダウン設定",
@@ -924,7 +926,9 @@
     "confirm": "プラグインを削除しますか?"
   },
   "cloud_setting_management": {
-    "to_cloud_settings": "GROWI.cloud の管理画面へ"
+    "to_cloud_settings": "GROWI.cloud の管理画面へ",
+    "change_from_cloud": "GROWI.cloud の管理画面から設定を変更できます。",
+    "storage_change_from_cloud": "現在のファイルアップロード方法は {{fileUploadType}} です。変更は GROWI.cloud の管理画面から行えます。"
   },
   "audit_log_action_category": {
     "Page": "ページ",

+ 17 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -1117,5 +1117,22 @@
     "success-toaster": "最新の本文を同期しました",
     "skipped-toaster": "エディターがアクティブではないため、同期をスキップしました。エディターを開いて再度お試しください。",
     "error-toaster": "最新の本文の同期に失敗しました"
+  },
+  "toolbar": {
+    "attachments": "添付ファイル",
+    "bold": "太字",
+    "bullet_list": "箇条書きリスト",
+    "checklist": "チェックリスト",
+    "code": "コード",
+    "diagram": "ダイアグラム",
+    "emoji": "絵文字",
+    "heading": "見出し",
+    "italic": "イタリック",
+    "numbered_list": "番号付きリスト",
+    "quote": "引用",
+    "strikethrough": "取り消し線",
+    "table": "テーブル",
+    "template": "テンプレート",
+    "text_formatting": "テキスト書式"
   }
 }

+ 6 - 2
apps/app/public/static/locales/ko_KR/admin.json

@@ -413,6 +413,7 @@
     "azure_storage_account_name": "스토리지 계정 이름",
     "azure_storage_container_name": "컨테이너 이름",
     "azure_note_for_the_only_env_option": "Azure 설정은 환경 변수 값에 의해 제한됩니다.<br>이 설정을 변경하려면 환경 변수 <code>{{env}}</code>의 값을 false로 변경하거나 삭제하십시오.",
+    "azure_note_for_the_only_env_option_cloud": "Azure 설정은 GROWI.cloud 관리 화면에서 변경할 수 있습니다.",
     "file_upload": "파일 업로드 설정용입니다. 파일 업로드 설정을 완료하면 파일 업로드 기능, 프로필 사진 기능 등이 활성화됩니다.",
     "test_connection": "메일 연결 테스트",
     "change_setting": "주의: 이 설정을 완료하지 않으면 지금까지 업로드한 파일에 접근할 수 없습니다.",
@@ -425,7 +426,8 @@
     "enable": "활성화",
     "disable": "비활성화",
     "use_env_var_if_empty": "데이터베이스 값이 비어 있으면 환경 변수 <code>{{variable}}</code>의 값이 사용됩니다.",
-    "note_for_the_only_env_option": "GCS 설정은 환경 변수 값에 의해 제한됩니다.<br>이 설정을 변경하려면 환경 변수 <code>{{env}}</code>의 값을 false로 변경하거나 삭제하십시오."
+    "note_for_the_only_env_option": "GCS 설정은 환경 변수 값에 의해 제한됩니다.<br>이 설정을 변경하려면 환경 변수 <code>{{env}}</code>의 값을 false로 변경하거나 삭제하십시오.",
+    "note_for_the_only_env_option_cloud": "GCS 설정은 GROWI.cloud 관리 화면에서 변경할 수 있습니다."
   },
   "markdown_settings": {
     "markdown_settings": "마크다운 설정",
@@ -915,7 +917,9 @@
     "confirm": "플러그인 삭제?"
   },
   "cloud_setting_management": {
-    "to_cloud_settings": "GROWI.cloud 설정 열기"
+    "to_cloud_settings": "GROWI.cloud 설정 열기",
+    "change_from_cloud": "GROWI.cloud 관리 화면에서 설정을 변경할 수 있습니다.",
+    "storage_change_from_cloud": "현재 파일 업로드 방법은 {{fileUploadType}} 입니다. 설정은 GROWI.cloud 관리 화면에서 변경할 수 있습니다."
   },
   "audit_log_action_category": {
     "Page": "페이지",

+ 17 - 0
apps/app/public/static/locales/ko_KR/translation.json

@@ -1044,5 +1044,22 @@
     "success-toaster": "최신 텍스트 동기화됨",
     "skipped-toaster": "편집기가 활성화되지 않아 동기화 건너뜀. 편집기를 열고 다시 시도하십시오.",
     "error-toaster": "최신 텍스트 동기화 실패"
+  },
+  "toolbar": {
+    "attachments": "첨부 파일",
+    "bold": "굵게",
+    "bullet_list": "글머리 기호 목록",
+    "checklist": "체크리스트",
+    "code": "코드",
+    "diagram": "다이어그램",
+    "emoji": "이모지",
+    "heading": "제목",
+    "italic": "기울임꼴",
+    "numbered_list": "번호 매기기 목록",
+    "quote": "인용",
+    "strikethrough": "취소선",
+    "table": "표",
+    "template": "템플릿",
+    "text_formatting": "텍스트 서식"
   }
 }

+ 6 - 2
apps/app/public/static/locales/zh_CN/admin.json

@@ -421,6 +421,7 @@
     "azure_storage_account_name": "Storage Account Name",
     "azure_storage_container_name": "Container Name",
     "azure_note_for_the_only_env_option": "The Azure Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+    "azure_note_for_the_only_env_option_cloud": "Azure 设置可以从 GROWI.cloud 管理页面进行更改。",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{envKey}}={{envVar}}</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.",
     "test_connection": "测试邮件服务器连接",
@@ -434,7 +435,8 @@
     "enable": "启用",
     "disable": "停用",
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <cod>{{variable}}</code> 启用。",
-    "note_for_the_only_env_option": "The GCS settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
+    "note_for_the_only_env_option": "The GCS settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+    "note_for_the_only_env_option_cloud": "GCS 设置可以从 GROWI.cloud 管理页面进行更改。"
   },
   "markdown_settings": {
     "markdown_settings": "Markdown设置",
@@ -924,7 +926,9 @@
     "confirm": "Delete plugin?"
   },
   "cloud_setting_management": {
-    "to_cloud_settings": "進入 GROWI.cloud 的管理界面"
+    "to_cloud_settings": "進入 GROWI.cloud 的管理界面",
+    "change_from_cloud": "您可以从 GROWI.cloud 管理界面更改设置。",
+    "storage_change_from_cloud": "当前文件上传方式为 {{fileUploadType}}。设置可以从 GROWI.cloud 管理页面进行更改。"
   },
   "audit_log_action_category": {
     "Page": "页面",

+ 17 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -1089,5 +1089,22 @@
     "success-toaster": "同步最新文本",
     "skipped-toaster": "由于编辑器未激活,因此跳过同步。 请打开编辑器并重试。",
     "error-toaster": "同步最新文本失败"
+  },
+  "toolbar": {
+    "attachments": "附件",
+    "bold": "粗体",
+    "bullet_list": "无序列表",
+    "checklist": "清单",
+    "code": "代码",
+    "diagram": "图表",
+    "emoji": "表情符号",
+    "heading": "标题",
+    "italic": "斜体",
+    "numbered_list": "有序列表",
+    "quote": "引用",
+    "strikethrough": "删除线",
+    "table": "表格",
+    "template": "模板",
+    "text_formatting": "文本格式"
   }
 }

+ 0 - 26
apps/app/regconfig.json

@@ -1,26 +0,0 @@
-{
-  "core": {
-    "workingDir": ".reg",
-    "actualDir": "test/playwright/screenshots",
-    "thresholdRate": 0.001,
-    "addIgnore": true,
-    "ximgdiff": {
-      "invocationType": "client"
-    }
-  },
-  "plugins": {
-    "reg-keygen-git-hash-plugin": true,
-    "reg-notify-github-plugin": {
-      "prCommentBehavior": "new",
-      "setCommitStatus": false,
-      "shortDescription": true,
-      "clientId": "$REG_NOTIFY_GITHUB_PLUGIN_CLIENTID"
-    },
-    "reg-notify-slack-plugin": {
-      "webhookUrl": "$SLACK_WEBHOOK_URL"
-    },
-    "reg-publish-s3-plugin": {
-      "bucketName": "growi-vrt-snapshots"
-    }
-  }
-}

+ 21 - 11
apps/app/src/client/components/Admin/App/AzureSetting.tsx

@@ -15,6 +15,7 @@ export type AzureSettingMoleculeProps = {
   envAzureStorageAccountName?: string;
   envAzureStorageContainerName?: string;
   onChangeAzureReferenceFileWithRelayMode: (val: boolean) => void;
+  isCloud: boolean;
 };
 
 export const AzureSettingMolecule = (
@@ -30,6 +31,7 @@ export const AzureSettingMolecule = (
     envAzureClientSecret,
     envAzureStorageAccountName,
     envAzureStorageContainerName,
+    isCloud,
   } = props;
 
   return (
@@ -84,17 +86,25 @@ export const AzureSettingMolecule = (
         </div>
       </div>
 
-      {azureUseOnlyEnvVars && (
-        <p
-          className="alert alert-info"
-          // biome-ignore lint/security/noDangerouslySetInnerHtml: includes <br> and <code> from i18n strings
-          dangerouslySetInnerHTML={{
-            __html: t('admin:app_setting.azure_note_for_the_only_env_option', {
-              env: 'AZURE_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS',
-            }),
-          }}
-        />
-      )}
+      {azureUseOnlyEnvVars &&
+        (isCloud ? (
+          <p className="alert alert-info">
+            {t('admin:app_setting.azure_note_for_the_only_env_option_cloud')}
+          </p>
+        ) : (
+          <p
+            className="alert alert-info"
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: includes <br> and <code> from i18n strings
+            dangerouslySetInnerHTML={{
+              __html: t(
+                'admin:app_setting.azure_note_for_the_only_env_option',
+                {
+                  env: 'AZURE_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS',
+                },
+              ),
+            }}
+          />
+        ))}
       <table
         className={`table settings-table ${azureUseOnlyEnvVars && 'use-only-env-vars'}`}
       >

+ 64 - 39
apps/app/src/client/components/Admin/App/FileUploadSetting.tsx

@@ -5,6 +5,7 @@ import { useController, useForm } from 'react-hook-form';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { FileUploadType } from '~/interfaces/file-uploader';
+import { useGrowiAppIdForGrowiCloud, useGrowiCloudUri } from '~/states/global';
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import { AwsSettingMolecule } from './AwsSetting';
@@ -15,6 +16,9 @@ import { useFileUploadSettings } from './useFileUploadSettings';
 
 const FileUploadSetting = (): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
+  const growiCloudUri = useGrowiCloudUri();
+  const growiAppIdForGrowiCloud = useGrowiAppIdForGrowiCloud();
+  const isCloud = growiCloudUri != null && growiAppIdForGrowiCloud != null;
   const { data, isLoading, error, updateSettings } = useFileUploadSettings();
 
   const { register, handleSubmit, control, watch, formState } =
@@ -107,44 +111,63 @@ const FileUploadSetting = (): JSX.Element => {
           {t('admin:app_setting.file_upload_method')}
         </span>
 
-        <div className="col-md-6 py-2">
-          {Object.values(FileUploadType).map((type) => {
-            return (
-              <div key={type} className="form-check form-check-inline">
-                <input
-                  type="radio"
-                  className="form-check-input"
-                  name="file-upload-type"
-                  id={`file-upload-type-radio-${type}`}
-                  checked={fileUploadTypeField.value === type}
-                  disabled={data.isFixedFileUploadByEnvVar}
-                  onChange={() => fileUploadTypeField.onChange(type)}
-                />
-                <label
-                  className="form-label form-check-label"
-                  htmlFor={`file-upload-type-radio-${type}`}
-                >
-                  {t(`admin:app_setting.${type}_label`)}
-                </label>
-              </div>
-            );
-          })}
-        </div>
-        {data.isFixedFileUploadByEnvVar && (
-          <p className="alert alert-warning mt-2 text-start offset-3 col-6">
-            <span className="material-symbols-outlined">help</span>
-            <b>FIXED</b>
-            <br />
-            <b
-              // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
-              dangerouslySetInnerHTML={{
-                __html: t('admin:app_setting.fixed_by_env_var', {
-                  envKey: 'FILE_UPLOAD',
-                  envVar: data.envFileUploadType,
-                }),
-              }}
-            />
-          </p>
+        {!isCloud && (
+          <div className="col-md-6 py-2">
+            {Object.values(FileUploadType).map((type) => {
+              return (
+                <div key={type} className="form-check form-check-inline">
+                  <input
+                    type="radio"
+                    className="form-check-input"
+                    name="file-upload-type"
+                    id={`file-upload-type-radio-${type}`}
+                    checked={fileUploadTypeField.value === type}
+                    disabled={data.isFixedFileUploadByEnvVar}
+                    onChange={() => fileUploadTypeField.onChange(type)}
+                  />
+                  <label
+                    className="form-label form-check-label"
+                    htmlFor={`file-upload-type-radio-${type}`}
+                  >
+                    {t(`admin:app_setting.${type}_label`)}
+                  </label>
+                </div>
+              );
+            })}
+          </div>
+        )}
+        {isCloud ? (
+          <div className="alert alert-warning mt-2 text-start offset-3 col-6">
+            <p>
+              {t('admin:cloud_setting_management.storage_change_from_cloud', {
+                fileUploadType: t(`admin:app_setting.${fileUploadType}_label`),
+              })}
+            </p>
+            <a
+              href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
+              className="btn btn-outline-secondary"
+            >
+              <span className="material-symbols-outlined me-1">share</span>
+              {t('admin:cloud_setting_management.to_cloud_settings')}
+            </a>
+          </div>
+        ) : (
+          data.isFixedFileUploadByEnvVar && (
+            <p className="alert alert-warning mt-2 text-start offset-3 col-6">
+              <span className="material-symbols-outlined">help</span>
+              <b>FIXED</b>
+              <br />
+              <b
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
+                dangerouslySetInnerHTML={{
+                  __html: t('admin:app_setting.fixed_by_env_var', {
+                    envKey: 'FILE_UPLOAD',
+                    envVar: data.envFileUploadType,
+                  }),
+                }}
+              />
+            </p>
+          )
         )}
       </div>
 
@@ -165,6 +188,7 @@ const FileUploadSetting = (): JSX.Element => {
           envGcsBucket={data.envGcsBucket}
           envGcsUploadNamespace={data.envGcsUploadNamespace}
           onChangeGcsReferenceFileWithRelayMode={gcsRelayModeField.onChange}
+          isCloud={isCloud}
         />
       )}
 
@@ -179,10 +203,11 @@ const FileUploadSetting = (): JSX.Element => {
           envAzureStorageAccountName={data.envAzureStorageAccountName}
           envAzureStorageContainerName={data.envAzureStorageContainerName}
           onChangeAzureReferenceFileWithRelayMode={azureRelayModeField.onChange}
+          isCloud={isCloud}
         />
       )}
 
-      <AdminUpdateButtonRow type="submit" disabled={isLoading} />
+      {!isCloud && <AdminUpdateButtonRow type="submit" disabled={isLoading} />}
     </form>
   );
 };

+ 18 - 11
apps/app/src/client/components/Admin/App/GcsSetting.tsx

@@ -12,6 +12,7 @@ export type GcsSettingMoleculeProps = {
   envGcsBucket?: string;
   envGcsUploadNamespace?: string;
   onChangeGcsReferenceFileWithRelayMode: (val: boolean) => void;
+  isCloud: boolean;
 };
 
 export const GcsSettingMolecule = (
@@ -25,6 +26,7 @@ export const GcsSettingMolecule = (
     envGcsApiKeyJsonPath,
     envGcsBucket,
     envGcsUploadNamespace,
+    isCloud,
   } = props;
 
   return (
@@ -79,17 +81,22 @@ export const GcsSettingMolecule = (
         </div>
       </div>
 
-      {gcsUseOnlyEnvVars && (
-        <p
-          className="alert alert-info"
-          // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
-          dangerouslySetInnerHTML={{
-            __html: t('admin:app_setting.note_for_the_only_env_option', {
-              env: 'GCS_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS',
-            }),
-          }}
-        />
-      )}
+      {gcsUseOnlyEnvVars &&
+        (isCloud ? (
+          <p className="alert alert-info">
+            {t('admin:app_setting.note_for_the_only_env_option_cloud')}
+          </p>
+        ) : (
+          <p
+            className="alert alert-info"
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
+            dangerouslySetInnerHTML={{
+              __html: t('admin:app_setting.note_for_the_only_env_option', {
+                env: 'GCS_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS',
+              }),
+            }}
+          />
+        ))}
       <table
         className={`table settings-table ${gcsUseOnlyEnvVars && 'use-only-env-vars'}`}
       >

+ 42 - 10
apps/app/src/client/components/Admin/AuditLog/AuditLogExportModal.tsx

@@ -17,12 +17,24 @@ import { useAuditLogExport } from './useAuditLogExport';
 type Props = {
   isOpen: boolean;
   onClose: () => void;
+  initialStartDate?: Date | null;
+  initialEndDate?: Date | null;
+  initialSelectedUsernames?: string[];
+  initialActionMap?: Map<SupportedActionType, boolean>;
 };
 
 const AuditLogExportModalSubstance = ({
   onClose,
+  initialStartDate,
+  initialEndDate,
+  initialSelectedUsernames,
+  initialActionMap,
 }: {
   onClose: () => void;
+  initialStartDate?: Date | null;
+  initialEndDate?: Date | null;
+  initialSelectedUsernames?: string[];
+  initialActionMap?: Map<SupportedActionType, boolean>;
 }): JSX.Element => {
   const { t } = useTranslation('admin');
 
@@ -30,14 +42,19 @@ const AuditLogExportModalSubstance = ({
     auditLogAvailableActionsAtom,
   );
 
-  const [startDate, setStartDate] = useState<Date | null>(null);
-  const [endDate, setEndDate] = useState<Date | null>(null);
-  const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
-  const [actionMap, setActionMap] = useState(
-    () =>
-      new Map<SupportedActionType, boolean>(
-        auditLogAvailableActionsData?.map((action) => [action, true]) ?? [],
-      ),
+  const [startDate, setStartDate] = useState<Date | null>(
+    initialStartDate ?? null,
+  );
+  const [endDate, setEndDate] = useState<Date | null>(initialEndDate ?? null);
+  const [selectedUsernames, setSelectedUsernames] = useState<string[]>(
+    initialSelectedUsernames ?? [],
+  );
+  const [actionMap, setActionMap] = useState(() =>
+    initialActionMap != null
+      ? new Map(initialActionMap)
+      : new Map<SupportedActionType, boolean>(
+          auditLogAvailableActionsData?.map((action) => [action, true]) ?? [],
+        ),
   );
 
   const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
@@ -115,7 +132,10 @@ const AuditLogExportModalSubstance = ({
       <ModalBody>
         <div className="mb-3">
           <div className="form-label">{t('audit_log_management.username')}</div>
-          <SearchUsernameTypeahead onChange={setUsernamesHandler} />
+          <SearchUsernameTypeahead
+            onChange={setUsernamesHandler}
+            initialUsernames={initialSelectedUsernames}
+          />
         </div>
 
         <div className="mb-3">
@@ -173,10 +193,22 @@ const AuditLogExportModalSubstance = ({
 export const AuditLogExportModal = ({
   isOpen,
   onClose,
+  initialStartDate,
+  initialEndDate,
+  initialSelectedUsernames,
+  initialActionMap,
 }: Props): JSX.Element => {
   return (
     <Modal isOpen={isOpen} toggle={onClose}>
-      {isOpen && <AuditLogExportModalSubstance onClose={onClose} />}
+      {isOpen && (
+        <AuditLogExportModalSubstance
+          onClose={onClose}
+          initialStartDate={initialStartDate}
+          initialEndDate={initialEndDate}
+          initialSelectedUsernames={initialSelectedUsernames}
+          initialActionMap={initialActionMap}
+        />
+      )}
     </Modal>
   );
 };

+ 12 - 1
apps/app/src/client/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -29,21 +29,30 @@ type UserDataType = {
 
 type Props = {
   onChange: (text: string[]) => void;
+  initialUsernames?: string[];
 };
 
 const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<
   IClearable,
   Props
 > = (props: Props, ref) => {
-  const { onChange } = props;
+  const { onChange, initialUsernames } = props;
   const { t } = useTranslation();
 
   const typeaheadRef = useRef<TypeaheadRef>(null);
 
+  const toUserDataItem = (username: string): UserDataType => ({
+    username,
+    category: Categories.activeUser,
+  });
+
   /*
    * State
    */
   const [searchKeyword, setSearchKeyword] = useState<string>('');
+  const [selectedItems, setSelectedItems] = useState<UserDataType[]>(() =>
+    (initialUsernames ?? []).map(toUserDataItem),
+  );
 
   /*
    * Fetch
@@ -87,6 +96,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<
    */
   const changeHandler = useCallback(
     (userData: UserDataType[]) => {
+      setSelectedItems(userData);
       const usernames = userData.map((user) => user.username);
       onChange(usernames);
     },
@@ -148,6 +158,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<
         placeholder={t('admin:audit_log_management.username')}
         isLoading={isLoading}
         options={allUser}
+        selected={selectedItems}
         onSearch={searchHandler}
         onChange={changeHandler}
         renderMenu={renderMenu}

+ 4 - 0
apps/app/src/client/components/Admin/AuditLogManagement.tsx

@@ -349,6 +349,10 @@ export const AuditLogManagement: FC = () => {
           <AuditLogExportModal
             isOpen={isExportModalOpen}
             onClose={() => setIsExportModalOpen(false)}
+            initialStartDate={startDate}
+            initialEndDate={endDate}
+            initialSelectedUsernames={selectedUsernames}
+            initialActionMap={actionMap}
           />
         </>
       )}

+ 1 - 1
apps/app/src/client/components/PageComment/Comment.module.scss

@@ -33,7 +33,7 @@
         visibility: hidden;
       }
 
-      &:hover > .page-comment-control {
+      &:hover > :global(.page-comment-control) {
         visibility: visible;
       }
     }

+ 20 - 2
apps/app/src/client/components/PageCreateModal.tsx

@@ -21,7 +21,7 @@ import { debounce } from 'throttle-debounce';
 import { useCreateTemplatePage } from '~/client/services/create-page';
 import { useCreatePage } from '~/client/services/create-page/use-create-page';
 import { useToastrOnError } from '~/client/services/use-toastr-on-error';
-import { useCurrentUser } from '~/states/global';
+import { useCurrentUser, useGrowiCloudUri } from '~/states/global';
 import { isSearchServiceReachableAtom } from '~/states/server-configurations';
 import {
   usePageCreateModalActions,
@@ -35,9 +35,10 @@ import styles from './PageCreateModal.module.scss';
 const { isCreatablePage, isUsersHomepage } = pagePathUtils;
 
 const PageCreateModal: React.FC = () => {
-  const { t } = useTranslation();
+  const { t, i18n } = useTranslation();
 
   const currentUser = useCurrentUser();
+  const growiCloudUri = useGrowiCloudUri();
 
   const { isOpened, path: pathname = '' } = usePageCreateModalStatus();
   const { close: closeCreateModal } = usePageCreateModalActions();
@@ -71,6 +72,12 @@ const PageCreateModal: React.FC = () => {
     [userHomepagePath, t, now],
   );
 
+  const templateHelpLang = i18n.language === 'ja' ? 'ja' : 'en';
+  const templateHelpUrl =
+    growiCloudUri != null
+      ? `https://growi.cloud/help/${templateHelpLang}/guide/features/template.html`
+      : `https://docs.growi.org/${templateHelpLang}/guide/features/template.html`;
+
   const [todayInput, setTodayInput] = useState('');
   const [pageNameInput, setPageNameInput] = useState(pageNameInputInitialValue);
   const [template, setTemplate] = useState(null);
@@ -295,6 +302,16 @@ const PageCreateModal: React.FC = () => {
         <fieldset className="col-12">
           <h3 className="pb-2">
             {t('template.modal_label.Create template under')}
+            <a
+              href={templateHelpUrl}
+              target="_blank"
+              rel="noopener noreferrer"
+              className="ms-1"
+            >
+              <span className="material-symbols-outlined fs-6 text-secondary">
+                help
+              </span>
+            </a>
             <br />
             <code className="h6" data-testid="grw-page-create-modal-path-name">
               {pathname}
@@ -353,6 +370,7 @@ const PageCreateModal: React.FC = () => {
     isOpened,
     pathname,
     template,
+    templateHelpUrl,
     onChangeTemplateHandler,
     createTemplateWithToastr,
     t,

+ 8 - 0
apps/app/src/client/components/Sidebar/PageCreateButton/CreateButton.module.scss

@@ -19,6 +19,12 @@
   :global(svg .background) {
     pointer-events: fill;
   }
+
+  :global(.hexagon-main),
+  :global(.pencil-icon) {
+    transition: transform 0.2s ease-in-out;
+    transform-origin: 22.5px 22px;
+  }
 }
 
 // == Colors
@@ -35,11 +41,13 @@
 .btn-create:hover {
   :global(svg) {
     fill: var(--bs-btn-hover-bg);
+    transform: scale(1.1);
   }
 }
 
 .btn-create:active {
   :global(svg) {
     fill: var(--bs-btn-active-bg);
+    transform: scale(1.1);
   }
 }

+ 4 - 2
apps/app/src/client/components/Sidebar/PageCreateButton/DropendToggle.module.scss

@@ -4,7 +4,7 @@
 .btn-toggle {
   @extend %btn-basis;
 
-  left: 12px;
+  left: 14px;
   padding: 0;
 
   :global(.icon) {
@@ -18,6 +18,7 @@
 // no caret
 .btn-toggle {
   & {
+
     // no caret
     &::after {
       display: none !important;
@@ -28,7 +29,7 @@
 // hitarea
 .btn-toggle {
   :global(.hitarea) {
-    inset: 0 -10px 0 0;
+    inset: 0 -14px 0 0;
   }
 }
 
@@ -54,3 +55,4 @@
     fill: var(--grw-primary-600);
   }
 }
+

+ 5 - 6
apps/app/src/client/components/Sidebar/PageCreateButton/Hexagon.tsx

@@ -7,17 +7,16 @@ type Props = {
 export const Hexagon = React.memo(
   (props: Props): JSX.Element => (
     <svg
+      width="41"
+      height="36"
+      viewBox="0 0 41 36"
+      fill="none"
       xmlns="http://www.w3.org/2000/svg"
-      viewBox="0 0 27.691 23.999"
-      height="36px"
       className={props.className}
     >
       <title>Create</title>
       <g className="background" transform="translate(0 0)">
-        <path
-          d="M20.768,0l6.923,12L20.768,24H6.923L0,12,6.923,0Z"
-          transform="translate(0)"
-        ></path>
+        <path d="M28.5717 0C29.9943 0 31.3099 0.755645 32.0268 1.98452L40.1934 15.9845C40.92 17.23 40.92 18.77 40.1934 20.0155L32.0268 34.0155C31.3099 35.2444 29.9943 36 28.5717 36H12.1666C10.744 36 9.42835 35.2444 8.71151 34.0155L0.544839 20.0155C-0.181674 18.77 -0.181673 17.23 0.54484 15.9845L8.71151 1.98451C9.42835 0.755643 10.744 0 12.1666 0H28.5717Z" />
       </g>
     </svg>
   ),

+ 78 - 0
apps/app/src/features/ai-tools/suggest-path/server/routes/apiv3/index.ts

@@ -20,6 +20,50 @@ import { generateSuggestions } from '../../services/generate-suggestions';
 
 const logger = loggerFactory('growi:features:suggest-path:routes');
 
+/**
+ * @swagger
+ *
+ * components:
+ *   schemas:
+ *     PathSuggestion:
+ *       type: object
+ *       required:
+ *         - type
+ *         - path
+ *         - label
+ *         - description
+ *         - grant
+ *       properties:
+ *         type:
+ *           type: string
+ *           enum: [memo, search, category]
+ *           description: The type of suggestion
+ *         path:
+ *           type: string
+ *           description: Suggested page path
+ *           example: "/user/alice/2026/04/01/meeting-notes"
+ *         label:
+ *           type: string
+ *           description: Human-readable label for the suggestion
+ *         description:
+ *           type: string
+ *           description: Explanation of why this path is suggested
+ *         grant:
+ *           type: integer
+ *           description: Page grant (1=public, 4=owner_only, 5=user_group)
+ *         informationType:
+ *           type: string
+ *           enum: [flow, stock]
+ *           description: Whether the content is flow (time-based) or stock (reference)
+ *     SuggestPathResponse:
+ *       type: object
+ *       properties:
+ *         suggestions:
+ *           type: array
+ *           items:
+ *             $ref: '#/components/schemas/PathSuggestion'
+ */
+
 type ReqBody = {
   body: string;
 };
@@ -44,6 +88,40 @@ const validator = [
     .withMessage(`body must not exceed ${MAX_BODY_LENGTH} characters`),
 ];
 
+/**
+ * @swagger
+ *
+ * /ai-tools/suggest-path:
+ *   post:
+ *     summary: Suggest page paths based on content
+ *     description: Analyzes the given content and suggests appropriate page paths using keyword extraction, search, and AI evaluation.
+ *     tags: [AI Tools]
+ *     security:
+ *       - bearer: []
+ *       - accessTokenInQuery: []
+ *     requestBody:
+ *       required: true
+ *       content:
+ *         application/json:
+ *           schema:
+ *             type: object
+ *             required:
+ *               - body
+ *             properties:
+ *               body:
+ *                 type: string
+ *                 description: The page content to analyze for path suggestions
+ *                 maxLength: 50000
+ *     responses:
+ *       200:
+ *         description: Path suggestions generated successfully
+ *         content:
+ *           application/json:
+ *             schema:
+ *               $ref: '#/components/schemas/SuggestPathResponse'
+ *       500:
+ *         description: Failed to generate path suggestions
+ */
 export const suggestPathHandlersFactory = (crowi: Crowi): RequestHandler[] => {
   const loginRequiredStrictly = loginRequiredFactory(crowi);
 

+ 1 - 1
apps/app/src/server/crowi/index.ts

@@ -50,7 +50,7 @@ import {
 } from '../service/g2g-transfer';
 import { GrowiBridgeService } from '../service/growi-bridge';
 import { initializeImportService } from '../service/import';
-import InAppNotificationService from '../service/in-app-notification';
+import { InAppNotificationService } from '../service/in-app-notification';
 import { InstallerService } from '../service/installer';
 import { normalizeData } from '../service/normalize-data';
 import PageService from '../service/page';

+ 1 - 1
apps/app/src/server/middlewares/exclude-read-only-user.spec.ts

@@ -44,7 +44,7 @@ describe('excludeReadOnlyUser', () => {
 
     expect(next).not.toBeCalled();
     expect(res.apiv3Err).toBeCalledWith(
-      new ErrorV3('This user is read only user', 'validatioin_failed'),
+      new ErrorV3('This user is read only user', 'validation_failed'),
     );
   });
 });

+ 1 - 3
apps/app/src/server/service/in-app-notification.ts

@@ -24,7 +24,7 @@ const { STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;
 
 const logger = loggerFactory('growi:service:inAppNotification');
 
-export default class InAppNotificationService {
+export class InAppNotificationService {
   crowi!: Crowi;
 
   socketIoService!: any;
@@ -254,5 +254,3 @@ export default class InAppNotificationService {
     return;
   };
 }
-
-module.exports = InAppNotificationService;

+ 14 - 2
apps/app/vitest.workspace.mts

@@ -1,13 +1,18 @@
 import react from '@vitejs/plugin-react';
 import tsconfigPaths from 'vite-tsconfig-paths';
-import { defineConfig, defineWorkspace, mergeConfig } from 'vitest/config';
+import {
+  defaultExclude,
+  defineConfig,
+  defineWorkspace,
+  mergeConfig,
+} from 'vitest/config';
 
 const configShared = defineConfig({
   plugins: [tsconfigPaths()],
   test: {
     clearMocks: true,
     globals: true,
-    exclude: ['playwright/**'],
+    exclude: [...defaultExclude, 'playwright/**', 'tmp/**'],
   },
 });
 
@@ -27,6 +32,13 @@ export default defineWorkspace([
       // Prefer require (CJS) for server-side packages
       conditions: ['require', 'node', 'default'],
     },
+    ssr: {
+      resolve: {
+        // Vite 6+: SSR uses ssr.resolve.conditions (default: ['node', 'import']).
+        // Override to match resolve.conditions so CJS-only server packages resolve correctly.
+        conditions: ['require', 'node', 'default'],
+      },
+    },
     test: {
       name: 'app-integration',
       environment: 'node',

+ 1 - 1
apps/slackbot-proxy/package.json

@@ -5,7 +5,7 @@
   "private": true,
   "scripts": {
     "build": "tspc -p tsconfig.build.json",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "cp:public": "cp -RT ./src/public ./dist/public",
     "cp:views": "cp -RT ./src/views ./dist/views",
     "cp:bootstrap": "cp -RT ./node_modules/bootstrap/dist ./dist/public/bootstrap",

+ 12 - 16
package.json

@@ -57,23 +57,19 @@
     "@types/glob": "^8.1.0",
     "@types/node": "^24.0.0",
     "@typescript/native-preview": "7.0.0-dev.20260114.1",
-    "@vitejs/plugin-react": "^4.3.1",
-    "@vitest/coverage-v8": "^2.1.1",
-    "@vitest/ui": "^2.1.1",
+    "@vitejs/plugin-react": "^4.7.0",
+    "@vitest/coverage-v8": "^3.2.4",
+    "@vitest/ui": "^3.2.4",
+    "cpy-cli": "^7.0.0",
     "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
     "glob": "^8.1.0",
     "lefthook": "^2.0.13",
     "mock-require": "^3.0.3",
-    "nodemon": "^3.1.3",
-    "npm-run-all": "^4.1.5",
-    "reg-keygen-git-hash-plugin": "^0.11.1",
-    "reg-notify-github-plugin": "^0.11.1",
-    "reg-notify-slack-plugin": "^0.11.0",
-    "reg-publish-s3-plugin": "^0.11.0",
-    "reg-suit": "^0.12.2",
+    "nodemon": "^3.1.14",
+    "npm-run-all2": "^8.0.4",
     "rollup-plugin-node-externals": "^6.1.1",
-    "shx": "^0.3.4",
+    "rimraf": "^6.1.3",
     "stylelint": "^16.5.0",
     "stylelint-config-recess-order": "^5.0.1",
     "stylelint-config-recommended-scss": "^14.0.0",
@@ -85,11 +81,11 @@
     "turbo": "^2.1.3",
     "typescript": "^5.9.3",
     "typescript-transform-paths": "^3.5.6",
-    "vite": "^5.4.21",
-    "vite-plugin-dts": "^3.9.1",
-    "vite-tsconfig-paths": "^5.0.1",
-    "vitest": "^2.1.1",
-    "vitest-mock-extended": "^2.0.2"
+    "vite": "^6.4.1",
+    "vite-plugin-dts": "^4.5.4",
+    "vite-tsconfig-paths": "^5.1.4",
+    "vitest": "^3.2.4",
+    "vitest-mock-extended": "^3.1.0"
   },
   "// comments for pnpm.overrides": {
     "@lykmapipo/common>flat": "flat v6 is provided only by ESM, but @lykmapipo/common requires CommonJS version",

+ 6 - 0
packages/core/CHANGELOG.md

@@ -1,5 +1,11 @@
 # @growi/core
 
+## 2.2.0
+
+### Minor Changes
+
+- [`ef1c8b0`](https://github.com/growilabs/growi/commit/ef1c8b0e8b605c8c84c23e7650e02bd168817ff5) Thanks [@yuki-takei](https://github.com/yuki-takei)! - add YJS_WEBSOCKET_BASE_PATH
+
 ## 2.1.0
 
 ### Minor Changes

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "2.1.0",
+  "version": "2.2.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "repository": {
@@ -65,7 +65,7 @@
   },
   "scripts": {
     "build": "vite build",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
     "lint:biome": "biome check",

+ 2 - 1
packages/editor/package.json

@@ -9,7 +9,7 @@
   "module": "dist/index.js",
   "scripts": {
     "build": "tsc && vite build",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
     "serve": "vite",
@@ -58,6 +58,7 @@
     "csv-to-markdown-table": "^1.4.1",
     "emoji-mart": "^5.6.0",
     "i18next": "^23.16.5",
+    "react-i18next": "^15.1.1",
     "lib0": "^0.2.94",
     "markdown-table": "^3.0.3",
     "react-dropzone": "^14.2.3",

+ 12 - 1
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/AttachmentsDropup.tsx

@@ -1,10 +1,12 @@
-import { type JSX, useState } from 'react';
+import { type JSX, useId, useState } from 'react';
 import { AcceptedUploadFileType } from '@growi/core';
+import { useTranslation } from 'react-i18next';
 import {
   Dropdown,
   DropdownItem,
   DropdownMenu,
   DropdownToggle,
+  UncontrolledTooltip,
 } from 'reactstrap';
 
 import type { GlobalCodeMirrorEditorKey } from '../../../../consts';
@@ -26,6 +28,9 @@ export const AttachmentsDropup = (props: Props): JSX.Element => {
 
   const [isOpen, setOpen] = useState(false);
 
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   return (
     <>
       <Dropdown
@@ -35,6 +40,7 @@ export const AttachmentsDropup = (props: Props): JSX.Element => {
         className="lh-1"
       >
         <DropdownToggle
+          id={id}
           className={`${btnAttachmentToggleClass} btn-toolbar-button rounded-circle`}
           color="unset"
         >
@@ -74,6 +80,11 @@ export const AttachmentsDropup = (props: Props): JSX.Element => {
           <LinkEditButton editorKey={editorKey} />
         </DropdownMenu>
       </Dropdown>
+      {!isOpen && (
+        <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+          {t('toolbar.attachments')}
+        </UncontrolledTooltip>
+      )}
     </>
   );
 };

+ 21 - 9
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/DiagramButton.tsx

@@ -1,4 +1,6 @@
-import { type JSX, useCallback } from 'react';
+import { type JSX, useCallback, useId } from 'react';
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
 
 import { useDrawioModalForEditorActions } from '../../../../states/modal/drawio-for-editor';
 
@@ -9,17 +11,27 @@ type Props = {
 export const DiagramButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
   const { open: openDrawioModal } = useDrawioModalForEditorActions();
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   const onClickDiagramButton = useCallback(() => {
     openDrawioModal(editorKey);
   }, [editorKey, openDrawioModal]);
+
   return (
-    <button
-      type="button"
-      className="btn btn-toolbar-button"
-      onClick={onClickDiagramButton}
-    >
-      {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
-      <span className="growi-custom-icons fs-6">drawer_io</span>
-    </button>
+    <>
+      <button
+        id={id}
+        type="button"
+        className="btn btn-toolbar-button"
+        onClick={onClickDiagramButton}
+      >
+        {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
+        <span className="growi-custom-icons fs-6">drawer_io</span>
+      </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.diagram')}
+      </UncontrolledTooltip>
+    </>
   );
 };

+ 15 - 2
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/EmojiButton.tsx

@@ -4,9 +4,11 @@ import {
   type JSX,
   useCallback,
   useEffect,
+  useId,
   useState,
 } from 'react';
-import { Modal } from 'reactstrap';
+import { useTranslation } from 'react-i18next';
+import { Modal, UncontrolledTooltip } from 'reactstrap';
 
 import { useResolvedTheme } from '../../../../states/ui/resolved-theme';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
@@ -24,6 +26,9 @@ type Props = {
 export const EmojiButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
 
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   const [isOpen, setIsOpen] = useState(false);
   const [Picker, setPicker] = useState<ComponentType<PickerProps> | null>(null);
   const [emojiData, setEmojiData] = useState<unknown>(null);
@@ -89,9 +94,17 @@ export const EmojiButton = (props: Props): JSX.Element => {
 
   return (
     <>
-      <button type="button" className="btn btn-toolbar-button" onClick={toggle}>
+      <button
+        id={id}
+        type="button"
+        className="btn btn-toolbar-button"
+        onClick={toggle}
+      >
         <span className="material-symbols-outlined fs-5">emoji_emotions</span>
       </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.emoji')}
+      </UncontrolledTooltip>
       {isOpen && Picker != null && emojiData != null && (
         <div className="mb-2 d-none d-md-block">
           <Modal

+ 20 - 8
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TableButton.tsx

@@ -1,4 +1,6 @@
-import { type JSX, useCallback } from 'react';
+import { type JSX, useCallback, useId } from 'react';
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
 
 import { useHandsontableModalForEditorActions } from '../../../../states/modal/handsontable';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
@@ -9,6 +11,10 @@ type Props = {
 
 export const TableButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
+
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
   const { open: openTableModal } = useHandsontableModalForEditorActions();
   const editor = codeMirrorEditor?.view;
@@ -17,12 +23,18 @@ export const TableButton = (props: Props): JSX.Element => {
   }, [editor, openTableModal]);
 
   return (
-    <button
-      type="button"
-      className="btn btn-toolbar-button"
-      onClick={onClickTableButton}
-    >
-      <span className="material-symbols-outlined fs-5">table</span>
-    </button>
+    <>
+      <button
+        id={id}
+        type="button"
+        className="btn btn-toolbar-button"
+        onClick={onClickTableButton}
+      >
+        <span className="material-symbols-outlined fs-5">table</span>
+      </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.table')}
+      </UncontrolledTooltip>
+    </>
   );
 };

+ 21 - 9
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TemplateButton.tsx

@@ -1,4 +1,6 @@
-import { type JSX, useCallback } from 'react';
+import { type JSX, useCallback, useId } from 'react';
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
 
 import { useTemplateModalActions } from '../../../../states/modal/template';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
@@ -9,6 +11,10 @@ type Props = {
 
 export const TemplateButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
+
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
   const { open: openTemplateModal } = useTemplateModalActions();
 
@@ -23,13 +29,19 @@ export const TemplateButton = (props: Props): JSX.Element => {
   }, [codeMirrorEditor?.view, openTemplateModal]);
 
   return (
-    <button
-      type="button"
-      className="btn btn-toolbar-button"
-      onClick={onClickTempleteButton}
-      data-testid="open-template-button"
-    >
-      <span className="material-symbols-outlined fs-5">file_copy</span>
-    </button>
+    <>
+      <button
+        id={id}
+        type="button"
+        className="btn btn-toolbar-button"
+        onClick={onClickTempleteButton}
+        data-testid="open-template-button"
+      >
+        <span className="material-symbols-outlined fs-5">file_copy</span>
+      </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.template')}
+      </UncontrolledTooltip>
+    </>
   );
 };

+ 83 - 9
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TextFormatTools.tsx

@@ -1,5 +1,6 @@
-import { type JSX, useCallback, useState } from 'react';
-import { Collapse } from 'reactstrap';
+import { type JSX, useCallback, useId, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Collapse, UncontrolledTooltip } from 'reactstrap';
 
 import type { GlobalCodeMirrorEditorKey } from '../../../../consts';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
@@ -16,16 +17,24 @@ type TogglarProps = {
 const TextFormatToolsToggler = (props: TogglarProps): JSX.Element => {
   const { isOpen, onClick } = props;
 
+  const id = useId();
+  const { t } = useTranslation('translation');
   const activeClass = isOpen ? 'active' : '';
 
   return (
-    <button
-      type="button"
-      className={`btn btn-toolbar-button ${btnTextFormatToolsTogglerClass} ${activeClass}`}
-      onClick={onClick}
-    >
-      <span className="material-symbols-outlined fs-3">match_case</span>
-    </button>
+    <>
+      <button
+        id={id}
+        type="button"
+        className={`btn btn-toolbar-button ${btnTextFormatToolsTogglerClass} ${activeClass}`}
+        onClick={onClick}
+      >
+        <span className="material-symbols-outlined fs-3">match_case</span>
+      </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.text_formatting')}
+      </UncontrolledTooltip>
+    </>
   );
 };
 
@@ -37,6 +46,8 @@ type TextFormatToolsType = {
 export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
   const { editorKey, onTextFormatToolsCollapseChange } = props;
   const [isOpen, setOpen] = useState(false);
+  const baseId = useId();
+  const { t } = useTranslation('translation');
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
 
   const toggle = useCallback(() => {
@@ -66,13 +77,21 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
       >
         <div className="d-flex px-1 gap-1" style={{ width: '220px' }}>
           <button
+            id={`${baseId}-bold`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertMarkdownElements('**', '**')}
           >
             <span className="material-symbols-outlined fs-5">format_bold</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-bold`)}
+          >
+            {t('toolbar.bold')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-italic`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertMarkdownElements('*', '*')}
@@ -81,7 +100,14 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
               format_italic
             </span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-italic`)}
+          >
+            {t('toolbar.italic')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-strikethrough`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertMarkdownElements('~', '~')}
@@ -90,7 +116,14 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
               format_strikethrough
             </span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-strikethrough`)}
+          >
+            {t('toolbar.strikethrough')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-heading`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('#', true)}
@@ -98,14 +131,28 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
             <span className="growi-custom-icons">header</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-heading`)}
+          >
+            {t('toolbar.heading')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-code`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertMarkdownElements('`', '`')}
           >
             <span className="material-symbols-outlined fs-5">code</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-code`)}
+          >
+            {t('toolbar.code')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-bullet-list`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('-')}
@@ -114,7 +161,14 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
               format_list_bulleted
             </span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-bullet-list`)}
+          >
+            {t('toolbar.bullet_list')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-numbered-list`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('1.')}
@@ -123,7 +177,14 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
               format_list_numbered
             </span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-numbered-list`)}
+          >
+            {t('toolbar.numbered_list')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-quote`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('>')}
@@ -131,13 +192,26 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
             <span className="growi-custom-icons">format_quote</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-quote`)}
+          >
+            {t('toolbar.quote')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-checklist`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('- [ ]')}
           >
             <span className="material-symbols-outlined fs-5">checklist</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-checklist`)}
+          >
+            {t('toolbar.checklist')}
+          </UncontrolledTooltip>
         </div>
       </Collapse>
     </div>

+ 1 - 0
packages/editor/vite.config.ts

@@ -91,6 +91,7 @@ export default defineConfig({
         ignore: [...excludeFiles, '**/*.spec.ts'],
       }),
       name: 'editor-libs',
+      cssFileName: 'style',
       formats: ['es'],
     },
     rollupOptions: {

+ 7 - 0
packages/pluginkit/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @growi/pluginkit
 
+## 1.2.2
+
+### Patch Changes
+
+- Updated dependencies [[`ef1c8b0`](https://github.com/growilabs/growi/commit/ef1c8b0e8b605c8c84c23e7650e02bd168817ff5)]:
+  - @growi/core@2.2.0
+
 ## 1.2.1
 
 ### Patch Changes

+ 2 - 2
packages/pluginkit/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/pluginkit",
-  "version": "1.2.1",
+  "version": "1.2.2",
   "description": "Plugin kit for GROWI",
   "license": "MIT",
   "repository": {
@@ -16,7 +16,7 @@
   ],
   "scripts": {
     "build": "vite build",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
     "lint:biome": "biome check",

+ 1 - 1
packages/presentation/package.json

@@ -31,7 +31,7 @@
   "scripts": {
     "generate:marpit-base-css": "node scripts/extract-marpit-css.ts",
     "build": "vite build",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
     "lint:biome": "biome check",

+ 1 - 0
packages/presentation/vite.config.ts

@@ -27,6 +27,7 @@ export default defineConfig({
         ignore: '**/*.spec.ts',
       }),
       name: 'presentation-libs',
+      cssFileName: 'style',
       formats: ['es'],
     },
     rollupOptions: {

+ 1 - 1
packages/preset-themes/package.json

@@ -14,7 +14,7 @@
     "build": "run-p build:*",
     "build:libs": "vite -c vite.libs.config.ts build",
     "build:themes": "vite -c vite.themes.config.ts build",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "run-p dev:*",
     "dev:libs": "pnpm run build:libs --mode dev",
     "dev:themes": "pnpm run build:themes --mode dev",

+ 2 - 2
packages/remark-attachment-refs/package.json

@@ -30,7 +30,7 @@
     "build": "run-p build:*",
     "build:server": "vite build -c vite.server.config.ts",
     "build:client": "vite build -c vite.client.config.ts",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "run-p dev:*",
     "dev:server": "vite build -c vite.server.config.ts --mode dev",
     "dev:client": "vite build -c vite.client.config.ts --mode dev",
@@ -66,7 +66,7 @@
     "happy-dom": "^15.7.4",
     "hast-util-sanitize": "^5.0.1",
     "hast-util-select": "^6.0.2",
-    "npm-run-all": "^4.1.5",
+    "npm-run-all2": "^8.0.4",
     "supertest": "^7.0.0",
     "unified": "^11.0.0",
     "unist-util-visit": "^5.0.0"

+ 1 - 0
packages/remark-attachment-refs/vite.client.config.ts

@@ -27,6 +27,7 @@ export default defineConfig({
         index: 'src/client/index.ts',
       },
       name: 'remark-attachment-refs-libs',
+      cssFileName: 'style',
       formats: ['es'],
     },
   },

+ 1 - 1
packages/remark-drawio/package.json

@@ -22,7 +22,7 @@
   "typings": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
     "lint:biome": "biome check",

+ 1 - 0
packages/remark-drawio/vite.config.ts

@@ -26,6 +26,7 @@ export default defineConfig({
         index: 'src/index.ts',
       },
       name: 'remark-drawio-libs',
+      cssFileName: 'style',
       formats: ['es'],
     },
   },

+ 3 - 3
packages/remark-growi-directive/package.json

@@ -17,8 +17,8 @@
   "typings": "dist/index.d.ts",
   "scripts": {
     "build": "tsc -p tsconfig.build.json",
-    "postbuild": "shx cp ./src/mdast-util-growi-directive/index.d.ts ./dist/mdast-util-growi-directive/index.d.ts",
-    "clean": "shx rm -rf dist",
+    "postbuild": "cpy src/mdast-util-growi-directive/index.d.ts dist/mdast-util-growi-directive/",
+    "clean": "rimraf dist",
     "dev": "pnpm run build",
     "watch": "tsc -w",
     "test": "cross-env NODE_ENV=test npm run test-coverage",
@@ -49,7 +49,7 @@
     "mdast-util-from-markdown": "^2.0.1",
     "micromark": "^4.0.0",
     "remark": "^15.0.1",
-    "rimraf": "^3.0.0",
+    "rimraf": "^6.1.3",
     "to-vfile": "^7.0.0",
     "type-coverage": "^2.0.0",
     "unist-util-remove-position": "^5.0.0"

+ 1 - 1
packages/remark-lsx/package.json

@@ -16,7 +16,7 @@
     "build": "run-p build:*",
     "build:client": "vite build -c vite.client.config.ts",
     "build:server": "vite build -c vite.server.config.ts",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "run-p dev:*",
     "dev:client": "vite build -c vite.client.config.ts --mode dev",
     "dev:server": "vite build -c vite.server.config.ts --mode dev",

+ 50 - 0
packages/remark-lsx/src/client/services/renderer/lsx.spec.ts

@@ -0,0 +1,50 @@
+import type { LeafGrowiPluginDirective } from '@growi/remark-growi-directive';
+import { remarkGrowiDirectivePluginType } from '@growi/remark-growi-directive';
+
+import { remarkPlugin } from './lsx';
+
+const createNode = (
+  attributes: Record<string, string>,
+): LeafGrowiPluginDirective => ({
+  type: remarkGrowiDirectivePluginType.Leaf,
+  name: 'lsx',
+  attributes,
+  children: [],
+});
+
+const runPlugin = (node: LeafGrowiPluginDirective) => {
+  const tree = { type: 'root', children: [node] };
+  (remarkPlugin as () => (tree: unknown) => void)()(tree);
+};
+
+describe('remarkPlugin', () => {
+  describe('prefix extraction', () => {
+    it('case 1: should use explicit prefix attribute', () => {
+      // $lsx(prefix=/path)
+      const node = createNode({ prefix: '/path' });
+      runPlugin(node);
+      expect(node.data?.hProperties).toMatchObject({ prefix: '/path' });
+    });
+
+    it('case 2: should use first bare attribute as prefix', () => {
+      // $lsx(/path)
+      const node = createNode({ '/path': '' });
+      runPlugin(node);
+      expect(node.data?.hProperties).toMatchObject({ prefix: '/path' });
+    });
+
+    it('case 3: should prefer explicit prefix over bare attribute', () => {
+      // $lsx(/foo, prefix=/bar)
+      const node = createNode({ '/foo': '', prefix: '/bar' });
+      runPlugin(node);
+      expect(node.data?.hProperties).toMatchObject({ prefix: '/bar' });
+    });
+
+    it('case 4: should join consecutive bare attributes as prefix when path contains spaces', () => {
+      // $lsx(/foo bar) - micromark parser splits "/foo bar" into "/foo" and "bar"
+      const node = createNode({ '/foo': '', bar: '' });
+      runPlugin(node);
+      expect(node.data?.hProperties).toMatchObject({ prefix: '/foo bar' });
+    });
+  });
+});

+ 11 - 1
packages/remark-lsx/src/client/services/renderer/lsx.ts

@@ -53,6 +53,7 @@ export const remarkPlugin: Plugin = () => (tree) => {
       //   case 1: lsx(prefix=/path..., ...)    => prefix="/path"
       //   case 2: lsx(/path, ...)              => prefix="/path"
       //   case 3: lsx(/foo, prefix=/bar ...)   => prefix="/bar"
+      //   case 4: lsx(/foo bar, ...)           => prefix="/foo bar"
       if (attributes.prefix == null) {
         const attrEntries = Object.entries(attributes);
 
@@ -63,7 +64,16 @@ export const remarkPlugin: Plugin = () => (tree) => {
             firstAttrValue === '' &&
             !SUPPORTED_ATTRIBUTES.includes(firstAttrKey)
           ) {
-            attributes.prefix = firstAttrKey;
+            // Consecutive bare attributes are joined with spaces to restore the prefix path,
+            // because the micromark parser splits space-separated words into separate attributes.
+            const prefixParts: string[] = [];
+            for (const [key, value] of attrEntries) {
+              if (value !== '' || SUPPORTED_ATTRIBUTES.includes(key)) {
+                break;
+              }
+              prefixParts.push(key);
+            }
+            attributes.prefix = prefixParts.join(' ');
           }
         }
       }

+ 1 - 3
packages/remark-lsx/src/server/routes/list-pages/add-depth-condition.spec.ts

@@ -40,9 +40,7 @@ describe('addDepthCondition()', () => {
 
       // then
       expect(caller).toThrowError(
-        new Error(
-          "The specified option 'depth' is { start: -1, end: 10 } : the start must be larger or equal than 1",
-        ),
+        "The specified option 'depth' is { start: -1, end: 10 } : the start must be larger or equal than 1",
       );
       expect(mocks.getDepthOfPathMock).not.toHaveBeenCalled();
     });

+ 1 - 0
packages/remark-lsx/vite.client.config.ts

@@ -27,6 +27,7 @@ export default defineConfig({
         index: 'src/client/index.ts',
       },
       name: 'remark-lsx-libs',
+      cssFileName: 'style',
       formats: ['es'],
     },
   },

+ 1 - 1
packages/slack/package.json

@@ -40,7 +40,7 @@
   },
   "scripts": {
     "build": "vite build",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
     "lint:biome": "biome check",

+ 2 - 1
packages/ui/package.json

@@ -23,13 +23,14 @@
       "import": "./dist/utils/index.js"
     },
     "./dist/style": {
+      "style": "./dist/style.css",
       "import": "./dist/style.css"
     },
     "./scss/*": "./scss/*.scss"
   },
   "scripts": {
     "build": "vite build",
-    "clean": "shx rm -rf dist",
+    "clean": "rimraf dist",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
     "lint:biome": "biome check",

+ 1 - 0
packages/ui/vite.config.ts

@@ -29,6 +29,7 @@ export default defineConfig({
       }),
       name: 'ui-libs',
       formats: ['es'],
+      cssFileName: 'style',
     },
     rollupOptions: {
       output: {

Разница между файлами не показана из-за своего большого размера
+ 271 - 275
pnpm-lock.yaml


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