From e58e5b087986dcfce872d48498c78646e9766609 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 17 Sep 2026 02:50:31 +0000 Subject: [PATCH 01/28] ci: shard API unit tests across runners (#42374) --- .github/workflows/api-tests.yml | 38 +++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index f579862eb8b..1e0d0b8f541 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -15,23 +15,30 @@ concurrency: jobs: api-unit: - name: API Unit Tests + name: API Unit Tests (${{ matrix.python-version }}, shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) runs-on: depot-ubuntu-24.04 env: - COVERAGE_FILE: coverage-unit + COVERAGE_FILE: coverage-unit-${{ matrix.python-version }}-${{ matrix.shardIndex }} defaults: run: shell: bash strategy: + fail-fast: false matrix: python-version: - '3.12' + shardIndex: + - 1 + - 2 + - 3 + - 4 + shardTotal: + - 4 steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 persist-credentials: false - name: Setup UV and Python @@ -53,6 +60,10 @@ jobs: -p no:benchmark \ --timeout "${PYTEST_TIMEOUT:-20}" \ -n auto \ + --shard-index "${{ matrix.shardIndex }}" \ + --shard-total "${{ matrix.shardTotal }}" \ + -o 'addopts=--cov=./api --cov-branch --import-mode=importlib' \ + --cov-report= \ api/tests/unit_tests \ api/providers/vdb/*/tests/unit_tests \ api/providers/trace/*/tests/unit_tests \ @@ -60,14 +71,19 @@ jobs: # Controller tests register Flask routes at import time, so keep them out of xdist. uv run --project api pytest \ --timeout "${PYTEST_TIMEOUT:-20}" \ + --shard-index "${{ matrix.shardIndex }}" \ + --shard-total "${{ matrix.shardTotal }}" \ + -o 'addopts=--cov=./api --cov-branch --import-mode=importlib' \ + --cov-report= \ --cov-append \ api/tests/unit_tests/controllers - name: Upload unit coverage data uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: api-coverage-unit - path: coverage-unit + name: api-coverage-unit-${{ matrix.python-version }}-${{ matrix.shardIndex }} + path: ${{ env.COVERAGE_FILE }} + if-no-files-found: error retention-days: 1 api-integration: @@ -98,7 +114,6 @@ jobs: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 persist-credentials: false - name: Setup UV and Python @@ -122,6 +137,8 @@ jobs: -n auto \ --shard-index "${{ matrix.shardIndex }}" \ --shard-total "${{ matrix.shardTotal }}" \ + -o 'addopts=--cov=./api --cov-branch --import-mode=importlib' \ + --cov-report= \ --timeout "${PYTEST_TIMEOUT:-180}" \ api/tests/integration_tests/workflow \ api/tests/integration_tests/tools \ @@ -181,16 +198,19 @@ jobs: echo "Merged backend coverage report generated for Codecov project status." >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - unit_coverage="$(find coverage-data -type f -name coverage-unit -print -quit)" + mapfile -t unit_coverage < <(find coverage-data -type f -name 'coverage-unit-*' -print | sort) mapfile -t integration_coverage < <(find coverage-data -type f -name 'coverage-integration-*' -print | sort) - : "${unit_coverage:?coverage-unit artifact not found}" + if [[ "${#unit_coverage[@]}" -ne 4 ]]; then + echo "expected 4 unit coverage artifacts, found ${#unit_coverage[@]}" >&2 + exit 1 + fi if [[ "${#integration_coverage[@]}" -ne 4 ]]; then echo "expected 4 integration coverage artifacts, found ${#integration_coverage[@]}" >&2 exit 1 fi report_file="$(mktemp)" - uv run --project api coverage combine "$unit_coverage" "${integration_coverage[@]}" + uv run --project api coverage combine "${unit_coverage[@]}" "${integration_coverage[@]}" uv run --project api coverage report --show-missing | tee "$report_file" echo "Summary: \`$(tail -n 1 "$report_file")\`" >> "$GITHUB_STEP_SUMMARY" { From fea6b6e999395abfe611218371947f6e2453a77e Mon Sep 17 00:00:00 2001 From: coderdailyone <128658232+coderdailyone@users.noreply.github.com> Date: Thu, 17 Sep 2026 02:56:58 +0000 Subject: [PATCH 02/28] fix(rag): derive url file suffix from content-type media type in load_from_url (#42364) Co-authored-by: coderdailyone Co-authored-by: Claude Fable 5.1 --- api/core/rag/extractor/extract_processor.py | 8 +++--- .../rag/extractor/test_extract_processor.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/api/core/rag/extractor/extract_processor.py b/api/core/rag/extractor/extract_processor.py index 1d2f98075ef..dd73b436089 100644 --- a/api/core/rag/extractor/extract_processor.py +++ b/api/core/rag/extractor/extract_processor.py @@ -83,10 +83,12 @@ class ExtractProcessor: suffix = Path(url).suffix if not suffix and suffix != ".": # get content-type - if response.headers.get("Content-Type"): - suffix = "." + response.headers.get("Content-Type").split("/")[-1] + content_type = response.headers.get("Content-Type") + if content_type: + # strip parameters such as "; charset=utf-8" before using the subtype as suffix + suffix = "." + content_type.split(";")[0].strip().split("/")[-1] else: - content_disposition = response.headers.get("Content-Disposition") + content_disposition = response.headers.get("Content-Disposition", "") filename_match = re.search(r'filename="([^"]+)"', content_disposition) if filename_match: filename = unquote(filename_match.group(1)) diff --git a/api/tests/unit_tests/core/rag/extractor/test_extract_processor.py b/api/tests/unit_tests/core/rag/extractor/test_extract_processor.py index 6b81f24fcb8..2f00fda06db 100644 --- a/api/tests/unit_tests/core/rag/extractor/test_extract_processor.py +++ b/api/tests/unit_tests/core/rag/extractor/test_extract_processor.py @@ -105,6 +105,16 @@ class TestExtractProcessorLoaders: [ ("https://example.com/file.txt", {"Content-Type": "text/plain"}, ".txt"), ("https://example.com/no_suffix", {"Content-Type": "application/pdf"}, ".pdf"), + ( + "https://example.com/no_suffix", + {"Content-Type": "application/pdf; charset=binary"}, + ".pdf", + ), + ( + "https://example.com/no_suffix", + {"Content-Type": "text/html; charset=utf-8"}, + ".html", + ), ( "https://example.com/no_suffix", {"Content-Disposition": 'attachment; filename="report.md"'}, @@ -115,6 +125,7 @@ class TestExtractProcessorLoaders: {"Content-Disposition": 'attachment; filename="report"'}, "", ), + ("https://example.com/no_suffix", {}, ""), ], ) def test_load_from_url_builds_temp_file_with_correct_suffix( @@ -167,6 +178,21 @@ class TestExtractProcessorLoaders: assert args[1] is None assert args[2] is None + def test_load_from_url_routes_parameterized_content_type(self, monkeypatch: pytest.MonkeyPatch): + # Servers commonly append parameters such as "; charset=binary" to + # Content-Type; the media type must be parsed before deriving the suffix. + response = SimpleNamespace( + headers={"Content-Type": "application/pdf; charset=binary"}, content=b"%PDF-1.1 body" + ) + monkeypatch.setattr(processor_module.remote_fetcher, "make_request", lambda *args, **kwargs: response) + factory = _patch_all_extractors(monkeypatch) + apply_config_overrides(monkeypatch, ETL_TYPE="dify") + + text = ExtractProcessor.load_from_url("https://example.com/report", return_text=True) + + assert text == "extracted-by-PdfExtractor" + assert factory.calls[-1][0] == "PdfExtractor" + class TestExtractProcessorFileRouting: @pytest.fixture(autouse=True) From 47a28bf2622b4050f0736be88da4b04f4e4a88a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BF=97=E8=B0=A6?= <89645338+simpleqt@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:13:48 +0000 Subject: [PATCH 03/28] fix(api): align the datasource docstrings with their signatures (#42369) Signed-off-by: simpleqt <89645338+simpleqt@users.noreply.github.com> --- api/core/datasource/datasource_manager.py | 2 +- api/core/datasource/entities/datasource_entities.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core/datasource/datasource_manager.py b/api/core/datasource/datasource_manager.py index f0dcb13b622..b96d98d3f9c 100644 --- a/api/core/datasource/datasource_manager.py +++ b/api/core/datasource/datasource_manager.py @@ -115,7 +115,7 @@ class DatasourceManager: """ get the datasource runtime - :param provider_type: the type of the provider + :param datasource_type: the type of the datasource :param provider_id: the id of the provider :param datasource_name: the name of the datasource :param tenant_id: the tenant id diff --git a/api/core/datasource/entities/datasource_entities.py b/api/core/datasource/entities/datasource_entities.py index 443b503a693..af9b63ad723 100644 --- a/api/core/datasource/entities/datasource_entities.py +++ b/api/core/datasource/entities/datasource_entities.py @@ -93,7 +93,7 @@ class DatasourceParameter(PluginParameter): :param required: if the parameter is required :param options: the options of the parameter """ - # convert options to ToolParameterOption + # convert options to PluginParameterOption # FIXME fix the type error if options: option_objs = [ From f7ed1c017617783daf237378905a8a661e058a4e Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 17 Sep 2026 03:26:13 +0000 Subject: [PATCH 04/28] fix(ci): preserve system npx in shared web setup (#42382) --- .github/actions/setup-web/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/setup-web/action.yml b/.github/actions/setup-web/action.yml index 01c525bf2e8..1b52e263710 100644 --- a/.github/actions/setup-web/action.yml +++ b/.github/actions/setup-web/action.yml @@ -14,5 +14,6 @@ runs: node-manager: false package-manager: | pnpm: false + npm: false cache: true run-install: true From 8b3c4db9ac4d6475109d6992d8b0848d2fcdb0c7 Mon Sep 17 00:00:00 2001 From: xiaodu55 <1704788159@qq.com> Date: Thu, 17 Sep 2026 03:38:18 +0000 Subject: [PATCH 05/28] refactor(models): pass session into recommended-app and trial-record accessors (#42325) --- api/models/model.py | 19 ++-- .../unit_tests/models/test_app_models.py | 7 +- .../test_recommended_app_session_accessors.py | 102 ++++++++++++++++++ 3 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 api/tests/unit_tests/models/test_recommended_app_session_accessors.py diff --git a/api/models/model.py b/api/models/model.py index 1af4d50c163..79f52eab5a1 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -449,10 +449,6 @@ class App(Base): ) use_icon_as_answer_icon: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.false()) - @property - def desc_or_prompt(self) -> str: - return self.desc_or_prompt_with_session(session=db.session()) - def desc_or_prompt_with_session(self, *, session: Session) -> str: if self.description: return self.description @@ -979,9 +975,8 @@ class RecommendedApp(TypeBase): init=False, ) - @property - def app(self) -> App | None: - return db.session.scalar(select(App).where(App.id == self.app_id)) + def app(self, *, session: Session) -> App | None: + return session.scalar(select(App).where(App.id == self.app_id)) class InstalledApp(TypeBase): @@ -1054,13 +1049,11 @@ class AccountTrialAppRecord(TypeBase): sa.DateTime, nullable=False, server_default=func.current_timestamp(), init=False ) - @property - def app(self) -> App | None: - return db.session.scalar(select(App).where(App.id == self.app_id)) + def app(self, *, session: Session) -> App | None: + return session.scalar(select(App).where(App.id == self.app_id)) - @property - def user(self) -> Account | None: - return db.session.scalar(select(Account).where(Account.id == self.account_id)) + def user(self, *, session: Session) -> Account | None: + return session.scalar(select(Account).where(Account.id == self.account_id)) class ExporleBanner(TypeBase): diff --git a/api/tests/unit_tests/models/test_app_models.py b/api/tests/unit_tests/models/test_app_models.py index 7485031575a..7d0a42ebcb8 100644 --- a/api/tests/unit_tests/models/test_app_models.py +++ b/api/tests/unit_tests/models/test_app_models.py @@ -122,8 +122,9 @@ class TestAppModelValidation: # Assert assert {t.value for t in IconType} == {"image", "emoji", "link"} - def test_app_desc_or_prompt_with_description(self): - """Test desc_or_prompt property when description exists.""" + @pytest.mark.parametrize("sqlite_session", [(App, AppModelConfig)], indirect=True) + def test_app_desc_or_prompt_with_description(self, sqlite_session: Session): + """Test desc_or_prompt_with_session when description exists.""" # Arrange app = App( tenant_id=str(uuid4()), @@ -136,7 +137,7 @@ class TestAppModelValidation: ) # Act - result = app.desc_or_prompt + result = app.desc_or_prompt_with_session(session=sqlite_session) # Assert assert result == "App description" diff --git a/api/tests/unit_tests/models/test_recommended_app_session_accessors.py b/api/tests/unit_tests/models/test_recommended_app_session_accessors.py new file mode 100644 index 00000000000..e560144b66f --- /dev/null +++ b/api/tests/unit_tests/models/test_recommended_app_session_accessors.py @@ -0,0 +1,102 @@ +"""Regression coverage for the ``@property``→session-parameter refactor on +``RecommendedApp`` and ``AccountTrialAppRecord``. + +The legacy ``@property`` accessors reached for the global ``db.session`` internally and have +been converted to plain methods taking an explicit ``session: Session`` (per the pattern +established in #40370/#41394/#41830/#42000, tracked in #40372). +""" + +from uuid import uuid4 + +from sqlalchemy.orm import Session + +from models.account import Account +from models.model import AccountTrialAppRecord, App, AppMode, RecommendedApp + + +def _persist_app(session: Session) -> App: + app = App( + tenant_id=str(uuid4()), + name="Test App", + mode=AppMode.CHAT, + enable_site=True, + enable_api=False, + created_by=str(uuid4()), + description="App description", + ) + session.add(app) + session.flush() + return app + + +def _persist_recommended_app(session: Session, *, app_id: str) -> RecommendedApp: + recommended_app = RecommendedApp( + app_id=app_id, + description={"en_US": "A recommended app"}, + copyright="Dify", + privacy_policy="https://dify.ai/privacy", + category="Assistant", + ) + session.add(recommended_app) + session.flush() + return recommended_app + + +def _persist_account(session: Session) -> Account: + account = Account(name="Test User", email=f"user-{uuid4()}@example.com") + session.add(account) + session.flush() + return account + + +def _persist_trial_record(session: Session, *, account_id: str, app_id: str) -> AccountTrialAppRecord: + record = AccountTrialAppRecord(account_id=account_id, app_id=app_id, count=1) + session.add(record) + session.flush() + return record + + +class TestRecommendedAppApp: + def test_returns_the_matching_app(self, sqlite_session: Session) -> None: + app = _persist_app(sqlite_session) + recommended_app = _persist_recommended_app(sqlite_session, app_id=app.id) + + result = recommended_app.app(session=sqlite_session) + + assert result is not None + assert result.id == app.id + + def test_returns_none_when_app_missing(self, sqlite_session: Session) -> None: + recommended_app = _persist_recommended_app(sqlite_session, app_id=str(uuid4())) + + assert recommended_app.app(session=sqlite_session) is None + + +class TestAccountTrialAppRecordAccessors: + def test_app_returns_the_matching_app(self, sqlite_session: Session) -> None: + app = _persist_app(sqlite_session) + record = _persist_trial_record(sqlite_session, account_id=str(uuid4()), app_id=app.id) + + result = record.app(session=sqlite_session) + + assert result is not None + assert result.id == app.id + + def test_app_returns_none_when_app_missing(self, sqlite_session: Session) -> None: + record = _persist_trial_record(sqlite_session, account_id=str(uuid4()), app_id=str(uuid4())) + + assert record.app(session=sqlite_session) is None + + def test_user_returns_the_matching_account(self, sqlite_session: Session) -> None: + account = _persist_account(sqlite_session) + record = _persist_trial_record(sqlite_session, account_id=account.id, app_id=str(uuid4())) + + result = record.user(session=sqlite_session) + + assert result is not None + assert result.id == account.id + + def test_user_returns_none_when_account_missing(self, sqlite_session: Session) -> None: + record = _persist_trial_record(sqlite_session, account_id=str(uuid4()), app_id=str(uuid4())) + + assert record.user(session=sqlite_session) is None From 4afc3cbd4f3d521c1972cb807c43afbd3f925a7e Mon Sep 17 00:00:00 2001 From: Keith <148296621+keith991001@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:39:04 +0000 Subject: [PATCH 06/28] refactor(models): drop dead to_dict serializers and remaining global-session accessors on dataset models (#42324) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/models/dataset.py | 153 ------------------ .../models/test_dataset_models.py | 4 +- .../unit_tests/models/test_dataset_models.py | 99 +----------- 3 files changed, 3 insertions(+), 253 deletions(-) diff --git a/api/models/dataset.py b/api/models/dataset.py index 28f4ce0ebe5..e0092a09d37 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -96,67 +96,6 @@ class DatasetBindingItem(TypedDict): name: str -class ExternalKnowledgeApiDict(TypedDict): - id: str - tenant_id: str - name: str - description: str - settings: dict[str, Any] | None - dataset_bindings: list[DatasetBindingItem] - created_by: str - created_at: str - - -class DocumentDict(TypedDict): - id: str - tenant_id: str - dataset_id: str - position: int - data_source_type: str - data_source_info: str | None - dataset_process_rule_id: str | None - batch: str - name: str - created_from: str - created_by: str - created_api_request_id: str | None - created_at: datetime - processing_started_at: datetime | None - file_id: str | None - word_count: int | None - parsing_completed_at: datetime | None - cleaning_completed_at: datetime | None - splitting_completed_at: datetime | None - tokens: int | None - indexing_latency: float | None - completed_at: datetime | None - is_paused: bool | None - paused_by: str | None - paused_at: datetime | None - error: str | None - stopped_at: datetime | None - indexing_status: str - enabled: bool - disabled_at: datetime | None - disabled_by: str | None - archived: bool - archived_reason: str | None - archived_by: str | None - archived_at: datetime | None - updated_at: datetime - doc_type: str | None - doc_metadata: Any - doc_form: IndexStructureType - doc_language: str | None - display_status: str | None - data_source_info_dict: dict[str, Any] - average_segment_length: int - dataset_process_rule: ProcessRuleDict | None - dataset: None - segment_count: int | None - hit_count: int | None - - from models.enums import PermissionEnum # Backward-compatible alias — new code should import PermissionEnum from models.enums @@ -627,16 +566,6 @@ class Document(Base): return result return {} - @property - def average_segment_length(self): - if self.word_count and self.word_count != 0 and self.segment_count and self.segment_count != 0: - return self.word_count // self.segment_count - return 0 - - @property - def dataset_process_rule(self): - return self.get_dataset_process_rule(session=db.session()) - def get_dataset_process_rule(self, *, session: Session) -> "DatasetProcessRule | None": if self.dataset_process_rule_id: return session.get(DatasetProcessRule, self.dataset_process_rule_id) @@ -646,17 +575,9 @@ class Document(Base): """Load the owning dataset with the caller-owned database session.""" return session.get(Dataset, self.dataset_id) - @property - def segment_count(self): - return self.get_segment_count(session=db.session()) - def get_segment_count(self, *, session: Session) -> int: return session.scalar(select(func.count(DocumentSegment.id)).where(DocumentSegment.document_id == self.id)) or 0 - @property - def hit_count(self): - return self.get_hit_count(session=db.session()) - def get_hit_count(self, *, session: Session) -> int: return ( session.scalar( @@ -707,12 +628,6 @@ class Document(Base): return metadata_list return None - @property - def process_rule_dict(self) -> ProcessRuleDict | None: - if self.dataset_process_rule_id and self.dataset_process_rule: - return self.dataset_process_rule.to_dict() - return None - def get_built_in_fields(self, *, session: Session) -> list[DocMetadataDetailItem]: built_in_fields: list[DocMetadataDetailItem] = [] built_in_fields.append( @@ -757,58 +672,6 @@ class Document(Base): ) return built_in_fields - def to_dict(self) -> DocumentDict: - result: DocumentDict = { - "id": self.id, - "tenant_id": self.tenant_id, - "dataset_id": self.dataset_id, - "position": self.position, - "data_source_type": self.data_source_type, - "data_source_info": self.data_source_info, - "dataset_process_rule_id": self.dataset_process_rule_id, - "batch": self.batch, - "name": self.name, - "created_from": self.created_from, - "created_by": self.created_by, - "created_api_request_id": self.created_api_request_id, - "created_at": self.created_at, - "processing_started_at": self.processing_started_at, - "file_id": self.file_id, - "word_count": self.word_count, - "parsing_completed_at": self.parsing_completed_at, - "cleaning_completed_at": self.cleaning_completed_at, - "splitting_completed_at": self.splitting_completed_at, - "tokens": self.tokens, - "indexing_latency": self.indexing_latency, - "completed_at": self.completed_at, - "is_paused": self.is_paused, - "paused_by": self.paused_by, - "paused_at": self.paused_at, - "error": self.error, - "stopped_at": self.stopped_at, - "indexing_status": self.indexing_status, - "enabled": self.enabled, - "disabled_at": self.disabled_at, - "disabled_by": self.disabled_by, - "archived": self.archived, - "archived_reason": self.archived_reason, - "archived_by": self.archived_by, - "archived_at": self.archived_at, - "updated_at": self.updated_at, - "doc_type": self.doc_type, - "doc_metadata": self.doc_metadata, - "doc_form": self.doc_form, - "doc_language": self.doc_language, - "display_status": self.display_status, - "data_source_info_dict": self.data_source_info_dict, - "average_segment_length": self.average_segment_length, - "dataset_process_rule": self.dataset_process_rule.to_dict() if self.dataset_process_rule else None, - "dataset": None, - "segment_count": self.segment_count, - "hit_count": self.hit_count, - } - return result - @classmethod def from_dict(cls, data: dict[str, Any]): return cls( @@ -1408,18 +1271,6 @@ class ExternalKnowledgeApis(TypeBase): DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), init=False ) - def to_dict(self) -> ExternalKnowledgeApiDict: - return { - "id": self.id, - "tenant_id": self.tenant_id, - "name": self.name, - "description": self.description, - "settings": self.settings_dict, - "dataset_bindings": self.dataset_bindings, - "created_by": self.created_by, - "created_at": self.created_at.isoformat(), - } - @property def settings_dict(self) -> dict[str, Any] | None: try: @@ -1427,10 +1278,6 @@ class ExternalKnowledgeApis(TypeBase): except JSONDecodeError: return None - @property - def dataset_bindings(self): - return self.get_dataset_bindings(session=db.session()) - def get_dataset_bindings(self, *, session: Session) -> list[DatasetBindingItem]: external_knowledge_bindings = session.scalars( select(ExternalKnowledgeBindings).where(ExternalKnowledgeBindings.external_knowledge_api_id == self.id) diff --git a/api/tests/test_containers_integration_tests/models/test_dataset_models.py b/api/tests/test_containers_integration_tests/models/test_dataset_models.py index 0f25a543c9c..73097625552 100644 --- a/api/tests/test_containers_integration_tests/models/test_dataset_models.py +++ b/api/tests/test_containers_integration_tests/models/test_dataset_models.py @@ -228,7 +228,7 @@ class TestDatasetDocumentProperties: db_session_with_containers.add(seg) db_session_with_containers.flush() - assert doc.segment_count == 3 + assert doc.get_segment_count(session=db_session_with_containers) == 3 def test_document_hit_count_aggregation(self, db_session_with_containers: Session) -> None: """Test document can aggregate hit count from segments.""" @@ -269,7 +269,7 @@ class TestDatasetDocumentProperties: db_session_with_containers.add(seg) db_session_with_containers.flush() - assert doc.hit_count == 25 + assert doc.get_hit_count(session=db_session_with_containers) == 25 class TestDocumentSegmentNavigationProperties: diff --git a/api/tests/unit_tests/models/test_dataset_models.py b/api/tests/unit_tests/models/test_dataset_models.py index 2ca01cf44f9..77e36f10f63 100644 --- a/api/tests/unit_tests/models/test_dataset_models.py +++ b/api/tests/unit_tests/models/test_dataset_models.py @@ -17,12 +17,11 @@ from urllib.parse import parse_qs, urlparse from uuid import uuid4 import pytest -from sqlalchemy.orm import Session, scoped_session, sessionmaker +from sqlalchemy.orm import Session from core.rag.entities import ParentMode from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from extensions.storage.storage_type import StorageType -from models import dataset as dataset_module from models.account import Account from models.dataset import ( AppDatasetJoin, @@ -821,59 +820,6 @@ class TestDocumentModelRelationships: assert document.get_dataset(session=sqlite_session) is dataset - @pytest.mark.parametrize("sqlite_session", [(Document, DocumentSegment)], indirect=True) - def test_document_average_segment_length( - self, - sqlite_session: Session, - sqlite_session_factory: sessionmaker[Session], - monkeypatch: pytest.MonkeyPatch, - ): - """Test average_segment_length property calculation.""" - # Arrange - document = Document( - tenant_id=str(uuid4()), - dataset_id=str(uuid4()), - position=1, - data_source_type=DataSourceType.UPLOAD_FILE, - batch="batch_001", - name="test.pdf", - created_from=DocumentCreatedFrom.WEB, - created_by=str(uuid4()), - word_count=1000, - ) - sqlite_session.add(document) - sqlite_session.flush() - sqlite_session.add_all(_make_segments(document, [0] * 10)) - sqlite_session.commit() - monkeypatch.setattr(dataset_module.db, "session", scoped_session(sqlite_session_factory)) - - # Act - result = document.average_segment_length - - # Assert - assert result == 100 - - def test_document_average_segment_length_zero(self): - """Test average_segment_length property when word_count is zero.""" - # Arrange - document = Document( - tenant_id=str(uuid4()), - dataset_id=str(uuid4()), - position=1, - data_source_type=DataSourceType.UPLOAD_FILE, - batch="batch_001", - name="test.pdf", - created_from=DocumentCreatedFrom.WEB, - created_by=str(uuid4()), - word_count=0, - ) - - # Act - result = document.average_segment_length - - # Assert - assert result == 0 - class TestDocumentSegmentIndexing: """Test suite for DocumentSegment model indexing and operations.""" @@ -1573,49 +1519,6 @@ class TestModelIntegration: assert document.word_count == 100 assert segment.status == SegmentStatus.COMPLETED - @pytest.mark.parametrize("sqlite_session", [(Document, DocumentSegment)], indirect=True) - def test_document_to_dict_serialization( - self, - sqlite_session: Session, - sqlite_session_factory: sessionmaker[Session], - monkeypatch: pytest.MonkeyPatch, - ): - """Test document to_dict method for serialization.""" - # Arrange - tenant_id = str(uuid4()) - dataset_id = str(uuid4()) - created_by = str(uuid4()) - - document = Document( - tenant_id=tenant_id, - dataset_id=dataset_id, - position=1, - data_source_type=DataSourceType.UPLOAD_FILE, - batch="batch_001", - name="test.pdf", - created_from=DocumentCreatedFrom.WEB, - created_by=created_by, - word_count=100, - indexing_status=IndexingStatus.COMPLETED, - ) - sqlite_session.add(document) - sqlite_session.flush() - sqlite_session.add_all(_make_segments(document, [2, 2, 2, 2, 2])) - sqlite_session.commit() - monkeypatch.setattr(dataset_module.db, "session", scoped_session(sqlite_session_factory)) - - # Act - result = document.to_dict() - - # Assert - assert result["tenant_id"] == tenant_id - assert result["dataset_id"] == dataset_id - assert result["name"] == "test.pdf" - assert result["word_count"] == 100 - assert result["indexing_status"] == IndexingStatus.COMPLETED - assert result["segment_count"] == 5 - assert result["hit_count"] == 10 - class TestChildChunkSessionAccessors: """Regression coverage for the ``ChildChunk`` accessors refactored to take a caller-provided session. From f66773b63ce0dbe7a74cd51a05ba89a66415f3d5 Mon Sep 17 00:00:00 2001 From: Eddy ZHANG Date: Thu, 17 Sep 2026 03:40:50 +0000 Subject: [PATCH 07/28] refactor(api): dep-inject the last three GET query models with @model_validate (#42318) --- api/controllers/console/auth/activate.py | 5 +- api/controllers/files/upload_file_delivery.py | 16 ++-- .../console/auth/test_account_activation.py | 19 +++- .../files/test_upload_file_delivery.py | 92 +++++++++++-------- 4 files changed, 83 insertions(+), 49 deletions(-) diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 69ed1899f2e..685342d5a67 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -7,6 +7,7 @@ from controllers.common.schema import query_params_from_model, register_schema_m from controllers.console import console_ns from controllers.console.auth.error import InvitationAccountMismatchError as InvitationAccountMismatchHTTPError from controllers.console.error import AccountInFreezeError, AlreadyActivateError, EmailDomainSuspendedError +from controllers.console.wraps import model_validate from extensions.ext_application_services import application_services from libs.helper import EmailStr, dump_response, timezone from libs.login import current_account_with_tenant @@ -88,8 +89,8 @@ class ActivateCheckApi(Resource): "Success", console_ns.models[ActivationCheckResponse.__name__], ) - def get(self): - args = ActivateCheckQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(ActivateCheckQuery) + def get(self, args: ActivateCheckQuery): result = application_services().account_activation.check( InvitationLookup( workspace_id=args.workspace_id, diff --git a/api/controllers/files/upload_file_delivery.py b/api/controllers/files/upload_file_delivery.py index 044e2c1a8fa..e1424d383d7 100644 --- a/api/controllers/files/upload_file_delivery.py +++ b/api/controllers/files/upload_file_delivery.py @@ -2,7 +2,7 @@ import os from urllib.parse import quote from uuid import UUID -from flask import Response, request +from flask import Response from flask_restx import Resource from pydantic import BaseModel, Field from werkzeug.exceptions import NotFound @@ -10,6 +10,7 @@ from werkzeug.exceptions import NotFound from controllers.common.errors import UnsupportedFileTypeError from controllers.common.file_response import enforce_download_for_html from controllers.common.schema import query_params_from_model, register_schema_models +from controllers.console.wraps import model_validate from controllers.files import files_ns from extensions.ext_application_services import application_services from services.errors.file import UnsupportedFileTypeError as UnsupportedFileTypeServiceError @@ -74,13 +75,13 @@ class ImagePreviewApi(Resource): @files_ns.doc( responses={ 200: "Image preview returned successfully", - 400: "Missing or invalid query parameters", 404: "File not found or signature is invalid", 415: "Unsupported file type", + 422: "Missing or invalid query parameters", } ) - def get(self, file_id: UUID) -> Response: - args = FileSignatureQuery.model_validate(request.args.to_dict(flat=True)) + @model_validate(FileSignatureQuery) + def get(self, args: FileSignatureQuery, file_id: UUID) -> Response: try: delivery = application_services().upload_file_delivery.get_signed_image_preview( file_id=str(file_id), @@ -109,13 +110,12 @@ class FilePreviewApi(Resource): @files_ns.doc( responses={ 200: "File stream returned successfully", - 400: "Missing or invalid query parameters", 404: "File not found or signature is invalid", + 422: "Missing or invalid query parameters", } ) - def get(self, file_id: UUID) -> Response: - args = FilePreviewQuery.model_validate(request.args.to_dict(flat=True)) - + @model_validate(FilePreviewQuery) + def get(self, args: FilePreviewQuery, file_id: UUID) -> Response: try: delivery = application_services().upload_file_delivery.get_signed_file_preview( file_id=str(file_id), diff --git a/api/tests/unit_tests/controllers/console/auth/test_account_activation.py b/api/tests/unit_tests/controllers/console/auth/test_account_activation.py index 6f5ba50e56a..f3ca84a309f 100644 --- a/api/tests/unit_tests/controllers/console/auth/test_account_activation.py +++ b/api/tests/unit_tests/controllers/console/auth/test_account_activation.py @@ -6,6 +6,7 @@ from unittest.mock import Mock, patch import pytest from flask import Flask +from werkzeug.exceptions import UnprocessableEntity from controllers.console.auth.activate import ActivateApi, ActivateCheckApi from controllers.console.auth.error import InvitationAccountMismatchError as InvitationAccountMismatchHTTPError @@ -74,7 +75,7 @@ class TestActivateCheckApi: return_value=_services(activation_service), ), ): - response = unwrap(ActivateCheckApi.get)(ActivateCheckApi()) + response = ActivateCheckApi().get() assert response == { "is_valid": True, @@ -104,10 +105,24 @@ class TestActivateCheckApi: return_value=_services(activation_service), ), ): - response = unwrap(ActivateCheckApi.get)(ActivateCheckApi()) + response = ActivateCheckApi().get() assert response == {"is_valid": False} + def test_rejects_request_without_token(self, app: Flask, activation_service: Mock) -> None: + """`token` is required, so a tokenless query must not reach the application service.""" + with ( + app.test_request_context("/activate/check?workspace_id=workspace-123"), + patch( + "controllers.console.auth.activate.application_services", + return_value=_services(activation_service), + ), + pytest.raises(UnprocessableEntity), + ): + ActivateCheckApi().get() + + activation_service.check.assert_not_called() + class TestActivateApi: def test_passes_parsed_command_to_application_service( diff --git a/api/tests/unit_tests/controllers/files/test_upload_file_delivery.py b/api/tests/unit_tests/controllers/files/test_upload_file_delivery.py index b390c526ac4..fcb05f3f564 100644 --- a/api/tests/unit_tests/controllers/files/test_upload_file_delivery.py +++ b/api/tests/unit_tests/controllers/files/test_upload_file_delivery.py @@ -1,9 +1,9 @@ -import types from inspect import unwrap from unittest.mock import patch import pytest -from werkzeug.exceptions import NotFound +from flask import Flask +from werkzeug.exceptions import NotFound, UnprocessableEntity import controllers.files.upload_file_delivery as module from services.errors.file import UnsupportedFileTypeError as UnsupportedFileTypeServiceError @@ -34,18 +34,17 @@ def _delivery( ) -def _fake_request(args: dict[str, object]): - return types.SimpleNamespace(args=types.SimpleNamespace(to_dict=lambda **_kwargs: args)) +_SIGNED_QUERY = "/?timestamp=123&nonce=abc&sign=sig" class TestImagePreviewApi: @patch.object(module, "application_services") - def test_success(self, mock_application_services): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig"}) + def test_success(self, mock_application_services, app: Flask): service = mock_application_services.return_value.upload_file_delivery service.get_signed_image_preview.return_value = _delivery(mime_type="image/png", extension="png") - response = unwrap(module.ImagePreviewApi().get)("file-id") + with app.test_request_context(_SIGNED_QUERY): + response = module.ImagePreviewApi().get("file-id") assert response.mimetype == "image/png" service.get_signed_image_preview.assert_called_once_with( @@ -56,31 +55,39 @@ class TestImagePreviewApi: ) @patch.object(module, "application_services") - def test_not_found(self, mock_application_services): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig"}) + def test_not_found(self, mock_application_services, app: Flask): service = mock_application_services.return_value.upload_file_delivery service.get_signed_image_preview.side_effect = UploadFileDeliveryNotFoundError( "File not found or signature is invalid" ) - with pytest.raises(NotFound, match="File not found or signature is invalid"): - unwrap(module.ImagePreviewApi().get)("file-id") + with ( + app.test_request_context(_SIGNED_QUERY), + pytest.raises(NotFound, match="File not found or signature is invalid"), + ): + module.ImagePreviewApi().get("file-id") @patch.object(module, "application_services") - def test_unsupported_file_type(self, mock_application_services): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig"}) + def test_unsupported_file_type(self, mock_application_services, app: Flask): service = mock_application_services.return_value.upload_file_delivery service.get_signed_image_preview.side_effect = UnsupportedFileTypeServiceError() - with pytest.raises(module.UnsupportedFileTypeError): - unwrap(module.ImagePreviewApi().get)("file-id") + with app.test_request_context(_SIGNED_QUERY), pytest.raises(module.UnsupportedFileTypeError): + module.ImagePreviewApi().get("file-id") + + @patch.object(module, "application_services") + def test_missing_signature_params_are_rejected(self, mock_application_services, app: Flask): + """An incomplete signature must fail validation before the service is reached.""" + with app.test_request_context("/?timestamp=123"), pytest.raises(UnprocessableEntity): + module.ImagePreviewApi().get("file-id") + + mock_application_services.return_value.upload_file_delivery.get_signed_image_preview.assert_not_called() class TestFilePreviewApi: @patch.object(module, "enforce_download_for_html") @patch.object(module, "application_services") - def test_inline_preview_uses_file_metadata(self, mock_application_services, mock_enforce): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig", "as_attachment": False}) + def test_inline_preview_uses_file_metadata(self, mock_application_services, mock_enforce, app: Flask): service = mock_application_services.return_value.upload_file_delivery service.get_signed_file_preview.return_value = _delivery( mime_type="application/pdf", @@ -89,7 +96,8 @@ class TestFilePreviewApi: extension="pdf", ) - response = unwrap(module.FilePreviewApi().get)("file-id") + with app.test_request_context(_SIGNED_QUERY): + response = module.FilePreviewApi().get("file-id") assert response.mimetype == "application/pdf" assert response.headers["Content-Type"] == "application/pdf" @@ -103,25 +111,25 @@ class TestFilePreviewApi: ) @patch.object(module, "application_services") - def test_audio_preview_supports_ranges(self, mock_application_services): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig", "as_attachment": False}) + def test_audio_preview_supports_ranges(self, mock_application_services, app: Flask): mock_application_services.return_value.upload_file_delivery.get_signed_file_preview.return_value = _delivery( mime_type="audio/mpeg", extension="mp3", ) - response = unwrap(module.FilePreviewApi().get)("file-id") + with app.test_request_context(_SIGNED_QUERY): + response = module.FilePreviewApi().get("file-id") assert response.headers["Accept-Ranges"] == "bytes" @patch.object(module, "application_services") - def test_zero_size_omits_content_length(self, mock_application_services): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig", "as_attachment": False}) + def test_zero_size_omits_content_length(self, mock_application_services, app: Flask): mock_application_services.return_value.upload_file_delivery.get_signed_file_preview.return_value = _delivery( size=0 ) - response = unwrap(module.FilePreviewApi().get)("file-id") + with app.test_request_context(_SIGNED_QUERY): + response = module.FilePreviewApi().get("file-id") assert "Content-Length" not in response.headers @@ -135,8 +143,7 @@ class TestFilePreviewApi: ids=("mime-type", "filename", "extension"), ) @patch.object(module, "application_services") - def test_svg_preview_forces_download(self, mock_application_services, mime_type, name, extension): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig", "as_attachment": False}) + def test_svg_preview_forces_download(self, mock_application_services, mime_type, name, extension, app: Flask): mock_application_services.return_value.upload_file_delivery.get_signed_file_preview.return_value = _delivery( mime_type=mime_type, size=11, @@ -144,15 +151,15 @@ class TestFilePreviewApi: extension=extension, ) - response = unwrap(module.FilePreviewApi().get)("file-id") + with app.test_request_context(_SIGNED_QUERY): + response = module.FilePreviewApi().get("file-id") assert response.headers["Content-Disposition"].startswith("attachment") assert response.headers["Content-Type"] == "application/octet-stream" assert response.headers["X-Content-Type-Options"] == "nosniff" @patch.object(module, "application_services") - def test_html_preview_still_forces_download(self, mock_application_services): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig", "as_attachment": False}) + def test_html_preview_still_forces_download(self, mock_application_services, app: Flask): mock_application_services.return_value.upload_file_delivery.get_signed_file_preview.return_value = _delivery( mime_type="text/html", size=25, @@ -160,35 +167,46 @@ class TestFilePreviewApi: extension="html", ) - response = unwrap(module.FilePreviewApi().get)("file-id") + with app.test_request_context(_SIGNED_QUERY): + response = module.FilePreviewApi().get("file-id") assert response.headers["Content-Disposition"].startswith("attachment") assert response.headers["Content-Type"] == "application/octet-stream" assert response.headers["X-Content-Type-Options"] == "nosniff" @patch.object(module, "application_services") - def test_as_attachment_encodes_filename(self, mock_application_services): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig", "as_attachment": True}) + def test_as_attachment_encodes_filename(self, mock_application_services, app: Flask): mock_application_services.return_value.upload_file_delivery.get_signed_file_preview.return_value = _delivery( mime_type="application/pdf", name="报告.pdf", extension="pdf", ) - response = unwrap(module.FilePreviewApi().get)("file-id") + with app.test_request_context(f"{_SIGNED_QUERY}&as_attachment=true"): + response = module.FilePreviewApi().get("file-id") assert response.headers["Content-Disposition"] == "attachment; filename*=UTF-8''%E6%8A%A5%E5%91%8A.pdf" assert response.headers["Content-Type"] == "application/octet-stream" @patch.object(module, "application_services") - def test_not_found(self, mock_application_services): - module.request = _fake_request({"timestamp": "123", "nonce": "abc", "sign": "sig", "as_attachment": False}) + def test_not_found(self, mock_application_services, app: Flask): mock_application_services.return_value.upload_file_delivery.get_signed_file_preview.side_effect = ( UploadFileDeliveryNotFoundError("File not found or signature is invalid") ) - with pytest.raises(NotFound, match="File not found or signature is invalid"): - unwrap(module.FilePreviewApi().get)("file-id") + with ( + app.test_request_context(_SIGNED_QUERY), + pytest.raises(NotFound, match="File not found or signature is invalid"), + ): + module.FilePreviewApi().get("file-id") + + @patch.object(module, "application_services") + def test_missing_signature_params_are_rejected(self, mock_application_services, app: Flask): + """An incomplete signature must fail validation before the service is reached.""" + with app.test_request_context("/?nonce=abc"), pytest.raises(UnprocessableEntity): + module.FilePreviewApi().get("file-id") + + mock_application_services.return_value.upload_file_delivery.get_signed_file_preview.assert_not_called() class TestWorkspaceWebappLogoApi: From 66bf75d1d8c066d9f81b3306f58ee69a7cd62d82 Mon Sep 17 00:00:00 2001 From: "Syrinx." <1821287165@qq.com> Date: Thu, 17 Sep 2026 03:41:45 +0000 Subject: [PATCH 08/28] refactor(models): pass session into AppMCPServer.generate_server_code (#42316) --- api/controllers/console/app/mcp_server.py | 4 +- api/models/model.py | 4 +- .../console/app/test_mcp_server_response.py | 12 +++--- .../test_app_mcp_server_session_accessors.py | 39 +++++++++++++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 api/tests/unit_tests/models/test_app_mcp_server_session_accessors.py diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py index 8a6ab77b3dd..d6615124cb3 100644 --- a/api/controllers/console/app/mcp_server.py +++ b/api/controllers/console/app/mcp_server.py @@ -123,7 +123,7 @@ class AppMCPServerController(Resource): status=AppMCPServerStatus.ACTIVE, app_id=app_model.id, tenant_id=current_tenant_id, - server_code=AppMCPServer.generate_server_code(16), + server_code=AppMCPServer.generate_server_code(16, session=db.session()), ) db.session.add(server) db.session.commit() @@ -201,6 +201,6 @@ class AppMCPServerRefreshController(Resource): ) if not server: raise NotFound() - server.server_code = AppMCPServer.generate_server_code(16) + server.server_code = AppMCPServer.generate_server_code(16, session=db.session()) db.session.commit() return dump_response(AppMCPServerResponse, server) diff --git a/api/models/model.py b/api/models/model.py index 79f52eab5a1..2bb8dfc35bc 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -2167,11 +2167,11 @@ class AppMCPServer(TypeBase): ) @staticmethod - def generate_server_code(n: int) -> str: + def generate_server_code(n: int, *, session: Session) -> str: while True: result = generate_string(n) while ( - db.session.scalar(select(func.count(AppMCPServer.id)).where(AppMCPServer.server_code == result)) or 0 + session.scalar(select(func.count(AppMCPServer.id)).where(AppMCPServer.server_code == result)) or 0 ) > 0: result = generate_string(n) diff --git a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py index 5db0cbe2a4c..0f53d0de7d8 100644 --- a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py +++ b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from flask import Flask from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, scoped_session, sessionmaker from werkzeug.exceptions import NotFound from controllers.console import console_ns @@ -156,7 +156,7 @@ class TestAppMCPServerController: assert response == {} - def test_post_returns_201(self, sqlite_session: Session) -> None: + def test_post_returns_201(self, sqlite_session: Session, sqlite_session_factory: sessionmaker[Session]) -> None: api = AppMCPServerController() method = unwrap(api.post) payload = {"parameters": {"timeout": 30}} @@ -166,7 +166,7 @@ class TestAppMCPServerController: with ( app.test_request_context("/", json=payload), - patch("controllers.console.app.mcp_server.db.session", sqlite_session), + patch("controllers.console.app.mcp_server.db.session", scoped_session(sqlite_session_factory)), patch("controllers.console.app.mcp_server.AppMCPServer.generate_server_code", return_value="server-code"), ): response, status_code = method( @@ -257,7 +257,9 @@ class TestAppMCPServerController: class TestAppMCPServerRefreshController: - def test_post_refreshes_server_bound_to_app_and_tenant(self, sqlite_session: Session) -> None: + def test_post_refreshes_server_bound_to_app_and_tenant( + self, sqlite_session: Session, sqlite_session_factory: sessionmaker[Session] + ) -> None: api = AppMCPServerRefreshController() method = unwrap(api.post) server = _server(server_code="old-code") @@ -270,7 +272,7 @@ class TestAppMCPServerRefreshController: sqlite_session.commit() with ( - patch("controllers.console.app.mcp_server.db.session", sqlite_session), + patch("controllers.console.app.mcp_server.db.session", scoped_session(sqlite_session_factory)), patch("controllers.console.app.mcp_server.AppMCPServer.generate_server_code", return_value="new-code"), ): response = method(api, "tenant-1", app_model=_app()) diff --git a/api/tests/unit_tests/models/test_app_mcp_server_session_accessors.py b/api/tests/unit_tests/models/test_app_mcp_server_session_accessors.py new file mode 100644 index 00000000000..376dea85b86 --- /dev/null +++ b/api/tests/unit_tests/models/test_app_mcp_server_session_accessors.py @@ -0,0 +1,39 @@ +"""Regression coverage for the ``db.session``→session-parameter refactor on +``AppMCPServer.generate_server_code``. + +The static method reached for the global ``db.session`` internally and has been converted to +take an explicit ``session: Session`` (per the pattern established in #40370/#40797/#41394, +tracked in #40372). +""" + +from unittest.mock import patch +from uuid import uuid4 + +from sqlalchemy.orm import Session + +from models.enums import AppMCPServerStatus +from models.model import AppMCPServer + + +def _persist_server(session: Session, *, server_code: str) -> AppMCPServer: + server = AppMCPServer( + tenant_id=str(uuid4()), + app_id=str(uuid4()), + name="Test MCP Server", + description="Test server", + server_code=server_code, + status=AppMCPServerStatus.ACTIVE, + parameters="{}", + ) + session.add(server) + session.flush() + return server + + +def test_generate_server_code_retries_a_taken_code_through_the_given_session(sqlite_session: Session) -> None: + _persist_server(sqlite_session, server_code="taken-code") + + with patch("models.model.generate_string", side_effect=["taken-code", "fresh-code"]): + result = AppMCPServer.generate_server_code(16, session=sqlite_session) + + assert result == "fresh-code" From ff18b6de092178900ea4dcbeb0526147a1ec1a0e Mon Sep 17 00:00:00 2001 From: "Syrinx." <1821287165@qq.com> Date: Thu, 17 Sep 2026 03:42:31 +0000 Subject: [PATCH 09/28] refactor(console): dep-inject the snippet draft-variable query with @model_validate (#42315) --- .../console/snippets/snippet_workflow_draft_variable.py | 9 +++++---- .../snippets/test_snippet_workflow_draft_variable.py | 9 +++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/api/controllers/console/snippets/snippet_workflow_draft_variable.py b/api/controllers/console/snippets/snippet_workflow_draft_variable.py index ebf456edf5c..7091cd24c24 100644 --- a/api/controllers/console/snippets/snippet_workflow_draft_variable.py +++ b/api/controllers/console/snippets/snippet_workflow_draft_variable.py @@ -14,7 +14,7 @@ from collections.abc import Callable from functools import wraps from typing import Any, Concatenate -from flask import Response, request +from flask import Response from flask_restx import Resource from sqlalchemy.orm import Session, sessionmaker @@ -106,9 +106,10 @@ class SnippetWorkflowVariableCollectionApi(Resource): console_ns.models[WorkflowDraftVariableListWithoutValueResponse.__name__], ) @_snippet_draft_var_prerequisite - def get(self, current_user: Account, snippet: CustomizedSnippet) -> dict[str, Any]: - args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore - + @model_validate(WorkflowDraftVariableListQuery) + def get( + self, args: WorkflowDraftVariableListQuery, current_user: Account, snippet: CustomizedSnippet + ) -> dict[str, Any]: snippet_service = _snippet_service() if snippet_service.get_draft_workflow(snippet=snippet) is None: raise DraftWorkflowNotExist() diff --git a/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow_draft_variable.py b/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow_draft_variable.py index 3d7d8900c94..fb41206f65c 100644 --- a/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow_draft_variable.py +++ b/api/tests/unit_tests/controllers/console/snippets/test_snippet_workflow_draft_variable.py @@ -187,9 +187,14 @@ def test_variable_collection_get_raises_when_draft_workflow_missing( api = module.SnippetWorkflowVariableCollectionApi() handler = unwrap(api.get) - with app.test_request_context("/?page=1&limit=20"): + with app.test_request_context("/"): with pytest.raises(module.DraftWorkflowNotExist): - handler(api, _make_account(), snippet=SimpleNamespace(id="snippet-1")) + handler( + api, + module.WorkflowDraftVariableListQuery(page=1, limit=20), + _make_account(), + snippet=SimpleNamespace(id="snippet-1"), + ) def test_node_variable_collection_get_lists_persisted_node_variables( From 1203aa72c1086e11a74652c283161c821cbe8cf2 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 17 Sep 2026 03:45:32 +0000 Subject: [PATCH 10/28] perf(api): avoid duplicate Redis client scans in unit tests (#42379) --- api/tests/unit_tests/conftest.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/api/tests/unit_tests/conftest.py b/api/tests/unit_tests/conftest.py index 9ca5f9774e6..7b4dff1cf76 100644 --- a/api/tests/unit_tests/conftest.py +++ b/api/tests/unit_tests/conftest.py @@ -70,7 +70,7 @@ def _provide_app_context(app: Flask) -> Iterator[None]: @pytest.fixture(autouse=True) def _patch_redis_clients() -> Iterator[None]: - """Patch redis_client to MagicMock only for unit test executions.""" + """Patch and rebind loaded Redis clients to the shared mock for each unit test.""" with ( patch.object(ext_redis, "redis_client", redis_mock), @@ -81,8 +81,8 @@ def _patch_redis_clients() -> Iterator[None]: @pytest.fixture(autouse=True) -def reset_redis_mock() -> None: - """reset the Redis mock before each test""" +def reset_redis_mock(_patch_redis_clients: None) -> None: + """Reset the shared Redis mock after per-test client rebinding.""" redis_mock.reset_mock() redis_mock.get.return_value = None redis_mock.setex.return_value = None @@ -95,9 +95,6 @@ def reset_redis_mock() -> None: redis_mock.hdel.return_value = None redis_mock.incr.return_value = 1 - # Keep any imported modules pointing at the mock between tests - _patch_redis_clients_on_loaded_modules() - @pytest.fixture(autouse=True) def reset_secret_key(monkeypatch: pytest.MonkeyPatch) -> None: From 8006e8e13ded60ce8e751b76dc63a5c038dae595 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 17 Sep 2026 04:04:25 +0000 Subject: [PATCH 11/28] ci: reduce API unit tests to three shards (#42390) --- .github/workflows/api-tests.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 1e0d0b8f541..15c245ff4e9 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -31,9 +31,8 @@ jobs: - 1 - 2 - 3 - - 4 shardTotal: - - 4 + - 3 steps: - name: Checkout code @@ -200,8 +199,8 @@ jobs: mapfile -t unit_coverage < <(find coverage-data -type f -name 'coverage-unit-*' -print | sort) mapfile -t integration_coverage < <(find coverage-data -type f -name 'coverage-integration-*' -print | sort) - if [[ "${#unit_coverage[@]}" -ne 4 ]]; then - echo "expected 4 unit coverage artifacts, found ${#unit_coverage[@]}" >&2 + if [[ "${#unit_coverage[@]}" -ne 3 ]]; then + echo "expected 3 unit coverage artifacts, found ${#unit_coverage[@]}" >&2 exit 1 fi if [[ "${#integration_coverage[@]}" -ne 4 ]]; then From d528dea83e4e3eca9b136c16b39b658bfcb85735 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:55:31 +0000 Subject: [PATCH 12/28] fix(web): remove ineffective Tailwind classes (#42394) --- .../webapp-reset-password/check-code/page.tsx | 2 +- .../(shareLayout)/webapp-signin/check-code/page.tsx | 2 +- web/app/(shareLayout)/webapp-signin/normalForm.tsx | 8 ++++---- web/app/account/oauth/authorize/page.tsx | 2 +- .../components/app/configuration/config-var/index.tsx | 2 +- .../app/configuration/configuration-view/index.tsx | 2 +- .../deployment-configuration/credential-field.tsx | 2 +- .../base/chat/chat-with-history/header/index.tsx | 2 +- web/app/components/base/error-boundary/index.tsx | 4 ++-- .../base/form/components/base/base-field.tsx | 2 +- .../step-two/components/indexing-mode-section.tsx | 2 +- .../datasets/create/website/base/crawled-result.tsx | 2 +- .../datasets/create/website/base/text-input.tsx | 2 +- .../data-source-options/option-card.tsx | 2 +- .../online-drive/file-list/__tests__/index.spec.tsx | 11 ++++++++--- .../file-list/header/breadcrumbs/index.tsx | 4 ++-- .../data-source/online-drive/file-list/list/index.tsx | 2 +- .../create-from-pipeline/preview/file-preview.tsx | 2 +- .../preview/online-document-preview.tsx | 2 +- .../create-from-pipeline/preview/web-preview.tsx | 2 +- .../external-knowledge-api-card/index.tsx | 2 +- .../hit-testing/modify-external-retrieval-modal.tsx | 2 +- .../model-auth/credential-selector.tsx | 2 +- .../plugins/install-plugin/install-bundle/index.tsx | 2 +- .../install-plugin/install-from-github/index.tsx | 2 +- .../install-from-local-package/index.tsx | 2 +- .../install-plugin/install-from-marketplace/index.tsx | 2 +- .../preparation/data-source-options/option-card.tsx | 2 +- .../panel/test-run/result/result-preview/index.tsx | 1 - .../tools/edit-custom-collection-modal/test-api.tsx | 4 ++-- web/app/components/workflow/block-selector/tabs.tsx | 2 +- .../workflow/header/view-workflow-history.tsx | 2 +- .../components/delivery-method/mail-body-input.tsx | 2 +- .../components/mixed-variable-text-input/index.tsx | 1 - .../components/workflow/variable-inspect/right.tsx | 4 +--- web/app/signin/normal-form.tsx | 8 ++++---- web/app/signup/check-code/page.tsx | 2 +- 37 files changed, 51 insertions(+), 50 deletions(-) diff --git a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx index 1f4ead65353..0e378cd8e2e 100644 --- a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx @@ -107,7 +107,7 @@ export default function CheckCode() { onClick={() => router.back()} className="flex h-9 cursor-pointer appearance-none items-center justify-center text-text-tertiary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" > - + {t(($) => $.back, { ns: 'login' })} diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx index 7e09f107e72..f586ae17452 100644 --- a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -158,7 +158,7 @@ export default function CheckCode() { onClick={() => router.back()} className="flex h-9 cursor-pointer appearance-none items-center justify-center text-text-tertiary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" > - + {t(($) => $.back, { ns: 'login' })} diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx index 4232354bb4c..bfa8906f023 100644 --- a/web/app/(shareLayout)/webapp-signin/normalForm.tsx +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -64,7 +64,7 @@ const NormalForm = () => {
-
+
@@ -84,7 +84,7 @@ const NormalForm = () => {
-
+
@@ -104,7 +104,7 @@ const NormalForm = () => {
-
+
@@ -196,7 +196,7 @@ const NormalForm = () => { {allMethodsAreDisabled && ( <>
-
+

diff --git a/web/app/account/oauth/authorize/page.tsx b/web/app/account/oauth/authorize/page.tsx index 7853316aa0c..bbdf7f76188 100644 --- a/web/app/account/oauth/authorize/page.tsx +++ b/web/app/account/oauth/authorize/page.tsx @@ -198,7 +198,7 @@ export default function OAuthAuthorize() {

-
{userProfile.name}
+
{userProfile.name}
{userProfile.email}
diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 5edb0c22261..654a16c50e0 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -338,7 +338,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar {hasVar && (
= ({ <>
-
+
diff --git a/web/app/components/app/deploy/shared/deployment-configuration/credential-field.tsx b/web/app/components/app/deploy/shared/deployment-configuration/credential-field.tsx index c5bb3d37915..c8d89dce08c 100644 --- a/web/app/components/app/deploy/shared/deployment-configuration/credential-field.tsx +++ b/web/app/components/app/deploy/shared/deployment-configuration/credential-field.tsx @@ -88,7 +88,7 @@ export function CredentialField({
-
+

{t(($) => $['studio.noCredentialsYet'])}

diff --git a/web/app/components/base/chat/chat-with-history/header/index.tsx b/web/app/components/base/chat/chat-with-history/header/index.tsx index 6fdbc6f8ce0..de5123af87e 100644 --- a/web/app/components/base/chat/chat-with-history/header/index.tsx +++ b/web/app/components/base/chat/chat-with-history/header/index.tsx @@ -86,7 +86,7 @@ const Header = () => {
- +

{customTitle || copy.title}

diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index 7d847bfe4b4..231053e78db 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -401,7 +401,7 @@ const BaseField = ({ > = ({
- +
{t(($) => $['stepTwo.highQualityTip'], { ns: 'datasetCreation' })} diff --git a/web/app/components/datasets/create/website/base/crawled-result.tsx b/web/app/components/datasets/create/website/base/crawled-result.tsx index 8568d87409d..f9e1f53c274 100644 --- a/web/app/components/datasets/create/website/base/crawled-result.tsx +++ b/web/app/components/datasets/create/website/base/crawled-result.tsx @@ -76,7 +76,7 @@ const CrawledResult: FC = ({ ? t(($) => $[`${I18N_PREFIX}.resetAll`], { ns: 'datasetCreation' }) : t(($) => $[`${I18N_PREFIX}.selectAll`], { ns: 'datasetCreation' }) } - labelClassName="system-[13px] leading-[16px] font-medium text-text-secondary" + labelClassName="leading-[16px] font-medium text-text-secondary" />
{t(($) => $[`${I18N_PREFIX}.scrapTimeInfo`], { diff --git a/web/app/components/datasets/create/website/base/text-input.tsx b/web/app/components/datasets/create/website/base/text-input.tsx index d3ab1d3b09f..0f00ab8b850 100644 --- a/web/app/components/datasets/create/website/base/text-input.tsx +++ b/web/app/components/datasets/create/website/base/text-input.tsx @@ -46,7 +46,7 @@ const Input: FC = ({ value, onChange, placeholder = '', isNumber = false {...otherOption} value={value} onChange={handleChange} - className="focus:bg-components-inout-border-active flex h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal p-2 system-xs-regular text-components-input-text-filled caret-[#295eff] placeholder:text-components-input-text-placeholder hover:border hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border focus:border-components-input-border-active focus:shadow-xs focus:shadow-shadow-shadow-3 focus-visible:outline-hidden" + className="flex h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal p-2 system-xs-regular text-components-input-text-filled caret-[#295eff] placeholder:text-components-input-text-placeholder hover:border hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border focus:border-components-input-border-active focus:shadow-xs focus:shadow-shadow-shadow-3 focus-visible:outline-hidden" placeholder={placeholder} /> ) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source-options/option-card.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source-options/option-card.tsx index 09a6d733fe7..93403b8625f 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source-options/option-card.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source-options/option-card.tsx @@ -20,7 +20,7 @@ const OptionCard = ({ label, selected, nodeData, onClick }: OptionCardProps) => 'flex cursor-pointer items-center gap-2 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-3 shadow-shadow-shadow-3', selected ? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs inset-ring-[0.5px] inset-ring-components-option-card-option-selected-border' - : 'hover:bg-components-option-card-bg-hover hover:border-components-option-card-option-border-hover hover:shadow-xs', + : 'hover:border-components-option-card-option-border-hover hover:shadow-xs', )} onClick={onClick} > diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/__tests__/index.spec.tsx index ab7d83a450f..c10ec958f00 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/__tests__/index.spec.tsx @@ -211,10 +211,15 @@ describe('FileList', () => { const fileList = [createMockOnlineDriveFile()] const props = createDefaultProps({ isLoading: true, fileList }) - const { container } = render() + const { rerender } = render() - // Assert - Should show spinner icon at the bottom - expect(container.querySelector('.animation-spin')).toBeInTheDocument() + expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + expect(screen.getByRole('checkbox', { name: 'test-file.txt' })).toBeInTheDocument() + + rerender() + + expect(screen.queryByRole('status', { name: 'appApi.loading' })).not.toBeInTheDocument() + expect(screen.getByRole('checkbox', { name: 'test-file.txt' })).toBeInTheDocument() }) }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/index.tsx index c10cdafa2b0..051aff77cdb 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/index.tsx @@ -96,7 +96,7 @@ const Breadcrumbs = ({ return (
{showSearchResult && ( -
+
{t(($) => $['onlineDrive.breadcrumbs.searchResult'], { ns: 'datasetPipeline', searchResultsLength, @@ -105,7 +105,7 @@ const Breadcrumbs = ({
)} {!showSearchResult && showBucketListTitle && ( -
+
{t(($) => $['onlineDrive.breadcrumbs.allBuckets'], { ns: 'datasetPipeline' })}
)} diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/index.tsx index ed77e0a4fb5..993f9b3fd0f 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/index.tsx @@ -105,7 +105,7 @@ const List = ({ aria-live="polite" aria-label={t(($) => $.loading, { ns: 'appApi' })} > - +
)}
diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/file-preview.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/file-preview.tsx index a1b9a2edff4..21100e0d30c 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/file-preview.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/file-preview.tsx @@ -31,7 +31,7 @@ const FilePreview = ({ file, hidePreview }: FilePreviewProps) => {
{t(($) => $['addDocuments.stepOne.preview'], { ns: 'datasetPipeline' })}
-
{`${fileName}.${file.extension || ''}`}
+
{`${fileName}.${file.extension || ''}`}
{t(($) => $['addDocuments.stepOne.preview'], { ns: 'datasetPipeline' })}
-
{currentPage?.page_name}
+
{currentPage?.page_name}
{currentPage.type} diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/web-preview.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/web-preview.tsx index 447a9779141..623cd257405 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/web-preview.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/web-preview.tsx @@ -20,7 +20,7 @@ const WebsitePreview = ({ currentWebsite, hidePreview }: WebsitePreviewProps) =>
{t(($) => $['addDocuments.stepOne.preview'], { ns: 'datasetPipeline' })}
-
{currentWebsite.title}
+
{currentWebsite.title}
diff --git a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx index 56ed1f085f4..56aaa357eb0 100644 --- a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx +++ b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx @@ -121,7 +121,7 @@ const ExternalKnowledgeAPICard: React.FC = ({ return ( <>
diff --git a/web/app/components/datasets/hit-testing/modify-external-retrieval-modal.tsx b/web/app/components/datasets/hit-testing/modify-external-retrieval-modal.tsx index 1b353ab1527..bf5793699ce 100644 --- a/web/app/components/datasets/hit-testing/modify-external-retrieval-modal.tsx +++ b/web/app/components/datasets/hit-testing/modify-external-retrieval-modal.tsx @@ -50,7 +50,7 @@ const ModifyExternalRetrievalModal: React.FC } return ( -
+
{t(($) => $.settingTitle, { ns: 'datasetHitTesting' })} diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx index d095af2710f..0cd7ac09d5a 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx @@ -68,7 +68,7 @@ const CredentialSelector = ({
{credentials.map((credential) => ( diff --git a/web/app/components/plugins/install-plugin/install-bundle/index.tsx b/web/app/components/plugins/install-plugin/install-bundle/index.tsx index 3df9f9f90ef..129205b6009 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/index.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/index.tsx @@ -71,7 +71,7 @@ const InstallBundle: FC = ({ 'w-full max-w-120 overflow-hidden! text-left align-middle', cn( modalClassName, - 'shadows-shadow-xl flex max-h-[calc(100dvh-48px)] min-w-140 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0', + 'flex max-h-[calc(100dvh-48px)] min-w-140 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0', ), )} > diff --git a/web/app/components/plugins/install-plugin/install-from-github/index.tsx b/web/app/components/plugins/install-plugin/install-from-github/index.tsx index 3be2c3c797d..c599227ff1b 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/index.tsx @@ -185,7 +185,7 @@ const InstallFromGitHub: React.FC = ({ 'w-140 overflow-hidden! text-left align-middle', cn( modalClassName, - 'shadows-shadow-xl flex max-h-[calc(100dvh-48px)] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0', + 'flex max-h-[calc(100dvh-48px)] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0', ), )} > diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx index 825484b0d4b..4c48e75c7c0 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx @@ -93,7 +93,7 @@ const InstallFromLocalPackage: React.FC = ({ 'w-140 overflow-hidden! text-left align-middle', cn( modalClassName, - 'shadows-shadow-xl flex max-h-[calc(100dvh-48px)] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0', + 'flex max-h-[calc(100dvh-48px)] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0', ), )} > diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx index 4617eb682c0..0d9ecf84e7b 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx @@ -89,7 +89,7 @@ const InstallFromMarketplace: React.FC = ({ 'w-140 overflow-hidden! text-left align-middle', cn( modalClassName, - 'shadows-shadow-xl flex max-h-[calc(100dvh-48px)] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0', + 'flex max-h-[calc(100dvh-48px)] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0', ), )} > diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/option-card.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/option-card.tsx index 48aecf54a94..c8f948fff02 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/option-card.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/option-card.tsx @@ -27,7 +27,7 @@ const OptionCard = ({ label, value, selected, nodeData, onClick }: OptionCardPro 'flex cursor-pointer flex-col gap-1 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-2.5 shadow-shadow-shadow-3', selected ? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs inset-ring-[0.5px] inset-ring-components-option-card-option-selected-border' - : 'hover:bg-components-option-card-bg-hover hover:border-components-option-card-option-border-hover hover:shadow-xs', + : 'hover:border-components-option-card-option-border-hover hover:shadow-xs', )} onClick={handleClickCard} > diff --git a/web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx index 12cd59f647c..362190468f7 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx @@ -47,7 +47,6 @@ const ResultPreview = ({ isRunning, outputs, error, onSwitchToDetail }: ResultTe
$['result.resultPreview.footerTip'], { ns: 'pipeline', count: RAG_PIPELINE_PREVIEW_CHUNK_NUM, diff --git a/web/app/components/tools/edit-custom-collection-modal/test-api.tsx b/web/app/components/tools/edit-custom-collection-modal/test-api.tsx index 2759df58f85..f5a20945aea 100644 --- a/web/app/components/tools/edit-custom-collection-modal/test-api.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/test-api.tsx @@ -156,7 +156,7 @@ const TestApi: FC = ({ positionCenter, customCollection, tool, onHide }) }) } type="text" - className="!hover:border-transparent !hover:bg-transparent !focus:border-transparent !focus:bg-transparent border-transparent! bg-transparent!" + className="border-transparent! bg-transparent!" /> @@ -179,7 +179,7 @@ const TestApi: FC = ({ positionCenter, customCollection, tool, onHide })
{t(($) => $['test.testResult'], { ns: 'tools' })}
-
+
{result || ( diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 885b2b24782..31d660f12fc 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -273,7 +273,7 @@ function BlockSelectorPanels({ ))} {
-
+
{t(($) => $['changeHistory.title'], { ns: 'workflow' })}
- - {t(($) => $['debug.variableInspect.edited'], { ns: 'workflow' })} - + {t(($) => $['debug.variableInspect.edited'], { ns: 'workflow' })} )} {!isTruncated && diff --git a/web/app/signin/normal-form.tsx b/web/app/signin/normal-form.tsx index 52848170cd8..7db9b968331 100644 --- a/web/app/signin/normal-form.tsx +++ b/web/app/signin/normal-form.tsx @@ -125,7 +125,7 @@ function NormalForm() {
-
+
@@ -145,7 +145,7 @@ function NormalForm() {
-
+
@@ -165,7 +165,7 @@ function NormalForm() {
-
+
@@ -287,7 +287,7 @@ function NormalForm() { {allMethodsAreDisabled && ( <>
-
+

diff --git a/web/app/signup/check-code/page.tsx b/web/app/signup/check-code/page.tsx index 8ab5c895e77..cafddd4495b 100644 --- a/web/app/signup/check-code/page.tsx +++ b/web/app/signup/check-code/page.tsx @@ -122,7 +122,7 @@ export default function CheckCode() { onClick={() => router.back()} className="flex h-9 cursor-pointer appearance-none items-center justify-center text-text-tertiary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" > - + {t(($) => $.back, { ns: 'login' })} From a14ebae6a4a4b65fb5861392e03adf2749085244 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:20:07 +0000 Subject: [PATCH 13/28] refactor(web): use Zod validate for boolean form checks (#42398) --- web/app/forgot-password/ForgotPasswordForm.tsx | 2 +- web/app/install/installForm.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/app/forgot-password/ForgotPasswordForm.tsx b/web/app/forgot-password/ForgotPasswordForm.tsx index b47ce212c47..21b128af43e 100644 --- a/web/app/forgot-password/ForgotPasswordForm.tsx +++ b/web/app/forgot-password/ForgotPasswordForm.tsx @@ -93,7 +93,7 @@ const ForgotPasswordForm = () => { - emailSchema.safeParse(value).success + emailSchema.validate(value) ? null : t(($) => $['error.emailInValid'], { ns: 'login' }) } diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index 7066080c042..9c87efa4374 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -110,7 +110,7 @@ const InstallForm = () => { - accountFormSchema.shape.email.safeParse(value).success + accountFormSchema.shape.email.validate(value) ? null : t(($) => $['error.emailInValid'], { ns: 'login' }) } @@ -130,7 +130,7 @@ const InstallForm = () => { - accountFormSchema.shape.name.safeParse(value).success + accountFormSchema.shape.name.validate(value) ? null : t(($) => $['error.nameEmpty'], { ns: 'login' }) } @@ -149,7 +149,7 @@ const InstallForm = () => { - accountFormSchema.shape.password.safeParse(value).success + accountFormSchema.shape.password.validate(value) ? null : t(($) => $['error.passwordInvalid'], { ns: 'login' }) } From 64deb9c4213652375dce8680ca754c29e4a9d989 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:39:40 +0000 Subject: [PATCH 14/28] docs: update README agent feature description (#42399) --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index c3f68564c85..9f74c251e8b 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,7 @@ Intuitive interface for crafting prompts, comparing model performance, and addin **4. RAG Pipeline**: Extensive RAG capabilities that cover everything from document ingestion to retrieval, with out-of-box support for text extraction from PDFs, PPTs, and other common document formats. -**5. Agent capabilities**: -You can define agents based on LLM Function Calling or ReAct, and add pre-built or custom tools for the agent. Dify provides 50+ built-in tools for AI agents, such as Google Search, DALL·E, Stable Diffusion and WolframAlpha. +**5. Agent**: Autonomous agents with a sandbox of their own: they run commands, install software, and handle files to get open-ended tasks done. Describe the agent you want and it builds itself. Give it skills, connect tools from [Dify Marketplace](https://marketplace.dify.ai/?category=tool), MCP servers, or your own APIs, and put it to work as a chat app or as a step in any workflow. **6. LLMOps**: Monitor and analyze application logs and performance over time. You could continuously improve prompts, datasets, and models based on production data and annotations. From 28e2682986de27ea7c71eafac3ce086009733d6b Mon Sep 17 00:00:00 2001 From: JessYan <15711968+JessYanCoding@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:43:18 +0000 Subject: [PATCH 15/28] refactor(models): pass session into App.app_model_config and App.workflow (#42190) Co-authored-by: Claude Opus 5 (1M context) --- api/controllers/mcp/mcp.py | 14 ++- api/core/plugin/backwards_invocation/app.py | 5 +- api/models/model.py | 8 -- api/services/agent/dsl_service.py | 2 +- api/services/app_service.py | 7 +- .../tools/workflow_tools_manage_service.py | 20 ++-- .../services/test_agent_service.py | 5 +- .../test_workflow_tools_manage_service.py | 9 ++ .../workflow/test_workflow_converter.py | 2 +- .../console/app/test_model_config_api.py | 19 ++-- .../unit_tests/models/test_app_models.py | 105 ++++++++++++++++++ .../unit_tests/services/test_app_service.py | 27 +++++ 12 files changed, 183 insertions(+), 40 deletions(-) diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index 95aa2be8218..fb4912b0741 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -88,7 +88,7 @@ class MCPAppApi(Resource): self._validate_server_status(mcp_server) # Get user input form - user_input_form = self._get_user_input_form(app) + user_input_form = self._get_user_input_form(app, session) # Handle notification vs request differently return self._process_mcp_message( @@ -180,17 +180,19 @@ class MCPAppApi(Resource): return helper.compact_generate_response(result.model_dump(by_alias=True, mode="json", exclude_none=True)) - def _get_user_input_form(self, app: App) -> list[VariableEntity]: + def _get_user_input_form(self, app: App, session: Session) -> list[VariableEntity]: """Get and convert user input form""" # Get raw user input form based on app mode if app.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}: - if not app.workflow: + workflow = app.workflow_with_session(session=session) + if not workflow: raise MCPRequestError(mcp_types.INVALID_REQUEST, "App is unavailable") - raw_user_input_form = app.workflow.user_input_form(to_old_structure=True) + raw_user_input_form = workflow.user_input_form(to_old_structure=True) else: - if not app.app_model_config: + app_model_config = app.app_model_config_with_session(session=session) + if not app_model_config: raise MCPRequestError(mcp_types.INVALID_REQUEST, "App is unavailable") - features_dict = app.app_model_config.to_dict() + features_dict = app_model_config.to_dict() raw_user_input_form = features_dict.get("user_input_form", []) # Convert to VariableEntity objects diff --git a/api/core/plugin/backwards_invocation/app.py b/api/core/plugin/backwards_invocation/app.py index 2972e2be207..4165b585d1c 100644 --- a/api/core/plugin/backwards_invocation/app.py +++ b/api/core/plugin/backwards_invocation/app.py @@ -281,7 +281,10 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation): @classmethod def _get_workflow(cls, app: App) -> Workflow | None: """ - get workflow without relying on App.workflow's request-scoped session property + get workflow with an owned session, additionally scoped by tenant and app + + `App.workflow_with_session` resolves by `workflow_id` alone; a plugin + invocation must also confirm the workflow belongs to this tenant and app. """ if not app.workflow_id: return None diff --git a/api/models/model.py b/api/models/model.py index 2bb8dfc35bc..7b94b4e38c0 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -466,20 +466,12 @@ class App(Base): def site_with_session(self, *, session: Session) -> Site | None: return session.scalar(select(Site).where(Site.app_id == self.id)) - @property - def app_model_config(self) -> AppModelConfig | None: - return self.app_model_config_with_session(session=db.session()) - def app_model_config_with_session(self, *, session: Session) -> AppModelConfig | None: if self.app_model_config_id: return session.scalar(select(AppModelConfig).where(AppModelConfig.id == self.app_model_config_id)) return None - @property - def workflow(self) -> Workflow | None: - return self.workflow_with_session(session=db.session()) - def workflow_with_session(self, *, session: Session) -> Workflow | None: if self.workflow_id: from .workflow import Workflow diff --git a/api/services/agent/dsl_service.py b/api/services/agent/dsl_service.py index 3c13ee0112b..454c887f901 100644 --- a/api/services/agent/dsl_service.py +++ b/api/services/agent/dsl_service.py @@ -203,7 +203,7 @@ class AgentDslService: package=package, package_path="agent", ) - if app.app_model_config is None: + if app.app_model_config_with_session(session=self.session) is None: model_config = AppModelConfig(app_id=app.id, created_by=account.id, updated_by=account.id) self.session.add(model_config) self.session.flush() diff --git a/api/services/app_service.py b/api/services/app_service.py index e90e962cf2b..4fa63c76f76 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -809,9 +809,12 @@ class AppService: def __init__(self, app): self.__dict__.update(app.__dict__) - @property @override - def app_model_config(self): + def app_model_config_with_session(self, *, session: Session) -> AppModelConfig | None: + # Hand back the in-memory config the masking pass above produced, and + # deliberately ignore `session`: re-reading the row here would undo the + # masking. Response paths resolve the config through this accessor + # (`AppResponseView.app_model_config`), so the override has to sit here. return model_config app = ModifiedApp(app) diff --git a/api/services/tools/workflow_tools_manage_service.py b/api/services/tools/workflow_tools_manage_service.py index b094d4231d1..99dda57dae8 100644 --- a/api/services/tools/workflow_tools_manage_service.py +++ b/api/services/tools/workflow_tools_manage_service.py @@ -61,18 +61,18 @@ class WorkflowToolManageService: if existing_workflow_tool_provider is not None: raise ValueError(f"Tool with name {name} or app_id {workflow_app_id} already exists") - # query the app + # query the app and its published workflow in the same session app: App | None = None + workflow: Workflow | None = None with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: app = _session.scalar(select(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).limit(1)) + if app is not None: + workflow = app.workflow_with_session(session=_session) # if not found raise error if app is None: raise ValueError(f"App {workflow_app_id} not found") - # query the workflow - workflow: Workflow | None = app.workflow - # if not found raise error if workflow is None: raise ValueError(f"Workflow not found for app {workflow_app_id}") @@ -172,20 +172,20 @@ class WorkflowToolManageService: if workflow_tool_provider is None: raise ValueError(f"Tool {workflow_tool_id} not found") - # query the app + # query the app and its published workflow in the same session app: App | None = None + workflow: Workflow | None = None with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: app = _session.scalar( select(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).limit(1) ) + if app is not None: + workflow = app.workflow_with_session(session=_session) # if not found raise error if app is None: raise ValueError(f"App {workflow_tool_provider.app_id} not found") - # query the workflow - workflow: Workflow | None = app.workflow - # if not found raise error if workflow is None: raise ValueError(f"Workflow not found for app {workflow_tool_provider.app_id}") @@ -342,15 +342,17 @@ class WorkflowToolManageService: raise ValueError("Tool not found") workflow_app: App | None = None + workflow: Workflow | None = None with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: workflow_app = _session.scalar( select(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).limit(1) ) + if workflow_app is not None: + workflow = workflow_app.workflow_with_session(session=_session) if workflow_app is None: raise ValueError(f"App {db_tool.app_id} not found") - workflow = workflow_app.workflow if not workflow: raise ValueError("Workflow not found") diff --git a/api/tests/test_containers_integration_tests/services/test_agent_service.py b/api/tests/test_containers_integration_tests/services/test_agent_service.py index d6e0ff103c1..4217e573b5a 100644 --- a/api/tests/test_containers_integration_tests/services/test_agent_service.py +++ b/api/tests/test_containers_integration_tests/services/test_agent_service.py @@ -133,8 +133,9 @@ class TestAgentService: app = app_service.create_app(tenant.id, app_args, account, session=db_session_with_containers) # Update the app model config to set agent_mode for agent-chat mode - if app.mode == AppMode.AGENT_CHAT and app.app_model_config: - app.app_model_config.agent_mode = json.dumps({"enabled": True, "strategy": "react", "tools": []}) + app_model_config = app.app_model_config_with_session(session=db_session_with_containers) + if app.mode == AppMode.AGENT_CHAT and app_model_config: + app_model_config.agent_mode = json.dumps({"enabled": True, "strategy": "react", "tools": []}) db_session_with_containers.commit() diff --git a/api/tests/test_containers_integration_tests/services/tools/test_workflow_tools_manage_service.py b/api/tests/test_containers_integration_tests/services/tools/test_workflow_tools_manage_service.py index c2c5fe838fb..f05a0fb28da 100644 --- a/api/tests/test_containers_integration_tests/services/tools/test_workflow_tools_manage_service.py +++ b/api/tests/test_containers_integration_tests/services/tools/test_workflow_tools_manage_service.py @@ -537,6 +537,9 @@ class TestWorkflowToolManageService: ] } ) + # The service reads the published workflow in its own session, so the graph has + # to be committed — matching test_update_workflow_tool_human_input_node_error. + db_session_with_containers.commit() tool_parameters = self._create_test_workflow_tool_parameters() with pytest.raises(WorkflowToolHumanInputNotSupportedError) as exc_info: @@ -877,6 +880,9 @@ class TestWorkflowToolManageService: ] } workflow.graph = json.dumps(workflow_graph) + # The service reads the published workflow in its own session, so the graph has + # to be committed for this setup to reach it. + db_session_with_containers.commit() # Setup workflow tool parameters with FILE type file_parameters = [ @@ -952,6 +958,9 @@ class TestWorkflowToolManageService: ] } workflow.graph = json.dumps(workflow_graph) + # The service reads the published workflow in its own session, so the graph has + # to be committed for this setup to reach it. + db_session_with_containers.commit() # Setup workflow tool parameters with FILES type files_parameters = [ diff --git a/api/tests/test_containers_integration_tests/services/workflow/test_workflow_converter.py b/api/tests/test_containers_integration_tests/services/workflow/test_workflow_converter.py index 8cd9526f6a0..fa102683cdf 100644 --- a/api/tests/test_containers_integration_tests/services/workflow/test_workflow_converter.py +++ b/api/tests/test_containers_integration_tests/services/workflow/test_workflow_converter.py @@ -325,7 +325,7 @@ class TestWorkflowConverter: workflow_converter = WorkflowConverter() workflow = workflow_converter.convert_app_model_config_to_workflow( app_model=app, - app_model_config=app.app_model_config, + app_model_config=app.app_model_config_with_session(session=db_session_with_containers), account_id=account.id, session=db_session_with_containers, ) diff --git a/api/tests/unit_tests/controllers/console/app/test_model_config_api.py b/api/tests/unit_tests/controllers/console/app/test_model_config_api.py index 43d4925e165..d9d99221dff 100644 --- a/api/tests/unit_tests/controllers/console/app/test_model_config_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_model_config_api.py @@ -4,7 +4,6 @@ import importlib import json import os from inspect import unwrap -from typing import Never from unittest.mock import MagicMock from uuid import uuid4 @@ -21,13 +20,13 @@ from models.model import App, AppMode, AppModelConfig app_wraps_module = importlib.import_module("controllers.console.app.wraps") -def _poison_implicit_app_config_properties(monkeypatch: pytest.MonkeyPatch) -> None: - def fail(_app: App) -> Never: - raise AssertionError("implicit App model-config property was accessed") - - # `App.is_agent` used to be poisoned here too. It no longer exists as an - # implicit property, so its absence enforces the same thing outright. - monkeypatch.setattr(App, "app_model_config", property(fail)) +def _assert_no_implicit_app_config_properties() -> None: + # `App.is_agent` and `App.app_model_config` used to be poisoned here with a + # raising property. Neither exists as an implicit accessor any more, so their + # absence enforces the same thing outright — assert it instead of + # monkeypatching an attribute that is gone. + assert not hasattr(App, "is_agent") + assert not hasattr(App, "app_model_config") @pytest.mark.parametrize("app_mode", [AppMode.CHAT, AppMode.COMPLETION]) @@ -54,7 +53,7 @@ def test_post_updates_non_agent_model_config_without_implicit_properties( original_config.agent_mode = None sqlite_session.add(original_config) sqlite_session.commit() - _poison_implicit_app_config_properties(monkeypatch) + _assert_no_implicit_app_config_properties() monkeypatch.setattr( model_config_module.AppModelConfigService, "validate_configuration", @@ -184,7 +183,7 @@ def test_post_encrypts_agent_tool_parameters( updated_by=None, updated_at=None, ) - _poison_implicit_app_config_properties(monkeypatch) + _assert_no_implicit_app_config_properties() original_config = AppModelConfig(app_id="app-1", created_by="u1", updated_by="u1") original_config.id = "config-0" diff --git a/api/tests/unit_tests/models/test_app_models.py b/api/tests/unit_tests/models/test_app_models.py index 7d0a42ebcb8..3ae1824a2e9 100644 --- a/api/tests/unit_tests/models/test_app_models.py +++ b/api/tests/unit_tests/models/test_app_models.py @@ -33,6 +33,7 @@ from models.model import ( Site, load_annotation_reply_config, ) +from models.workflow import Workflow, WorkflowType class TestAppModelValidation: @@ -268,6 +269,110 @@ class TestAppModelValidation: # Assert assert result == AppMode.AGENT_CHAT + @pytest.mark.parametrize("sqlite_session", [(App, AppModelConfig)], indirect=True) + def test_app_model_config_with_session_reads_through_the_caller_session(self, sqlite_session: Session): + """`app_model_config_with_session` resolves the linked config through the caller's session.""" + # Arrange + app = App( + tenant_id=str(uuid4()), + name="Test App", + mode=AppMode.CHAT, + enable_site=True, + enable_api=False, + created_by=str(uuid4()), + ) + sqlite_session.add(app) + sqlite_session.flush() + model_config = AppModelConfig(app_id=app.id, agent_mode=json.dumps({"enabled": False})) + sqlite_session.add(model_config) + sqlite_session.flush() + app.app_model_config_id = model_config.id + sqlite_session.commit() + + # Act: a session the model never owns, proving the lookup does not reach for a global one + with Session(sqlite_session.get_bind(), expire_on_commit=False) as caller_session: + result = app.app_model_config_with_session(session=caller_session) + + # Assert + assert result is not None + assert result.id == model_config.id + assert result in caller_session + + @pytest.mark.parametrize("sqlite_session", [(App, AppModelConfig)], indirect=True) + def test_app_model_config_with_session_returns_none_when_unlinked(self, sqlite_session: Session): + """An app without `app_model_config_id` resolves to None without querying.""" + # Arrange + app = App( + tenant_id=str(uuid4()), + name="Test App", + mode=AppMode.CHAT, + enable_site=True, + enable_api=False, + created_by=str(uuid4()), + ) + sqlite_session.add(app) + sqlite_session.flush() + + # Act / Assert: an unbound session would raise if the guard were dropped + with Session(expire_on_commit=False) as unbound_session: + assert app.app_model_config_with_session(session=unbound_session) is None + + @pytest.mark.parametrize("sqlite_session", [(App, AppModelConfig)], indirect=True) + def test_workflow_with_session_reads_through_the_caller_session(self, sqlite_session: Session): + """`workflow_with_session` resolves the published workflow through the caller's session.""" + # Arrange + app = App( + tenant_id=str(uuid4()), + name="Test App", + mode=AppMode.WORKFLOW, + enable_site=True, + enable_api=False, + created_by=str(uuid4()), + ) + sqlite_session.add(app) + sqlite_session.flush() + workflow = Workflow( + tenant_id=app.tenant_id, + app_id=app.id, + type=WorkflowType.WORKFLOW, + version="1", + graph=json.dumps({"nodes": [], "edges": []}), + created_by=app.created_by, + ) + workflow._features = "{}" + sqlite_session.add(workflow) + sqlite_session.flush() + app.workflow_id = workflow.id + sqlite_session.commit() + + # Act: a session the model never owns, proving the lookup does not reach for a global one + with Session(sqlite_session.get_bind(), expire_on_commit=False) as caller_session: + result = app.workflow_with_session(session=caller_session) + + # Assert + assert result is not None + assert result.id == workflow.id + assert result in caller_session + + @pytest.mark.parametrize("sqlite_session", [(App, AppModelConfig)], indirect=True) + def test_workflow_with_session_returns_none_when_unpublished(self, sqlite_session: Session): + """An app without `workflow_id` resolves to None without querying.""" + # Arrange + app = App( + tenant_id=str(uuid4()), + name="Test App", + mode=AppMode.WORKFLOW, + enable_site=True, + enable_api=False, + created_by=str(uuid4()), + ) + sqlite_session.add(app) + sqlite_session.flush() + + # Act / Assert: an unbound session would raise if the guard were dropped + with Session(expire_on_commit=False) as unbound_session: + assert app.workflow_with_session(session=unbound_session) is None + @pytest.mark.parametrize("sqlite_session", [(App, AppModelConfig)], indirect=True) def test_deleted_tools_checks_plugin_builtin_providers_through_core_plugin_service(self, sqlite_session: Session): """Plugin-backed built-in tools are checked through core PluginService.""" diff --git a/api/tests/unit_tests/services/test_app_service.py b/api/tests/unit_tests/services/test_app_service.py index 1e6f79261f3..d568fb6742c 100644 --- a/api/tests/unit_tests/services/test_app_service.py +++ b/api/tests/unit_tests/services/test_app_service.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from collections.abc import Callable from datetime import datetime from types import SimpleNamespace @@ -544,6 +545,32 @@ class TestGetApp: is_agent.assert_not_called() get_model_config.assert_called_once_with(session=unbound_session) + def test_masked_agent_config_is_served_through_the_session_accessor(self, unbound_session: Session): + """The masked config must be reachable via `app_model_config_with_session`. + + Every response path resolves an `App`'s model config through that accessor + (`AppResponseView.app_model_config`), never through a raw attribute, so the + masking `get_app` applies has to be observable there. + """ + app = App(mode=AppMode.AGENT_CHAT) + app.id = str(uuid4()) + masked_config = AppModelConfig(app_id=app.id, agent_mode=json.dumps({"enabled": True, "tools": []})) + # A second, unmasked instance: what a fresh lookup would hand back if the + # returned app ever fell through to the base accessor. + refetched_config = AppModelConfig(app_id=app.id, agent_mode=json.dumps({"enabled": True, "tools": []})) + account = Account(name="Test Account", email="test@example.com") + account._current_tenant = Tenant(name="Test Tenant") + account._current_tenant.id = "tenant-1" + + with ( + patch.object(App, "app_model_config_with_session", side_effect=[masked_config, refetched_config]), + patch("services.app_service.current_user", account), + ): + result = AppService().get_app(app, session=unbound_session) + + assert result is not app + assert result.app_model_config_with_session(session=unbound_session) is masked_config + class TestAgentAppType: """S1: new ``AppMode.AGENT`` app type wiring.""" From 153cc72f17528a823cdc52fea74039ecfe459938 Mon Sep 17 00:00:00 2001 From: Mesrop Harutyunyan <104313591+mesrop2008@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:53:04 +0000 Subject: [PATCH 16/28] fix(web): correct Slovenian display language name (#42361) --- web/i18n-config/languages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n-config/languages.ts b/web/i18n-config/languages.ts index 5eebd8777b1..77702afccdc 100644 --- a/web/i18n-config/languages.ts +++ b/web/i18n-config/languages.ts @@ -142,7 +142,7 @@ const data = { }, { value: 'sl-SI', - name: 'Slovensko (Slovenija)', + name: 'Slovenščina (Slovenija)', prompt_name: 'Slovensko', example: 'Zdravo, Dify!', supported: true, From 0825e6ca515e23cab260edea5239b255ab604b87 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Thu, 17 Sep 2026 05:54:38 +0000 Subject: [PATCH 17/28] fix: fix some skill issues (#42389) --- api/services/skill_management_service.py | 48 ++++++------ .../services/test_skill_management_service.py | 74 +++++++++++++++++++ web/features/skills/detail/skill-metadata.tsx | 7 +- web/service/console/client.spec.ts | 66 +++++++++++++++++ web/service/console/query-policies.ts | 14 ++++ 5 files changed, 184 insertions(+), 25 deletions(-) diff --git a/api/services/skill_management_service.py b/api/services/skill_management_service.py index cb2df162927..b0731b82f3f 100644 --- a/api/services/skill_management_service.py +++ b/api/services/skill_management_service.py @@ -60,6 +60,7 @@ from models.account import Account from models.agent import ( Agent, AgentConfigDraft, + AgentConfigDraftType, AgentConfigSnapshot, AgentKind, AgentScope, @@ -2638,36 +2639,35 @@ class SkillManagementService: ) ) ) - configured_names: set[str] = set() - snapshot = session.scalar( - select(AgentConfigSnapshot).where( - AgentConfigSnapshot.tenant_id == tenant_id, - AgentConfigSnapshot.agent_id == agent_id, - AgentConfigSnapshot.id - == select(Agent.active_config_snapshot_id) - .where(Agent.tenant_id == tenant_id, Agent.id == agent_id) - .scalar_subquery(), - ) - ) - if snapshot is not None: - configured_names.update( - skill.name - for skill in AgentSoulConfig.model_validate(snapshot.config_snapshot_dict).config_skills - if not skill.is_missing - ) - drafts = session.scalars( + # Bindings are edited against the normal draft, including unpublished removals. + config: AgentConfigDraft | AgentConfigSnapshot | None = session.scalar( select(AgentConfigDraft).where( AgentConfigDraft.tenant_id == tenant_id, AgentConfigDraft.agent_id == agent_id, + AgentConfigDraft.draft_type == AgentConfigDraftType.DRAFT, + AgentConfigDraft.account_id.is_(None), ) ) - for draft in drafts: - configured_names.update( - skill.name - for skill in AgentSoulConfig.model_validate(draft.config_snapshot_dict).config_skills - if not skill.is_missing + if config is None: + config = session.scalar( + select(AgentConfigSnapshot).where( + AgentConfigSnapshot.tenant_id == tenant_id, + AgentConfigSnapshot.agent_id == agent_id, + AgentConfigSnapshot.id + == select(Agent.active_config_snapshot_id) + .where(Agent.tenant_id == tenant_id, Agent.id == agent_id) + .scalar_subquery(), + ) ) - + configured_names = ( + { + skill.name + for skill in AgentSoulConfig.model_validate(config.config_snapshot_dict).config_skills + if not skill.is_missing + } + if config is not None + else set() + ) conflicts = sorted(set(selected_skill_names) & (configured_names - current_bound_names)) if conflicts: raise SkillManagementServiceError( diff --git a/api/tests/unit_tests/services/test_skill_management_service.py b/api/tests/unit_tests/services/test_skill_management_service.py index 8bdb7503dcc..951c7147a36 100644 --- a/api/tests/unit_tests/services/test_skill_management_service.py +++ b/api/tests/unit_tests/services/test_skill_management_service.py @@ -1150,6 +1150,80 @@ def test_replace_agent_bindings_rejects_skill_name_conflict_with_agent_config_sk assert exc_info.value.details == {"names": ["finance-sop"]} +@pytest.mark.parametrize( + ("snapshot_has_skill", "draft_has_skill", "debug_has_skill", "expect_conflict"), + [ + (True, False, False, False), + (True, False, True, False), + (False, True, False, True), + (True, True, False, True), + (False, None, True, False), + (True, None, True, True), + ], +) +def test_replace_agent_bindings_checks_editable_config( + snapshot_has_skill: bool, + draft_has_skill: bool | None, + debug_has_skill: bool, + expect_conflict: bool, +) -> None: + service = SkillManagementService(tool_file_manager=_FakeToolFileManager()) + created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop")) + skill_ref = AgentConfigSkillRefConfig(name="finance-sop", description="Embedded skill.", file_id="tool-file-1") + with session_factory.create_session() as session: + snapshot = AgentConfigSnapshot( + tenant_id=TENANT, + agent_id=AGENT, + version=1, + config_snapshot=AgentSoulConfig(config_skills=[skill_ref] if snapshot_has_skill else []), + created_by=USER, + ) + session.add(snapshot) + session.flush() + agent = session.get(Agent, AGENT) + assert agent is not None + agent.active_config_snapshot_id = snapshot.id + if draft_has_skill is not None: + session.add( + AgentConfigDraft( + tenant_id=TENANT, + agent_id=AGENT, + draft_type=AgentConfigDraftType.DRAFT, + account_id=None, + draft_owner_key="", + base_snapshot_id=snapshot.id, + config_snapshot=AgentSoulConfig(config_skills=[skill_ref] if draft_has_skill else []), + created_by=USER, + ) + ) + if debug_has_skill: + session.add( + AgentConfigDraft( + tenant_id=TENANT, + agent_id=AGENT, + draft_type=AgentConfigDraftType.DEBUG_BUILD, + account_id=USER, + draft_owner_key=USER, + base_snapshot_id=snapshot.id, + config_snapshot=AgentSoulConfig(config_skills=[skill_ref]), + created_by=USER, + ) + ) + session.commit() + + if expect_conflict: + with pytest.raises(SkillManagementServiceError) as exc_info: + service.replace_agent_bindings(tenant_id=TENANT, user_id=USER, agent_id=AGENT, skill_ids=[created["id"]]) + assert exc_info.value.code == "agent_skill_name_conflict" + assert exc_info.value.details == {"names": ["finance-sop"]} + else: + result = service.replace_agent_bindings( + tenant_id=TENANT, user_id=USER, agent_id=AGENT, skill_ids=[created["id"]] + ) + assert result["skill_ids"] == [created["id"]] + assert service.list_agent_bindings(tenant_id=TENANT, agent_id=AGENT)["data"][0]["id"] == created["id"] + + def test_replace_agent_bindings_allows_existing_bound_workspace_skill_name_in_agent_config() -> None: service = SkillManagementService(tool_file_manager=_FakeToolFileManager()) created = service.create_skill(tenant_id=TENANT, user_id=USER, payload=SkillCreatePayload(name="finance-sop")) diff --git a/web/features/skills/detail/skill-metadata.tsx b/web/features/skills/detail/skill-metadata.tsx index 16a167c9d81..461d7e895a2 100644 --- a/web/features/skills/detail/skill-metadata.tsx +++ b/web/features/skills/detail/skill-metadata.tsx @@ -564,7 +564,12 @@ function SkillReferenceItem({ } /> - + {workflowName} { expect(queryClient.getQueryData(listKey)).toEqual([remainingExtension]) }) }) + +describe('workspace skill deletion cache', () => { + it.each([true, false])('refreshes agent bindings after deletion (active: %s)', async (active) => { + const consoleQuery = await loadConsoleQuery() + const client = new QueryClient({ + defaultOptions: { queries: { staleTime: Infinity, retry: false } }, + }) + const input = { params: { agent_id: 'agent-1' } } + const queryKey = consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryKey({ input }) + const previousBindings = { agent_id: 'agent-1', skill_ids: ['skill-1'], data: [] } + const emptyBindings = { agent_id: 'agent-1', skill_ids: [], data: [] } + client.setQueryData(queryKey, previousBindings) + const queryFn = vi.fn(async () => emptyBindings) + const observer = new QueryObserver(client, { queryKey, queryFn }) + let unsubscribe = active ? observer.subscribe(() => {}) : undefined + const onSuccess = vi.fn() + const mutation = new MutationObserver( + client, + consoleQuery.workspaces.current.skills.bySkillId.delete.mutationOptions({ + mutationFn: async () => ({ id: 'skill-1', deleted: true }), + onSuccess, + }), + ) + + try { + await mutation.mutate({ + params: { skill_id: 'skill-1' }, + body: { confirmation_name: 'Skill' }, + }) + if (!active) unsubscribe = observer.subscribe(() => {}) + await vi.waitFor(() => expect(observer.getCurrentResult().data).toEqual(emptyBindings)) + expect(queryFn).toHaveBeenCalledTimes(1) + expect(onSuccess).toHaveBeenCalledTimes(1) + } finally { + unsubscribe?.() + client.clear() + } + }) + + it('preserves cached bindings when deletion fails', async () => { + const consoleQuery = await loadConsoleQuery() + const client = new QueryClient() + const queryKey = consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryKey({ + input: { params: { agent_id: 'agent-1' } }, + }) + const bindings = { agent_id: 'agent-1', skill_ids: ['skill-1'], data: [] } + client.setQueryData(queryKey, bindings) + const mutation = new MutationObserver( + client, + consoleQuery.workspaces.current.skills.bySkillId.delete.mutationOptions({ + mutationFn: async () => { + throw new Error('Deletion failed') + }, + }), + ) + try { + await expect(mutation.mutate({ params: { skill_id: 'skill-1' }, body: {} })).rejects.toThrow( + 'Deletion failed', + ) + expect(client.getQueryData(queryKey)).toEqual(bindings) + expect(client.getQueryState(queryKey)?.isInvalidated).toBe(false) + } finally { + client.clear() + } + }) +}) diff --git a/web/service/console/query-policies.ts b/web/service/console/query-policies.ts index 48f35dc0e72..4838d3e9282 100644 --- a/web/service/console/query-policies.ts +++ b/web/service/console/query-policies.ts @@ -15,6 +15,20 @@ export function createConsoleQuery(consoleClient: ConsoleClient) { experimental_defaults: { workspaces: { current: { + skills: { + bySkillId: { + delete: { + mutationOptions: { + onSettled: (_data, error, _variables, _result, context) => { + if (error) return + return context.client.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.agents.byAgentId.skills.get.key(), + }) + }, + }, + }, + }, + }, rbac: { accessPolicies: { post: { From cddfe06a8668adb3a0edc06d355245f12f10f5a7 Mon Sep 17 00:00:00 2001 From: invalid w Date: Thu, 17 Sep 2026 06:02:51 +0000 Subject: [PATCH 18/28] chore(web): add tooltips to workflow header buttons (#42307) --- .../workflow/header/chat-variable-button.tsx | 49 ++++++++++----- .../workflow/header/checklist/index.tsx | 60 +++++++++++-------- .../components/workflow/header/env-button.tsx | 40 ++++++++----- .../header/global-variable-button.tsx | 44 ++++++++------ 4 files changed, 117 insertions(+), 76 deletions(-) diff --git a/web/app/components/workflow/header/chat-variable-button.tsx b/web/app/components/workflow/header/chat-variable-button.tsx index a3804835505..6b532172b46 100644 --- a/web/app/components/workflow/header/chat-variable-button.tsx +++ b/web/app/components/workflow/header/chat-variable-button.tsx @@ -1,5 +1,6 @@ import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo } from 'react' import { useTranslation } from 'react-i18next' import { useStore } from '@/app/components/workflow/store' @@ -22,23 +23,39 @@ const ChatVariableButton = ({ disabled }: { disabled: boolean }) => { } return ( - $['chatVariable.panelTitle'], { ns: 'workflow' })} - aria-expanded={showChatVariablePanel} - size="lg" - className={cn( - 'border border-transparent', - theme === 'dark' && showChatVariablePanel && 'border-black/5 bg-white/10 backdrop-blur-xs', - )} - disabled={disabled} - onClick={handleClick} - variant="ghost" - > - + $['chatVariable.panelTitle'], { + ns: 'workflow', + })} + aria-expanded={showChatVariablePanel} + size="lg" + className={cn( + 'border border-transparent', + theme === 'dark' && + showChatVariablePanel && + 'border-black/5 bg-white/10 backdrop-blur-xs', + )} + disabled={disabled} + onClick={handleClick} + variant="ghost" + > + + + } /> - + + + {t(($) => $['chatVariable.panelTitle'], { + ns: 'workflow', + })} + + ) } diff --git a/web/app/components/workflow/header/checklist/index.tsx b/web/app/components/workflow/header/checklist/index.tsx index 191c529f630..856b56f0726 100644 --- a/web/app/components/workflow/header/checklist/index.tsx +++ b/web/app/components/workflow/header/checklist/index.tsx @@ -9,6 +9,7 @@ import { PopoverTitle, PopoverTrigger, } from '@langgenius/dify-ui/popover' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useEdges } from 'reactflow' @@ -58,32 +59,39 @@ const WorkflowChecklist = ({ disabled, showGoTo = true, onItemClick }: WorkflowC return ( !disabled && setOpen(newOpen)}> - - - - {!!needWarningNodes.length && ( - - {needWarningNodes.length} - - )} - - } - /> + + + + + {!!needWarningNodes.length && ( + + {needWarningNodes.length} + + )} + + } + /> + } + /> + {checklistLabel} + { } return ( - $['env.envPanelTitle'], { ns: 'workflow' })} - aria-expanded={showEnvPanel} - size="lg" - className={cn( - 'border border-transparent', - theme === 'dark' && showEnvPanel && 'border-black/5 bg-white/10 backdrop-blur-xs', - )} - variant="ghost" - disabled={disabled} - onClick={handleClick} - > - + $['env.envPanelTitle'], { ns: 'workflow' })} + aria-expanded={showEnvPanel} + size="lg" + className={cn( + 'border border-transparent', + theme === 'dark' && showEnvPanel && 'border-black/5 bg-white/10 backdrop-blur-xs', + )} + variant="ghost" + disabled={disabled} + onClick={handleClick} + > + + + } /> - + {t(($) => $['env.envPanelTitle'], { ns: 'workflow' })} + ) } diff --git a/web/app/components/workflow/header/global-variable-button.tsx b/web/app/components/workflow/header/global-variable-button.tsx index 99916c9eacb..6388d472aa0 100644 --- a/web/app/components/workflow/header/global-variable-button.tsx +++ b/web/app/components/workflow/header/global-variable-button.tsx @@ -1,5 +1,6 @@ import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo } from 'react' import { useTranslation } from 'react-i18next' import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks/use-input-field-panel' @@ -25,25 +26,32 @@ const GlobalVariableButton = ({ disabled }: { disabled: boolean }) => { } return ( - $['globalVar.title'], { ns: 'workflow' })} - aria-expanded={showGlobalVariablePanel} - size="lg" - className={cn( - 'border border-transparent', - theme === 'dark' && - showGlobalVariablePanel && - 'border-black/5 bg-white/10 backdrop-blur-xs', - )} - disabled={disabled} - onClick={handleClick} - variant="ghost" - > - + $['globalVar.title'], { ns: 'workflow' })} + aria-expanded={showGlobalVariablePanel} + size="lg" + className={cn( + 'border border-transparent', + theme === 'dark' && + showGlobalVariablePanel && + 'border-black/5 bg-white/10 backdrop-blur-xs', + )} + disabled={disabled} + onClick={handleClick} + variant="ghost" + > + + + } /> - + {t(($) => $['globalVar.title'], { ns: 'workflow' })} + ) } From 81a65bd75037bce6ffad8ebb47711e0cffe95742 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 17 Sep 2026 06:23:43 +0000 Subject: [PATCH 19/28] chore(deps): upgrade compatible workspace dependencies (#42397) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 601 +++++++++++++++++++++----------------------- pnpm-workspace.yaml | 28 +-- 2 files changed, 294 insertions(+), 335 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a434f12a643..5d762e4b8a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,8 +201,8 @@ catalogs: specifier: 0.27.20 version: 0.27.20 '@formatjs/intl-localematcher': - specifier: 0.8.14 - version: 0.8.14 + specifier: 0.9.0 + version: 0.9.0 '@heroicons/react': specifier: 2.2.0 version: 2.2.0 @@ -264,8 +264,8 @@ catalogs: specifier: 4.9.0 version: 4.9.0 '@rgrove/parse-xml': - specifier: 4.2.3 - version: 4.2.3 + specifier: 5.0.0 + version: 5.0.0 '@sentry/react': specifier: 10.75.0 version: 10.75.0 @@ -339,8 +339,8 @@ catalogs: specifier: 10.4.2 version: 10.4.2 '@testing-library/jest-dom': - specifier: 6.9.1 - version: 6.9.1 + specifier: 7.0.1 + version: 7.0.1 '@testing-library/react': specifier: 16.3.3 version: 16.3.3 @@ -423,8 +423,8 @@ catalogs: specifier: 0.6.5 version: 0.6.5 cn: - specifier: 0.2.6 - version: 0.2.6 + specifier: 0.3.0 + version: 0.3.0 code-inspector-plugin: specifier: 1.6.6 version: 1.6.6 @@ -453,8 +453,8 @@ catalogs: specifier: 3.0.6 version: 3.0.6 elkjs: - specifier: 0.11.1 - version: 0.11.1 + specifier: 0.12.0 + version: 0.12.0 embla-carousel-autoplay: specifier: 8.6.0 version: 8.6.0 @@ -477,8 +477,8 @@ catalogs: specifier: 2.4.0 version: 2.4.0 eslint-markdown: - specifier: 0.12.1 - version: 0.12.1 + specifier: 0.17.0 + version: 0.17.0 eslint-plugin-antfu: specifier: 3.2.3 version: 3.2.3 @@ -486,26 +486,26 @@ catalogs: specifier: 4.7.0 version: 4.7.0 eslint-plugin-command: - specifier: 3.5.3 - version: 3.5.3 + specifier: 4.0.0 + version: 4.0.0 eslint-plugin-erasable-syntax-only: - specifier: 0.4.2 - version: 0.4.2 + specifier: 0.7.1 + version: 0.7.1 eslint-plugin-jsdoc: - specifier: 63.3.3 - version: 63.3.3 + specifier: 64.5.2 + version: 64.5.2 eslint-plugin-jsonc: specifier: 3.4.2 version: 3.4.2 eslint-plugin-markdown-preferences: - specifier: 0.41.1 - version: 0.41.1 + specifier: 0.42.0 + version: 0.42.0 eslint-plugin-n: specifier: 18.3.0 version: 18.3.0 eslint-plugin-no-barrel-files: - specifier: 1.3.1 - version: 1.3.1 + specifier: 2.2.0 + version: 2.2.0 eslint-plugin-perfectionist: specifier: 5.11.1 version: 5.11.1 @@ -522,14 +522,14 @@ catalogs: specifier: 1.5.0 version: 1.5.0 eslint-plugin-unicorn: - specifier: 71.1.0 - version: 71.1.0 + specifier: 75.0.0 + version: 75.0.0 eslint-plugin-yml: specifier: 3.8.1 version: 3.8.1 eventsource-parser: - specifier: 3.1.1 - version: 3.1.1 + specifier: 4.1.1 + version: 4.1.1 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 @@ -615,8 +615,8 @@ catalogs: specifier: 3.0.1 version: 3.0.1 motion: - specifier: 12.43.0 - version: 12.43.0 + specifier: 13.4.0 + version: 13.4.0 negotiator: specifier: 1.1.0 version: 1.1.0 @@ -839,7 +839,7 @@ importers: version: 2.4.0(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) eslint-markdown: specifier: 'catalog:' - version: 0.12.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 0.17.0(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-antfu: specifier: 'catalog:' version: 3.2.3(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) @@ -848,25 +848,25 @@ importers: version: 4.7.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1)))(tailwindcss@4.3.3) eslint-plugin-command: specifier: 'catalog:' - version: 3.5.3(@typescript-eslint/typescript-estree@8.70.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0))(@typescript-eslint/utils@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 4.0.0(@typescript-eslint/typescript-estree@8.70.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0))(@typescript-eslint/utils@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-erasable-syntax-only: specifier: 'catalog:' - version: 0.4.2(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 0.7.1(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-jsdoc: specifier: 'catalog:' - version: 63.3.3(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 64.5.2(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-jsonc: specifier: 'catalog:' version: 3.4.2(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-markdown-preferences: specifier: 'catalog:' - version: 0.41.1(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 0.42.0(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-n: specifier: 'catalog:' version: 18.3.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-no-barrel-files: specifier: 'catalog:' - version: 1.3.1(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 2.2.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-perfectionist: specifier: 'catalog:' version: 5.11.1(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) @@ -884,7 +884,7 @@ importers: version: 1.5.0(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-unicorn: specifier: 'catalog:' - version: 71.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 75.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-yml: specifier: 'catalog:' version: 3.8.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) @@ -926,7 +926,7 @@ importers: version: 0.6.5 eventsource-parser: specifier: 'catalog:' - version: 3.1.1 + version: 4.1.1 js-yaml: specifier: 'catalog:' version: 5.4.2 @@ -1023,7 +1023,7 @@ importers: version: typescript@7.0.2 eventsource-parser: specifier: 'catalog:' - version: 3.1.1 + version: 4.1.1 tsx: specifier: 'catalog:' version: 4.23.13 @@ -1118,7 +1118,7 @@ importers: dependencies: cn: specifier: 'catalog:' - version: 0.2.6 + version: 0.3.0 devDependencies: '@base-ui/react': specifier: 'catalog:' @@ -1368,7 +1368,7 @@ importers: version: 0.27.20(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@formatjs/intl-localematcher': specifier: 'catalog:' - version: 0.8.14 + version: 0.9.0 '@heroicons/react': specifier: 'catalog:' version: 2.2.0(react@19.3.0) @@ -1476,7 +1476,7 @@ importers: version: 3.0.6(echarts@6.1.0)(react@19.3.0) elkjs: specifier: 'catalog:' - version: 0.11.1 + version: 0.12.0 embla-carousel-autoplay: specifier: 'catalog:' version: 8.6.0(embla-carousel@8.6.0) @@ -1563,7 +1563,7 @@ importers: version: 3.0.1 motion: specifier: 'catalog:' - version: 12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 13.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) negotiator: specifier: 'catalog:' version: 1.1.0 @@ -1693,7 +1693,7 @@ importers: version: link:../packages/dify-ui '@rgrove/parse-xml': specifier: 'catalog:' - version: 4.2.3 + version: 5.0.0 '@storybook/addon-docs': specifier: 'catalog:' version: 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0)(vite-plus@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(happy-dom@20.14.5)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1))) @@ -1723,7 +1723,7 @@ importers: version: 10.4.2 '@testing-library/jest-dom': specifier: 'catalog:' - version: 6.9.1 + version: 7.0.1(@testing-library/dom@10.4.2)(vitest@4.1.11) '@testing-library/react': specifier: 'catalog:' version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -2180,13 +2180,13 @@ packages: '@emoji-mart/data@1.2.1': resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} - '@es-joy/jsdoccomment@0.88.0': - resolution: {integrity: sha512-GK/HL/claLLNo5KG705auIlZMwEtmn88ofSGuLsmVZwKBqMPJhW9DiznYNq07QEqz9BPtA3LBfYImtZmhVvRAw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@es-joy/jsdoccomment@0.92.0': + resolution: {integrity: sha512-WivZqV5jPTYDQJgIMp0hTsyQESKYNY6yCJr4b0l84tYtQntkAMkc6zARh2vlRQyeg4aYHPUkCUDpvr2IWAiGpQ==} + engines: {node: ^22.22.2 || >=24.15.0} - '@es-joy/jsdoccomment@0.91.0': - resolution: {integrity: sha512-vgqlMGNNhZxwDYbUNIHj3Hskb4R28iqdXx90ufHyt/NeuTQkeqjTDslAs9I0/GCAfbxP5BpH5WsL1R1fht5Lxg==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@es-joy/jsdoccomment@0.97.0': + resolution: {integrity: sha512-EP8uoFfh6+GsdGCduYtmWAW0h7AO+Ayik9Vh5YbA2r/3N6lmJKkCNZX+q3QBXC1K6ixjQ/9igF2b7WVvLm063g==} + engines: {node: ^22.22.2 || >=24.15.0} '@es-joy/resolve.exports@1.2.0': resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} @@ -2354,6 +2354,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2430,10 +2436,6 @@ packages: resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2442,10 +2444,6 @@ packages: resolution: {integrity: sha512-iPmijIAq4hlIJB86PYmY/fcZORHtjphSqICDbwuw32A/JmkhZQ/K/6TjHE03zqf3n5yABpVcbRAMG8Mi9ojy8g==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/markdown@7.5.1': - resolution: {integrity: sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/markdown@8.0.3': resolution: {integrity: sha512-rBTSSShrq7e4O+PWfeE4azH4/CWPNrC+VGxBXiW00o3vYVJnznsZiDayj3KC9JztIMVRZxZHn2nrDIUau/4j7A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2454,10 +2452,6 @@ packages: resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.7.2': resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2490,8 +2484,8 @@ packages: '@formatjs/fast-memoize@3.1.7': resolution: {integrity: sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==} - '@formatjs/intl-localematcher@0.8.14': - resolution: {integrity: sha512-8O7V38QmbB11+ryJ3KM/znrzXtiqgpXzqNoqDsndHZZ5/zDKTTL+cf5F0mzXkQEwhx8IlWnqYvEovf2S5EwLTw==} + '@formatjs/intl-localematcher@0.9.0': + resolution: {integrity: sha512-Z7TD3ssNry1Av1dVDoq/mzpxGyxqWwU0ClWEo+BB3k0JQTeQvUGmeK9WZGYvlqStS0UqkaKXk3dfjsb6NaHAxw==} '@heroicons/react@2.2.0': resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} @@ -4186,9 +4180,9 @@ packages: resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==} engines: {node: '>= 10'} - '@rgrove/parse-xml@4.2.3': - resolution: {integrity: sha512-Jhlb+0zYez1T1yXUQs3F1qAtFuJljBVNdy9TKmLDauAXkxsOXopKYOyQ5Wm6SvP3fycav0GviX4Y15WWhGetMw==} - engines: {node: '>=14.0.0'} + '@rgrove/parse-xml@5.0.0': + resolution: {integrity: sha512-SgHFyisNAkS2ZbM8SCriUm1N0bxLiz0POKvpDeAtrnLrXaYG1PawNwu1rSjmE/vcVHbZzYWNgVP6xkep6R4uXg==} + engines: {node: '>=22.12.0'} '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -4637,6 +4631,16 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@7.0.1': + resolution: {integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' + vitest: 4.1.11 + peerDependenciesMeta: + vitest: + optional: true + '@testing-library/react@16.3.3': resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} engines: {node: '>=18'} @@ -5600,9 +5604,9 @@ packages: resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} - are-docs-informative@0.0.2: - resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} - engines: {node: '>=14'} + are-docs-informative@0.1.1: + resolution: {integrity: sha512-sqRsNQBwbKLRX0jV5Cu5uzmtflf892n4Vukz7T659ebL4pz3mpOqCMU7lxMoBTFwnp10E3YB5ZcyHM41W5bcDA==} + engines: {node: '>=18'} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -5625,8 +5629,8 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} - ast-v8-to-istanbul@1.0.4: - resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} + ast-v8-to-istanbul@1.0.6: + resolution: {integrity: sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==} async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -5673,6 +5677,10 @@ packages: bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -5868,8 +5876,8 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - cn@0.2.6: - resolution: {integrity: sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==} + cn@0.3.0: + resolution: {integrity: sha512-qRoU0ARrZD3Bbj1wKFftEKxPjSnzTEdFkf22NVhW+zU9D/wajhoP+qUZ5DChhhSOBCH1onV12hrmGqfu1+5Z2Q==} engines: {node: '>=20'} hasBin: true @@ -5931,10 +5939,6 @@ packages: resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} - convert-hrtime@5.0.0: - resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} - engines: {node: '>=12'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -6316,8 +6320,8 @@ packages: electron-to-chromium@1.5.417: resolution: {integrity: sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ==} - elkjs@0.11.1: - resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} + elkjs@0.12.0: + resolution: {integrity: sha512-YZcKynxVxYoKIOEpywEPwCFdg+BTbxQRNf3pbwdDCvc8O3kQD8bmIwSxKU1eOTVc4Xo+VG9Te+575mlfvOrhEQ==} embla-carousel-autoplay@8.6.0: resolution: {integrity: sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==} @@ -6403,9 +6407,6 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -6454,11 +6455,12 @@ packages: '@eslint/json': optional: true - eslint-markdown@0.12.1: - resolution: {integrity: sha512-zQ2pnYdZtLLDW2QeeJ0szqYy7yD+ntwBe1mAibeWSo5tZaUrhPpfXqsSf6q09K9OArl9OP4/sxgSlDgZ5cQliA==} + eslint-markdown@0.17.0: + resolution: {integrity: sha512-WG5uqP9dH3Dg01hUt/LyYZcpCG9IdnZuMmiKtxEKFaF0g32GTneI2nGbnOf8uDlg9vaJC3AMAETDwz4VjYgyqg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} peerDependencies: - eslint: ^9.31.0 || ^10.0.0-rc.0 + '@eslint/markdown': ^8.0.3 + eslint: ^9.39.0 || ^10.0.0 peerDependenciesMeta: eslint: optional: true @@ -6481,15 +6483,16 @@ packages: oxlint: optional: true - eslint-plugin-command@3.5.3: - resolution: {integrity: sha512-JIhmUOW2K2Dw1K+p4mtj5potkqTZ4ZaicrTkgCygPZCWErP2+O2F46n7ZwGKapeEjiwcVqoY8u01SxNUUXWPeQ==} + eslint-plugin-command@4.0.0: + resolution: {integrity: sha512-mM1UdKu39YF5nUBVbA+PaVvpB3R3Niwjes8CQ8rJTTbgIThgrfQILgDqmtpAAQL1xGaolCoES1SfiwzwPVGkXQ==} + engines: {node: ^22.22.2 || >=24.15.0} peerDependencies: '@typescript-eslint/typescript-estree': '*' '@typescript-eslint/utils': '*' eslint: '*' - eslint-plugin-erasable-syntax-only@0.4.2: - resolution: {integrity: sha512-eLeg6CgNVY6LYPiPZnDn2JLh1HQVGoqjq1OD2GKGo70EcwbTwpWcqgi1zJJ/PJIKRO87cejCp9lwv5psrdxuKA==} + eslint-plugin-erasable-syntax-only@0.7.1: + resolution: {integrity: sha512-2zpgLgFq/HmIVQU2KNOsuuQdJQAiRdq23xpCamhatbH4ZlzinqXdDmbxGALj7VSiC9GMBHqmu4Cu7KaSqQfNhw==} engines: {node: '>=20.18.0'} peerDependencies: '@typescript-eslint/parser': '>=8' @@ -6502,11 +6505,15 @@ packages: peerDependencies: eslint: '>=8' - eslint-plugin-jsdoc@63.3.3: - resolution: {integrity: sha512-xI4IeVRzRFA2DGHrPLIxF3U+oJHU3FE+P9Zb27fVs5dPHgfcpoAs0PyCbznVhK7pwR+9BPUztFeXSgpw/CL4Yg==} - engines: {node: ^22.13.0 || >=24} + eslint-plugin-jsdoc@64.5.2: + resolution: {integrity: sha512-GirLf/jpVQ/HSLVT98ztIrJ8GUlWWrrwExkofqzeIjjOxlqFtyqnpkRs30FLwEKh0xHEpgy35CU0qFn0I/Mh9w==} + engines: {node: ^22.22.2 || >=24.15.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true eslint-plugin-jsonc@3.4.2: resolution: {integrity: sha512-q5xzjCYFQuhFomTbctXzd3STfDJ8XduvaigjQmMEbP2gzSKrc58y3Fv6D775/cIK1/sRUn+4kpljiU2iW8k0zw==} @@ -6514,8 +6521,8 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-markdown-preferences@0.41.1: - resolution: {integrity: sha512-Xi4rlT7oBZ8PMGDl7J9khgO2vF9X0F/6ag05/25Vyq7r3llaK95x9D6DpzXidxC2Gagl/e8bp2Hw47r4I3wWSA==} + eslint-plugin-markdown-preferences@0.42.0: + resolution: {integrity: sha512-LVoGwsRDeLFooaj9OndJrof5OslnQQnZKStP+GX0DMnAt6Nt2tgJzP+URRNXqS54lYnIa0m6pBo7CO1n/utTsw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: '@eslint/markdown': ^7.4.0 || ^8.0.0 @@ -6534,10 +6541,14 @@ packages: typescript: optional: true - eslint-plugin-no-barrel-files@1.3.1: - resolution: {integrity: sha512-y7OX5kyH7PMNRFhLF6SmM4JapxvaxExrgWPndPNTzilpO5uBqybuN480g3E8TTxT3OLOOhQDynmcJ0dnipIyNA==} + eslint-plugin-no-barrel-files@2.2.0: + resolution: {integrity: sha512-bksrf75UqNXwKnTdK6lCAZI+Ci0b2Mvt9VOY2wBq8Tqu4yxH36bPePaT6Cn4jOdBiY2eWzM6rxXVjOscwiUE4g==} peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 + typescript: ^5.6.3 || ^6.0.0 + peerDependenciesMeta: + typescript: + optional: true eslint-plugin-perfectionist@5.11.1: resolution: {integrity: sha512-xloHScnOpin9Jy3CegfzoBeY33ASk0spLyM2sJs0LyNHQjv9m0osUD2PYNT7NrJieZbUR2Ebn4A5Q0+0dOcv+A==} @@ -6609,8 +6620,8 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-unicorn@71.1.0: - resolution: {integrity: sha512-dn3YmR3qLLUeYyo/os3ubZ7UHQJ1WbBAgC9cIhnLTyMj9J6kivuc2U1fCmYetLexUlTDVYtBqhjSj/VaebTe6Q==} + eslint-plugin-unicorn@75.0.0: + resolution: {integrity: sha512-72I7eSn+/z5CpgValFBl9mp2JE5UPD+nUUyB/VIGUMF1rCP1i37mA05zfIAVIU312A+qCZ38T8tSZr3oaMv4Mw==} engines: {node: '>=22'} peerDependencies: eslint: '>=10.4' @@ -6680,9 +6691,9 @@ packages: event-target-bus@1.1.0: resolution: {integrity: sha512-24JX34nM0qRcsj7KvtLbVpMcX8ZQYmb0+0a6viaV2GJtVFg5UGyJyDLITkBipJIG0fSscNskI2PxdhTBIMeg8g==} - eventsource-parser@3.1.1: - resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} - engines: {node: '>=18.0.0'} + eventsource-parser@4.1.1: + resolution: {integrity: sha512-zaWNqeSLeyzEAAt2Q6aDBRDRk/VnPmsKqvToyGvv6aahvLv71A4Jw7jG93Hckf11zXurbqgsffvpSgKPoeU1Qw==} + engines: {node: '>=22.12'} expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} @@ -6788,15 +6799,12 @@ packages: react-dom: optional: true - framer-motion@12.43.0: - resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} + framer-motion@13.4.0: + resolution: {integrity: sha512-HCpqKM9BTu6UYKtj0u6J1QNxojnnirNcghcSDZ+SkpiL3myxvtsgXPS3ZGEELC2eSma7YiA937rAEXtRyydSXQ==} peerDependencies: - '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true react: optional: true react-dom: @@ -6815,10 +6823,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-timeout@1.0.2: - resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} - engines: {node: '>=18'} - fuse.js@7.5.0: resolution: {integrity: sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==} engines: {node: '>=10'} @@ -7114,10 +7118,6 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - is-identifier@1.1.0: - resolution: {integrity: sha512-NhOds0mDx9lJu+1lBRO0xbwFo5nobA7GCk/0e5xjr6+6XugX985+0OyGX35BNrTkPAsdLcIKg02HUQJOK8D8kw==} - engines: {node: '>=18'} - is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -7218,21 +7218,17 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true js-yaml@5.4.2: resolution: {integrity: sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==} hasBin: true - jsdoc-type-pratt-parser@7.2.0: - resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} - engines: {node: '>=20.0.0'} - - jsdoc-type-pratt-parser@8.0.0: - resolution: {integrity: sha512-uQu/fXVqVaMg6gM8/E5G5+eygVcZ1NV0Z51CvqhNa2bDWxvHMl484ETr6vph4oPyC+KUcbP/w2W2pewfCiR9aQ==} - engines: {node: '>=20.0.0'} + jsdoc-type-pratt-parser@9.0.1: + resolution: {integrity: sha512-1LJ/negRKJxH0+ulOsJYEpUgzIs1DAOz9QctuS4In2LW/Ro3l8C4ej5al+5FU2hBGEOAY3R3SCnP6x8226JMIw==} + engines: {node: ^22.22.2 || >=24.15.0} jsdoc-type-pratt-parser@9.2.1: resolution: {integrity: sha512-V4Ww4EHnTcTLSOMoB0FsF72JhQvcAsriCm/LWnxJeGWoxIjEL2l9na11abQok5SYShq8m0Gl02el/xAbTCulvQ==} @@ -7541,9 +7537,8 @@ packages: magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} - make-asynchronous@1.1.0: - resolution: {integrity: sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==} - engines: {node: '>=18'} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -7756,6 +7751,10 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -7782,21 +7781,18 @@ packages: moo-color@1.0.3: resolution: {integrity: sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==} - motion-dom@12.43.0: - resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} + motion-dom@13.3.0: + resolution: {integrity: sha512-AmAnB6pHdZ1vHGzWzuzteG6Q3j1lHKeX/lbST6jGlgm0cjvLUgaCk1xhOuPMOy5SgovN6wnnUdwFrkcsus7Ftg==} - motion-utils@12.39.0: - resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + motion-utils@13.3.0: + resolution: {integrity: sha512-sgSschQp7EseHInIlR7hBbMuvet3RA0bs28KPZAXJcGKGdxHGvh1ogpYDilY3bOMtl73EqPNmp75sAKHYPU5sg==} - motion@12.43.0: - resolution: {integrity: sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==} + motion@13.4.0: + resolution: {integrity: sha512-aiNSA29N+vgV5p5cJSB5cFzx8KTuuxB99z8zGtxsaqlwKtBK6sq8YgPRpkVQLkbJMOcLDU0q/ShdT95qpBy/0g==} peerDependencies: - '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true react: optional: true react-dom: @@ -8075,6 +8071,10 @@ packages: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + obug@3.0.0: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} @@ -8179,10 +8179,6 @@ packages: vite-plus: optional: true - p-event@6.0.1: - resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} - engines: {node: '>=16.17'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -8191,10 +8187,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-timeout@6.1.4: - resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} - engines: {node: '>=14.16'} - package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} @@ -8349,10 +8341,6 @@ packages: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.28: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} @@ -8425,9 +8413,6 @@ packages: resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==} engines: {node: '>=14.18.0'} - rc9@3.0.1: - resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} - rc9@3.1.0: resolution: {integrity: sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==} @@ -8841,8 +8826,8 @@ packages: state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} std-semver@1.0.8: resolution: {integrity: sha512-9SN0XIjBBXCT6ZXXVnScJN4KP2RyFg6B8sEoFlugVHMANysfaEni4LTWlvUQQ/R0wgZl1Ovt9KBQbzn21kHoZA==} @@ -8948,10 +8933,6 @@ packages: stylis@4.4.0: resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} - super-regex@1.1.0: - resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} - engines: {node: '>=18'} - supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -9014,10 +8995,6 @@ packages: resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} engines: {node: '>=18'} - time-span@5.1.0: - resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} - engines: {node: '>=12'} - tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -9037,6 +9014,10 @@ packages: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -9053,6 +9034,10 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.4: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} @@ -9475,9 +9460,6 @@ packages: web-vitals@5.1.0: resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} - web-worker@1.5.0: - resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} - webpack-sources@3.5.0: resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} engines: {node: '>=10.13.0'} @@ -9776,7 +9758,7 @@ snapshots: '@amplitude/rrweb-snapshot@2.1.0': dependencies: - postcss: 8.5.26 + postcss: 8.5.28 '@amplitude/rrweb-types@2.0.0-alpha.40': {} @@ -10213,21 +10195,21 @@ snapshots: '@emoji-mart/data@1.2.1': {} - '@es-joy/jsdoccomment@0.88.0': + '@es-joy/jsdoccomment@0.92.0': dependencies: '@types/estree': 1.0.9 - '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/types': 8.70.0 comment-parser: 1.4.7 esquery: 1.7.0 - jsdoc-type-pratt-parser: 7.2.0 + jsdoc-type-pratt-parser: 9.0.1 - '@es-joy/jsdoccomment@0.91.0': + '@es-joy/jsdoccomment@0.97.0': dependencies: '@types/estree': 1.0.9 - '@typescript-eslint/types': 8.69.0 - comment-parser: 1.4.7 + '@typescript-eslint/types': 8.70.0 + comment-parser: 1.4.8 esquery: 1.7.0 - jsdoc-type-pratt-parser: 8.0.0 + jsdoc-type-pratt-parser: 9.2.1 '@es-joy/resolve.exports@1.2.0': {} @@ -10315,6 +10297,11 @@ snapshots: eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) ignore: 7.0.8 + '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))': + dependencies: + eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))': dependencies: eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) @@ -10426,10 +10413,6 @@ snapshots: dependencies: '@eslint/core': 1.2.1 - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - '@eslint/core@1.2.1': dependencies: '@types/json-schema': 7.0.15 @@ -10439,20 +10422,6 @@ snapshots: mdn-data: 2.29.0 source-map-js: 1.2.1 - '@eslint/markdown@7.5.1(supports-color@11.0.0)': - dependencies: - '@eslint/core': 0.17.0 - '@eslint/plugin-kit': 0.4.1 - github-slugger: 2.0.0 - mdast-util-from-markdown: 2.0.3(supports-color@11.0.0) - mdast-util-frontmatter: 2.0.1(supports-color@11.0.0) - mdast-util-gfm: 3.1.0(supports-color@11.0.0) - micromark-extension-frontmatter: 2.0.0 - micromark-extension-gfm: 3.0.0 - micromark-util-normalize-identifier: 2.0.1 - transitivePeerDependencies: - - supports-color - '@eslint/markdown@8.0.3(supports-color@11.0.0)': dependencies: '@eslint/core': 1.2.1 @@ -10471,11 +10440,6 @@ snapshots: '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - '@eslint/plugin-kit@0.7.2': dependencies: '@eslint/core': 1.2.1 @@ -10513,7 +10477,7 @@ snapshots: '@formatjs/fast-memoize@3.1.7': {} - '@formatjs/intl-localematcher@0.8.14': + '@formatjs/intl-localematcher@0.9.0': dependencies: '@formatjs/fast-memoize': 3.1.7 @@ -10534,7 +10498,7 @@ snapshots: dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 - js-yaml: 4.3.0 + js-yaml: 4.2.0 '@hey-api/openapi-ts@0.98.2(@typescript/typescript6@6.0.2)(magicast@0.5.3)': dependencies: @@ -10772,7 +10736,7 @@ snapshots: '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jsdevtools/ono@7.1.3': {} @@ -11785,7 +11749,7 @@ snapshots: '@resvg/resvg-wasm@2.4.0': {} - '@rgrove/parse-xml@4.2.3': {} + '@rgrove/parse-xml@5.0.0': {} '@rolldown/pluginutils@1.0.1': {} @@ -12302,6 +12266,18 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 + '@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.2)(vitest@4.1.11)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.2 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + optionalDependencies: + vitest: 4.1.11(@types/node@25.9.7)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1))(happy-dom@20.14.5) + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: '@babel/runtime': 7.29.7 @@ -12858,7 +12834,7 @@ snapshots: '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@4.1.11) '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1)) playwright: 1.63.0 - tinyrainbow: 3.1.0 + tinyrainbow: 3.1.1 vitest: 4.1.11(@types/node@25.9.7)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1))(happy-dom@20.12.0) transitivePeerDependencies: - bufferutil @@ -12871,7 +12847,7 @@ snapshots: '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.1))(vitest@4.1.11) '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.1)) playwright: 1.63.0 - tinyrainbow: 3.1.0 + tinyrainbow: 3.1.1 vitest: 4.1.11(@types/node@25.9.7)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.1))(happy-dom@20.12.0) transitivePeerDependencies: - bufferutil @@ -12942,14 +12918,14 @@ snapshots: dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.11 - ast-v8-to-istanbul: 1.0.4 + ast-v8-to-istanbul: 1.0.6 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 - magicast: 0.5.3 - obug: 2.1.3 - std-env: 4.1.0 - tinyrainbow: 3.1.0 + magicast: 0.5.5 + obug: 2.2.1 + std-env: 4.2.0 + tinyrainbow: 3.1.1 vitest: 4.1.11(@types/node@25.9.7)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1))(happy-dom@20.12.0) optionalDependencies: '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@4.1.11) @@ -13030,7 +13006,7 @@ snapshots: '@oxc-project/runtime': 0.149.0 '@oxc-project/types': 0.149.0 lightningcss: 1.33.0 - postcss: 8.5.26 + postcss: 8.5.28 yuku-codegen: 0.9.5 yuku-parser: 0.9.5 optionalDependencies: @@ -13055,7 +13031,7 @@ snapshots: '@oxc-project/runtime': 0.149.0 '@oxc-project/types': 0.149.0 lightningcss: 1.33.0 - postcss: 8.5.26 + postcss: 8.5.28 yuku-codegen: 0.9.5 yuku-parser: 0.9.5 optionalDependencies: @@ -13261,7 +13237,7 @@ snapshots: ansis@4.3.1: {} - are-docs-informative@0.0.2: {} + are-docs-informative@0.1.1: {} argparse@2.0.1: {} @@ -13283,7 +13259,7 @@ snapshots: dependencies: tslib: 2.8.1 - ast-v8-to-istanbul@1.0.4: + ast-v8-to-istanbul@1.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -13321,6 +13297,10 @@ snapshots: bowser@2.14.1: {} + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -13363,14 +13343,14 @@ snapshots: confbox: 0.2.4 defu: 6.1.7 dotenv: 17.4.2 - exsolve: 1.1.0 + exsolve: 1.1.1 giget: 3.3.0 jiti: 2.7.0 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 2.1.0 - pkg-types: 2.3.1 - rc9: 3.0.1 + pkg-types: 2.3.3 + rc9: 3.1.0 optionalDependencies: magicast: 0.5.3 @@ -13515,7 +13495,7 @@ snapshots: clsx@2.1.1: {} - cn@0.2.6: {} + cn@0.3.0: {} code-inspector-plugin@1.6.6(supports-color@11.0.0): dependencies: @@ -13568,8 +13548,6 @@ snapshots: content-type@2.1.0: {} - convert-hrtime@5.0.0: {} - convert-source-map@2.0.0: {} copy-to-clipboard@4.0.2: {} @@ -13962,7 +13940,7 @@ snapshots: electron-to-chromium@1.5.417: {} - elkjs@0.11.1: {} + elkjs@0.12.0: {} embla-carousel-autoplay@8.6.0(embla-carousel@8.6.0): dependencies: @@ -14038,8 +14016,6 @@ snapshots: es-module-lexer@1.7.0: {} - es-module-lexer@2.3.1: {} - es-module-lexer@2.3.2: {} es-toolkit@1.52.0: {} @@ -14097,15 +14073,13 @@ snapshots: esquery: 1.7.0 jsonc-eslint-parser: 3.3.0 - eslint-markdown@0.12.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-markdown@0.17.0(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - '@eslint/markdown': 7.5.1(supports-color@11.0.0) + '@eslint/markdown': 8.0.3(supports-color@11.0.0) micromark-util-normalize-identifier: 2.0.1 parse5: 8.0.1 optionalDependencies: eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) - transitivePeerDependencies: - - supports-color eslint-plugin-antfu@3.2.3(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)): dependencies: @@ -14129,17 +14103,17 @@ snapshots: - '@eslint/css' - typescript - eslint-plugin-command@3.5.3(@typescript-eslint/typescript-estree@8.70.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0))(@typescript-eslint/utils@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-command@4.0.0(@typescript-eslint/typescript-estree@8.70.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0))(@typescript-eslint/utils@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - '@es-joy/jsdoccomment': 0.88.0 + '@es-joy/jsdoccomment': 0.92.0 '@typescript-eslint/typescript-estree': 8.70.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) '@typescript-eslint/utils': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) - eslint-plugin-erasable-syntax-only@0.4.2(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-erasable-syntax-only@0.7.1(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: '@typescript-eslint/parser': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/utils': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) cached-factory: 0.3.0 eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' @@ -14153,14 +14127,15 @@ snapshots: eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) eslint-compat-utils: 0.5.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) - eslint-plugin-jsdoc@63.3.3(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-jsdoc@64.5.2(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@es-joy/jsdoccomment': 0.91.0 + '@es-joy/jsdoccomment': 0.97.0 '@es-joy/resolve.exports': 1.2.0 - are-docs-informative: 0.0.2 - comment-parser: 1.4.7 + '@typescript-eslint/utils': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + are-docs-informative: 0.1.1 + comment-parser: 1.4.8 debug: 4.4.3(supports-color@11.0.0) - escape-string-regexp: 4.0.0 + escape-string-regexp: 5.0.0 eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) espree: 11.2.0 esquery: 1.7.0 @@ -14170,6 +14145,8 @@ snapshots: semver: 7.8.5 spdx-expression-parse: 5.0.0 to-valid-identifier: 1.0.0 + optionalDependencies: + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -14188,7 +14165,7 @@ snapshots: transitivePeerDependencies: - '@eslint/json' - eslint-plugin-markdown-preferences@0.41.1(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-markdown-preferences@0.42.0(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: '@eslint/markdown': 8.0.3(supports-color@11.0.0) diff-sequences: 29.6.3 @@ -14203,6 +14180,7 @@ snapshots: micromark-extension-math: 3.1.0 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 micromark-util-symbol: 2.0.1 string-width: 8.2.2 transitivePeerDependencies: @@ -14222,13 +14200,16 @@ snapshots: optionalDependencies: typescript: '@typescript/typescript6@6.0.2' - eslint-plugin-no-barrel-files@1.3.1(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-no-barrel-files@2.2.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/parser': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/utils': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) + minimatch: 10.2.6 + optionalDependencies: + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - - typescript eslint-plugin-perfectionist@5.11.1(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: @@ -14379,26 +14360,29 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unicorn@71.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-unicorn@75.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)) + '@eslint/css-tree': 4.0.5 browserslist: 4.28.8 change-case: 5.4.4 ci-info: 4.4.0 core-js-compat: 3.50.0 detect-indent: 7.0.2 + entities: 8.0.0 eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) find-up-simple: 1.0.1 globals: 17.11.0 + identifier-regex: 1.1.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 - is-identifier: 1.1.0 pluralize: 8.0.0 quote-js-string: 0.1.0 regjsparser: 0.13.2 reserved-identifiers: 1.2.0 semver: 7.8.5 strip-indent: 4.1.1 + yaml: 2.9.1 eslint-plugin-yml@3.8.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0)): dependencies: @@ -14489,7 +14473,7 @@ snapshots: event-target-bus@1.1.0: {} - eventsource-parser@3.1.1: {} + eventsource-parser@4.1.1: {} expand-template@2.0.3: optional: true @@ -14591,10 +14575,10 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - framer-motion@12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + framer-motion@13.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - motion-dom: 12.43.0 - motion-utils: 12.39.0 + motion-dom: 13.3.0 + motion-utils: 13.3.0 tslib: 2.8.1 optionalDependencies: react: 19.3.0 @@ -14609,8 +14593,6 @@ snapshots: fsevents@2.3.3: optional: true - function-timeout@1.0.2: {} - fuse.js@7.5.0: {} gensync@1.0.0-beta.2: {} @@ -14955,11 +14937,6 @@ snapshots: is-hexadecimal@2.0.1: {} - is-identifier@1.1.0: - dependencies: - identifier-regex: 1.1.0 - super-regex: 1.1.0 - is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -15028,7 +15005,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.0: + js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -15036,9 +15013,9 @@ snapshots: dependencies: argparse: 2.0.1 - jsdoc-type-pratt-parser@7.2.0: {} - - jsdoc-type-pratt-parser@8.0.0: {} + jsdoc-type-pratt-parser@9.0.1: + dependencies: + '@types/estree': 1.0.9 jsdoc-type-pratt-parser@9.2.1: dependencies: @@ -15297,12 +15274,13 @@ snapshots: '@babel/parser': 7.29.7 '@babel/types': 7.29.7 source-map-js: 1.2.1 + optional: true - make-asynchronous@1.1.0: + magicast@0.5.5: dependencies: - p-event: 6.0.1 - type-fest: 4.41.0 - web-worker: 1.5.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 make-dir@4.0.0: dependencies: @@ -15783,6 +15761,10 @@ snapshots: dependencies: brace-expansion: 5.0.6 + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + minimist@1.2.8: {} minipass@7.1.3: {} @@ -15809,15 +15791,15 @@ snapshots: dependencies: color-name: 1.1.4 - motion-dom@12.43.0: + motion-dom@13.3.0: dependencies: - motion-utils: 12.39.0 + motion-utils: 13.3.0 - motion-utils@12.39.0: {} + motion-utils@13.3.0: {} - motion@12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + motion@13.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - framer-motion: 12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + framer-motion: 13.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) tslib: 2.8.1 optionalDependencies: react: 19.3.0 @@ -15956,6 +15938,8 @@ snapshots: obug@2.1.3: {} + obug@2.2.1: {} + obug@3.0.0: {} ohash@2.0.11: {} @@ -16306,10 +16290,6 @@ snapshots: oxlint-tsgolint: 7.0.2001 vite-plus: 0.3.2(@types/node@25.9.7)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.1) - p-event@6.0.1: - dependencies: - p-timeout: 6.1.4 - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -16318,8 +16298,6 @@ snapshots: dependencies: p-limit: 3.1.0 - p-timeout@6.1.4: {} - package-manager-detector@1.8.0: {} pad-right@0.2.2: @@ -16477,12 +16455,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.26: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.28: dependencies: nanoid: 3.3.18 @@ -16563,11 +16535,6 @@ snapshots: radash@12.1.1: {} - rc9@3.0.1: - dependencies: - defu: 6.1.7 - destr: 2.0.5 - rc9@3.1.0: dependencies: defu: 6.1.7 @@ -17083,7 +17050,7 @@ snapshots: state-local@1.0.7: {} - std-env@4.1.0: {} + std-env@4.2.0: {} std-semver@1.0.8: {} @@ -17232,12 +17199,6 @@ snapshots: stylis@4.4.0: {} - super-regex@1.1.0: - dependencies: - function-timeout: 1.0.2 - make-asynchronous: 1.1.0 - time-span: 5.1.0 - supports-color@10.2.2: {} supports-color@11.0.0: {} @@ -17299,10 +17260,6 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - time-span@5.1.0: - dependencies: - convert-hrtime: 5.0.0 - tiny-case@1.0.3: {} tiny-inflate@1.0.3: {} @@ -17315,6 +17272,8 @@ snapshots: tinyexec@1.2.4: {} + tinyexec@1.3.0: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.7) @@ -17326,6 +17285,8 @@ snapshots: tinyrainbow@3.1.0: {} + tinyrainbow@3.1.1: {} + tinyspy@4.0.4: {} tldts-core@7.4.13: {} @@ -17872,17 +17833,17 @@ snapshots: '@vitest/snapshot': 4.1.11 '@vitest/spy': 4.1.11 '@vitest/utils': 4.1.11 - es-module-lexer: 2.3.1 + es-module-lexer: 2.3.2 expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.3 + obug: 2.2.1 pathe: 2.0.3 picomatch: 4.0.7 - std-env: 4.1.0 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.2.4 + tinyexec: 1.3.0 tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 + tinyrainbow: 3.1.1 vite: '@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1)' why-is-node-running: 2.3.0 optionalDependencies: @@ -17903,17 +17864,17 @@ snapshots: '@vitest/snapshot': 4.1.11 '@vitest/spy': 4.1.11 '@vitest/utils': 4.1.11 - es-module-lexer: 2.3.1 + es-module-lexer: 2.3.2 expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.3 + obug: 2.2.1 pathe: 2.0.3 picomatch: 4.0.7 - std-env: 4.1.0 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.2.4 + tinyexec: 1.3.0 tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 + tinyrainbow: 3.1.1 vite: '@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1)' why-is-node-running: 2.3.0 optionalDependencies: @@ -17934,17 +17895,17 @@ snapshots: '@vitest/snapshot': 4.1.11 '@vitest/spy': 4.1.11 '@vitest/utils': 4.1.11 - es-module-lexer: 2.3.1 + es-module-lexer: 2.3.2 expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.3 + obug: 2.2.1 pathe: 2.0.3 picomatch: 4.0.7 - std-env: 4.1.0 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.2.4 + tinyexec: 1.3.0 tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 + tinyrainbow: 3.1.1 vite: '@voidzero-dev/vite-plus-core@0.3.2(@types/node@25.9.7)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.1)' why-is-node-running: 2.3.0 optionalDependencies: @@ -17966,8 +17927,6 @@ snapshots: web-vitals@5.1.0: {} - web-worker@1.5.0: {} - webpack-sources@3.5.0: {} webpack-virtual-modules@0.6.2: {} @@ -18148,7 +18107,7 @@ time: '@eslint-react/eslint-plugin@5.19.1': '2026-09-14T17:56:33.726Z' '@eslint/markdown@8.0.3': '2026-07-01T13:28:44.339Z' '@floating-ui/react@0.27.20': '2026-07-11T08:41:33.223Z' - '@formatjs/intl-localematcher@0.8.14': '2026-09-12T10:54:55.218Z' + '@formatjs/intl-localematcher@0.9.0': '2026-09-12T16:07:12.757Z' '@heroicons/react@2.2.0': '2024-11-18T15:33:27.317Z' '@hey-api/openapi-ts@0.98.2': '2026-06-08T05:37:17.524Z' '@hono/node-server@2.1.1': '2026-08-14T01:14:02.732Z' @@ -18169,7 +18128,7 @@ time: '@orpc/tanstack-query@1.15.1': '2026-09-15T07:09:56.725Z' '@playwright/test@1.63.0': '2026-09-04T22:44:00.304Z' '@remixicon/react@4.9.0': '2026-01-29T10:53:18.993Z' - '@rgrove/parse-xml@4.2.3': '2026-07-26T23:12:17.833Z' + '@rgrove/parse-xml@5.0.0': '2026-08-30T03:19:42.049Z' '@sentry/react@10.75.0': '2026-09-16T14:58:08.681Z' '@storybook/addon-a11y@10.6.0': '2026-09-02T13:57:15.272Z' '@storybook/addon-docs@10.6.0': '2026-09-02T13:56:43.113Z' @@ -18194,7 +18153,7 @@ time: '@tanstack/react-query@5.103.1': '2026-09-16T15:01:15.056Z' '@tanstack/react-virtual@3.14.13': '2026-09-14T10:22:44.535Z' '@testing-library/dom@10.4.2': '2026-09-13T18:08:36.116Z' - '@testing-library/jest-dom@6.9.1': '2025-10-01T20:04:22.720Z' + '@testing-library/jest-dom@7.0.1': '2026-08-09T23:44:33.598Z' '@testing-library/react@16.3.3': '2026-08-27T17:41:18.735Z' '@testing-library/user-event@14.6.7': '2026-09-02T01:56:32.109Z' '@tsslint/cli@3.1.4': '2026-06-16T18:21:29.075Z' @@ -18223,7 +18182,7 @@ time: chokidar@5.0.0: '2025-11-25T23:28:06.854Z' class-variance-authority@0.7.1: '2024-11-26T08:20:34.604Z' cli-table3@0.6.5: '2024-05-12T16:36:50.079Z' - cn@0.2.6: '2026-09-06T12:13:35.681Z' + cn@0.3.0: '2026-09-12T11:11:13.029Z' code-inspector-plugin@1.6.6: '2026-07-07T04:00:26.523Z' concurrently@10.0.5: '2026-08-15T03:33:20.323Z' copy-to-clipboard@4.0.2: '2026-04-24T22:15:18.933Z' @@ -18233,32 +18192,32 @@ time: dompurify@3.4.15: '2026-09-06T11:39:57.312Z' echarts-for-react@3.0.6: '2026-01-21T04:38:21.243Z' echarts@6.1.0: '2026-05-19T17:52:11.076Z' - elkjs@0.11.1: '2026-03-03T12:21:48.463Z' + elkjs@0.12.0: '2026-07-17T08:07:54.693Z' embla-carousel-autoplay@8.6.0: '2025-04-04T17:37:46.303Z' embla-carousel-fade@8.6.0: '2025-04-04T17:37:50.278Z' embla-carousel-react@8.6.0: '2025-04-04T17:37:53.976Z' emoji-mart@5.6.0: '2024-04-25T14:22:21.440Z' es-toolkit@1.52.0: '2026-08-28T07:22:17.687Z' eslint-config-flat-gitignore@2.4.0: '2026-08-27T07:24:43.063Z' - eslint-markdown@0.12.1: '2026-07-10T23:35:26.055Z' + eslint-markdown@0.17.0: '2026-09-16T06:20:36.564Z' eslint-plugin-antfu@3.2.3: '2026-05-11T02:24:38.348Z' eslint-plugin-better-tailwindcss@4.7.0: '2026-07-19T13:26:01.366Z' - eslint-plugin-command@3.5.3: '2026-07-14T04:59:31.991Z' - eslint-plugin-erasable-syntax-only@0.4.2: '2026-06-18T00:56:01.038Z' - eslint-plugin-jsdoc@63.3.3: '2026-08-02T18:02:10.488Z' + eslint-plugin-command@4.0.0: '2026-07-31T04:38:09.766Z' + eslint-plugin-erasable-syntax-only@0.7.1: '2026-08-19T04:46:35.017Z' + eslint-plugin-jsdoc@64.5.2: '2026-09-16T13:00:55.286Z' eslint-plugin-jsonc@3.4.2: '2026-08-22T12:13:35.450Z' - eslint-plugin-markdown-preferences@0.41.1: '2026-04-09T23:28:41.552Z' + eslint-plugin-markdown-preferences@0.42.0: '2026-07-14T00:58:47.572Z' eslint-plugin-n@18.3.0: '2026-08-08T14:03:55.663Z' - eslint-plugin-no-barrel-files@1.3.1: '2026-04-12T18:28:18.653Z' + eslint-plugin-no-barrel-files@2.2.0: '2026-07-28T19:13:34.360Z' eslint-plugin-perfectionist@5.11.1: '2026-09-15T11:18:22.662Z' eslint-plugin-pnpm@1.9.1: '2026-09-02T01:31:47.410Z' eslint-plugin-regexp@3.3.0: '2026-09-04T01:16:25.907Z' eslint-plugin-storybook@10.6.0: '2026-09-02T14:01:21.299Z' eslint-plugin-toml@1.5.0: '2026-07-25T01:46:13.957Z' - eslint-plugin-unicorn@71.1.0: '2026-07-06T22:07:48.695Z' + eslint-plugin-unicorn@75.0.0: '2026-09-16T06:50:59.617Z' eslint-plugin-yml@3.8.1: '2026-08-05T03:50:22.441Z' eslint@10.10.0: '2026-09-04T14:34:21.799Z' - eventsource-parser@3.1.1: '2026-08-10T15:55:29.938Z' + eventsource-parser@4.1.1: '2026-09-15T18:30:29.195Z' fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z' foxact@0.3.11: '2026-09-05T12:25:54.102Z' fuse.js@7.5.0: '2026-07-13T17:23:36.705Z' @@ -18288,7 +18247,7 @@ time: mermaid@11.17.2: '2026-08-25T11:52:39.930Z' mime@4.1.0: '2025-09-12T17:53:01.376Z' mitt@3.0.1: '2023-07-04T17:31:47.638Z' - motion@12.43.0: '2026-07-28T14:29:31.291Z' + motion@13.4.0: '2026-09-16T13:10:18.420Z' negotiator@1.1.0: '2026-08-20T16:45:05.235Z' next-themes@0.4.6: '2025-03-11T21:02:05.882Z' next@16.3.5: '2026-09-11T17:26:46.049Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2b2e2a51380..e6160a92379 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -73,7 +73,7 @@ catalog: '@eslint-react/eslint-plugin': 5.19.1 '@eslint/markdown': 8.0.3 '@floating-ui/react': 0.27.20 - '@formatjs/intl-localematcher': 0.8.14 + '@formatjs/intl-localematcher': 0.9.0 '@heroicons/react': 2.2.0 '@hey-api/openapi-ts': 0.98.2 '@hono/node-server': 2.1.1 @@ -95,7 +95,7 @@ catalog: '@orpc/tanstack-query': 1.15.1 '@playwright/test': 1.63.0 '@remixicon/react': 4.9.0 - '@rgrove/parse-xml': 4.2.3 + '@rgrove/parse-xml': 5.0.0 '@sentry/react': 10.75.0 '@storybook/addon-a11y': 10.6.0 '@storybook/addon-docs': 10.6.0 @@ -120,7 +120,7 @@ catalog: '@tanstack/react-query': 5.103.1 '@tanstack/react-virtual': 3.14.13 '@testing-library/dom': 10.4.2 - '@testing-library/jest-dom': 6.9.1 + '@testing-library/jest-dom': 7.0.1 '@testing-library/react': 16.3.3 '@testing-library/user-event': 14.6.7 '@tsslint/cli': 3.1.4 @@ -148,7 +148,7 @@ catalog: chokidar: 5.0.0 class-variance-authority: 0.7.1 cli-table3: 0.6.5 - cn: 0.2.6 + cn: 0.3.0 code-inspector-plugin: 1.6.6 concurrently: 10.0.5 copy-to-clipboard: 4.0.2 @@ -158,7 +158,7 @@ catalog: dompurify: 3.4.15 echarts: 6.1.0 echarts-for-react: 3.0.6 - elkjs: 0.11.1 + elkjs: 0.12.0 embla-carousel-autoplay: 8.6.0 embla-carousel-fade: 8.6.0 embla-carousel-react: 8.6.0 @@ -166,24 +166,24 @@ catalog: es-toolkit: 1.52.0 eslint: 10.10.0 eslint-config-flat-gitignore: 2.4.0 - eslint-markdown: 0.12.1 + eslint-markdown: 0.17.0 eslint-plugin-antfu: 3.2.3 eslint-plugin-better-tailwindcss: 4.7.0 - eslint-plugin-command: 3.5.3 - eslint-plugin-erasable-syntax-only: 0.4.2 - eslint-plugin-jsdoc: 63.3.3 + eslint-plugin-command: 4.0.0 + eslint-plugin-erasable-syntax-only: 0.7.1 + eslint-plugin-jsdoc: 64.5.2 eslint-plugin-jsonc: 3.4.2 - eslint-plugin-markdown-preferences: 0.41.1 + eslint-plugin-markdown-preferences: 0.42.0 eslint-plugin-n: 18.3.0 - eslint-plugin-no-barrel-files: 1.3.1 + eslint-plugin-no-barrel-files: 2.2.0 eslint-plugin-perfectionist: 5.11.1 eslint-plugin-pnpm: 1.9.1 eslint-plugin-regexp: 3.3.0 eslint-plugin-storybook: 10.6.0 eslint-plugin-toml: 1.5.0 - eslint-plugin-unicorn: 71.1.0 + eslint-plugin-unicorn: 75.0.0 eslint-plugin-yml: 3.8.1 - eventsource-parser: 3.1.1 + eventsource-parser: 4.1.1 fast-deep-equal: 3.1.3 foxact: 0.3.11 fuse.js: 7.5.0 @@ -212,7 +212,7 @@ catalog: mermaid: 11.17.2 mime: 4.1.0 mitt: 3.0.1 - motion: 12.43.0 + motion: 13.4.0 negotiator: 1.1.0 next: 16.3.5 next-themes: 0.4.6 From 81aa065608859971ce82a50161f6b58bca4ef53f Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 17 Sep 2026 06:55:55 +0000 Subject: [PATCH 20/28] chore: assign frontend tooling code owners and remove invalid owner (#42409) --- .github/CODEOWNERS | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e1b9e97dbb6..249c2932f65 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -51,7 +51,7 @@ /api/tests/*/controllers/trigger/ @CourTeous33 /api/tests/*/core/trigger/ @CourTeous33 /api/tests/*/trigger/ @CourTeous33 -/api/tests/*/controllers/console/billing/ @hj24 @zyssyz123 +/api/tests/*/controllers/console/billing/ @zyssyz123 /api/tests/*/services/enterprise/ @GareArc /api/tests/*/migrations/ @snakevash @laipz8200 @MRZHUH @@ -128,8 +128,8 @@ /api/tasks/async_workflow_tasks.py @Mairuis # Backend - Billing -/api/services/billing_service.py @hj24 @zyssyz123 -/api/controllers/console/billing/ @hj24 @zyssyz123 +/api/services/billing_service.py @zyssyz123 +/api/controllers/console/billing/ @zyssyz123 # Backend - Enterprise /api/configs/enterprise/ @GareArc @@ -147,6 +147,12 @@ # Frontend /web/ @iamjoel +# Frontend - Tooling +/lint.config.ts @hyoban @lyzno1 +/eslint.config.mjs @hyoban @lyzno1 +/knip.config.ts @hyoban @lyzno1 +/vite.config.ts @hyoban @lyzno1 + # Frontend - Platform and Features /web/config/ @lyzno1 /web/env.ts @lyzno1 From 5e9017dbb8347d76f49d421aff7acbcebe632adc Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 17 Sep 2026 07:04:27 +0000 Subject: [PATCH 21/28] refactor(web): finish legacy input migration (#42405) --- lint.config.ts | 10 - oxlint-suppressions.json | 77 +--- .../__tests__/email-change-modal.spec.tsx | 105 +++++ .../account-page/email-change-modal.tsx | 415 +++++++++-------- .../components/base/emoji-picker/Inner.tsx | 26 +- .../base/form/__tests__/index.spec.tsx | 22 +- .../base/__tests__/base-field.spec.tsx | 19 +- .../base/form/components/base/base-field.tsx | 108 +++-- .../components/field/__tests__/text.spec.tsx | 33 -- .../base/form/components/field/text.tsx | 12 +- .../__tests__/use-get-validators.spec.ts | 15 + .../base/form/hooks/use-get-validators.ts | 6 +- .../input-with-copy/__tests__/index.spec.tsx | 222 ---------- .../components/base/input-with-copy/index.tsx | 90 ---- .../base/input/__tests__/index.spec.tsx | 47 -- .../components/base/input/index.stories.tsx | 419 ------------------ web/app/components/base/input/index.tsx | 168 ------- .../markdown-blocks/__tests__/form.spec.tsx | 31 +- .../components/base/markdown-blocks/form.tsx | 71 ++- .../components/__tests__/inputs.spec.tsx | 176 +------- .../components/general-chunking-options.tsx | 5 +- .../create/step-two/components/inputs.tsx | 27 +- .../components/parent-child-options.tsx | 4 +- .../get-schema.tsx | 69 +-- .../edit-custom-collection-modal/test-api.tsx | 44 +- .../components/tools/workflow-tool/index.tsx | 86 ++-- .../__tests__/form-input-item.helpers.spec.ts | 7 +- .../__tests__/form-input-item.spec.tsx | 27 +- .../components/before-run-form/form-item.tsx | 35 +- .../components/form-input-item.helpers.ts | 8 - .../_base/components/form-input-item.tsx | 30 +- .../assigner/__tests__/integration.spec.tsx | 28 +- .../var-list/__tests__/branches.spec.tsx | 29 +- .../assigner/components/var-list/index.tsx | 43 +- .../http/components/authorization/index.tsx | 40 +- .../key-value/key-value-edit/item.tsx | 1 - .../nodes/tool/components/tool-form/item.tsx | 9 +- .../components/trigger-form/item.tsx | 6 +- .../trigger-webhook/__tests__/panel.spec.tsx | 90 ++-- .../workflow/nodes/trigger-webhook/panel.tsx | 102 +++-- .../panel/__tests__/inputs-panel.spec.tsx | 2 +- .../panel/env-panel/variable-modal.tsx | 142 +++--- 42 files changed, 1046 insertions(+), 1860 deletions(-) create mode 100644 web/app/account/(commonLayout)/account-page/__tests__/email-change-modal.spec.tsx delete mode 100644 web/app/components/base/form/components/field/__tests__/text.spec.tsx delete mode 100644 web/app/components/base/input-with-copy/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/input-with-copy/index.tsx delete mode 100644 web/app/components/base/input/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/input/index.stories.tsx delete mode 100644 web/app/components/base/input/index.tsx diff --git a/lint.config.ts b/lint.config.ts index a8aa394ac57..bf84dec886b 100644 --- a/lint.config.ts +++ b/lint.config.ts @@ -912,11 +912,6 @@ export const lintConfig = { message: 'Do not import Floating UI directly in web. Use @langgenius/dify-ui/* primitives instead.', }, - { - group: ['**/base/input', '**/base/input/*'], - message: - 'Do not import the deprecated web base Input. Use @langgenius/dify-ui/input for standalone inputs, and @langgenius/dify-ui/field for labelled or validated form composition.', - }, { group: [ '@/service/base', @@ -984,11 +979,6 @@ export const lintConfig = { message: 'Do not import Floating UI directly in web. Use @langgenius/dify-ui/* primitives instead.', }, - { - group: ['**/base/input', '**/base/input/*'], - message: - 'Do not import the deprecated web base Input. Use @langgenius/dify-ui/input for standalone inputs, and @langgenius/dify-ui/field for labelled or validated form composition.', - }, { group: [ '@/service/base', diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index a1d36a661e9..a8e19639158 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -100,14 +100,8 @@ } }, "web/app/account/(commonLayout)/account-page/email-change-modal.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 3 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 3 - }, "no-restricted-imports": { - "count": 2 + "count": 1 } }, "web/app/components/app/annotation/__tests__/index.spec.tsx": { @@ -965,11 +959,6 @@ "count": 1 } }, - "web/app/components/base/emoji-picker/Inner.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/error-boundary/index.tsx": { "eslint-react/jsx-no-key-after-spread": { "count": 1 @@ -1140,11 +1129,6 @@ "count": 1 } }, - "web/app/components/base/form/components/base/base-field.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/form/components/base/base-form.tsx": { "typescript/no-explicit-any": { "count": 6 @@ -1478,17 +1462,6 @@ "count": 2 } }, - "web/app/components/base/input/index.stories.tsx": { - "jsx-a11y/label-has-associated-control": { - "count": 9 - }, - "no-console": { - "count": 2 - }, - "typescript/no-explicit-any": { - "count": 1 - } - }, "web/app/components/base/markdown-blocks/__tests__/paragraph.spec.tsx": { "jsx-a11y/anchor-is-valid": { "count": 1 @@ -1505,9 +1478,6 @@ } }, "web/app/components/base/markdown-blocks/form.tsx": { - "no-restricted-imports": { - "count": 1 - }, "prefer-regex-literals": { "count": 1 } @@ -2048,11 +2018,6 @@ "count": 5 } }, - "web/app/components/datasets/create/step-two/components/inputs.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, "web/app/components/datasets/create/step-two/components/option-card.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -3248,11 +3213,6 @@ "count": 2 } }, - "web/app/components/tools/edit-custom-collection-modal/get-schema.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/tools/edit-custom-collection-modal/index.tsx": { "eslint-react/set-state-in-effect": { "count": 4 @@ -3262,15 +3222,6 @@ } }, "web/app/components/tools/edit-custom-collection-modal/test-api.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - }, "typescript/no-explicit-any": { "count": 1 } @@ -3293,11 +3244,6 @@ "count": 4 } }, - "web/app/components/tools/workflow-tool/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow-app/components/__tests__/workflow-children.spec.tsx": { "eslint-react/static-components": { "count": 2 @@ -3517,9 +3463,6 @@ "jsx-a11y/no-autofocus": { "count": 3 }, - "no-restricted-imports": { - "count": 1 - }, "typescript/no-explicit-any": { "count": 11 } @@ -3598,11 +3541,6 @@ "count": 2 } }, - "web/app/components/workflow/nodes/_base/components/form-input-item.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx": { "typescript/no-explicit-any": { "count": 1 @@ -3875,11 +3813,6 @@ "count": 15 } }, - "web/app/components/workflow/nodes/assigner/components/var-list/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/assigner/hooks.ts": { "typescript/no-explicit-any": { "count": 1 @@ -3972,11 +3905,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/http/components/authorization/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx": { "typescript/no-explicit-any": { "count": 2 @@ -4765,9 +4693,6 @@ "web/app/components/workflow/panel/env-panel/variable-modal.tsx": { "eslint-react/set-state-in-effect": { "count": 4 - }, - "no-restricted-imports": { - "count": 1 } }, "web/app/components/workflow/panel/global-variable-panel/index.tsx": { diff --git a/web/app/account/(commonLayout)/account-page/__tests__/email-change-modal.spec.tsx b/web/app/account/(commonLayout)/account-page/__tests__/email-change-modal.spec.tsx new file mode 100644 index 00000000000..dacc69eaf32 --- /dev/null +++ b/web/app/account/(commonLayout)/account-page/__tests__/email-change-modal.spec.tsx @@ -0,0 +1,105 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common' +import EmailChangeModal from '../email-change-modal' + +vi.mock('@/service/common', () => ({ + checkEmailExisted: vi.fn(), + resetEmail: vi.fn(), + sendVerifyCode: vi.fn(), + verifyEmail: vi.fn(), +})) + +vi.mock('@/service/use-common', () => ({ + useLogout: () => ({ mutateAsync: vi.fn() }), +})) + +vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) })) + +describe('EmailChangeModal', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(sendVerifyCode).mockResolvedValue({ result: 'success', data: 'email-token' }) + vi.mocked(verifyEmail).mockResolvedValue({ + result: 'success', + email: 'old@example.com', + is_valid: true, + token: 'verified-token', + }) + vi.mocked(checkEmailExisted).mockResolvedValue({ result: 'success' }) + }) + + it('submits the labeled verification fields with Enter through both email phases', async () => { + const user = userEvent.setup() + render() + + await user.click( + screen.getByRole('button', { name: 'common.account.changeEmail.sendVerifyCode' }), + ) + const originCode = await screen.findByRole('textbox', { + name: 'common.account.changeEmail.codeLabel', + }) + await user.type(originCode, '123456{Enter}') + expect(verifyEmail).toHaveBeenCalledWith({ + email: 'old@example.com', + code: '123456', + token: 'email-token', + }) + + const email = await screen.findByRole('textbox', { + name: 'common.account.changeEmail.emailLabel', + }) + await user.type(email, 'new@example.com') + await waitFor(() => + expect( + screen.getByRole('button', { name: 'common.account.changeEmail.sendVerifyCode' }), + ).toBeEnabled(), + ) + await user.type(email, '{Enter}') + expect(sendVerifyCode).toHaveBeenLastCalledWith({ + email: 'new@example.com', + phase: 'new_email', + token: 'verified-token', + }) + + const newCode = await screen.findByRole('textbox', { + name: 'common.account.changeEmail.codeLabel', + }) + expect(newCode).toHaveValue('') + await user.type(newCode, '654321{Enter}') + await waitFor(() => + expect(resetEmail).toHaveBeenCalledWith({ + new_email: 'new@example.com', + token: 'verified-token', + }), + ) + }) + + it('associates an unavailable-email error and clears it when the user edits the address', async () => { + vi.mocked(checkEmailExisted).mockRejectedValue({ + status: 400, + json: async () => ({ code: 'email_already_in_use' }), + }) + const user = userEvent.setup() + render() + + await user.click( + screen.getByRole('button', { name: 'common.account.changeEmail.sendVerifyCode' }), + ) + await user.type( + await screen.findByRole('textbox', { name: 'common.account.changeEmail.codeLabel' }), + '123456{Enter}', + ) + const email = await screen.findByRole('textbox', { + name: 'common.account.changeEmail.emailLabel', + }) + await user.type(email, 'used@example.com') + + await screen.findByText('common.account.changeEmail.existingEmail') + expect(email).toBeInvalid() + expect(email).toHaveAccessibleDescription('common.account.changeEmail.existingEmail') + await user.clear(email) + expect(email).not.toBeInvalid() + expect(screen.queryByText('common.account.changeEmail.existingEmail')).not.toBeInTheDocument() + }) +}) diff --git a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx index 968d76a36e5..391a8d36281 100644 --- a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx +++ b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx @@ -1,12 +1,14 @@ import type { ResponseError } from '@/service/fetch' import { Button } from '@langgenius/dify-ui/button' -import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' +import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' +import { IconButton } from '@langgenius/dify-ui/icon-button' +import { Input } from '@langgenius/dify-ui/input' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import { useDebounceFn } from 'ahooks' import { useCallback, useEffect, useRef, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -import Input from '@/app/components/base/input' import { useRouter } from '@/next/navigation' import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common' import { useLogout } from '@/service/use-common' @@ -224,201 +226,228 @@ const EmailChangeModal = ({ onClose, email }: Props) => { return (

!open && onClose()}> -
- -
- {step === STEP.start && ( - <> -
- {t(($) => $['account.changeEmail.title'], { ns: 'common' })} -
-
-
- {t(($) => $['account.changeEmail.authTip'], { ns: 'common' })} -
-
- $['account.changeEmail.content1']} - ns="common" - components={{ email: }} - values={{ email }} - /> -
-
-
-
- - -
- - )} - {step === STEP.verifyOrigin && ( - <> -
- {t(($) => $['account.changeEmail.verifyEmail'], { ns: 'common' })} -
-
-
- $['account.changeEmail.content2']} - ns="common" - components={{ email: }} - values={{ email }} - /> -
-
-
-
- {t(($) => $['account.changeEmail.codeLabel'], { ns: 'common' })} -
- $['account.changeEmail.codePlaceholder'], { ns: 'common' })} - value={code} - onChange={(e) => setCode(e.target.value)} - maxLength={6} - /> -
-
- - -
-
- {t(($) => $['account.changeEmail.resendTip'], { ns: 'common' })} - {time > 0 && ( - - {t(($) => $['account.changeEmail.resendCount'], { ns: 'common', count: time })} - - )} - {!time && ( - - {t(($) => $['account.changeEmail.resend'], { ns: 'common' })} - - )} -
- - )} - {step === STEP.newEmail && ( - <> -
- {t(($) => $['account.changeEmail.newEmail'], { ns: 'common' })} -
-
-
- {t(($) => $['account.changeEmail.content3'], { ns: 'common' })} -
-
-
-
- {t(($) => $['account.changeEmail.emailLabel'], { ns: 'common' })} -
- $['account.changeEmail.emailPlaceholder'], { ns: 'common' })} - value={mail} - onChange={(e) => handleNewEmailValueChange(e.target.value)} - destructive={newEmailExited || unAvailableEmail} - /> - {newEmailExited && ( -
- {t(($) => $['account.changeEmail.existingEmail'], { ns: 'common' })} +
{ + event.preventDefault() + if (step === STEP.start) void sendCodeToOriginEmail() + else if (step === STEP.verifyOrigin) void handleVerifyOriginEmail() + else if (step === STEP.newEmail) void sendCodeToNewEmail() + else void submitNewEmail() + }} + > + $['operation.close'], { ns: 'common' })} + className="absolute top-5 right-5 size-8" + onClick={onClose} + > + + + {step === STEP.start && ( + <> + + {t(($) => $['account.changeEmail.title'], { ns: 'common' })} + +
+
+ {t(($) => $['account.changeEmail.authTip'], { ns: 'common' })}
- )} - {unAvailableEmail && ( -
- {t(($) => $['account.changeEmail.unAvailableEmail'], { ns: 'common' })} +
+ $['account.changeEmail.content1']} + ns="common" + components={{ + email: , + }} + values={{ email }} + />
- )} -
-
- - -
- - )} - {step === STEP.verifyNew && ( - <> -
- {t(($) => $['account.changeEmail.verifyNew'], { ns: 'common' })} -
-
-
- $['account.changeEmail.content4']} - ns="common" - components={{ email: }} - values={{ email: mail }} +
+
+
+ + +
+ + )} + {step === STEP.verifyOrigin && ( + <> + + {t(($) => $['account.changeEmail.verifyEmail'], { ns: 'common' })} + +
+
+ $['account.changeEmail.content2']} + ns="common" + components={{ + email: , + }} + values={{ email }} + /> +
+
+ + + {t(($) => $['account.changeEmail.codeLabel'], { ns: 'common' })} + + $['account.changeEmail.codePlaceholder'], { ns: 'common' })} + value={code} + onValueChange={(value) => setCode(value)} + autoComplete="one-time-code" + maxLength={6} /> -
-
-
-
- {t(($) => $['account.changeEmail.codeLabel'], { ns: 'common' })} -
- $['account.changeEmail.codePlaceholder'], { ns: 'common' })} - value={code} - onChange={(e) => setCode(e.target.value)} - maxLength={6} - /> -
-
- - -
-
- {t(($) => $['account.changeEmail.resendTip'], { ns: 'common' })} - {time > 0 && ( - - {t(($) => $['account.changeEmail.resendCount'], { ns: 'common', count: time })} - - )} - {!time && ( - +
+
- - )} + {t(($) => $['account.changeEmail.continue'], { ns: 'common' })} + + +
+
+ {t(($) => $['account.changeEmail.resendTip'], { ns: 'common' })} + {time > 0 && ( + + {t(($) => $['account.changeEmail.resendCount'], { ns: 'common', count: time })} + + )} + {!time && ( + + )} +
+ + )} + {step === STEP.newEmail && ( + <> + + {t(($) => $['account.changeEmail.newEmail'], { ns: 'common' })} + +
+
+ {t(($) => $['account.changeEmail.content3'], { ns: 'common' })} +
+
+ + + {t(($) => $['account.changeEmail.emailLabel'], { ns: 'common' })} + + $['account.changeEmail.emailPlaceholder'], { + ns: 'common', + })} + value={mail} + onValueChange={(value) => handleNewEmailValueChange(value)} + type="email" + autoComplete="email" + /> + {newEmailExited && ( + + {t(($) => $['account.changeEmail.existingEmail'], { ns: 'common' })} + + )} + {unAvailableEmail && ( + + {t(($) => $['account.changeEmail.unAvailableEmail'], { ns: 'common' })} + + )} + +
+ + +
+ + )} + {step === STEP.verifyNew && ( + <> + + {t(($) => $['account.changeEmail.verifyNew'], { ns: 'common' })} + +
+
+ $['account.changeEmail.content4']} + ns="common" + components={{ + email: , + }} + values={{ email: mail }} + /> +
+
+ + + {t(($) => $['account.changeEmail.codeLabel'], { ns: 'common' })} + + $['account.changeEmail.codePlaceholder'], { ns: 'common' })} + value={code} + onValueChange={(value) => setCode(value)} + autoComplete="one-time-code" + maxLength={6} + /> + +
+ + +
+
+ {t(($) => $['account.changeEmail.resendTip'], { ns: 'common' })} + {time > 0 && ( + + {t(($) => $['account.changeEmail.resendCount'], { ns: 'common', count: time })} + + )} + {!time && ( + + )} +
+ + )} +
) diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index f30319045e7..c23c93ded0f 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -1,14 +1,14 @@ 'use client' import type { EmojiMartData } from '@emoji-mart/data' -import type { ChangeEvent } from 'react' import data from '@emoji-mart/data' import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' import { cn } from '@langgenius/dify-ui/cn' +import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' import { init } from 'emoji-mart' import * as React from 'react' import { useState } from 'react' +import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import Input from '@/app/components/base/input' import { searchEmoji } from '@/utils/emoji' import { backgroundColors, defaultEmojiBackground } from './constants' @@ -22,6 +22,7 @@ type IEmojiPickerInnerProps = { } function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPickerInnerProps) { + const { t } = useTranslation() const { categories } = data as EmojiMartData const [selectedEmoji, setSelectedEmoji] = useState(emoji || '') const [selectedBackground, setSelectedBackground] = useState(background || defaultEmojiBackground) @@ -45,26 +46,25 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick return (
-
-
-
- + $['operation.search'], { ns: 'common' })} placeholder="Search emojis..." - onChange={async (e: ChangeEvent) => { - if (e.target.value === '') { + onValueChange={async (value) => { + if (value === '') { setIsSearching(false) } else { setIsSearching(true) - const emojis = await searchEmoji(e.target.value) + const emojis = await searchEmoji(value) setSearchedEmojis(emojis) } }} /> -
+ + +
diff --git a/web/app/components/base/form/__tests__/index.spec.tsx b/web/app/components/base/form/__tests__/index.spec.tsx index 11283fdb979..57c3a9d747c 100644 --- a/web/app/components/base/form/__tests__/index.spec.tsx +++ b/web/app/components/base/form/__tests__/index.spec.tsx @@ -1,4 +1,5 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { useAppForm, withForm } from '../index' const FormHarness = ({ onSubmit }: { onSubmit: (value: Record) => void }) => { @@ -8,12 +9,15 @@ const FormHarness = ({ onSubmit }: { onSubmit: (value: Record) }) return ( -
+ { + event.preventDefault() + void form.handleSubmit() + }} + > } /> - + ) @@ -40,12 +44,16 @@ const WithFormHarness = () => { describe('form index exports', () => { it('should submit values through the generated app form', async () => { const onSubmit = vi.fn() + const user = userEvent.setup() render() - fireEvent.click(screen.getByRole('button', { name: /submit/i })) + const input = screen.getByRole('textbox', { name: 'Title' }) + await user.clear(input) + await user.type(input, 'Updated title') + await user.click(screen.getByRole('button', { name: /submit/i })) await waitFor(() => { - expect(onSubmit).toHaveBeenCalledWith({ title: 'Initial title' }) + expect(onSubmit).toHaveBeenCalledWith({ title: 'Updated title' }) }) }) diff --git a/web/app/components/base/form/components/base/__tests__/base-field.spec.tsx b/web/app/components/base/form/components/base/__tests__/base-field.spec.tsx index 169fb6f6d56..d448a797b65 100644 --- a/web/app/components/base/form/components/base/__tests__/base-field.spec.tsx +++ b/web/app/components/base/form/components/base/__tests__/base-field.spec.tsx @@ -344,7 +344,9 @@ describe('BaseField', () => { expect(screen.getByDisplayValue('abc')).toHaveAttribute('type', 'password') }) - it('should render number input with number type', () => { + it('keeps numeric edits, zero and an empty value distinct at the form boundary', async () => { + const user = userEvent.setup() + const onChange = vi.fn() renderBaseField({ formSchema: { type: FormTypeEnum.textNumber, @@ -353,9 +355,22 @@ describe('BaseField', () => { required: false, }, defaultValues: { count: 7 }, + onChange, }) - expect(screen.getByDisplayValue('7')).toHaveAttribute('type', 'number') + const input = screen.getByRole('textbox', { name: 'Count' }) + expect(input).toHaveValue('7') + await user.clear(input) + await user.type(input, '3.5') + await user.tab() + expect(onChange).toHaveBeenLastCalledWith('count', 3.5) + await user.clear(input) + await user.type(input, '0') + await user.tab() + expect(onChange).toHaveBeenLastCalledWith('count', 0) + await user.clear(input) + await user.tab() + expect(onChange).toHaveBeenLastCalledWith('count', null) }) it('should render translated object label content', () => { diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index 231053e78db..7e7c2383d90 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -3,6 +3,9 @@ import type { FieldState, FormSchema, TypeWithI18N } from '@/app/components/base import { cn } from '@langgenius/dify-ui/cn' import { Field, FieldItem, FieldLabel } from '@langgenius/dify-ui/field' import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset' +import { Input } from '@langgenius/dify-ui/input' +import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' +import { NumberField, NumberFieldGroup, NumberFieldInput } from '@langgenius/dify-ui/number-field' import { Radio, RadioGroup } from '@langgenius/dify-ui/radio-group' import { Select, @@ -18,9 +21,9 @@ import { useStore } from '@tanstack/react-form' import { isValidElement, memo, useCallback, useId, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { CheckboxList } from '@/app/components/base/checkbox-list' +import { CopyFeedback } from '@/app/components/base/copy-feedback' import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/form/types' import { Infotip } from '@/app/components/base/infotip' -import Input from '@/app/components/base/input' import { useRenderI18nObject } from '@/hooks/use-i18n' import { useTriggerPluginDynamicOptions } from '@/service/use-triggers' @@ -28,8 +31,6 @@ const getExtraProps = (type: FormTypeEnum) => { switch (type) { case FormTypeEnum.secretInput: return { type: 'password', autoComplete: 'new-password' } - case FormTypeEnum.textNumber: - return { type: 'number' } default: return { type: 'text' } } @@ -85,7 +86,7 @@ const VALIDATE_STATUS_STYLE_MAP: Record< }, [FormItemValidateStatusEnum.Warning]: { componentClassName: - 'border-components-input-border-warning focus:border-components-input-border-warning', + 'border-components-input-border-warning focus:border-components-input-border-warning has-[input:focus]:border-components-input-border-warning', textClassName: 'text-text-warning', infoFieldName: 'warnings', }, @@ -273,6 +274,17 @@ const BaseField = ({ const selectPlaceholder = isDynamicSelect ? dynamicPlaceholder : translatedPlaceholder const handleSelectChange = isDynamicSelect ? field.handleChange : handleChange const selectDisabled = disabled || (isDynamicSelect && isDynamicOptionsLoading) + const textInputProps = { + id: controlId, + name: field.name, + ...controlProps, + value: stringValue ?? '', + onValueChange: handleChange, + onBlur: field.handleBlur, + disabled, + placeholder: translatedPlaceholder, + ...getExtraProps(formItemType), + } const content = ( <> @@ -301,28 +313,74 @@ const BaseField = ({ )}
- {[FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes( - formItemType, - ) && ( - + {showCopy ? ( + + + + + + + ) : ( + )} - value={value || ''} - onChange={(e) => { - handleChange(e.target.value) - }} - onBlur={field.handleBlur} - disabled={disabled} - placeholder={translatedPlaceholder} - {...getExtraProps(formItemType)} - showCopyIcon={showCopy} - /> + + )} + {formItemType === FormTypeEnum.textNumber && ( + + handleChange(value)} + > + + + {showCopy && ( + + )} + + + )} {isSelect && ( <> diff --git a/web/app/components/base/form/components/field/__tests__/text.spec.tsx b/web/app/components/base/form/components/field/__tests__/text.spec.tsx deleted file mode 100644 index 5ebc82e8586..00000000000 --- a/web/app/components/base/form/components/field/__tests__/text.spec.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import TextField from '../text' - -const mockField = { - name: 'text-field', - state: { - value: 'Initial text', - }, - handleChange: vi.fn(), - handleBlur: vi.fn(), -} - -vi.mock('../../..', () => ({ - useFieldContext: () => mockField, -})) - -describe('TextField', () => { - beforeEach(() => { - vi.clearAllMocks() - mockField.state.value = 'Initial text' - }) - - it('should render current value', () => { - render() - expect(screen.getByLabelText('Name')).toHaveValue('Initial text') - }) - - it('should update value when users type', () => { - render() - fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Updated text' } }) - expect(mockField.handleChange).toHaveBeenCalledWith('Updated text') - }) -}) diff --git a/web/app/components/base/form/components/field/text.tsx b/web/app/components/base/form/components/field/text.tsx index 28a4919cee6..6ec2592cb88 100644 --- a/web/app/components/base/form/components/field/text.tsx +++ b/web/app/components/base/form/components/field/text.tsx @@ -1,16 +1,19 @@ -import type { InputProps } from '../../../input' +import type { InputProps } from '@langgenius/dify-ui/input' import type { LabelProps } from '../label' import { cn } from '@langgenius/dify-ui/cn' +import { Input } from '@langgenius/dify-ui/input' import * as React from 'react' import { useFieldContext } from '../..' -import Input from '../../../input' import Label from '../label' type TextFieldProps = { label: string labelOptions?: Omit className?: string -} & Omit +} & Omit< + InputProps, + 'className' | 'onChange' | 'onValueChange' | 'onBlur' | 'value' | 'defaultValue' | 'id' | 'name' +> const TextField = ({ label, labelOptions, className, ...inputProps }: TextFieldProps) => { const field = useFieldContext() @@ -20,8 +23,9 @@ const TextField = ({ label, labelOptions, className, ...inputProps }: TextFieldP