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

Merge branch 'master' into support/136168-136169-update-axios-for-pdf-converter-client-and-remark-packages

Futa Arai 7 месяцев назад
Родитель
Сommit
d9aa75cf58
49 измененных файлов с 1089 добавлено и 351 удалено
  1. 71 0
      .serena/memories/coding_conventions.md
  2. 45 0
      .serena/memories/development_environment.md
  3. 26 0
      .serena/memories/project_overview.md
  4. 90 0
      .serena/memories/project_structure.md
  5. 94 0
      .serena/memories/suggested_commands.md
  6. 95 0
      .serena/memories/task_completion_checklist.md
  7. 42 0
      .serena/memories/tech_stack.md
  8. 68 0
      .serena/project.yml
  9. 20 0
      .vscode/mcp.json
  10. 14 1
      CHANGELOG.md
  11. 3 1
      apps/app/.env.development
  12. 1 1
      apps/app/.env.production
  13. 2 0
      apps/app/.eslintrc.js
  14. 2 1
      apps/app/bin/openapi/definition-apiv1.js
  15. 3 11
      apps/app/bin/openapi/definition-apiv3.js
  16. 14 10
      apps/app/bin/openapi/generate-operation-ids/cli.spec.ts
  17. 5 4
      apps/app/bin/openapi/generate-operation-ids/cli.ts
  18. 29 31
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts
  19. 42 16
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.ts
  20. 8 2
      apps/app/config/cdn.js
  21. 2 2
      apps/app/config/migrate-mongo-config.js
  22. 4 8
      apps/app/config/migrate-mongo-config.spec.ts
  23. 6 7
      apps/app/config/next-i18next.config.js
  24. 4 4
      apps/app/package.json
  25. 6 8
      apps/app/resource/Contributor.js
  26. 4 7
      apps/app/resource/search/mappings-es7.ts
  27. 4 7
      apps/app/resource/search/mappings-es8.ts
  28. 5 11
      apps/app/resource/search/mappings-es9-for-ci.ts
  29. 4 7
      apps/app/resource/search/mappings-es9.ts
  30. 24 19
      apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx
  31. 27 18
      apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.tsx
  32. 1 0
      apps/app/src/client/components/PageHeader/PageTitleHeader.tsx
  33. 12 2
      apps/app/src/components/Common/PagePathNav/PagePathNavLayout.tsx
  34. 13 1
      apps/app/src/components/Layout/Admin.module.scss
  35. 60 6
      apps/app/src/features/opentelemetry/server/node-sdk.spec.ts
  36. 3 2
      apps/app/src/features/opentelemetry/server/node-sdk.ts
  37. 5 1
      apps/app/src/server/middlewares/access-token-parser/access-token.ts
  38. 3 13
      apps/app/src/server/middlewares/access-token-parser/api-token.ts
  39. 11 0
      apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts
  40. 0 1
      apps/app/src/server/middlewares/access-token-parser/index.ts
  41. 39 27
      apps/app/src/server/routes/apiv3/page/index.ts
  42. 16 17
      apps/app/src/server/routes/apiv3/share-links.js
  43. 2 2
      apps/app/src/server/routes/apiv3/users.js
  44. 1 1
      apps/app/src/server/service/config-manager/config-definition.ts
  45. 0 5
      apps/app/test-with-vite/download-mongo-binary/index.spec.ts
  46. 0 16
      apps/app/test-with-vite/download-mongo-binary/vitest.config.ts
  47. 1 6
      biome.json
  48. 1 0
      packages/core/src/consts/system.ts
  49. 157 75
      pnpm-lock.yaml

+ 71 - 0
.serena/memories/coding_conventions.md

@@ -0,0 +1,71 @@
+# コーディング規約とスタイルガイド
+
+## Linter・フォーマッター設定
+
+### Biome設定(統一予定)
+- **適用範囲**: 
+  - dist/, node_modules/, coverage/ などは除外
+  - .next/, bin/, config/ などのビルド成果物は除外
+  - package.json, .eslintrc.js などの設定ファイルは除外
+- **推奨**: 新規開発では Biome を使用
+
+### ESLint設定(廃止予定・過渡期)
+- **ベース設定**: weseek ESLint設定を使用
+- **TypeScript**: weseek/typescript 設定を適用
+- **React**: React関連のルールを適用
+- **主要なルール**:
+  - `import/prefer-default-export`: オフ(名前付きエクスポートを推奨)
+  - `import/order`: import文の順序を規定
+    - React を最初に
+    - 内部モジュール(`/**`)をparentグループの前に配置
+
+## TypeScript設定
+- **ターゲット**: ESNext
+- **モジュール**: ESNext  
+- **厳格モード**: 有効(strict: true)
+- **モジュール解決**: Bundler
+- **その他**:
+  - allowJs: true(JSファイルも許可)
+  - skipLibCheck: true(型チェックの最適化)
+  - isolatedModules: true(単独モジュールとしてコンパイル)
+
+## Stylelint設定
+- SCSS/CSSファイルに対して適用
+- recess-order設定を使用(プロパティの順序規定)
+- recommended-scss設定を適用
+
+## ファイル命名規則
+- TypeScript/JavaScriptファイル: キャメルケースまたはケバブケース
+- コンポーネントファイル: PascalCase(Reactコンポーネント)
+- 設定ファイル: ドット記法(.eslintrc.js など)
+
+## テストファイル命名規則(Vitest)
+vitest.workspace.mts の設定に基づく:
+
+### 単体テスト(Unit Test)
+- **ファイル名**: `*.spec.{ts,js}`
+- **環境**: Node.js
+- **例**: `utils.spec.ts`, `helper.spec.js`
+
+### 統合テスト(Integration Test)
+- **ファイル名**: `*.integ.ts`
+- **環境**: Node.js(MongoDB設定あり)
+- **例**: `api.integ.ts`, `service.integ.ts`
+
+### コンポーネントテスト(Component Test)
+- **ファイル名**: `*.spec.{tsx,jsx}`
+- **環境**: happy-dom
+- **例**: `Button.spec.tsx`, `Modal.spec.jsx`
+
+## ディレクトリ構造の規則
+- `src/`: ソースコード
+- `test/`: Jest用の古いテストファイル(廃止予定)
+- `test-with-vite/`: Vitest用の新しいテストファイル
+- `playwright/`: E2Eテストファイル
+- `config/`: 設定ファイル
+- `public/`: 静的ファイル
+- `dist/`: ビルド出力
+
+## 移行ガイドライン
+- 新規開発: Biome + Vitest を使用
+- 既存コード: 段階的に ESLint → Biome、Jest → Vitest に移行

+ 45 - 0
.serena/memories/development_environment.md

@@ -0,0 +1,45 @@
+# 開発環境とツール
+
+## 推奨システム要件
+- **Node.js**: ^20 || ^22
+- **パッケージマネージャー**: pnpm 10.4.1
+- **OS**: Linux(Ubuntuベース)、macOS、Windows
+
+## 利用可能なLinuxコマンド
+基本的なLinuxコマンドが利用可能:
+- `apt`, `dpkg`: パッケージ管理
+- `git`: バージョン管理
+- `curl`, `wget`: HTTP クライアント
+- `ssh`, `scp`, `rsync`: ネットワーク関連
+- `ps`, `lsof`, `netstat`, `top`: プロセス・ネットワーク監視
+- `tree`, `find`, `grep`: ファイル検索・操作
+- `zip`, `unzip`, `tar`, `gzip`, `bzip2`, `xz`: アーカイブ操作
+
+## 開発用ブラウザ
+```bash
+# ローカルサーバーをブラウザで開く
+"$BROWSER" http://localhost:3000
+```
+
+## 環境変数管理
+- **dotenv-flow**: 環境ごとの設定管理
+- 環境ファイル:
+  - `.env.development`: 開発環境
+  - `.env.production`: 本番環境
+  - `.env.test`: テスト環境
+  - `.env.*.local`: ローカル固有設定
+
+## デバッグ
+```bash
+# デバッグモードでサーバー起動
+cd apps/app && pnpm run dev  # --inspectフラグ付きでnodemon起動
+
+# REPL(Read-Eval-Print Loop)
+cd apps/app && pnpm run repl
+```
+
+## VS Code設定
+`.vscode/` ディレクトリに設定ファイルが含まれており、推奨拡張機能や設定が適用される。
+
+## Docker対応
+各アプリケーションにDockerファイルが含まれており、コンテナベースでの開発も可能。

+ 26 - 0
.serena/memories/project_overview.md

@@ -0,0 +1,26 @@
+# GROWIプロジェクト概要
+
+## 目的
+GROWIは、マークダウンを使用したチームコラボレーションソフトウェアです。Wikiとドキュメント作成ツールの機能を持ち、チーム間の情報共有とコラボレーションを促進します。
+
+## プロジェクトの詳細
+- **プロジェクト名**: GROWI
+- **バージョン**: 7.3.0-RC.0
+- **ライセンス**: MIT
+- **作者**: Yuki Takei <yuki@weseek.co.jp>
+- **リポジトリ**: https://github.com/weseek/growi.git
+- **公式サイト**: https://growi.org
+
+## 主な特徴
+- Markdownベースのドキュメント作成
+- チームコラボレーション機能
+- Wikiのような情報共有プラットフォーム
+- ドキュメント管理とバージョン管理
+
+## アーキテクチャ
+- **モノレポ構成**: pnpm workspace + Turbo.js を使用
+- **主要アプリケーション**: apps/app (メインアプリケーション)
+- **追加アプリケーション**: 
+  - apps/pdf-converter (PDF変換サービス)
+  - apps/slackbot-proxy (Slackボットプロキシ)
+- **パッケージ**: packages/ 配下に複数の共有ライブラリ

+ 90 - 0
.serena/memories/project_structure.md

@@ -0,0 +1,90 @@
+# プロジェクト構造
+
+## ルートディレクトリ構造
+```
+growi/
+├── apps/                    # アプリケーション群
+│   ├── app/                # メインのGROWIアプリケーション
+│   ├── pdf-converter/      # PDF変換サービス
+│   └── slackbot-proxy/     # Slackボットプロキシ
+├── packages/               # 共有パッケージ群
+│   ├── core/              # コアライブラリ
+│   ├── core-styles/       # 共通スタイル
+│   ├── editor/            # エディターコンポーネント
+│   ├── pluginkit/         # プラグインキット
+│   ├── ui/                # UIコンポーネント
+│   ├── presentation/      # プレゼンテーション層
+│   ├── preset-templates/  # テンプレート
+│   ├── preset-themes/     # テーマ
+│   └── remark-*/          # remarkプラグイン群
+├── bin/                   # ユーティリティスクリプト
+└── 設定ファイル群
+```
+
+## メインアプリケーション (apps/app/)
+```
+apps/app/
+├── src/                   # ソースコード
+├── test/                  # 古いJestテストファイル(廃止予定)
+├── test-with-vite/        # 新しいVitestテストファイル
+├── playwright/            # E2Eテスト(Playwright)
+├── config/                # 設定ファイル
+├── public/                # 静的ファイル
+├── docker/                # Docker関連
+├── bin/                   # スクリプト
+└── 設定ファイル群
+```
+
+## テストディレクトリの詳細
+
+### test/ (廃止予定)
+- Jest用の古いテストファイル
+- 段階的にtest-with-vite/に移行予定
+- 新規テストは作成しない
+
+### test-with-vite/
+- Vitest用の新しいテストファイル
+- 新規テストはここに作成
+- セットアップファイル: `setup/mongoms.ts` (MongoDB用)
+
+### playwright/
+- E2Eテスト用ディレクトリ
+- ブラウザ操作を含むテスト
+
+## テストファイルの配置ルール
+
+### Vitestテストファイル
+以下のパターンでソースコードと同じディレクトリまたはtest-with-vite/配下に配置:
+
+- **単体テスト**: `*.spec.{ts,js}`
+- **統合テスト**: `*.integ.ts` 
+- **コンポーネントテスト**: `*.spec.{tsx,jsx}`
+
+例:
+```
+src/
+├── utils/
+│   ├── helper.ts
+│   └── helper.spec.ts       # 単体テスト
+├── components/
+│   ├── Button.tsx
+│   └── Button.spec.tsx      # コンポーネントテスト
+└── services/
+    ├── api.ts
+    └── api.integ.ts         # 統合テスト
+```
+
+## パッケージ(packages/)
+各パッケージは独立したnpmパッケージとして管理され、以下の構造を持つ:
+- `src/`: ソースコード
+- `dist/`: ビルド出力
+- `package.json`: パッケージ設定
+- `tsconfig.json`: TypeScript設定
+
+## 重要な設定ファイル
+- **pnpm-workspace.yaml**: ワークスペース設定
+- **turbo.json**: Turbo.jsビルド設定
+- **tsconfig.base.json**: TypeScript基本設定
+- **biome.json**: Biome linter/formatter設定
+- **.eslintrc.js**: ESLint設定(廃止予定)
+- **vitest.workspace.mts**: Vitestワークスペース設定

+ 94 - 0
.serena/memories/suggested_commands.md

@@ -0,0 +1,94 @@
+# 推奨開発コマンド集
+
+## セットアップ
+```bash
+# 初期セットアップ
+pnpm run bootstrap
+# または
+pnpm install
+```
+
+## 開発サーバー
+```bash
+# メインアプリケーション開発モード
+cd apps/app && pnpm run dev
+
+# ルートから起動(本番用ビルド後)
+pnpm start
+```
+
+## ビルド
+```bash
+# メインアプリケーションのビルド
+pnpm run app:build
+
+# Slackbot Proxyのビルド
+pnpm run slackbot-proxy:build
+
+# 全体ビルド(Turboで並列実行)
+turbo run build
+```
+
+## Lint・フォーマット
+```bash
+# 【推奨】Biome実行(lint + format)
+pnpm run lint:biome
+
+# 【過渡期】ESLint実行(廃止予定)
+pnpm run lint:eslint
+
+# Stylelint実行
+pnpm run lint:styles
+
+# 全てのLint実行(過渡期対応)
+pnpm run lint
+
+# TypeScript型チェック
+pnpm run lint:typecheck
+```
+
+## テスト
+```bash
+# 【推奨】Vitestテスト実行
+pnpm run test:vitest
+
+# 【過渡期】Jest(統合テスト)(廃止予定)
+pnpm run test:jest
+
+# 全てのテスト実行(過渡期対応)
+pnpm run test
+
+# Vitestで特定のファイルに絞って実行
+pnpm run test:vitest {target-file-name}
+
+# E2Eテスト(Playwright)
+npx playwright test
+```
+
+## データベース関連
+```bash
+# マイグレーション実行
+cd apps/app && pnpm run migrate
+
+# 開発環境でのマイグレーション
+cd apps/app && pnpm run dev:migrate
+
+# マイグレーション状態確認
+cd apps/app && pnpm run dev:migrate:status
+```
+
+## その他の便利コマンド
+```bash
+# REPL起動
+cd apps/app && pnpm run repl
+
+# OpenAPI仕様生成
+cd apps/app && pnpm run openapi:generate-spec:apiv3
+
+# クリーンアップ
+cd apps/app && pnpm run clean
+```
+
+## 注意事項
+- ESLintとJestは廃止予定のため、新規開発ではBiomeとVitestを使用してください
+- 既存のコードは段階的に移行中です

+ 95 - 0
.serena/memories/task_completion_checklist.md

@@ -0,0 +1,95 @@
+# タスク完了時のチェックリスト
+
+## コードを書いた後に必ず実行すべきコマンド
+
+### 1. Lint・フォーマットの実行
+```bash
+# 【推奨】Biome実行(新規開発)
+pnpm run lint:biome
+
+# 【過渡期】全てのLint実行(既存コード)
+pnpm run lint
+
+# 個別実行(必要に応じて)
+pnpm run lint:eslint      # ESLint(廃止予定)
+pnpm run lint:styles      # Stylelint
+pnpm run lint:typecheck   # TypeScript型チェック
+```
+
+### 2. テストの実行
+```bash
+# 【推奨】Vitestテスト実行(新規開発)
+pnpm run test:vitest
+
+# 【過渡期】全てのテスト実行(既存コード)
+pnpm run test
+
+# 個別実行
+pnpm run test:jest        # Jest(廃止予定)
+pnpm run test:vitest {target-file-name}     # Vitest
+```
+
+### 3. E2Eテストの実行(重要な機能変更時)
+```bash
+cd apps/app
+npx playwright test
+```
+
+### 4. ビルドの確認
+```bash
+# メインアプリケーションのビルド
+pnpm run app:build
+
+# 関連パッケージのビルド
+turbo run build
+```
+
+### 5. 動作確認
+```bash
+# 開発サーバーでの動作確認
+cd apps/app && pnpm run dev
+
+# または本番ビルドでの確認
+pnpm start
+```
+
+## 特別な確認事項
+
+### OpenAPI仕様の確認(API変更時)
+```bash
+cd apps/app
+pnpm run openapi:generate-spec:apiv3
+pnpm run lint:openapi:apiv3
+```
+
+### データベーススキーマ変更時
+```bash
+cd apps/app
+pnpm run dev:migrate:status  # 現在の状態確認
+pnpm run dev:migrate         # マイグレーション実行
+```
+
+## テストファイル作成時の注意
+
+### 新規テストファイル
+- **単体テスト**: `*.spec.{ts,js}` (Node.js環境)
+- **統合テスト**: `*.integ.ts` (Node.js + MongoDB環境)  
+- **コンポーネントテスト**: `*.spec.{tsx,jsx}` (happy-dom環境)
+- test-with-vite/ または対象ファイルと同じディレクトリに配置
+
+### 既存テストの修正
+- test/ 配下のJestテストは段階的に移行
+- 可能であればtest-with-vite/にVitestテストとして書き直し
+
+## コミット前の最終チェック
+1. Biome(または過渡期はESLint)エラーが解消されているか
+2. Vitestテスト(または過渡期はJest)がパスしているか
+3. 重要な変更はPlaywright E2Eテストも実行
+4. ビルドが成功するか
+5. 変更による既存機能への影響がないか
+6. 適切なコミットメッセージを作成したか
+
+## 移行期間中の注意事項
+- 新規開発: Biome + Vitest を使用
+- 既存コード修正: 可能な限り Biome + Vitest に移行
+- レガシーツールは段階的に廃止予定

+ 42 - 0
.serena/memories/tech_stack.md

@@ -0,0 +1,42 @@
+# 技術スタック
+
+## プログラミング言語
+- **TypeScript**: メイン言語(~5.0.0)
+- **JavaScript**: 一部のコンポーネント
+
+## フロントエンド
+- **Next.js**: Reactベースのフレームワーク
+- **React**: UIライブラリ
+- **Vite**: ビルドツール、開発サーバー
+- **SCSS**: スタイルシート
+- **SWR**: グローバルステート管理、データフェッチ・キャッシュ管理(^2.3.2)
+
+## バックエンド
+- **Node.js**: ランタイム(^20 || ^22)
+- **Express.js**: Webフレームワーク(推測)
+- **MongoDB**: データベース
+- **Mongoose**: MongoDB用ORM(^6.13.6)
+  - mongoose-gridfs: GridFS対応(^1.2.42)
+  - mongoose-paginate-v2: ページネーション(^1.3.9)
+  - mongoose-unique-validator: バリデーション(^2.0.3)
+
+## 開発ツール
+- **pnpm**: パッケージマネージャー(10.4.1)
+- **Turbo**: モノレポビルドシステム(^2.1.3)
+- **ESLint**: Linter(weseek設定を使用)【廃止予定 - 現在は過渡期】
+- **Biome**: 統一予定のLinter/Formatter
+- **Stylelint**: CSS/SCSSのLinter
+- **Jest**: テスティングフレームワーク【廃止予定 - 現在は過渡期】
+- **Vitest**: 高速テスティングフレームワーク【統一予定】
+- **Playwright**: E2Eテスト【統一予定】
+
+## その他のツール
+- **SWC**: TypeScriptコンパイラー(高速)
+- **ts-node**: TypeScript直接実行
+- **nodemon**: 開発時のホットリロード
+- **dotenv-flow**: 環境変数管理
+- **Swagger/OpenAPI**: API仕様
+
+## 移行計画
+- **Linter**: ESLint → Biome に統一予定
+- **テスト**: Jest → Vitest + Playwright に統一予定

+ 68 - 0
.serena/project.yml

@@ -0,0 +1,68 @@
+# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
+#  * For C, use cpp
+#  * For JavaScript, use typescript
+# Special requirements:
+#  * csharp: Requires the presence of a .sln file in the project folder.
+language: typescript
+
+# whether to use the project's gitignore file to ignore files
+# Added on 2025-04-07
+ignore_all_files_in_gitignore: true
+# list of additional paths to ignore
+# same syntax as gitignore, so you can use * and **
+# Was previously called `ignored_dirs`, please update your config if you are using that.
+# Added (renamed) on 2025-04-07
+ignored_paths: []
+
+# whether the project is in read-only mode
+# If set to true, all editing tools will be disabled and attempts to use them will result in an error
+# Added on 2025-04-18
+read_only: false
+
+
+# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
+# Below is the complete list of tools for convenience.
+# To make sure you have the latest list of tools, and to view their descriptions, 
+# execute `uv run scripts/print_tool_overview.py`.
+#
+#  * `activate_project`: Activates a project by name.
+#  * `check_onboarding_performed`: Checks whether project onboarding was already performed.
+#  * `create_text_file`: Creates/overwrites a file in the project directory.
+#  * `delete_lines`: Deletes a range of lines within a file.
+#  * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
+#  * `execute_shell_command`: Executes a shell command.
+#  * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
+#  * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
+#  * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
+#  * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
+#  * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
+#  * `initial_instructions`: Gets the initial instructions for the current project.
+#     Should only be used in settings where the system prompt cannot be set,
+#     e.g. in clients you have no control over, like Claude Desktop.
+#  * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
+#  * `insert_at_line`: Inserts content at a given line in a file.
+#  * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
+#  * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
+#  * `list_memories`: Lists memories in Serena's project-specific memory store.
+#  * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
+#  * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
+#  * `read_file`: Reads a file within the project directory.
+#  * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
+#  * `remove_project`: Removes a project from the Serena configuration.
+#  * `replace_lines`: Replaces a range of lines within a file with new content.
+#  * `replace_symbol_body`: Replaces the full definition of a symbol.
+#  * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
+#  * `search_for_pattern`: Performs a search for a pattern in the project.
+#  * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
+#  * `switch_modes`: Activates modes by providing a list of their names
+#  * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
+#  * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
+#  * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
+#  * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
+excluded_tools: []
+
+# initial prompt for the project. It will always be given to the LLM upon activating the project
+# (contrary to the memories, which are loaded on demand).
+initial_prompt: ""
+
+project_name: "growi"

+ 20 - 0
.vscode/mcp.json

@@ -0,0 +1,20 @@
+{
+  "servers": {
+    "context7": {
+      "type": "http",
+      "url": "https://mcp.context7.com/mcp"
+    },
+    "serena": {
+      "type": "stdio",
+      "command": "uvx",
+      "args": [
+        "--from",
+        "git+https://github.com/oraios/serena",
+        "serena",
+        "start-mcp-server",
+        "--context",
+        "ide-assistant"
+      ]
+    }
+  }
+}

+ 14 - 1
CHANGELOG.md

@@ -1,9 +1,22 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.2.8...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.2.9...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.2.9](https://github.com/weseek/growi/compare/v7.2.8...v7.2.9) - 2025-07-01
+
+### 🚀 Improvement
+
+* imprv(ai): Assistant instructions (#10129) @yuki-takei
+* imprv: OpenTelemetry phase 2 (#10095) @yuki-takei
+* imprv: Adjust margin-top for .main at md and lg breakpoints (#10131) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Sharelink expiration date parsing when the date is cleared by the calendar UI (#10132) @yuki-takei
+* fix: Cannot change file upload destination to "MongoDB (GridFS)" or "local" for dev/7.2.x (#10119) @miya
+
 ## [v7.2.8](https://github.com/weseek/growi/compare/v7.2.7...v7.2.8) - 2025-06-26
 ## [v7.2.8](https://github.com/weseek/growi/compare/v7.2.7...v7.2.8) - 2025-06-26
 
 
 ### 💎 Features
 ### 💎 Features

+ 3 - 1
apps/app/.env.development

@@ -30,7 +30,9 @@ OGP_URI="http://ogp:8088"
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 
 
-# OpenTelemetry Official Configuration
+SERVICE_TYPE=dev
+
+# OpenTelemetry Official Configuration for dev
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 OPENTELEMETRY_ENABLED=false
 OPENTELEMETRY_ENABLED=false
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

+ 1 - 1
apps/app/.env.production

@@ -7,6 +7,6 @@ MIGRATIONS_DIR=dist/migrations/
 
 
 # OpenTelemetry Official Configuration
 # OpenTelemetry Official Configuration
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
-OTEL_TRACES_SAMPLER_ARG=0.1
+OTEL_TRACES_SAMPLER_ARG=0.01
 OTEL_METRIC_EXPORT_INTERVAL=300000
 OTEL_METRIC_EXPORT_INTERVAL=300000
 OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.growi.org"
 OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.growi.org"

+ 2 - 0
apps/app/.eslintrc.js

@@ -26,6 +26,8 @@ module.exports = {
     'test/integration/migrations/**',
     'test/integration/migrations/**',
     'test/integration/models/**',
     'test/integration/models/**',
     'test/integration/setup.js',
     'test/integration/setup.js',
+    'bin/**',
+    'config/**',
   ],
   ],
   settings: {
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
     // resolve path aliases by eslint-import-resolver-typescript

+ 2 - 1
apps/app/bin/openapi/definition-apiv1.js

@@ -12,7 +12,8 @@ module.exports = {
       variables: {
       variables: {
         server: {
         server: {
           default: 'https://demo.growi.org',
           default: 'https://demo.growi.org',
-          description: 'The base URL for the GROWI API except for the version path (/_api). This can be set to your GROWI instance URL.',
+          description:
+            'The base URL for the GROWI API except for the version path (/_api). This can be set to your GROWI instance URL.',
         },
         },
       },
       },
     },
     },

+ 3 - 11
apps/app/bin/openapi/definition-apiv3.js

@@ -12,7 +12,8 @@ module.exports = {
       variables: {
       variables: {
         server: {
         server: {
           default: 'https://demo.growi.org',
           default: 'https://demo.growi.org',
-          description: 'The base URL for the GROWI API except for the version path (/_api/v3). This can be set to your GROWI instance URL.',
+          description:
+            'The base URL for the GROWI API except for the version path (/_api/v3). This can be set to your GROWI instance URL.',
         },
         },
       },
       },
     },
     },
@@ -115,16 +116,7 @@ module.exports = {
     },
     },
     {
     {
       name: 'Public API',
       name: 'Public API',
-      tags: [
-        'Healthcheck',
-        'Statistics',
-        '',
-        '',
-        '',
-        '',
-        '',
-        '',
-      ],
+      tags: ['Healthcheck', 'Statistics', '', '', '', '', '', ''],
     },
     },
   ],
   ],
 };
 };

+ 14 - 10
apps/app/bin/openapi/generate-operation-ids/cli.spec.ts

@@ -1,8 +1,6 @@
 import { writeFileSync } from 'fs';
 import { writeFileSync } from 'fs';
 
 
-import {
-  beforeEach, describe, expect, it, vi,
-} from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 
 import { generateOperationIds } from './generate-operation-ids';
 import { generateOperationIds } from './generate-operation-ids';
 
 
@@ -23,7 +21,7 @@ describe('cli', () => {
     vi.spyOn(console, 'error').mockImplementation(() => {});
     vi.spyOn(console, 'error').mockImplementation(() => {});
   });
   });
 
 
-  it('processes input file and writes output to specified file', async() => {
+  it('processes input file and writes output to specified file', async () => {
     // Mock generateOperationIds to return success
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
 
@@ -35,13 +33,15 @@ describe('cli', () => {
     await cliModule.main();
     await cliModule.main();
 
 
     // Verify generateOperationIds was called with correct arguments
     // Verify generateOperationIds was called with correct arguments
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: undefined });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: undefined,
+    });
 
 
     // Verify writeFileSync was called with correct arguments
     // Verify writeFileSync was called with correct arguments
     expect(writeFileSync).toHaveBeenCalledWith('output.json', mockJsonStrings);
     expect(writeFileSync).toHaveBeenCalledWith('output.json', mockJsonStrings);
   });
   });
 
 
-  it('uses input file as output when no output file is specified', async() => {
+  it('uses input file as output when no output file is specified', async () => {
     // Mock generateOperationIds to return success
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
 
@@ -53,13 +53,15 @@ describe('cli', () => {
     await cliModule.main();
     await cliModule.main();
 
 
     // Verify generateOperationIds was called with correct arguments
     // Verify generateOperationIds was called with correct arguments
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: undefined });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: undefined,
+    });
 
 
     // Verify writeFileSync was called with input file as output
     // Verify writeFileSync was called with input file as output
     expect(writeFileSync).toHaveBeenCalledWith('input.json', mockJsonStrings);
     expect(writeFileSync).toHaveBeenCalledWith('input.json', mockJsonStrings);
   });
   });
 
 
-  it('handles overwrite-existing option correctly', async() => {
+  it('handles overwrite-existing option correctly', async () => {
     // Mock generateOperationIds to return success
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
 
@@ -71,10 +73,12 @@ describe('cli', () => {
     await cliModule.main();
     await cliModule.main();
 
 
     // Verify generateOperationIds was called with overwriteExisting option
     // Verify generateOperationIds was called with overwriteExisting option
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: true });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: true,
+    });
   });
   });
 
 
-  it('handles generateOperationIds error correctly', async() => {
+  it('handles generateOperationIds error correctly', async () => {
     // Mock generateOperationIds to throw error
     // Mock generateOperationIds to throw error
     const error = new Error('Test error');
     const error = new Error('Test error');
     vi.mocked(generateOperationIds).mockRejectedValue(error);
     vi.mocked(generateOperationIds).mockRejectedValue(error);

+ 5 - 4
apps/app/bin/openapi/generate-operation-ids/cli.ts

@@ -1,10 +1,9 @@
-import { writeFileSync } from 'fs';
-
 import { Command } from 'commander';
 import { Command } from 'commander';
+import { writeFileSync } from 'fs';
 
 
 import { generateOperationIds } from './generate-operation-ids';
 import { generateOperationIds } from './generate-operation-ids';
 
 
-export const main = async(): Promise<void> => {
+export const main = async (): Promise<void> => {
   // parse command line arguments
   // parse command line arguments
   const program = new Command();
   const program = new Command();
   program
   program
@@ -18,7 +17,9 @@ export const main = async(): Promise<void> => {
   const [inputFile] = program.args;
   const [inputFile] = program.args;
 
 
   // eslint-disable-next-line no-console
   // eslint-disable-next-line no-console
-  const jsonStrings = await generateOperationIds(inputFile, { overwriteExisting }).catch(console.error);
+  const jsonStrings = await generateOperationIds(inputFile, {
+    overwriteExisting,
+  }).catch(console.error);
   if (jsonStrings != null) {
   if (jsonStrings != null) {
     writeFileSync(outputFile ?? inputFile, jsonStrings);
     writeFileSync(outputFile ?? inputFile, jsonStrings);
   }
   }

+ 29 - 31
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts

@@ -1,13 +1,11 @@
 import fs from 'fs/promises';
 import fs from 'fs/promises';
+import type { OpenAPI3 } from 'openapi-typescript';
 import { tmpdir } from 'os';
 import { tmpdir } from 'os';
 import path from 'path';
 import path from 'path';
-
-import type { OpenAPI3 } from 'openapi-typescript';
 import { describe, expect, it } from 'vitest';
 import { describe, expect, it } from 'vitest';
 
 
 import { generateOperationIds } from './generate-operation-ids';
 import { generateOperationIds } from './generate-operation-ids';
 
 
-
 async function createTempOpenAPIFile(spec: OpenAPI3): Promise<string> {
 async function createTempOpenAPIFile(spec: OpenAPI3): Promise<string> {
   const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'openapi-test-'));
   const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'openapi-test-'));
   const filePath = path.join(tempDir, 'openapi.json');
   const filePath = path.join(tempDir, 'openapi.json');
@@ -19,15 +17,14 @@ async function cleanup(filePath: string): Promise<void> {
   try {
   try {
     await fs.unlink(filePath);
     await fs.unlink(filePath);
     await fs.rmdir(path.dirname(filePath));
     await fs.rmdir(path.dirname(filePath));
-  }
-  catch (err) {
+  } catch (err) {
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
     console.error('Cleanup failed:', err);
     console.error('Cleanup failed:', err);
   }
   }
 }
 }
 
 
 describe('generateOperationIds', () => {
 describe('generateOperationIds', () => {
-  it('should generate correct operationId for simple paths', async() => {
+  it('should generate correct operationId for simple paths', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -46,13 +43,12 @@ describe('generateOperationIds', () => {
 
 
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
       expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
       expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should generate correct operationId for paths with parameters', async() => {
+  it('should generate correct operationId for paths with parameters', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -72,14 +68,15 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/foo/{id}'].get.operationId).toBe('getFooById');
       expect(parsed.paths['/foo/{id}'].get.operationId).toBe('getFooById');
-      expect(parsed.paths['/foo/{id}/bar/{page}'].get.operationId).toBe('getBarByPageByIdForFoo');
-    }
-    finally {
+      expect(parsed.paths['/foo/{id}/bar/{page}'].get.operationId).toBe(
+        'getBarByPageByIdForFoo',
+      );
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should generate correct operationId for nested resources', async() => {
+  it('should generate correct operationId for nested resources', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -96,13 +93,12 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
       expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should preserve existing operationId when overwriteExisting is false', async() => {
+  it('should preserve existing operationId when overwriteExisting is false', async () => {
     const existingOperationId = 'existingOperation';
     const existingOperationId = 'existingOperation';
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
@@ -118,17 +114,18 @@ describe('generateOperationIds', () => {
 
 
     const filePath = await createTempOpenAPIFile(spec);
     const filePath = await createTempOpenAPIFile(spec);
     try {
     try {
-      const result = await generateOperationIds(filePath, { overwriteExisting: false });
+      const result = await generateOperationIds(filePath, {
+        overwriteExisting: false,
+      });
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
       expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should overwrite existing operationId when overwriteExisting is true', async() => {
+  it('should overwrite existing operationId when overwriteExisting is true', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -143,17 +140,18 @@ describe('generateOperationIds', () => {
 
 
     const filePath = await createTempOpenAPIFile(spec);
     const filePath = await createTempOpenAPIFile(spec);
     try {
     try {
-      const result = await generateOperationIds(filePath, { overwriteExisting: true });
+      const result = await generateOperationIds(filePath, {
+        overwriteExisting: true,
+      });
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should generate correct operationId for root path', async() => {
+  it('should generate correct operationId for root path', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -170,13 +168,12 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/'].get.operationId).toBe('getRoot');
       expect(parsed.paths['/'].get.operationId).toBe('getRoot');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should generate operationId for all HTTP methods', async() => {
+  it('should generate operationId for all HTTP methods', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -207,13 +204,14 @@ describe('generateOperationIds', () => {
       expect(parsed.paths['/foo'].options.operationId).toBe('optionsFoo');
       expect(parsed.paths['/foo'].options.operationId).toBe('optionsFoo');
       expect(parsed.paths['/foo'].head.operationId).toBe('headFoo');
       expect(parsed.paths['/foo'].head.operationId).toBe('headFoo');
       expect(parsed.paths['/foo'].trace.operationId).toBe('traceFoo');
       expect(parsed.paths['/foo'].trace.operationId).toBe('traceFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should throw error for non-existent file', async() => {
-    await expect(generateOperationIds('non-existent-file.json')).rejects.toThrow();
+  it('should throw error for non-existent file', async () => {
+    await expect(
+      generateOperationIds('non-existent-file.json'),
+    ).rejects.toThrow();
   });
   });
 });
 });

+ 42 - 16
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.ts

@@ -1,15 +1,25 @@
 import SwaggerParser from '@apidevtools/swagger-parser';
 import SwaggerParser from '@apidevtools/swagger-parser';
-import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
+import type {
+  OpenAPI3,
+  OperationObject,
+  PathItemObject,
+} from 'openapi-typescript';
 
 
-const toPascal = (s: string): string => s.split('-').map(w => w[0]?.toUpperCase() + w.slice(1)).join('');
+const toPascal = (s: string): string =>
+  s
+    .split('-')
+    .map((w) => w[0]?.toUpperCase() + w.slice(1))
+    .join('');
 
 
 const createParamSuffix = (params: string[]): string => {
 const createParamSuffix = (params: string[]): string => {
   return params.length > 0
   return params.length > 0
-    ? params.reverse().map(param => `By${toPascal(param.slice(1, -1))}`).join('')
+    ? params
+        .reverse()
+        .map((param) => `By${toPascal(param.slice(1, -1))}`)
+        .join('')
     : '';
     : '';
 };
 };
 
 
-
 /**
 /**
  * Generates a PascalCase operation name based on the HTTP method and path.
  * Generates a PascalCase operation name based on the HTTP method and path.
  *
  *
@@ -24,8 +34,8 @@ const createParamSuffix = (params: string[]): string => {
  */
  */
 function createOperationId(method: string, path: string): string {
 function createOperationId(method: string, path: string): string {
   const segments = path.split('/').filter(Boolean);
   const segments = path.split('/').filter(Boolean);
-  const params = segments.filter(s => s.startsWith('{'));
-  const paths = segments.filter(s => !s.startsWith('{'));
+  const params = segments.filter((s) => s.startsWith('{'));
+  const paths = segments.filter((s) => !s.startsWith('{'));
 
 
   const paramSuffix = createParamSuffix(params);
   const paramSuffix = createParamSuffix(params);
 
 
@@ -37,19 +47,35 @@ function createOperationId(method: string, path: string): string {
   return `${method.toLowerCase()}${toPascal(resource)}${paramSuffix}For${context.reverse().map(toPascal).join('')}`;
   return `${method.toLowerCase()}${toPascal(resource)}${paramSuffix}For${context.reverse().map(toPascal).join('')}`;
 }
 }
 
 
-export async function generateOperationIds(inputFile: string, opts?: { overwriteExisting: boolean }): Promise<string> {
-  const api = await SwaggerParser.parse(inputFile) as OpenAPI3;
+export async function generateOperationIds(
+  inputFile: string,
+  opts?: { overwriteExisting: boolean },
+): Promise<string> {
+  const api = (await SwaggerParser.parse(inputFile)) as OpenAPI3;
 
 
   Object.entries(api.paths || {}).forEach(([path, pathItem]) => {
   Object.entries(api.paths || {}).forEach(([path, pathItem]) => {
     const item = pathItem as PathItemObject;
     const item = pathItem as PathItemObject;
-    (['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'] as const)
-      .forEach((method) => {
-        const operation = item[method] as OperationObject | undefined;
-        if (operation == null || (operation.operationId != null && !opts?.overwriteExisting)) {
-          return;
-        }
-        operation.operationId = createOperationId(method, path);
-      });
+    (
+      [
+        'get',
+        'post',
+        'put',
+        'delete',
+        'patch',
+        'options',
+        'head',
+        'trace',
+      ] as const
+    ).forEach((method) => {
+      const operation = item[method] as OperationObject | undefined;
+      if (
+        operation == null ||
+        (operation.operationId != null && !opts?.overwriteExisting)
+      ) {
+        return;
+      }
+      operation.operationId = createOperationId(method, path);
+    });
   });
   });
 
 
   const output = JSON.stringify(api, null, 2);
   const output = JSON.stringify(api, null, 2);

+ 8 - 2
apps/app/config/cdn.js

@@ -2,7 +2,13 @@ import path from 'path';
 
 
 import { projectRoot } from '~/utils/project-dir-utils';
 import { projectRoot } from '~/utils/project-dir-utils';
 
 
-export const cdnLocalScriptRoot = path.join(projectRoot, 'public/static/js/cdn');
+export const cdnLocalScriptRoot = path.join(
+  projectRoot,
+  'public/static/js/cdn',
+);
 export const cdnLocalScriptWebRoot = '/static/js/cdn';
 export const cdnLocalScriptWebRoot = '/static/js/cdn';
-export const cdnLocalStyleRoot = path.join(projectRoot, 'public/static/styles/cdn');
+export const cdnLocalStyleRoot = path.join(
+  projectRoot,
+  'public/static/styles/cdn',
+);
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';

+ 2 - 2
apps/app/config/migrate-mongo-config.js

@@ -9,8 +9,8 @@ const isProduction = process.env.NODE_ENV === 'production';
 const { URL } = require('url');
 const { URL } = require('url');
 
 
 const { getMongoUri, mongoOptions } = isProduction
 const { getMongoUri, mongoOptions } = isProduction
-  // eslint-disable-next-line import/extensions, import/no-unresolved
-  ? require('../dist/server/util/mongoose-utils')
+  ? // eslint-disable-next-line import/extensions, import/no-unresolved
+    require('../dist/server/util/mongoose-utils')
   : require('../src/server/util/mongoose-utils');
   : require('../src/server/util/mongoose-utils');
 
 
 // get migrationsDir from env var
 // get migrationsDir from env var

+ 4 - 8
apps/app/config/migrate-mongo-config.spec.ts

@@ -2,11 +2,8 @@ import mockRequire from 'mock-require';
 
 
 const { reRequire } = mockRequire;
 const { reRequire } = mockRequire;
 
 
-
 describe('config/migrate-mongo-config.js', () => {
 describe('config/migrate-mongo-config.js', () => {
-
   test.concurrent('throws an error when MIGRATIONS_DIR is not set', () => {
   test.concurrent('throws an error when MIGRATIONS_DIR is not set', () => {
-
     const getMongoUriMock = vi.fn();
     const getMongoUriMock = vi.fn();
     const mongoOptionsMock = vi.fn();
     const mongoOptionsMock = vi.fn();
 
 
@@ -32,13 +29,11 @@ describe('config/migrate-mongo-config.js', () => {
     ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
     ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
     ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
     ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
   `('returns', ({ MONGO_URI, expectedDbName }) => {
   `('returns', ({ MONGO_URI, expectedDbName }) => {
-
-    beforeEach(async() => {
+    beforeEach(async () => {
       process.env.MIGRATIONS_DIR = 'testdir/migrations';
       process.env.MIGRATIONS_DIR = 'testdir/migrations';
     });
     });
 
 
     test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
     test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
-
       const getMongoUriMock = vi.fn(() => MONGO_URI);
       const getMongoUriMock = vi.fn(() => MONGO_URI);
       const mongoOptionsMock = vi.fn();
       const mongoOptionsMock = vi.fn();
 
 
@@ -49,7 +44,9 @@ describe('config/migrate-mongo-config.js', () => {
       });
       });
 
 
       // use reRequire to avoid using module cache
       // use reRequire to avoid using module cache
-      const { mongodb, migrationsDir, changelogCollectionName } = reRequire('./migrate-mongo-config');
+      const { mongodb, migrationsDir, changelogCollectionName } = reRequire(
+        './migrate-mongo-config',
+      );
 
 
       mockRequire.stop('../src/server/util/mongoose-utils');
       mockRequire.stop('../src/server/util/mongoose-utils');
 
 
@@ -61,5 +58,4 @@ describe('config/migrate-mongo-config.js', () => {
       expect(changelogCollectionName).toBe('migrations');
       expect(changelogCollectionName).toBe('migrations');
     });
     });
   });
   });
-
 });
 });

+ 6 - 7
apps/app/config/next-i18next.config.js

@@ -26,17 +26,17 @@ module.exports = {
     ? isServer()
     ? isServer()
       ? [new HMRPlugin({ webpack: { server: true } })]
       ? [new HMRPlugin({ webpack: { server: true } })]
       : [
       : [
-        require('i18next-chained-backend').default,
-        new HMRPlugin({ webpack: { client: true } }),
-      ]
+          require('i18next-chained-backend').default,
+          new HMRPlugin({ webpack: { client: true } }),
+        ]
     : [],
     : [],
   backend: {
   backend: {
     backends: isServer()
     backends: isServer()
       ? []
       ? []
       : [
       : [
-        require('i18next-localstorage-backend').default,
-        require('i18next-http-backend').default,
-      ],
+          require('i18next-localstorage-backend').default,
+          require('i18next-http-backend').default,
+        ],
     backendOptions: [
     backendOptions: [
       // options for i18next-localstorage-backend
       // options for i18next-localstorage-backend
       { expirationTime: isDev ? 0 : 24 * 60 * 60 * 1000 }, // 1 day in production
       { expirationTime: isDev ? 0 : 24 * 60 * 60 * 1000 }, // 1 day in production
@@ -44,5 +44,4 @@ module.exports = {
       { loadPath: '/static/locales/{{lng}}/{{ns}}.json' },
       { loadPath: '/static/locales/{{lng}}/{{ns}}.json' },
     ],
     ],
   },
   },
-
 };
 };

+ 4 - 4
apps/app/package.json

@@ -35,12 +35,12 @@
     "lint": "run-p lint:**",
     "lint": "run-p lint:**",
     "prelint:openapi:apiv3": "pnpm run openapi:generate-spec:apiv3",
     "prelint:openapi:apiv3": "pnpm run openapi:generate-spec:apiv3",
     "prelint:openapi:apiv1": "pnpm run openapi:generate-spec:apiv1",
     "prelint:openapi:apiv1": "pnpm run openapi:generate-spec:apiv1",
-    "test": "run-p test:*",
+    "test": "run-p test:jest test:vitest:coverage",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
-    "test:vitest": "vitest run --coverage",
+    "test:vitest": "vitest run",
+    "test:vitest:coverage": "COLUMNS=200 vitest run --coverage",
     "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "reg:run": "reg-suit run",
     "reg:run": "reg-suit run",
-    "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "//// misc": "",
     "//// misc": "",
     "console": "npm run repl",
     "console": "npm run repl",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
@@ -158,7 +158,7 @@
     "mdast-util-from-markdown": "^2.0.1",
     "mdast-util-from-markdown": "^2.0.1",
     "mdast-util-gfm-table": "^2.0.0",
     "mdast-util-gfm-table": "^2.0.0",
     "mdast-util-wiki-link": "^0.1.2",
     "mdast-util-wiki-link": "^0.1.2",
-    "mermaid": "^11.9.0",
+    "mermaid": "^11.10.0",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
     "micromark-extension-gfm-table": "^2.1.0",
     "micromark-extension-gfm-table": "^2.1.0",
     "micromark-extension-wiki-link": "^0.0.4",
     "micromark-extension-wiki-link": "^0.0.4",

+ 6 - 8
apps/app/resource/Contributor.js

@@ -104,10 +104,7 @@ const contributors = [
       },
       },
       {
       {
         additionalClass: 'col-md-6 my-4',
         additionalClass: 'col-md-6 my-4',
-        members: [
-          { name: 'shaminmeerankutty' },
-          { name: 'rabitarochan' },
-        ],
+        members: [{ name: 'shaminmeerankutty' }, { name: 'rabitarochan' }],
       },
       },
       {
       {
         additionalClass: 'col-md-4 my-4',
         additionalClass: 'col-md-4 my-4',
@@ -150,7 +147,10 @@ const contributors = [
           { position: 'Flatt Security', name: 'stypr' },
           { position: 'Flatt Security', name: 'stypr' },
           { position: 'Flatt Security', name: 'Azara/Norihide Saito' },
           { position: 'Flatt Security', name: 'Azara/Norihide Saito' },
           { position: 'CyberAgent, Inc.', name: 'Daisuke Takahashi' },
           { position: 'CyberAgent, Inc.', name: 'Daisuke Takahashi' },
-          { position: 'Mitsui Bussan Secure Directions, Inc.', name: 'Yuji Tounai' },
+          {
+            position: 'Mitsui Bussan Secure Directions, Inc.',
+            name: 'Yuji Tounai',
+          },
           { name: 'yy0931' },
           { name: 'yy0931' },
         ],
         ],
       },
       },
@@ -172,9 +172,7 @@ const contributors = [
       },
       },
       {
       {
         additionalClass: 'col-12 staff-credit-mt-10rem',
         additionalClass: 'col-12 staff-credit-mt-10rem',
-        members: [
-          { name: 'AND YOU' },
-        ],
+        members: [{ name: 'AND YOU' }],
       },
       },
     ],
     ],
   },
   },

+ 4 - 7
apps/app/resource/search/mappings-es7.ts

@@ -4,15 +4,15 @@ import type { estypes } from '@elastic/elasticsearch7';
 type Mappings = {
 type Mappings = {
   settings: NonNullable<estypes.IndicesCreateRequest['body']>['settings'];
   settings: NonNullable<estypes.IndicesCreateRequest['body']>['settings'];
   mappings: NonNullable<estypes.IndicesCreateRequest['body']>['mappings'];
   mappings: NonNullable<estypes.IndicesCreateRequest['body']>['mappings'];
-}
+};
 
 
 export const mappings: Mappings = {
 export const mappings: Mappings = {
   settings: {
   settings: {
     analysis: {
     analysis: {
       filter: {
       filter: {
         english_stop: {
         english_stop: {
-          type:       'stop',
-          stopwords:  '_english_',
+          type: 'stop',
+          stopwords: '_english_',
         },
         },
       },
       },
       tokenizer: {
       tokenizer: {
@@ -32,10 +32,7 @@ export const mappings: Mappings = {
         english_edge_ngram: {
         english_edge_ngram: {
           type: 'custom',
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
         },
       },
       },
     },
     },

+ 4 - 7
apps/app/resource/search/mappings-es8.ts

@@ -3,15 +3,15 @@ import type { estypes } from '@elastic/elasticsearch8';
 type Mappings = {
 type Mappings = {
   settings: estypes.IndicesCreateRequest['settings'];
   settings: estypes.IndicesCreateRequest['settings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
-}
+};
 
 
 export const mappings: Mappings = {
 export const mappings: Mappings = {
   settings: {
   settings: {
     analysis: {
     analysis: {
       filter: {
       filter: {
         english_stop: {
         english_stop: {
-          type:       'stop',
-          stopwords:  '_english_',
+          type: 'stop',
+          stopwords: '_english_',
         },
         },
       },
       },
       tokenizer: {
       tokenizer: {
@@ -31,10 +31,7 @@ export const mappings: Mappings = {
         english_edge_ngram: {
         english_edge_ngram: {
           type: 'custom',
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
         },
       },
       },
     },
     },

+ 5 - 11
apps/app/resource/search/mappings-es9-for-ci.ts

@@ -3,15 +3,15 @@ import type { estypes } from '@elastic/elasticsearch9';
 type Mappings = {
 type Mappings = {
   settings: estypes.IndicesCreateRequest['settings'];
   settings: estypes.IndicesCreateRequest['settings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
-}
+};
 
 
 export const mappings: Mappings = {
 export const mappings: Mappings = {
   settings: {
   settings: {
     analysis: {
     analysis: {
       filter: {
       filter: {
         english_stop: {
         english_stop: {
-          type:       'stop',
-          stopwords:  '_english_',
+          type: 'stop',
+          stopwords: '_english_',
         },
         },
       },
       },
       tokenizer: {
       tokenizer: {
@@ -26,18 +26,12 @@ export const mappings: Mappings = {
         japanese: {
         japanese: {
           type: 'custom',
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
         },
         english_edge_ngram: {
         english_edge_ngram: {
           type: 'custom',
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
         },
       },
       },
     },
     },

+ 4 - 7
apps/app/resource/search/mappings-es9.ts

@@ -3,15 +3,15 @@ import type { estypes } from '@elastic/elasticsearch9';
 type Mappings = {
 type Mappings = {
   settings: estypes.IndicesCreateRequest['settings'];
   settings: estypes.IndicesCreateRequest['settings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
-}
+};
 
 
 export const mappings: Mappings = {
 export const mappings: Mappings = {
   settings: {
   settings: {
     analysis: {
     analysis: {
       filter: {
       filter: {
         english_stop: {
         english_stop: {
-          type:       'stop',
-          stopwords:  '_english_',
+          type: 'stop',
+          stopwords: '_english_',
         },
         },
       },
       },
       tokenizer: {
       tokenizer: {
@@ -31,10 +31,7 @@ export const mappings: Mappings = {
         english_edge_ngram: {
         english_edge_ngram: {
           type: 'custom',
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
         },
       },
       },
     },
     },

+ 24 - 19
apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx

@@ -11,9 +11,9 @@ import { Tooltip } from 'reactstrap';
 
 
 import type { IActivityHasId } from '~/interfaces/activity';
 import type { IActivityHasId } from '~/interfaces/activity';
 
 
-type Props = {
-  activityList: IActivityHasId[]
-}
+ type Props = {
+   activityList: IActivityHasId[]
+ }
 
 
 const formatDate = (date: Date): string => {
 const formatDate = (date: Date): string => {
   return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
   return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
@@ -21,17 +21,18 @@ const formatDate = (date: Date): string => {
 
 
 export const ActivityTable : FC<Props> = (props: Props) => {
 export const ActivityTable : FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const [tooltopOpen, setTooltipOpen] = useState(false);
+  const [activeTooltipId, setActiveTooltipId] = useState<string | null>(null);
 
 
-  const showToolTip = useCallback(() => {
-    setTooltipOpen(true);
+
+  const showToolTip = useCallback((id: string) => {
+    setActiveTooltipId(id);
     setTimeout(() => {
     setTimeout(() => {
-      setTooltipOpen(false);
+      setActiveTooltipId(null);
     }, 1000);
     }, 1000);
-  }, [setTooltipOpen]);
+  }, []);
 
 
   return (
   return (
-    <div className="table-responsive text-nowrap h-100">
+    <div className="table-responsive admin-audit-log">
       <table className="table table-default table-bordered table-user-list">
       <table className="table table-default table-bordered table-user-list">
         <thead>
         <thead>
           <tr>
           <tr>
@@ -62,16 +63,20 @@ export const ActivityTable : FC<Props> = (props: Props) => {
                 <td>{formatDate(activity.createdAt)}</td>
                 <td>{formatDate(activity.createdAt)}</td>
                 <td>{t(`admin:audit_log_action.${activity.action}`)}</td>
                 <td>{t(`admin:audit_log_action.${activity.action}`)}</td>
                 <td>{activity.ip}</td>
                 <td>{activity.ip}</td>
-                <td>
-                  {activity.endpoint}
-                  <CopyToClipboard text={activity.endpoint} onCopy={showToolTip}>
-                    <button type="button" className="btn btn-outline-secondary border-0 pull-right" id="tooltipTarget">
-                      <span className="material-symbols-outlined" aria-hidden="true">content_paste</span>
-                    </button>
-                  </CopyToClipboard>
-                  <Tooltip placement="top" isOpen={tooltopOpen} fade={false} target="tooltipTarget">
-                    copied!
-                  </Tooltip>
+                <td className="audit-log-url-cell">
+                  <div className="d-flex align-items-center">
+                    <span className="flex-grow-1 text-truncate">
+                      {activity.endpoint}
+                    </span>
+                    <CopyToClipboard text={activity.endpoint} onCopy={() => showToolTip(activity._id)}>
+                      <button type="button" className="btn btn-outline-secondary border-0 ms-2" id={`tooltipTarget-${activity._id}`}>
+                        <span className="material-symbols-outlined" aria-hidden="true">content_paste</span>
+                      </button>
+                    </CopyToClipboard>
+                    <Tooltip placement="top" isOpen={activeTooltipId === activity._id} fade={false} target={`tooltipTarget-${activity._id}`}>
+                      copied!
+                    </Tooltip>
+                  </div>
                 </td>
                 </td>
               </tr>
               </tr>
             );
             );

+ 27 - 18
apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.jsx → apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.tsx

@@ -1,10 +1,9 @@
 import React, {
 import React, {
-  useState, useMemo, useCallback,
+  useState, useMemo, useCallback, type ReactNode, type CSSProperties,
 } from 'react';
 } from 'react';
 
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import {
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
@@ -15,9 +14,26 @@ import styles from './CopyDropdown.module.scss';
 
 
 const { encodeSpaces } = pagePathUtils;
 const { encodeSpaces } = pagePathUtils;
 
 
+interface DropdownItemContentsProps {
+  title: string;
+  contents: ReactNode;
+  className?: string;
+  style?: CSSProperties;
+}
+
+interface CopyDropdownProps {
+  children: ReactNode;
+  dropdownToggleId: string;
+  pagePath: string;
+  pageId?: string;
+  dropdownToggleClassName?: string;
+  dropdownMenuContainer?: string | HTMLElement | React.RefObject<HTMLElement>;
+  isShareLinkMode?: boolean;
+}
+
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
-const DropdownItemContents = ({
-  title, contents, className, style,
+const DropdownItemContents: React.FC<DropdownItemContentsProps> = ({
+  title, contents, className = '', style,
 }) => (
 }) => (
   <>
   <>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
@@ -27,7 +43,7 @@ const DropdownItemContents = ({
 /* eslint-enable react/prop-types */
 /* eslint-enable react/prop-types */
 
 
 
 
-export const CopyDropdown = (props) => {
+export const CopyDropdown: React.FC<CopyDropdownProps> = (props) => {
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const [tooltipOpen, setTooltipOpen] = useState(false);
   const [tooltipOpen, setTooltipOpen] = useState(false);
   const [isParamsAppended, setParamsAppended] = useState(!props.isShareLinkMode);
   const [isParamsAppended, setParamsAppended] = useState(!props.isShareLinkMode);
@@ -105,7 +121,10 @@ export const CopyDropdown = (props) => {
    */
    */
   const { t } = useTranslation('commons');
   const { t } = useTranslation('commons');
   const {
   const {
-    dropdownToggleId, pageId, dropdownToggleClassName, children, isShareLinkMode,
+    dropdownToggleId, pageId,
+    dropdownToggleClassName,
+    dropdownMenuContainer,
+    children, isShareLinkMode,
   } = props;
   } = props;
 
 
   const customSwitchForParamsId = `customSwitchForParams_${dropdownToggleId}`;
   const customSwitchForParamsId = `customSwitchForParams_${dropdownToggleId}`;
@@ -128,7 +147,7 @@ export const CopyDropdown = (props) => {
         <DropdownMenu
         <DropdownMenu
           className={`${styles['copy-clipboard-dropdown-menu']}`}
           className={`${styles['copy-clipboard-dropdown-menu']}`}
           strategy="fixed"
           strategy="fixed"
-          container="body"
+          container={dropdownMenuContainer}
         >
         >
           <div className="d-flex align-items-center justify-content-between">
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
             <DropdownItem header className="px-3">
@@ -209,7 +228,7 @@ export const CopyDropdown = (props) => {
           { pageId && (
           { pageId && (
             <CopyToClipboard text={markdownLink} onCopy={showToolTip}>
             <CopyToClipboard text={markdownLink} onCopy={showToolTip}>
               <DropdownItem className="px-3 text-wrap">
               <DropdownItem className="px-3 text-wrap">
-                <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={markdownLink} isContentsWrap />
+                <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={markdownLink} />
               </DropdownItem>
               </DropdownItem>
             </CopyToClipboard>
             </CopyToClipboard>
           )}
           )}
@@ -223,13 +242,3 @@ export const CopyDropdown = (props) => {
     </>
     </>
   );
   );
 };
 };
-
-CopyDropdown.propTypes = {
-  children: PropTypes.node.isRequired,
-  dropdownToggleId: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-
-  pageId: PropTypes.string,
-  dropdownToggleClassName: PropTypes.string,
-  isShareLinkMode: PropTypes.bool,
-};

+ 1 - 0
apps/app/src/client/components/PageHeader/PageTitleHeader.tsx

@@ -166,6 +166,7 @@ export const PageTitleHeader = (props: Props): JSX.Element => {
           pagePath={currentPage.path}
           pagePath={currentPage.path}
           dropdownToggleId={`copydropdown-in-pagetitleheader-${currentPage._id}`}
           dropdownToggleId={`copydropdown-in-pagetitleheader-${currentPage._id}`}
           dropdownToggleClassName="p-1"
           dropdownToggleClassName="p-1"
+          dropdownMenuContainer="body"
         >
         >
           <span className="material-symbols-outlined fs-6">content_paste</span>
           <span className="material-symbols-outlined fs-6">content_paste</span>
         </CopyDropdown>
         </CopyDropdown>

+ 12 - 2
apps/app/src/components/Common/PagePathNav/PagePathNavLayout.tsx

@@ -49,7 +49,11 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
       className={`${className} ${moduleClass}`}
       className={`${className} ${moduleClass}`}
       style={{ maxWidth }}
       style={{ maxWidth }}
     >
     >
-      <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']}`}>{formerLink}</span>
+      {formerLink && (
+        <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']} mb-2 d-block`}>
+          {formerLink}
+        </span>
+      )}
       <div className={containerLayoutClass}>
       <div className={containerLayoutClass}>
         <h1 className={`m-0 d-inline align-bottom ${latterLinkClassName}`}>
         <h1 className={`m-0 d-inline align-bottom ${latterLinkClassName}`}>
           {latterLink}
           {latterLink}
@@ -60,7 +64,13 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
               <span className="badge text-bg-secondary">WIP</span>
               <span className="badge text-bg-secondary">WIP</span>
             )}
             )}
             <span className="grw-page-path-nav-copydropdown">
             <span className="grw-page-path-nav-copydropdown">
-              <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
+              <CopyDropdown
+                pageId={pageId}
+                pagePath={pagePath}
+                dropdownToggleId={copyDropdownId}
+                dropdownToggleClassName="p-2"
+                dropdownMenuContainer="body"
+              >
                 <span className="material-symbols-outlined">content_paste</span>
                 <span className="material-symbols-outlined">content_paste</span>
               </CopyDropdown>
               </CopyDropdown>
             </span>
             </span>

+ 13 - 1
apps/app/src/components/Layout/Admin.module.scss

@@ -230,14 +230,22 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     }
     .date-range-picker {
     .date-range-picker {
       width: 188px;
       width: 188px;
+      height: auto;
     }
     }
     .jump-page-input {
     .jump-page-input {
       width: 50px;
       width: 50px;
     }
     }
+
+    .table-bordered {
+      table-layout: fixed;
+    }
+
   }
   }
 
 
+
+
+
   .settings-table {
   .settings-table {
-    table-layout: fixed;
 
 
     .item-name {
     .item-name {
       width: 150px;
       width: 150px;
@@ -252,6 +260,10 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     }
   }
   }
 
 
+
+
+
+
 }
 }
 
 
 
 

+ 60 - 6
apps/app/src/features/opentelemetry/server/node-sdk.spec.ts

@@ -3,7 +3,7 @@ import { NodeSDK } from '@opentelemetry/sdk-node';
 
 
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 
 
-import { setupAdditionalResourceAttributes, initInstrumentation } from './node-sdk';
+import { setupAdditionalResourceAttributes, initInstrumentation, startOpenTelemetry } from './node-sdk';
 import { getResource } from './node-sdk-resource';
 import { getResource } from './node-sdk-resource';
 
 
 // Only mock configManager as it's external to what we're testing
 // Only mock configManager as it's external to what we're testing
@@ -67,15 +67,10 @@ describe('node-sdk', () => {
 
 
   describe('initInstrumentation', () => {
   describe('initInstrumentation', () => {
     it('should call setupCustomMetrics when instrumentation is enabled', async() => {
     it('should call setupCustomMetrics when instrumentation is enabled', async() => {
-      const { setupCustomMetrics } = await import('./custom-metrics');
-
       // Mock instrumentation as enabled
       // Mock instrumentation as enabled
       mockInstrumentationEnabled();
       mockInstrumentationEnabled();
 
 
       await initInstrumentation();
       await initInstrumentation();
-
-      // Verify setupCustomMetrics was called
-      expect(setupCustomMetrics).toHaveBeenCalledOnce();
     });
     });
 
 
     it('should not call setupCustomMetrics when instrumentation is disabled', async() => {
     it('should not call setupCustomMetrics when instrumentation is disabled', async() => {
@@ -203,4 +198,63 @@ describe('node-sdk', () => {
       await expect(setupAdditionalResourceAttributes()).resolves.toBeUndefined();
       await expect(setupAdditionalResourceAttributes()).resolves.toBeUndefined();
     });
     });
   });
   });
+
+  describe('startOpenTelemetry', () => {
+    it('should start SDK and call setupCustomMetrics when instrumentation is enabled and SDK instance exists', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as enabled
+      mockInstrumentationEnabled();
+
+      // Initialize SDK first
+      await initInstrumentation();
+
+      // Get SDK instance and mock its start method
+      const { __testing__ } = await import('./node-sdk');
+      const sdkInstance = __testing__.getSdkInstance();
+      expect(sdkInstance).toBeDefined();
+
+      if (sdkInstance != null) {
+        const startSpy = vi.spyOn(sdkInstance, 'start');
+
+        // Call startOpenTelemetry
+        startOpenTelemetry();
+
+        // Verify that start method was called
+        expect(startSpy).toHaveBeenCalledOnce();
+
+        // Verify that setupCustomMetrics was called
+        expect(setupCustomMetrics).toHaveBeenCalledOnce();
+      }
+    });
+
+    it('should not start SDK when instrumentation is disabled', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as disabled
+      mockInstrumentationDisabled();
+
+      // Initialize SDK (should not create instance)
+      await initInstrumentation();
+
+      // Call startOpenTelemetry
+      startOpenTelemetry();
+
+      // Verify that setupCustomMetrics was not called
+      expect(setupCustomMetrics).not.toHaveBeenCalled();
+    });
+
+    it('should not start SDK when SDK instance does not exist', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as enabled but don't initialize SDK
+      mockInstrumentationEnabled();
+
+      // Call startOpenTelemetry without initializing SDK
+      startOpenTelemetry();
+
+      // Verify that setupCustomMetrics was not called
+      expect(setupCustomMetrics).not.toHaveBeenCalled();
+    });
+  });
 });
 });

+ 3 - 2
apps/app/src/features/opentelemetry/server/node-sdk.ts

@@ -72,8 +72,6 @@ For more information, see https://docs.growi.org/en/admin-guide/admin-cookbook/t
 
 
     const sdkConfig = generateNodeSDKConfiguration({ enableAnonymization });
     const sdkConfig = generateNodeSDKConfiguration({ enableAnonymization });
 
 
-    setupCustomMetrics();
-
     sdkInstance = new NodeSDK(sdkConfig);
     sdkInstance = new NodeSDK(sdkConfig);
   }
   }
 };
 };
@@ -106,6 +104,9 @@ export const startOpenTelemetry = (): void => {
       throw new Error('OpenTelemetry instrumentation is not initialized');
       throw new Error('OpenTelemetry instrumentation is not initialized');
     }
     }
     sdkInstance.start();
     sdkInstance.start();
+
+    // setup custom metrics after SDK start
+    setupCustomMetrics();
   }
   }
 };
 };
 
 

+ 5 - 1
apps/app/src/server/middlewares/access-token-parser/access-token.ts

@@ -5,14 +5,18 @@ import type { Response } from 'express';
 import { AccessToken } from '~/server/models/access-token';
 import { AccessToken } from '~/server/models/access-token';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { extractBearerToken } from './extract-bearer-token';
 import type { AccessTokenParserReq } from './interfaces';
 import type { AccessTokenParserReq } from './interfaces';
 
 
 const logger = loggerFactory('growi:middleware:access-token-parser:access-token');
 const logger = loggerFactory('growi:middleware:access-token-parser:access-token');
 
 
 export const parserForAccessToken = (scopes: Scope[]) => {
 export const parserForAccessToken = (scopes: Scope[]) => {
   return async(req: AccessTokenParserReq, res: Response): Promise<void> => {
   return async(req: AccessTokenParserReq, res: Response): Promise<void> => {
+    // Extract token from Authorization header first
+    // It is more efficient to call it only once in "AccessTokenParser," which is the caller of the method
+    const bearerToken = extractBearerToken(req.headers.authorization);
 
 
-    const accessToken = req.query.access_token ?? req.body.access_token;
+    const accessToken = bearerToken ?? req.query.access_token ?? req.body.access_token;
     if (accessToken == null || typeof accessToken !== 'string') {
     if (accessToken == null || typeof accessToken !== 'string') {
       return;
       return;
     }
     }

+ 3 - 13
apps/app/src/server/middlewares/access-token-parser/api-token.ts

@@ -1,30 +1,20 @@
 import type { IUser, IUserHasId } from '@growi/core/dist/interfaces';
 import type { IUser, IUserHasId } from '@growi/core/dist/interfaces';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
-import type { NextFunction, Response } from 'express';
+import type { Response } from 'express';
 import type { HydratedDocument } from 'mongoose';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { extractBearerToken } from './extract-bearer-token';
 import type { AccessTokenParserReq } from './interfaces';
 import type { AccessTokenParserReq } from './interfaces';
 
 
 const logger = loggerFactory('growi:middleware:access-token-parser:api-token');
 const logger = loggerFactory('growi:middleware:access-token-parser:api-token');
 
 
-const extractBearerToken = (authHeader: string | undefined): string | null => {
-  if (authHeader == null) {
-    return null;
-  }
-
-  if (!authHeader.startsWith('Bearer ')) {
-    return null;
-  }
-
-  return authHeader.substring(7); // Remove 'Bearer ' prefix
-};
-
 
 
 export const parserForApiToken = async(req: AccessTokenParserReq, res: Response): Promise<void> => {
 export const parserForApiToken = async(req: AccessTokenParserReq, res: Response): Promise<void> => {
   // Extract token from Authorization header first
   // Extract token from Authorization header first
+  // It is more efficient to call it only once in "AccessTokenParser," which is the caller of the method
   const bearerToken = extractBearerToken(req.headers.authorization);
   const bearerToken = extractBearerToken(req.headers.authorization);
 
 
   // Try all possible token sources in order of priority
   // Try all possible token sources in order of priority

+ 11 - 0
apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts

@@ -0,0 +1,11 @@
+export const extractBearerToken = (authHeader: string | undefined): string | null => {
+  if (authHeader == null) {
+    return null;
+  }
+
+  if (!authHeader.startsWith('Bearer ')) {
+    return null;
+  }
+
+  return authHeader.substring(7); // Remove 'Bearer ' prefix
+};

+ 0 - 1
apps/app/src/server/middlewares/access-token-parser/index.ts

@@ -14,7 +14,6 @@ export type AccessTokenParser = (scopes?: Scope[], opts?: {acceptLegacy: boolean
 
 
 export const accessTokenParser: AccessTokenParser = (scopes, opts) => {
 export const accessTokenParser: AccessTokenParser = (scopes, opts) => {
   return async(req, res, next): Promise<void> => {
   return async(req, res, next): Promise<void> => {
-    // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
     if (scopes == null || scopes.length === 0) {
     if (scopes == null || scopes.length === 0) {
       logger.warn('scopes is empty');
       logger.warn('scopes is empty');
       return next();
       return next();

+ 39 - 27
apps/app/src/server/routes/apiv3/page/index.ts

@@ -4,7 +4,7 @@ import { pipeline } from 'stream/promises';
 
 
 import type { IPage, IRevision } from '@growi/core';
 import type { IPage, IRevision } from '@growi/core';
 import {
 import {
-  AllSubscriptionStatusType, PageGrant, SubscriptionStatusType,
+  AllSubscriptionStatusType, PageGrant, SCOPE, SubscriptionStatusType,
   getIdForRef,
   getIdForRef,
 } from '@growi/core';
 } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
@@ -16,7 +16,6 @@ import sanitize from 'sanitize-filename';
 
 
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import type { IPageGrantData } from '~/interfaces/page';
 import type { IPageGrantData } from '~/interfaces/page';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
@@ -279,9 +278,18 @@ module.exports = (crowi) => {
    *                  grant:
    *                  grant:
    *                    $ref: '#/components/schemas/PageGrant'
    *                    $ref: '#/components/schemas/PageGrant'
    *                  grantUserGroupIds:
    *                  grantUserGroupIds:
-   *                    type: string
-   *                    description: UserGroup ID
-   *                    example: 5ae5fccfc5577b0004dbd8ab
+   *                    type: array
+   *                    items:
+   *                      type: object
+   *                      properties:
+   *                        type:
+   *                          type: string
+   *                          description: Group type
+   *                          example: 'UserGroup'
+   *                        item:
+   *                          type: string
+   *                          description: UserGroup ID
+   *                          example: '5ae5fccfc5577b0004dbd8ab'
    *                  pageTags:
    *                  pageTags:
    *                    type: array
    *                    type: array
    *                    items:
    *                    items:
@@ -295,18 +303,16 @@ module.exports = (crowi) => {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
+   *                  type: object
    *                  properties:
    *                  properties:
-   *                    data:
-   *                      type: object
-   *                      properties:
-   *                        page:
-   *                          $ref: '#/components/schemas/Page'
-   *                        tags:
-   *                          type: array
-   *                          items:
-   *                            $ref: '#/components/schemas/Tags'
-   *                        revision:
-   *                          $ref: '#/components/schemas/Revision'
+   *                    page:
+   *                      $ref: '#/components/schemas/Page'
+   *                    tags:
+   *                      type: array
+   *                      items:
+   *                        $ref: '#/components/schemas/Tags'
+   *                    revision:
+   *                       $ref: '#/components/schemas/Revision'
    *          409:
    *          409:
    *            description: page path is already existed
    *            description: page path is already existed
    */
    */
@@ -335,8 +341,16 @@ module.exports = (crowi) => {
    *                  userRelatedGrantUserGroupIds:
    *                  userRelatedGrantUserGroupIds:
    *                    type: array
    *                    type: array
    *                    items:
    *                    items:
-   *                      type: string
-   *                      description: UserGroup ID
+   *                      type: object
+   *                      properties:
+   *                        type:
+   *                          type: string
+   *                          description: Group type
+   *                          example: 'UserGroup'
+   *                        item:
+   *                          type: string
+   *                          description: UserGroup ID
+   *                          example: '5ae5fccfc5577b0004dbd8ab'
    *                  overwriteScopesOfDescendants:
    *                  overwriteScopesOfDescendants:
    *                    type: boolean
    *                    type: boolean
    *                    description: Determine whether the scopes of descendants should be overwritten
    *                    description: Determine whether the scopes of descendants should be overwritten
@@ -362,14 +376,12 @@ module.exports = (crowi) => {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
+   *                  type: object
    *                  properties:
    *                  properties:
-   *                    data:
-   *                      type: object
-   *                      properties:
-   *                        page:
-   *                          $ref: '#/components/schemas/Page'
-   *                        revision:
-   *                          $ref: '#/components/schemas/Revision'
+   *                    page:
+   *                      $ref: '#/components/schemas/Page'
+   *                    revision:
+   *                      $ref: '#/components/schemas/Revision'
    *          403:
    *          403:
    *            $ref: '#/components/responses/Forbidden'
    *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
@@ -1077,7 +1089,7 @@ module.exports = (crowi) => {
 
 
   /**
   /**
    * @swagger
    * @swagger
-   *   /{pageId}/publish:
+   *   /page/{pageId}/publish:
    *     put:
    *     put:
    *       tags: [Page]
    *       tags: [Page]
    *       summary: Publish page
    *       summary: Publish page
@@ -1101,7 +1113,7 @@ module.exports = (crowi) => {
 
 
   /**
   /**
    * @swagger
    * @swagger
-   *   /{pageId}/unpublish:
+   *   /page/{pageId}/unpublish:
    *     put:
    *     put:
    *       tags: [Page]
    *       tags: [Page]
    *       summary: Unpublish page
    *       summary: Unpublish page

+ 16 - 17
apps/app/src/server/routes/apiv3/share-links.js

@@ -185,23 +185,22 @@ module.exports = (crowi) => {
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        description: Create new share link
    *        description: Create new share link
-   *        parameters:
-   *          - name: relatedPage
-   *            in: query
-   *            required: true
-   *            description: page id of share link
-   *            schema:
-   *              type: string
-   *          - name: expiredAt
-   *            in: query
-   *            description: expiration date of share link
-   *            schema:
-   *              type: string
-   *          - name: description
-   *            in: query
-   *            description: description of share link
-   *            schema:
-   *              type: string
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                required:
+   *                  - relatedPage
+   *                properties:
+   *                  relatedPage:
+   *                    description: page id of share link
+   *                    type: string
+   *                  expiredAt:
+   *                    description: expiration date of share link
+   *                    type: string
+   *                  description:
+   *                    description: description of share link
+   *                    type: string
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Succeeded to create one share link
    *            description: Succeeded to create one share link

+ 2 - 2
apps/app/src/server/routes/apiv3/users.js

@@ -1,5 +1,6 @@
 import path from 'path';
 import path from 'path';
 
 
+import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
@@ -11,7 +12,6 @@ import { isEmail } from 'validator';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { deleteUserAiAssistant } from '~/features/openai/server/services/delete-ai-assistant';
 import { deleteUserAiAssistant } from '~/features/openai/server/services/delete-ai-assistant';
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import Activity from '~/server/models/activity';
 import Activity from '~/server/models/activity';
 import ExternalAccount from '~/server/models/external-account';
 import ExternalAccount from '~/server/models/external-account';
@@ -371,7 +371,7 @@ module.exports = (crowi) => {
    * @swagger
    * @swagger
    *
    *
    *  paths:
    *  paths:
-   *    /{id}/recent:
+   *    /users/{id}/recent:
    *      get:
    *      get:
    *        tags: [Users]
    *        tags: [Users]
    *        summary: /usersIdReacent
    *        summary: /usersIdReacent

+ 1 - 1
apps/app/src/server/service/config-manager/config-definition.ts

@@ -503,7 +503,7 @@ export const CONFIG_DEFINITIONS = {
   }),
   }),
   'app:deploymentType': defineConfig<GrowiDeploymentType>({
   'app:deploymentType': defineConfig<GrowiDeploymentType>({
     envVarName: 'DEPLOYMENT_TYPE',
     envVarName: 'DEPLOYMENT_TYPE',
-    defaultValue: GrowiDeploymentType.others,
+    defaultValue: GrowiDeploymentType.node,
   }),
   }),
   'app:ssrMaxRevisionBodyLength': defineConfig<number>({
   'app:ssrMaxRevisionBodyLength': defineConfig<number>({
     envVarName: 'SSR_MAX_REVISION_BODY_LENGTH',
     envVarName: 'SSR_MAX_REVISION_BODY_LENGTH',

+ 0 - 5
apps/app/test-with-vite/download-mongo-binary/index.spec.ts

@@ -1,5 +0,0 @@
-describe('Download mongo-binary', () => {
-  it('should be success', () => {
-    expect(true).toBeTruthy();
-  });
-});

+ 0 - 16
apps/app/test-with-vite/download-mongo-binary/vitest.config.ts

@@ -1,16 +0,0 @@
-import tsconfigPaths from 'vite-tsconfig-paths';
-import { defineConfig } from 'vitest/config';
-
-export default defineConfig({
-  plugins: [
-    tsconfigPaths(),
-  ],
-  test: {
-    clearMocks: true,
-    globals: true,
-    hookTimeout: 60000, // increased for downloading MongoDB binary file
-    setupFiles: [
-      './test-with-vite/setup/mongoms.ts',
-    ],
-  },
-});

+ 1 - 6
biome.json

@@ -23,16 +23,11 @@
       "!apps/slackbot-proxy/src/public/bootstrap/**",
       "!apps/slackbot-proxy/src/public/bootstrap/**",
       "!packages/editor/**",
       "!packages/editor/**",
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/src/index.ts",
-      "!apps/app/bin/**",
-      "!apps/app/config/**",
-      "!apps/app/docker/**",
       "!apps/app/playwright/**",
       "!apps/app/playwright/**",
       "!apps/app/public/**",
       "!apps/app/public/**",
-      "!apps/app/resource/**",
       "!apps/app/src/**",
       "!apps/app/src/**",
       "!apps/app/test/integration/service/**",
       "!apps/app/test/integration/service/**",
-      "!apps/app/test-with-vite/**",
-      "!apps/app/tmp/**"
+      "!apps/app/test-with-vite/**"
     ]
     ]
   },
   },
   "formatter": {
   "formatter": {

+ 1 - 0
packages/core/src/consts/system.ts

@@ -2,6 +2,7 @@ export const GrowiServiceType = {
   cloud: 'cloud',
   cloud: 'cloud',
   privateCloud: 'private-cloud',
   privateCloud: 'private-cloud',
   onPremise: 'on-premise',
   onPremise: 'on-premise',
+  dev: 'dev',
   others: 'others',
   others: 'others',
 } as const;
 } as const;
 
 

+ 157 - 75
pnpm-lock.yaml

@@ -485,8 +485,8 @@ importers:
         specifier: ^0.1.2
         specifier: ^0.1.2
         version: 0.1.2
         version: 0.1.2
       mermaid:
       mermaid:
-        specifier: ^11.9.0
-        version: 11.9.0
+        specifier: ^11.10.0
+        version: 11.10.0
       method-override:
       method-override:
         specifier: ^3.0.0
         specifier: ^3.0.0
         version: 3.0.0
         version: 3.0.0
@@ -2547,8 +2547,8 @@ packages:
     cpu: [x64]
     cpu: [x64]
     os: [win32]
     os: [win32]
 
 
-  '@braintree/sanitize-url@7.1.0':
-    resolution: {integrity: sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==}
+  '@braintree/sanitize-url@7.1.1':
+    resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
 
 
   '@browser-bunyan/console-formatted-stream@1.8.0':
   '@browser-bunyan/console-formatted-stream@1.8.0':
     resolution: {integrity: sha512-Lg5SC2uXrvZ6aLwLZT6SErfN1Is4NcrTOb5km4BW/BfL8Lv0CfpsYuhuD7ltdURL6awTYBUiT+BwhKw1Xd9glQ==}
     resolution: {integrity: sha512-Lg5SC2uXrvZ6aLwLZT6SErfN1Is4NcrTOb5km4BW/BfL8Lv0CfpsYuhuD7ltdURL6awTYBUiT+BwhKw1Xd9glQ==}
@@ -2707,6 +2707,9 @@ packages:
   '@codemirror/language@6.11.2':
   '@codemirror/language@6.11.2':
     resolution: {integrity: sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==}
     resolution: {integrity: sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==}
 
 
+  '@codemirror/language@6.11.3':
+    resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
+
   '@codemirror/legacy-modes@6.4.1':
   '@codemirror/legacy-modes@6.4.1':
     resolution: {integrity: sha512-vdg3XY7OAs5uLDx2Iw+cGfnwtd7kM+Et/eMsqAGTfT/JKiVBQZXosTzjEbWAi/FrY6DcQIz8mQjBozFHZEUWQA==}
     resolution: {integrity: sha512-vdg3XY7OAs5uLDx2Iw+cGfnwtd7kM+Et/eMsqAGTfT/JKiVBQZXosTzjEbWAi/FrY6DcQIz8mQjBozFHZEUWQA==}
 
 
@@ -3278,19 +3281,28 @@ packages:
   '@jridgewell/gen-mapping@0.3.12':
   '@jridgewell/gen-mapping@0.3.12':
     resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
     resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
 
 
+  '@jridgewell/gen-mapping@0.3.13':
+    resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
   '@jridgewell/resolve-uri@3.1.2':
   '@jridgewell/resolve-uri@3.1.2':
     resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
     resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
     engines: {node: '>=6.0.0'}
     engines: {node: '>=6.0.0'}
 
 
-  '@jridgewell/source-map@0.3.10':
-    resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==}
+  '@jridgewell/source-map@0.3.11':
+    resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
 
 
   '@jridgewell/sourcemap-codec@1.5.4':
   '@jridgewell/sourcemap-codec@1.5.4':
     resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
     resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
 
 
+  '@jridgewell/sourcemap-codec@1.5.5':
+    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
   '@jridgewell/trace-mapping@0.3.29':
   '@jridgewell/trace-mapping@0.3.29':
     resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
     resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
 
 
+  '@jridgewell/trace-mapping@0.3.30':
+    resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
+
   '@jridgewell/trace-mapping@0.3.9':
   '@jridgewell/trace-mapping@0.3.9':
     resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
     resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
 
 
@@ -5438,8 +5450,8 @@ packages:
   '@types/d3-delaunay@6.0.4':
   '@types/d3-delaunay@6.0.4':
     resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
     resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
 
 
-  '@types/d3-dispatch@3.0.6':
-    resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==}
+  '@types/d3-dispatch@3.0.7':
+    resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
 
 
   '@types/d3-drag@3.0.7':
   '@types/d3-drag@3.0.7':
     resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
     resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
@@ -6608,6 +6620,11 @@ packages:
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
     hasBin: true
 
 
+  browserslist@4.25.3:
+    resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==}
+    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+    hasBin: true
+
   bs-recipes@1.3.4:
   bs-recipes@1.3.4:
     resolution: {integrity: sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==}
     resolution: {integrity: sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==}
 
 
@@ -6751,6 +6768,9 @@ packages:
   caniuse-lite@1.0.30001727:
   caniuse-lite@1.0.30001727:
     resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
     resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
 
 
+  caniuse-lite@1.0.30001735:
+    resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
+
   capital-case@1.0.4:
   capital-case@1.0.4:
     resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
     resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
 
 
@@ -7715,15 +7735,15 @@ packages:
     peerDependencies:
     peerDependencies:
       cytoscape: ^3.2.0
       cytoscape: ^3.2.0
 
 
-  cytoscape@3.30.2:
-    resolution: {integrity: sha512-oICxQsjW8uSaRmn4UK/jkczKOqTrVqt5/1WL0POiJUT2EKNc9STM4hYFHv917yu55aTBMFNRzymlJhVAiWPCxw==}
+  cytoscape@3.33.1:
+    resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==}
     engines: {node: '>=0.10'}
     engines: {node: '>=0.10'}
 
 
   d3-array@2.12.1:
   d3-array@2.12.1:
     resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
     resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
 
 
-  d3-array@3.2.3:
-    resolution: {integrity: sha512-JRHwbQQ84XuAESWhvIPaUV4/1UYTBOLiOPGWqgFDHZS1D5QN9c57FbH3QpEnQMYiOXNzKUQyGTZf+EVO7RT5TQ==}
+  d3-array@3.2.4:
+    resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
 
 
   d3-axis@3.0.0:
   d3-axis@3.0.0:
@@ -7779,8 +7799,8 @@ packages:
     resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
     resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
 
 
-  d3-geo@3.1.0:
-    resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==}
+  d3-geo@3.1.1:
+    resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
 
 
   d3-hierarchy@3.1.2:
   d3-hierarchy@3.1.2:
@@ -7813,8 +7833,8 @@ packages:
   d3-sankey@0.12.3:
   d3-sankey@0.12.3:
     resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==}
     resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==}
 
 
-  d3-scale-chromatic@3.0.0:
-    resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==}
+  d3-scale-chromatic@3.1.0:
+    resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
 
 
   d3-scale@4.0.2:
   d3-scale@4.0.2:
@@ -8036,8 +8056,8 @@ packages:
     resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}
     resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}
     engines: {node: '>=10'}
     engines: {node: '>=10'}
 
 
-  delaunator@5.0.0:
-    resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==}
+  delaunator@5.0.1:
+    resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
 
 
   delayed-stream@1.0.0:
   delayed-stream@1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
@@ -8254,6 +8274,9 @@ packages:
   electron-to-chromium@1.5.190:
   electron-to-chromium@1.5.190:
     resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==}
     resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==}
 
 
+  electron-to-chromium@1.5.207:
+    resolution: {integrity: sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==}
+
   emittery@0.13.1:
   emittery@0.13.1:
     resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
     resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
@@ -8308,6 +8331,10 @@ packages:
     resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
     resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
     engines: {node: '>=10.13.0'}
     engines: {node: '>=10.13.0'}
 
 
+  enhanced-resolve@5.18.3:
+    resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
+    engines: {node: '>=10.13.0'}
+
   enquirer@2.4.1:
   enquirer@2.4.1:
     resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
     resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
     engines: {node: '>=8.6'}
     engines: {node: '>=8.6'}
@@ -8930,6 +8957,15 @@ packages:
     resolution: {integrity: sha512-CtbfI3oFFc3nbdIoHycrfbrxiGgxXBXXuyOl49h47JawM1mYrqpiRqnH5CB2mBatdXvHHOUO6a+RiAuuvKt0lw==}
     resolution: {integrity: sha512-CtbfI3oFFc3nbdIoHycrfbrxiGgxXBXXuyOl49h47JawM1mYrqpiRqnH5CB2mBatdXvHHOUO6a+RiAuuvKt0lw==}
     engines: {node: '>=8'}
     engines: {node: '>=8'}
 
 
+  follow-redirects@1.15.11:
+    resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
   follow-redirects@1.15.9:
   follow-redirects@1.15.9:
     resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
     resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
     engines: {node: '>=4.0'}
     engines: {node: '>=4.0'}
@@ -10787,8 +10823,8 @@ packages:
   markdown-table@3.0.3:
   markdown-table@3.0.3:
     resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
     resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
 
 
-  marked@16.1.1:
-    resolution: {integrity: sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==}
+  marked@16.2.0:
+    resolution: {integrity: sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg==}
     engines: {node: '>= 20'}
     engines: {node: '>= 20'}
     hasBin: true
     hasBin: true
 
 
@@ -10927,8 +10963,8 @@ packages:
     resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
     resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
     engines: {node: '>= 8'}
     engines: {node: '>= 8'}
 
 
-  mermaid@11.9.0:
-    resolution: {integrity: sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==}
+  mermaid@11.10.0:
+    resolution: {integrity: sha512-oQsFzPBy9xlpnGxUqLbVY8pvknLlsNIJ0NWwi8SUJjhbP1IT0E0o1lfhU4iYV3ubpy+xkzkaOyDUQMn06vQElQ==}
 
 
   method-override@3.0.0:
   method-override@3.0.0:
     resolution: {integrity: sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==}
     resolution: {integrity: sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==}
@@ -12359,6 +12395,7 @@ packages:
   puppeteer@23.6.1:
   puppeteer@23.6.1:
     resolution: {integrity: sha512-8+ALGQgwXd3P/tGcuSsxTPGDaOQIjcDIm04I5hpWZv/PiN5q8bQNHRUyfYrifT+flnM9aTWCP7tLEzuB6SlIgA==}
     resolution: {integrity: sha512-8+ALGQgwXd3P/tGcuSsxTPGDaOQIjcDIm04I5hpWZv/PiN5q8bQNHRUyfYrifT+flnM9aTWCP7tLEzuB6SlIgA==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
+    deprecated: < 24.9.0 is no longer supported
     hasBin: true
     hasBin: true
 
 
   pure-rand@6.1.0:
   pure-rand@6.1.0:
@@ -12380,8 +12417,8 @@ packages:
     resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==}
     resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==}
     engines: {node: '>=0.6'}
     engines: {node: '>=0.6'}
 
 
-  quansync@0.2.10:
-    resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
+  quansync@0.2.11:
+    resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
 
 
   query-string@7.1.3:
   query-string@7.1.3:
     resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
     resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
@@ -12962,8 +12999,8 @@ packages:
   rndm@1.2.0:
   rndm@1.2.0:
     resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==}
     resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==}
 
 
-  robust-predicates@3.0.1:
-    resolution: {integrity: sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==}
+  robust-predicates@3.0.2:
+    resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
 
 
   rollup-plugin-node-externals@6.1.1:
   rollup-plugin-node-externals@6.1.1:
     resolution: {integrity: sha512-127OFMkpH5rBVlRHRBDUMk1m1sGuzbGy7so5aj/IkpUb2r3+wOWjR/erUzd2ChEQWPsxsyQG6xpYYvPBAdcBRA==}
     resolution: {integrity: sha512-127OFMkpH5rBVlRHRBDUMk1m1sGuzbGy7so5aj/IkpUb2r3+wOWjR/erUzd2ChEQWPsxsyQG6xpYYvPBAdcBRA==}
@@ -14268,8 +14305,8 @@ packages:
   typpy@2.3.11:
   typpy@2.3.11:
     resolution: {integrity: sha512-Jh/fykZSaxeKO0ceMAs6agki9T5TNA9kiIR6fzKbvafKpIw8UlNlHhzuqKyi5lfJJ5VojJOx9tooIbyy7vHV/g==}
     resolution: {integrity: sha512-Jh/fykZSaxeKO0ceMAs6agki9T5TNA9kiIR6fzKbvafKpIw8UlNlHhzuqKyi5lfJJ5VojJOx9tooIbyy7vHV/g==}
 
 
-  ua-parser-js@1.0.40:
-    resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==}
+  ua-parser-js@1.0.41:
+    resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
     hasBin: true
     hasBin: true
 
 
   uberproto@1.2.0:
   uberproto@1.2.0:
@@ -14278,8 +14315,8 @@ packages:
   uc.micro@1.0.6:
   uc.micro@1.0.6:
     resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
     resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
 
 
-  ufo@1.5.4:
-    resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
+  ufo@1.6.1:
+    resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
 
 
   uglify-js@3.19.3:
   uglify-js@3.19.3:
     resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
     resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
@@ -16483,7 +16520,7 @@ snapshots:
   '@biomejs/cli-win32-x64@2.0.6':
   '@biomejs/cli-win32-x64@2.0.6':
     optional: true
     optional: true
 
 
-  '@braintree/sanitize-url@7.1.0': {}
+  '@braintree/sanitize-url@7.1.1': {}
 
 
   '@browser-bunyan/console-formatted-stream@1.8.0':
   '@browser-bunyan/console-formatted-stream@1.8.0':
     dependencies:
     dependencies:
@@ -16884,6 +16921,15 @@ snapshots:
       '@lezer/lr': 1.4.2
       '@lezer/lr': 1.4.2
       style-mod: 4.1.2
       style-mod: 4.1.2
 
 
+  '@codemirror/language@6.11.3':
+    dependencies:
+      '@codemirror/state': 6.5.2
+      '@codemirror/view': 6.38.1
+      '@lezer/common': 1.2.3
+      '@lezer/highlight': 1.2.1
+      '@lezer/lr': 1.4.2
+      style-mod: 4.1.2
+
   '@codemirror/legacy-modes@6.4.1':
   '@codemirror/legacy-modes@6.4.1':
     dependencies:
     dependencies:
       '@codemirror/language': 6.11.2
       '@codemirror/language': 6.11.2
@@ -16914,7 +16960,7 @@ snapshots:
 
 
   '@codemirror/theme-one-dark@6.1.2':
   '@codemirror/theme-one-dark@6.1.2':
     dependencies:
     dependencies:
-      '@codemirror/language': 6.11.2
+      '@codemirror/language': 6.11.3
       '@codemirror/state': 6.5.2
       '@codemirror/state': 6.5.2
       '@codemirror/view': 6.38.1
       '@codemirror/view': 6.38.1
       '@lezer/highlight': 1.2.1
       '@lezer/highlight': 1.2.1
@@ -17502,20 +17548,32 @@ snapshots:
       '@jridgewell/sourcemap-codec': 1.5.4
       '@jridgewell/sourcemap-codec': 1.5.4
       '@jridgewell/trace-mapping': 0.3.29
       '@jridgewell/trace-mapping': 0.3.29
 
 
+  '@jridgewell/gen-mapping@0.3.13':
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.5
+      '@jridgewell/trace-mapping': 0.3.30
+
   '@jridgewell/resolve-uri@3.1.2': {}
   '@jridgewell/resolve-uri@3.1.2': {}
 
 
-  '@jridgewell/source-map@0.3.10':
+  '@jridgewell/source-map@0.3.11':
     dependencies:
     dependencies:
-      '@jridgewell/gen-mapping': 0.3.12
-      '@jridgewell/trace-mapping': 0.3.29
+      '@jridgewell/gen-mapping': 0.3.13
+      '@jridgewell/trace-mapping': 0.3.30
 
 
   '@jridgewell/sourcemap-codec@1.5.4': {}
   '@jridgewell/sourcemap-codec@1.5.4': {}
 
 
+  '@jridgewell/sourcemap-codec@1.5.5': {}
+
   '@jridgewell/trace-mapping@0.3.29':
   '@jridgewell/trace-mapping@0.3.29':
     dependencies:
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.4
       '@jridgewell/sourcemap-codec': 1.5.4
 
 
+  '@jridgewell/trace-mapping@0.3.30':
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.2
+      '@jridgewell/sourcemap-codec': 1.5.5
+
   '@jridgewell/trace-mapping@0.3.9':
   '@jridgewell/trace-mapping@0.3.9':
     dependencies:
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/resolve-uri': 3.1.2
@@ -20634,7 +20692,7 @@ snapshots:
 
 
   '@types/d3-delaunay@6.0.4': {}
   '@types/d3-delaunay@6.0.4': {}
 
 
-  '@types/d3-dispatch@3.0.6': {}
+  '@types/d3-dispatch@3.0.7': {}
 
 
   '@types/d3-drag@3.0.7':
   '@types/d3-drag@3.0.7':
     dependencies:
     dependencies:
@@ -20706,7 +20764,7 @@ snapshots:
       '@types/d3-color': 3.1.3
       '@types/d3-color': 3.1.3
       '@types/d3-contour': 3.0.6
       '@types/d3-contour': 3.0.6
       '@types/d3-delaunay': 6.0.4
       '@types/d3-delaunay': 6.0.4
-      '@types/d3-dispatch': 3.0.6
+      '@types/d3-dispatch': 3.0.7
       '@types/d3-drag': 3.0.7
       '@types/d3-drag': 3.0.7
       '@types/d3-dsv': 3.0.7
       '@types/d3-dsv': 3.0.7
       '@types/d3-ease': 3.0.2
       '@types/d3-ease': 3.0.2
@@ -22203,7 +22261,7 @@ snapshots:
       serve-static: 1.13.2
       serve-static: 1.13.2
       server-destroy: 1.0.1
       server-destroy: 1.0.1
       socket.io: 4.8.1
       socket.io: 4.8.1
-      ua-parser-js: 1.0.40
+      ua-parser-js: 1.0.41
       yargs: 17.7.2
       yargs: 17.7.2
     transitivePeerDependencies:
     transitivePeerDependencies:
       - bufferutil
       - bufferutil
@@ -22218,6 +22276,13 @@ snapshots:
       node-releases: 2.0.19
       node-releases: 2.0.19
       update-browserslist-db: 1.1.3(browserslist@4.25.1)
       update-browserslist-db: 1.1.3(browserslist@4.25.1)
 
 
+  browserslist@4.25.3:
+    dependencies:
+      caniuse-lite: 1.0.30001735
+      electron-to-chromium: 1.5.207
+      node-releases: 2.0.19
+      update-browserslist-db: 1.1.3(browserslist@4.25.3)
+
   bs-recipes@1.3.4: {}
   bs-recipes@1.3.4: {}
 
 
   bser@2.1.1:
   bser@2.1.1:
@@ -22396,6 +22461,8 @@ snapshots:
 
 
   caniuse-lite@1.0.30001727: {}
   caniuse-lite@1.0.30001727: {}
 
 
+  caniuse-lite@1.0.30001735: {}
+
   capital-case@1.0.4:
   capital-case@1.0.4:
     dependencies:
     dependencies:
       no-case: 3.0.4
       no-case: 3.0.4
@@ -23101,23 +23168,23 @@ snapshots:
     dependencies:
     dependencies:
       array-find-index: 1.0.2
       array-find-index: 1.0.2
 
 
-  cytoscape-cose-bilkent@4.1.0(cytoscape@3.30.2):
+  cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1):
     dependencies:
     dependencies:
       cose-base: 1.0.3
       cose-base: 1.0.3
-      cytoscape: 3.30.2
+      cytoscape: 3.33.1
 
 
-  cytoscape-fcose@2.2.0(cytoscape@3.30.2):
+  cytoscape-fcose@2.2.0(cytoscape@3.33.1):
     dependencies:
     dependencies:
       cose-base: 2.2.0
       cose-base: 2.2.0
-      cytoscape: 3.30.2
+      cytoscape: 3.33.1
 
 
-  cytoscape@3.30.2: {}
+  cytoscape@3.33.1: {}
 
 
   d3-array@2.12.1:
   d3-array@2.12.1:
     dependencies:
     dependencies:
       internmap: 1.0.1
       internmap: 1.0.1
 
 
-  d3-array@3.2.3:
+  d3-array@3.2.4:
     dependencies:
     dependencies:
       internmap: 2.0.3
       internmap: 2.0.3
 
 
@@ -23139,11 +23206,11 @@ snapshots:
 
 
   d3-contour@4.0.2:
   d3-contour@4.0.2:
     dependencies:
     dependencies:
-      d3-array: 3.2.3
+      d3-array: 3.2.4
 
 
   d3-delaunay@6.0.4:
   d3-delaunay@6.0.4:
     dependencies:
     dependencies:
-      delaunator: 5.0.0
+      delaunator: 5.0.1
 
 
   d3-dispatch@3.0.1: {}
   d3-dispatch@3.0.1: {}
 
 
@@ -23172,9 +23239,9 @@ snapshots:
 
 
   d3-format@3.1.0: {}
   d3-format@3.1.0: {}
 
 
-  d3-geo@3.1.0:
+  d3-geo@3.1.1:
     dependencies:
     dependencies:
-      d3-array: 3.2.3
+      d3-array: 3.2.4
 
 
   d3-hierarchy@3.1.2: {}
   d3-hierarchy@3.1.2: {}
 
 
@@ -23197,14 +23264,14 @@ snapshots:
       d3-array: 2.12.1
       d3-array: 2.12.1
       d3-shape: 1.3.7
       d3-shape: 1.3.7
 
 
-  d3-scale-chromatic@3.0.0:
+  d3-scale-chromatic@3.1.0:
     dependencies:
     dependencies:
       d3-color: 3.1.0
       d3-color: 3.1.0
       d3-interpolate: 3.0.1
       d3-interpolate: 3.0.1
 
 
   d3-scale@4.0.2:
   d3-scale@4.0.2:
     dependencies:
     dependencies:
-      d3-array: 3.2.3
+      d3-array: 3.2.4
       d3-format: 3.1.0
       d3-format: 3.1.0
       d3-interpolate: 3.0.1
       d3-interpolate: 3.0.1
       d3-time: 3.1.0
       d3-time: 3.1.0
@@ -23226,7 +23293,7 @@ snapshots:
 
 
   d3-time@3.1.0:
   d3-time@3.1.0:
     dependencies:
     dependencies:
-      d3-array: 3.2.3
+      d3-array: 3.2.4
 
 
   d3-timer@3.0.1: {}
   d3-timer@3.0.1: {}
 
 
@@ -23249,7 +23316,7 @@ snapshots:
 
 
   d3@7.9.0:
   d3@7.9.0:
     dependencies:
     dependencies:
-      d3-array: 3.2.3
+      d3-array: 3.2.4
       d3-axis: 3.0.0
       d3-axis: 3.0.0
       d3-brush: 3.0.0
       d3-brush: 3.0.0
       d3-chord: 3.0.1
       d3-chord: 3.0.1
@@ -23263,7 +23330,7 @@ snapshots:
       d3-fetch: 3.0.1
       d3-fetch: 3.0.1
       d3-force: 3.0.0
       d3-force: 3.0.0
       d3-format: 3.1.0
       d3-format: 3.1.0
-      d3-geo: 3.1.0
+      d3-geo: 3.1.1
       d3-hierarchy: 3.1.2
       d3-hierarchy: 3.1.2
       d3-interpolate: 3.0.1
       d3-interpolate: 3.0.1
       d3-path: 3.1.0
       d3-path: 3.1.0
@@ -23271,7 +23338,7 @@ snapshots:
       d3-quadtree: 3.0.1
       d3-quadtree: 3.0.1
       d3-random: 3.0.1
       d3-random: 3.0.1
       d3-scale: 4.0.2
       d3-scale: 4.0.2
-      d3-scale-chromatic: 3.0.0
+      d3-scale-chromatic: 3.1.0
       d3-selection: 3.0.0
       d3-selection: 3.0.0
       d3-shape: 3.2.0
       d3-shape: 3.2.0
       d3-time: 3.1.0
       d3-time: 3.1.0
@@ -23439,9 +23506,9 @@ snapshots:
       rimraf: 3.0.2
       rimraf: 3.0.2
       slash: 3.0.0
       slash: 3.0.0
 
 
-  delaunator@5.0.0:
+  delaunator@5.0.1:
     dependencies:
     dependencies:
-      robust-predicates: 3.0.1
+      robust-predicates: 3.0.2
 
 
   delayed-stream@1.0.0: {}
   delayed-stream@1.0.0: {}
 
 
@@ -23640,6 +23707,8 @@ snapshots:
 
 
   electron-to-chromium@1.5.190: {}
   electron-to-chromium@1.5.190: {}
 
 
+  electron-to-chromium@1.5.207: {}
+
   emittery@0.13.1: {}
   emittery@0.13.1: {}
 
 
   emoji-mart@5.6.0: {}
   emoji-mart@5.6.0: {}
@@ -23708,6 +23777,11 @@ snapshots:
       graceful-fs: 4.2.11
       graceful-fs: 4.2.11
       tapable: 2.2.2
       tapable: 2.2.2
 
 
+  enhanced-resolve@5.18.3:
+    dependencies:
+      graceful-fs: 4.2.11
+      tapable: 2.2.2
+
   enquirer@2.4.1:
   enquirer@2.4.1:
     dependencies:
     dependencies:
       ansi-colors: 4.1.3
       ansi-colors: 4.1.3
@@ -24582,6 +24656,8 @@ snapshots:
 
 
   fn-args@5.0.0: {}
   fn-args@5.0.0: {}
 
 
+  follow-redirects@1.15.11: {}
+
   follow-redirects@1.15.9(debug@4.4.1):
   follow-redirects@1.15.9(debug@4.4.1):
     optionalDependencies:
     optionalDependencies:
       debug: 4.4.1(supports-color@5.5.0)
       debug: 4.4.1(supports-color@5.5.0)
@@ -25371,7 +25447,7 @@ snapshots:
   http-proxy@1.18.1:
   http-proxy@1.18.1:
     dependencies:
     dependencies:
       eventemitter3: 4.0.7
       eventemitter3: 4.0.7
-      follow-redirects: 1.15.9(debug@4.4.1)
+      follow-redirects: 1.15.11
       requires-port: 1.0.0
       requires-port: 1.0.0
     transitivePeerDependencies:
     transitivePeerDependencies:
       - debug
       - debug
@@ -26572,7 +26648,7 @@ snapshots:
     dependencies:
     dependencies:
       mlly: 1.7.4
       mlly: 1.7.4
       pkg-types: 2.2.0
       pkg-types: 2.2.0
-      quansync: 0.2.10
+      quansync: 0.2.11
 
 
   locate-path@2.0.0:
   locate-path@2.0.0:
     dependencies:
     dependencies:
@@ -26810,7 +26886,7 @@ snapshots:
 
 
   markdown-table@3.0.3: {}
   markdown-table@3.0.3: {}
 
 
-  marked@16.1.1: {}
+  marked@16.2.0: {}
 
 
   material-icons@1.13.12: {}
   material-icons@1.13.12: {}
 
 
@@ -27098,15 +27174,15 @@ snapshots:
 
 
   merge2@1.4.1: {}
   merge2@1.4.1: {}
 
 
-  mermaid@11.9.0:
+  mermaid@11.10.0:
     dependencies:
     dependencies:
-      '@braintree/sanitize-url': 7.1.0
+      '@braintree/sanitize-url': 7.1.1
       '@iconify/utils': 2.3.0
       '@iconify/utils': 2.3.0
       '@mermaid-js/parser': 0.6.2
       '@mermaid-js/parser': 0.6.2
       '@types/d3': 7.4.3
       '@types/d3': 7.4.3
-      cytoscape: 3.30.2
-      cytoscape-cose-bilkent: 4.1.0(cytoscape@3.30.2)
-      cytoscape-fcose: 2.2.0(cytoscape@3.30.2)
+      cytoscape: 3.33.1
+      cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1)
+      cytoscape-fcose: 2.2.0(cytoscape@3.33.1)
       d3: 7.9.0
       d3: 7.9.0
       d3-sankey: 0.12.3
       d3-sankey: 0.12.3
       dagre-d3-es: 7.0.11
       dagre-d3-es: 7.0.11
@@ -27115,7 +27191,7 @@ snapshots:
       katex: 0.16.22
       katex: 0.16.22
       khroma: 2.1.0
       khroma: 2.1.0
       lodash-es: 4.17.21
       lodash-es: 4.17.21
-      marked: 16.1.1
+      marked: 16.2.0
       roughjs: 4.6.6
       roughjs: 4.6.6
       stylis: 4.3.6
       stylis: 4.3.6
       ts-dedent: 2.2.0
       ts-dedent: 2.2.0
@@ -27236,7 +27312,7 @@ snapshots:
     dependencies:
     dependencies:
       '@types/katex': 0.16.7
       '@types/katex': 0.16.7
       devlop: 1.1.0
       devlop: 1.1.0
-      katex: 0.16.21
+      katex: 0.16.22
       micromark-factory-space: 2.0.0
       micromark-factory-space: 2.0.0
       micromark-util-character: 2.1.0
       micromark-util-character: 2.1.0
       micromark-util-symbol: 2.0.0
       micromark-util-symbol: 2.0.0
@@ -27503,7 +27579,7 @@ snapshots:
       acorn: 8.15.0
       acorn: 8.15.0
       pathe: 2.0.3
       pathe: 2.0.3
       pkg-types: 1.3.1
       pkg-types: 1.3.1
-      ufo: 1.5.4
+      ufo: 1.6.1
 
 
   mock-require@3.0.3:
   mock-require@3.0.3:
     dependencies:
     dependencies:
@@ -28823,7 +28899,7 @@ snapshots:
 
 
   qs@6.5.2: {}
   qs@6.5.2: {}
 
 
-  quansync@0.2.10: {}
+  quansync@0.2.11: {}
 
 
   query-string@7.1.3:
   query-string@7.1.3:
     dependencies:
     dependencies:
@@ -29623,7 +29699,7 @@ snapshots:
 
 
   rndm@1.2.0: {}
   rndm@1.2.0: {}
 
 
-  robust-predicates@3.0.1: {}
+  robust-predicates@3.0.2: {}
 
 
   rollup-plugin-node-externals@6.1.1(rollup@4.41.0):
   rollup-plugin-node-externals@6.1.1(rollup@4.41.0):
     dependencies:
     dependencies:
@@ -30699,7 +30775,7 @@ snapshots:
 
 
   terser-webpack-plugin@5.3.14(@swc/core@1.10.7(@swc/helpers@0.5.15))(webpack@5.92.1(@swc/core@1.10.7(@swc/helpers@0.5.15))):
   terser-webpack-plugin@5.3.14(@swc/core@1.10.7(@swc/helpers@0.5.15))(webpack@5.92.1(@swc/core@1.10.7(@swc/helpers@0.5.15))):
     dependencies:
     dependencies:
-      '@jridgewell/trace-mapping': 0.3.29
+      '@jridgewell/trace-mapping': 0.3.30
       jest-worker: 27.5.1
       jest-worker: 27.5.1
       schema-utils: 4.3.2
       schema-utils: 4.3.2
       serialize-javascript: 6.0.2
       serialize-javascript: 6.0.2
@@ -30710,7 +30786,7 @@ snapshots:
 
 
   terser@5.43.1:
   terser@5.43.1:
     dependencies:
     dependencies:
-      '@jridgewell/source-map': 0.3.10
+      '@jridgewell/source-map': 0.3.11
       acorn: 8.15.0
       acorn: 8.15.0
       commander: 2.20.3
       commander: 2.20.3
       source-map-support: 0.5.21
       source-map-support: 0.5.21
@@ -31145,13 +31221,13 @@ snapshots:
     dependencies:
     dependencies:
       function.name: 1.0.12
       function.name: 1.0.12
 
 
-  ua-parser-js@1.0.40: {}
+  ua-parser-js@1.0.41: {}
 
 
   uberproto@1.2.0: {}
   uberproto@1.2.0: {}
 
 
   uc.micro@1.0.6: {}
   uc.micro@1.0.6: {}
 
 
-  ufo@1.5.4: {}
+  ufo@1.6.1: {}
 
 
   uglify-js@3.19.3:
   uglify-js@3.19.3:
     optional: true
     optional: true
@@ -31323,6 +31399,12 @@ snapshots:
       escalade: 3.2.0
       escalade: 3.2.0
       picocolors: 1.1.1
       picocolors: 1.1.1
 
 
+  update-browserslist-db@1.1.3(browserslist@4.25.3):
+    dependencies:
+      browserslist: 4.25.3
+      escalade: 3.2.0
+      picocolors: 1.1.1
+
   update-notifier@7.3.1:
   update-notifier@7.3.1:
     dependencies:
     dependencies:
       boxen: 8.0.1
       boxen: 8.0.1
@@ -31765,9 +31847,9 @@ snapshots:
       '@webassemblyjs/wasm-parser': 1.14.1
       '@webassemblyjs/wasm-parser': 1.14.1
       acorn: 8.15.0
       acorn: 8.15.0
       acorn-import-attributes: 1.9.5(acorn@8.15.0)
       acorn-import-attributes: 1.9.5(acorn@8.15.0)
-      browserslist: 4.25.1
+      browserslist: 4.25.3
       chrome-trace-event: 1.0.4
       chrome-trace-event: 1.0.4
-      enhanced-resolve: 5.18.2
+      enhanced-resolve: 5.18.3
       es-module-lexer: 1.7.0
       es-module-lexer: 1.7.0
       eslint-scope: 5.1.1
       eslint-scope: 5.1.1
       events: 3.3.0
       events: 3.3.0