diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml
index d2b892d9aa..4ce121ba60 100644
--- a/.github/workflows/style.yml
+++ b/.github/workflows/style.yml
@@ -109,6 +109,8 @@ jobs:
- name: Web tsslint
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
+ env:
+ NODE_OPTIONS: --max-old-space-size=4096
run: vp run lint:tss
- name: Web type check
diff --git a/README.md b/README.md
index 778028fc76..e6f8d84931 100644
--- a/README.md
+++ b/README.md
@@ -76,10 +76,11 @@ The easiest way to start the Dify server is through [Docker Compose](docker/dock
```bash
cd dify
cd docker
-cp .env.example .env
-docker compose up -d
+./dify-compose up -d
```
+On Windows PowerShell, run `.\dify-compose.ps1 up -d` from the `docker` directory.
+
After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization process.
#### Seeking help
@@ -137,7 +138,7 @@ Star Dify on GitHub and be instantly notified of new releases.
### Custom configurations
-If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
+If you need to customize the configuration, add only the values you want to override to `docker/.env`. The default values live in [`docker/.env.default`](docker/.env.default), and the full reference remains in [`docker/.env.example`](docker/.env.example). After making any changes, re-run `./dify-compose up -d` or `.\dify-compose.ps1 up -d` from the `docker` directory. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
### Metrics Monitoring with Grafana
diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py
index e32ba5f66c..c688a69074 100644
--- a/api/controllers/console/app/workflow_draft_variable.py
+++ b/api/controllers/console/app/workflow_draft_variable.py
@@ -75,14 +75,15 @@ console_ns.schema_model(
def _convert_values_to_json_serializable_object(value: Segment):
- if isinstance(value, FileSegment):
- return value.value.model_dump()
- elif isinstance(value, ArrayFileSegment):
- return [i.model_dump() for i in value.value]
- elif isinstance(value, SegmentGroup):
- return [_convert_values_to_json_serializable_object(i) for i in value.value]
- else:
- return value.value
+ match value:
+ case FileSegment():
+ return value.value.model_dump()
+ case ArrayFileSegment():
+ return [i.model_dump() for i in value.value]
+ case SegmentGroup():
+ return [_convert_values_to_json_serializable_object(i) for i in value.value]
+ case _:
+ return value.value
def _serialize_var_value(variable: WorkflowDraftVariable):
diff --git a/api/libs/typing.py b/api/libs/typing.py
deleted file mode 100644
index f84e9911e0..0000000000
--- a/api/libs/typing.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from typing import TypeGuard
-
-
-def is_str_dict(v: object) -> TypeGuard[dict[str, object]]:
- return isinstance(v, dict)
-
-
-def is_str(v: object) -> TypeGuard[str]:
- return isinstance(v, str)
diff --git a/api/tests/test_containers_integration_tests/controllers/console/auth/test_oauth.py b/api/tests/test_containers_integration_tests/controllers/console/auth/test_oauth.py
index 01d88d247c..55b6a919d8 100644
--- a/api/tests/test_containers_integration_tests/controllers/console/auth/test_oauth.py
+++ b/api/tests/test_containers_integration_tests/controllers/console/auth/test_oauth.py
@@ -90,7 +90,7 @@ class TestOAuthLogin:
mock_redirect,
mock_get_providers,
resource,
- app,
+ app: Flask,
mock_oauth_provider,
invite_token,
expected_token,
@@ -165,7 +165,7 @@ class TestOAuthCallback:
mock_get_providers,
mock_config,
resource,
- app,
+ app: Flask,
oauth_setup,
):
mock_config.CONSOLE_WEB_URL = "http://localhost:3000"
@@ -218,7 +218,7 @@ class TestOAuthCallback:
mock_get_providers,
mock_config,
resource,
- app,
+ app: Flask,
oauth_setup,
):
mock_config.CONSOLE_WEB_URL = "http://localhost:3000"
@@ -262,7 +262,7 @@ class TestOAuthCallback:
mock_tenant_service,
mock_account_service,
resource,
- app,
+ app: Flask,
oauth_setup,
account_status,
expected_redirect,
@@ -301,7 +301,7 @@ class TestOAuthCallback:
mock_get_providers,
mock_config,
resource,
- app,
+ app: Flask,
oauth_setup,
):
mock_get_providers.return_value = {"github": oauth_setup["provider"]}
@@ -337,7 +337,7 @@ class TestOAuthCallback:
mock_get_providers,
mock_config,
resource,
- app,
+ app: Flask,
oauth_setup,
):
"""Defensive test for CLOSED account status handling in OAuth callback.
@@ -466,7 +466,7 @@ class TestAccountGeneration:
mock_register_service,
mock_feature_service,
mock_get_account,
- app,
+ app: Flask,
user_info,
mock_account,
allow_register,
@@ -505,7 +505,7 @@ class TestAccountGeneration:
mock_register_service,
mock_feature_service,
mock_get_account,
- app,
+ app: Flask,
):
user_info = OAuthUserInfo(id="123", name="Test User", email="Upper@Example.com")
mock_feature_service.get_system_features.return_value.is_allow_register = True
@@ -530,7 +530,7 @@ class TestAccountGeneration:
mock_feature_service,
mock_tenant_service,
mock_get_account,
- app,
+ app: Flask,
user_info,
mock_account,
):
diff --git a/api/tests/test_containers_integration_tests/controllers/console/auth/test_password_reset.py b/api/tests/test_containers_integration_tests/controllers/console/auth/test_password_reset.py
index 8d6b25b5b3..d017e8f2bd 100644
--- a/api/tests/test_containers_integration_tests/controllers/console/auth/test_password_reset.py
+++ b/api/tests/test_containers_integration_tests/controllers/console/auth/test_password_reset.py
@@ -47,7 +47,7 @@ class TestForgotPasswordSendEmailApi:
mock_send_email,
mock_get_account,
mock_is_ip_limit,
- app,
+ app: Flask,
mock_account,
):
# Arrange
@@ -105,7 +105,7 @@ class TestForgotPasswordSendEmailApi:
mock_send_email,
mock_get_account,
mock_is_ip_limit,
- app,
+ app: Flask,
mock_account,
language_input,
expected_language,
@@ -154,7 +154,7 @@ class TestForgotPasswordCheckApi:
mock_revoke_token,
mock_get_data,
mock_is_rate_limit,
- app,
+ app: Flask,
):
"""
Test successful verification code validation.
@@ -201,7 +201,7 @@ class TestForgotPasswordCheckApi:
mock_revoke_token,
mock_get_data,
mock_is_rate_limit,
- app,
+ app: Flask,
):
mock_is_rate_limit.return_value = False
mock_get_data.return_value = {"email": "User@Example.com", "code": "999888"}
@@ -345,7 +345,7 @@ class TestForgotPasswordResetApi:
mock_get_account,
mock_revoke_token,
mock_get_data,
- app,
+ app: Flask,
mock_account,
):
"""
diff --git a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py
index 2752e6b34f..7aa4aff1cc 100644
--- a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py
+++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py
@@ -30,7 +30,7 @@ class TestPipelineTemplateListApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = PipelineTemplateListApi()
method = unwrap(api.get)
@@ -54,7 +54,7 @@ class TestPipelineTemplateDetailApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = PipelineTemplateDetailApi()
method = unwrap(api.get)
@@ -75,7 +75,7 @@ class TestPipelineTemplateDetailApi:
assert status == 200
assert response == template
- def test_get_returns_404_when_template_not_found(self, app):
+ def test_get_returns_404_when_template_not_found(self, app: Flask):
api = PipelineTemplateDetailApi()
method = unwrap(api.get)
@@ -94,7 +94,7 @@ class TestPipelineTemplateDetailApi:
assert status == 404
assert "error" in response
- def test_get_returns_404_for_customized_type_not_found(self, app):
+ def test_get_returns_404_for_customized_type_not_found(self, app: Flask):
api = PipelineTemplateDetailApi()
method = unwrap(api.get)
@@ -119,7 +119,7 @@ class TestCustomizedPipelineTemplateApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_patch_success(self, app):
+ def test_patch_success(self, app: Flask):
api = CustomizedPipelineTemplateApi()
method = unwrap(api.patch)
@@ -141,7 +141,7 @@ class TestCustomizedPipelineTemplateApi:
update_mock.assert_called_once()
assert response == 200
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = CustomizedPipelineTemplateApi()
method = unwrap(api.delete)
@@ -156,7 +156,7 @@ class TestCustomizedPipelineTemplateApi:
delete_mock.assert_called_once_with("tpl-1")
assert response == 200
- def test_post_success(self, app, db_session_with_containers: Session):
+ def test_post_success(self, app: Flask, db_session_with_containers: Session):
api = CustomizedPipelineTemplateApi()
method = unwrap(api.post)
@@ -183,7 +183,7 @@ class TestCustomizedPipelineTemplateApi:
assert status == 200
assert response == {"data": "yaml-data"}
- def test_post_template_not_found(self, app):
+ def test_post_template_not_found(self, app: Flask):
api = CustomizedPipelineTemplateApi()
method = unwrap(api.post)
@@ -197,7 +197,7 @@ class TestPublishCustomizedPipelineTemplateApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = PublishCustomizedPipelineTemplateApi()
method = unwrap(api.post)
diff --git a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py
index f238ca13ee..44eb5c336c 100644
--- a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py
+++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py
@@ -36,7 +36,7 @@ class TestRagPipelineImportApi:
"name": "Test",
}
- def test_post_success_200(self, app):
+ def test_post_success_200(self, app: Flask):
api = RagPipelineImportApi()
method = unwrap(api.post)
@@ -66,7 +66,7 @@ class TestRagPipelineImportApi:
assert status == 200
assert response == {"status": "success"}
- def test_post_failed_400(self, app):
+ def test_post_failed_400(self, app: Flask):
api = RagPipelineImportApi()
method = unwrap(api.post)
@@ -96,7 +96,7 @@ class TestRagPipelineImportApi:
assert status == 400
assert response == {"status": "failed"}
- def test_post_pending_202(self, app):
+ def test_post_pending_202(self, app: Flask):
api = RagPipelineImportApi()
method = unwrap(api.post)
@@ -132,7 +132,7 @@ class TestRagPipelineImportConfirmApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_confirm_success(self, app):
+ def test_confirm_success(self, app: Flask):
api = RagPipelineImportConfirmApi()
method = unwrap(api.post)
@@ -160,7 +160,7 @@ class TestRagPipelineImportConfirmApi:
assert status == 200
assert response == {"ok": True}
- def test_confirm_failed(self, app):
+ def test_confirm_failed(self, app: Flask):
api = RagPipelineImportConfirmApi()
method = unwrap(api.post)
@@ -194,7 +194,7 @@ class TestRagPipelineImportCheckDependenciesApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = RagPipelineImportCheckDependenciesApi()
method = unwrap(api.get)
@@ -223,7 +223,7 @@ class TestRagPipelineExportApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_with_include_secret(self, app):
+ def test_get_with_include_secret(self, app: Flask):
api = RagPipelineExportApi()
method = unwrap(api.get)
diff --git a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py
index 1fdb3057b8..c17a83cad3 100644
--- a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py
+++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py
@@ -391,7 +391,7 @@ class TestPublishedPipelineApis:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_publish_success(self, app, db_session_with_containers: Session):
+ def test_publish_success(self, app: Flask, db_session_with_containers: Session):
from models.dataset import Pipeline
api = PublishedRagPipelineApi()
diff --git a/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py
index 50ad92afa1..b59009f7c4 100644
--- a/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py
+++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py
@@ -55,7 +55,7 @@ class TestDataSourceApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_success(self, app, patch_tenant):
+ def test_get_success(self, app: Flask, patch_tenant):
api = DataSourceApi()
method = unwrap(api.get)
@@ -79,7 +79,7 @@ class TestDataSourceApi:
assert status == 200
assert response["data"][0]["is_bound"] is True
- def test_get_no_bindings(self, app, patch_tenant):
+ def test_get_no_bindings(self, app: Flask, patch_tenant):
api = DataSourceApi()
method = unwrap(api.get)
@@ -95,7 +95,7 @@ class TestDataSourceApi:
assert status == 200
assert response["data"] == []
- def test_patch_enable_binding(self, app, patch_tenant, mock_engine):
+ def test_patch_enable_binding(self, app: Flask, patch_tenant, mock_engine):
api = DataSourceApi()
method = unwrap(api.patch)
@@ -116,7 +116,7 @@ class TestDataSourceApi:
assert status == 200
assert binding.disabled is False
- def test_patch_disable_binding(self, app, patch_tenant, mock_engine):
+ def test_patch_disable_binding(self, app: Flask, patch_tenant, mock_engine):
api = DataSourceApi()
method = unwrap(api.patch)
@@ -137,7 +137,7 @@ class TestDataSourceApi:
assert status == 200
assert binding.disabled is True
- def test_patch_binding_not_found(self, app, patch_tenant, mock_engine):
+ def test_patch_binding_not_found(self, app: Flask, patch_tenant, mock_engine):
api = DataSourceApi()
method = unwrap(api.patch)
@@ -152,7 +152,7 @@ class TestDataSourceApi:
with pytest.raises(NotFound):
method(api, "b1", "enable")
- def test_patch_enable_already_enabled(self, app, patch_tenant, mock_engine):
+ def test_patch_enable_already_enabled(self, app: Flask, patch_tenant, mock_engine):
api = DataSourceApi()
method = unwrap(api.patch)
@@ -169,7 +169,7 @@ class TestDataSourceApi:
with pytest.raises(ValueError):
method(api, "b1", "enable")
- def test_patch_disable_already_disabled(self, app, patch_tenant, mock_engine):
+ def test_patch_disable_already_disabled(self, app: Flask, patch_tenant, mock_engine):
api = DataSourceApi()
method = unwrap(api.patch)
@@ -192,7 +192,7 @@ class TestDataSourceNotionListApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_credential_not_found(self, app, patch_tenant):
+ def test_get_credential_not_found(self, app: Flask, patch_tenant):
api = DataSourceNotionListApi()
method = unwrap(api.get)
@@ -206,7 +206,7 @@ class TestDataSourceNotionListApi:
with pytest.raises(NotFound):
method(api)
- def test_get_success_no_dataset_id(self, app, patch_tenant, mock_engine):
+ def test_get_success_no_dataset_id(self, app: Flask, patch_tenant, mock_engine):
api = DataSourceNotionListApi()
method = unwrap(api.get)
@@ -247,7 +247,7 @@ class TestDataSourceNotionListApi:
assert status == 200
- def test_get_success_with_dataset_id(self, app, patch_tenant, mock_engine):
+ def test_get_success_with_dataset_id(self, app: Flask, patch_tenant, mock_engine):
api = DataSourceNotionListApi()
method = unwrap(api.get)
@@ -300,7 +300,7 @@ class TestDataSourceNotionListApi:
assert status == 200
- def test_get_invalid_dataset_type(self, app, patch_tenant, mock_engine):
+ def test_get_invalid_dataset_type(self, app: Flask, patch_tenant, mock_engine):
api = DataSourceNotionListApi()
method = unwrap(api.get)
@@ -327,7 +327,7 @@ class TestDataSourceNotionApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_preview_success(self, app, patch_tenant):
+ def test_get_preview_success(self, app: Flask, patch_tenant):
api = DataSourceNotionApi()
method = unwrap(api.get)
@@ -348,7 +348,7 @@ class TestDataSourceNotionApi:
assert status == 200
- def test_post_indexing_estimate_success(self, app, patch_tenant):
+ def test_post_indexing_estimate_success(self, app: Flask, patch_tenant):
api = DataSourceNotionApi()
method = unwrap(api.post)
@@ -385,7 +385,7 @@ class TestDataSourceNotionDatasetSyncApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_success(self, app, patch_tenant):
+ def test_get_success(self, app: Flask, patch_tenant):
api = DataSourceNotionDatasetSyncApi()
method = unwrap(api.get)
@@ -408,7 +408,7 @@ class TestDataSourceNotionDatasetSyncApi:
assert status == 200
- def test_get_dataset_not_found(self, app, patch_tenant):
+ def test_get_dataset_not_found(self, app: Flask, patch_tenant):
api = DataSourceNotionDatasetSyncApi()
method = unwrap(api.get)
@@ -428,7 +428,7 @@ class TestDataSourceNotionDocumentSyncApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_success(self, app, patch_tenant):
+ def test_get_success(self, app: Flask, patch_tenant):
api = DataSourceNotionDocumentSyncApi()
method = unwrap(api.get)
@@ -451,7 +451,7 @@ class TestDataSourceNotionDocumentSyncApi:
assert status == 200
- def test_get_document_not_found(self, app, patch_tenant):
+ def test_get_document_not_found(self, app: Flask, patch_tenant):
api = DataSourceNotionDocumentSyncApi()
method = unwrap(api.get)
diff --git a/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py b/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py
index 0b53ca5585..917aa35fe6 100644
--- a/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py
+++ b/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py
@@ -57,7 +57,7 @@ class TestConversationListApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_success(self, app, chat_app, user):
+ def test_get_success(self, app: Flask, chat_app, user):
api = conversation_module.ConversationListApi()
method = unwrap(api.get)
@@ -82,7 +82,7 @@ class TestConversationListApi:
assert result["has_more"] is False
assert len(result["data"]) == 2
- def test_last_conversation_not_exists(self, app, chat_app, user):
+ def test_last_conversation_not_exists(self, app: Flask, chat_app, user):
api = conversation_module.ConversationListApi()
method = unwrap(api.get)
@@ -98,7 +98,7 @@ class TestConversationListApi:
with pytest.raises(NotFound):
method(chat_app)
- def test_wrong_app_mode(self, app, non_chat_app):
+ def test_wrong_app_mode(self, app: Flask, non_chat_app):
api = conversation_module.ConversationListApi()
method = unwrap(api.get)
@@ -112,7 +112,7 @@ class TestConversationApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_delete_success(self, app, chat_app, user):
+ def test_delete_success(self, app: Flask, chat_app, user):
api = conversation_module.ConversationApi()
method = unwrap(api.delete)
@@ -130,7 +130,7 @@ class TestConversationApi:
assert status == 204
assert body["result"] == "success"
- def test_delete_not_found(self, app, chat_app, user):
+ def test_delete_not_found(self, app: Flask, chat_app, user):
api = conversation_module.ConversationApi()
method = unwrap(api.delete)
@@ -146,7 +146,7 @@ class TestConversationApi:
with pytest.raises(NotFound):
method(chat_app, "cid")
- def test_delete_wrong_app_mode(self, app, non_chat_app):
+ def test_delete_wrong_app_mode(self, app: Flask, non_chat_app):
api = conversation_module.ConversationApi()
method = unwrap(api.delete)
@@ -160,7 +160,7 @@ class TestConversationRenameApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_rename_success(self, app, chat_app, user):
+ def test_rename_success(self, app: Flask, chat_app, user):
api = conversation_module.ConversationRenameApi()
method = unwrap(api.post)
@@ -179,7 +179,7 @@ class TestConversationRenameApi:
assert result["id"] == "cid"
- def test_rename_not_found(self, app, chat_app, user):
+ def test_rename_not_found(self, app: Flask, chat_app, user):
api = conversation_module.ConversationRenameApi()
method = unwrap(api.post)
@@ -201,7 +201,7 @@ class TestConversationPinApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_pin_success(self, app, chat_app, user):
+ def test_pin_success(self, app: Flask, chat_app, user):
api = conversation_module.ConversationPinApi()
method = unwrap(api.patch)
@@ -223,7 +223,7 @@ class TestConversationUnPinApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_unpin_success(self, app, chat_app, user):
+ def test_unpin_success(self, app: Flask, chat_app, user):
api = conversation_module.ConversationUnPinApi()
method = unwrap(api.patch)
diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py
index 6efdaf2943..e41adccf3c 100644
--- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py
+++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py
@@ -49,7 +49,7 @@ class TestTriggerProviderApis:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_icon_success(self, app):
+ def test_icon_success(self, app: Flask):
api = TriggerProviderIconApi()
method = unwrap(api.get)
@@ -63,7 +63,7 @@ class TestTriggerProviderApis:
):
assert method(api, "github") == "icon"
- def test_list_providers(self, app):
+ def test_list_providers(self, app: Flask):
api = TriggerProviderListApi()
method = unwrap(api.get)
@@ -77,7 +77,7 @@ class TestTriggerProviderApis:
):
assert method(api) == []
- def test_provider_info(self, app):
+ def test_provider_info(self, app: Flask):
api = TriggerProviderInfoApi()
method = unwrap(api.get)
@@ -97,7 +97,7 @@ class TestTriggerSubscriptionListApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_list_success(self, app):
+ def test_list_success(self, app: Flask):
api = TriggerSubscriptionListApi()
method = unwrap(api.get)
@@ -111,7 +111,7 @@ class TestTriggerSubscriptionListApi:
):
assert method(api, "github") == []
- def test_list_invalid_provider(self, app):
+ def test_list_invalid_provider(self, app: Flask):
api = TriggerSubscriptionListApi()
method = unwrap(api.get)
@@ -132,7 +132,7 @@ class TestTriggerSubscriptionBuilderApis:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_create_builder(self, app):
+ def test_create_builder(self, app: Flask):
api = TriggerSubscriptionBuilderCreateApi()
method = unwrap(api.post)
@@ -147,7 +147,7 @@ class TestTriggerSubscriptionBuilderApis:
result = method(api, "github")
assert "subscription_builder" in result
- def test_get_builder(self, app):
+ def test_get_builder(self, app: Flask):
api = TriggerSubscriptionBuilderGetApi()
method = unwrap(api.get)
@@ -160,7 +160,7 @@ class TestTriggerSubscriptionBuilderApis:
):
assert method(api, "github", "b1") == {"id": "b1"}
- def test_verify_builder(self, app):
+ def test_verify_builder(self, app: Flask):
api = TriggerSubscriptionBuilderVerifyApi()
method = unwrap(api.post)
@@ -174,7 +174,7 @@ class TestTriggerSubscriptionBuilderApis:
):
assert method(api, "github", "b1") == {"ok": True}
- def test_verify_builder_error(self, app):
+ def test_verify_builder_error(self, app: Flask):
api = TriggerSubscriptionBuilderVerifyApi()
method = unwrap(api.post)
@@ -189,7 +189,7 @@ class TestTriggerSubscriptionBuilderApis:
with pytest.raises(ValueError):
method(api, "github", "b1")
- def test_update_builder(self, app):
+ def test_update_builder(self, app: Flask):
api = TriggerSubscriptionBuilderUpdateApi()
method = unwrap(api.post)
@@ -203,7 +203,7 @@ class TestTriggerSubscriptionBuilderApis:
):
assert method(api, "github", "b1") == {"id": "b1"}
- def test_logs(self, app):
+ def test_logs(self, app: Flask):
api = TriggerSubscriptionBuilderLogsApi()
method = unwrap(api.get)
@@ -220,7 +220,7 @@ class TestTriggerSubscriptionBuilderApis:
):
assert "logs" in method(api, "github", "b1")
- def test_build(self, app):
+ def test_build(self, app: Flask):
api = TriggerSubscriptionBuilderBuildApi()
method = unwrap(api.post)
@@ -240,7 +240,7 @@ class TestTriggerSubscriptionCrud:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_update_rename_only(self, app):
+ def test_update_rename_only(self, app: Flask):
api = TriggerSubscriptionUpdateApi()
method = unwrap(api.post)
@@ -259,7 +259,7 @@ class TestTriggerSubscriptionCrud:
):
assert method(api, "s1") == 200
- def test_update_not_found(self, app):
+ def test_update_not_found(self, app: Flask):
api = TriggerSubscriptionUpdateApi()
method = unwrap(api.post)
@@ -274,7 +274,7 @@ class TestTriggerSubscriptionCrud:
with pytest.raises(NotFoundError):
method(api, "x")
- def test_update_rebuild(self, app):
+ def test_update_rebuild(self, app: Flask):
api = TriggerSubscriptionUpdateApi()
method = unwrap(api.post)
@@ -297,7 +297,7 @@ class TestTriggerSubscriptionCrud:
):
assert method(api, "s1") == 200
- def test_delete_subscription(self, app):
+ def test_delete_subscription(self, app: Flask):
api = TriggerSubscriptionDeleteApi()
method = unwrap(api.post)
@@ -320,7 +320,7 @@ class TestTriggerSubscriptionCrud:
assert result["result"] == "success"
- def test_delete_subscription_value_error(self, app):
+ def test_delete_subscription_value_error(self, app: Flask):
api = TriggerSubscriptionDeleteApi()
method = unwrap(api.post)
@@ -346,7 +346,7 @@ class TestTriggerOAuthApis:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_oauth_authorize_success(self, app):
+ def test_oauth_authorize_success(self, app: Flask):
api = TriggerOAuthAuthorizeApi()
method = unwrap(api.get)
@@ -373,7 +373,7 @@ class TestTriggerOAuthApis:
resp = method(api, "github")
assert resp.status_code == 200
- def test_oauth_authorize_no_client(self, app):
+ def test_oauth_authorize_no_client(self, app: Flask):
api = TriggerOAuthAuthorizeApi()
method = unwrap(api.get)
@@ -388,7 +388,7 @@ class TestTriggerOAuthApis:
with pytest.raises(NotFoundError):
method(api, "github")
- def test_oauth_callback_forbidden(self, app):
+ def test_oauth_callback_forbidden(self, app: Flask):
api = TriggerOAuthCallbackApi()
method = unwrap(api.get)
@@ -396,7 +396,7 @@ class TestTriggerOAuthApis:
with pytest.raises(Forbidden):
method(api, "github")
- def test_oauth_callback_success(self, app):
+ def test_oauth_callback_success(self, app: Flask):
api = TriggerOAuthCallbackApi()
method = unwrap(api.get)
@@ -426,7 +426,7 @@ class TestTriggerOAuthApis:
resp = method(api, "github")
assert resp.status_code == 302
- def test_oauth_callback_no_oauth_client(self, app):
+ def test_oauth_callback_no_oauth_client(self, app: Flask):
api = TriggerOAuthCallbackApi()
method = unwrap(api.get)
@@ -450,7 +450,7 @@ class TestTriggerOAuthApis:
with pytest.raises(Forbidden):
method(api, "github")
- def test_oauth_callback_empty_credentials(self, app):
+ def test_oauth_callback_empty_credentials(self, app: Flask):
api = TriggerOAuthCallbackApi()
method = unwrap(api.get)
@@ -484,7 +484,7 @@ class TestTriggerOAuthClientManageApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_get_client(self, app):
+ def test_get_client(self, app: Flask):
api = TriggerOAuthClientManageApi()
method = unwrap(api.get)
@@ -511,7 +511,7 @@ class TestTriggerOAuthClientManageApi:
result = method(api, "github")
assert "configured" in result
- def test_post_client(self, app):
+ def test_post_client(self, app: Flask):
api = TriggerOAuthClientManageApi()
method = unwrap(api.post)
@@ -525,7 +525,7 @@ class TestTriggerOAuthClientManageApi:
):
assert method(api, "github") == {"ok": True}
- def test_delete_client(self, app):
+ def test_delete_client(self, app: Flask):
api = TriggerOAuthClientManageApi()
method = unwrap(api.delete)
@@ -539,7 +539,7 @@ class TestTriggerOAuthClientManageApi:
):
assert method(api, "github") == {"ok": True}
- def test_oauth_client_post_value_error(self, app):
+ def test_oauth_client_post_value_error(self, app: Flask):
api = TriggerOAuthClientManageApi()
method = unwrap(api.post)
@@ -560,7 +560,7 @@ class TestTriggerSubscriptionVerifyApi:
def app(self, flask_app_with_containers: Flask):
return flask_app_with_containers
- def test_verify_success(self, app):
+ def test_verify_success(self, app: Flask):
api = TriggerSubscriptionVerifyApi()
method = unwrap(api.post)
diff --git a/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py
index 5791d2f6e2..b73d28e4c4 100644
--- a/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py
+++ b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py
@@ -291,7 +291,7 @@ class TestDatasetListApiGet:
mock_current_user,
mock_provider_mgr,
mock_marshal,
- app,
+ app: Flask,
mock_tenant,
):
from controllers.service_api.dataset.dataset import DatasetListApi
@@ -326,7 +326,7 @@ class TestDatasetListApiPost:
mock_dataset_svc,
mock_current_user,
mock_marshal,
- app,
+ app: Flask,
mock_tenant,
):
from controllers.service_api.dataset.dataset import DatasetListApi
@@ -352,7 +352,7 @@ class TestDatasetListApiPost:
self,
mock_dataset_svc,
mock_current_user,
- app,
+ app: Flask,
mock_tenant,
):
from controllers.service_api.dataset.dataset import DatasetListApi
@@ -390,7 +390,7 @@ class TestDatasetApiGet:
mock_provider_mgr,
mock_marshal,
mock_perm_svc,
- app,
+ app: Flask,
mock_dataset,
):
from controllers.service_api.dataset.dataset import DatasetApi
@@ -440,7 +440,7 @@ class TestDatasetApiGet:
self,
mock_dataset_svc,
mock_current_user,
- app,
+ app: Flask,
mock_dataset,
):
from controllers.service_api.dataset.dataset import DatasetApi
@@ -468,7 +468,7 @@ class TestDatasetApiDelete:
mock_dataset_svc,
mock_current_user,
mock_perm_svc,
- app,
+ app: Flask,
mock_dataset,
):
from controllers.service_api.dataset.dataset import DatasetApi
@@ -490,7 +490,7 @@ class TestDatasetApiDelete:
self,
mock_dataset_svc,
mock_current_user,
- app,
+ app: Flask,
mock_dataset,
):
from controllers.service_api.dataset.dataset import DatasetApi
@@ -511,7 +511,7 @@ class TestDatasetApiDelete:
self,
mock_dataset_svc,
mock_current_user,
- app,
+ app: Flask,
mock_dataset,
):
from controllers.service_api.dataset.dataset import DatasetApi
@@ -543,7 +543,7 @@ class TestDocumentStatusApiPatch:
mock_dataset_svc,
mock_current_user,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -574,7 +574,7 @@ class TestDocumentStatusApiPatch:
def test_batch_update_status_dataset_not_found(
self,
mock_dataset_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -603,7 +603,7 @@ class TestDocumentStatusApiPatch:
mock_dataset_svc,
mock_current_user,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -636,7 +636,7 @@ class TestDocumentStatusApiPatch:
mock_dataset_svc,
mock_current_user,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -669,7 +669,7 @@ class TestDocumentStatusApiPatch:
mock_dataset_svc,
mock_current_user,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -709,7 +709,7 @@ class TestDatasetTagsApiGet:
self,
mock_current_user,
mock_tag_svc,
- app,
+ app: Flask,
):
from controllers.service_api.dataset.dataset import DatasetTagsApi
@@ -731,7 +731,7 @@ class TestDatasetTagsApiGet:
def test_list_tags_from_db(
self,
mock_current_user,
- app,
+ app: Flask,
db_session_with_containers: Session,
):
"""Integration test: creates real Tag rows and retrieves them
@@ -774,7 +774,7 @@ class TestDatasetTagsApiPost:
self,
mock_current_user,
mock_tag_svc,
- app,
+ app: Flask,
):
from controllers.service_api.dataset.dataset import DatasetTagsApi
@@ -797,7 +797,7 @@ class TestDatasetTagsApiPost:
mock_tag_svc.save_tags.assert_called_once()
@patch("controllers.service_api.dataset.dataset.current_user")
- def test_create_tag_forbidden(self, mock_current_user, app):
+ def test_create_tag_forbidden(self, mock_current_user, app: Flask):
from controllers.service_api.dataset.dataset import DatasetTagsApi
mock_current_user.__class__ = Account
@@ -826,7 +826,7 @@ class TestDatasetTagsApiPatch:
mock_current_user,
mock_service_api_ns,
mock_tag_svc,
- app,
+ app: Flask,
):
from controllers.service_api.dataset.dataset import DatasetTagsApi
@@ -852,7 +852,7 @@ class TestDatasetTagsApiPatch:
mock_tag_svc.update_tags.assert_called_once_with({"name": "Updated Tag", "type": "knowledge"}, "tag-1")
@patch("controllers.service_api.dataset.dataset.current_user")
- def test_update_tag_forbidden(self, mock_current_user, app):
+ def test_update_tag_forbidden(self, mock_current_user, app: Flask):
from controllers.service_api.dataset.dataset import DatasetTagsApi
mock_current_user.__class__ = Account
@@ -880,7 +880,7 @@ class TestDatasetTagsApiDelete:
mock_current_user,
mock_service_api_ns,
mock_tag_svc,
- app,
+ app: Flask,
):
from controllers.service_api.dataset.dataset import DatasetTagsApi
@@ -905,7 +905,7 @@ class TestDatasetTagsApiDelete:
mock_tag_svc.delete_tag.assert_called_once_with("tag-1")
@patch("libs.login.current_user")
- def test_delete_tag_forbidden(self, mock_current_user, app):
+ def test_delete_tag_forbidden(self, mock_current_user, app: Flask):
from controllers.service_api.dataset.dataset import DatasetTagsApi
user_obj = Mock(spec=Account)
@@ -933,7 +933,7 @@ class TestDatasetTagsBindingStatusApi:
self,
mock_current_user,
mock_tag_svc,
- app,
+ app: Flask,
):
from controllers.service_api.dataset.dataset import DatasetTagsBindingStatusApi
@@ -963,7 +963,7 @@ class TestDatasetTagBindingApiPost:
self,
mock_current_user,
mock_tag_svc,
- app,
+ app: Flask,
):
from controllers.service_api.dataset.dataset import DatasetTagBindingApi
@@ -988,7 +988,7 @@ class TestDatasetTagBindingApiPost:
)
@patch("controllers.service_api.dataset.dataset.current_user")
- def test_bind_tags_forbidden(self, mock_current_user, app):
+ def test_bind_tags_forbidden(self, mock_current_user, app: Flask):
from controllers.service_api.dataset.dataset import DatasetTagBindingApi
mock_current_user.__class__ = Account
@@ -1014,7 +1014,7 @@ class TestDatasetTagUnbindingApiPost:
self,
mock_current_user,
mock_tag_svc,
- app,
+ app: Flask,
):
from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi
@@ -1044,7 +1044,7 @@ class TestDatasetTagUnbindingApiPost:
self,
mock_current_user,
mock_tag_svc,
- app,
+ app: Flask,
):
from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi
@@ -1069,7 +1069,7 @@ class TestDatasetTagUnbindingApiPost:
)
@patch("controllers.service_api.dataset.dataset.current_user")
- def test_unbind_tag_forbidden(self, mock_current_user, app):
+ def test_unbind_tag_forbidden(self, mock_current_user, app: Flask):
from controllers.service_api.dataset.dataset import DatasetTagUnbindingApi
mock_current_user.__class__ = Account
diff --git a/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py b/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py
index de9e691434..0a4e495f36 100644
--- a/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py
+++ b/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py
@@ -240,7 +240,7 @@ class TestDecodeJwtToken:
mock_access_mode: MagicMock,
mock_validate_token: MagicMock,
mock_validate_user: MagicMock,
- app,
+ app: Flask,
db_session_with_containers: Session,
) -> None:
app_model, site, end_user = self._create_app_site_enduser(db_session_with_containers)
@@ -300,7 +300,7 @@ class TestDecodeJwtToken:
mock_extract: MagicMock,
mock_passport_cls: MagicMock,
mock_features: MagicMock,
- app,
+ app: Flask,
db_session_with_containers: Session,
) -> None:
app_model, site, end_user = self._create_app_site_enduser(db_session_with_containers, enable_site=False)
@@ -325,7 +325,7 @@ class TestDecodeJwtToken:
mock_extract: MagicMock,
mock_passport_cls: MagicMock,
mock_features: MagicMock,
- app,
+ app: Flask,
db_session_with_containers: Session,
) -> None:
app_model, site, _ = self._create_app_site_enduser(db_session_with_containers)
@@ -351,7 +351,7 @@ class TestDecodeJwtToken:
mock_extract: MagicMock,
mock_passport_cls: MagicMock,
mock_features: MagicMock,
- app,
+ app: Flask,
db_session_with_containers: Session,
) -> None:
app_model, site, end_user = self._create_app_site_enduser(db_session_with_containers)
diff --git a/api/tests/test_containers_integration_tests/core/rag/pipeline/test_queue_integration.py b/api/tests/test_containers_integration_tests/core/rag/pipeline/test_queue_integration.py
index 54ee133bfe..d1af0a56ef 100644
--- a/api/tests/test_containers_integration_tests/core/rag/pipeline/test_queue_integration.py
+++ b/api/tests/test_containers_integration_tests/core/rag/pipeline/test_queue_integration.py
@@ -21,6 +21,8 @@ from core.rag.pipeline.queue import TaskWrapper, TenantIsolatedTaskQueue
from extensions.ext_redis import redis_client
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
+TenantAndAccount = tuple[Tenant, Account]
+
@dataclass
class TestTask:
@@ -74,18 +76,18 @@ class TestTenantIsolatedTaskQueueIntegration:
return tenant, account
@pytest.fixture
- def test_queue(self, test_tenant_and_account):
+ def test_queue(self, test_tenant_and_account: TenantAndAccount):
"""Create a generic test queue for testing."""
tenant, _ = test_tenant_and_account
return TenantIsolatedTaskQueue(tenant.id, "test_queue")
@pytest.fixture
- def secondary_queue(self, test_tenant_and_account):
+ def secondary_queue(self, test_tenant_and_account: TenantAndAccount):
"""Create a secondary test queue for testing isolation."""
tenant, _ = test_tenant_and_account
return TenantIsolatedTaskQueue(tenant.id, "secondary_queue")
- def test_queue_initialization(self, test_tenant_and_account):
+ def test_queue_initialization(self, test_tenant_and_account: TenantAndAccount):
"""Test queue initialization with correct key generation."""
tenant, _ = test_tenant_and_account
queue = TenantIsolatedTaskQueue(tenant.id, "test-key")
@@ -95,7 +97,9 @@ class TestTenantIsolatedTaskQueueIntegration:
assert queue._queue == f"tenant_self_test-key_task_queue:{tenant.id}"
assert queue._task_key == f"tenant_test-key_task:{tenant.id}"
- def test_tenant_isolation(self, test_tenant_and_account, db_session_with_containers: Session, fake: Faker):
+ def test_tenant_isolation(
+ self, test_tenant_and_account: TenantAndAccount, db_session_with_containers: Session, fake: Faker
+ ):
"""Test that different tenants have isolated queues."""
tenant1, _ = test_tenant_and_account
@@ -115,7 +119,7 @@ class TestTenantIsolatedTaskQueueIntegration:
assert queue1._queue == f"tenant_self_same-key_task_queue:{tenant1.id}"
assert queue2._queue == f"tenant_self_same-key_task_queue:{tenant2.id}"
- def test_key_isolation(self, test_tenant_and_account):
+ def test_key_isolation(self, test_tenant_and_account: TenantAndAccount):
"""Test that different keys have isolated queues."""
tenant, _ = test_tenant_and_account
queue1 = TenantIsolatedTaskQueue(tenant.id, "key1")
@@ -293,7 +297,7 @@ class TestTenantIsolatedTaskQueueIntegration:
assert isinstance(task, dict)
assert task["index"] == i # FIFO order
- def test_queue_operations_isolation(self, test_tenant_and_account, fake: Faker):
+ def test_queue_operations_isolation(self, test_tenant_and_account: TenantAndAccount, fake: Faker):
"""Test concurrent operations on different queues."""
tenant, _ = test_tenant_and_account
@@ -436,7 +440,7 @@ class TestTenantIsolatedTaskQueueCompatibility:
return tenant, account
- def test_legacy_string_queue_compatibility(self, test_tenant_and_account, fake: Faker):
+ def test_legacy_string_queue_compatibility(self, test_tenant_and_account: TenantAndAccount, fake: Faker):
"""
Test compatibility with legacy queues containing only string data.
@@ -466,7 +470,7 @@ class TestTenantIsolatedTaskQueueCompatibility:
expected_order = ["legacy_task_1", "legacy_task_2", "legacy_task_3", "legacy_task_4", "legacy_task_5"]
assert pulled_tasks == expected_order
- def test_legacy_queue_migration_scenario(self, test_tenant_and_account, fake: Faker):
+ def test_legacy_queue_migration_scenario(self, test_tenant_and_account: TenantAndAccount, fake: Faker):
"""
Test complete migration scenario from legacy to new system.
@@ -547,7 +551,7 @@ class TestTenantIsolatedTaskQueueCompatibility:
assert task["tenant_id"] == tenant.id
assert task["processing_type"] == "new_system"
- def test_legacy_queue_error_recovery(self, test_tenant_and_account, fake: Faker):
+ def test_legacy_queue_error_recovery(self, test_tenant_and_account: TenantAndAccount, fake: Faker):
"""
Test error recovery when legacy queue contains malformed data.
diff --git a/api/tests/test_containers_integration_tests/services/plugin/test_plugin_permission_service.py b/api/tests/test_containers_integration_tests/services/plugin/test_plugin_permission_service.py
new file mode 100644
index 0000000000..49d06986fd
--- /dev/null
+++ b/api/tests/test_containers_integration_tests/services/plugin/test_plugin_permission_service.py
@@ -0,0 +1,94 @@
+from __future__ import annotations
+
+from uuid import uuid4
+
+from sqlalchemy import func, select
+from sqlalchemy.orm import Session
+
+from models.account import TenantPluginPermission
+from services.plugin.plugin_permission_service import PluginPermissionService
+
+
+def _tenant_id() -> str:
+ return str(uuid4())
+
+
+def _get_permission(session: Session, tenant_id: str) -> TenantPluginPermission | None:
+ session.expire_all()
+ stmt = select(TenantPluginPermission).where(TenantPluginPermission.tenant_id == tenant_id)
+ return session.scalars(stmt).one_or_none()
+
+
+def _count_permissions(session: Session, tenant_id: str) -> int:
+ stmt = select(func.count()).select_from(TenantPluginPermission).where(TenantPluginPermission.tenant_id == tenant_id)
+ return session.scalar(stmt) or 0
+
+
+class TestGetPermission:
+ """Integration tests for PluginPermissionService.get_permission using testcontainers."""
+
+ def test_returns_permission_when_found(self, db_session_with_containers: Session):
+ tenant_id = _tenant_id()
+ permission = TenantPluginPermission(
+ tenant_id=tenant_id,
+ install_permission=TenantPluginPermission.InstallPermission.ADMINS,
+ debug_permission=TenantPluginPermission.DebugPermission.EVERYONE,
+ )
+ db_session_with_containers.add(permission)
+ db_session_with_containers.commit()
+
+ result = PluginPermissionService.get_permission(tenant_id)
+
+ assert result is not None
+ assert result.id == permission.id
+ assert result.tenant_id == tenant_id
+ assert result.install_permission == TenantPluginPermission.InstallPermission.ADMINS
+ assert result.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE
+
+ def test_returns_none_when_not_found(self, db_session_with_containers: Session):
+ result = PluginPermissionService.get_permission(_tenant_id())
+
+ assert result is None
+
+
+class TestChangePermission:
+ """Integration tests for PluginPermissionService.change_permission using testcontainers."""
+
+ def test_creates_new_permission_when_not_exists(self, db_session_with_containers: Session):
+ tenant_id = _tenant_id()
+
+ result = PluginPermissionService.change_permission(
+ tenant_id,
+ TenantPluginPermission.InstallPermission.EVERYONE,
+ TenantPluginPermission.DebugPermission.EVERYONE,
+ )
+
+ permission = _get_permission(db_session_with_containers, tenant_id)
+ assert result is True
+ assert permission is not None
+ assert permission.install_permission == TenantPluginPermission.InstallPermission.EVERYONE
+ assert permission.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE
+
+ def test_updates_existing_permission(self, db_session_with_containers: Session):
+ tenant_id = _tenant_id()
+ existing = TenantPluginPermission(
+ tenant_id=tenant_id,
+ install_permission=TenantPluginPermission.InstallPermission.EVERYONE,
+ debug_permission=TenantPluginPermission.DebugPermission.EVERYONE,
+ )
+ db_session_with_containers.add(existing)
+ db_session_with_containers.commit()
+
+ result = PluginPermissionService.change_permission(
+ tenant_id,
+ TenantPluginPermission.InstallPermission.ADMINS,
+ TenantPluginPermission.DebugPermission.ADMINS,
+ )
+
+ permission = _get_permission(db_session_with_containers, tenant_id)
+ assert result is True
+ assert permission is not None
+ assert permission.id == existing.id
+ assert permission.install_permission == TenantPluginPermission.InstallPermission.ADMINS
+ assert permission.debug_permission == TenantPluginPermission.DebugPermission.ADMINS
+ assert _count_permissions(db_session_with_containers, tenant_id) == 1
diff --git a/api/tests/test_containers_integration_tests/services/test_app_generate_service.py b/api/tests/test_containers_integration_tests/services/test_app_generate_service.py
index 3229693fd4..e2fe6c8476 100644
--- a/api/tests/test_containers_integration_tests/services/test_app_generate_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_app_generate_service.py
@@ -7,6 +7,7 @@ from faker import Faker
from sqlalchemy.orm import Session
from core.app.entities.app_invoke_entities import InvokeFrom
+from models import App
from models.model import EndUser
from models.workflow import Workflow
from services.app_generate_service import AppGenerateService
@@ -184,7 +185,7 @@ class TestAppGenerateService:
return app, account
- def _create_test_workflow(self, db_session_with_containers: Session, app):
+ def _create_test_workflow(self, db_session_with_containers: Session, app: App):
"""
Helper method to create a test workflow for testing.
diff --git a/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py b/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py
index cd63d3ad6c..1a1efe0337 100644
--- a/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py
@@ -165,7 +165,7 @@ class TestMessagesCleanServiceIntegration:
return app
- def _create_conversation(self, db_session_with_containers: Session, app):
+ def _create_conversation(self, db_session_with_containers: Session, app: App):
"""Helper to create a conversation."""
conversation = Conversation(
app_id=app.id,
diff --git a/api/tests/test_containers_integration_tests/services/test_saved_message_service.py b/api/tests/test_containers_integration_tests/services/test_saved_message_service.py
index 70aa813142..7b9e9924cd 100644
--- a/api/tests/test_containers_integration_tests/services/test_saved_message_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_saved_message_service.py
@@ -4,6 +4,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
+from models import App, CreatorUserRole
from models.enums import ConversationFromSource
from models.model import EndUser, Message
from models.web import SavedMessage
@@ -88,7 +89,7 @@ class TestSavedMessageService:
return app, account
- def _create_test_end_user(self, db_session_with_containers: Session, app):
+ def _create_test_end_user(self, db_session_with_containers: Session, app: App):
"""
Helper method to create a test end user for testing.
@@ -116,7 +117,7 @@ class TestSavedMessageService:
return end_user
- def _create_test_message(self, db_session_with_containers: Session, app, user):
+ def _create_test_message(self, db_session_with_containers: Session, app: App, user):
"""
Helper method to create a test message for testing.
@@ -199,13 +200,13 @@ class TestSavedMessageService:
saved_message1 = SavedMessage(
app_id=app.id,
message_id=message1.id,
- created_by_role="account",
+ created_by_role=CreatorUserRole.ACCOUNT,
created_by=account.id,
)
saved_message2 = SavedMessage(
app_id=app.id,
message_id=message2.id,
- created_by_role="account",
+ created_by_role=CreatorUserRole.ACCOUNT,
created_by=account.id,
)
@@ -272,13 +273,13 @@ class TestSavedMessageService:
saved_message1 = SavedMessage(
app_id=app.id,
message_id=message1.id,
- created_by_role="end_user",
+ created_by_role=CreatorUserRole.END_USER,
created_by=end_user.id,
)
saved_message2 = SavedMessage(
app_id=app.id,
message_id=message2.id,
- created_by_role="end_user",
+ created_by_role=CreatorUserRole.END_USER,
created_by=end_user.id,
)
@@ -449,7 +450,7 @@ class TestSavedMessageService:
saved_message = SavedMessage(
app_id=app.id,
message_id=message.id,
- created_by_role="account",
+ created_by_role=CreatorUserRole.ACCOUNT,
created_by=account.id,
)
@@ -540,7 +541,9 @@ class TestSavedMessageService:
message = self._create_test_message(db_session_with_containers, app, account)
# Pre-create a saved message
- saved = SavedMessage(app_id=app.id, message_id=message.id, created_by_role="account", created_by=account.id)
+ saved = SavedMessage(
+ app_id=app.id, message_id=message.id, created_by_role=CreatorUserRole.ACCOUNT, created_by=account.id
+ )
db_session_with_containers.add(saved)
db_session_with_containers.commit()
@@ -571,7 +574,9 @@ class TestSavedMessageService:
end_user = self._create_test_end_user(db_session_with_containers, app)
message = self._create_test_message(db_session_with_containers, app, end_user)
- saved = SavedMessage(app_id=app.id, message_id=message.id, created_by_role="end_user", created_by=end_user.id)
+ saved = SavedMessage(
+ app_id=app.id, message_id=message.id, created_by_role=CreatorUserRole.END_USER, created_by=end_user.id
+ )
db_session_with_containers.add(saved)
db_session_with_containers.commit()
@@ -596,10 +601,10 @@ class TestSavedMessageService:
# Both users save the same message
saved_account = SavedMessage(
- app_id=app.id, message_id=message.id, created_by_role="account", created_by=account1.id
+ app_id=app.id, message_id=message.id, created_by_role=CreatorUserRole.ACCOUNT, created_by=account1.id
)
saved_end_user = SavedMessage(
- app_id=app.id, message_id=message.id, created_by_role="end_user", created_by=end_user.id
+ app_id=app.id, message_id=message.id, created_by_role=CreatorUserRole.END_USER, created_by=end_user.id
)
db_session_with_containers.add_all([saved_account, saved_end_user])
db_session_with_containers.commit()
diff --git a/api/tests/test_containers_integration_tests/services/test_web_conversation_service.py b/api/tests/test_containers_integration_tests/services/test_web_conversation_service.py
index f2307fbd7d..797731d04b 100644
--- a/api/tests/test_containers_integration_tests/services/test_web_conversation_service.py
+++ b/api/tests/test_containers_integration_tests/services/test_web_conversation_service.py
@@ -6,7 +6,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from core.app.entities.app_invoke_entities import InvokeFrom
-from models import Account
+from models import Account, App
from models.enums import ConversationFromSource
from models.model import Conversation, EndUser
from models.web import PinnedConversation
@@ -93,7 +93,7 @@ class TestWebConversationService:
return app, account
- def _create_test_end_user(self, db_session_with_containers: Session, app):
+ def _create_test_end_user(self, db_session_with_containers: Session, app: App):
"""
Helper method to create a test end user for testing.
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 78413a0798..0fb0ebc330 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
@@ -185,7 +185,7 @@ class TestActivateApi:
mock_db,
mock_revoke_token,
mock_get_invitation,
- app,
+ app: Flask,
mock_invitation,
mock_account,
):
@@ -263,7 +263,7 @@ class TestActivateApi:
mock_db,
mock_revoke_token,
mock_get_invitation,
- app,
+ app: Flask,
mock_invitation,
mock_account,
):
@@ -312,7 +312,7 @@ class TestActivateApi:
mock_db,
mock_revoke_token,
mock_get_invitation,
- app,
+ app: Flask,
mock_invitation,
mock_account,
language,
@@ -358,7 +358,7 @@ class TestActivateApi:
mock_db,
mock_revoke_token,
mock_get_invitation,
- app,
+ app: Flask,
mock_invitation,
):
"""
@@ -398,7 +398,7 @@ class TestActivateApi:
mock_db,
mock_revoke_token,
mock_get_invitation,
- app,
+ app: Flask,
mock_invitation,
):
"""
@@ -438,7 +438,7 @@ class TestActivateApi:
mock_db,
mock_revoke_token,
mock_get_invitation,
- app,
+ app: Flask,
mock_invitation,
mock_account,
):
diff --git a/api/tests/unit_tests/controllers/console/auth/test_email_verification.py b/api/tests/unit_tests/controllers/console/auth/test_email_verification.py
index 7b2c7569fe..102af9b250 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_email_verification.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_email_verification.py
@@ -195,7 +195,7 @@ class TestEmailCodeLoginSendEmailApi:
mock_get_user,
mock_is_ip_limit,
mock_db,
- app,
+ app: Flask,
mock_account,
language_input,
expected_language,
@@ -267,7 +267,7 @@ class TestEmailCodeLoginApi:
mock_revoke_token,
mock_get_data,
mock_db,
- app,
+ app: Flask,
mock_account,
mock_token_pair,
):
@@ -315,7 +315,7 @@ class TestEmailCodeLoginApi:
mock_revoke_token,
mock_get_data,
mock_db,
- app,
+ app: Flask,
mock_account,
mock_token_pair,
):
@@ -431,7 +431,7 @@ class TestEmailCodeLoginApi:
mock_revoke_token,
mock_get_data,
mock_db,
- app,
+ app: Flask,
mock_account,
):
"""
@@ -474,7 +474,7 @@ class TestEmailCodeLoginApi:
mock_revoke_token,
mock_get_data,
mock_db,
- app,
+ app: Flask,
mock_account,
):
"""
@@ -515,7 +515,7 @@ class TestEmailCodeLoginApi:
mock_revoke_token,
mock_get_data,
mock_db,
- app,
+ app: Flask,
mock_account,
):
"""
diff --git a/api/tests/unit_tests/controllers/console/auth/test_login_logout.py b/api/tests/unit_tests/controllers/console/auth/test_login_logout.py
index 5284f29eed..ace2ce5706 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_login_logout.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_login_logout.py
@@ -412,7 +412,7 @@ class TestLoginApi:
mock_get_invitation,
mock_is_rate_limit,
mock_db,
- app,
+ app: Flask,
mock_account,
mock_token_pair,
):
@@ -448,7 +448,7 @@ class TestLoginApi:
mock_revoke_token,
mock_get_token_data,
mock_db,
- app,
+ app: Flask,
):
mock_get_token_data.return_value = {"email": "User@Example.com", "code": "123456"}
mock_get_account.side_effect = Unauthorized("Account is banned.")
diff --git a/api/tests/unit_tests/controllers/console/auth/test_token_refresh.py b/api/tests/unit_tests/controllers/console/auth/test_token_refresh.py
index 15c95f6b94..22974ca416 100644
--- a/api/tests/unit_tests/controllers/console/auth/test_token_refresh.py
+++ b/api/tests/unit_tests/controllers/console/auth/test_token_refresh.py
@@ -74,7 +74,7 @@ class TestRefreshTokenApi:
assert response.json["result"] == "success"
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
- def test_refresh_fails_without_token(self, mock_extract_token, app):
+ def test_refresh_fails_without_token(self, mock_extract_token, app: Flask):
"""
Test token refresh failure when no refresh token provided.
@@ -98,7 +98,7 @@ class TestRefreshTokenApi:
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
- def test_refresh_fails_with_invalid_token(self, mock_refresh_token, mock_extract_token, app):
+ def test_refresh_fails_with_invalid_token(self, mock_refresh_token, mock_extract_token, app: Flask):
"""
Test token refresh failure with invalid refresh token.
@@ -123,7 +123,7 @@ class TestRefreshTokenApi:
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
- def test_refresh_fails_with_expired_token(self, mock_refresh_token, mock_extract_token, app):
+ def test_refresh_fails_with_expired_token(self, mock_refresh_token, mock_extract_token, app: Flask):
"""
Test token refresh failure with expired refresh token.
@@ -148,7 +148,7 @@ class TestRefreshTokenApi:
@patch("controllers.console.auth.login.extract_refresh_token", autospec=True)
@patch("controllers.console.auth.login.AccountService.refresh_token", autospec=True)
- def test_refresh_with_empty_token(self, mock_refresh_token, mock_extract_token, app):
+ def test_refresh_with_empty_token(self, mock_refresh_token, mock_extract_token, app: Flask):
"""
Test token refresh with empty string token.
diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py
index 5136922e88..9c5b5ec256 100644
--- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py
+++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import Forbidden, NotFound
from controllers.console import console_ns
@@ -29,7 +30,7 @@ def unwrap(func):
class TestDatasourcePluginOAuthAuthorizationUrl:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasourcePluginOAuthAuthorizationUrl()
method = unwrap(api.get)
@@ -61,7 +62,7 @@ class TestDatasourcePluginOAuthAuthorizationUrl:
assert response.status_code == 200
- def test_get_no_oauth_config(self, app):
+ def test_get_no_oauth_config(self, app: Flask):
api = DatasourcePluginOAuthAuthorizationUrl()
method = unwrap(api.get)
@@ -80,7 +81,7 @@ class TestDatasourcePluginOAuthAuthorizationUrl:
with pytest.raises(ValueError):
method(api, "notion")
- def test_get_without_credential_id_sets_cookie(self, app):
+ def test_get_without_credential_id_sets_cookie(self, app: Flask):
api = DatasourcePluginOAuthAuthorizationUrl()
method = unwrap(api.get)
@@ -115,7 +116,7 @@ class TestDatasourcePluginOAuthAuthorizationUrl:
class TestDatasourceOAuthCallback:
- def test_callback_success_new_credential(self, app):
+ def test_callback_success_new_credential(self, app: Flask):
api = DatasourceOAuthCallback()
method = unwrap(api.get)
@@ -157,7 +158,7 @@ class TestDatasourceOAuthCallback:
assert response.status_code == 302
- def test_callback_missing_context(self, app):
+ def test_callback_missing_context(self, app: Flask):
api = DatasourceOAuthCallback()
method = unwrap(api.get)
@@ -165,7 +166,7 @@ class TestDatasourceOAuthCallback:
with pytest.raises(Forbidden):
method(api, "notion")
- def test_callback_invalid_context(self, app):
+ def test_callback_invalid_context(self, app: Flask):
api = DatasourceOAuthCallback()
method = unwrap(api.get)
@@ -180,7 +181,7 @@ class TestDatasourceOAuthCallback:
with pytest.raises(Forbidden):
method(api, "notion")
- def test_callback_oauth_config_not_found(self, app):
+ def test_callback_oauth_config_not_found(self, app: Flask):
api = DatasourceOAuthCallback()
method = unwrap(api.get)
@@ -202,7 +203,7 @@ class TestDatasourceOAuthCallback:
with pytest.raises(NotFound):
method(api, "notion")
- def test_callback_reauthorize_existing_credential(self, app):
+ def test_callback_reauthorize_existing_credential(self, app: Flask):
api = DatasourceOAuthCallback()
method = unwrap(api.get)
@@ -245,7 +246,7 @@ class TestDatasourceOAuthCallback:
assert response.status_code == 302
assert "/oauth-callback" in response.location
- def test_callback_context_id_from_cookie(self, app):
+ def test_callback_context_id_from_cookie(self, app: Flask):
api = DatasourceOAuthCallback()
method = unwrap(api.get)
@@ -289,7 +290,7 @@ class TestDatasourceOAuthCallback:
class TestDatasourceAuth:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = DatasourceAuth()
method = unwrap(api.post)
@@ -312,7 +313,7 @@ class TestDatasourceAuth:
assert status == 200
- def test_post_invalid_credentials(self, app):
+ def test_post_invalid_credentials(self, app: Flask):
api = DatasourceAuth()
method = unwrap(api.post)
@@ -334,7 +335,7 @@ class TestDatasourceAuth:
with pytest.raises(ValueError):
method(api, "notion")
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasourceAuth()
method = unwrap(api.get)
@@ -355,7 +356,7 @@ class TestDatasourceAuth:
assert status == 200
assert response["result"]
- def test_post_missing_credentials(self, app):
+ def test_post_missing_credentials(self, app: Flask):
api = DatasourceAuth()
method = unwrap(api.post)
@@ -372,7 +373,7 @@ class TestDatasourceAuth:
with pytest.raises(ValueError):
method(api, "notion")
- def test_get_empty_list(self, app):
+ def test_get_empty_list(self, app: Flask):
api = DatasourceAuth()
method = unwrap(api.get)
@@ -395,7 +396,7 @@ class TestDatasourceAuth:
class TestDatasourceAuthDeleteApi:
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = DatasourceAuthDeleteApi()
method = unwrap(api.post)
@@ -418,7 +419,7 @@ class TestDatasourceAuthDeleteApi:
assert status == 200
- def test_delete_missing_credential_id(self, app):
+ def test_delete_missing_credential_id(self, app: Flask):
api = DatasourceAuthDeleteApi()
method = unwrap(api.post)
@@ -437,7 +438,7 @@ class TestDatasourceAuthDeleteApi:
class TestDatasourceAuthUpdateApi:
- def test_update_success(self, app):
+ def test_update_success(self, app: Flask):
api = DatasourceAuthUpdateApi()
method = unwrap(api.post)
@@ -460,7 +461,7 @@ class TestDatasourceAuthUpdateApi:
assert status == 201
- def test_update_with_credentials_none(self, app):
+ def test_update_with_credentials_none(self, app: Flask):
api = DatasourceAuthUpdateApi()
method = unwrap(api.post)
@@ -484,7 +485,7 @@ class TestDatasourceAuthUpdateApi:
update_mock.assert_called_once()
assert status == 201
- def test_update_name_only(self, app):
+ def test_update_name_only(self, app: Flask):
api = DatasourceAuthUpdateApi()
method = unwrap(api.post)
@@ -507,7 +508,7 @@ class TestDatasourceAuthUpdateApi:
assert status == 201
- def test_update_with_empty_credentials_dict(self, app):
+ def test_update_with_empty_credentials_dict(self, app: Flask):
api = DatasourceAuthUpdateApi()
method = unwrap(api.post)
@@ -533,7 +534,7 @@ class TestDatasourceAuthUpdateApi:
class TestDatasourceAuthListApi:
- def test_list_success(self, app):
+ def test_list_success(self, app: Flask):
api = DatasourceAuthListApi()
method = unwrap(api.get)
@@ -553,7 +554,7 @@ class TestDatasourceAuthListApi:
assert status == 200
- def test_auth_list_empty(self, app):
+ def test_auth_list_empty(self, app: Flask):
api = DatasourceAuthListApi()
method = unwrap(api.get)
@@ -574,7 +575,7 @@ class TestDatasourceAuthListApi:
assert status == 200
assert response["result"] == []
- def test_hardcode_list_empty(self, app):
+ def test_hardcode_list_empty(self, app: Flask):
api = DatasourceHardCodeAuthListApi()
method = unwrap(api.get)
@@ -597,7 +598,7 @@ class TestDatasourceAuthListApi:
class TestDatasourceHardCodeAuthListApi:
- def test_list_success(self, app):
+ def test_list_success(self, app: Flask):
api = DatasourceHardCodeAuthListApi()
method = unwrap(api.get)
@@ -619,7 +620,7 @@ class TestDatasourceHardCodeAuthListApi:
class TestDatasourceAuthOauthCustomClient:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = DatasourceAuthOauthCustomClient()
method = unwrap(api.post)
@@ -642,7 +643,7 @@ class TestDatasourceAuthOauthCustomClient:
assert status == 200
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = DatasourceAuthOauthCustomClient()
method = unwrap(api.delete)
@@ -662,7 +663,7 @@ class TestDatasourceAuthOauthCustomClient:
assert status == 200
- def test_post_empty_payload(self, app):
+ def test_post_empty_payload(self, app: Flask):
api = DatasourceAuthOauthCustomClient()
method = unwrap(api.post)
@@ -685,7 +686,7 @@ class TestDatasourceAuthOauthCustomClient:
assert status == 200
- def test_post_disabled_flag(self, app):
+ def test_post_disabled_flag(self, app: Flask):
api = DatasourceAuthOauthCustomClient()
method = unwrap(api.post)
@@ -714,7 +715,7 @@ class TestDatasourceAuthOauthCustomClient:
class TestDatasourceAuthDefaultApi:
- def test_set_default_success(self, app):
+ def test_set_default_success(self, app: Flask):
api = DatasourceAuthDefaultApi()
method = unwrap(api.post)
@@ -737,7 +738,7 @@ class TestDatasourceAuthDefaultApi:
assert status == 200
- def test_default_missing_id(self, app):
+ def test_default_missing_id(self, app: Flask):
api = DatasourceAuthDefaultApi()
method = unwrap(api.post)
@@ -756,7 +757,7 @@ class TestDatasourceAuthDefaultApi:
class TestDatasourceUpdateProviderNameApi:
- def test_update_name_success(self, app):
+ def test_update_name_success(self, app: Flask):
api = DatasourceUpdateProviderNameApi()
method = unwrap(api.post)
@@ -779,7 +780,7 @@ class TestDatasourceUpdateProviderNameApi:
assert status == 200
- def test_update_name_too_long(self, app):
+ def test_update_name_too_long(self, app: Flask):
api = DatasourceUpdateProviderNameApi()
method = unwrap(api.post)
@@ -799,7 +800,7 @@ class TestDatasourceUpdateProviderNameApi:
with pytest.raises(ValueError):
method(api, "notion")
- def test_update_name_missing_credential_id(self, app):
+ def test_update_name_missing_credential_id(self, app: Flask):
api = DatasourceUpdateProviderNameApi()
method = unwrap(api.post)
diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py
index 7a8ccde55a..d4c6a775ec 100644
--- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py
+++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import Forbidden
from controllers.console import console_ns
@@ -25,7 +26,7 @@ class TestDataSourceContentPreviewApi:
"credential_id": "cred-1",
}
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = DataSourceContentPreviewApi()
method = unwrap(api.post)
@@ -66,7 +67,7 @@ class TestDataSourceContentPreviewApi:
assert status == 200
assert response == preview_result
- def test_post_forbidden_non_account_user(self, app):
+ def test_post_forbidden_non_account_user(self, app: Flask):
api = DataSourceContentPreviewApi()
method = unwrap(api.post)
@@ -85,7 +86,7 @@ class TestDataSourceContentPreviewApi:
with pytest.raises(Forbidden):
method(api, pipeline, "node-1")
- def test_post_invalid_payload(self, app):
+ def test_post_invalid_payload(self, app: Flask):
api = DataSourceContentPreviewApi()
method = unwrap(api.post)
@@ -108,7 +109,7 @@ class TestDataSourceContentPreviewApi:
with pytest.raises(ValueError):
method(api, pipeline, "node-1")
- def test_post_without_credential_id(self, app):
+ def test_post_without_credential_id(self, app: Flask):
api = DataSourceContentPreviewApi()
method = unwrap(api.post)
diff --git a/api/tests/unit_tests/controllers/console/datasets/test_datasets.py b/api/tests/unit_tests/controllers/console/datasets/test_datasets.py
index 9465936f28..e28d68ee5a 100644
--- a/api/tests/unit_tests/controllers/console/datasets/test_datasets.py
+++ b/api/tests/unit_tests/controllers/console/datasets/test_datasets.py
@@ -2,6 +2,7 @@ import datetime
from unittest.mock import MagicMock, PropertyMock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
import services
@@ -58,7 +59,7 @@ class TestDatasetList:
user.is_dataset_editor = True
return user
- def test_get_success_basic(self, app):
+ def test_get_success_basic(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.get)
@@ -93,7 +94,7 @@ class TestDatasetList:
assert resp["total"] == 1
assert resp["data"][0]["embedding_available"] is True
- def test_get_with_ids_filter(self, app):
+ def test_get_with_ids_filter(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.get)
@@ -128,7 +129,7 @@ class TestDatasetList:
assert status == 200
assert resp["total"] == 2
- def test_get_with_tag_ids(self, app):
+ def test_get_with_tag_ids(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.get)
@@ -161,7 +162,7 @@ class TestDatasetList:
assert status == 200
- def test_embedding_available_false(self, app):
+ def test_embedding_available_false(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.get)
@@ -203,7 +204,7 @@ class TestDatasetList:
assert resp["data"][0]["embedding_available"] is False
- def test_partial_members_permission(self, app):
+ def test_partial_members_permission(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.get)
@@ -242,7 +243,7 @@ class TestDatasetList:
class TestDatasetListApiPost:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.post)
@@ -290,7 +291,7 @@ class TestDatasetListApiPost:
assert status == 201
- def test_post_forbidden(self, app):
+ def test_post_forbidden(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.post)
@@ -310,7 +311,7 @@ class TestDatasetListApiPost:
with pytest.raises(Forbidden):
method(api)
- def test_post_duplicate_name(self, app):
+ def test_post_duplicate_name(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.post)
@@ -335,7 +336,7 @@ class TestDatasetListApiPost:
with pytest.raises(DatasetNameDuplicateError):
method(api)
- def test_post_invalid_payload_missing_name(self, app):
+ def test_post_invalid_payload_missing_name(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.post)
@@ -343,7 +344,7 @@ class TestDatasetListApiPost:
with pytest.raises(ValueError):
method(api)
- def test_post_invalid_indexing_technique(self, app):
+ def test_post_invalid_indexing_technique(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.post)
@@ -356,7 +357,7 @@ class TestDatasetListApiPost:
with pytest.raises(ValueError, match="Invalid indexing technique"):
method(api)
- def test_post_invalid_provider(self, app):
+ def test_post_invalid_provider(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.post)
@@ -371,7 +372,7 @@ class TestDatasetListApiPost:
class TestDatasetApiGet:
- def test_get_success_basic(self, app):
+ def test_get_success_basic(self, app: Flask):
api = DatasetApi()
method = unwrap(api.get)
@@ -427,7 +428,7 @@ class TestDatasetApiGet:
assert status == 200
assert data["embedding_available"] is True
- def test_get_dataset_not_found(self, app):
+ def test_get_dataset_not_found(self, app: Flask):
api = DatasetApi()
method = unwrap(api.get)
@@ -448,7 +449,7 @@ class TestDatasetApiGet:
with pytest.raises(NotFound, match="Dataset not found"):
method(api, dataset_id)
- def test_get_permission_denied(self, app):
+ def test_get_permission_denied(self, app: Flask):
api = DatasetApi()
method = unwrap(api.get)
@@ -475,7 +476,7 @@ class TestDatasetApiGet:
with pytest.raises(Forbidden, match="no access"):
method(api, dataset_id)
- def test_get_high_quality_embedding_unavailable(self, app):
+ def test_get_high_quality_embedding_unavailable(self, app: Flask):
api = DatasetApi()
method = unwrap(api.get)
@@ -530,7 +531,7 @@ class TestDatasetApiGet:
assert data["embedding_available"] is False
- def test_get_partial_members_permission(self, app):
+ def test_get_partial_members_permission(self, app: Flask):
api = DatasetApi()
method = unwrap(api.get)
@@ -590,7 +591,7 @@ class TestDatasetApiGet:
class TestDatasetApiPatch:
- def test_patch_success_basic(self, app):
+ def test_patch_success_basic(self, app: Flask):
api = DatasetApi()
method = unwrap(api.patch)
@@ -659,7 +660,7 @@ class TestDatasetApiPatch:
assert status == 200
assert result["partial_member_list"] == []
- def test_patch_dataset_not_found(self, app):
+ def test_patch_dataset_not_found(self, app: Flask):
api = DatasetApi()
method = unwrap(api.patch)
@@ -674,7 +675,7 @@ class TestDatasetApiPatch:
with pytest.raises(NotFound, match="Dataset not found"):
method(api, "missing")
- def test_patch_permission_denied(self, app):
+ def test_patch_permission_denied(self, app: Flask):
api = DatasetApi()
method = unwrap(api.patch)
@@ -704,7 +705,7 @@ class TestDatasetApiPatch:
with pytest.raises(Forbidden):
method(api, dataset_id)
- def test_patch_partial_members_update(self, app):
+ def test_patch_partial_members_update(self, app: Flask):
api = DatasetApi()
method = unwrap(api.patch)
@@ -773,7 +774,7 @@ class TestDatasetApiPatch:
assert result["partial_member_list"] == payload["partial_member_list"]
- def test_patch_clear_partial_members(self, app):
+ def test_patch_clear_partial_members(self, app: Flask):
api = DatasetApi()
method = unwrap(api.patch)
@@ -843,7 +844,7 @@ class TestDatasetApiPatch:
class TestDatasetApiDelete:
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = DatasetApi()
method = unwrap(api.delete)
@@ -874,7 +875,7 @@ class TestDatasetApiDelete:
assert status == 204
assert result == {"result": "success"}
- def test_delete_forbidden_no_permission(self, app):
+ def test_delete_forbidden_no_permission(self, app: Flask):
api = DatasetApi()
method = unwrap(api.delete)
@@ -893,7 +894,7 @@ class TestDatasetApiDelete:
with pytest.raises(Forbidden):
method(api, dataset_id)
- def test_delete_dataset_not_found(self, app):
+ def test_delete_dataset_not_found(self, app: Flask):
api = DatasetApi()
method = unwrap(api.delete)
@@ -917,7 +918,7 @@ class TestDatasetApiDelete:
with pytest.raises(NotFound, match="Dataset not found"):
method(api, dataset_id)
- def test_delete_dataset_in_use(self, app):
+ def test_delete_dataset_in_use(self, app: Flask):
api = DatasetApi()
method = unwrap(api.delete)
@@ -943,7 +944,7 @@ class TestDatasetApiDelete:
class TestDatasetUseCheckApi:
- def test_get_use_check_true(self, app):
+ def test_get_use_check_true(self, app: Flask):
api = DatasetUseCheckApi()
method = unwrap(api.get)
@@ -962,7 +963,7 @@ class TestDatasetUseCheckApi:
assert status == 200
assert result == {"is_using": True}
- def test_get_use_check_false(self, app):
+ def test_get_use_check_false(self, app: Flask):
api = DatasetUseCheckApi()
method = unwrap(api.get)
@@ -983,7 +984,7 @@ class TestDatasetUseCheckApi:
class TestDatasetQueryApi:
- def test_get_queries_success(self, app):
+ def test_get_queries_success(self, app: Flask):
api = DatasetQueryApi()
method = unwrap(api.get)
@@ -1027,7 +1028,7 @@ class TestDatasetQueryApi:
assert response["has_more"] is False
assert len(response["data"]) == 2
- def test_get_queries_dataset_not_found(self, app):
+ def test_get_queries_dataset_not_found(self, app: Flask):
api = DatasetQueryApi()
method = unwrap(api.get)
@@ -1049,7 +1050,7 @@ class TestDatasetQueryApi:
with pytest.raises(NotFound, match="Dataset not found"):
method(api, dataset_id)
- def test_get_queries_permission_denied(self, app):
+ def test_get_queries_permission_denied(self, app: Flask):
api = DatasetQueryApi()
method = unwrap(api.get)
@@ -1078,7 +1079,7 @@ class TestDatasetQueryApi:
with pytest.raises(Forbidden):
method(api, dataset_id)
- def test_get_queries_pagination_has_more(self, app):
+ def test_get_queries_pagination_has_more(self, app: Flask):
api = DatasetQueryApi()
method = unwrap(api.get)
@@ -1152,7 +1153,7 @@ class TestDatasetIndexingEstimateApi:
"dataset_id": None,
}
- def test_post_success_upload_file(self, app):
+ def test_post_success_upload_file(self, app: Flask):
api = DatasetIndexingEstimateApi()
method = unwrap(api.post)
@@ -1193,7 +1194,7 @@ class TestDatasetIndexingEstimateApi:
assert status == 200
assert response == {"tokens": 100}
- def test_post_file_not_found(self, app):
+ def test_post_file_not_found(self, app: Flask):
api = DatasetIndexingEstimateApi()
method = unwrap(api.post)
@@ -1223,7 +1224,7 @@ class TestDatasetIndexingEstimateApi:
with pytest.raises(NotFound):
method(api)
- def test_post_llm_bad_request_error(self, app):
+ def test_post_llm_bad_request_error(self, app: Flask):
api = DatasetIndexingEstimateApi()
method = unwrap(api.post)
mock_file = self._upload_file()
@@ -1258,7 +1259,7 @@ class TestDatasetIndexingEstimateApi:
with pytest.raises(ProviderNotInitializeError):
method(api)
- def test_post_provider_token_not_init(self, app):
+ def test_post_provider_token_not_init(self, app: Flask):
api = DatasetIndexingEstimateApi()
method = unwrap(api.post)
mock_file = self._upload_file()
@@ -1293,7 +1294,7 @@ class TestDatasetIndexingEstimateApi:
with pytest.raises(ProviderNotInitializeError):
method(api)
- def test_post_generic_exception(self, app):
+ def test_post_generic_exception(self, app: Flask):
api = DatasetIndexingEstimateApi()
method = unwrap(api.post)
mock_file = self._upload_file()
@@ -1330,7 +1331,7 @@ class TestDatasetIndexingEstimateApi:
class TestDatasetRelatedAppListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasetRelatedAppListApi()
method = unwrap(api.get)
@@ -1368,7 +1369,7 @@ class TestDatasetRelatedAppListApi:
assert response["total"] == 2
assert response["data"] == [app1, app2]
- def test_get_dataset_not_found(self, app):
+ def test_get_dataset_not_found(self, app: Flask):
api = DatasetRelatedAppListApi()
method = unwrap(api.get)
@@ -1386,7 +1387,7 @@ class TestDatasetRelatedAppListApi:
with pytest.raises(NotFound):
method(api, "dataset-1")
- def test_get_permission_denied(self, app):
+ def test_get_permission_denied(self, app: Flask):
api = DatasetRelatedAppListApi()
method = unwrap(api.get)
@@ -1410,7 +1411,7 @@ class TestDatasetRelatedAppListApi:
with pytest.raises(Forbidden):
method(api, "dataset-1")
- def test_get_filters_none_apps(self, app):
+ def test_get_filters_none_apps(self, app: Flask):
api = DatasetRelatedAppListApi()
method = unwrap(api.get)
@@ -1449,7 +1450,7 @@ class TestDatasetRelatedAppListApi:
class TestDatasetIndexingStatusApi:
- def test_get_success_with_documents(self, app):
+ def test_get_success_with_documents(self, app: Flask):
api = DatasetIndexingStatusApi()
method = unwrap(api.get)
@@ -1490,7 +1491,7 @@ class TestDatasetIndexingStatusApi:
assert item["completed_segments"] == 3
assert item["total_segments"] == 3
- def test_get_success_no_documents(self, app):
+ def test_get_success_no_documents(self, app: Flask):
api = DatasetIndexingStatusApi()
method = unwrap(api.get)
@@ -1510,7 +1511,7 @@ class TestDatasetIndexingStatusApi:
assert status == 200
assert response == {"data": []}
- def test_segment_counts_different_values(self, app):
+ def test_segment_counts_different_values(self, app: Flask):
api = DatasetIndexingStatusApi()
method = unwrap(api.get)
@@ -1550,7 +1551,7 @@ class TestDatasetIndexingStatusApi:
class TestDatasetApiKeyApi:
- def test_get_api_keys_success(self, app):
+ def test_get_api_keys_success(self, app: Flask):
api = DatasetApiKeyApi()
method = unwrap(api.get)
@@ -1587,7 +1588,7 @@ class TestDatasetApiKeyApi:
assert response["data"][1]["id"] == "key-2"
assert response["data"][1]["token"] == "ds-def"
- def test_post_create_api_key_success(self, app):
+ def test_post_create_api_key_success(self, app: Flask):
api = DatasetApiKeyApi()
method = unwrap(api.post)
@@ -1632,7 +1633,7 @@ class TestDatasetApiKeyApi:
assert response["type"] == "dataset"
assert response["created_at"] is not None
- def test_post_exceed_max_keys(self, app):
+ def test_post_exceed_max_keys(self, app: Flask):
api = DatasetApiKeyApi()
method = unwrap(api.post)
@@ -1658,7 +1659,7 @@ class TestDatasetApiKeyApi:
class TestDatasetApiDeleteApi:
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = DatasetApiDeleteApi()
method = unwrap(api.delete)
@@ -1688,7 +1689,7 @@ class TestDatasetApiDeleteApi:
assert status == 204
assert response["result"] == "success"
- def test_delete_key_not_found(self, app):
+ def test_delete_key_not_found(self, app: Flask):
api = DatasetApiDeleteApi()
method = unwrap(api.delete)
@@ -1708,7 +1709,7 @@ class TestDatasetApiDeleteApi:
class TestDatasetEnableApiApi:
- def test_enable_api(self, app):
+ def test_enable_api(self, app: Flask):
api = DatasetEnableApiApi()
method = unwrap(api.post)
@@ -1724,7 +1725,7 @@ class TestDatasetEnableApiApi:
assert status == 200
assert response["result"] == "success"
- def test_disable_api(self, app):
+ def test_disable_api(self, app: Flask):
api = DatasetEnableApiApi()
method = unwrap(api.post)
@@ -1742,7 +1743,7 @@ class TestDatasetEnableApiApi:
class TestDatasetApiBaseUrlApi:
- def test_get_api_base_url_from_config(self, app):
+ def test_get_api_base_url_from_config(self, app: Flask):
api = DatasetApiBaseUrlApi()
method = unwrap(api.get)
@@ -1757,7 +1758,7 @@ class TestDatasetApiBaseUrlApi:
assert response["api_base_url"] == "https://example.com/v1"
- def test_get_api_base_url_from_request(self, app):
+ def test_get_api_base_url_from_request(self, app: Flask):
api = DatasetApiBaseUrlApi()
method = unwrap(api.get)
@@ -1772,7 +1773,7 @@ class TestDatasetApiBaseUrlApi:
assert response["api_base_url"] == "http://localhost:5000/v1"
- def test_get_api_base_url_no_double_v1(self, app):
+ def test_get_api_base_url_no_double_v1(self, app: Flask):
api = DatasetApiBaseUrlApi()
method = unwrap(api.get)
@@ -1789,7 +1790,7 @@ class TestDatasetApiBaseUrlApi:
class TestDatasetRetrievalSettingApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasetRetrievalSettingApi()
method = unwrap(api.get)
@@ -1810,7 +1811,7 @@ class TestDatasetRetrievalSettingApi:
class TestDatasetRetrievalSettingMockApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasetRetrievalSettingMockApi()
method = unwrap(api.get)
@@ -1827,7 +1828,7 @@ class TestDatasetRetrievalSettingMockApi:
class TestDatasetErrorDocs:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasetErrorDocs()
method = unwrap(api.get)
@@ -1850,7 +1851,7 @@ class TestDatasetErrorDocs:
assert status == 200
assert response["total"] == 1
- def test_get_dataset_not_found(self, app):
+ def test_get_dataset_not_found(self, app: Flask):
api = DatasetErrorDocs()
method = unwrap(api.get)
@@ -1866,7 +1867,7 @@ class TestDatasetErrorDocs:
class TestDatasetPermissionUserListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasetPermissionUserListApi()
method = unwrap(api.get)
@@ -1897,7 +1898,7 @@ class TestDatasetPermissionUserListApi:
assert status == 200
assert response["data"] == users
- def test_get_permission_denied(self, app):
+ def test_get_permission_denied(self, app: Flask):
api = DatasetPermissionUserListApi()
method = unwrap(api.get)
@@ -1923,7 +1924,7 @@ class TestDatasetPermissionUserListApi:
class TestDatasetAutoDisableLogApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasetAutoDisableLogApi()
method = unwrap(api.get)
@@ -1946,7 +1947,7 @@ class TestDatasetAutoDisableLogApi:
assert status == 200
assert response == logs
- def test_get_dataset_not_found(self, app):
+ def test_get_dataset_not_found(self, app: Flask):
api = DatasetAutoDisableLogApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py b/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py
index d9b02ac453..ff9e1736d2 100644
--- a/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py
+++ b/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py
@@ -2,6 +2,7 @@ from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import Forbidden, NotFound
import services
@@ -239,7 +240,7 @@ class TestDatasetDocumentListApi:
assert "documents" in response
- def test_post_forbidden(self, app):
+ def test_post_forbidden(self, app: Flask):
api = DatasetDocumentListApi()
method = unwrap(api.post)
@@ -395,7 +396,7 @@ class TestDocumentDownloadApi:
class TestDocumentProcessingApi:
- def test_processing_forbidden_when_not_editor(self, app):
+ def test_processing_forbidden_when_not_editor(self, app: Flask):
api = DocumentProcessingApi()
method = unwrap(api.patch)
@@ -1185,7 +1186,7 @@ class TestDocumentPermissionCases:
"preview": [],
}
- def test_document_tenant_mismatch(self, app):
+ def test_document_tenant_mismatch(self, app: Flask):
api = DocumentApi()
method = unwrap(api.get)
@@ -1253,7 +1254,7 @@ class TestDocumentPermissionCases:
assert status == 200
assert response["mode"] == "custom"
- def test_process_rule_permission_denied(self, app):
+ def test_process_rule_permission_denied(self, app: Flask):
api = GetProcessRuleApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/datasets/test_datasets_segments.py b/api/tests/unit_tests/controllers/console/datasets/test_datasets_segments.py
index 693b06e95b..412edb9dfe 100644
--- a/api/tests/unit_tests/controllers/console/datasets/test_datasets_segments.py
+++ b/api/tests/unit_tests/controllers/console/datasets/test_datasets_segments.py
@@ -2,6 +2,7 @@ from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import Forbidden, NotFound
import services
@@ -82,7 +83,7 @@ def test_get_segment_with_summary(monkeypatch):
class TestDatasetDocumentSegmentListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasetDocumentSegmentListApi()
method = unwrap(api.get)
@@ -132,7 +133,7 @@ class TestDatasetDocumentSegmentListApi:
assert status == 200
- def test_get_dataset_not_found(self, app):
+ def test_get_dataset_not_found(self, app: Flask):
api = DatasetDocumentSegmentListApi()
method = unwrap(api.get)
@@ -150,7 +151,7 @@ class TestDatasetDocumentSegmentListApi:
with pytest.raises(NotFound):
method(api, "ds-1", "doc-1")
- def test_get_permission_denied(self, app):
+ def test_get_permission_denied(self, app: Flask):
api = DatasetDocumentSegmentListApi()
method = unwrap(api.get)
@@ -176,7 +177,7 @@ class TestDatasetDocumentSegmentListApi:
class TestDatasetDocumentSegmentApi:
- def test_patch_success(self, app):
+ def test_patch_success(self, app: Flask):
api = DatasetDocumentSegmentApi()
method = unwrap(api.patch)
@@ -221,7 +222,7 @@ class TestDatasetDocumentSegmentApi:
assert status == 200
assert response["result"] == "success"
- def test_patch_document_indexing_in_progress(self, app):
+ def test_patch_document_indexing_in_progress(self, app: Flask):
api = DatasetDocumentSegmentApi()
method = unwrap(api.patch)
@@ -264,7 +265,7 @@ class TestDatasetDocumentSegmentApi:
with pytest.raises(InvalidActionError):
method(api, "ds-1", "doc-1", "disable")
- def test_patch_llm_bad_request(self, app):
+ def test_patch_llm_bad_request(self, app: Flask):
api = DatasetDocumentSegmentApi()
method = unwrap(api.patch)
@@ -308,7 +309,7 @@ class TestDatasetDocumentSegmentApi:
with pytest.raises(ProviderNotInitializeError):
method(api, "ds-1", "doc-1", "enable")
- def test_patch_provider_token_not_init(self, app):
+ def test_patch_provider_token_not_init(self, app: Flask):
api = DatasetDocumentSegmentApi()
method = unwrap(api.patch)
@@ -354,7 +355,7 @@ class TestDatasetDocumentSegmentApi:
class TestDatasetDocumentSegmentAddApi:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = DatasetDocumentSegmentAddApi()
method = unwrap(api.post)
@@ -413,7 +414,7 @@ class TestDatasetDocumentSegmentAddApi:
assert status == 200
assert response["data"]["id"] == "seg-1"
- def test_post_llm_bad_request(self, app):
+ def test_post_llm_bad_request(self, app: Flask):
api = DatasetDocumentSegmentAddApi()
method = unwrap(api.post)
@@ -452,7 +453,7 @@ class TestDatasetDocumentSegmentAddApi:
with pytest.raises(ProviderNotInitializeError):
method(api, "ds-1", "doc-1")
- def test_post_provider_token_not_init(self, app):
+ def test_post_provider_token_not_init(self, app: Flask):
api = DatasetDocumentSegmentAddApi()
method = unwrap(api.post)
@@ -493,7 +494,7 @@ class TestDatasetDocumentSegmentAddApi:
class TestDatasetDocumentSegmentUpdateApi:
- def test_patch_success(self, app):
+ def test_patch_success(self, app: Flask):
api = DatasetDocumentSegmentUpdateApi()
method = unwrap(api.patch)
@@ -551,7 +552,7 @@ class TestDatasetDocumentSegmentUpdateApi:
assert status == 200
assert "data" in response
- def test_patch_llm_bad_request(self, app):
+ def test_patch_llm_bad_request(self, app: Flask):
api = DatasetDocumentSegmentUpdateApi()
method = unwrap(api.patch)
@@ -596,7 +597,7 @@ class TestDatasetDocumentSegmentUpdateApi:
class TestDatasetDocumentSegmentBatchImportApi:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -638,7 +639,7 @@ class TestDatasetDocumentSegmentBatchImportApi:
assert status == 200
assert response["job_status"] == "waiting"
- def test_post_dataset_not_found(self, app):
+ def test_post_dataset_not_found(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -659,7 +660,7 @@ class TestDatasetDocumentSegmentBatchImportApi:
with pytest.raises(NotFound):
method(api, "ds-1", "doc-1")
- def test_post_document_not_found(self, app):
+ def test_post_document_not_found(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -684,7 +685,7 @@ class TestDatasetDocumentSegmentBatchImportApi:
with pytest.raises(NotFound):
method(api, "ds-1", "doc-1")
- def test_post_upload_file_not_found(self, app):
+ def test_post_upload_file_not_found(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -713,7 +714,7 @@ class TestDatasetDocumentSegmentBatchImportApi:
with pytest.raises(NotFound):
method(api, "ds-1", "doc-1")
- def test_post_invalid_file_type(self, app):
+ def test_post_invalid_file_type(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -745,7 +746,7 @@ class TestDatasetDocumentSegmentBatchImportApi:
with pytest.raises(ValueError):
method(api, "ds-1", "doc-1")
- def test_post_async_task_failure(self, app):
+ def test_post_async_task_failure(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -783,7 +784,7 @@ class TestDatasetDocumentSegmentBatchImportApi:
assert status == 500
assert "error" in response
- def test_get_job_not_found_in_redis(self, app):
+ def test_get_job_not_found_in_redis(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.get)
@@ -799,7 +800,7 @@ class TestDatasetDocumentSegmentBatchImportApi:
class TestChildChunkAddApi:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = ChildChunkAddApi()
method = unwrap(api.post)
@@ -852,7 +853,7 @@ class TestChildChunkAddApi:
assert status == 200
assert response["data"]["id"] == "cc-1"
- def test_post_child_chunk_indexing_error(self, app):
+ def test_post_child_chunk_indexing_error(self, app: Flask):
api = ChildChunkAddApi()
method = unwrap(api.post)
@@ -897,7 +898,7 @@ class TestChildChunkAddApi:
class TestChildChunkUpdateApi:
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = ChildChunkUpdateApi()
method = unwrap(api.delete)
@@ -941,7 +942,7 @@ class TestChildChunkUpdateApi:
assert status == 204
assert response["result"] == "success"
- def test_delete_child_chunk_index_error(self, app):
+ def test_delete_child_chunk_index_error(self, app: Flask):
api = ChildChunkUpdateApi()
method = unwrap(api.delete)
@@ -984,7 +985,7 @@ class TestChildChunkUpdateApi:
class TestSegmentListAdvancedCases:
- def test_segment_list_with_keyword_filter(self, app):
+ def test_segment_list_with_keyword_filter(self, app: Flask):
api = DatasetDocumentSegmentListApi()
method = unwrap(api.get)
@@ -1035,7 +1036,7 @@ class TestSegmentListAdvancedCases:
assert status == 200
assert response["total"] == 1
- def test_segment_list_permission_denied(self, app):
+ def test_segment_list_permission_denied(self, app: Flask):
"""Test segment list with permission denied"""
api = DatasetDocumentSegmentListApi()
method = unwrap(api.get)
@@ -1058,7 +1059,7 @@ class TestSegmentListAdvancedCases:
with pytest.raises(Forbidden):
method(api, "ds-1", "doc-1")
- def test_segment_list_dataset_not_found(self, app):
+ def test_segment_list_dataset_not_found(self, app: Flask):
"""Test segment list with dataset not found"""
api = DatasetDocumentSegmentListApi()
method = unwrap(api.get)
@@ -1079,7 +1080,7 @@ class TestSegmentListAdvancedCases:
class TestSegmentOperationCases:
- def test_segment_add_with_provider_token_error(self, app):
+ def test_segment_add_with_provider_token_error(self, app: Flask):
"""Test segment add with provider token not initialized"""
api = DatasetDocumentSegmentAddApi()
method = unwrap(api.post)
@@ -1117,7 +1118,7 @@ class TestSegmentOperationCases:
with pytest.raises(ProviderTokenNotInitError):
method(api, "ds-1", "doc-1")
- def test_batch_import_with_document_not_found(self, app):
+ def test_batch_import_with_document_not_found(self, app: Flask):
"""Test batch import with document not found"""
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -1146,7 +1147,7 @@ class TestSegmentOperationCases:
with pytest.raises(NotFound):
method(api, "ds-1", "doc-1")
- def test_batch_import_with_invalid_file(self, app):
+ def test_batch_import_with_invalid_file(self, app: Flask):
"""Test batch import with invalid file type"""
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -1181,7 +1182,7 @@ class TestSegmentOperationCases:
with pytest.raises(NotFound):
method(api, "ds-1", "doc-1")
- def test_batch_import_with_async_task_failure(self, app):
+ def test_batch_import_with_async_task_failure(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.post)
@@ -1226,7 +1227,7 @@ class TestSegmentOperationCases:
assert status == 500
assert "error" in response
- def test_batch_import_get_job_not_found(self, app):
+ def test_batch_import_get_job_not_found(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/datasets/test_external.py b/api/tests/unit_tests/controllers/console/datasets/test_external.py
index 514bbbe040..7254bf7670 100644
--- a/api/tests/unit_tests/controllers/console/datasets/test_external.py
+++ b/api/tests/unit_tests/controllers/console/datasets/test_external.py
@@ -57,7 +57,7 @@ def mock_auth(monkeypatch, current_user):
class TestExternalApiTemplateListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = ExternalApiTemplateListApi()
method = unwrap(api.get)
@@ -78,7 +78,7 @@ class TestExternalApiTemplateListApi:
assert resp["total"] == 1
assert resp["data"][0]["id"] == "1"
- def test_post_forbidden(self, app, current_user):
+ def test_post_forbidden(self, app: Flask, current_user):
current_user.is_dataset_editor = False
api = ExternalApiTemplateListApi()
method = unwrap(api.post)
@@ -93,7 +93,7 @@ class TestExternalApiTemplateListApi:
with pytest.raises(Forbidden):
method(api)
- def test_post_duplicate_name(self, app):
+ def test_post_duplicate_name(self, app: Flask):
api = ExternalApiTemplateListApi()
method = unwrap(api.post)
@@ -114,7 +114,7 @@ class TestExternalApiTemplateListApi:
class TestExternalApiTemplateApi:
- def test_get_not_found(self, app):
+ def test_get_not_found(self, app: Flask):
api = ExternalApiTemplateApi()
method = unwrap(api.get)
@@ -129,7 +129,7 @@ class TestExternalApiTemplateApi:
with pytest.raises(NotFound):
method(api, "api-id")
- def test_delete_forbidden(self, app, current_user):
+ def test_delete_forbidden(self, app: Flask, current_user):
current_user.has_edit_permission = False
current_user.is_dataset_operator = False
@@ -142,7 +142,7 @@ class TestExternalApiTemplateApi:
class TestExternalApiUseCheckApi:
- def test_get_scopes_usage_check_to_current_tenant(self, app):
+ def test_get_scopes_usage_check_to_current_tenant(self, app: Flask):
api = ExternalApiUseCheckApi()
method = unwrap(api.get)
@@ -162,7 +162,7 @@ class TestExternalApiUseCheckApi:
class TestExternalDatasetCreateApi:
- def test_create_success(self, app):
+ def test_create_success(self, app: Flask):
api = ExternalDatasetCreateApi()
method = unwrap(api.post)
@@ -206,7 +206,7 @@ class TestExternalDatasetCreateApi:
assert status == 201
- def test_create_forbidden(self, app, current_user):
+ def test_create_forbidden(self, app: Flask, current_user):
current_user.is_dataset_editor = False
api = ExternalDatasetCreateApi()
method = unwrap(api.post)
@@ -226,7 +226,7 @@ class TestExternalDatasetCreateApi:
class TestExternalKnowledgeHitTestingApi:
- def test_hit_testing_dataset_not_found(self, app):
+ def test_hit_testing_dataset_not_found(self, app: Flask):
api = ExternalKnowledgeHitTestingApi()
method = unwrap(api.post)
@@ -241,7 +241,7 @@ class TestExternalKnowledgeHitTestingApi:
with pytest.raises(NotFound):
method(api, "dataset-id")
- def test_hit_testing_success(self, app):
+ def test_hit_testing_success(self, app: Flask):
api = ExternalKnowledgeHitTestingApi()
method = unwrap(api.post)
@@ -266,7 +266,7 @@ class TestExternalKnowledgeHitTestingApi:
class TestBedrockRetrievalApi:
- def test_bedrock_retrieval(self, app):
+ def test_bedrock_retrieval(self, app: Flask):
api = BedrockRetrievalApi()
method = unwrap(api.post)
diff --git a/api/tests/unit_tests/controllers/console/datasets/test_metadata.py b/api/tests/unit_tests/controllers/console/datasets/test_metadata.py
index de834c2d4d..0105aacd65 100644
--- a/api/tests/unit_tests/controllers/console/datasets/test_metadata.py
+++ b/api/tests/unit_tests/controllers/console/datasets/test_metadata.py
@@ -269,7 +269,7 @@ class TestDatasetMetadataApi:
class TestDatasetMetadataBuiltInFieldApi:
- def test_get_built_in_fields(self, app):
+ def test_get_built_in_fields(self, app: Flask):
api = DatasetMetadataBuiltInFieldApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/explore/test_banner.py b/api/tests/unit_tests/controllers/console/explore/test_banner.py
index c8f674f515..d1cb6b6a03 100644
--- a/api/tests/unit_tests/controllers/console/explore/test_banner.py
+++ b/api/tests/unit_tests/controllers/console/explore/test_banner.py
@@ -1,6 +1,8 @@
from datetime import datetime
from unittest.mock import MagicMock, patch
+from flask import Flask
+
import controllers.console.explore.banner as banner_module
from models.enums import BannerStatus
@@ -12,7 +14,7 @@ def unwrap(func):
class TestBannerApi:
- def test_get_banners_with_requested_language(self, app):
+ def test_get_banners_with_requested_language(self, app: Flask):
api = banner_module.BannerApi()
method = unwrap(api.get)
@@ -41,7 +43,7 @@ class TestBannerApi:
}
]
- def test_get_banners_fallback_to_en_us(self, app):
+ def test_get_banners_fallback_to_en_us(self, app: Flask):
api = banner_module.BannerApi()
method = unwrap(api.get)
@@ -76,7 +78,7 @@ class TestBannerApi:
}
]
- def test_get_banners_default_language_en_us(self, app):
+ def test_get_banners_default_language_en_us(self, app: Flask):
api = banner_module.BannerApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/explore/test_message.py b/api/tests/unit_tests/controllers/console/explore/test_message.py
index 145cc9cdd7..3d41489435 100644
--- a/api/tests/unit_tests/controllers/console/explore/test_message.py
+++ b/api/tests/unit_tests/controllers/console/explore/test_message.py
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import InternalServerError, NotFound
import controllers.console.explore.message as module
@@ -54,7 +55,7 @@ def make_message():
class TestMessageListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = module.MessageListApi()
method = unwrap(api.get)
@@ -96,7 +97,7 @@ class TestMessageListApi:
with pytest.raises(NotChatAppError):
method(installed_app)
- def test_conversation_not_exists(self, app):
+ def test_conversation_not_exists(self, app: Flask):
api = module.MessageListApi()
method = unwrap(api.get)
@@ -118,7 +119,7 @@ class TestMessageListApi:
with pytest.raises(NotFound):
method(installed_app)
- def test_first_message_not_exists(self, app):
+ def test_first_message_not_exists(self, app: Flask):
api = module.MessageListApi()
method = unwrap(api.get)
@@ -142,7 +143,7 @@ class TestMessageListApi:
class TestMessageFeedbackApi:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = module.MessageFeedbackApi()
method = unwrap(api.post)
@@ -161,7 +162,7 @@ class TestMessageFeedbackApi:
assert result["result"] == "success"
- def test_message_not_exists(self, app):
+ def test_message_not_exists(self, app: Flask):
api = module.MessageFeedbackApi()
method = unwrap(api.post)
@@ -182,7 +183,7 @@ class TestMessageFeedbackApi:
class TestMessageMoreLikeThisApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = module.MessageMoreLikeThisApi()
method = unwrap(api.get)
@@ -221,7 +222,7 @@ class TestMessageMoreLikeThisApi:
with pytest.raises(NotCompletionAppError):
method(installed_app, "mid")
- def test_more_like_this_disabled(self, app):
+ def test_more_like_this_disabled(self, app: Flask):
api = module.MessageMoreLikeThisApi()
method = unwrap(api.get)
@@ -243,7 +244,7 @@ class TestMessageMoreLikeThisApi:
with pytest.raises(AppMoreLikeThisDisabledError):
method(installed_app, "mid")
- def test_message_not_exists_more_like_this(self, app):
+ def test_message_not_exists_more_like_this(self, app: Flask):
api = module.MessageMoreLikeThisApi()
method = unwrap(api.get)
@@ -265,7 +266,7 @@ class TestMessageMoreLikeThisApi:
with pytest.raises(NotFound):
method(installed_app, "mid")
- def test_provider_not_init_more_like_this(self, app):
+ def test_provider_not_init_more_like_this(self, app: Flask):
api = module.MessageMoreLikeThisApi()
method = unwrap(api.get)
@@ -287,7 +288,7 @@ class TestMessageMoreLikeThisApi:
with pytest.raises(ProviderNotInitializeError):
method(installed_app, "mid")
- def test_quota_exceeded_more_like_this(self, app):
+ def test_quota_exceeded_more_like_this(self, app: Flask):
api = module.MessageMoreLikeThisApi()
method = unwrap(api.get)
@@ -309,7 +310,7 @@ class TestMessageMoreLikeThisApi:
with pytest.raises(ProviderQuotaExceededError):
method(installed_app, "mid")
- def test_model_not_support_more_like_this(self, app):
+ def test_model_not_support_more_like_this(self, app: Flask):
api = module.MessageMoreLikeThisApi()
method = unwrap(api.get)
@@ -331,7 +332,7 @@ class TestMessageMoreLikeThisApi:
with pytest.raises(ProviderModelCurrentlyNotSupportError):
method(installed_app, "mid")
- def test_invoke_error_more_like_this(self, app):
+ def test_invoke_error_more_like_this(self, app: Flask):
api = module.MessageMoreLikeThisApi()
method = unwrap(api.get)
@@ -353,7 +354,7 @@ class TestMessageMoreLikeThisApi:
with pytest.raises(CompletionRequestError):
method(installed_app, "mid")
- def test_unexpected_error_more_like_this(self, app):
+ def test_unexpected_error_more_like_this(self, app: Flask):
api = module.MessageMoreLikeThisApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py b/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py
index 76c863577a..557fded37e 100644
--- a/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py
+++ b/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py
@@ -1,5 +1,7 @@
from unittest.mock import MagicMock, patch
+from flask import Flask
+
import controllers.console.explore.recommended_app as module
from models.model import AppMode, IconType
@@ -11,7 +13,7 @@ def unwrap(func):
class TestRecommendedAppListApi:
- def test_get_with_language_param(self, app):
+ def test_get_with_language_param(self, app: Flask):
api = module.RecommendedAppListApi()
method = unwrap(api.get)
@@ -31,7 +33,7 @@ class TestRecommendedAppListApi:
service_mock.assert_called_once_with("en-US")
assert result == result_data
- def test_get_fallback_to_user_language(self, app):
+ def test_get_fallback_to_user_language(self, app: Flask):
api = module.RecommendedAppListApi()
method = unwrap(api.get)
@@ -51,7 +53,7 @@ class TestRecommendedAppListApi:
service_mock.assert_called_once_with("fr-FR")
assert result == result_data
- def test_get_fallback_to_default_language(self, app):
+ def test_get_fallback_to_default_language(self, app: Flask):
api = module.RecommendedAppListApi()
method = unwrap(api.get)
@@ -73,7 +75,7 @@ class TestRecommendedAppListApi:
class TestRecommendedAppApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = module.RecommendedAppApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/explore/test_saved_message.py b/api/tests/unit_tests/controllers/console/explore/test_saved_message.py
index bb7cdd55c4..71241890e9 100644
--- a/api/tests/unit_tests/controllers/console/explore/test_saved_message.py
+++ b/api/tests/unit_tests/controllers/console/explore/test_saved_message.py
@@ -2,6 +2,7 @@ from unittest.mock import MagicMock, PropertyMock, patch
from uuid import uuid4
import pytest
+from flask import Flask
from werkzeug.exceptions import NotFound
import controllers.console.explore.saved_message as module
@@ -42,7 +43,7 @@ def payload_patch():
class TestSavedMessageListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = module.SavedMessageListApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/explore/test_trial.py b/api/tests/unit_tests/controllers/console/explore/test_trial.py
index 3625056af9..14f00e6295 100644
--- a/api/tests/unit_tests/controllers/console/explore/test_trial.py
+++ b/api/tests/unit_tests/controllers/console/explore/test_trial.py
@@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
from uuid import uuid4
import pytest
+from flask import Flask
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import controllers.console.explore.trial as module
@@ -88,7 +89,7 @@ def valid_parameters():
class TestTrialAppWorkflowRunApi:
- def test_not_workflow_app(self, app):
+ def test_not_workflow_app(self, app: Flask):
api = module.TrialAppWorkflowRunApi()
method = unwrap(api.post)
@@ -224,7 +225,7 @@ class TestTrialAppWorkflowRunApi:
class TestTrialChatApi:
- def test_not_chat_app(self, app):
+ def test_not_chat_app(self, app: Flask):
api = module.TrialChatApi()
method = unwrap(api.post)
@@ -408,7 +409,7 @@ class TestTrialChatApi:
class TestTrialCompletionApi:
- def test_not_completion_app(self, app):
+ def test_not_completion_app(self, app: Flask):
api = module.TrialCompletionApi()
method = unwrap(api.post)
@@ -560,7 +561,7 @@ class TestTrialCompletionApi:
class TestTrialMessageSuggestedQuestionApi:
- def test_not_chat_app(self, app):
+ def test_not_chat_app(self, app: Flask):
api = module.TrialMessageSuggestedQuestionApi()
method = unwrap(api.get)
@@ -952,7 +953,7 @@ class TestTrialAppWorkflowTaskStopApi:
class TestTrialSitApi:
- def test_no_site(self, app):
+ def test_no_site(self, app: Flask):
api = module.TrialSitApi()
method = unwrap(api.get)
app_model = MagicMock()
@@ -963,7 +964,7 @@ class TestTrialSitApi:
with pytest.raises(Forbidden):
method(api, app_model)
- def test_archived_tenant(self, app):
+ def test_archived_tenant(self, app: Flask):
api = module.TrialSitApi()
method = unwrap(api.get)
@@ -978,7 +979,7 @@ class TestTrialSitApi:
with pytest.raises(Forbidden):
method(api, app_model)
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = module.TrialSitApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/tag/test_tags.py b/api/tests/unit_tests/controllers/console/tag/test_tags.py
index a26d171649..8b47da25fb 100644
--- a/api/tests/unit_tests/controllers/console/tag/test_tags.py
+++ b/api/tests/unit_tests/controllers/console/tag/test_tags.py
@@ -73,7 +73,7 @@ def payload_patch():
class TestTagListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = TagListApi()
method = unwrap(api.get)
@@ -124,7 +124,7 @@ class TestTagListApi:
assert result["name"] == "test-tag"
assert result["binding_count"] == "0"
- def test_post_forbidden(self, app, readonly_user, payload_patch):
+ def test_post_forbidden(self, app: Flask, readonly_user, payload_patch):
api = TagListApi()
method = unwrap(api.post)
@@ -170,7 +170,7 @@ class TestTagUpdateDeleteApi:
assert status == 200
assert result["binding_count"] == "3"
- def test_patch_forbidden(self, app, readonly_user, payload_patch):
+ def test_patch_forbidden(self, app: Flask, readonly_user, payload_patch):
api = TagUpdateDeleteApi()
method = unwrap(api.patch)
@@ -231,7 +231,7 @@ class TestTagBindingCollectionApi:
assert status == 200
assert result["result"] == "success"
- def test_create_forbidden(self, app, readonly_user, payload_patch):
+ def test_create_forbidden(self, app: Flask, readonly_user, payload_patch):
api = TagBindingCollectionApi()
method = unwrap(api.post)
@@ -275,7 +275,7 @@ class TestTagBindingRemoveApi:
assert status == 200
assert result["result"] == "success"
- def test_remove_forbidden(self, app, readonly_user, payload_patch):
+ def test_remove_forbidden(self, app: Flask, readonly_user, payload_patch):
api = TagBindingRemoveApi()
method = unwrap(api.post)
diff --git a/api/tests/unit_tests/controllers/console/test_files.py b/api/tests/unit_tests/controllers/console/test_files.py
index 5df9daa7f8..eebc6f9d60 100644
--- a/api/tests/unit_tests/controllers/console/test_files.py
+++ b/api/tests/unit_tests/controllers/console/test_files.py
@@ -82,7 +82,7 @@ def mock_file_service(mock_db):
class TestFileApiGet:
- def test_get_upload_config(self, app):
+ def test_get_upload_config(self, app: Flask):
api = FileApi()
get_method = unwrap(api.get)
@@ -290,7 +290,7 @@ class TestFilePreviewApi:
class TestFileSupportTypeApi:
- def test_get_supported_types(self, app):
+ def test_get_supported_types(self, app: Flask):
api = FileSupportTypeApi()
get_method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/test_workspace_account.py b/api/tests/unit_tests/controllers/console/test_workspace_account.py
index 0b1a32581a..4b4f968c8f 100644
--- a/api/tests/unit_tests/controllers/console/test_workspace_account.py
+++ b/api/tests/unit_tests/controllers/console/test_workspace_account.py
@@ -58,7 +58,7 @@ class TestChangeEmailSend:
mock_get_change_data,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
mock_features.return_value = SimpleNamespace(enable_change_email=True)
mock_account = _build_account("current@example.com", "acc1")
@@ -107,7 +107,7 @@ class TestChangeEmailSend:
mock_get_change_data,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
"""GHSA-4q3w-q5mc-45rq: a phase-1 token must not unlock the new-email send step."""
from controllers.console.auth.error import InvalidTokenError
@@ -155,7 +155,7 @@ class TestChangeEmailValidity:
mock_reset_rate,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
mock_features.return_value = SimpleNamespace(enable_change_email=True)
mock_account = _build_account("user@example.com", "acc2")
@@ -214,7 +214,7 @@ class TestChangeEmailValidity:
mock_reset_rate,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
mock_features.return_value = SimpleNamespace(enable_change_email=True)
mock_current_account.return_value = (_build_account("old@example.com", "acc"), None)
@@ -267,7 +267,7 @@ class TestChangeEmailValidity:
mock_reset_rate,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
"""A token whose phase marker is a string but not a known transition must be rejected."""
from controllers.console.auth.error import InvalidTokenError
@@ -316,7 +316,7 @@ class TestChangeEmailValidity:
mock_reset_rate,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
"""A token minted without a phase marker (e.g. a hand-crafted token) must not validate."""
from controllers.console.auth.error import InvalidTokenError
@@ -366,7 +366,7 @@ class TestChangeEmailReset:
mock_send_notify,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
mock_features.return_value = SimpleNamespace(enable_change_email=True)
current_user = _build_account("old@example.com", "acc3")
@@ -418,7 +418,7 @@ class TestChangeEmailReset:
mock_send_notify,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
"""GHSA-4q3w-q5mc-45rq PoC: phase-1 token must not be usable against /reset."""
from controllers.console.auth.error import InvalidTokenError
@@ -471,7 +471,7 @@ class TestChangeEmailReset:
mock_send_notify,
mock_current_account,
mock_db,
- app,
+ app: Flask,
):
"""A verified token for address A must not be replayed to change to address B."""
from controllers.console.auth.error import InvalidTokenError
@@ -547,7 +547,7 @@ class TestAccountServiceSendChangeEmailEmail:
class TestAccountDeletionFeedback:
@patch("controllers.console.wraps.db")
@patch("controllers.console.workspace.account.BillingService.update_account_deletion_feedback")
- def test_should_normalize_feedback_email(self, mock_update, mock_db, app):
+ def test_should_normalize_feedback_email(self, mock_update, mock_db, app: Flask):
with app.test_request_context(
"/account/delete/feedback",
method="POST",
@@ -563,7 +563,7 @@ class TestCheckEmailUnique:
@patch("controllers.console.wraps.db")
@patch("controllers.console.workspace.account.AccountService.check_email_unique")
@patch("controllers.console.workspace.account.AccountService.is_account_in_freeze")
- def test_should_normalize_email(self, mock_is_freeze, mock_check_unique, mock_db, app):
+ def test_should_normalize_email(self, mock_is_freeze, mock_check_unique, mock_db, app: Flask):
mock_is_freeze.return_value = False
mock_check_unique.return_value = True
diff --git a/api/tests/unit_tests/controllers/console/test_workspace_members.py b/api/tests/unit_tests/controllers/console/test_workspace_members.py
index 811bf5b1e7..412d6a6c52 100644
--- a/api/tests/unit_tests/controllers/console/test_workspace_members.py
+++ b/api/tests/unit_tests/controllers/console/test_workspace_members.py
@@ -43,7 +43,7 @@ class TestMemberInviteEmailApi:
mock_current_account,
mock_invite_member,
mock_get_features,
- app,
+ app: Flask,
):
mock_get_features.return_value = _build_feature_flags()
mock_invite_member.return_value = "token-abc"
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_accounts.py b/api/tests/unit_tests/controllers/console/workspace/test_accounts.py
index bbe9d09521..064726da05 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_accounts.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_accounts.py
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, PropertyMock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import NotFound
from controllers.console import console_ns
@@ -41,7 +42,7 @@ def unwrap(func):
class TestAccountInitApi:
- def test_init_success(self, app):
+ def test_init_success(self, app: Flask):
api = AccountInitApi()
method = unwrap(api.post)
@@ -64,7 +65,7 @@ class TestAccountInitApi:
assert resp["result"] == "success"
- def test_init_already_initialized(self, app):
+ def test_init_already_initialized(self, app: Flask):
api = AccountInitApi()
method = unwrap(api.post)
@@ -79,7 +80,7 @@ class TestAccountInitApi:
class TestAccountProfileApi:
- def test_get_profile_success(self, app):
+ def test_get_profile_success(self, app: Flask):
api = AccountProfileApi()
method = unwrap(api.get)
@@ -140,7 +141,7 @@ class TestAccountUpdateApis:
class TestAccountAvatarApiGet:
"""GET /account/avatar must not sign arbitrary upload_file IDs (IDOR)."""
- def test_get_avatar_signed_url_when_upload_owned_by_current_account(self, app):
+ def test_get_avatar_signed_url_when_upload_owned_by_current_account(self, app: Flask):
api = AccountAvatarApi()
method = unwrap(api.get)
@@ -172,7 +173,7 @@ class TestAccountAvatarApiGet:
assert result == {"avatar_url": "https://signed/example"}
sign_mock.assert_called_once_with(upload_file_id=file_id)
- def test_get_avatar_not_found_when_upload_created_by_other_account_same_tenant(self, app):
+ def test_get_avatar_not_found_when_upload_created_by_other_account_same_tenant(self, app: Flask):
api = AccountAvatarApi()
method = unwrap(api.get)
@@ -204,7 +205,7 @@ class TestAccountAvatarApiGet:
sign_mock.assert_not_called()
- def test_get_avatar_not_found_when_upload_belongs_to_other_tenant(self, app):
+ def test_get_avatar_not_found_when_upload_belongs_to_other_tenant(self, app: Flask):
api = AccountAvatarApi()
method = unwrap(api.get)
@@ -236,7 +237,7 @@ class TestAccountAvatarApiGet:
sign_mock.assert_not_called()
- def test_get_avatar_https_pass_through_without_signing(self, app):
+ def test_get_avatar_https_pass_through_without_signing(self, app: Flask):
api = AccountAvatarApi()
method = unwrap(api.get)
@@ -263,7 +264,7 @@ class TestAccountAvatarApiGet:
class TestAccountPasswordApi:
- def test_password_success(self, app):
+ def test_password_success(self, app: Flask):
api = AccountPasswordApi()
method = unwrap(api.post)
@@ -292,7 +293,7 @@ class TestAccountPasswordApi:
assert result["id"] == "u1"
- def test_password_wrong_current(self, app):
+ def test_password_wrong_current(self, app: Flask):
api = AccountPasswordApi()
method = unwrap(api.post)
@@ -317,7 +318,7 @@ class TestAccountPasswordApi:
class TestAccountIntegrateApi:
- def test_get_integrates(self, app):
+ def test_get_integrates(self, app: Flask):
api = AccountIntegrateApi()
method = unwrap(api.get)
@@ -336,7 +337,7 @@ class TestAccountIntegrateApi:
class TestAccountDeleteApi:
- def test_delete_verify_success(self, app):
+ def test_delete_verify_success(self, app: Flask):
api = AccountDeleteVerifyApi()
method = unwrap(api.get)
@@ -358,7 +359,7 @@ class TestAccountDeleteApi:
assert result["result"] == "success"
- def test_delete_invalid_code(self, app):
+ def test_delete_invalid_code(self, app: Flask):
api = AccountDeleteApi()
method = unwrap(api.post)
@@ -379,7 +380,7 @@ class TestAccountDeleteApi:
class TestChangeEmailApis:
- def test_check_email_code_invalid(self, app):
+ def test_check_email_code_invalid(self, app: Flask):
api = ChangeEmailCheckApi()
method = unwrap(api.post)
@@ -405,7 +406,7 @@ class TestChangeEmailApis:
with pytest.raises(EmailCodeError):
method(api)
- def test_reset_email_already_used(self, app):
+ def test_reset_email_already_used(self, app: Flask):
api = ChangeEmailResetApi()
method = unwrap(api.post)
@@ -427,7 +428,7 @@ class TestChangeEmailApis:
class TestCheckEmailUniqueApi:
- def test_email_unique_success(self, app):
+ def test_email_unique_success(self, app: Flask):
api = CheckEmailUnique()
method = unwrap(api.post)
@@ -448,7 +449,7 @@ class TestCheckEmailUniqueApi:
assert result["result"] == "success"
- def test_email_in_freeze(self, app):
+ def test_email_in_freeze(self, app: Flask):
api = CheckEmailUnique()
method = unwrap(api.post)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_agent_providers.py b/api/tests/unit_tests/controllers/console/workspace/test_agent_providers.py
index b4e03f681d..eb0ca15d2e 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_agent_providers.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_agent_providers.py
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from controllers.console.error import AccountNotFound
from controllers.console.workspace.agent_providers import (
@@ -16,7 +17,7 @@ def unwrap(func):
class TestAgentProviderListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = AgentProviderListApi()
method = unwrap(api.get)
@@ -39,7 +40,7 @@ class TestAgentProviderListApi:
assert result == providers
- def test_get_empty_list(self, app):
+ def test_get_empty_list(self, app: Flask):
api = AgentProviderListApi()
method = unwrap(api.get)
@@ -61,7 +62,7 @@ class TestAgentProviderListApi:
assert result == []
- def test_get_account_not_found(self, app):
+ def test_get_account_not_found(self, app: Flask):
api = AgentProviderListApi()
method = unwrap(api.get)
@@ -77,7 +78,7 @@ class TestAgentProviderListApi:
class TestAgentProviderApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = AgentProviderApi()
method = unwrap(api.get)
@@ -101,7 +102,7 @@ class TestAgentProviderApi:
assert result == provider_data
- def test_get_provider_not_found(self, app):
+ def test_get_provider_not_found(self, app: Flask):
api = AgentProviderApi()
method = unwrap(api.get)
@@ -124,7 +125,7 @@ class TestAgentProviderApi:
assert result is None
- def test_get_account_not_found(self, app):
+ def test_get_account_not_found(self, app: Flask):
api = AgentProviderApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py b/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py
index 0b3d7ef6d7..ed7b2d606f 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_endpoint.py
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from controllers.console import console_ns
from controllers.console.workspace.endpoint import (
@@ -39,7 +40,7 @@ def patch_current_account(user_and_tenant):
@pytest.mark.usefixtures("patch_current_account")
class TestEndpointCollectionApi:
- def test_create_success(self, app):
+ def test_create_success(self, app: Flask):
api = EndpointCollectionApi()
method = unwrap(api.post)
@@ -57,7 +58,7 @@ class TestEndpointCollectionApi:
assert result["success"] is True
- def test_create_permission_denied(self, app):
+ def test_create_permission_denied(self, app: Flask):
api = EndpointCollectionApi()
method = unwrap(api.post)
@@ -77,7 +78,7 @@ class TestEndpointCollectionApi:
with pytest.raises(ValueError):
method(api)
- def test_create_validation_error(self, app):
+ def test_create_validation_error(self, app: Flask):
api = EndpointCollectionApi()
method = unwrap(api.post)
@@ -96,7 +97,7 @@ class TestEndpointCollectionApi:
@pytest.mark.usefixtures("patch_current_account")
class TestDeprecatedEndpointCreateApi:
- def test_create_success(self, app):
+ def test_create_success(self, app: Flask):
api = DeprecatedEndpointCreateApi()
method = unwrap(api.post)
@@ -117,7 +118,7 @@ class TestDeprecatedEndpointCreateApi:
@pytest.mark.usefixtures("patch_current_account")
class TestEndpointListApi:
- def test_list_success(self, app):
+ def test_list_success(self, app: Flask):
api = EndpointListApi()
method = unwrap(api.get)
@@ -130,7 +131,7 @@ class TestEndpointListApi:
assert "endpoints" in result
assert len(result["endpoints"]) == 1
- def test_list_invalid_query(self, app):
+ def test_list_invalid_query(self, app: Flask):
api = EndpointListApi()
method = unwrap(api.get)
@@ -143,7 +144,7 @@ class TestEndpointListApi:
@pytest.mark.usefixtures("patch_current_account")
class TestEndpointListForSinglePluginApi:
- def test_list_for_plugin_success(self, app):
+ def test_list_for_plugin_success(self, app: Flask):
api = EndpointListForSinglePluginApi()
method = unwrap(api.get)
@@ -158,7 +159,7 @@ class TestEndpointListForSinglePluginApi:
assert "endpoints" in result
- def test_list_for_plugin_missing_param(self, app):
+ def test_list_for_plugin_missing_param(self, app: Flask):
api = EndpointListForSinglePluginApi()
method = unwrap(api.get)
@@ -171,7 +172,7 @@ class TestEndpointListForSinglePluginApi:
@pytest.mark.usefixtures("patch_current_account")
class TestEndpointItemApi:
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = EndpointItemApi()
method = unwrap(api.delete)
@@ -187,7 +188,7 @@ class TestEndpointItemApi:
assert result["success"] is True
mock_delete.assert_called_once_with(tenant_id="t1", user_id="u1", endpoint_id="e1")
- def test_delete_service_failure(self, app):
+ def test_delete_service_failure(self, app: Flask):
api = EndpointItemApi()
method = unwrap(api.delete)
@@ -199,7 +200,7 @@ class TestEndpointItemApi:
assert result["success"] is False
- def test_update_success(self, app):
+ def test_update_success(self, app: Flask):
api = EndpointItemApi()
method = unwrap(api.patch)
@@ -226,7 +227,7 @@ class TestEndpointItemApi:
settings={"x": 1},
)
- def test_update_validation_error(self, app):
+ def test_update_validation_error(self, app: Flask):
api = EndpointItemApi()
method = unwrap(api.patch)
@@ -238,7 +239,7 @@ class TestEndpointItemApi:
with pytest.raises(ValueError):
method(api, "e1")
- def test_update_service_failure(self, app):
+ def test_update_service_failure(self, app: Flask):
api = EndpointItemApi()
method = unwrap(api.patch)
@@ -258,7 +259,7 @@ class TestEndpointItemApi:
@pytest.mark.usefixtures("patch_current_account")
class TestDeprecatedEndpointDeleteApi:
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = DeprecatedEndpointDeleteApi()
method = unwrap(api.post)
@@ -272,7 +273,7 @@ class TestDeprecatedEndpointDeleteApi:
assert result["success"] is True
- def test_delete_invalid_payload(self, app):
+ def test_delete_invalid_payload(self, app: Flask):
api = DeprecatedEndpointDeleteApi()
method = unwrap(api.post)
@@ -282,7 +283,7 @@ class TestDeprecatedEndpointDeleteApi:
with pytest.raises(ValueError):
method(api)
- def test_delete_service_failure(self, app):
+ def test_delete_service_failure(self, app: Flask):
api = DeprecatedEndpointDeleteApi()
method = unwrap(api.post)
@@ -299,7 +300,7 @@ class TestDeprecatedEndpointDeleteApi:
@pytest.mark.usefixtures("patch_current_account")
class TestDeprecatedEndpointUpdateApi:
- def test_update_success(self, app):
+ def test_update_success(self, app: Flask):
api = DeprecatedEndpointUpdateApi()
method = unwrap(api.post)
@@ -317,7 +318,7 @@ class TestDeprecatedEndpointUpdateApi:
assert result["success"] is True
- def test_update_validation_error(self, app):
+ def test_update_validation_error(self, app: Flask):
api = DeprecatedEndpointUpdateApi()
method = unwrap(api.post)
@@ -329,7 +330,7 @@ class TestDeprecatedEndpointUpdateApi:
with pytest.raises(ValueError):
method(api)
- def test_update_service_failure(self, app):
+ def test_update_service_failure(self, app: Flask):
api = DeprecatedEndpointUpdateApi()
method = unwrap(api.post)
@@ -380,7 +381,7 @@ class TestEndpointRouteMetadata:
@pytest.mark.usefixtures("patch_current_account")
class TestEndpointEnableApi:
- def test_enable_success(self, app):
+ def test_enable_success(self, app: Flask):
api = EndpointEnableApi()
method = unwrap(api.post)
@@ -394,7 +395,7 @@ class TestEndpointEnableApi:
assert result["success"] is True
- def test_enable_invalid_payload(self, app):
+ def test_enable_invalid_payload(self, app: Flask):
api = EndpointEnableApi()
method = unwrap(api.post)
@@ -404,7 +405,7 @@ class TestEndpointEnableApi:
with pytest.raises(ValueError):
method(api)
- def test_enable_service_failure(self, app):
+ def test_enable_service_failure(self, app: Flask):
api = EndpointEnableApi()
method = unwrap(api.post)
@@ -421,7 +422,7 @@ class TestEndpointEnableApi:
@pytest.mark.usefixtures("patch_current_account")
class TestEndpointDisableApi:
- def test_disable_success(self, app):
+ def test_disable_success(self, app: Flask):
api = EndpointDisableApi()
method = unwrap(api.post)
@@ -435,7 +436,7 @@ class TestEndpointDisableApi:
assert result["success"] is True
- def test_disable_invalid_payload(self, app):
+ def test_disable_invalid_payload(self, app: Flask):
api = EndpointDisableApi()
method = unwrap(api.post)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_members.py b/api/tests/unit_tests/controllers/console/workspace/test_members.py
index 718b57ba6b..0788ff603c 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_members.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_members.py
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import HTTPException
import services
@@ -34,7 +35,7 @@ def unwrap(func):
class TestMemberListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = MemberListApi()
method = unwrap(api.get)
@@ -59,7 +60,7 @@ class TestMemberListApi:
assert status == 200
assert len(result["accounts"]) == 1
- def test_get_no_tenant(self, app):
+ def test_get_no_tenant(self, app: Flask):
api = MemberListApi()
method = unwrap(api.get)
@@ -74,7 +75,7 @@ class TestMemberListApi:
class TestMemberInviteEmailApi:
- def test_invite_success(self, app):
+ def test_invite_success(self, app: Flask):
api = MemberInviteEmailApi()
method = unwrap(api.post)
@@ -101,7 +102,7 @@ class TestMemberInviteEmailApi:
assert status == 201
assert result["result"] == "success"
- def test_invite_limit_exceeded(self, app):
+ def test_invite_limit_exceeded(self, app: Flask):
api = MemberInviteEmailApi()
method = unwrap(api.post)
@@ -123,7 +124,7 @@ class TestMemberInviteEmailApi:
with pytest.raises(WorkspaceMembersLimitExceeded):
method(api)
- def test_invite_already_member(self, app):
+ def test_invite_already_member(self, app: Flask):
api = MemberInviteEmailApi()
method = unwrap(api.post)
@@ -151,7 +152,7 @@ class TestMemberInviteEmailApi:
assert result["invitation_results"][0]["status"] == "success"
- def test_invite_invalid_role(self, app):
+ def test_invite_invalid_role(self, app: Flask):
api = MemberInviteEmailApi()
method = unwrap(api.post)
@@ -166,7 +167,7 @@ class TestMemberInviteEmailApi:
assert status == 400
assert result["code"] == "invalid-role"
- def test_invite_generic_exception(self, app):
+ def test_invite_generic_exception(self, app: Flask):
api = MemberInviteEmailApi()
method = unwrap(api.post)
@@ -196,7 +197,7 @@ class TestMemberInviteEmailApi:
class TestMemberCancelInviteApi:
- def test_cancel_success(self, app):
+ def test_cancel_success(self, app: Flask):
api = MemberCancelInviteApi()
method = unwrap(api.delete)
@@ -216,7 +217,7 @@ class TestMemberCancelInviteApi:
assert status == 200
assert result["result"] == "success"
- def test_cancel_not_found(self, app):
+ def test_cancel_not_found(self, app: Flask):
api = MemberCancelInviteApi()
method = unwrap(api.delete)
@@ -233,7 +234,7 @@ class TestMemberCancelInviteApi:
with pytest.raises(HTTPException):
method(api, "x")
- def test_cancel_cannot_operate_self(self, app):
+ def test_cancel_cannot_operate_self(self, app: Flask):
api = MemberCancelInviteApi()
method = unwrap(api.delete)
@@ -255,7 +256,7 @@ class TestMemberCancelInviteApi:
assert status == 400
- def test_cancel_no_permission(self, app):
+ def test_cancel_no_permission(self, app: Flask):
api = MemberCancelInviteApi()
method = unwrap(api.delete)
@@ -277,7 +278,7 @@ class TestMemberCancelInviteApi:
assert status == 403
- def test_cancel_member_not_in_tenant(self, app):
+ def test_cancel_member_not_in_tenant(self, app: Flask):
api = MemberCancelInviteApi()
method = unwrap(api.delete)
@@ -301,7 +302,7 @@ class TestMemberCancelInviteApi:
class TestMemberUpdateRoleApi:
- def test_update_success(self, app):
+ def test_update_success(self, app: Flask):
api = MemberUpdateRoleApi()
method = unwrap(api.put)
@@ -324,7 +325,7 @@ class TestMemberUpdateRoleApi:
assert result["result"] == "success"
- def test_update_invalid_role(self, app):
+ def test_update_invalid_role(self, app: Flask):
api = MemberUpdateRoleApi()
method = unwrap(api.put)
@@ -335,7 +336,7 @@ class TestMemberUpdateRoleApi:
assert status == 400
- def test_update_member_not_found(self, app):
+ def test_update_member_not_found(self, app: Flask):
api = MemberUpdateRoleApi()
method = unwrap(api.put)
@@ -354,7 +355,7 @@ class TestMemberUpdateRoleApi:
class TestDatasetOperatorMemberListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = DatasetOperatorMemberListApi()
method = unwrap(api.get)
@@ -381,7 +382,7 @@ class TestDatasetOperatorMemberListApi:
assert status == 200
assert len(result["accounts"]) == 1
- def test_get_no_tenant(self, app):
+ def test_get_no_tenant(self, app: Flask):
api = DatasetOperatorMemberListApi()
method = unwrap(api.get)
@@ -396,7 +397,7 @@ class TestDatasetOperatorMemberListApi:
class TestSendOwnerTransferEmailApi:
- def test_send_success(self, app):
+ def test_send_success(self, app: Flask):
api = SendOwnerTransferEmailApi()
method = unwrap(api.post)
@@ -419,7 +420,7 @@ class TestSendOwnerTransferEmailApi:
assert result["result"] == "success"
- def test_send_ip_limit(self, app):
+ def test_send_ip_limit(self, app: Flask):
api = SendOwnerTransferEmailApi()
method = unwrap(api.post)
@@ -433,7 +434,7 @@ class TestSendOwnerTransferEmailApi:
with pytest.raises(EmailSendIpLimitError):
method(api)
- def test_send_not_owner(self, app):
+ def test_send_not_owner(self, app: Flask):
api = SendOwnerTransferEmailApi()
method = unwrap(api.post)
@@ -452,7 +453,7 @@ class TestSendOwnerTransferEmailApi:
class TestOwnerTransferCheckApi:
- def test_check_invalid_code(self, app):
+ def test_check_invalid_code(self, app: Flask):
api = OwnerTransferCheckApi()
method = unwrap(api.post)
@@ -477,7 +478,7 @@ class TestOwnerTransferCheckApi:
with pytest.raises(EmailCodeError):
method(api)
- def test_rate_limited(self, app):
+ def test_rate_limited(self, app: Flask):
api = OwnerTransferCheckApi()
method = unwrap(api.post)
@@ -498,7 +499,7 @@ class TestOwnerTransferCheckApi:
with pytest.raises(OwnerTransferLimitError):
method(api)
- def test_invalid_token(self, app):
+ def test_invalid_token(self, app: Flask):
api = OwnerTransferCheckApi()
method = unwrap(api.post)
@@ -520,7 +521,7 @@ class TestOwnerTransferCheckApi:
with pytest.raises(InvalidTokenError):
method(api)
- def test_invalid_email(self, app):
+ def test_invalid_email(self, app: Flask):
api = OwnerTransferCheckApi()
method = unwrap(api.post)
@@ -547,7 +548,7 @@ class TestOwnerTransferCheckApi:
class TestOwnerTransferApi:
- def test_transfer_self(self, app):
+ def test_transfer_self(self, app: Flask):
api = OwnerTransfer()
method = unwrap(api.post)
@@ -564,7 +565,7 @@ class TestOwnerTransferApi:
with pytest.raises(CannotTransferOwnerToSelfError):
method(api, "1")
- def test_invalid_token(self, app):
+ def test_invalid_token(self, app: Flask):
api = OwnerTransfer()
method = unwrap(api.post)
@@ -582,7 +583,7 @@ class TestOwnerTransferApi:
with pytest.raises(InvalidTokenError):
method(api, "2")
- def test_member_not_in_tenant(self, app):
+ def test_member_not_in_tenant(self, app: Flask):
api = OwnerTransfer()
method = unwrap(api.post)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py b/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py
index 168479af1e..e836a3cc55 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py
@@ -1,6 +1,7 @@
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from pydantic_core import ValidationError
from werkzeug.exceptions import Forbidden
@@ -26,7 +27,7 @@ def unwrap(func):
class TestModelProviderListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = ModelProviderListApi()
method = unwrap(api.get)
@@ -47,7 +48,7 @@ class TestModelProviderListApi:
class TestModelProviderCredentialApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = ModelProviderCredentialApi()
method = unwrap(api.get)
@@ -66,7 +67,7 @@ class TestModelProviderCredentialApi:
assert "credentials" in result
- def test_get_invalid_uuid(self, app):
+ def test_get_invalid_uuid(self, app: Flask):
api = ModelProviderCredentialApi()
method = unwrap(api.get)
@@ -80,7 +81,7 @@ class TestModelProviderCredentialApi:
with pytest.raises(ValidationError):
method(api, provider="openai")
- def test_post_create_success(self, app):
+ def test_post_create_success(self, app: Flask):
api = ModelProviderCredentialApi()
method = unwrap(api.post)
@@ -102,7 +103,7 @@ class TestModelProviderCredentialApi:
assert result["result"] == "success"
assert status == 201
- def test_post_create_validation_error(self, app):
+ def test_post_create_validation_error(self, app: Flask):
api = ModelProviderCredentialApi()
method = unwrap(api.post)
@@ -122,7 +123,7 @@ class TestModelProviderCredentialApi:
with pytest.raises(ValueError):
method(api, provider="openai")
- def test_put_update_success(self, app):
+ def test_put_update_success(self, app: Flask):
api = ModelProviderCredentialApi()
method = unwrap(api.put)
@@ -143,7 +144,7 @@ class TestModelProviderCredentialApi:
assert result["result"] == "success"
- def test_put_invalid_uuid(self, app):
+ def test_put_invalid_uuid(self, app: Flask):
api = ModelProviderCredentialApi()
method = unwrap(api.put)
@@ -159,7 +160,7 @@ class TestModelProviderCredentialApi:
with pytest.raises(ValidationError):
method(api, provider="openai")
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = ModelProviderCredentialApi()
method = unwrap(api.delete)
@@ -183,7 +184,7 @@ class TestModelProviderCredentialApi:
class TestModelProviderCredentialSwitchApi:
- def test_switch_success(self, app):
+ def test_switch_success(self, app: Flask):
api = ModelProviderCredentialSwitchApi()
method = unwrap(api.post)
@@ -204,7 +205,7 @@ class TestModelProviderCredentialSwitchApi:
assert result["result"] == "success"
- def test_switch_invalid_uuid(self, app):
+ def test_switch_invalid_uuid(self, app: Flask):
api = ModelProviderCredentialSwitchApi()
method = unwrap(api.post)
@@ -222,7 +223,7 @@ class TestModelProviderCredentialSwitchApi:
class TestModelProviderValidateApi:
- def test_validate_success(self, app):
+ def test_validate_success(self, app: Flask):
api = ModelProviderValidateApi()
method = unwrap(api.post)
@@ -243,7 +244,7 @@ class TestModelProviderValidateApi:
assert result["result"] == "success"
- def test_validate_failure(self, app):
+ def test_validate_failure(self, app: Flask):
api = ModelProviderValidateApi()
method = unwrap(api.post)
@@ -266,7 +267,7 @@ class TestModelProviderValidateApi:
class TestModelProviderIconApi:
- def test_icon_success(self, app):
+ def test_icon_success(self, app: Flask):
api = ModelProviderIconApi()
with (
@@ -280,7 +281,7 @@ class TestModelProviderIconApi:
assert response.mimetype == "image/png"
- def test_icon_not_found(self, app):
+ def test_icon_not_found(self, app: Flask):
api = ModelProviderIconApi()
with (
@@ -295,7 +296,7 @@ class TestModelProviderIconApi:
class TestPreferredProviderTypeUpdateApi:
- def test_update_success(self, app):
+ def test_update_success(self, app: Flask):
api = PreferredProviderTypeUpdateApi()
method = unwrap(api.post)
@@ -316,7 +317,7 @@ class TestPreferredProviderTypeUpdateApi:
assert result["result"] == "success"
- def test_invalid_enum(self, app):
+ def test_invalid_enum(self, app: Flask):
api = PreferredProviderTypeUpdateApi()
method = unwrap(api.post)
@@ -334,7 +335,7 @@ class TestPreferredProviderTypeUpdateApi:
class TestModelProviderPaymentCheckoutUrlApi:
- def test_checkout_success(self, app):
+ def test_checkout_success(self, app: Flask):
api = ModelProviderPaymentCheckoutUrlApi()
method = unwrap(api.get)
@@ -359,7 +360,7 @@ class TestModelProviderPaymentCheckoutUrlApi:
assert "url" in result
- def test_invalid_provider(self, app):
+ def test_invalid_provider(self, app: Flask):
api = ModelProviderPaymentCheckoutUrlApi()
method = unwrap(api.get)
@@ -367,7 +368,7 @@ class TestModelProviderPaymentCheckoutUrlApi:
with pytest.raises(ValueError):
method(api, provider="openai")
- def test_permission_denied(self, app):
+ def test_permission_denied(self, app: Flask):
api = ModelProviderPaymentCheckoutUrlApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_models.py b/api/tests/unit_tests/controllers/console/workspace/test_models.py
index f0d32f81fb..4246e3c04c 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_models.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_models.py
@@ -72,7 +72,7 @@ class TestDefaultModelApi:
assert result["result"] == "success"
- def test_get_returns_empty_when_no_default(self, app):
+ def test_get_returns_empty_when_no_default(self, app: Flask):
api = DefaultModelApi()
method = unwrap(api.get)
@@ -154,7 +154,7 @@ class TestModelProviderModelApi:
assert status == 204
- def test_get_models_returns_empty(self, app):
+ def test_get_models_returns_empty(self, app: Flask):
api = ModelProviderModelApi()
method = unwrap(api.get)
@@ -224,7 +224,7 @@ class TestModelProviderModelCredentialApi:
assert status == 201
- def test_get_empty_credentials(self, app):
+ def test_get_empty_credentials(self, app: Flask):
api = ModelProviderModelCredentialApi()
method = unwrap(api.get)
@@ -242,7 +242,7 @@ class TestModelProviderModelCredentialApi:
assert result["credentials"] == {}
- def test_delete_success(self, app):
+ def test_delete_success(self, app: Flask):
api = ModelProviderModelCredentialApi()
method = unwrap(api.delete)
@@ -416,7 +416,7 @@ class TestParameterAndAvailableModels:
assert "data" in result
- def test_empty_rules(self, app):
+ def test_empty_rules(self, app: Flask):
api = ModelProviderModelParameterRuleApi()
method = unwrap(api.get)
@@ -431,7 +431,7 @@ class TestParameterAndAvailableModels:
assert result["data"] == []
- def test_no_models(self, app):
+ def test_no_models(self, app: Flask):
api = ModelProviderAvailableModelApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_plugin.py b/api/tests/unit_tests/controllers/console/workspace/test_plugin.py
index ce5fd1c466..d01bf7d668 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_plugin.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_plugin.py
@@ -2,6 +2,7 @@ import io
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Forbidden
@@ -61,7 +62,7 @@ def tenant():
class TestPluginListLatestVersionsApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginListLatestVersionsApi()
method = unwrap(api.post)
@@ -77,7 +78,7 @@ class TestPluginListLatestVersionsApi:
assert "versions" in result
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginListLatestVersionsApi()
method = unwrap(api.post)
@@ -95,7 +96,7 @@ class TestPluginListLatestVersionsApi:
class TestPluginDebuggingKeyApi:
- def test_debugging_key_success(self, app):
+ def test_debugging_key_success(self, app: Flask):
api = PluginDebuggingKeyApi()
method = unwrap(api.get)
@@ -108,7 +109,7 @@ class TestPluginDebuggingKeyApi:
assert result["key"] == "k"
- def test_debugging_key_error(self, app):
+ def test_debugging_key_error(self, app: Flask):
api = PluginDebuggingKeyApi()
method = unwrap(api.get)
@@ -125,7 +126,7 @@ class TestPluginDebuggingKeyApi:
class TestPluginListApi:
- def test_plugin_list(self, app):
+ def test_plugin_list(self, app: Flask):
api = PluginListApi()
method = unwrap(api.get)
@@ -142,7 +143,7 @@ class TestPluginListApi:
class TestPluginIconApi:
- def test_plugin_icon(self, app):
+ def test_plugin_icon(self, app: Flask):
api = PluginIconApi()
method = unwrap(api.get)
@@ -156,7 +157,7 @@ class TestPluginIconApi:
class TestPluginAssetApi:
- def test_plugin_asset(self, app):
+ def test_plugin_asset(self, app: Flask):
api = PluginAssetApi()
method = unwrap(api.get)
@@ -171,7 +172,7 @@ class TestPluginAssetApi:
class TestPluginUploadFromPkgApi:
- def test_upload_pkg_success(self, app):
+ def test_upload_pkg_success(self, app: Flask):
api = PluginUploadFromPkgApi()
method = unwrap(api.post)
@@ -188,7 +189,7 @@ class TestPluginUploadFromPkgApi:
assert result["ok"] is True
- def test_upload_pkg_too_large(self, app):
+ def test_upload_pkg_too_large(self, app: Flask):
api = PluginUploadFromPkgApi()
method = unwrap(api.post)
@@ -210,7 +211,7 @@ class TestPluginUploadFromPkgApi:
class TestPluginInstallFromPkgApi:
- def test_install_from_pkg(self, app):
+ def test_install_from_pkg(self, app: Flask):
api = PluginInstallFromPkgApi()
method = unwrap(api.post)
@@ -229,7 +230,7 @@ class TestPluginInstallFromPkgApi:
class TestPluginUninstallApi:
- def test_uninstall(self, app):
+ def test_uninstall(self, app: Flask):
api = PluginUninstallApi()
method = unwrap(api.post)
@@ -246,7 +247,7 @@ class TestPluginUninstallApi:
class TestPluginChangePermissionApi:
- def test_change_permission_forbidden(self, app):
+ def test_change_permission_forbidden(self, app: Flask):
api = PluginChangePermissionApi()
method = unwrap(api.post)
@@ -264,7 +265,7 @@ class TestPluginChangePermissionApi:
with pytest.raises(Forbidden):
method(api)
- def test_change_permission_success(self, app):
+ def test_change_permission_success(self, app: Flask):
api = PluginChangePermissionApi()
method = unwrap(api.post)
@@ -286,7 +287,7 @@ class TestPluginChangePermissionApi:
class TestPluginFetchPermissionApi:
- def test_fetch_permission_default(self, app):
+ def test_fetch_permission_default(self, app: Flask):
api = PluginFetchPermissionApi()
method = unwrap(api.get)
@@ -319,7 +320,7 @@ class TestPluginFetchDynamicSelectOptionsApi:
class TestPluginReadmeApi:
- def test_fetch_readme(self, app):
+ def test_fetch_readme(self, app: Flask):
api = PluginReadmeApi()
method = unwrap(api.get)
@@ -334,7 +335,7 @@ class TestPluginReadmeApi:
class TestPluginListInstallationsFromIdsApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginListInstallationsFromIdsApi()
method = unwrap(api.post)
@@ -352,7 +353,7 @@ class TestPluginListInstallationsFromIdsApi:
assert "plugins" in result
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginListInstallationsFromIdsApi()
method = unwrap(api.post)
@@ -371,7 +372,7 @@ class TestPluginListInstallationsFromIdsApi:
class TestPluginUploadFromGithubApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginUploadFromGithubApi()
method = unwrap(api.post)
@@ -388,7 +389,7 @@ class TestPluginUploadFromGithubApi:
assert result["ok"] is True
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginUploadFromGithubApi()
method = unwrap(api.post)
@@ -407,7 +408,7 @@ class TestPluginUploadFromGithubApi:
class TestPluginUploadFromBundleApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginUploadFromBundleApi()
method = unwrap(api.post)
@@ -430,7 +431,7 @@ class TestPluginUploadFromBundleApi:
assert result["ok"] is True
- def test_too_large(self, app):
+ def test_too_large(self, app: Flask):
api = PluginUploadFromBundleApi()
method = unwrap(api.post)
@@ -458,7 +459,7 @@ class TestPluginUploadFromBundleApi:
class TestPluginInstallFromGithubApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginInstallFromGithubApi()
method = unwrap(api.post)
@@ -478,7 +479,7 @@ class TestPluginInstallFromGithubApi:
assert result["ok"] is True
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginInstallFromGithubApi()
method = unwrap(api.post)
@@ -502,7 +503,7 @@ class TestPluginInstallFromGithubApi:
class TestPluginInstallFromMarketplaceApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginInstallFromMarketplaceApi()
method = unwrap(api.post)
@@ -520,7 +521,7 @@ class TestPluginInstallFromMarketplaceApi:
assert result["ok"] is True
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginInstallFromMarketplaceApi()
method = unwrap(api.post)
@@ -539,7 +540,7 @@ class TestPluginInstallFromMarketplaceApi:
class TestPluginFetchMarketplacePkgApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginFetchMarketplacePkgApi()
method = unwrap(api.get)
@@ -552,7 +553,7 @@ class TestPluginFetchMarketplacePkgApi:
assert "manifest" in result
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginFetchMarketplacePkgApi()
method = unwrap(api.get)
@@ -569,7 +570,7 @@ class TestPluginFetchMarketplacePkgApi:
class TestPluginFetchManifestApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginFetchManifestApi()
method = unwrap(api.get)
@@ -585,7 +586,7 @@ class TestPluginFetchManifestApi:
assert "manifest" in result
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginFetchManifestApi()
method = unwrap(api.get)
@@ -602,7 +603,7 @@ class TestPluginFetchManifestApi:
class TestPluginFetchInstallTasksApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginFetchInstallTasksApi()
method = unwrap(api.get)
@@ -615,7 +616,7 @@ class TestPluginFetchInstallTasksApi:
assert "tasks" in result
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginFetchInstallTasksApi()
method = unwrap(api.get)
@@ -632,7 +633,7 @@ class TestPluginFetchInstallTasksApi:
class TestPluginFetchInstallTaskApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginFetchInstallTaskApi()
method = unwrap(api.get)
@@ -645,7 +646,7 @@ class TestPluginFetchInstallTaskApi:
assert "task" in result
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginFetchInstallTaskApi()
method = unwrap(api.get)
@@ -662,7 +663,7 @@ class TestPluginFetchInstallTaskApi:
class TestPluginDeleteInstallTaskApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginDeleteInstallTaskApi()
method = unwrap(api.post)
@@ -675,7 +676,7 @@ class TestPluginDeleteInstallTaskApi:
assert result["success"] is True
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginDeleteInstallTaskApi()
method = unwrap(api.post)
@@ -692,7 +693,7 @@ class TestPluginDeleteInstallTaskApi:
class TestPluginDeleteAllInstallTaskItemsApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginDeleteAllInstallTaskItemsApi()
method = unwrap(api.post)
@@ -707,7 +708,7 @@ class TestPluginDeleteAllInstallTaskItemsApi:
assert result["success"] is True
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginDeleteAllInstallTaskItemsApi()
method = unwrap(api.post)
@@ -724,7 +725,7 @@ class TestPluginDeleteAllInstallTaskItemsApi:
class TestPluginDeleteInstallTaskItemApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginDeleteInstallTaskItemApi()
method = unwrap(api.post)
@@ -737,7 +738,7 @@ class TestPluginDeleteInstallTaskItemApi:
assert result["success"] is True
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginDeleteInstallTaskItemApi()
method = unwrap(api.post)
@@ -754,7 +755,7 @@ class TestPluginDeleteInstallTaskItemApi:
class TestPluginUpgradeFromMarketplaceApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginUpgradeFromMarketplaceApi()
method = unwrap(api.post)
@@ -775,7 +776,7 @@ class TestPluginUpgradeFromMarketplaceApi:
assert result["ok"] is True
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginUpgradeFromMarketplaceApi()
method = unwrap(api.post)
@@ -797,7 +798,7 @@ class TestPluginUpgradeFromMarketplaceApi:
class TestPluginUpgradeFromGithubApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginUpgradeFromGithubApi()
method = unwrap(api.post)
@@ -821,7 +822,7 @@ class TestPluginUpgradeFromGithubApi:
assert result["ok"] is True
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginUpgradeFromGithubApi()
method = unwrap(api.post)
@@ -846,7 +847,7 @@ class TestPluginUpgradeFromGithubApi:
class TestPluginFetchDynamicSelectOptionsWithCredentialsApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginFetchDynamicSelectOptionsWithCredentialsApi()
method = unwrap(api.post)
@@ -873,7 +874,7 @@ class TestPluginFetchDynamicSelectOptionsWithCredentialsApi:
assert result["options"] == [1]
- def test_daemon_error(self, app):
+ def test_daemon_error(self, app: Flask):
api = PluginFetchDynamicSelectOptionsWithCredentialsApi()
method = unwrap(api.post)
@@ -901,7 +902,7 @@ class TestPluginFetchDynamicSelectOptionsWithCredentialsApi:
class TestPluginChangePreferencesApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginChangePreferencesApi()
method = unwrap(api.post)
@@ -931,7 +932,7 @@ class TestPluginChangePreferencesApi:
assert result["success"] is True
- def test_permission_fail(self, app):
+ def test_permission_fail(self, app: Flask):
api = PluginChangePreferencesApi()
method = unwrap(api.post)
@@ -962,7 +963,7 @@ class TestPluginChangePreferencesApi:
class TestPluginFetchPreferencesApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginFetchPreferencesApi()
method = unwrap(api.get)
@@ -996,7 +997,7 @@ class TestPluginFetchPreferencesApi:
class TestPluginAutoUpgradeExcludePluginApi:
- def test_success(self, app):
+ def test_success(self, app: Flask):
api = PluginAutoUpgradeExcludePluginApi()
method = unwrap(api.post)
@@ -1011,7 +1012,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
assert result["success"] is True
- def test_fail(self, app):
+ def test_fail(self, app: Flask):
api = PluginAutoUpgradeExcludePluginApi()
method = unwrap(api.post)
diff --git a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py
index e82a29f045..a52518c2d2 100644
--- a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py
+++ b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py
@@ -2,6 +2,7 @@ from io import BytesIO
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Unauthorized
@@ -37,7 +38,7 @@ def unwrap(func):
class TestTenantListApi:
- def test_get_success_saas_path(self, app):
+ def test_get_success_saas_path(self, app: Flask):
api = TenantListApi()
method = unwrap(api.get)
@@ -85,7 +86,7 @@ class TestTenantListApi:
get_plan_bulk_mock.assert_called_once_with(["t1", "t2"])
get_features_mock.assert_not_called()
- def test_get_saas_path_partial_fallback_does_not_gate_plan_on_billing_enabled(self, app):
+ def test_get_saas_path_partial_fallback_does_not_gate_plan_on_billing_enabled(self, app: Flask):
"""Bulk omits a tenant: resolve plan via subscription.plan only; billing.enabled is not used.
billing.enabled is mocked False to prove the endpoint does not gate on it for this path
@@ -140,7 +141,7 @@ class TestTenantListApi:
get_plan_bulk_mock.assert_called_once_with(["t1", "t2"])
get_features_mock.assert_called_once_with("t2")
- def test_get_saas_path_falls_back_to_legacy_feature_path_on_bulk_error(self, app):
+ def test_get_saas_path_falls_back_to_legacy_feature_path_on_bulk_error(self, app: Flask):
"""Test fallback to FeatureService when bulk billing returns empty result.
BillingService.get_plan_bulk catches exceptions internally and returns empty dict,
@@ -197,7 +198,7 @@ class TestTenantListApi:
assert get_features_mock.call_count == 2
logger_warning_mock.assert_called_once()
- def test_get_billing_disabled_community_path(self, app):
+ def test_get_billing_disabled_community_path(self, app: Flask):
api = TenantListApi()
method = unwrap(api.get)
@@ -236,7 +237,7 @@ class TestTenantListApi:
assert result["workspaces"][0]["plan"] == CloudPlan.SANDBOX
get_features_mock.assert_called_once_with("t1")
- def test_get_enterprise_only_skips_feature_service(self, app):
+ def test_get_enterprise_only_skips_feature_service(self, app: Flask):
api = TenantListApi()
method = unwrap(api.get)
@@ -276,7 +277,7 @@ class TestTenantListApi:
assert result["workspaces"][1]["current"] is True
get_features_mock.assert_not_called()
- def test_get_enterprise_only_with_empty_tenants(self, app):
+ def test_get_enterprise_only_with_empty_tenants(self, app: Flask):
api = TenantListApi()
method = unwrap(api.get)
@@ -302,7 +303,7 @@ class TestTenantListApi:
class TestWorkspaceListApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = WorkspaceListApi()
method = unwrap(api.get)
@@ -324,7 +325,7 @@ class TestWorkspaceListApi:
assert result["total"] == 1
assert result["has_more"] is False
- def test_get_has_next_true(self, app):
+ def test_get_has_next_true(self, app: Flask):
api = WorkspaceListApi()
method = unwrap(api.get)
@@ -355,7 +356,7 @@ class TestWorkspaceListApi:
class TestTenantApi:
- def test_post_active_tenant(self, app):
+ def test_post_active_tenant(self, app: Flask):
api = TenantApi()
method = unwrap(api.post)
@@ -375,7 +376,7 @@ class TestTenantApi:
assert status == 200
assert result["id"] == "t1"
- def test_post_archived_with_switch(self, app):
+ def test_post_archived_with_switch(self, app: Flask):
api = TenantApi()
method = unwrap(api.post)
@@ -397,7 +398,7 @@ class TestTenantApi:
assert result["id"] == "new"
- def test_post_archived_no_tenant(self, app):
+ def test_post_archived_no_tenant(self, app: Flask):
api = TenantApi()
method = unwrap(api.post)
@@ -411,7 +412,7 @@ class TestTenantApi:
with pytest.raises(Unauthorized):
method(api)
- def test_post_info_path(self, app):
+ def test_post_info_path(self, app: Flask):
api = TenantApi()
method = unwrap(api.post)
@@ -454,7 +455,7 @@ class TestTenantInfoResponse:
class TestSwitchWorkspaceApi:
- def test_switch_success(self, app):
+ def test_switch_success(self, app: Flask):
api = SwitchWorkspaceApi()
method = unwrap(api.post)
@@ -477,7 +478,7 @@ class TestSwitchWorkspaceApi:
assert result["result"] == "success"
- def test_switch_not_linked(self, app):
+ def test_switch_not_linked(self, app: Flask):
api = SwitchWorkspaceApi()
method = unwrap(api.post)
@@ -493,7 +494,7 @@ class TestSwitchWorkspaceApi:
with pytest.raises(AccountNotLinkTenantError):
method(api)
- def test_switch_tenant_not_found(self, app):
+ def test_switch_tenant_not_found(self, app: Flask):
api = SwitchWorkspaceApi()
method = unwrap(api.post)
@@ -515,7 +516,7 @@ class TestSwitchWorkspaceApi:
class TestCustomConfigWorkspaceApi:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = CustomConfigWorkspaceApi()
method = unwrap(api.post)
@@ -538,7 +539,7 @@ class TestCustomConfigWorkspaceApi:
assert result["result"] == "success"
- def test_logo_fallback(self, app):
+ def test_logo_fallback(self, app: Flask):
api = CustomConfigWorkspaceApi()
method = unwrap(api.post)
@@ -569,7 +570,7 @@ class TestCustomConfigWorkspaceApi:
class TestWebappLogoWorkspaceApi:
- def test_no_file(self, app):
+ def test_no_file(self, app: Flask):
api = WebappLogoWorkspaceApi()
method = unwrap(api.post)
@@ -582,7 +583,7 @@ class TestWebappLogoWorkspaceApi:
with pytest.raises(NoFileUploadedError):
method(api)
- def test_too_many_files(self, app):
+ def test_too_many_files(self, app: Flask):
api = WebappLogoWorkspaceApi()
method = unwrap(api.post)
@@ -601,7 +602,7 @@ class TestWebappLogoWorkspaceApi:
with pytest.raises(TooManyFilesError):
method(api)
- def test_invalid_extension(self, app):
+ def test_invalid_extension(self, app: Flask):
api = WebappLogoWorkspaceApi()
method = unwrap(api.post)
@@ -616,7 +617,7 @@ class TestWebappLogoWorkspaceApi:
with pytest.raises(UnsupportedFileTypeError):
method(api)
- def test_upload_success(self, app):
+ def test_upload_success(self, app: Flask):
api = WebappLogoWorkspaceApi()
method = unwrap(api.post)
@@ -648,7 +649,7 @@ class TestWebappLogoWorkspaceApi:
assert status == 201
assert result["id"] == "file1"
- def test_filename_missing(self, app):
+ def test_filename_missing(self, app: Flask):
api = WebappLogoWorkspaceApi()
method = unwrap(api.post)
@@ -672,7 +673,7 @@ class TestWebappLogoWorkspaceApi:
with pytest.raises(FilenameNotExistsError):
method(api)
- def test_file_too_large(self, app):
+ def test_file_too_large(self, app: Flask):
api = WebappLogoWorkspaceApi()
method = unwrap(api.post)
@@ -701,7 +702,7 @@ class TestWebappLogoWorkspaceApi:
with pytest.raises(FileTooLargeError):
method(api)
- def test_service_unsupported_file(self, app):
+ def test_service_unsupported_file(self, app: Flask):
api = WebappLogoWorkspaceApi()
method = unwrap(api.post)
@@ -732,7 +733,7 @@ class TestWebappLogoWorkspaceApi:
class TestWorkspaceInfoApi:
- def test_post_success(self, app):
+ def test_post_success(self, app: Flask):
api = WorkspaceInfoApi()
method = unwrap(api.post)
@@ -756,7 +757,7 @@ class TestWorkspaceInfoApi:
assert result["result"] == "success"
- def test_no_current_tenant(self, app):
+ def test_no_current_tenant(self, app: Flask):
api = WorkspaceInfoApi()
method = unwrap(api.post)
@@ -774,7 +775,7 @@ class TestWorkspaceInfoApi:
class TestWorkspacePermissionApi:
- def test_get_success(self, app):
+ def test_get_success(self, app: Flask):
api = WorkspacePermissionApi()
method = unwrap(api.get)
@@ -799,7 +800,7 @@ class TestWorkspacePermissionApi:
assert status == 200
assert result["workspace_id"] == "t1"
- def test_no_current_tenant(self, app):
+ def test_no_current_tenant(self, app: Flask):
api = WorkspacePermissionApi()
method = unwrap(api.get)
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_app.py b/api/tests/unit_tests/controllers/service_api/app/test_app.py
index f5d93b5ac3..ae0edcf382 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_app.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_app.py
@@ -41,7 +41,7 @@ class TestAppParameterApi:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.db")
def test_get_parameters_for_chat_app(
- self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app, mock_app_model
+ self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app: Flask, mock_app_model
):
"""Test retrieving parameters for a chat app."""
# Arrange
@@ -91,7 +91,7 @@ class TestAppParameterApi:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.db")
def test_get_parameters_for_workflow_app(
- self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app, mock_app_model
+ self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app: Flask, mock_app_model
):
"""Test retrieving parameters for a workflow app."""
# Arrange
@@ -136,7 +136,7 @@ class TestAppParameterApi:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.db")
def test_get_parameters_raises_error_when_chat_config_missing(
- self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app, mock_app_model
+ self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app: Flask, mock_app_model
):
"""Test that AppUnavailableError is raised when chat app has no config."""
# Arrange
@@ -174,7 +174,7 @@ class TestAppParameterApi:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.db")
def test_get_parameters_raises_error_when_workflow_missing(
- self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app, mock_app_model
+ self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app: Flask, mock_app_model
):
"""Test that AppUnavailableError is raised when workflow app has no workflow."""
# Arrange
@@ -234,7 +234,14 @@ class TestAppMetaApi:
@patch("controllers.service_api.wraps.db")
@patch("controllers.service_api.app.app.AppService")
def test_get_app_meta(
- self, mock_app_service, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app, mock_app_model
+ self,
+ mock_app_service,
+ mock_db,
+ mock_validate_token,
+ mock_current_app,
+ mock_user_logged_in,
+ app: Flask,
+ mock_app_model,
):
"""Test retrieving app metadata via AppService."""
# Arrange
@@ -310,7 +317,7 @@ class TestAppInfoApi:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.db")
def test_get_app_info(
- self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app, mock_app_model
+ self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app: Flask, mock_app_model
):
"""Test retrieving basic app information."""
mock_current_app.login_manager = Mock()
@@ -402,7 +409,9 @@ class TestAppInfoApi:
@patch("controllers.service_api.wraps.current_app")
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.db")
- def test_get_app_info_with_no_tags(self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app):
+ def test_get_app_info_with_no_tags(
+ self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app: Flask
+ ):
"""Test retrieving app info when app has no tags."""
# Arrange
mock_current_app.login_manager = Mock()
@@ -453,7 +462,7 @@ class TestAppInfoApi:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.db")
def test_get_app_info_returns_correct_mode(
- self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app, app_mode
+ self, mock_db, mock_validate_token, mock_current_app, mock_user_logged_in, app: Flask, app_mode
):
"""Test that all app modes are correctly returned."""
# Arrange
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_audio.py b/api/tests/unit_tests/controllers/service_api/app/test_audio.py
index c16ebad739..4741481ef6 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_audio.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_audio.py
@@ -13,6 +13,7 @@ from types import SimpleNamespace
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import InternalServerError
@@ -190,7 +191,7 @@ class TestAudioServiceMockedBehavior:
class TestAudioApi:
- def test_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AudioService, "transcript_asr", lambda **_kwargs: {"text": "ok"})
api = AudioApi()
handler = _unwrap(api.post)
@@ -216,7 +217,7 @@ class TestAudioApi:
(InvokeError("invoke"), CompletionRequestError),
],
)
- def test_error_mapping(self, app, monkeypatch: pytest.MonkeyPatch, exc, expected) -> None:
+ def test_error_mapping(self, app: Flask, monkeypatch: pytest.MonkeyPatch, exc, expected) -> None:
monkeypatch.setattr(AudioService, "transcript_asr", lambda **_kwargs: (_ for _ in ()).throw(exc))
api = AudioApi()
handler = _unwrap(api.post)
@@ -227,7 +228,7 @@ class TestAudioApi:
with pytest.raises(expected):
handler(api, app_model=app_model, end_user=end_user)
- def test_unhandled_error(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_unhandled_error(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
AudioService, "transcript_asr", lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("boom"))
)
@@ -242,7 +243,7 @@ class TestAudioApi:
class TestTextApi:
- def test_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AudioService, "transcript_tts", lambda **_kwargs: {"audio": "ok"})
api = TextApi()
@@ -259,7 +260,7 @@ class TestTextApi:
assert response == {"audio": "ok"}
- def test_error_mapping(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_error_mapping(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
AudioService, "transcript_tts", lambda **_kwargs: (_ for _ in ()).throw(QuotaExceededError())
)
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_completion.py b/api/tests/unit_tests/controllers/service_api/app/test_completion.py
index 3364c07e62..259741937f 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_completion.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_completion.py
@@ -16,6 +16,7 @@ from types import SimpleNamespace
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from pydantic import ValidationError
from werkzeug.exceptions import BadRequest, NotFound
@@ -295,7 +296,7 @@ class TestCompletionControllerLogic:
@patch("controllers.service_api.app.completion.service_api_ns")
@patch("controllers.service_api.app.completion.AppGenerateService")
- def test_completion_api_post_success(self, mock_generate_service, mock_service_api_ns, app):
+ def test_completion_api_post_success(self, mock_generate_service, mock_service_api_ns, app: Flask):
"""Test CompletionApi.post success path."""
from controllers.service_api.app.completion import CompletionApi
@@ -320,7 +321,7 @@ class TestCompletionControllerLogic:
mock_generate_service.generate.assert_called_once()
@patch("controllers.service_api.app.completion.service_api_ns")
- def test_completion_api_post_wrong_app_mode(self, mock_service_api_ns, app):
+ def test_completion_api_post_wrong_app_mode(self, mock_service_api_ns, app: Flask):
"""Test CompletionApi.post with wrong app mode."""
from controllers.service_api.app.completion import CompletionApi
@@ -334,7 +335,7 @@ class TestCompletionControllerLogic:
@patch("controllers.service_api.app.completion.service_api_ns")
@patch("controllers.service_api.app.completion.AppGenerateService")
- def test_chat_api_post_success(self, mock_generate_service, mock_service_api_ns, app):
+ def test_chat_api_post_success(self, mock_generate_service, mock_service_api_ns, app: Flask):
"""Test ChatApi.post success path."""
from controllers.service_api.app.completion import ChatApi
@@ -355,7 +356,7 @@ class TestCompletionControllerLogic:
assert response == {"text": "compacted"}
@patch("controllers.service_api.app.completion.service_api_ns")
- def test_chat_api_post_wrong_app_mode(self, mock_service_api_ns, app):
+ def test_chat_api_post_wrong_app_mode(self, mock_service_api_ns, app: Flask):
"""Test ChatApi.post with wrong app mode."""
from controllers.service_api.app.completion import ChatApi
@@ -368,7 +369,7 @@ class TestCompletionControllerLogic:
ChatApi().post.__wrapped__(ChatApi(), mock_app_model, mock_end_user)
@patch("controllers.service_api.app.completion.AppTaskService")
- def test_completion_stop_api_success(self, mock_task_service, app):
+ def test_completion_stop_api_success(self, mock_task_service, app: Flask):
"""Test CompletionStopApi.post success."""
from controllers.service_api.app.completion import CompletionStopApi
@@ -385,7 +386,7 @@ class TestCompletionControllerLogic:
mock_task_service.stop_task.assert_called_once()
@patch("controllers.service_api.app.completion.AppTaskService")
- def test_chat_stop_api_success(self, mock_task_service, app):
+ def test_chat_stop_api_success(self, mock_task_service, app: Flask):
"""Test ChatStopApi.post success."""
from controllers.service_api.app.completion import ChatStopApi
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py
index 4fb8ecf784..6dc8f54d42 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py
@@ -20,6 +20,7 @@ from types import SimpleNamespace
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import BadRequest, NotFound
import services
@@ -504,7 +505,7 @@ class TestConversationApiController:
with pytest.raises(NotChatAppError):
handler(api, app_model=app_model, end_user=end_user)
- def test_list_last_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_list_last_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
class _BeginStub:
def __enter__(self):
return SimpleNamespace()
@@ -552,7 +553,7 @@ class TestConversationDetailApiController:
with pytest.raises(NotChatAppError):
handler(api, app_model=app_model, end_user=end_user, c_id="00000000-0000-0000-0000-000000000001")
- def test_delete_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_delete_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
ConversationService,
"delete",
@@ -570,7 +571,7 @@ class TestConversationDetailApiController:
class TestConversationRenameApiController:
- def test_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
ConversationService,
"rename",
@@ -602,7 +603,7 @@ class TestConversationVariablesApiController:
with pytest.raises(NotChatAppError):
handler(api, app_model=app_model, end_user=end_user, c_id="00000000-0000-0000-0000-000000000001")
- def test_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
ConversationService,
"get_conversational_variable",
@@ -621,7 +622,7 @@ class TestConversationVariablesApiController:
with pytest.raises(NotFound):
handler(api, app_model=app_model, end_user=end_user, c_id="00000000-0000-0000-0000-000000000001")
- def test_success_serializes_response(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_success_serializes_response(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
created_at = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC)
monkeypatch.setattr(
ConversationService,
@@ -661,7 +662,7 @@ class TestConversationVariablesApiController:
class TestConversationVariableDetailApiController:
- def test_update_type_mismatch(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_update_type_mismatch(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
ConversationService,
"update_conversation_variable",
@@ -687,7 +688,7 @@ class TestConversationVariableDetailApiController:
variable_id="00000000-0000-0000-0000-000000000002",
)
- def test_update_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_update_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
ConversationService,
"update_conversation_variable",
@@ -713,7 +714,7 @@ class TestConversationVariableDetailApiController:
variable_id="00000000-0000-0000-0000-000000000002",
)
- def test_update_success_serializes_response(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_update_success_serializes_response(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
created_at = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC)
monkeypatch.setattr(
ConversationService,
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_file.py b/api/tests/unit_tests/controllers/service_api/app/test_file.py
index 7060bd79df..2615c3edac 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_file.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_file.py
@@ -16,6 +16,7 @@ import uuid
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from controllers.common.errors import (
FilenameNotExistsError,
@@ -282,7 +283,7 @@ class TestFileApiPost:
assert status == 201
mock_file_svc_cls.return_value.upload_file.assert_called_once()
- def test_upload_no_file(self, app, mock_app_model, mock_end_user):
+ def test_upload_no_file(self, app: Flask, mock_app_model, mock_end_user):
"""Test NoFileUploadedError when no file in request."""
from controllers.service_api.app.file import FileApi
@@ -296,7 +297,7 @@ class TestFileApiPost:
with pytest.raises(NoFileUploadedError):
_unwrap(api.post)(api, app_model=mock_app_model, end_user=mock_end_user)
- def test_upload_too_many_files(self, app, mock_app_model, mock_end_user):
+ def test_upload_too_many_files(self, app: Flask, mock_app_model, mock_end_user):
"""Test TooManyFilesError when multiple files uploaded."""
from io import BytesIO
@@ -317,7 +318,7 @@ class TestFileApiPost:
with pytest.raises(TooManyFilesError):
_unwrap(api.post)(api, app_model=mock_app_model, end_user=mock_end_user)
- def test_upload_no_mimetype(self, app, mock_app_model, mock_end_user):
+ def test_upload_no_mimetype(self, app: Flask, mock_app_model, mock_end_user):
"""Test UnsupportedFileTypeError when file has no mimetype."""
from io import BytesIO
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py b/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py
index 846d5368f3..510d4a9470 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py
@@ -11,6 +11,7 @@ from types import SimpleNamespace
from unittest.mock import ANY, MagicMock, Mock
import pytest
+from flask import Flask
import services.app_generate_service as ags_module
from controllers.service_api.app.workflow_events import WorkflowEventsApi
@@ -281,7 +282,7 @@ class TestHitlServiceApi:
workflow_generator.convert_to_event_stream.assert_called_once_with(["raw-event"])
def test_workflow_events_snapshot_continue_on_pause_keeps_pause_open(
- self, app, monkeypatch: pytest.MonkeyPatch
+ self, app: Flask, monkeypatch: pytest.MonkeyPatch
) -> None:
workflow_run = SimpleNamespace(
id="run-1",
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_message.py b/api/tests/unit_tests/controllers/service_api/app/test_message.py
index c2b8aed1ae..2bc9771862 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_message.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_message.py
@@ -19,6 +19,7 @@ from types import SimpleNamespace
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
from controllers.service_api.app.error import NotChatAppError
@@ -390,7 +391,7 @@ class TestMessageListApi:
with pytest.raises(NotChatAppError):
handler(api, app_model=app_model, end_user=end_user)
- def test_conversation_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_conversation_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
MessageService,
"pagination_by_first_id",
@@ -409,7 +410,7 @@ class TestMessageListApi:
with pytest.raises(NotFound):
handler(api, app_model=app_model, end_user=end_user)
- def test_first_message_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_first_message_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
MessageService,
"pagination_by_first_id",
@@ -430,7 +431,7 @@ class TestMessageListApi:
class TestMessageFeedbackApi:
- def test_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
MessageService,
"create_feedback",
@@ -452,7 +453,7 @@ class TestMessageFeedbackApi:
class TestAppGetFeedbacksApi:
- def test_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(MessageService, "get_all_messages_feedbacks", lambda *_args, **_kwargs: ["f1"])
api = AppGetFeedbacksApi()
@@ -476,7 +477,7 @@ class TestMessageSuggestedApi:
with pytest.raises(NotChatAppError):
handler(api, app_model=app_model, end_user=end_user, message_id="m1")
- def test_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
MessageService,
"get_suggested_questions_after_answer",
@@ -492,7 +493,7 @@ class TestMessageSuggestedApi:
with pytest.raises(NotFound):
handler(api, app_model=app_model, end_user=end_user, message_id="m1")
- def test_disabled(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_disabled(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
MessageService,
"get_suggested_questions_after_answer",
@@ -508,7 +509,7 @@ class TestMessageSuggestedApi:
with pytest.raises(BadRequest):
handler(api, app_model=app_model, end_user=end_user, message_id="m1")
- def test_internal_error(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_internal_error(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
MessageService,
"get_suggested_questions_after_answer",
@@ -524,7 +525,7 @@ class TestMessageSuggestedApi:
with pytest.raises(InternalServerError):
handler(api, app_model=app_model, end_user=end_user, message_id="m1")
- def test_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
MessageService,
"get_suggested_questions_after_answer",
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py
index da09ec13ce..7115ea1e12 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py
@@ -20,6 +20,7 @@ from types import SimpleNamespace
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import BadRequest, NotFound
from controllers.service_api.app.error import NotWorkflowAppError
@@ -366,7 +367,7 @@ class TestWorkflowRunRepository:
class TestWorkflowRunDetailApi:
- def test_not_workflow_app(self, app) -> None:
+ def test_not_workflow_app(self, app: Flask) -> None:
api = WorkflowRunDetailApi()
handler = _unwrap(api.get)
app_model = SimpleNamespace(mode=AppMode.CHAT.value)
@@ -397,7 +398,7 @@ class TestWorkflowRunDetailApi:
class TestWorkflowRunApi:
- def test_not_workflow_app(self, app) -> None:
+ def test_not_workflow_app(self, app: Flask) -> None:
api = WorkflowRunApi()
handler = _unwrap(api.post)
app_model = SimpleNamespace(mode=AppMode.CHAT.value)
@@ -407,7 +408,7 @@ class TestWorkflowRunApi:
with pytest.raises(NotWorkflowAppError):
handler(api, app_model=app_model, end_user=end_user)
- def test_rate_limit(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_rate_limit(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
AppGenerateService,
"generate",
@@ -425,7 +426,7 @@ class TestWorkflowRunApi:
class TestWorkflowRunByIdApi:
- def test_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
AppGenerateService,
"generate",
@@ -441,7 +442,7 @@ class TestWorkflowRunByIdApi:
with pytest.raises(NotFound):
handler(api, app_model=app_model, end_user=end_user, workflow_id="w1")
- def test_draft_workflow(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_draft_workflow(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
AppGenerateService,
"generate",
@@ -459,7 +460,7 @@ class TestWorkflowRunByIdApi:
class TestWorkflowTaskStopApi:
- def test_wrong_mode(self, app) -> None:
+ def test_wrong_mode(self, app: Flask) -> None:
api = WorkflowTaskStopApi()
handler = _unwrap(api.post)
app_model = SimpleNamespace(mode=AppMode.CHAT.value)
@@ -469,7 +470,7 @@ class TestWorkflowTaskStopApi:
with pytest.raises(NotWorkflowAppError):
handler(api, app_model=app_model, end_user=end_user, task_id="t1")
- def test_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
stop_mock = Mock()
send_mock = Mock()
monkeypatch.setattr(AppQueueManager, "set_stop_flag_no_user_check", stop_mock)
@@ -489,7 +490,7 @@ class TestWorkflowTaskStopApi:
class TestWorkflowAppLogApi:
- def test_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
class _BeginStub:
def __enter__(self):
return SimpleNamespace()
@@ -557,7 +558,7 @@ class TestWorkflowRunDetailApiGet:
self,
mock_db,
mock_repo_factory,
- app,
+ app: Flask,
mock_workflow_app,
):
"""Test successful workflow run detail retrieval."""
@@ -579,7 +580,7 @@ class TestWorkflowRunDetailApiGet:
assert result["status"] == "succeeded"
@patch("controllers.service_api.app.workflow.db")
- def test_get_workflow_run_wrong_app_mode(self, mock_db, app):
+ def test_get_workflow_run_wrong_app_mode(self, mock_db, app: Flask):
"""Test NotWorkflowAppError when app mode is not workflow or advanced_chat."""
from controllers.service_api.app.workflow import WorkflowRunDetailApi
@@ -604,7 +605,7 @@ class TestWorkflowTaskStopApiPost:
self,
mock_queue_mgr,
mock_graph_mgr,
- app,
+ app: Flask,
mock_workflow_app,
):
"""Test successful workflow task stop."""
@@ -624,7 +625,7 @@ class TestWorkflowTaskStopApiPost:
mock_graph_mgr.assert_called_once()
mock_graph_mgr.return_value.send_stop_command.assert_called_once_with("task-1")
- def test_stop_workflow_task_wrong_app_mode(self, app):
+ def test_stop_workflow_task_wrong_app_mode(self, app: Flask):
"""Test NotWorkflowAppError when app mode is not workflow."""
from controllers.service_api.app.workflow import WorkflowTaskStopApi
@@ -649,7 +650,7 @@ class TestWorkflowAppLogApiGet:
self,
mock_db,
mock_wf_svc_cls,
- app,
+ app: Flask,
mock_workflow_app,
):
"""Test successful workflow log retrieval."""
diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow_events.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow_events.py
index f45a7f9632..b3edc2ecd8 100644
--- a/api/tests/unit_tests/controllers/service_api/app/test_workflow_events.py
+++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow_events.py
@@ -9,6 +9,7 @@ from types import SimpleNamespace
from unittest.mock import Mock
import pytest
+from flask import Flask
from werkzeug.exceptions import NotFound
from controllers.service_api.app.error import NotWorkflowAppError
@@ -41,7 +42,7 @@ class TestWorkflowEventsApi:
with pytest.raises(NotWorkflowAppError):
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
- def test_workflow_run_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_workflow_run_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
_mock_repo_for_run(monkeypatch, workflow_run=None)
api = WorkflowEventsApi()
handler = _unwrap(api.get)
@@ -52,7 +53,7 @@ class TestWorkflowEventsApi:
with pytest.raises(NotFound):
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
- def test_workflow_run_permission_denied(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_workflow_run_permission_denied(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
workflow_run = SimpleNamespace(
id="run-1",
app_id="app-1",
@@ -70,7 +71,7 @@ class TestWorkflowEventsApi:
with pytest.raises(NotFound):
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
- def test_finished_run_returns_sse(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_finished_run_returns_sse(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
workflow_run = SimpleNamespace(
id="run-1",
app_id="app-1",
@@ -103,7 +104,7 @@ class TestWorkflowEventsApi:
assert payload["task_id"] == "run-1"
assert payload["event"] == "workflow_finished"
- def test_running_run_streams_events(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_running_run_streams_events(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
workflow_run = SimpleNamespace(
id="run-1",
app_id="app-1",
@@ -135,7 +136,7 @@ class TestWorkflowEventsApi:
)
workflow_generator.convert_to_event_stream.assert_called_once_with(["raw-event"])
- def test_running_run_with_snapshot(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_running_run_with_snapshot(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
workflow_run = SimpleNamespace(
id="run-1",
app_id="app-1",
diff --git a/api/tests/unit_tests/controllers/service_api/dataset/rag_pipeline/test_rag_pipeline_workflow.py b/api/tests/unit_tests/controllers/service_api/dataset/rag_pipeline/test_rag_pipeline_workflow.py
index f33c482d04..362af883ed 100644
--- a/api/tests/unit_tests/controllers/service_api/dataset/rag_pipeline/test_rag_pipeline_workflow.py
+++ b/api/tests/unit_tests/controllers/service_api/dataset/rag_pipeline/test_rag_pipeline_workflow.py
@@ -23,6 +23,7 @@ from datetime import UTC, datetime
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Forbidden, NotFound
@@ -373,7 +374,7 @@ class TestDatasourcePluginsApiGet:
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.RagPipelineService")
- def test_get_plugins_success(self, mock_svc_cls, mock_db, app):
+ def test_get_plugins_success(self, mock_svc_cls, mock_db, app: Flask):
"""Test successful retrieval of datasource plugins."""
tenant_id = str(uuid.uuid4())
dataset_id = str(uuid.uuid4())
@@ -396,7 +397,7 @@ class TestDatasourcePluginsApiGet:
)
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
- def test_get_plugins_not_found(self, mock_db, app):
+ def test_get_plugins_not_found(self, mock_db, app: Flask):
"""Test NotFound when dataset check fails."""
mock_db.session.scalar.return_value = None
@@ -407,7 +408,7 @@ class TestDatasourcePluginsApiGet:
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.RagPipelineService")
- def test_get_plugins_empty_list(self, mock_svc_cls, mock_db, app):
+ def test_get_plugins_empty_list(self, mock_svc_cls, mock_db, app: Flask):
"""Test empty plugin list."""
mock_db.session.scalar.return_value = Mock()
mock_svc_instance = Mock()
@@ -439,7 +440,7 @@ class TestDatasourceNodeRunApiPost:
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.RagPipelineService")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.service_api_ns")
- def test_post_success(self, mock_ns, mock_db, mock_svc_cls, mock_current_user, mock_gen, mock_helper, app):
+ def test_post_success(self, mock_ns, mock_db, mock_svc_cls, mock_current_user, mock_gen, mock_helper, app: Flask):
"""Test successful datasource node run."""
tenant_id = str(uuid.uuid4())
dataset_id = str(uuid.uuid4())
@@ -473,7 +474,7 @@ class TestDatasourceNodeRunApiPost:
mock_svc_instance.run_datasource_workflow_node.assert_called_once()
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
- def test_post_not_found(self, mock_db, app):
+ def test_post_not_found(self, mock_db, app: Flask):
"""Test NotFound when dataset check fails."""
mock_db.session.scalar.return_value = None
@@ -488,7 +489,7 @@ class TestDatasourceNodeRunApiPost:
)
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.service_api_ns")
- def test_post_fails_when_current_user_not_account(self, mock_ns, mock_db, app):
+ def test_post_fails_when_current_user_not_account(self, mock_ns, mock_db, app: Flask):
"""Test AssertionError when current_user is not an Account instance."""
mock_db.session.scalar.return_value = Mock()
mock_ns.payload = {
@@ -549,7 +550,7 @@ class TestPipelineRunApiPost:
mock_gen_svc.generate.assert_called_once()
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
- def test_post_not_found(self, mock_db, app):
+ def test_post_not_found(self, mock_db, app: Flask):
"""Test NotFound when dataset check fails."""
mock_db.session.scalar.return_value = None
@@ -561,7 +562,7 @@ class TestPipelineRunApiPost:
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.current_user", new="not_account")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.service_api_ns")
- def test_post_forbidden_non_account_user(self, mock_ns, mock_db, app):
+ def test_post_forbidden_non_account_user(self, mock_ns, mock_db, app: Flask):
"""Test Forbidden when current_user is not an Account."""
mock_db.session.scalar.return_value = Mock()
mock_ns.payload = {
@@ -585,7 +586,7 @@ class TestFileUploadApiPost:
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.FileService")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.current_user")
@patch("controllers.service_api.dataset.rag_pipeline.rag_pipeline_workflow.db")
- def test_upload_success(self, mock_db, mock_current_user, mock_file_svc_cls, app):
+ def test_upload_success(self, mock_db, mock_current_user, mock_file_svc_cls, app: Flask):
"""Test successful file upload."""
mock_current_user.__bool__ = Mock(return_value=True)
@@ -621,7 +622,7 @@ class TestFileUploadApiPost:
assert response["name"] == "doc.pdf"
assert response["extension"] == "pdf"
- def test_upload_no_file(self, app):
+ def test_upload_no_file(self, app: Flask):
"""Test error when no file is uploaded."""
with app.test_request_context(
"/datasets/pipeline/file-upload",
diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py b/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py
index e9c3e6d376..fe8fc02548 100644
--- a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py
+++ b/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py
@@ -18,6 +18,7 @@ import uuid
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import NotFound
from controllers.service_api.dataset.segment import (
@@ -782,7 +783,7 @@ class TestSegmentApiGet:
mock_seg_svc,
mock_marshal,
mock_summary_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
mock_segment,
@@ -893,7 +894,7 @@ class TestSegmentApiPost:
mock_seg_svc,
mock_marshal,
mock_summary_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
mock_segment,
@@ -946,7 +947,7 @@ class TestSegmentApiPost:
mock_db,
mock_account_fn,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -989,7 +990,7 @@ class TestSegmentApiPost:
mock_db,
mock_account_fn,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1041,7 +1042,7 @@ class TestDatasetSegmentApiDelete:
mock_doc_svc,
mock_dataset_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
mock_segment,
@@ -1086,7 +1087,7 @@ class TestDatasetSegmentApiDelete:
mock_account_fn,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1128,7 +1129,7 @@ class TestDatasetSegmentApiDelete:
mock_account_fn,
mock_doc_svc,
mock_dataset_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1162,7 +1163,7 @@ class TestDatasetSegmentApiDelete:
mock_account_fn,
mock_dataset_svc,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1232,7 +1233,7 @@ class TestDatasetSegmentApiUpdate:
mock_seg_svc,
mock_marshal,
mock_summary_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
mock_segment,
@@ -1282,7 +1283,7 @@ class TestDatasetSegmentApiUpdate:
mock_account_fn,
mock_dataset_svc,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1322,7 +1323,7 @@ class TestDatasetSegmentApiUpdate:
mock_dataset_svc,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1374,7 +1375,7 @@ class TestDatasetSegmentApiGetSingle:
mock_seg_svc,
mock_marshal,
mock_summary_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
mock_segment,
@@ -1421,7 +1422,7 @@ class TestDatasetSegmentApiGetSingle:
mock_seg_svc,
mock_marshal,
mock_summary_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
mock_segment,
@@ -1460,7 +1461,7 @@ class TestDatasetSegmentApiGetSingle:
self,
mock_db,
mock_account_fn,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1491,7 +1492,7 @@ class TestDatasetSegmentApiGetSingle:
mock_account_fn,
mock_dataset_svc,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1526,7 +1527,7 @@ class TestDatasetSegmentApiGetSingle:
mock_dataset_svc,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1570,7 +1571,7 @@ class TestChildChunkApiGet:
mock_doc_svc,
mock_seg_svc,
mock_marshal,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1609,7 +1610,7 @@ class TestChildChunkApiGet:
self,
mock_db,
mock_account_fn,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1638,7 +1639,7 @@ class TestChildChunkApiGet:
mock_db,
mock_account_fn,
mock_doc_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1670,7 +1671,7 @@ class TestChildChunkApiGet:
mock_account_fn,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1729,7 +1730,7 @@ class TestChildChunkApiPost:
mock_doc_svc,
mock_seg_svc,
mock_marshal,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1771,7 +1772,7 @@ class TestChildChunkApiPost:
mock_feature_svc,
mock_db,
mock_account_fn,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1809,7 +1810,7 @@ class TestChildChunkApiPost:
mock_account_fn,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1863,7 +1864,7 @@ class TestDatasetChildChunkApiDelete:
mock_account_fn,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1913,7 +1914,7 @@ class TestDatasetChildChunkApiDelete:
mock_account_fn,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1954,7 +1955,7 @@ class TestDatasetChildChunkApiDelete:
mock_account_fn,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -1994,7 +1995,7 @@ class TestDatasetChildChunkApiDelete:
mock_account_fn,
mock_doc_svc,
mock_seg_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_metadata.py b/api/tests/unit_tests/controllers/service_api/dataset/test_metadata.py
index b93a1cf14b..b7e24f9201 100644
--- a/api/tests/unit_tests/controllers/service_api/dataset/test_metadata.py
+++ b/api/tests/unit_tests/controllers/service_api/dataset/test_metadata.py
@@ -19,6 +19,7 @@ import uuid
from unittest.mock import Mock, patch
import pytest
+from flask import Flask
from werkzeug.exceptions import NotFound
from controllers.service_api.dataset.metadata import (
@@ -76,7 +77,7 @@ class TestDatasetMetadataCreatePost:
mock_dataset_svc,
mock_meta_svc,
mock_marshal,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -106,7 +107,7 @@ class TestDatasetMetadataCreatePost:
def test_create_metadata_dataset_not_found(
self,
mock_dataset_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -136,7 +137,7 @@ class TestDatasetMetadataCreateGet:
self,
mock_dataset_svc,
mock_meta_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -160,7 +161,7 @@ class TestDatasetMetadataCreateGet:
def test_get_metadata_dataset_not_found(
self,
mock_dataset_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -201,7 +202,7 @@ class TestDatasetMetadataServiceApiPatch:
mock_dataset_svc,
mock_meta_svc,
mock_marshal,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -232,7 +233,7 @@ class TestDatasetMetadataServiceApiPatch:
def test_update_metadata_dataset_not_found(
self,
mock_dataset_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -273,7 +274,7 @@ class TestDatasetMetadataServiceApiDelete:
mock_current_user,
mock_dataset_svc,
mock_meta_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -302,7 +303,7 @@ class TestDatasetMetadataServiceApiDelete:
def test_delete_metadata_dataset_not_found(
self,
mock_dataset_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -336,7 +337,7 @@ class TestDatasetMetadataBuiltInFieldGet:
def test_get_built_in_fields_success(
self,
mock_meta_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -382,7 +383,7 @@ class TestDatasetMetadataBuiltInFieldAction:
mock_current_user,
mock_dataset_svc,
mock_meta_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -414,7 +415,7 @@ class TestDatasetMetadataBuiltInFieldAction:
mock_current_user,
mock_dataset_svc,
mock_meta_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -441,7 +442,7 @@ class TestDatasetMetadataBuiltInFieldAction:
def test_action_dataset_not_found(
self,
mock_dataset_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -485,7 +486,7 @@ class TestDocumentMetadataEditPost:
mock_current_user,
mock_dataset_svc,
mock_meta_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
@@ -513,7 +514,7 @@ class TestDocumentMetadataEditPost:
def test_update_documents_metadata_dataset_not_found(
self,
mock_dataset_svc,
- app,
+ app: Flask,
mock_tenant,
mock_dataset,
):
diff --git a/api/tests/unit_tests/controllers/service_api/test_index.py b/api/tests/unit_tests/controllers/service_api/test_index.py
index c560a3c698..8441118181 100644
--- a/api/tests/unit_tests/controllers/service_api/test_index.py
+++ b/api/tests/unit_tests/controllers/service_api/test_index.py
@@ -5,6 +5,7 @@ Unit tests for Service API Index endpoint
from unittest.mock import MagicMock, patch
import pytest
+from flask import Flask
from controllers.service_api.index import IndexApi
@@ -13,7 +14,7 @@ class TestIndexApi:
"""Test suite for IndexApi resource."""
@patch("controllers.service_api.index.dify_config", autospec=True)
- def test_get_returns_api_info(self, mock_config, app):
+ def test_get_returns_api_info(self, mock_config, app: Flask):
"""Test that GET returns API metadata with correct structure."""
# Arrange
mock_config.project.version = "1.0.0-test"
@@ -32,7 +33,7 @@ class TestIndexApi:
assert response["api_version"] == "v1"
assert response["server_version"] == "1.0.0-test"
- def test_get_response_has_required_fields(self, app):
+ def test_get_response_has_required_fields(self, app: Flask):
"""Test that response contains all required fields."""
# Arrange
mock_config = MagicMock()
diff --git a/api/tests/unit_tests/controllers/service_api/test_wraps.py b/api/tests/unit_tests/controllers/service_api/test_wraps.py
index 6dfbdcf98e..30d7b92913 100644
--- a/api/tests/unit_tests/controllers/service_api/test_wraps.py
+++ b/api/tests/unit_tests/controllers/service_api/test_wraps.py
@@ -39,7 +39,7 @@ class TestValidateAndGetApiToken:
app.config["TESTING"] = True
return app
- def test_missing_authorization_header(self, app):
+ def test_missing_authorization_header(self, app: Flask):
"""Test that Unauthorized is raised when Authorization header is missing."""
# Arrange
with app.test_request_context("/", method="GET"):
@@ -50,7 +50,7 @@ class TestValidateAndGetApiToken:
validate_and_get_api_token("app")
assert "Authorization header must be provided" in str(exc_info.value)
- def test_invalid_auth_scheme(self, app):
+ def test_invalid_auth_scheme(self, app: Flask):
"""Test that Unauthorized is raised when auth scheme is not Bearer."""
# Arrange
with app.test_request_context("/", method="GET", headers={"Authorization": "Basic token123"}):
@@ -62,7 +62,7 @@ class TestValidateAndGetApiToken:
@patch("controllers.service_api.wraps.record_token_usage")
@patch("controllers.service_api.wraps.ApiTokenCache")
@patch("controllers.service_api.wraps.fetch_token_with_single_flight")
- def test_valid_token_returns_api_token(self, mock_fetch_token, mock_cache_cls, mock_record_usage, app):
+ def test_valid_token_returns_api_token(self, mock_fetch_token, mock_cache_cls, mock_record_usage, app: Flask):
"""Test that valid token returns the ApiToken object."""
# Arrange
mock_api_token = Mock(spec=ApiToken)
@@ -84,7 +84,7 @@ class TestValidateAndGetApiToken:
@patch("controllers.service_api.wraps.record_token_usage")
@patch("controllers.service_api.wraps.ApiTokenCache")
@patch("controllers.service_api.wraps.fetch_token_with_single_flight")
- def test_invalid_token_raises_unauthorized(self, mock_fetch_token, mock_cache_cls, mock_record_usage, app):
+ def test_invalid_token_raises_unauthorized(self, mock_fetch_token, mock_cache_cls, mock_record_usage, app: Flask):
"""Test that invalid token raises Unauthorized."""
# Arrange
from werkzeug.exceptions import Unauthorized
@@ -161,7 +161,7 @@ class TestValidateAppToken:
@patch("controllers.service_api.wraps.db")
@patch("controllers.service_api.wraps.validate_and_get_api_token")
- def test_app_not_found_raises_forbidden(self, mock_validate_token, mock_db, app):
+ def test_app_not_found_raises_forbidden(self, mock_validate_token, mock_db, app: Flask):
"""Test that Forbidden is raised when app no longer exists."""
# Arrange
mock_api_token = Mock()
@@ -182,7 +182,7 @@ class TestValidateAppToken:
@patch("controllers.service_api.wraps.db")
@patch("controllers.service_api.wraps.validate_and_get_api_token")
- def test_app_status_abnormal_raises_forbidden(self, mock_validate_token, mock_db, app):
+ def test_app_status_abnormal_raises_forbidden(self, mock_validate_token, mock_db, app: Flask):
"""Test that Forbidden is raised when app status is abnormal."""
# Arrange
mock_api_token = Mock()
@@ -205,7 +205,7 @@ class TestValidateAppToken:
@patch("controllers.service_api.wraps.db")
@patch("controllers.service_api.wraps.validate_and_get_api_token")
- def test_app_api_disabled_raises_forbidden(self, mock_validate_token, mock_db, app):
+ def test_app_api_disabled_raises_forbidden(self, mock_validate_token, mock_db, app: Flask):
"""Test that Forbidden is raised when app API is disabled."""
# Arrange
mock_api_token = Mock()
@@ -240,7 +240,7 @@ class TestCloudEditionBillingResourceCheck:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.FeatureService.get_features")
- def test_allows_when_under_limit(self, mock_get_features, mock_validate_token, app):
+ def test_allows_when_under_limit(self, mock_get_features, mock_validate_token, app: Flask):
"""Test that request is allowed when under resource limit."""
# Arrange
mock_validate_token.return_value = Mock(tenant_id="tenant123")
@@ -264,7 +264,7 @@ class TestCloudEditionBillingResourceCheck:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.FeatureService.get_features")
- def test_rejects_when_at_limit(self, mock_get_features, mock_validate_token, app):
+ def test_rejects_when_at_limit(self, mock_get_features, mock_validate_token, app: Flask):
"""Test that Forbidden is raised when at resource limit."""
# Arrange
mock_validate_token.return_value = Mock(tenant_id="tenant123")
@@ -287,7 +287,7 @@ class TestCloudEditionBillingResourceCheck:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.FeatureService.get_features")
- def test_allows_when_billing_disabled(self, mock_get_features, mock_validate_token, app):
+ def test_allows_when_billing_disabled(self, mock_get_features, mock_validate_token, app: Flask):
"""Test that request is allowed when billing is disabled."""
# Arrange
mock_validate_token.return_value = Mock(tenant_id="tenant123")
@@ -320,7 +320,7 @@ class TestCloudEditionBillingKnowledgeLimitCheck:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.FeatureService.get_features")
- def test_rejects_add_segment_in_sandbox(self, mock_get_features, mock_validate_token, app):
+ def test_rejects_add_segment_in_sandbox(self, mock_get_features, mock_validate_token, app: Flask):
"""Test that add_segment is rejected in SANDBOX plan."""
# Arrange
mock_validate_token.return_value = Mock(tenant_id="tenant123")
@@ -342,7 +342,7 @@ class TestCloudEditionBillingKnowledgeLimitCheck:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.FeatureService.get_features")
- def test_allows_other_operations_in_sandbox(self, mock_get_features, mock_validate_token, app):
+ def test_allows_other_operations_in_sandbox(self, mock_get_features, mock_validate_token, app: Flask):
"""Test that non-add_segment operations are allowed in SANDBOX."""
# Arrange
mock_validate_token.return_value = Mock(tenant_id="tenant123")
@@ -376,7 +376,7 @@ class TestCloudEditionBillingRateLimitCheck:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.FeatureService.get_knowledge_rate_limit")
- def test_allows_within_rate_limit(self, mock_get_rate_limit, mock_validate_token, app):
+ def test_allows_within_rate_limit(self, mock_get_rate_limit, mock_validate_token, app: Flask):
"""Test that request is allowed when within rate limit."""
# Arrange
mock_validate_token.return_value = Mock(tenant_id="tenant123")
@@ -406,7 +406,7 @@ class TestCloudEditionBillingRateLimitCheck:
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.FeatureService.get_knowledge_rate_limit")
@patch("controllers.service_api.wraps.db")
- def test_rejects_over_rate_limit(self, mock_db, mock_get_rate_limit, mock_validate_token, app):
+ def test_rejects_over_rate_limit(self, mock_db, mock_get_rate_limit, mock_validate_token, app: Flask):
"""Test that Forbidden is raised when over rate limit."""
# Arrange
mock_validate_token.return_value = Mock(tenant_id="tenant123")
@@ -445,7 +445,7 @@ class TestValidateDatasetToken:
@patch("controllers.service_api.wraps.db")
@patch("controllers.service_api.wraps.validate_and_get_api_token")
@patch("controllers.service_api.wraps.current_app")
- def test_valid_dataset_token(self, mock_current_app, mock_validate_token, mock_db, mock_user_logged_in, app):
+ def test_valid_dataset_token(self, mock_current_app, mock_validate_token, mock_db, mock_user_logged_in, app: Flask):
"""Test that valid dataset token allows access."""
# Arrange
# Use standard Mock for login_manager
@@ -487,7 +487,7 @@ class TestValidateDatasetToken:
@patch("controllers.service_api.wraps.db")
@patch("controllers.service_api.wraps.validate_and_get_api_token")
- def test_dataset_not_found_raises_not_found(self, mock_validate_token, mock_db, app):
+ def test_dataset_not_found_raises_not_found(self, mock_validate_token, mock_db, app: Flask):
"""Test that NotFound is raised when dataset doesn't exist."""
# Arrange
mock_api_token = Mock()
diff --git a/api/tests/unit_tests/core/external_data_tool/test_external_data_fetch.py b/api/tests/unit_tests/core/external_data_tool/test_external_data_fetch.py
index 86b461cf04..c1c1291281 100644
--- a/api/tests/unit_tests/core/external_data_tool/test_external_data_fetch.py
+++ b/api/tests/unit_tests/core/external_data_tool/test_external_data_fetch.py
@@ -13,7 +13,7 @@ class TestExternalDataFetch:
app = Flask(__name__)
return app
- def test_fetch_success(self, app):
+ def test_fetch_success(self, app: Flask):
with app.app_context():
fetcher = ExternalDataFetch()
@@ -79,7 +79,7 @@ class TestExternalDataFetch:
assert result_inputs == inputs
assert result_inputs is not inputs # Should be a copy
- def test_fetch_with_none_variable(self, app):
+ def test_fetch_with_none_variable(self, app: Flask):
with app.app_context():
fetcher = ExternalDataFetch()
tool = ExternalDataVariableEntity(variable="var1", type="type1", config={})
@@ -95,7 +95,7 @@ class TestExternalDataFetch:
assert "var1" not in result_inputs
assert result_inputs == {"in": "val"}
- def test_query_external_data_tool(self, app):
+ def test_query_external_data_tool(self, app: Flask):
fetcher = ExternalDataFetch()
tool = ExternalDataVariableEntity(variable="var1", type="type1", config={"k": "v"})
diff --git a/api/tests/unit_tests/services/plugin/test_plugin_permission_service.py b/api/tests/unit_tests/services/plugin/test_plugin_permission_service.py
deleted file mode 100644
index 53a9e6210c..0000000000
--- a/api/tests/unit_tests/services/plugin/test_plugin_permission_service.py
+++ /dev/null
@@ -1,79 +0,0 @@
-from unittest.mock import MagicMock, patch
-
-from models.account import TenantPluginPermission
-
-MODULE = "services.plugin.plugin_permission_service"
-
-
-def _patched_session():
- """Patch session_factory.create_session() to return a mock session as context manager."""
- session = MagicMock()
- session.__enter__ = MagicMock(return_value=session)
- session.__exit__ = MagicMock(return_value=False)
- session.begin.return_value.__enter__ = MagicMock(return_value=session)
- session.begin.return_value.__exit__ = MagicMock(return_value=False)
- mock_factory = MagicMock()
- mock_factory.create_session.return_value = session
- patcher = patch(f"{MODULE}.session_factory", mock_factory)
- return patcher, session
-
-
-class TestGetPermission:
- def test_returns_permission_when_found(self):
- p1, session = _patched_session()
- permission = MagicMock()
- session.scalar.return_value = permission
-
- with p1:
- from services.plugin.plugin_permission_service import PluginPermissionService
-
- result = PluginPermissionService.get_permission("t1")
-
- assert result is permission
-
- def test_returns_none_when_not_found(self):
- p1, session = _patched_session()
- session.scalar.return_value = None
-
- with p1:
- from services.plugin.plugin_permission_service import PluginPermissionService
-
- result = PluginPermissionService.get_permission("t1")
-
- assert result is None
-
-
-class TestChangePermission:
- def test_creates_new_permission_when_not_exists(self):
- p1, session = _patched_session()
- session.scalar.return_value = None
-
- with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginPermission") as perm_cls:
- perm_cls.return_value = MagicMock()
- from services.plugin.plugin_permission_service import PluginPermissionService
-
- result = PluginPermissionService.change_permission(
- "t1", TenantPluginPermission.InstallPermission.EVERYONE, TenantPluginPermission.DebugPermission.EVERYONE
- )
-
- assert result is True
- session.begin.assert_called_once()
- session.add.assert_called_once()
-
- def test_updates_existing_permission(self):
- p1, session = _patched_session()
- existing = MagicMock()
- session.scalar.return_value = existing
-
- with p1:
- from services.plugin.plugin_permission_service import PluginPermissionService
-
- result = PluginPermissionService.change_permission(
- "t1", TenantPluginPermission.InstallPermission.ADMINS, TenantPluginPermission.DebugPermission.ADMINS
- )
-
- assert result is True
- session.begin.assert_called_once()
- assert existing.install_permission == TenantPluginPermission.InstallPermission.ADMINS
- assert existing.debug_permission == TenantPluginPermission.DebugPermission.ADMINS
- session.add.assert_not_called()
diff --git a/docker/.env.default b/docker/.env.default
new file mode 100644
index 0000000000..6f6683b9f5
--- /dev/null
+++ b/docker/.env.default
@@ -0,0 +1,51 @@
+# ------------------------------------------------------------------
+# Minimal defaults for Docker Compose deployments.
+#
+# Keep local changes in .env. Use .env.example as the full reference
+# for advanced and service-specific settings.
+# ------------------------------------------------------------------
+
+# Public URLs used when Dify generates links. Change these together when
+# exposing Dify under another hostname, IP address, or port.
+CONSOLE_WEB_URL=http://localhost
+SERVICE_API_URL=http://localhost
+APP_WEB_URL=http://localhost
+FILES_URL=http://localhost
+INTERNAL_FILES_URL=http://api:5001
+TRIGGER_URL=http://localhost
+ENDPOINT_URL_TEMPLATE=http://localhost/e/{hook_id}
+NEXT_PUBLIC_SOCKET_URL=ws://localhost
+EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
+EXPOSE_PLUGIN_DEBUGGING_PORT=5003
+
+# Built-in metadata database defaults.
+DB_TYPE=postgresql
+DB_USERNAME=postgres
+DB_PASSWORD=difyai123456
+DB_HOST=db_postgres
+DB_PORT=5432
+DB_DATABASE=dify
+
+# Built-in Redis defaults.
+REDIS_HOST=redis
+REDIS_PORT=6379
+REDIS_PASSWORD=difyai123456
+
+# Default file storage.
+STORAGE_TYPE=opendal
+OPENDAL_SCHEME=fs
+OPENDAL_FS_ROOT=storage
+
+# Default vector database.
+VECTOR_STORE=weaviate
+
+# Internal service authentication. Paired values must match.
+PLUGIN_DAEMON_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi
+PLUGIN_DIFY_INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
+
+# Host ports.
+EXPOSE_NGINX_PORT=80
+EXPOSE_NGINX_SSL_PORT=443
+
+# Docker Compose profiles for bundled services.
+COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql}
diff --git a/docker/.env.example b/docker/.env.example
index 29741474fa..122228cdd1 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -1003,7 +1003,7 @@ NOTION_INTERNAL_SECRET=
# ------------------------------
# Mail type, support: resend, smtp, sendgrid
-MAIL_TYPE=resend
+MAIL_TYPE=
# Default send from email address, if not specified
# If using SendGrid, use the 'from' field for authentication if necessary.
@@ -1011,7 +1011,7 @@ MAIL_DEFAULT_SEND_FROM=
# API-Key for the Resend email provider, used when MAIL_TYPE is `resend`.
RESEND_API_URL=https://api.resend.com
-RESEND_API_KEY=your-resend-api-key
+RESEND_API_KEY=
# SMTP server configuration, used when MAIL_TYPE is `smtp`
@@ -1359,10 +1359,10 @@ NGINX_ENABLE_CERTBOT_CHALLENGE=false
# ------------------------------
# Email address (required to get certificates from Let's Encrypt)
-CERTBOT_EMAIL=your_email@example.com
+CERTBOT_EMAIL=
# Domain name
-CERTBOT_DOMAIN=your_domain.com
+CERTBOT_DOMAIN=
# certbot command options
# i.e: --force-renewal --dry-run --test-cert --debug
diff --git a/docker/README.md b/docker/README.md
index 3130fa9886..3a7f4c2ad5 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -7,28 +7,28 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
- **Certbot Container**: `docker-compose.yaml` now contains `certbot` for managing SSL certificates. This container automatically renews certificates and ensures secure HTTPS connections.\
For more information, refer `docker/certbot/README.md`.
-- **Persistent Environment Variables**: Environment variables are now managed through a `.env` file, ensuring that your configurations persist across deployments.
+- **Persistent Environment Variables**: Default environment variables are managed through `.env.default`, while local overrides are stored in `.env`, ensuring that your configurations persist across deployments.
> What is `.env`?
- > The `.env` file is a crucial component in Docker and Docker Compose environments, serving as a centralized configuration file where you can define environment variables that are accessible to the containers at runtime. This file simplifies the management of environment settings across different stages of development, testing, and production, providing consistency and ease of configuration to deployments.
+ > The `.env` file is a local override file. Keep it small by adding only the values that differ from `.env.default`. Use `.env.example` as the full reference when you need advanced configuration.
- **Unified Vector Database Services**: All vector database services are now managed from a single Docker Compose file `docker-compose.yaml`. You can switch between different vector databases by setting the `VECTOR_STORE` environment variable in your `.env` file.
-- **Mandatory .env File**: A `.env` file is now required to run `docker compose up`. This file is crucial for configuring your deployment and for any custom settings to persist through upgrades.
+- **Local .env Overrides**: The `dify-compose` and `dify-compose.ps1` wrappers create `.env` if it is missing and generate a persistent `SECRET_KEY` for this deployment.
### How to Deploy Dify with `docker-compose.yaml`
1. **Prerequisites**: Ensure Docker and Docker Compose are installed on your system.
1. **Environment Setup**:
- Navigate to the `docker` directory.
- - Copy the `.env.example` file to a new file named `.env` by running `cp .env.example .env`.
- - Customize the `.env` file as needed. Refer to the `.env.example` file for detailed configuration options.
- - **Optional (Recommended for upgrades)**:
- You may use the environment synchronization tool to help keep your `.env` file aligned with the latest `.env.example` updates, while preserving your custom settings.
- This is especially useful when upgrading Dify or managing a large, customized `.env` file.
+ - No copy step is required. The `dify-compose` wrappers create `.env` if it is missing and write a generated `SECRET_KEY` to it.
+ - When prompted on first run, press Enter to use the default deployment, or answer `y` to stop and edit `.env` first.
+ - Customize `.env` only when you need to override defaults from `.env.default`. Refer to `.env.example` for the full list of available variables.
+ - **Optional (for advanced deployments)**:
+ If you maintain a full `.env` file copied from `.env.example`, you may use the environment synchronization tool to keep it aligned with the latest `.env.example` updates while preserving your custom settings.
See the [Environment Variables Synchronization](#environment-variables-synchronization) section below.
1. **Running the Services**:
- - Execute `docker compose up` from the `docker` directory to start the services.
+ - Execute `./dify-compose up -d` from the `docker` directory to start the services. On Windows PowerShell, run `.\dify-compose.ps1 up -d`.
- To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`.
1. **SSL Certificate Setup**:
- Refer `docker/certbot/README.md` to set up SSL certificates using Certbot.
@@ -58,7 +58,13 @@ For users migrating from the `docker-legacy` setup:
1. **Data Migration**:
- Ensure that data from services like databases and caches is backed up and migrated appropriately to the new structure if necessary.
-### Overview of `.env`
+### Overview of `.env.default`, `.env`, and `.env.example`
+
+- `.env.default` contains the minimal default configuration for Docker Compose deployments.
+- `.env` contains the generated `SECRET_KEY` plus any local overrides.
+- `.env.example` is the full reference for advanced configuration.
+
+The `dify-compose` wrappers merge `.env.default` and `.env` into a temporary environment file, append paired internal service keys when needed, and remove the temporary file after Docker Compose starts.
#### Key Modules and Customization
@@ -118,9 +124,11 @@ The `.env.example` file provided in the Docker setup is extensive and covers a w
### Environment Variables Synchronization
-When upgrading Dify or pulling the latest changes, new environment variables may be introduced in `.env.example`.
+When upgrading Dify or pulling the latest changes, new environment variables may be introduced in `.env.default` or `.env.example`.
-To help keep your existing `.env` file up to date **without losing your custom values**, an optional environment variables synchronization tool is provided.
+If you use the default override-only workflow, review `.env.default` and add only the values you need to override to `.env`.
+
+If you maintain a full `.env` file copied from `.env.example`, an optional environment variables synchronization tool is provided.
> This tool performs a **one-way synchronization** from `.env.example` to `.env`.
> Existing values in `.env` are never overwritten automatically.
@@ -143,9 +151,9 @@ Before synchronization, the current `.env` file is saved to the `env-backup/` di
**When to use**
-- After upgrading Dify to a newer version
+- After upgrading Dify to a newer version with a full `.env` file
- When `.env.example` has been updated with new environment variables
-- When managing a large or heavily customized `.env` file
+- When managing a large or heavily customized `.env` file copied from `.env.example`
**Usage**
diff --git a/docker/dify-compose b/docker/dify-compose
new file mode 100755
index 0000000000..16bbd6b538
--- /dev/null
+++ b/docker/dify-compose
@@ -0,0 +1,334 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+DEFAULT_ENV_FILE=".env.default"
+USER_ENV_FILE=".env"
+
+log() {
+ printf '%s\n' "$*" >&2
+}
+
+die() {
+ printf 'Error: %s\n' "$*" >&2
+ exit 1
+}
+
+detect_compose() {
+ if docker compose version >/dev/null 2>&1; then
+ COMPOSE_CMD=(docker compose)
+ return
+ fi
+
+ if command -v docker-compose >/dev/null 2>&1; then
+ COMPOSE_CMD=(docker-compose)
+ return
+ fi
+
+ die "Docker Compose is not available. Install Docker Compose, then run this command again."
+}
+
+generate_secret_key() {
+ if command -v openssl >/dev/null 2>&1; then
+ openssl rand -base64 42
+ return
+ fi
+
+ if command -v dd >/dev/null 2>&1 && command -v base64 >/dev/null 2>&1; then
+ dd if=/dev/urandom bs=42 count=1 2>/dev/null | base64 | tr -d '\n'
+ printf '\n'
+ return
+ fi
+
+ return 1
+}
+
+ensure_env_files() {
+ [[ -f "$DEFAULT_ENV_FILE" ]] || die "$DEFAULT_ENV_FILE is missing."
+
+ if [[ -f "$USER_ENV_FILE" ]]; then
+ return
+ fi
+
+ : >"$USER_ENV_FILE"
+
+ if [[ ! -t 0 ]]; then
+ log "Created $USER_ENV_FILE for local overrides."
+ return
+ fi
+
+ printf 'Created %s for local overrides.\n' "$USER_ENV_FILE"
+ printf 'Do you need a custom deployment now? (Most users can press Enter to skip.) [y/N] '
+ read -r answer
+
+ case "${answer:-}" in
+ y | Y | yes | YES | Yes)
+ cat <<'EOF'
+Edit .env with the settings you want to override, using .env.example as the full reference.
+Run ./dify-compose up -d again when you are ready.
+EOF
+ exit 0
+ ;;
+ esac
+}
+
+user_env_value() {
+ local key="$1"
+ awk -F= -v target="$key" '
+ /^[[:space:]]*#/ || !/=/{ next }
+ {
+ key = $1
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
+ if (key == target) {
+ value = substr($0, index($0, "=") + 1)
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
+ if ((value ~ /^".*"$/) || (value ~ /^'\''.*'\''$/)) {
+ value = substr(value, 2, length(value) - 2)
+ }
+ result = value
+ }
+ }
+ END { print result }
+ ' "$USER_ENV_FILE"
+}
+
+set_user_env_value() {
+ local key="$1"
+ local value="$2"
+ local temp_file
+
+ temp_file="$(mktemp "${TMPDIR:-/tmp}/dify-env.XXXXXX")"
+ awk -F= -v target="$key" -v replacement="$key=$value" '
+ BEGIN { replaced = 0 }
+ /^[[:space:]]*#/ || !/=/{ print; next }
+ {
+ key = $1
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
+ if (key == target) {
+ if (!replaced) {
+ print replacement
+ replaced = 1
+ }
+ next
+ }
+ print
+ }
+ END {
+ if (!replaced) {
+ print replacement
+ }
+ }
+ ' "$USER_ENV_FILE" >"$temp_file"
+ mv "$temp_file" "$USER_ENV_FILE"
+}
+
+ensure_secret_key() {
+ local current_secret_key
+ local secret_key
+
+ current_secret_key="$(user_env_value SECRET_KEY)"
+ if [[ -n "$current_secret_key" ]]; then
+ return
+ fi
+
+ secret_key="$(generate_secret_key)" || die "Unable to generate SECRET_KEY. Install openssl or configure SECRET_KEY in .env."
+ set_user_env_value SECRET_KEY "$secret_key"
+ log "Generated SECRET_KEY in $USER_ENV_FILE."
+}
+
+env_value() {
+ local key="$1"
+ awk -F= -v target="$key" '
+ /^[[:space:]]*#/ || !/=/{ next }
+ {
+ key = $1
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
+ if (key == target) {
+ value = substr($0, index($0, "=") + 1)
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
+ if ((value ~ /^".*"$/) || (value ~ /^'\''.*'\''$/)) {
+ value = substr(value, 2, length(value) - 2)
+ }
+ result = value
+ }
+ }
+ END { print result }
+ ' "$DEFAULT_ENV_FILE" "$USER_ENV_FILE"
+}
+
+user_overrides() {
+ local key="$1"
+ grep -Eq "^[[:space:]]*${key}[[:space:]]*=" "$USER_ENV_FILE"
+}
+
+write_merged_env() {
+ awk '
+ function trim(s) {
+ sub(/^[[:space:]]+/, "", s)
+ sub(/[[:space:]]+$/, "", s)
+ return s
+ }
+
+ /^[[:space:]]*#/ || !/=/{ next }
+
+ {
+ key = $0
+ sub(/=.*/, "", key)
+ key = trim(key)
+ if (key == "") {
+ next
+ }
+
+ value = substr($0, index($0, "=") + 1)
+ value = trim(value)
+
+ if (!(key in seen)) {
+ order[++count] = key
+ seen[key] = 1
+ }
+
+ values[key] = value
+ }
+
+ END {
+ for (i = 1; i <= count; i++) {
+ key = order[i]
+ print key "=" values[key]
+ }
+ }
+ ' "$DEFAULT_ENV_FILE" "$USER_ENV_FILE" >"$MERGED_ENV_FILE"
+}
+
+set_merged_env_value() {
+ local key="$1"
+ local value="$2"
+ local temp_file
+
+ temp_file="$(mktemp "${TMPDIR:-/tmp}/dify-compose-env.XXXXXX")"
+ awk -F= -v target="$key" -v replacement="$key=$value" '
+ BEGIN { replaced = 0 }
+ /^[[:space:]]*#/ || !/=/{ print; next }
+ {
+ key = $1
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
+ if (key == target) {
+ if (!replaced) {
+ print replacement
+ replaced = 1
+ }
+ next
+ }
+ print
+ }
+ END {
+ if (!replaced) {
+ print replacement
+ }
+ }
+ ' "$MERGED_ENV_FILE" >"$temp_file"
+ mv "$temp_file" "$MERGED_ENV_FILE"
+}
+
+set_if_not_overridden() {
+ local key="$1"
+ local value="$2"
+
+ if user_overrides "$key"; then
+ return
+ fi
+
+ set_merged_env_value "$key" "$value"
+}
+
+metadata_db_host() {
+ case "$1" in
+ mysql) printf 'db_mysql' ;;
+ postgresql | '') printf 'db_postgres' ;;
+ *) printf '%s' "$(env_value DB_HOST)" ;;
+ esac
+}
+
+metadata_db_port() {
+ case "$1" in
+ mysql) printf '3306' ;;
+ postgresql | '') printf '5432' ;;
+ *) printf '%s' "$(env_value DB_PORT)" ;;
+ esac
+}
+
+metadata_db_user() {
+ case "$1" in
+ mysql) printf 'root' ;;
+ postgresql | '') printf 'postgres' ;;
+ *) printf '%s' "$(env_value DB_USERNAME)" ;;
+ esac
+}
+
+build_merged_env() {
+ MERGED_ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/dify-compose.XXXXXX")"
+ trap 'rm -f "$MERGED_ENV_FILE"' EXIT
+
+ write_merged_env
+
+ local db_type
+ local redis_host
+ local redis_port
+ local redis_username
+ local redis_password
+ local redis_auth
+ local code_execution_api_key
+ local weaviate_api_key
+
+ db_type="$(env_value DB_TYPE)"
+
+ set_if_not_overridden DB_HOST "$(metadata_db_host "$db_type")"
+ set_if_not_overridden DB_PORT "$(metadata_db_port "$db_type")"
+ set_if_not_overridden DB_USERNAME "$(metadata_db_user "$db_type")"
+
+ if ! user_overrides CELERY_BROKER_URL; then
+ redis_host="$(env_value REDIS_HOST)"
+ redis_port="$(env_value REDIS_PORT)"
+ redis_username="$(env_value REDIS_USERNAME)"
+ redis_password="$(env_value REDIS_PASSWORD)"
+ redis_auth=""
+
+ if [[ -n "$redis_username" && -n "$redis_password" ]]; then
+ redis_auth="${redis_username}:${redis_password}@"
+ elif [[ -n "$redis_password" ]]; then
+ redis_auth=":${redis_password}@"
+ elif [[ -n "$redis_username" ]]; then
+ redis_auth="${redis_username}@"
+ fi
+
+ set_merged_env_value CELERY_BROKER_URL "redis://${redis_auth}${redis_host:-redis}:${redis_port:-6379}/1"
+ fi
+
+ if ! user_overrides SANDBOX_API_KEY; then
+ code_execution_api_key="$(env_value CODE_EXECUTION_API_KEY)"
+ set_if_not_overridden SANDBOX_API_KEY "${code_execution_api_key:-dify-sandbox}"
+ fi
+
+ if ! user_overrides WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS; then
+ weaviate_api_key="$(env_value WEAVIATE_API_KEY)"
+ set_if_not_overridden WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS \
+ "${weaviate_api_key:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih}"
+ fi
+}
+
+main() {
+ detect_compose
+ ensure_env_files
+ ensure_secret_key
+ build_merged_env
+
+ if [[ "$#" -eq 0 ]]; then
+ set -- up -d
+ fi
+
+ "${COMPOSE_CMD[@]}" --env-file "$MERGED_ENV_FILE" "$@"
+}
+
+main "$@"
diff --git a/docker/dify-compose.ps1 b/docker/dify-compose.ps1
new file mode 100644
index 0000000000..851f8b76fe
--- /dev/null
+++ b/docker/dify-compose.ps1
@@ -0,0 +1,317 @@
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version Latest
+
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+Set-Location $ScriptDir
+
+$DefaultEnvFile = ".env.default"
+$UserEnvFile = ".env"
+$MergedEnvFile = $null
+$Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList $false
+
+function Write-Info {
+ param([string]$Message)
+ [Console]::Error.WriteLine($Message)
+}
+
+function Fail {
+ param([string]$Message)
+ [Console]::Error.WriteLine("Error: $Message")
+ exit 1
+}
+
+function Test-CommandSuccess {
+ param([string[]]$Command)
+
+ try {
+ $Executable = $Command[0]
+ $CommandArgs = @()
+ if ($Command.Length -gt 1) {
+ $CommandArgs = @($Command[1..($Command.Length - 1)])
+ }
+
+ & $Executable @CommandArgs *> $null
+ return $LASTEXITCODE -eq 0
+ }
+ catch {
+ return $false
+ }
+}
+
+function Get-ComposeCommand {
+ if (Test-CommandSuccess @("docker", "compose", "version")) {
+ return @("docker", "compose")
+ }
+
+ if ((Get-Command "docker-compose" -ErrorAction SilentlyContinue) -and (Test-CommandSuccess @("docker-compose", "version"))) {
+ return @("docker-compose")
+ }
+
+ Fail "Docker Compose is not available. Install Docker Compose, then run this command again."
+}
+
+function New-SecretKey {
+ $Bytes = New-Object byte[] 42
+ $Generator = [System.Security.Cryptography.RandomNumberGenerator]::Create()
+
+ try {
+ $Generator.GetBytes($Bytes)
+ }
+ finally {
+ $Generator.Dispose()
+ }
+
+ return [Convert]::ToBase64String($Bytes)
+}
+
+function Ensure-EnvFiles {
+ if (-not (Test-Path $DefaultEnvFile -PathType Leaf)) {
+ Fail "$DefaultEnvFile is missing."
+ }
+
+ if (Test-Path $UserEnvFile -PathType Leaf) {
+ return
+ }
+
+ New-Item -ItemType File -Path $UserEnvFile | Out-Null
+
+ if ([Console]::IsInputRedirected) {
+ Write-Info "Created $UserEnvFile for local overrides."
+ return
+ }
+
+ Write-Info "Created $UserEnvFile for local overrides."
+ $Answer = Read-Host "Do you need a custom deployment now? (Most users can press Enter to skip.) [y/N]"
+
+ if ($Answer -match "^(y|yes)$") {
+ Write-Output "Edit .env with the settings you want to override, using .env.example as the full reference."
+ Write-Output "Run .\dify-compose.ps1 up -d again when you are ready."
+ exit 0
+ }
+}
+
+function Read-EnvFile {
+ param([string]$Path)
+
+ $Values = [ordered]@{}
+
+ if (-not (Test-Path $Path -PathType Leaf)) {
+ return $Values
+ }
+
+ foreach ($Line in Get-Content -Path $Path) {
+ if ($Line -match "^\s*#" -or $Line -notmatch "=") {
+ continue
+ }
+
+ $SeparatorIndex = $Line.IndexOf("=")
+ $Key = $Line.Substring(0, $SeparatorIndex).Trim()
+ $Value = $Line.Substring($SeparatorIndex + 1).Trim()
+
+ if (($Value.StartsWith('"') -and $Value.EndsWith('"')) -or ($Value.StartsWith("'") -and $Value.EndsWith("'"))) {
+ $Value = $Value.Substring(1, $Value.Length - 2)
+ }
+
+ if ($Key.Length -gt 0) {
+ $Values[$Key] = $Value
+ }
+ }
+
+ return $Values
+}
+
+function Set-UserEnvValue {
+ param(
+ [string]$Key,
+ [string]$Value
+ )
+
+ $Path = [string](Resolve-Path $UserEnvFile)
+ $Lines = [System.IO.File]::ReadAllLines($Path, [System.Text.Encoding]::UTF8)
+ $Output = New-Object System.Collections.Generic.List[string]
+ $Replaced = $false
+
+ foreach ($Line in $Lines) {
+ if ($Line -match "^\s*#" -or $Line -notmatch "=") {
+ $Output.Add($Line)
+ continue
+ }
+
+ $SeparatorIndex = $Line.IndexOf("=")
+ $CurrentKey = $Line.Substring(0, $SeparatorIndex).Trim()
+
+ if ($CurrentKey -eq $Key) {
+ if (-not $Replaced) {
+ $Output.Add("$Key=$Value")
+ $Replaced = $true
+ }
+ continue
+ }
+
+ $Output.Add($Line)
+ }
+
+ if (-not $Replaced) {
+ $Output.Add("$Key=$Value")
+ }
+
+ [System.IO.File]::WriteAllLines($Path, $Output, $Utf8NoBom)
+}
+
+function Ensure-SecretKey {
+ $Values = Read-EnvFile $UserEnvFile
+
+ if ($Values.Contains("SECRET_KEY") -and $Values["SECRET_KEY"]) {
+ return
+ }
+
+ Set-UserEnvValue "SECRET_KEY" (New-SecretKey)
+ Write-Info "Generated SECRET_KEY in $UserEnvFile."
+}
+
+function Merge-EnvValues {
+ $Values = [ordered]@{}
+
+ foreach ($Entry in (Read-EnvFile $DefaultEnvFile).GetEnumerator()) {
+ $Values[$Entry.Key] = $Entry.Value
+ }
+
+ foreach ($Entry in (Read-EnvFile $UserEnvFile).GetEnumerator()) {
+ $Values[$Entry.Key] = $Entry.Value
+ }
+
+ return $Values
+}
+
+function User-Overrides {
+ param([string]$Key)
+
+ if (-not (Test-Path $UserEnvFile -PathType Leaf)) {
+ return $false
+ }
+
+ return [bool](Select-String -Path $UserEnvFile -Pattern "^\s*$([regex]::Escape($Key))\s*=" -Quiet)
+}
+
+function Metadata-DbHost {
+ param([string]$DbType, $Values)
+
+ switch ($DbType) {
+ "mysql" { return "db_mysql" }
+ "postgresql" { return "db_postgres" }
+ "" { return "db_postgres" }
+ default { return $Values["DB_HOST"] }
+ }
+}
+
+function Metadata-DbPort {
+ param([string]$DbType, $Values)
+
+ switch ($DbType) {
+ "mysql" { return "3306" }
+ "postgresql" { return "5432" }
+ "" { return "5432" }
+ default { return $Values["DB_PORT"] }
+ }
+}
+
+function Metadata-DbUser {
+ param([string]$DbType, $Values)
+
+ switch ($DbType) {
+ "mysql" { return "root" }
+ "postgresql" { return "postgres" }
+ "" { return "postgres" }
+ default { return $Values["DB_USERNAME"] }
+ }
+}
+
+function Write-MergedEnv {
+ param($Values)
+
+ $Output = New-Object System.Collections.Generic.List[string]
+
+ foreach ($Entry in $Values.GetEnumerator()) {
+ $Output.Add("$($Entry.Key)=$($Entry.Value)")
+ }
+
+ [System.IO.File]::WriteAllLines($MergedEnvFile, $Output, $Utf8NoBom)
+}
+
+function Build-MergedEnv {
+ $Values = Merge-EnvValues
+ $script:MergedEnvFile = [System.IO.Path]::GetTempFileName()
+
+ $DbType = if ($Values.Contains("DB_TYPE")) { $Values["DB_TYPE"] } else { "postgresql" }
+
+ if (-not (User-Overrides "DB_HOST")) {
+ $Values["DB_HOST"] = Metadata-DbHost $DbType $Values
+ }
+
+ if (-not (User-Overrides "DB_PORT")) {
+ $Values["DB_PORT"] = Metadata-DbPort $DbType $Values
+ }
+
+ if (-not (User-Overrides "DB_USERNAME")) {
+ $Values["DB_USERNAME"] = Metadata-DbUser $DbType $Values
+ }
+
+ if (-not (User-Overrides "CELERY_BROKER_URL")) {
+ $RedisHost = if ($Values.Contains("REDIS_HOST") -and $Values["REDIS_HOST"]) { $Values["REDIS_HOST"] } else { "redis" }
+ $RedisPort = if ($Values.Contains("REDIS_PORT") -and $Values["REDIS_PORT"]) { $Values["REDIS_PORT"] } else { "6379" }
+ $RedisUsername = if ($Values.Contains("REDIS_USERNAME")) { $Values["REDIS_USERNAME"] } else { "" }
+ $RedisPassword = if ($Values.Contains("REDIS_PASSWORD")) { $Values["REDIS_PASSWORD"] } else { "" }
+ $RedisAuth = ""
+
+ if ($RedisUsername -and $RedisPassword) {
+ $RedisAuth = "${RedisUsername}:${RedisPassword}@"
+ }
+ elseif ($RedisPassword) {
+ $RedisAuth = ":${RedisPassword}@"
+ }
+ elseif ($RedisUsername) {
+ $RedisAuth = "${RedisUsername}@"
+ }
+
+ $Values["CELERY_BROKER_URL"] = "redis://$RedisAuth${RedisHost}:${RedisPort}/1"
+ }
+
+ if (-not (User-Overrides "SANDBOX_API_KEY")) {
+ $CodeExecutionApiKey = if ($Values.Contains("CODE_EXECUTION_API_KEY") -and $Values["CODE_EXECUTION_API_KEY"]) { $Values["CODE_EXECUTION_API_KEY"] } else { "dify-sandbox" }
+ $Values["SANDBOX_API_KEY"] = $CodeExecutionApiKey
+ }
+
+ if (-not (User-Overrides "WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS")) {
+ $WeaviateApiKey = if ($Values.Contains("WEAVIATE_API_KEY") -and $Values["WEAVIATE_API_KEY"]) { $Values["WEAVIATE_API_KEY"] } else { "WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih" }
+ $Values["WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS"] = $WeaviateApiKey
+ }
+
+ Write-MergedEnv $Values
+}
+
+$ComposeCommand = Get-ComposeCommand
+
+try {
+ Ensure-EnvFiles
+ Ensure-SecretKey
+ Build-MergedEnv
+
+ $ComposeArgs = @($args)
+ if ($ComposeArgs.Count -eq 0) {
+ $ComposeArgs = @("up", "-d")
+ }
+
+ $ComposeCommandArgs = @()
+ if ($ComposeCommand.Length -gt 1) {
+ $ComposeCommandArgs = @($ComposeCommand[1..($ComposeCommand.Length - 1)])
+ }
+
+ $ComposeExecutable = $ComposeCommand[0]
+ & $ComposeExecutable @ComposeCommandArgs --env-file $MergedEnvFile @ComposeArgs
+ exit $LASTEXITCODE
+}
+finally {
+ if ($MergedEnvFile -and (Test-Path $MergedEnvFile -PathType Leaf)) {
+ Remove-Item -Force $MergedEnvFile
+ }
+}
diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml
index 87fa01f671..b2df61ebb2 100644
--- a/docker/docker-compose-template.yaml
+++ b/docker/docker-compose-template.yaml
@@ -170,8 +170,8 @@ services:
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
- TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
- INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
+ TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-10}
+ INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100}
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
@@ -402,8 +402,8 @@ services:
- ./certbot/update-cert.template.txt:/update-cert.template.txt
- ./certbot/docker-entrypoint.sh:/docker-entrypoint.sh
environment:
- - CERTBOT_EMAIL=${CERTBOT_EMAIL}
- - CERTBOT_DOMAIN=${CERTBOT_DOMAIN}
+ - CERTBOT_EMAIL=${CERTBOT_EMAIL:-}
+ - CERTBOT_DOMAIN=${CERTBOT_DOMAIN:-}
- CERTBOT_OPTIONS=${CERTBOT_OPTIONS:-}
entrypoint: ["/docker-entrypoint.sh"]
command: ["tail", "-f", "/dev/null"]
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index a72136049d..6dcab4a9fc 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -441,10 +441,10 @@ x-shared-env: &shared-api-worker-env
NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-}
NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-}
NOTION_INTERNAL_SECRET: ${NOTION_INTERNAL_SECRET:-}
- MAIL_TYPE: ${MAIL_TYPE:-resend}
+ MAIL_TYPE: ${MAIL_TYPE:-}
MAIL_DEFAULT_SEND_FROM: ${MAIL_DEFAULT_SEND_FROM:-}
RESEND_API_URL: ${RESEND_API_URL:-https://api.resend.com}
- RESEND_API_KEY: ${RESEND_API_KEY:-your-resend-api-key}
+ RESEND_API_KEY: ${RESEND_API_KEY:-}
SMTP_SERVER: ${SMTP_SERVER:-}
SMTP_PORT: ${SMTP_PORT:-465}
SMTP_USERNAME: ${SMTP_USERNAME:-}
@@ -586,8 +586,8 @@ x-shared-env: &shared-api-worker-env
NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s}
NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s}
NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false}
- CERTBOT_EMAIL: ${CERTBOT_EMAIL:-your_email@example.com}
- CERTBOT_DOMAIN: ${CERTBOT_DOMAIN:-your_domain.com}
+ CERTBOT_EMAIL: ${CERTBOT_EMAIL:-}
+ CERTBOT_DOMAIN: ${CERTBOT_DOMAIN:-}
CERTBOT_OPTIONS: ${CERTBOT_OPTIONS:-}
SSRF_HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
SSRF_COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
@@ -894,8 +894,8 @@ services:
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
- TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
- INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
+ TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-10}
+ INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100}
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
@@ -1126,8 +1126,8 @@ services:
- ./certbot/update-cert.template.txt:/update-cert.template.txt
- ./certbot/docker-entrypoint.sh:/docker-entrypoint.sh
environment:
- - CERTBOT_EMAIL=${CERTBOT_EMAIL}
- - CERTBOT_DOMAIN=${CERTBOT_DOMAIN}
+ - CERTBOT_EMAIL=${CERTBOT_EMAIL:-}
+ - CERTBOT_DOMAIN=${CERTBOT_DOMAIN:-}
- CERTBOT_OPTIONS=${CERTBOT_OPTIONS:-}
entrypoint: ["/docker-entrypoint.sh"]
command: ["tail", "-f", "/dev/null"]
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index b4876dcf45..b5e67df509 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -315,20 +315,12 @@
"count": 4
}
},
- "web/app/components/app/configuration/config-var/config-modal/type-select.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/app/configuration/config-var/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/configuration/config-var/select-var-type.tsx": {
- "no-restricted-imports": {
- "count": 1
- },
"ts/no-explicit-any": {
"count": 1
}
@@ -363,9 +355,6 @@
}
},
"web/app/components/app/configuration/config/assistant-type-picker/index.tsx": {
- "no-restricted-imports": {
- "count": 1
- },
"ts/no-explicit-any": {
"count": 1
}
@@ -401,11 +390,6 @@
"count": 1
}
},
- "web/app/components/app/configuration/config/automatic/version-selector.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx": {
"no-restricted-imports": {
"count": 1
@@ -774,11 +758,6 @@
"count": 1
}
},
- "web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/base/chat/chat/answer/agent-content.tsx": {
"style/multiline-ternary": {
"count": 2
@@ -800,11 +779,6 @@
"count": 1
}
},
- "web/app/components/base/chat/chat/answer/operation.tsx": {
- "no-restricted-imports": {
- "count": 2
- }
- },
"web/app/components/base/chat/chat/answer/workflow-process.tsx": {
"react/set-state-in-effect": {
"count": 1
@@ -1055,14 +1029,6 @@
"count": 3
}
},
- "web/app/components/base/form/components/base/base-field.tsx": {
- "no-restricted-imports": {
- "count": 1
- },
- "ts/no-explicit-any": {
- "count": 3
- }
- },
"web/app/components/base/form/components/base/base-form.tsx": {
"ts/no-explicit-any": {
"count": 6
@@ -1589,14 +1555,6 @@
"count": 1
}
},
- "web/app/components/base/modal/modal.stories.tsx": {
- "no-console": {
- "count": 4
- },
- "react/set-state-in-effect": {
- "count": 1
- }
- },
"web/app/components/base/new-audio-button/index.tsx": {
"ts/no-explicit-any": {
"count": 1
@@ -2116,11 +2074,6 @@
"count": 1
}
},
- "web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx": {
"ts/no-explicit-any": {
"count": 1
@@ -2389,11 +2342,6 @@
"count": 1
}
},
- "web/app/components/datasets/settings/index-method/index.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/develop/code.tsx": {
"ts/no-empty-object-type": {
"count": 1
@@ -2579,11 +2527,8 @@
"erasable-syntax-only/enums": {
"count": 1
},
- "no-restricted-imports": {
- "count": 1
- },
"ts/no-explicit-any": {
- "count": 3
+ "count": 2
}
},
"web/app/components/header/account-setting/model-provider-page/declarations.ts": {
@@ -2612,11 +2557,6 @@
"count": 1
}
},
- "web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/header/account-setting/model-provider-page/model-auth/hooks/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 6
@@ -2912,44 +2852,11 @@
"count": 1
}
},
- "web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts": {
- "erasable-syntax-only/enums": {
- "count": 2
- }
- },
- "web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx": {
- "no-barrel-files/no-barrel-files": {
- "count": 3
- }
- },
- "web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/plugins/plugin-detail-panel/subscription-list/create/types.ts": {
"erasable-syntax-only/enums": {
"count": 1
}
},
- "web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx": {
- "erasable-syntax-only/enums": {
- "count": 1
- },
- "no-restricted-imports": {
- "count": 1
- }
- },
- "web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
- "web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 2
@@ -2978,11 +2885,6 @@
"count": 7
}
},
- "web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": {
"no-restricted-imports": {
"count": 1
@@ -2998,11 +2900,6 @@
"count": 5
}
},
- "web/app/components/plugins/plugin-item/action.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/plugins/plugin-item/index.tsx": {
"no-restricted-imports": {
"count": 1
@@ -3681,11 +3578,6 @@
"count": 1
}
},
- "web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/workflow/nodes/_base/components/error-handle/types.ts": {
"erasable-syntax-only/enums": {
"count": 1
@@ -3782,11 +3674,6 @@
"count": 1
}
},
- "web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/workflow/nodes/_base/components/variable/variable-label/hooks.ts": {
"react/no-unnecessary-use-prefix": {
"count": 2
@@ -4106,16 +3993,6 @@
"count": 1
}
},
- "web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
- "web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/workflow/nodes/if-else/default.ts": {
"ts/no-explicit-any": {
"count": 1
@@ -4151,11 +4028,6 @@
"count": 6
}
},
- "web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/selector.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/hooks.tsx": {
"ts/no-explicit-any": {
"count": 4
@@ -4199,11 +4071,6 @@
"count": 1
}
},
- "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/metadata-filter-selector.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/workflow/nodes/knowledge-retrieval/default.ts": {
"ts/no-explicit-any": {
"count": 1
@@ -4285,11 +4152,6 @@
"count": 1
}
},
- "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts": {
"ts/no-explicit-any": {
"count": 1
@@ -4341,16 +4203,6 @@
"count": 1
}
},
- "web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
- "web/app/components/workflow/nodes/loop/components/condition-number-input.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx": {
"ts/no-explicit-any": {
"count": 3
@@ -4522,9 +4374,6 @@
}
},
"web/app/components/workflow/nodes/tool/components/tool-form/item.tsx": {
- "no-restricted-imports": {
- "count": 1
- },
"ts/no-explicit-any": {
"count": 1
}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index ae9fdaff01..1380ed67d2 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -4,6 +4,17 @@ import antfu, { GLOB_MARKDOWN } from '@antfu/eslint-config'
import md from 'eslint-markdown'
import markdownPreferences from 'eslint-plugin-markdown-preferences'
+const GENERATED_IGNORES = [
+ '**/storybook-static/',
+ '**/.next/',
+ 'web/next/',
+ 'web/next-env.d.ts',
+ '**/dist/',
+ '**/coverage/',
+ 'e2e/.auth/',
+ 'e2e/cucumber-report/',
+]
+
export default antfu(
{
ignores: original => [
@@ -15,6 +26,7 @@ export default antfu(
'!package.json',
'!pnpm-workspace.yaml',
'!vite.config.ts',
+ ...GENERATED_IGNORES,
...original,
],
typescript: {
diff --git a/package.json b/package.json
index a563b574f7..baef89c4e8 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "dify",
"type": "module",
"private": true,
- "packageManager": "pnpm@11.0.0",
+ "packageManager": "pnpm@11.0.6",
"engines": {
"node": "^22.22.1"
},
diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md
index d8a59b7a0b..bdc2160702 100644
--- a/packages/dify-ui/AGENTS.md
+++ b/packages/dify-ui/AGENTS.md
@@ -56,4 +56,28 @@ The Figma design system uses `--radius/*` tokens whose scale is **offset by one
- When the Figma MCP returns `rounded-[var(--radius/sm, 6px)]`, convert it to the standard Tailwind class from the table above (e.g. `rounded-md`).
- For values without a standard Tailwind equivalent (10px, 20px, 28px), use arbitrary values like `rounded-[10px]`.
+## Search / Picker Primitive Selection: Autocomplete vs Combobox vs Select
+
+Pick by whether the user is entering free-form text, choosing a remembered value, or selecting from a closed list.
+
+Base UI decision rules:
+
+- [Autocomplete docs]: use `Combobox` instead of `Autocomplete` if the selection should be remembered and the input value cannot be custom.
+- [Combobox docs]: do not use `Combobox` for simple search widgets that require unrestricted text entry; use `Autocomplete` instead.
+
+Apply this split in Dify UI:
+
+- `Autocomplete` — free-form text input with optional suggestions or completions. The input value may be custom and does not necessarily become a selected option. Use for search boxes, command-style suggestions, tag suggestions, and async text completion.
+- `Combobox` — searchable picker whose value is one or more selected items from a collection. The chosen value is remembered by the root, and free-form text is not the final value. Use for model pickers, user pickers, dataset/document pickers, and multi-select chips.
+- `Select` — closed-list picker without text entry. Use when the option set is small or already scannable and filtering is unnecessary.
+
+Composition rules:
+
+- Keep Base UI primitive semantics visible in the public API. Export compound parts such as `ComboboxInputGroup`, `ComboboxInput`, `ComboboxContent`, `ComboboxList`, `ComboboxItem`, and `ComboboxItemIndicator` instead of wrapping them into one business component.
+- For `Combobox` multiple selection, follow the official chips pattern: `ComboboxInputGroup` contains `ComboboxChips`, `ComboboxValue` renders `ComboboxChip` items, and `ComboboxInput` remains inside the chips row. Chips should wrap and let the input group grow vertically instead of forcing horizontal overflow.
+- Content primitives must own their Base UI `Portal` and use `z-1002` on `Positioner`, matching the overlay contract in `README.md`.
+- Use `w-(--anchor-width)` with viewport-aware max-width for `Autocomplete` and `Combobox` popups. Do not add `min-w-(--anchor-width)` when it would defeat available-width clamping.
+
+[Autocomplete docs]: https://base-ui.com/react/components/autocomplete.md#usage-guidelines
+[Combobox docs]: https://base-ui.com/react/components/combobox.md#usage-guidelines
[docs]: https://base-ui.com/react/components/tooltip#infotips
diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md
index cd24a0c078..bdeeec33cb 100644
--- a/packages/dify-ui/README.md
+++ b/packages/dify-ui/README.md
@@ -36,12 +36,12 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
## Primitives
-| Category | Subpath | Notes |
-| -------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
-| Overlay | `./alert-dialog`, `./context-menu`, `./dialog`, `./dropdown-menu`, `./popover`, `./select`, `./toast`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
-| Form | `./number-field`, `./slider`, `./switch` | Controlled / uncontrolled per Base UI defaults. |
-| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
-| Media | `./avatar`, `./button` | Button exposes `cva` variants. |
+| Category | Subpath | Notes |
+| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
+| Overlay | `./alert-dialog`, `./autocomplete`, `./combobox`, `./context-menu`, `./dialog`, `./dropdown-menu`, `./popover`, `./select`, `./toast`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
+| Form | `./autocomplete`, `./combobox`, `./number-field`, `./slider`, `./switch` | Controlled / uncontrolled per Base UI defaults. |
+| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
+| Media | `./avatar`, `./button` | Button exposes `cva` variants. |
Utilities:
@@ -65,7 +65,7 @@ If a consumer uses Dify UI source files through the workspace, add an explicit s
## Overlay & portal contract
-All overlay primitives (`dialog`, `alert-dialog`, `popover`, `dropdown-menu`, `context-menu`, `select`, `tooltip`, `toast`) render their content inside a [Base UI Portal] attached to `document.body`. This is the Base UI default — see the upstream [Portals][Base UI Portal] docs for the underlying behavior. Consumers **do not** need to wrap anything in a portal manually.
+All overlay primitives (`dialog`, `alert-dialog`, `autocomplete`, `combobox`, `popover`, `dropdown-menu`, `context-menu`, `select`, `tooltip`, `toast`) render their content inside a [Base UI Portal] attached to `document.body`. This is the Base UI default — see the upstream [Portals][Base UI Portal] docs for the underlying behavior. Consumers **do not** need to wrap anything in a portal manually.
### Root isolation requirement
@@ -83,14 +83,14 @@ Equivalent: any root element with `isolation: isolate` in CSS. Without it, overl
Every overlay primitive uses a single, shared z-index. Do **not** override it at call sites.
-| Layer | z-index | Where |
-| ----------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
-| Overlays (Dialog, AlertDialog, Popover, DropdownMenu, ContextMenu, Select, Tooltip) | `z-1002` | Positioner / Backdrop |
-| Toast viewport | `z-1003` | One layer above overlays so notifications are never hidden under a dialog. |
+| Layer | z-index | Where |
+| ----------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
+| Overlays (Dialog, AlertDialog, Autocomplete, Combobox, Popover, DropdownMenu, ContextMenu, Select, Tooltip) | `z-1002` | Positioner / Backdrop |
+| Toast viewport | `z-1003` | One layer above overlays so notifications are never hidden under a dialog. |
-Rationale: during Dify's migration from legacy `portal-to-follow-elem` / `base/modal` / `base/dialog` overlays to this package, new and old overlays coexist in the DOM. `z-1002` sits above any common legacy layer, eliminating per-call-site z-index hacks. Among themselves, new primitives share the same z-index and **rely on DOM order** for stacking — the portal mounted later wins.
+Rationale: during Dify's migration from legacy `base/modal` / `base/dialog` overlays to this package, new and old overlays coexist in the DOM. `z-1002` sits above any common legacy layer, eliminating per-call-site z-index hacks. Among themselves, new primitives share the same z-index and **rely on DOM order** for stacking — the portal mounted later wins.
-See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for the Dify-web migration history and the remaining legacy allowlist. Once the legacy overlays are gone, the values in this table can drop back to `z-50` / `z-51`.
+See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for the Dify-web migration history. Once the legacy overlays are gone, the values in this table can drop back to `z-50` / `z-51`.
### Rules
diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json
index 73c6c0bd22..20e94c7dee 100644
--- a/packages/dify-ui/package.json
+++ b/packages/dify-ui/package.json
@@ -13,6 +13,10 @@
"types": "./src/alert-dialog/index.tsx",
"import": "./src/alert-dialog/index.tsx"
},
+ "./autocomplete": {
+ "types": "./src/autocomplete/index.tsx",
+ "import": "./src/autocomplete/index.tsx"
+ },
"./avatar": {
"types": "./src/avatar/index.tsx",
"import": "./src/avatar/index.tsx"
@@ -21,6 +25,10 @@
"types": "./src/button/index.tsx",
"import": "./src/button/index.tsx"
},
+ "./combobox": {
+ "types": "./src/combobox/index.tsx",
+ "import": "./src/combobox/index.tsx"
+ },
"./context-menu": {
"types": "./src/context-menu/index.tsx",
"import": "./src/context-menu/index.tsx"
@@ -103,6 +111,7 @@
"@storybook/addon-themes": "catalog:",
"@storybook/react-vite": "catalog:",
"@tailwindcss/vite": "catalog:",
+ "@tanstack/react-virtual": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@typescript/native-preview": "catalog:",
diff --git a/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx b/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx
new file mode 100644
index 0000000000..a7031c5b12
--- /dev/null
+++ b/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx
@@ -0,0 +1,252 @@
+import type { ReactNode } from 'react'
+import { render } from 'vitest-browser-react'
+import {
+ Autocomplete,
+ AutocompleteClear,
+ AutocompleteContent,
+ AutocompleteEmpty,
+ AutocompleteGroup,
+ AutocompleteInput,
+ AutocompleteInputGroup,
+ AutocompleteItem,
+ AutocompleteItemIndicator,
+ AutocompleteItemText,
+ AutocompleteLabel,
+ AutocompleteList,
+ AutocompleteSeparator,
+ AutocompleteStatus,
+ AutocompleteTrigger,
+} from '../index'
+
+const renderWithSafeViewport = (ui: ReactNode) => render(
+
+ {ui}
+
,
+)
+
+const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
+
+const renderAutocomplete = ({
+ children,
+ open = false,
+ defaultValue = 'workflow',
+}: {
+ children?: ReactNode
+ open?: boolean
+ defaultValue?: string
+} = {}) => renderWithSafeViewport(
+
+ {children ?? (
+ <>
+
+
+
+
+
+
+ 2 suggestions
+
+
+ Workflow
+
+
+
+ Dataset
+
+
+ No suggestions
+
+ >
+ )}
+ ,
+)
+
+describe('Autocomplete wrappers', () => {
+ describe('Input group and input', () => {
+ it('should apply medium input group and input classes by default', async () => {
+ const screen = await renderAutocomplete()
+
+ await expect.element(screen.getByTestId('input-group')).toHaveClass('rounded-lg')
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('px-3')
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('system-sm-regular')
+ })
+
+ it('should apply large input group and input classes when large size is provided', async () => {
+ const screen = await renderAutocomplete({
+ children: (
+
+
+
+ ),
+ })
+
+ await expect.element(screen.getByTestId('input-group')).toHaveClass('rounded-[10px]')
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('px-4')
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('system-md-regular')
+ })
+
+ it('should set input defaults and forward passthrough props', async () => {
+ const screen = await renderAutocomplete({
+ children: (
+
+
+
+ ),
+ })
+
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveAttribute('autocomplete', 'off')
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveAttribute('type', 'text')
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveAttribute('placeholder', 'Find a resource')
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toBeRequired()
+ await expect.element(screen.getByRole('combobox', { name: 'Search suggestions' })).toHaveClass('custom-input')
+ })
+ })
+
+ describe('Controls', () => {
+ it('should provide fallback aria labels and decorative icons when labels are omitted', async () => {
+ const screen = await renderAutocomplete()
+
+ await expect.element(screen.getByRole('button', { name: 'Clear autocomplete' })).toHaveAttribute('type', 'button')
+ await expect.element(screen.getByRole('button', { name: 'Open autocomplete suggestions' })).toHaveAttribute('type', 'button')
+ expect(screen.getByRole('button', { name: 'Clear autocomplete' }).element().querySelector('.i-ri-close-line')).toHaveAttribute('aria-hidden', 'true')
+ expect(screen.getByRole('button', { name: 'Open autocomplete suggestions' }).element().querySelector('.i-ri-arrow-down-s-line')).toHaveAttribute('aria-hidden', 'true')
+ })
+
+ it('should preserve explicit labels and custom children', async () => {
+ const screen = await renderAutocomplete({
+ children: (
+
+
+
+ reset
+
+
+ open
+
+
+ ),
+ })
+
+ expect(screen.getByRole('button', { name: 'Reset search' }).element()).toContainElement(screen.getByTestId('custom-clear').element())
+ expect(screen.getByRole('button', { name: 'Show suggestions' }).element()).toContainElement(screen.getByTestId('custom-trigger').element())
+ expect(screen.getByRole('button', { name: 'Reset search' }).element().querySelector('.i-ri-close-line')).not.toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Show suggestions' }).element().querySelector('.i-ri-arrow-down-s-line')).not.toBeInTheDocument()
+ })
+
+ it('should rely on aria-labelledby when provided instead of injecting fallback labels', async () => {
+ const screen = await renderAutocomplete({
+ children: (
+ <>
+ Clear from label
+ Trigger from label
+
+
+
+
+
+ >
+ ),
+ })
+
+ await expect.element(screen.getByRole('button', { name: 'Clear from label' })).not.toHaveAttribute('aria-label')
+ await expect.element(screen.getByRole('button', { name: 'Trigger from label' })).not.toHaveAttribute('aria-label')
+ })
+ })
+
+ describe('Content and options', () => {
+ it('should use default overlay placement and Dify popup classes', async () => {
+ const screen = await renderAutocomplete({ open: true })
+
+ await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveAttribute('data-side', 'bottom')
+ await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveAttribute('data-align', 'start')
+ await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveClass('z-1002')
+ await expect.element(screen.getByRole('dialog', { name: 'autocomplete popup' })).toHaveClass('rounded-xl')
+ await expect.element(screen.getByRole('dialog', { name: 'autocomplete popup' })).toHaveClass('w-(--anchor-width)')
+ await expect.element(screen.getByRole('listbox', { name: 'autocomplete list' })).toHaveClass('scroll-py-1')
+ })
+
+ it('should apply custom placement side and passthrough popup props', async () => {
+ const onPopupClick = vi.fn()
+ const screen = await renderWithSafeViewport(
+
+
+
+
+
+
+
+ Workflow
+
+
+
+ ,
+ )
+
+ asHTMLElement(screen.getByRole('dialog', { name: 'autocomplete popup' }).element()).click()
+
+ await expect.element(screen.getByRole('group', { name: 'autocomplete positioner' })).toHaveAttribute('data-side', 'top')
+ expect(onPopupClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('should render item text indicator status and empty wrappers with design classes', async () => {
+ const screen = await renderAutocomplete({ open: true })
+
+ await expect.element(screen.getByText('Workflow')).toHaveClass('system-sm-medium')
+ await expect.element(screen.getByTestId('status')).toHaveClass('text-text-tertiary')
+ await expect.element(screen.getByTestId('empty')).toHaveClass('system-sm-regular')
+ expect(screen.getByText('Workflow').element().parentElement?.querySelector('.i-ri-arrow-right-line')).toHaveAttribute('aria-hidden', 'true')
+ })
+
+ it('should forward custom classes to label separator item text and indicator', async () => {
+ const screen = await renderWithSafeViewport(
+
+
+
+
+
+
+
+ Resources
+
+
+ Workflow
+
+
+
+
+
+ ,
+ )
+
+ await expect.element(screen.getByText('Resources')).toHaveClass('custom-label')
+ await expect.element(screen.getByTestId('separator')).toHaveClass('custom-separator')
+ await expect.element(screen.getByRole('option', { name: 'Workflow' })).toHaveClass('custom-item')
+ await expect.element(screen.getByText('Workflow')).toHaveClass('custom-text')
+ await expect.element(screen.getByTestId('indicator')).toHaveClass('custom-indicator')
+ })
+ })
+})
diff --git a/packages/dify-ui/src/autocomplete/index.stories.tsx b/packages/dify-ui/src/autocomplete/index.stories.tsx
new file mode 100644
index 0000000000..71c7c6607d
--- /dev/null
+++ b/packages/dify-ui/src/autocomplete/index.stories.tsx
@@ -0,0 +1,721 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import type { Virtualizer } from '@tanstack/react-virtual'
+import type { RefObject } from 'react'
+import { useVirtualizer } from '@tanstack/react-virtual'
+import { useEffect, useMemo, useRef, useState } from 'react'
+import {
+ Autocomplete,
+ AutocompleteClear,
+ AutocompleteCollection,
+ AutocompleteContent,
+ AutocompleteEmpty,
+ AutocompleteGroup,
+ AutocompleteInput,
+ AutocompleteInputGroup,
+ AutocompleteItem,
+ AutocompleteItemText,
+ AutocompleteLabel,
+ AutocompleteList,
+ AutocompleteSeparator,
+ AutocompleteStatus,
+ AutocompleteTrigger,
+ useAutocompleteFilter,
+ useAutocompleteFilteredItems,
+} from '.'
+import { cn } from '../cn'
+
+type Suggestion = {
+ value: string
+ label: string
+ description?: string
+ icon?: string
+ meta?: string
+}
+
+type SuggestionGroup = {
+ label: string
+ items: Suggestion[]
+}
+
+const inputWidth = 'w-80'
+
+type StoryVirtualizer = Virtualizer
+
+const scrollHighlightedVirtualItem = (
+ item: unknown,
+ {
+ reason,
+ index,
+ }: {
+ reason: 'keyboard' | 'pointer' | 'none'
+ index: number
+ },
+ virtualizer: StoryVirtualizer | null,
+) => {
+ if (!item || !virtualizer)
+ return
+
+ const isStart = index === 0
+ const isEnd = index === virtualizer.options.count - 1
+ const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd))
+
+ if (shouldScroll) {
+ queueMicrotask(() => {
+ virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' })
+ })
+ }
+}
+
+const tagSuggestions: Suggestion[] = [
+ { value: 'feature', label: 'feature', description: 'Product work and launch notes' },
+ { value: 'fix', label: 'fix', description: 'Bug fixes and regressions' },
+ { value: 'docs', label: 'docs', description: 'Documentation updates' },
+ { value: 'internal', label: 'internal', description: 'Workspace-only notes' },
+ { value: 'mobile', label: 'mobile', description: 'Mobile app issues' },
+ { value: 'component: autocomplete', label: 'component: autocomplete', description: 'Base UI primitive wrapper' },
+ { value: 'component: combobox', label: 'component: combobox', description: 'Filterable predefined selection' },
+ { value: 'component: select', label: 'component: select', description: 'Compact predefined selection' },
+]
+
+const promptCompletions: Suggestion[] = [
+ { value: 'summarize this conversation', label: 'summarize this conversation' },
+ { value: 'summarize this dataset with citations', label: 'summarize this dataset with citations' },
+ { value: 'summarize this workflow run for an operator', label: 'summarize this workflow run for an operator' },
+ { value: 'summarize this support ticket in 3 bullets', label: 'summarize this support ticket in 3 bullets' },
+]
+
+const workflowSuggestions: Suggestion[] = [
+ { value: 'http-request', label: 'HTTP Request', description: 'Call an external API', icon: 'i-ri-global-line', meta: 'Tool' },
+ { value: 'knowledge-retrieval', label: 'Knowledge Retrieval', description: 'Search configured datasets', icon: 'i-ri-database-2-line', meta: 'Tool' },
+ { value: 'code-execution', label: 'Code Execution', description: 'Run sandboxed snippets', icon: 'i-ri-code-s-slash-line', meta: 'Tool' },
+ { value: 'template-transform', label: 'Template Transform', description: 'Compose variables into output', icon: 'i-ri-braces-line', meta: 'Tool' },
+ { value: 'question-classifier', label: 'Question Classifier', description: 'Route by intent', icon: 'i-ri-git-branch-line', meta: 'Tool' },
+ { value: 'parameter-extractor', label: 'Parameter Extractor', description: 'Extract typed values', icon: 'i-ri-list-check-3', meta: 'Tool' },
+ { value: 'answer-node', label: 'Answer Node', description: 'Return a final assistant answer', icon: 'i-ri-message-3-line', meta: 'Node' },
+ { value: 'iteration-node', label: 'Iteration Node', description: 'Run a loop over array items', icon: 'i-ri-repeat-line', meta: 'Node' },
+ { value: 'variable-assigner', label: 'Variable Assigner', description: 'Persist intermediate state', icon: 'i-ri-pencil-ruler-2-line', meta: 'Node' },
+]
+
+const groupedSuggestions: SuggestionGroup[] = [
+ {
+ label: 'Tags',
+ items: tagSuggestions.slice(0, 5),
+ },
+ {
+ label: 'Workflow Suggestions',
+ items: workflowSuggestions.slice(0, 5),
+ },
+ {
+ label: 'Prompt Starters',
+ items: promptCompletions.slice(0, 3),
+ },
+]
+
+const commandGroups: SuggestionGroup[] = [
+ {
+ label: 'App',
+ items: [
+ { value: '/run', label: 'Run workflow', description: 'Execute the current draft', icon: 'i-ri-play-circle-line' },
+ { value: '/publish', label: 'Publish app', description: 'Ship the current configuration', icon: 'i-ri-upload-cloud-2-line' },
+ { value: '/trace', label: 'Open trace', description: 'Inspect the latest workflow run', icon: 'i-ri-route-line' },
+ ],
+ },
+ {
+ label: 'Workspace',
+ items: [
+ { value: '/dataset', label: 'Search datasets', description: 'Find knowledge attached to this app', icon: 'i-ri-database-line' },
+ { value: '/members', label: 'Invite members', description: 'Open workspace access settings', icon: 'i-ri-user-add-line' },
+ { value: '/usage', label: 'View usage', description: 'Open model and workflow usage', icon: 'i-ri-bar-chart-line' },
+ ],
+ },
+]
+
+const remoteSuggestions: Suggestion[] = [
+ { value: 'agent-builder', label: 'Agent Builder', description: 'Workspace app' },
+ { value: 'agent-observability', label: 'Agent Observability', description: 'Dataset' },
+ { value: 'agent-routing-dataset', label: 'Agent Routing Dataset', description: 'Knowledge source' },
+]
+
+const virtualizedSuggestions: Suggestion[] = Array.from({ length: 1000 }, (_, index) => {
+ const family = ['workflow', 'dataset', 'prompt', 'tool'][index % 4]!
+ const number = new Intl.NumberFormat('en-US', {
+ minimumIntegerDigits: 4,
+ }).format(index + 1)
+
+ return {
+ value: `${family}-${index + 1}`,
+ label: `${family} suggestion ${number}`,
+ description: `Free-form autocomplete result from ${family} search`,
+ icon: family === 'dataset'
+ ? 'i-ri-database-2-line'
+ : family === 'prompt'
+ ? 'i-ri-text-snippet'
+ : family === 'tool'
+ ? 'i-ri-tools-line'
+ : 'i-ri-flow-chart',
+ meta: family,
+ }
+})
+
+const getSuggestionLabel = (item: Suggestion) => item.label
+
+const SuggestionItem = ({
+ item,
+ index,
+ dense,
+}: {
+ item: Suggestion
+ index?: number
+ dense?: boolean
+}) => (
+
+ {item.icon && }
+
+
{item.label}
+ {!dense && item.description && (
+
{item.description}
+ )}
+
+ {item.meta && (
+
+ {item.meta}
+
+ )}
+
+)
+
+const TagSuggestionItem = ({
+ item,
+ index,
+}: {
+ item: Suggestion
+ index?: number
+}) => (
+
+ {item.label}
+ {item.description && {item.description} }
+
+)
+
+const BasicTagAutocomplete = ({
+ size = 'medium',
+}: {
+ size?: 'small' | 'medium' | 'large'
+}) => (
+
+
+
+
+
+
+
+
+
+ {(item: Suggestion, index: number) => (
+
+ )}
+
+ No tag suggestion. Keep the typed value.
+
+
+)
+
+const GroupedSuggestionList = () => {
+ const groups = useAutocompleteFilteredItems()
+
+ return (
+
+ {groups.map((group, groupIndex) => (
+
+ {groupIndex > 0 && }
+ {group.label}
+
+ {(item: Suggestion) => (
+
+ )}
+
+
+ ))}
+
+ )
+}
+
+const CommandPaletteList = () => {
+ const groups = useAutocompleteFilteredItems()
+
+ return (
+
+ {groups.map((group, groupIndex) => (
+
+ {groupIndex > 0 && }
+ {group.label}
+
+ {(item: Suggestion) => (
+
+
+ {item.icon && }
+
+ {item.label}
+ {item.description}
+
+
+
+ Enter
+
+
+ )}
+
+
+ ))}
+
+ )
+}
+
+const LimitedStatus = ({
+ total,
+}: {
+ total: number
+}) => {
+ const items = useAutocompleteFilteredItems()
+ const hidden = Math.max(0, total - items.length)
+
+ return hidden > 0
+ ? `${hidden} more suggestions hidden. Refine the query to narrow results.`
+ : `${items.length} suggestions available.`
+}
+
+const AsyncSearchDemo = () => {
+ const [value, setValue] = useState('agent')
+ const [loading, setLoading] = useState(false)
+ const [items, setItems] = useState(remoteSuggestions)
+
+ useEffect(() => {
+ setLoading(true)
+ const timeout = window.setTimeout(() => {
+ setItems(
+ value.trim()
+ ? remoteSuggestions.filter(item => item.label.toLowerCase().includes(value.trim().toLowerCase()))
+ : remoteSuggestions,
+ )
+ setLoading(false)
+ }, 500)
+
+ return () => window.clearTimeout(timeout)
+ }, [value])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {loading ? 'Loading suggestions…' : `${items.length} remote suggestions`}
+
+
+ {(item: Suggestion, index: number) => (
+
+ )}
+
+ No remote suggestion. Keep the typed query.
+
+
+
+ )
+}
+
+const VirtualizedSuggestionList = ({
+ virtualizerRef,
+}: {
+ virtualizerRef: RefObject
+}) => {
+ const scrollRef = useRef(null)
+ const filteredItems = useAutocompleteFilteredItems()
+ const virtualizer = useVirtualizer({
+ count: filteredItems.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 44,
+ overscan: 6,
+ })
+
+ useEffect(() => {
+ virtualizerRef.current = virtualizer
+
+ return () => {
+ virtualizerRef.current = null
+ }
+ }, [virtualizer, virtualizerRef])
+
+ return (
+
+
+ {virtualizer.getVirtualItems().map((virtualItem) => {
+ const item = filteredItems[virtualItem.index]
+
+ if (!item)
+ return null
+
+ return (
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+const VirtualizedStatus = () => {
+ const filteredItems = useAutocompleteFilteredItems()
+
+ return (
+
+ {filteredItems.length}
+ {' '}
+ matching suggestions. Selecting one only replaces the input text.
+
+ )
+}
+
+const FuzzyHighlight = ({
+ text,
+ query,
+}: {
+ text: string
+ query: string
+}) => {
+ const parts = useMemo(() => {
+ const trimmed = query.trim()
+
+ if (!trimmed)
+ return [text]
+
+ const escaped = trimmed.slice(0, 80).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ return text.split(new RegExp(`(${escaped})`, 'i'))
+ }, [query, text])
+
+ return (
+ <>
+ {parts.map((part, index) => (
+ part.toLowerCase() === query.trim().toLowerCase()
+ ? {part}
+ : part
+ ))}
+ >
+ )
+}
+
+const FuzzyMatchingDemo = () => {
+ const [value, setValue] = useState('retr')
+ const { contains } = useAutocompleteFilter({ sensitivity: 'base' })
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {(item: Suggestion, index: number) => (
+
+ {item.icon && }
+
+
+
+
+
{item.description}
+
+
+ )}
+
+ No workflow suggestion. Keep typing freely.
+
+
+
+ )
+}
+
+const meta = {
+ title: 'Base/UI/Autocomplete',
+ component: Autocomplete,
+ parameters: {
+ layout: 'centered',
+ docs: {
+ description: {
+ component: 'Compound autocomplete built on Base UI Autocomplete. Use it for free-form inputs where suggestions can replace or complete the typed text, but selection is not persistent state.',
+ },
+ },
+ },
+ tags: ['autodocs'],
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const SearchTags: Story = {
+ render: () => (
+
+
+
+ ),
+}
+
+export const Sizes: Story = {
+ render: () => (
+
+ {(['small', 'medium', 'large'] as const).map(size => (
+
+
+
+ ))}
+
+ ),
+}
+
+export const InlineAutocomplete: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+ {(item: Suggestion, index: number) => (
+
+ )}
+
+ No inline completion. Continue typing freely.
+
+
+
+ ),
+}
+
+export const GroupedSuggestions: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+ No suggestion. Use the text as entered.
+
+
+
+ ),
+}
+
+export const FuzzyMatching: Story = {
+ render: () => ,
+}
+
+export const LimitResults: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(item: Suggestion, index: number) => (
+
+ )}
+
+ No suggestion. Submit the typed text instead.
+
+
+
+ ),
+}
+
+export const CommandPalette: Story = {
+ render: () => (
+
+ ),
+}
+
+const VirtualizedLongSuggestionsDemo = () => {
+ const virtualizerRef = useRef(null)
+
+ return (
+
+
{
+ scrollHighlightedVirtualItem(item, details, virtualizerRef.current)
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ No suggestion. Free-form text is still valid.
+
+
+
+ )
+}
+
+export const VirtualizedLongSuggestions: Story = {
+ render: () => ,
+}
+
+export const AsyncSearch: Story = {
+ render: () => ,
+}
+
+export const Empty: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+ {(item: Suggestion, index: number) => (
+
+ )}
+
+ No tag suggestion. The custom text remains valid.
+
+
+
+ ),
+}
+
+export const DisabledAndReadOnly: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+ {(item: Suggestion, index: number) => (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {(item: Suggestion, index: number) => (
+
+ )}
+
+
+
+
+ ),
+}
diff --git a/packages/dify-ui/src/autocomplete/index.tsx b/packages/dify-ui/src/autocomplete/index.tsx
new file mode 100644
index 0000000000..16c4b19673
--- /dev/null
+++ b/packages/dify-ui/src/autocomplete/index.tsx
@@ -0,0 +1,381 @@
+'use client'
+
+import type { VariantProps } from 'class-variance-authority'
+import type { HTMLAttributes, ReactNode } from 'react'
+import type { Placement } from '../placement'
+import { Autocomplete as BaseAutocomplete } from '@base-ui/react/autocomplete'
+import { cva } from 'class-variance-authority'
+import { cn } from '../cn'
+import {
+ overlayIndicatorClassName,
+ overlayLabelClassName,
+ overlayPopupAnimationClassName,
+ overlaySeparatorClassName,
+} from '../overlay-shared'
+import { parsePlacement } from '../placement'
+
+export type { Placement }
+
+export const Autocomplete = BaseAutocomplete.Root
+export const AutocompleteValue = BaseAutocomplete.Value
+export const AutocompleteGroup = BaseAutocomplete.Group
+export const AutocompleteCollection = BaseAutocomplete.Collection
+export const AutocompleteRow = BaseAutocomplete.Row
+export const useAutocompleteFilter = BaseAutocomplete.useFilter
+export const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems
+
+export type AutocompleteRootProps = BaseAutocomplete.Root.Props
+export type AutocompleteRootChangeEventDetails = BaseAutocomplete.Root.ChangeEventDetails
+export type AutocompleteRootHighlightEventDetails = BaseAutocomplete.Root.HighlightEventDetails
+
+const autocompletePopupClassName = [
+ 'w-(--anchor-width) max-w-[min(28rem,var(--available-width))] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg outline-hidden',
+ 'data-side-top:origin-bottom data-side-bottom:origin-top data-side-left:origin-right data-side-right:origin-left',
+]
+
+const autocompleteListClassName = [
+ 'max-h-[min(20rem,var(--available-height))] overflow-y-auto overflow-x-hidden overscroll-contain p-1 outline-hidden scroll-py-1',
+ 'data-empty:max-h-none data-empty:p-0',
+]
+
+const autocompleteItemClassName = [
+ 'mx-1 flex min-h-8 cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 text-text-secondary outline-hidden transition-colors',
+ 'hover:bg-state-base-hover-alt hover:text-text-primary',
+ 'data-highlighted:bg-state-base-hover data-highlighted:text-text-primary',
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-30 data-disabled:hover:bg-transparent data-disabled:hover:text-text-secondary',
+ 'motion-reduce:transition-none',
+]
+
+const autocompleteInputGroupVariants = cva(
+ [
+ 'group/autocomplete flex w-full min-w-0 items-center border border-transparent bg-components-input-bg-normal text-components-input-text-filled shadow-none outline-hidden transition-[background-color,border-color,box-shadow]',
+ 'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
+ 'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
+ 'data-focused:border-components-input-border-active data-focused:bg-components-input-bg-active data-focused:shadow-xs',
+ 'data-disabled:cursor-not-allowed data-disabled:border-transparent data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled',
+ 'data-disabled:hover:border-transparent data-disabled:hover:bg-components-input-bg-disabled',
+ 'data-readonly:shadow-none data-readonly:hover:border-transparent data-readonly:hover:bg-components-input-bg-normal',
+ 'motion-reduce:transition-none',
+ ],
+ {
+ variants: {
+ size: {
+ small: 'h-6 rounded-md',
+ medium: 'h-8 rounded-lg',
+ large: 'h-9 rounded-[10px]',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ },
+ },
+)
+
+export type AutocompleteSize = NonNullable['size']>
+
+export type AutocompleteInputGroupProps
+ = BaseAutocomplete.InputGroup.Props
+ & VariantProps
+
+export function AutocompleteInputGroup({
+ className,
+ size = 'medium',
+ ...props
+}: AutocompleteInputGroupProps) {
+ return (
+
+ )
+}
+
+const autocompleteInputVariants = cva(
+ [
+ 'w-0 min-w-0 flex-1 appearance-none border-0 bg-transparent text-components-input-text-filled caret-primary-600 outline-hidden',
+ 'placeholder:text-components-input-text-placeholder',
+ 'disabled:cursor-not-allowed disabled:text-components-input-text-filled-disabled disabled:placeholder:text-components-input-text-disabled',
+ 'data-readonly:cursor-default',
+ ],
+ {
+ variants: {
+ size: {
+ small: 'px-2 py-1 system-xs-regular',
+ medium: 'px-3 py-[7px] system-sm-regular',
+ large: 'px-4 py-2 system-md-regular',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ },
+ },
+)
+
+export type AutocompleteInputProps
+ = Omit
+ & VariantProps
+
+export function AutocompleteInput({
+ className,
+ size = 'medium',
+ type = 'text',
+ autoComplete = 'off',
+ ...props
+}: AutocompleteInputProps) {
+ return (
+
+ )
+}
+
+const autocompleteControlVariants = cva(
+ [
+ 'flex shrink-0 touch-manipulation items-center justify-center rounded-md text-text-tertiary outline-hidden transition-colors',
+ 'hover:bg-components-input-bg-hover hover:text-text-secondary focus-visible:bg-components-input-bg-hover focus-visible:text-text-secondary',
+ 'focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
+ 'disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-text-tertiary disabled:focus-visible:bg-transparent disabled:focus-visible:ring-0',
+ 'group-data-disabled/autocomplete:cursor-not-allowed group-data-disabled/autocomplete:hover:bg-transparent group-data-disabled/autocomplete:focus-visible:bg-transparent group-data-disabled/autocomplete:focus-visible:ring-0',
+ 'group-data-readonly/autocomplete:hidden',
+ 'motion-reduce:transition-none',
+ ],
+ {
+ variants: {
+ size: {
+ small: 'mr-1 size-4',
+ medium: 'mr-1.5 size-5',
+ large: 'mr-2 size-5',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ },
+ },
+)
+
+export type AutocompleteControlProps
+ = Omit
+ & VariantProps
+ & { className?: string }
+
+export function AutocompleteTrigger({
+ className,
+ children,
+ size = 'medium',
+ type = 'button',
+ ...props
+}: AutocompleteControlProps) {
+ return (
+
+ {children ?? }
+
+ )
+}
+
+export type AutocompleteClearProps
+ = Omit
+ & VariantProps
+ & { className?: string }
+
+export function AutocompleteClear({
+ className,
+ children,
+ size = 'medium',
+ type = 'button',
+ ...props
+}: AutocompleteClearProps) {
+ return (
+
+ {children ?? }
+
+ )
+}
+
+export function AutocompleteIcon({
+ className,
+ children,
+ ...props
+}: BaseAutocomplete.Icon.Props) {
+ return (
+
+ {children ?? }
+
+ )
+}
+
+type AutocompleteContentProps = {
+ children: ReactNode
+ placement?: Placement
+ sideOffset?: number
+ alignOffset?: number
+ className?: string
+ popupClassName?: string
+ portalProps?: Omit
+ positionerProps?: Omit<
+ BaseAutocomplete.Positioner.Props,
+ 'children' | 'className' | 'side' | 'align' | 'sideOffset' | 'alignOffset'
+ >
+ popupProps?: Omit<
+ BaseAutocomplete.Popup.Props,
+ 'children' | 'className'
+ >
+}
+
+export function AutocompleteContent({
+ children,
+ placement = 'bottom-start',
+ sideOffset = 4,
+ alignOffset = 0,
+ className,
+ popupClassName,
+ portalProps,
+ positionerProps,
+ popupProps,
+}: AutocompleteContentProps) {
+ const { side, align } = parsePlacement(placement)
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export function AutocompleteList({
+ className,
+ ...props
+}: BaseAutocomplete.List.Props) {
+ return (
+
+ )
+}
+
+export function AutocompleteItem({
+ className,
+ ...props
+}: BaseAutocomplete.Item.Props) {
+ return (
+
+ )
+}
+
+export type AutocompleteItemTextProps = HTMLAttributes
+
+export function AutocompleteItemText({
+ className,
+ ...props
+}: AutocompleteItemTextProps) {
+ return (
+
+ )
+}
+
+export function AutocompleteLabel({
+ className,
+ ...props
+}: BaseAutocomplete.GroupLabel.Props) {
+ return (
+
+ )
+}
+
+export function AutocompleteSeparator({
+ className,
+ ...props
+}: BaseAutocomplete.Separator.Props) {
+ return (
+
+ )
+}
+
+export function AutocompleteEmpty({
+ className,
+ ...props
+}: BaseAutocomplete.Empty.Props) {
+ return (
+
+ )
+}
+
+export function AutocompleteStatus({
+ className,
+ ...props
+}: BaseAutocomplete.Status.Props) {
+ return (
+
+ )
+}
+
+export function AutocompleteItemIndicator({
+ className,
+ children,
+ ...props
+}: HTMLAttributes) {
+ return (
+
+ {children ?? }
+
+ )
+}
diff --git a/packages/dify-ui/src/combobox/__tests__/index.spec.tsx b/packages/dify-ui/src/combobox/__tests__/index.spec.tsx
new file mode 100644
index 0000000000..705ebe9601
--- /dev/null
+++ b/packages/dify-ui/src/combobox/__tests__/index.spec.tsx
@@ -0,0 +1,363 @@
+import type { ReactNode } from 'react'
+import { render } from 'vitest-browser-react'
+import {
+ Combobox,
+ ComboboxChip,
+ ComboboxChipRemove,
+ ComboboxChips,
+ ComboboxClear,
+ ComboboxContent,
+ ComboboxEmpty,
+ ComboboxGroup,
+ ComboboxGroupLabel,
+ ComboboxInput,
+ ComboboxInputGroup,
+ ComboboxInputTrigger,
+ ComboboxItem,
+ ComboboxItemIndicator,
+ ComboboxItemText,
+ ComboboxLabel,
+ ComboboxList,
+ ComboboxSeparator,
+ ComboboxStatus,
+ ComboboxTrigger,
+ ComboboxValue,
+} from '../index'
+
+const renderWithSafeViewport = (ui: ReactNode) => render(
+
+ {ui}
+
,
+)
+
+const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
+
+const renderSelectLikeCombobox = ({
+ children,
+ open = false,
+}: {
+ children?: ReactNode
+ open?: boolean
+} = {}) => renderWithSafeViewport(
+
+ {children ?? (
+ <>
+ Resource type
+
+
+
+
+ 2 options
+
+
+ Workflow
+
+
+
+ Dataset
+
+
+ No options
+
+ >
+ )}
+ ,
+)
+
+const renderInputCombobox = ({
+ children,
+ open = false,
+}: {
+ children?: ReactNode
+ open?: boolean
+} = {}) => renderWithSafeViewport(
+
+ {children ?? (
+ <>
+
+
+
+
+
+
+
+
+ Workflow
+
+
+
+
+ >
+ )}
+ ,
+)
+
+describe('Combobox wrappers', () => {
+ describe('Select-like trigger', () => {
+ it('should render label and apply medium trigger classes by default', async () => {
+ const screen = await renderSelectLikeCombobox()
+
+ await expect.element(screen.getByText('Resource type')).toHaveClass('system-sm-medium')
+ await expect.element(screen.getByRole('combobox', { name: 'Resource type' })).toHaveClass('rounded-lg')
+ await expect.element(screen.getByRole('combobox', { name: 'Resource type' })).toHaveClass('system-sm-regular')
+ })
+
+ it('should apply small and large trigger size variants', async () => {
+ const smallScreen = await renderSelectLikeCombobox({
+ children: (
+
+
+
+ ),
+ })
+
+ await expect.element(smallScreen.getByRole('combobox', { name: 'Small resource type' })).toHaveClass('rounded-md')
+ await expect.element(smallScreen.getByRole('combobox', { name: 'Small resource type' })).toHaveClass('system-xs-regular')
+
+ const largeScreen = await renderSelectLikeCombobox({
+ children: (
+
+
+
+ ),
+ })
+
+ await expect.element(largeScreen.getByRole('combobox', { name: 'Large resource type' })).toHaveClass('rounded-[10px]')
+ await expect.element(largeScreen.getByRole('combobox', { name: 'Large resource type' })).toHaveClass('system-md-regular')
+ })
+
+ it('should render default trigger icon and support hiding it', async () => {
+ const withIcon = await renderSelectLikeCombobox()
+
+ expect(withIcon.getByTestId('trigger').element().querySelector('.i-ri-arrow-down-s-line')).toHaveAttribute('aria-hidden', 'true')
+
+ const withoutIcon = await renderSelectLikeCombobox({
+ children: (
+
+
+
+ ),
+ })
+
+ expect(withoutIcon.getByRole('combobox', { name: 'Resource type without icon' }).element().querySelector('.i-ri-arrow-down-s-line')).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Input group and controls', () => {
+ it('should apply medium input group and input classes by default', async () => {
+ const screen = await renderInputCombobox()
+
+ await expect.element(screen.getByTestId('input-group')).toHaveClass('rounded-lg')
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('px-3')
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('system-sm-regular')
+ })
+
+ it('should apply large input group and input classes when large size is provided', async () => {
+ const screen = await renderInputCombobox({
+ children: (
+
+
+
+ ),
+ })
+
+ await expect.element(screen.getByTestId('input-group')).toHaveClass('rounded-[10px]')
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('px-4')
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('system-md-regular')
+ })
+
+ it('should set input defaults and forward passthrough props', async () => {
+ const screen = await renderInputCombobox({
+ children: (
+
+
+
+ ),
+ })
+
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveAttribute('autocomplete', 'off')
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveAttribute('type', 'text')
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveAttribute('placeholder', 'Find a resource')
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toBeRequired()
+ await expect.element(screen.getByRole('combobox', { name: 'Search resources' })).toHaveClass('custom-input')
+ })
+
+ it('should provide fallback aria labels and decorative icons for input controls', async () => {
+ const screen = await renderInputCombobox()
+
+ await expect.element(screen.getByRole('button', { name: 'Clear combobox' })).toHaveAttribute('type', 'button')
+ await expect.element(screen.getByRole('button', { name: 'Open combobox options' })).toHaveAttribute('type', 'button')
+ expect(screen.getByRole('button', { name: 'Clear combobox' }).element().querySelector('.i-ri-close-line')).toHaveAttribute('aria-hidden', 'true')
+ expect(screen.getByRole('button', { name: 'Open combobox options' }).element().querySelector('.i-ri-arrow-down-s-line')).toHaveAttribute('aria-hidden', 'true')
+ })
+
+ it('should rely on aria-labelledby when provided instead of injecting fallback labels', async () => {
+ const screen = await renderInputCombobox({
+ children: (
+ <>
+ Clear from label
+ Trigger from label
+
+
+
+
+
+ >
+ ),
+ })
+
+ await expect.element(screen.getByRole('button', { name: 'Clear from label' })).not.toHaveAttribute('aria-label')
+ await expect.element(screen.getByRole('button', { name: 'Trigger from label' })).not.toHaveAttribute('aria-label')
+ })
+ })
+
+ describe('Content and options', () => {
+ it('should use default overlay placement and Dify popup classes', async () => {
+ const screen = await renderSelectLikeCombobox({ open: true })
+
+ await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveAttribute('data-side', 'bottom')
+ await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveAttribute('data-align', 'start')
+ await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveClass('z-1002')
+ await expect.element(screen.getByRole('dialog', { name: 'combobox popup' })).toHaveClass('rounded-xl')
+ await expect.element(screen.getByRole('dialog', { name: 'combobox popup' })).toHaveClass('w-(--anchor-width)')
+ await expect.element(screen.getByRole('listbox', { name: 'combobox list' })).toHaveClass('scroll-py-1')
+ })
+
+ it('should apply custom placement side and passthrough popup props', async () => {
+ const onPopupClick = vi.fn()
+ const screen = await renderWithSafeViewport(
+
+
+
+
+
+
+
+ Workflow
+
+
+
+ ,
+ )
+
+ asHTMLElement(screen.getByRole('dialog', { name: 'combobox popup' }).element()).click()
+
+ await expect.element(screen.getByRole('group', { name: 'combobox positioner' })).toHaveAttribute('data-side', 'top')
+ expect(onPopupClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('should render item text indicator status and empty wrappers with design classes', async () => {
+ const screen = await renderSelectLikeCombobox({ open: true })
+
+ await expect.element(screen.getByTestId('list').getByText('Workflow')).toHaveClass('system-sm-medium')
+ await expect.element(screen.getByTestId('status')).toHaveClass('text-text-tertiary')
+ await expect.element(screen.getByTestId('empty')).toHaveClass('system-sm-regular')
+ expect(screen.getByTestId('list').getByText('Workflow').element().parentElement?.querySelector('.i-ri-check-line')).toHaveAttribute('aria-hidden', 'true')
+ })
+
+ it('should forward custom classes to group label separator item text and indicator', async () => {
+ const screen = await renderWithSafeViewport(
+
+
+
+
+
+
+
+ Resources
+
+
+ Workflow
+
+
+
+
+
+ ,
+ )
+
+ await expect.element(screen.getByText('Resources')).toHaveClass('custom-label')
+ await expect.element(screen.getByTestId('separator')).toHaveClass('custom-separator')
+ await expect.element(screen.getByRole('option', { name: 'Workflow' })).toHaveClass('custom-item')
+ await expect.element(screen.getByTestId('custom-list').getByText('Workflow')).toHaveClass('custom-text')
+ await expect.element(screen.getByTestId('indicator')).toHaveClass('custom-indicator')
+ })
+ })
+
+ describe('Multiple selection chips', () => {
+ it('should render chip wrappers and default remove button label', async () => {
+ const screen = await renderWithSafeViewport(
+
+
+
+ {(selectedValue: string[]) => (
+
+ {selectedValue.map(item => (
+
+ {item}
+
+
+ ))}
+
+ )}
+
+
+
+ ,
+ )
+
+ await expect.element(screen.getByTestId('chips')).toHaveClass('custom-chips')
+ await expect.element(screen.getByText('maya').element().parentElement!).toHaveClass('custom-chip')
+ await expect.element(screen.getByRole('button', { name: 'Remove selected item' })).toHaveAttribute('type', 'button')
+ expect(screen.getByTestId('remove-chip').element().querySelector('.i-ri-close-line')).toHaveAttribute('aria-hidden', 'true')
+ })
+
+ it('should preserve chip remove aria-labelledby over fallback label', async () => {
+ const screen = await renderWithSafeViewport(
+
+
+
+ {(selectedValue: string[]) => (
+
+ {selectedValue.map(item => (
+
+ Remove Maya
+
+
+ ))}
+
+ )}
+
+
+
+ ,
+ )
+
+ await expect.element(screen.getByRole('button', { name: 'Remove Maya' })).not.toHaveAttribute('aria-label')
+ })
+ })
+})
diff --git a/packages/dify-ui/src/combobox/index.stories.tsx b/packages/dify-ui/src/combobox/index.stories.tsx
new file mode 100644
index 0000000000..f2b5f4d4c6
--- /dev/null
+++ b/packages/dify-ui/src/combobox/index.stories.tsx
@@ -0,0 +1,618 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import type { Virtualizer } from '@tanstack/react-virtual'
+import type { RefObject } from 'react'
+import { useVirtualizer } from '@tanstack/react-virtual'
+import { useEffect, useRef, useState } from 'react'
+import {
+ Combobox,
+ ComboboxChip,
+ ComboboxChipRemove,
+ ComboboxChips,
+ ComboboxClear,
+ ComboboxCollection,
+ ComboboxContent,
+ ComboboxEmpty,
+ ComboboxGroup,
+ ComboboxGroupLabel,
+ ComboboxInput,
+ ComboboxInputGroup,
+ ComboboxInputTrigger,
+ ComboboxItem,
+ ComboboxItemIndicator,
+ ComboboxItemText,
+ ComboboxLabel,
+ ComboboxList,
+ ComboboxSeparator,
+ ComboboxStatus,
+ ComboboxTrigger,
+ ComboboxValue,
+ useComboboxFilteredItems,
+} from '.'
+import { cn } from '../cn'
+
+type Option = {
+ value: string
+ label: string
+ meta?: string
+ icon?: string
+ disabled?: boolean
+}
+
+type OptionGroup = {
+ label: string
+ items: Option[]
+}
+
+const fieldWidth = 'w-80'
+const wideFieldWidth = 'w-[520px]'
+const nativeFieldLabelClassName = 'mb-1 block text-text-secondary system-sm-medium'
+
+type StoryVirtualizer = Virtualizer
+
+const scrollHighlightedVirtualItem = (
+ item: unknown,
+ {
+ reason,
+ index,
+ }: {
+ reason: 'keyboard' | 'pointer' | 'none'
+ index: number
+ },
+ virtualizer: StoryVirtualizer | null,
+) => {
+ if (!item || !virtualizer)
+ return
+
+ const isStart = index === 0
+ const isEnd = index === virtualizer.options.count - 1
+ const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd))
+
+ if (shouldScroll) {
+ queueMicrotask(() => {
+ virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' })
+ })
+ }
+}
+
+const providerOptions: Option[] = [
+ { value: 'openai', label: 'OpenAI', meta: 'GPT-5, GPT-4.1', icon: 'i-ri-openai-fill' },
+ { value: 'anthropic', label: 'Anthropic', meta: 'Claude Opus, Sonnet', icon: 'i-ri-sparkling-2-line' },
+ { value: 'google', label: 'Google', meta: 'Gemini 2.5', icon: 'i-ri-google-fill' },
+ { value: 'azure-openai', label: 'Azure OpenAI', meta: 'Enterprise workspace', icon: 'i-ri-microsoft-fill' },
+ { value: 'localai', label: 'LocalAI', meta: 'Self-hosted endpoint', icon: 'i-ri-server-line', disabled: true },
+]
+
+const dataSourceOptions: Option[] = [
+ { value: 'knowledge-base', label: 'Knowledge Base', meta: 'Vector index', icon: 'i-ri-database-2-line' },
+ { value: 'notion', label: 'Notion', meta: 'Synced pages', icon: 'i-ri-notion-fill' },
+ { value: 'website', label: 'Website crawler', meta: 'Public URLs', icon: 'i-ri-global-line' },
+ { value: 's3', label: 'S3 bucket', meta: 'Private files', icon: 'i-ri-cloud-line' },
+ { value: 'slack', label: 'Slack', meta: 'Channel history', icon: 'i-ri-slack-fill' },
+]
+
+const reviewerOptions: Option[] = [
+ { value: 'maya', label: 'Maya Chen', meta: 'Product owner' },
+ { value: 'liam', label: 'Liam Brooks', meta: 'Prompt engineer' },
+ { value: 'nora', label: 'Nora Park', meta: 'Data steward' },
+ { value: 'owen', label: 'Owen Reed', meta: 'Security reviewer' },
+ { value: 'yuki', label: 'Yuki Tanaka', meta: 'ML engineer' },
+]
+
+const toolGroups: OptionGroup[] = [
+ {
+ label: 'Retrieval',
+ items: [
+ { value: 'dataset-search', label: 'Dataset search', meta: 'Search workspace knowledge', icon: 'i-ri-search-eye-line' },
+ { value: 'web-scraper', label: 'Web scraper', meta: 'Fetch public pages', icon: 'i-ri-global-line' },
+ ],
+ },
+ {
+ label: 'Actions',
+ items: [
+ { value: 'http-request', label: 'HTTP request', meta: 'Call external APIs', icon: 'i-ri-terminal-box-line' },
+ { value: 'code-runner', label: 'Code runner', meta: 'Execute sandboxed scripts', icon: 'i-ri-code-s-slash-line' },
+ ],
+ },
+ {
+ label: 'Operations',
+ items: [
+ { value: 'human-review', label: 'Human review', meta: 'Assign approval task', icon: 'i-ri-user-voice-line' },
+ { value: 'audit-log', label: 'Audit log', meta: 'Record workflow events', icon: 'i-ri-file-list-3-line' },
+ ],
+ },
+]
+
+const tagOptions: Option[] = [
+ { value: 'rag', label: 'RAG' },
+ { value: 'agent', label: 'Agent' },
+ { value: 'production', label: 'Production' },
+ { value: 'evaluation', label: 'Evaluation' },
+ { value: 'finance', label: 'Finance' },
+ { value: 'support', label: 'Support' },
+]
+
+const directoryOptions: Option[] = [
+ { value: 'maya-chen', label: 'Maya Chen', meta: 'Product owner · maya@example.com', icon: 'i-ri-user-3-line' },
+ { value: 'liam-brooks', label: 'Liam Brooks', meta: 'Prompt engineer · liam@example.com', icon: 'i-ri-user-3-line' },
+ { value: 'nora-park', label: 'Nora Park', meta: 'Data steward · nora@example.com', icon: 'i-ri-user-3-line' },
+ { value: 'owen-reed', label: 'Owen Reed', meta: 'Security reviewer · owen@example.com', icon: 'i-ri-shield-user-line' },
+ { value: 'yuki-tanaka', label: 'Yuki Tanaka', meta: 'ML engineer · yuki@example.com', icon: 'i-ri-user-3-line' },
+ { value: 'ava-martin', label: 'Ava Martin', meta: 'Support lead · ava@example.com', icon: 'i-ri-customer-service-2-line' },
+]
+
+const emptyOptions: Option[] = [
+ { value: 'billing', label: 'Billing connector' },
+ { value: 'zendesk', label: 'Zendesk' },
+ { value: 'github', label: 'GitHub issues' },
+]
+
+const modelCatalogOptions: Option[] = Array.from({ length: 1000 }, (_, index) => {
+ const provider = ['OpenAI', 'Anthropic', 'Google', 'Mistral', 'DeepSeek'][index % 5]!
+ const family = ['chat', 'reasoning', 'vision', 'embedding'][index % 4]!
+ const number = new Intl.NumberFormat('en-US', {
+ minimumIntegerDigits: 4,
+ }).format(index + 1)
+
+ return {
+ value: `model-${index + 1}`,
+ label: `${provider} ${family} ${number}`,
+ meta: `${provider} provider · ${family}`,
+ icon: family === 'embedding'
+ ? 'i-ri-vector-triangle'
+ : family === 'vision'
+ ? 'i-ri-image-circle-line'
+ : family === 'reasoning'
+ ? 'i-ri-brain-line'
+ : 'i-ri-chat-1-line',
+ }
+})
+
+const sizeOptions: Option[] = providerOptions.slice(0, 3)
+const defaultProvider = providerOptions[0]!
+const disabledProvider = providerOptions[1]!
+const defaultDataSource = dataSourceOptions[0]!
+const defaultPopupDataSource = dataSourceOptions[1]!
+const readOnlyDataSource = dataSourceOptions[2]!
+const defaultTool = toolGroups[0]!.items[0]!
+const defaultReviewers = [reviewerOptions[0]!, reviewerOptions[2]!]
+const defaultTag = tagOptions[2]!
+
+const renderOptionItem = (option: Option, index?: number) => (
+
+
+ {option.icon && }
+
+ {option.label}
+ {option.meta && {option.meta} }
+
+
+
+
+)
+
+const renderSimpleOptionItem = (option: Option, index?: number) => (
+
+ {option.label}
+
+
+)
+
+const PopupSearchInput = ({
+ label,
+ placeholder,
+}: {
+ label: string
+ placeholder: string
+}) => (
+
+
+
+
+
+)
+
+const GroupedToolList = () => {
+ const groups = useComboboxFilteredItems()
+
+ return (
+
+ {groups.map((group, groupIndex) => (
+
+ {groupIndex > 0 && }
+ {group.label}
+
+ {(option: Option) => renderOptionItem(option)}
+
+
+ ))}
+
+ )
+}
+
+const VirtualizedModelList = ({
+ virtualizerRef,
+}: {
+ virtualizerRef: RefObject
+}) => {
+ const scrollRef = useRef(null)
+ const filteredItems = useComboboxFilteredItems()
+ const virtualizer = useVirtualizer({
+ count: filteredItems.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 42,
+ overscan: 6,
+ })
+
+ useEffect(() => {
+ virtualizerRef.current = virtualizer
+
+ return () => {
+ virtualizerRef.current = null
+ }
+ }, [virtualizer, virtualizerRef])
+
+ return (
+
+
+ {virtualizer.getVirtualItems().map((virtualItem) => {
+ const option = filteredItems[virtualItem.index]
+
+ if (!option)
+ return null
+
+ return (
+
+ {renderOptionItem(option, virtualItem.index)}
+
+ )
+ })}
+
+
+ )
+}
+
+const FilteredModelStatus = () => {
+ const filteredItems = useComboboxFilteredItems ()
+
+ return (
+
+ {filteredItems.length}
+ {' '}
+ matching models
+
+ )
+}
+
+const VirtualizedLongListDemo = () => {
+ const [value, setValue] = useState (modelCatalogOptions[137]!)
+ const virtualizerRef = useRef(null)
+
+ return (
+
+
{
+ scrollHighlightedVirtualItem(item, details, virtualizerRef.current)
+ }}
+ >
+ Model catalog
+
+
+
+
+
+
+
+ No model matches this filter
+
+
+
+ )
+}
+
+const AsyncDirectoryDemo = () => {
+ const [inputValue, setInputValue] = useState('ma')
+ const [value, setValue] = useState(null)
+ const [items, setItems] = useState(directoryOptions.slice(0, 3))
+ const [loading, setLoading] = useState(false)
+
+ useEffect(() => {
+ setLoading(true)
+ const timeout = window.setTimeout(() => {
+ const query = inputValue.trim().toLowerCase()
+ setItems(
+ query
+ ? directoryOptions.filter(option => `${option.label} ${option.meta}`.toLowerCase().includes(query))
+ : directoryOptions.slice(0, 5),
+ )
+ setLoading(false)
+ }, 450)
+
+ return () => window.clearTimeout(timeout)
+ }, [inputValue])
+
+ return (
+
+ item.value === value.value) ? [value, ...items] : items}
+ value={value}
+ onValueChange={setValue}
+ inputValue={inputValue}
+ onInputValueChange={setInputValue}
+ autoHighlight
+ >
+
+ Owner
+
+
+
+
+
+
+
+
+
+ {loading ? 'Loading directory matches…' : `${items.length} selectable owners`}
+
+ {renderOptionItem}
+ No owner matches this query
+
+
+
+ )
+}
+
+const meta = {
+ title: 'Base/UI/Combobox',
+ component: Combobox,
+ parameters: {
+ layout: 'centered',
+ docs: {
+ description: {
+ component: 'Compound combobox built on Base UI Combobox for searchable predefined selections. Compose triggers, inputs, lists, groups, status, empty states, and chips without importing Base UI primitives directly.',
+ },
+ },
+ },
+ tags: ['autodocs'],
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const SelectLikeDefault: Story = {
+ render: () => (
+
+
+ Model provider
+
+
+
+
+
+ {renderOptionItem}
+
+
+
+ ),
+}
+
+export const PopupInputSearchableSelect: Story = {
+ render: () => (
+
+
+ Data source
+
+
+
+
+
+ {renderOptionItem}
+
+
+
+ ),
+}
+
+export const AsyncSearchSingle: Story = {
+ render: () => ,
+}
+
+export const InputGroupSearchable: Story = {
+ render: () => (
+
+
+
+ Connect source
+
+
+
+
+
+
+
+
+ {renderOptionItem}
+
+
+
+ ),
+}
+
+export const Sizes: Story = {
+ render: () => (
+
+ {(['small', 'medium', 'large'] as const).map(size => (
+
+
+
+
+
+
+ {renderOptionItem}
+
+
+ ))}
+
+ ),
+}
+
+export const Grouped: Story = {
+ render: () => (
+
+
+ Workflow tool
+
+
+
+
+
+
+
+
+
+ ),
+}
+
+const MultipleChipsDemo = () => {
+ const [value, setValue] = useState(defaultReviewers)
+
+ return (
+
+
+
+ Reviewers
+
+
+ {(selectedValue: Option[]) => (
+ <>
+
+ {selectedValue.map(item => (
+
+ {item.label}
+
+
+ ))}
+
+
+ >
+ )}
+
+
+
+
+
+
+ {renderOptionItem}
+
+
+
+ )
+}
+
+export const MultipleChips: Story = {
+ render: () => ,
+}
+
+export const VirtualizedLongList: Story = {
+ render: () => ,
+}
+
+export const EmptyAndStatus: Story = {
+ render: () => (
+
+
+
+ Connector
+
+
+
+
+
+
+
+
+ Search workspace connectors
+ No connectors found
+ {renderSimpleOptionItem}
+
+
+
+ ),
+}
+
+export const DisabledAndReadOnly: Story = {
+ render: () => (
+
+
+ Disabled provider
+
+
+
+
+
+ {renderOptionItem}
+
+
+
+
+ Read-only source
+
+
+
+
+
+
+
+ {renderOptionItem}
+
+
+
+ ),
+}
+
+const ControlledDemo = () => {
+ const [value, setValue] = useState (defaultTag)
+
+ return (
+
+
+ Default app tag
+
+
+
+
+
+ {renderSimpleOptionItem}
+
+
+
+ Selected:
+ {' '}
+ {value?.label ?? 'None'}
+
+
+ )
+}
+
+export const Controlled: Story = {
+ render: () => ,
+}
diff --git a/packages/dify-ui/src/combobox/index.tsx b/packages/dify-ui/src/combobox/index.tsx
new file mode 100644
index 0000000000..c4f03241f6
--- /dev/null
+++ b/packages/dify-ui/src/combobox/index.tsx
@@ -0,0 +1,497 @@
+'use client'
+
+import type { VariantProps } from 'class-variance-authority'
+import type { HTMLAttributes, ReactNode } from 'react'
+import type { Placement } from '../placement'
+import { Combobox as BaseCombobox } from '@base-ui/react/combobox'
+import { cva } from 'class-variance-authority'
+import { cn } from '../cn'
+import {
+ overlayIndicatorClassName,
+ overlayLabelClassName,
+ overlayPopupAnimationClassName,
+ overlaySeparatorClassName,
+} from '../overlay-shared'
+import { parsePlacement } from '../placement'
+
+export type { Placement }
+
+export const Combobox = BaseCombobox.Root
+export const ComboboxValue = BaseCombobox.Value
+export const ComboboxGroup = BaseCombobox.Group
+export const ComboboxCollection = BaseCombobox.Collection
+export const ComboboxRow = BaseCombobox.Row
+export const useComboboxFilter = BaseCombobox.useFilter
+export const useComboboxFilteredItems = BaseCombobox.useFilteredItems
+
+export type ComboboxRootProps
+ = BaseCombobox.Root.Props
+export type ComboboxRootChangeEventDetails = BaseCombobox.Root.ChangeEventDetails
+export type ComboboxRootHighlightEventDetails = BaseCombobox.Root.HighlightEventDetails
+
+const comboboxPopupClassName = [
+ 'w-(--anchor-width) max-w-[min(28rem,var(--available-width))] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg outline-hidden',
+ 'data-side-top:origin-bottom data-side-bottom:origin-top data-side-left:origin-right data-side-right:origin-left',
+]
+
+const comboboxListClassName = [
+ 'max-h-[min(20rem,var(--available-height))] overflow-y-auto overflow-x-hidden overscroll-contain p-1 outline-hidden scroll-py-1',
+ 'data-empty:max-h-none data-empty:p-0',
+]
+
+const comboboxItemClassName = [
+ 'mx-1 grid min-h-8 cursor-pointer select-none grid-cols-[1fr_auto] items-center gap-2 rounded-lg px-2 py-1.5 text-text-secondary outline-hidden transition-colors',
+ 'hover:bg-state-base-hover-alt hover:text-text-primary',
+ 'data-highlighted:bg-state-base-hover data-highlighted:text-text-primary',
+ 'data-selected:text-text-primary',
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-30 data-disabled:hover:bg-transparent data-disabled:hover:text-text-secondary',
+ 'motion-reduce:transition-none',
+]
+
+const comboboxTriggerVariants = cva(
+ [
+ 'group/combobox-trigger flex w-full min-w-0 items-center border-0 bg-components-input-bg-normal text-left text-components-input-text-filled outline-hidden transition-colors',
+ 'hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-open:bg-state-base-hover-alt',
+ 'focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
+ 'data-placeholder:text-components-input-text-placeholder',
+ 'data-readonly:cursor-default data-readonly:bg-transparent data-readonly:hover:bg-transparent',
+ 'data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled data-disabled:hover:bg-components-input-bg-disabled',
+ 'data-disabled:data-placeholder:text-components-input-text-disabled',
+ 'motion-reduce:transition-none',
+ ],
+ {
+ variants: {
+ size: {
+ small: 'h-6 gap-px rounded-md px-2 py-1 system-xs-regular',
+ medium: 'h-8 gap-0.5 rounded-lg px-3 py-2 system-sm-regular',
+ large: 'h-9 gap-0.5 rounded-[10px] px-4 py-2 system-md-regular',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ },
+ },
+)
+
+export type ComboboxSize = NonNullable['size']>
+
+type ComboboxTriggerProps
+ = Omit
+ & VariantProps
+ & {
+ className?: string
+ icon?: ReactNode | false
+ }
+
+export function ComboboxTrigger({
+ className,
+ children,
+ icon,
+ size,
+ type = 'button',
+ ...props
+}: ComboboxTriggerProps) {
+ return (
+
+
+ {children}
+
+ {icon !== false && (
+
+ {icon ?? }
+
+ )}
+
+ )
+}
+
+const comboboxInputGroupVariants = cva(
+ [
+ 'group/combobox flex w-full min-w-0 items-center border border-transparent bg-components-input-bg-normal text-components-input-text-filled shadow-none outline-hidden transition-[background-color,border-color,box-shadow]',
+ 'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
+ 'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
+ 'data-focused:border-components-input-border-active data-focused:bg-components-input-bg-active data-focused:shadow-xs',
+ 'data-open:border-components-input-border-active data-open:bg-components-input-bg-active',
+ 'data-disabled:cursor-not-allowed data-disabled:border-transparent data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled',
+ 'data-disabled:hover:border-transparent data-disabled:hover:bg-components-input-bg-disabled',
+ 'data-readonly:shadow-none data-readonly:hover:border-transparent data-readonly:hover:bg-components-input-bg-normal',
+ 'motion-reduce:transition-none',
+ ],
+ {
+ variants: {
+ size: {
+ small: 'min-h-6 rounded-md',
+ medium: 'min-h-8 rounded-lg',
+ large: 'min-h-9 rounded-[10px]',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ },
+ },
+)
+
+export type ComboboxInputGroupProps
+ = BaseCombobox.InputGroup.Props
+ & VariantProps
+
+export function ComboboxInputGroup({
+ className,
+ size = 'medium',
+ ...props
+}: ComboboxInputGroupProps) {
+ return (
+
+ )
+}
+
+const comboboxInputVariants = cva(
+ [
+ 'w-0 min-w-0 flex-1 appearance-none border-0 bg-transparent text-components-input-text-filled caret-primary-600 outline-hidden',
+ 'placeholder:text-components-input-text-placeholder',
+ 'disabled:cursor-not-allowed disabled:text-components-input-text-filled-disabled disabled:placeholder:text-components-input-text-disabled',
+ 'data-readonly:cursor-default',
+ ],
+ {
+ variants: {
+ size: {
+ small: 'px-2 py-1 system-xs-regular',
+ medium: 'px-3 py-[7px] system-sm-regular',
+ large: 'px-4 py-2 system-md-regular',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ },
+ },
+)
+
+export type ComboboxInputProps
+ = Omit
+ & VariantProps
+
+export function ComboboxInput({
+ className,
+ size = 'medium',
+ type = 'text',
+ autoComplete = 'off',
+ ...props
+}: ComboboxInputProps) {
+ return (
+
+ )
+}
+
+const comboboxControlVariants = cva(
+ [
+ 'flex shrink-0 touch-manipulation items-center justify-center rounded-md text-text-tertiary outline-hidden transition-colors',
+ 'hover:bg-components-input-bg-hover hover:text-text-secondary focus-visible:bg-components-input-bg-hover focus-visible:text-text-secondary',
+ 'focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
+ 'disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-text-tertiary disabled:focus-visible:bg-transparent disabled:focus-visible:ring-0',
+ 'group-data-disabled/combobox:cursor-not-allowed group-data-disabled/combobox:hover:bg-transparent group-data-disabled/combobox:focus-visible:bg-transparent group-data-disabled/combobox:focus-visible:ring-0',
+ 'group-data-readonly/combobox:hidden',
+ 'motion-reduce:transition-none',
+ ],
+ {
+ variants: {
+ size: {
+ small: 'mr-1 size-4',
+ medium: 'mr-1.5 size-5',
+ large: 'mr-2 size-5',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ },
+ },
+)
+
+export type ComboboxClearProps
+ = Omit
+ & VariantProps
+ & { className?: string }
+
+export function ComboboxClear({
+ className,
+ children,
+ size = 'medium',
+ type = 'button',
+ ...props
+}: ComboboxClearProps) {
+ return (
+
+ {children ?? }
+
+ )
+}
+
+export type ComboboxInputTriggerProps
+ = Omit
+ & VariantProps
+ & { className?: string }
+
+export function ComboboxInputTrigger({
+ className,
+ children,
+ size = 'medium',
+ type = 'button',
+ ...props
+}: ComboboxInputTriggerProps) {
+ return (
+
+ {children ?? }
+
+ )
+}
+
+export function ComboboxIcon({
+ className,
+ children,
+ ...props
+}: BaseCombobox.Icon.Props) {
+ return (
+
+ {children ?? }
+
+ )
+}
+
+type ComboboxContentProps = {
+ children: ReactNode
+ placement?: Placement
+ sideOffset?: number
+ alignOffset?: number
+ className?: string
+ popupClassName?: string
+ portalProps?: Omit
+ positionerProps?: Omit<
+ BaseCombobox.Positioner.Props,
+ 'children' | 'className' | 'side' | 'align' | 'sideOffset' | 'alignOffset'
+ >
+ popupProps?: Omit<
+ BaseCombobox.Popup.Props,
+ 'children' | 'className'
+ >
+}
+
+export function ComboboxContent({
+ children,
+ placement = 'bottom-start',
+ sideOffset = 4,
+ alignOffset = 0,
+ className,
+ popupClassName,
+ portalProps,
+ positionerProps,
+ popupProps,
+}: ComboboxContentProps) {
+ const { side, align } = parsePlacement(placement)
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export function ComboboxList({
+ className,
+ ...props
+}: BaseCombobox.List.Props) {
+ return (
+
+ )
+}
+
+export function ComboboxItem({
+ className,
+ ...props
+}: BaseCombobox.Item.Props) {
+ return (
+
+ )
+}
+
+export type ComboboxItemTextProps = HTMLAttributes
+
+export function ComboboxItemText({
+ className,
+ ...props
+}: ComboboxItemTextProps) {
+ return (
+
+ )
+}
+
+export function ComboboxItemIndicator({
+ className,
+ children,
+ ...props
+}: Omit & { children?: ReactNode }) {
+ return (
+
+ {children ?? }
+
+ )
+}
+
+export function ComboboxLabel({
+ className,
+ ...props
+}: BaseCombobox.Label.Props) {
+ return (
+
+ )
+}
+
+export function ComboboxGroupLabel({
+ className,
+ ...props
+}: BaseCombobox.GroupLabel.Props) {
+ return (
+
+ )
+}
+
+export function ComboboxSeparator({
+ className,
+ ...props
+}: BaseCombobox.Separator.Props) {
+ return (
+
+ )
+}
+
+export function ComboboxEmpty({
+ className,
+ ...props
+}: BaseCombobox.Empty.Props) {
+ return (
+
+ )
+}
+
+export function ComboboxStatus({
+ className,
+ ...props
+}: BaseCombobox.Status.Props) {
+ return (
+
+ )
+}
+
+export function ComboboxChips({
+ className,
+ ...props
+}: BaseCombobox.Chips.Props) {
+ return (
+
+ )
+}
+
+export function ComboboxChip({
+ className,
+ ...props
+}: BaseCombobox.Chip.Props) {
+ return (
+
+ )
+}
+
+export function ComboboxChipRemove({
+ className,
+ children,
+ type = 'button',
+ ...props
+}: BaseCombobox.ChipRemove.Props) {
+ return (
+
+ {children ?? }
+
+ )
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4826ce8163..32cbac2852 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,11 +7,11 @@ settings:
catalogs:
default:
'@amplitude/analytics-browser':
- specifier: 2.42.0
- version: 2.42.0
+ specifier: 2.42.1
+ version: 2.42.1
'@amplitude/plugin-session-replay-browser':
- specifier: 1.28.1
- version: 1.28.1
+ specifier: 1.29.0
+ version: 1.29.0
'@antfu/eslint-config':
specifier: 8.2.0
version: 8.2.0
@@ -40,8 +40,8 @@ catalogs:
specifier: 0.27.19
version: 0.27.19
'@formatjs/intl-localematcher':
- specifier: 0.8.4
- version: 0.8.4
+ specifier: 0.8.6
+ version: 0.8.6
'@headlessui/react':
specifier: 2.2.10
version: 2.2.10
@@ -49,11 +49,11 @@ catalogs:
specifier: 2.2.0
version: 2.2.0
'@hey-api/openapi-ts':
- specifier: 0.97.0
- version: 0.97.0
+ specifier: 0.97.1
+ version: 0.97.1
'@hono/node-server':
- specifier: 2.0.0
- version: 2.0.0
+ specifier: 2.0.1
+ version: 2.0.1
'@iconify-json/heroicons':
specifier: 1.2.3
version: 1.2.3
@@ -97,17 +97,17 @@ catalogs:
specifier: 16.2.4
version: 16.2.4
'@orpc/client':
- specifier: 1.14.0
- version: 1.14.0
+ specifier: 1.14.1
+ version: 1.14.1
'@orpc/contract':
- specifier: 1.14.0
- version: 1.14.0
+ specifier: 1.14.1
+ version: 1.14.1
'@orpc/openapi-client':
- specifier: 1.14.0
- version: 1.14.0
+ specifier: 1.14.1
+ version: 1.14.1
'@orpc/tanstack-query':
- specifier: 1.14.0
- version: 1.14.0
+ specifier: 1.14.1
+ version: 1.14.1
'@playwright/test':
specifier: 1.59.1
version: 1.59.1
@@ -118,29 +118,29 @@ catalogs:
specifier: 4.2.0
version: 4.2.0
'@sentry/react':
- specifier: 10.50.0
- version: 10.50.0
+ specifier: 10.51.0
+ version: 10.51.0
'@storybook/addon-docs':
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
'@storybook/addon-links':
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
'@storybook/addon-onboarding':
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
'@storybook/addon-themes':
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
'@storybook/nextjs-vite':
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
'@storybook/react':
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
'@storybook/react-vite':
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
'@streamdown/math':
specifier: 1.0.2
version: 1.0.2
@@ -160,8 +160,8 @@ catalogs:
specifier: 4.2.4
version: 4.2.4
'@tanstack/eslint-plugin-query':
- specifier: 5.100.6
- version: 5.100.6
+ specifier: 5.100.9
+ version: 5.100.9
'@tanstack/react-devtools':
specifier: 0.10.2
version: 0.10.2
@@ -175,11 +175,11 @@ catalogs:
specifier: 0.10.0
version: 0.10.0
'@tanstack/react-query':
- specifier: 5.100.6
- version: 5.100.6
+ specifier: 5.100.9
+ version: 5.100.9
'@tanstack/react-query-devtools':
- specifier: 5.100.6
- version: 5.100.6
+ specifier: 5.100.9
+ version: 5.100.9
'@tanstack/react-virtual':
specifier: 3.13.24
version: 3.13.24
@@ -196,14 +196,14 @@ catalogs:
specifier: 14.6.1
version: 14.6.1
'@tsslint/cli':
- specifier: 3.1.0
- version: 3.1.0
+ specifier: 3.1.1
+ version: 3.1.1
'@tsslint/compat-eslint':
- specifier: 3.1.0
- version: 3.1.0
+ specifier: 3.1.1
+ version: 3.1.1
'@tsslint/config':
- specifier: 3.1.0
- version: 3.1.0
+ specifier: 3.1.1
+ version: 3.1.1
'@types/js-cookie':
specifier: 3.0.6
version: 3.0.6
@@ -229,14 +229,14 @@ catalogs:
specifier: 1.15.9
version: 1.15.9
'@typescript-eslint/eslint-plugin':
- specifier: 8.59.1
- version: 8.59.1
+ specifier: 8.59.2
+ version: 8.59.2
'@typescript-eslint/parser':
- specifier: 8.59.1
- version: 8.59.1
+ specifier: 8.59.2
+ version: 8.59.2
'@typescript/native-preview':
- specifier: 7.0.0-dev.20260428.1
- version: 7.0.0-dev.20260428.1
+ specifier: 7.0.0-dev.20260505.1
+ version: 7.0.0-dev.20260505.1
'@vitejs/plugin-react':
specifier: 6.0.1
version: 6.0.1
@@ -289,8 +289,8 @@ catalogs:
specifier: 10.6.0
version: 10.6.0
dompurify:
- specifier: 3.4.1
- version: 3.4.1
+ specifier: 3.4.2
+ version: 3.4.2
echarts:
specifier: 6.0.0
version: 6.0.0
@@ -310,14 +310,14 @@ catalogs:
specifier: 5.6.0
version: 5.6.0
es-toolkit:
- specifier: 1.46.0
- version: 1.46.0
+ specifier: 1.46.1
+ version: 1.46.1
eslint:
- specifier: 10.2.1
- version: 10.2.1
+ specifier: 10.3.0
+ version: 10.3.0
eslint-markdown:
- specifier: 0.7.0
- version: 0.7.0
+ specifier: 0.8.0
+ version: 0.8.0
eslint-plugin-better-tailwindcss:
specifier: 4.5.0
version: 4.5.0
@@ -337,11 +337,14 @@ catalogs:
specifier: 4.0.3
version: 4.0.3
eslint-plugin-storybook:
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
fast-deep-equal:
specifier: 3.1.3
version: 3.1.3
+ fuse.js:
+ specifier: 7.2.0
+ version: 7.2.0
happy-dom:
specifier: 20.9.0
version: 20.9.0
@@ -349,8 +352,8 @@ catalogs:
specifier: 2.3.6
version: 2.3.6
hono:
- specifier: 4.12.15
- version: 4.12.15
+ specifier: 4.12.17
+ version: 4.12.17
html-entities:
specifier: 2.6.0
version: 2.6.0
@@ -367,11 +370,11 @@ catalogs:
specifier: 0.2.0
version: 0.2.0
immer:
- specifier: 11.1.4
- version: 11.1.4
+ specifier: 11.1.6
+ version: 11.1.6
jotai:
- specifier: 2.19.1
- version: 2.19.1
+ specifier: 2.20.0
+ version: 2.20.0
js-audio-recorder:
specifier: 1.0.7
version: 1.0.7
@@ -388,8 +391,8 @@ catalogs:
specifier: 0.16.45
version: 0.16.45
knip:
- specifier: 6.7.0
- version: 6.7.0
+ specifier: 6.11.0
+ version: 6.11.0
ky:
specifier: 2.0.2
version: 2.0.2
@@ -400,8 +403,8 @@ catalogs:
specifier: 0.44.0
version: 0.44.0
loro-crdt:
- specifier: 1.12.0
- version: 1.12.0
+ specifier: 1.12.1
+ version: 1.12.1
mermaid:
specifier: 11.14.0
version: 11.14.0
@@ -430,8 +433,8 @@ catalogs:
specifier: 1.59.1
version: 1.59.1
postcss:
- specifier: 8.5.12
- version: 8.5.12
+ specifier: 8.5.14
+ version: 8.5.14
qrcode.react:
specifier: 4.2.0
version: 4.2.0
@@ -451,8 +454,8 @@ catalogs:
specifier: 5.5.7
version: 5.5.7
react-hotkeys-hook:
- specifier: 5.2.4
- version: 5.2.4
+ specifier: 5.3.2
+ version: 5.3.2
react-i18next:
specifier: 16.5.8
version: 16.5.8
@@ -502,8 +505,8 @@ catalogs:
specifier: 1.0.8
version: 1.0.8
storybook:
- specifier: 10.3.5
- version: 10.3.5
+ specifier: 10.3.6
+ version: 10.3.6
streamdown:
specifier: 2.5.0
version: 2.5.0
@@ -517,8 +520,8 @@ catalogs:
specifier: 4.2.4
version: 4.2.4
tldts:
- specifier: 7.0.29
- version: 7.0.29
+ specifier: 7.0.30
+ version: 7.0.30
tsx:
specifier: 4.21.0
version: 4.21.0
@@ -538,8 +541,8 @@ catalogs:
specifier: 14.0.0
version: 14.0.0
vinext:
- specifier: 0.0.45
- version: 0.0.45
+ specifier: 0.0.47
+ version: 0.0.47
vite-plugin-inspect:
specifier: 12.0.0-beta.1
version: 12.0.0-beta.1
@@ -553,14 +556,14 @@ catalogs:
specifier: 1.1.4
version: 1.1.4
zod:
- specifier: 4.3.6
- version: 4.3.6
+ specifier: 4.4.3
+ version: 4.4.3
zundo:
specifier: 2.3.0
version: 2.3.0
zustand:
- specifier: 5.0.12
- version: 5.0.12
+ specifier: 5.0.13
+ version: 5.0.13
overrides:
'@lexical/code': npm:lexical-code-no-prism@0.41.0
@@ -597,22 +600,22 @@ importers:
devDependencies:
'@antfu/eslint-config':
specifier: 'catalog:'
- version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.4)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.59.1(typescript@6.0.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.1(jiti@2.6.1)))(eslint@10.2.1(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)
+ version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.4)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3))(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)
concurrently:
specifier: 'catalog:'
version: 9.2.1
eslint:
specifier: 'catalog:'
- version: 10.2.1(jiti@2.6.1)
+ version: 10.3.0(jiti@2.6.1)
eslint-markdown:
specifier: 'catalog:'
- version: 0.7.0(eslint@10.2.1(jiti@2.6.1))
+ version: 0.8.0(eslint@10.3.0(jiti@2.6.1))
eslint-plugin-markdown-preferences:
specifier: 'catalog:'
- version: 0.41.1(@eslint/markdown@8.0.1)(eslint@10.2.1(jiti@2.6.1))
+ version: 0.41.1(@eslint/markdown@8.0.1)(eslint@10.3.0(jiti@2.6.1))
eslint-plugin-no-barrel-files:
specifier: 'catalog:'
- version: 1.3.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ version: 1.3.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
vite:
specifier: npm:@voidzero-dev/vite-plus-core@0.1.20
version: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)'
@@ -636,7 +639,7 @@ importers:
version: 25.6.0
'@typescript/native-preview':
specifier: 'catalog:'
- version: 7.0.0-dev.20260428.1
+ version: 7.0.0-dev.20260505.1
tsx:
specifier: 'catalog:'
version: 4.21.0
@@ -654,17 +657,17 @@ importers:
dependencies:
'@orpc/contract':
specifier: 'catalog:'
- version: 1.14.0
+ version: 1.14.1
zod:
specifier: 'catalog:'
- version: 4.3.6
+ version: 4.4.3
devDependencies:
'@dify/tsconfig':
specifier: workspace:*
version: link:../tsconfig
'@hey-api/openapi-ts':
specifier: 'catalog:'
- version: 0.97.0(magicast@0.5.2)(typescript@6.0.3)
+ version: 0.97.1(magicast@0.5.2)(typescript@6.0.3)
'@types/js-yaml':
specifier: 'catalog:'
version: 4.0.9
@@ -673,10 +676,10 @@ importers:
version: 25.6.0
'@typescript/native-preview':
specifier: 'catalog:'
- version: 7.0.0-dev.20260428.1
+ version: 7.0.0-dev.20260505.1
eslint:
specifier: 'catalog:'
- version: 10.2.1(jiti@2.6.1)
+ version: 10.3.0(jiti@2.6.1)
js-yaml:
specifier: 'catalog:'
version: 4.1.1
@@ -691,13 +694,13 @@ importers:
dependencies:
'@hono/node-server':
specifier: 'catalog:'
- version: 2.0.0(hono@4.12.15)
+ version: 2.0.1(hono@4.12.17)
c12:
specifier: 'catalog:'
version: 1.10.0
hono:
specifier: 'catalog:'
- version: 4.12.15
+ version: 4.12.17
devDependencies:
'@dify/tsconfig':
specifier: workspace:*
@@ -707,7 +710,7 @@ importers:
version: 25.6.0
'@typescript/native-preview':
specifier: 'catalog:'
- version: 7.0.0-dev.20260428.1
+ version: 7.0.0-dev.20260505.1
vite:
specifier: npm:@voidzero-dev/vite-plus-core@0.1.20
version: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)'
@@ -732,7 +735,7 @@ importers:
version: 1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@chromatic-com/storybook':
specifier: 'catalog:'
- version: 5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 5.1.2(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@dify/tsconfig':
specifier: workspace:*
version: link:../tsconfig
@@ -744,19 +747,22 @@ importers:
version: 1.2.10
'@storybook/addon-docs':
specifier: 'catalog:'
- version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 10.3.6(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@storybook/addon-links':
specifier: 'catalog:'
- version: 10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 10.3.6(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@storybook/addon-themes':
specifier: 'catalog:'
- version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 10.3.6(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@storybook/react-vite':
specifier: 'catalog:'
- version: 10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)
+ version: 10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.2.4(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
+ '@tanstack/react-virtual':
+ specifier: 'catalog:'
+ version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@types/react':
specifier: 'catalog:'
version: 19.2.14
@@ -765,7 +771,7 @@ importers:
version: 19.2.3(@types/react@19.2.14)
'@typescript/native-preview':
specifier: 'catalog:'
- version: 7.0.0-dev.20260428.1
+ version: 7.0.0-dev.20260505.1
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
@@ -786,7 +792,7 @@ importers:
version: 19.2.5(react@19.2.5)
storybook:
specifier: 'catalog:'
- version: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
tailwindcss:
specifier: 'catalog:'
version: 4.2.4
@@ -816,7 +822,7 @@ importers:
dependencies:
'@typescript/native-preview':
specifier: 'catalog:'
- version: 7.0.0-dev.20260428.1
+ version: 7.0.0-dev.20260505.1
typescript:
specifier: 'catalog:'
version: 6.0.3
@@ -843,25 +849,25 @@ importers:
version: link:../../packages/tsconfig
'@eslint/js':
specifier: 'catalog:'
- version: 10.0.1(eslint@10.2.1(jiti@2.6.1))
+ version: 10.0.1(eslint@10.3.0(jiti@2.6.1))
'@types/node':
specifier: 'catalog:'
version: 25.6.0
'@typescript-eslint/eslint-plugin':
specifier: 'catalog:'
- version: 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ version: 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
'@typescript-eslint/parser':
specifier: 'catalog:'
- version: 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ version: 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
'@typescript/native-preview':
specifier: 'catalog:'
- version: 7.0.0-dev.20260428.1
+ version: 7.0.0-dev.20260505.1
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)
eslint:
specifier: 'catalog:'
- version: 10.2.1(jiti@2.6.1)
+ version: 10.3.0(jiti@2.6.1)
typescript:
specifier: 'catalog:'
version: 6.0.3
@@ -879,10 +885,10 @@ importers:
dependencies:
'@amplitude/analytics-browser':
specifier: 'catalog:'
- version: 2.42.0
+ version: 2.42.1
'@amplitude/plugin-session-replay-browser':
specifier: 'catalog:'
- version: 1.28.1(@amplitude/rrweb@2.0.0-alpha.37)
+ version: 1.29.0(@amplitude/rrweb@2.0.0-alpha.40)
'@base-ui/react':
specifier: 'catalog:'
version: 1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -894,7 +900,7 @@ importers:
version: 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@formatjs/intl-localematcher':
specifier: 'catalog:'
- version: 0.8.4
+ version: 0.8.6
'@headlessui/react':
specifier: 'catalog:'
version: 2.2.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -927,22 +933,22 @@ importers:
version: 4.7.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@orpc/client':
specifier: 'catalog:'
- version: 1.14.0
+ version: 1.14.1
'@orpc/contract':
specifier: 'catalog:'
- version: 1.14.0
+ version: 1.14.1
'@orpc/openapi-client':
specifier: 'catalog:'
- version: 1.14.0
+ version: 1.14.1
'@orpc/tanstack-query':
specifier: 'catalog:'
- version: 1.14.0(@orpc/client@1.14.0)(@tanstack/query-core@5.100.6)
+ version: 1.14.1(@orpc/client@1.14.1)(@tanstack/query-core@5.100.9)
'@remixicon/react':
specifier: 'catalog:'
version: 4.9.0(react@19.2.5)
'@sentry/react':
specifier: 'catalog:'
- version: 10.50.0(react@19.2.5)
+ version: 10.51.0(react@19.2.5)
'@streamdown/math':
specifier: 'catalog:'
version: 1.0.2(react@19.2.5)
@@ -951,7 +957,7 @@ importers:
version: 3.2.5
'@t3-oss/env-nextjs':
specifier: 'catalog:'
- version: 0.13.11(typescript@6.0.3)(valibot@1.3.1(typescript@6.0.3))(zod@4.3.6)
+ version: 0.13.11(typescript@6.0.3)(valibot@1.3.1(typescript@6.0.3))(zod@4.4.3)
'@tailwindcss/typography':
specifier: 'catalog:'
version: 0.5.19(tailwindcss@4.2.4)
@@ -963,7 +969,7 @@ importers:
version: 0.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tanstack/react-query':
specifier: 'catalog:'
- version: 5.100.6(react@19.2.5)
+ version: 5.100.9(react@19.2.5)
'@tanstack/react-virtual':
specifier: 'catalog:'
version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -996,7 +1002,7 @@ importers:
version: 10.6.0
dompurify:
specifier: 'catalog:'
- version: 3.4.1
+ version: 3.4.2
echarts:
specifier: 'catalog:'
version: 6.0.0
@@ -1017,10 +1023,13 @@ importers:
version: 5.6.0
es-toolkit:
specifier: 'catalog:'
- version: 1.46.0
+ version: 1.46.1
fast-deep-equal:
specifier: 'catalog:'
version: 3.1.3
+ fuse.js:
+ specifier: 'catalog:'
+ version: 7.2.0
hast-util-to-jsx-runtime:
specifier: 'catalog:'
version: 2.3.6
@@ -1038,10 +1047,10 @@ importers:
version: 1.2.1
immer:
specifier: 'catalog:'
- version: 11.1.4
+ version: 11.1.6
jotai:
specifier: 'catalog:'
- version: 2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.5)
+ version: 2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.5)
js-audio-recorder:
specifier: 'catalog:'
version: 1.0.7
@@ -1068,7 +1077,7 @@ importers:
version: 0.44.0
loro-crdt:
specifier: 'catalog:'
- version: 1.12.0
+ version: 1.12.1
mermaid:
specifier: 'catalog:'
version: 11.14.0
@@ -1113,7 +1122,7 @@ importers:
version: 5.5.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react-hotkeys-hook:
specifier: 'catalog:'
- version: 5.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 5.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react-i18next:
specifier: 'catalog:'
version: 16.5.8(i18next@26.0.8(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3)
@@ -1134,7 +1143,7 @@ importers:
version: 8.5.9(@types/react@19.2.14)(react@19.2.5)
reactflow:
specifier: 'catalog:'
- version: 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
remark-breaks:
specifier: 'catalog:'
version: 4.0.0
@@ -1167,7 +1176,7 @@ importers:
version: 2.3.1
tldts:
specifier: 'catalog:'
- version: 7.0.29
+ version: 7.0.30
unist-util-visit:
specifier: 'catalog:'
version: 5.1.0
@@ -1179,20 +1188,20 @@ importers:
version: 14.0.0
zod:
specifier: 'catalog:'
- version: 4.3.6
+ version: 4.4.3
zundo:
specifier: 'catalog:'
- version: 2.3.0(zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)))
+ version: 2.3.0(zustand@5.0.13(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)))
zustand:
specifier: 'catalog:'
- version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
+ version: 5.0.13(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
devDependencies:
'@antfu/eslint-config':
specifier: 'catalog:'
- version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.4)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.59.1(typescript@6.0.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.1(jiti@2.6.1)))(eslint@10.2.1(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)
+ version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.4)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3))(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)
'@chromatic-com/storybook':
specifier: 'catalog:'
- version: 5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 5.1.2(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@dify/contracts':
specifier: workspace:*
version: link:../packages/contracts
@@ -1207,7 +1216,7 @@ importers:
version: 1.9.2(tailwindcss@4.2.4)
'@eslint-react/eslint-plugin':
specifier: 'catalog:'
- version: 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ version: 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
'@iconify-json/heroicons':
specifier: 'catalog:'
version: 1.2.3
@@ -1240,22 +1249,22 @@ importers:
version: 4.2.0
'@storybook/addon-docs':
specifier: 'catalog:'
- version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 10.3.6(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@storybook/addon-links':
specifier: 'catalog:'
- version: 10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 10.3.6(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@storybook/addon-onboarding':
specifier: 'catalog:'
- version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 10.3.6(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@storybook/addon-themes':
specifier: 'catalog:'
- version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ version: 10.3.6(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@storybook/nextjs-vite':
specifier: 'catalog:'
- version: 10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)
+ version: 10.3.6(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)
'@storybook/react':
specifier: 'catalog:'
- version: 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)
+ version: 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)
'@tailwindcss/postcss':
specifier: 'catalog:'
version: 4.2.4
@@ -1264,7 +1273,7 @@ importers:
version: 4.2.4(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
'@tanstack/eslint-plugin-query':
specifier: 'catalog:'
- version: 5.100.6(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ version: 5.100.9(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
'@tanstack/react-devtools':
specifier: 'catalog:'
version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -1273,7 +1282,7 @@ importers:
version: 0.2.22(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.5)(solid-js@1.9.11)
'@tanstack/react-query-devtools':
specifier: 'catalog:'
- version: 5.100.6(@tanstack/react-query@5.100.6(react@19.2.5))(react@19.2.5)
+ version: 5.100.9(@tanstack/react-query@5.100.9(react@19.2.5))(react@19.2.5)
'@testing-library/dom':
specifier: 'catalog:'
version: 10.4.1
@@ -1288,13 +1297,13 @@ importers:
version: 14.6.1(@testing-library/dom@10.4.1)
'@tsslint/cli':
specifier: 'catalog:'
- version: 3.1.0(@tsslint/compat-eslint@3.1.0(typescript@6.0.3))(typescript@6.0.3)
+ version: 3.1.1(@tsslint/compat-eslint@3.1.1(typescript@6.0.3))(typescript@6.0.3)
'@tsslint/compat-eslint':
specifier: 'catalog:'
- version: 3.1.0(typescript@6.0.3)
+ version: 3.1.1(typescript@6.0.3)
'@tsslint/config':
specifier: 'catalog:'
- version: 3.1.0(@tsslint/compat-eslint@3.1.0(typescript@6.0.3))(typescript@6.0.3)
+ version: 3.1.1(@tsslint/compat-eslint@3.1.1(typescript@6.0.3))(typescript@6.0.3)
'@types/js-cookie':
specifier: 'catalog:'
version: 3.0.6
@@ -1321,10 +1330,10 @@ importers:
version: 1.15.9
'@typescript-eslint/parser':
specifier: 'catalog:'
- version: 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ version: 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
'@typescript/native-preview':
specifier: 'catalog:'
- version: 7.0.0-dev.20260428.1
+ version: 7.0.0-dev.20260505.1
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
@@ -1342,46 +1351,46 @@ importers:
version: 1.5.1
eslint:
specifier: 'catalog:'
- version: 10.2.1(jiti@2.6.1)
+ version: 10.3.0(jiti@2.6.1)
eslint-markdown:
specifier: 'catalog:'
- version: 0.7.0(eslint@10.2.1(jiti@2.6.1))
+ version: 0.8.0(eslint@10.3.0(jiti@2.6.1))
eslint-plugin-better-tailwindcss:
specifier: 'catalog:'
- version: 4.5.0(eslint@10.2.1(jiti@2.6.1))(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tailwindcss@4.2.4)(typescript@6.0.3)
+ version: 4.5.0(eslint@10.3.0(jiti@2.6.1))(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tailwindcss@4.2.4)(typescript@6.0.3)
eslint-plugin-hyoban:
specifier: 'catalog:'
- version: 0.14.1(eslint@10.2.1(jiti@2.6.1))
+ version: 0.14.1(eslint@10.3.0(jiti@2.6.1))
eslint-plugin-markdown-preferences:
specifier: 'catalog:'
- version: 0.41.1(@eslint/markdown@8.0.1)(eslint@10.2.1(jiti@2.6.1))
+ version: 0.41.1(@eslint/markdown@8.0.1)(eslint@10.3.0(jiti@2.6.1))
eslint-plugin-no-barrel-files:
specifier: 'catalog:'
- version: 1.3.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ version: 1.3.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
eslint-plugin-react-refresh:
specifier: 'catalog:'
- version: 0.5.2(eslint@10.2.1(jiti@2.6.1))
+ version: 0.5.2(eslint@10.3.0(jiti@2.6.1))
eslint-plugin-sonarjs:
specifier: 'catalog:'
- version: 4.0.3(eslint@10.2.1(jiti@2.6.1))
+ version: 4.0.3(eslint@10.3.0(jiti@2.6.1))
eslint-plugin-storybook:
specifier: 'catalog:'
- version: 10.3.5(eslint@10.2.1(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)
+ version: 10.3.6(eslint@10.3.0(jiti@2.6.1))(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)
happy-dom:
specifier: 'catalog:'
version: 20.9.0
knip:
specifier: 'catalog:'
- version: 6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
+ version: 6.11.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
postcss:
specifier: 'catalog:'
- version: 8.5.12
+ version: 8.5.14
react-server-dom-webpack:
specifier: 'catalog:'
version: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
storybook:
specifier: 'catalog:'
- version: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
tailwindcss:
specifier: 'catalog:'
version: 4.2.4
@@ -1396,7 +1405,7 @@ importers:
version: 3.19.3
vinext:
specifier: 'catalog:'
- version: 0.0.45(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3)
+ version: 0.0.47(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3)
vite:
specifier: npm:@voidzero-dev/vite-plus-core@0.1.20
version: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)'
@@ -1422,11 +1431,8 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
- '@amplitude/analytics-browser@2.42.0':
- resolution: {integrity: sha512-xG1CU3M8kYjmQmxxvy8c8m1ww7wqp+kuttpVxWsItyKABBIZNofRo4E0UzENBu8PuXRcwKrLq99DdceVKtsL0g==}
-
- '@amplitude/analytics-client-common@2.4.46':
- resolution: {integrity: sha512-cvNzR7GY+PqvdT7b1jjs+LhLjkLr/raS8C6Jo4nTD/hDzWI+b73u12atttbgWKGJMCmki+xs+X0oyMt207+qtQ==}
+ '@amplitude/analytics-browser@2.42.1':
+ resolution: {integrity: sha512-lanSeX3DeOAx0aF3H0BOtanUEjibAZFwp9Gmv/Tqycr8YdlY3yTmjH2Rl1HsRVopWQ7s3fbu2S15z9MMwvuFWg==}
'@amplitude/analytics-client-common@2.4.47':
resolution: {integrity: sha512-AFmZK3e3mytXtwyLeNqF85T8bGMcw4slYP8mRu9Fm5B9OZEnEqjhKYzNDEMOKtwBkEQbusHCWWYGfZT5sY2+tg==}
@@ -1434,20 +1440,20 @@ packages:
'@amplitude/analytics-connector@1.6.4':
resolution: {integrity: sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==}
- '@amplitude/analytics-core@2.47.1':
- resolution: {integrity: sha512-ZdtAx5syGZBQpbZVLnc/zp7sMlq7+b1dxo/5gCG/4thNW0vOHfN4nYGlV2+k/VEEw4/hW893t5EPUCbxUJM+OQ==}
-
'@amplitude/analytics-core@2.48.0':
resolution: {integrity: sha512-6ckWWL60LiJJEQQ5V3Veviq0Gl5Lcvy1dFaRDKA/SwLT3cgiBrEFZXZPcri4wDGjchPmJXFCYcE55nr7rP6Wjg==}
+ '@amplitude/analytics-core@2.48.1':
+ resolution: {integrity: sha512-6ltecomnZc9z1AUE59orcoLYii0gPoVBBoI4qoMGXw2lsxFh35zx32LPrXO3ezFkTE71CS44AzwaBFsSxQWRuw==}
+
'@amplitude/analytics-types@2.11.1':
resolution: {integrity: sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A==}
'@amplitude/experiment-core@0.7.2':
resolution: {integrity: sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA==}
- '@amplitude/plugin-autocapture-browser@1.27.0':
- resolution: {integrity: sha512-iSQJQA2nMftjJ+MtB/ndrLRkSWqsTrGUwia9f+bRodWEQbLw36o/JVD6aHqm5rTvXYvsK/oK9Qv6PAv1lKIL5A==}
+ '@amplitude/plugin-autocapture-browser@1.27.1':
+ resolution: {integrity: sha512-vJxwfExfREBIMbVydAimO/diYsRfNBD/nqo+DABURBsdmb+gliUPd7J6g22ys7al59+ixRwJSQMUrk0bnqn76Q==}
'@amplitude/plugin-custom-enrichment-browser@0.1.8':
resolution: {integrity: sha512-PVg56GfQID/UKLZx7imbg6Tmlj2AX/euyG7nnouKpowgGJ7jz/t4o2u3csSgrKbLSrTjxdbXVdPyz/+CecJ4Zg==}
@@ -1464,46 +1470,40 @@ packages:
'@amplitude/plugin-page-view-tracking-browser@2.11.0':
resolution: {integrity: sha512-ZI/1kTQID0yXileGjvseMoFZ9zUbLG5r+MsznmT0Br+x91LdCrPHXdpU7lq53UAwIhgREXk9S/o/AwUZfFSgzg==}
- '@amplitude/plugin-session-replay-browser@1.28.1':
- resolution: {integrity: sha512-gSRIZUgUKEb4OvYcGYJZ553YVftq8lIzYkV9BsIQqUxFn3MVFygdQj+ZJCz75lJeyJZI7r9VhLTIzIcuIJ0KFg==}
+ '@amplitude/plugin-session-replay-browser@1.29.0':
+ resolution: {integrity: sha512-+j+nHjiwe4qHmL42Y9jkSMrlQ3GqHpiGwT2xyYZiaajYCfXWmCbIQuXqlf2m8iqsk2l/CxSCek3nLQ6C93F2Ug==}
'@amplitude/plugin-web-vitals-browser@1.1.32':
resolution: {integrity: sha512-cf/MR5WTJ5iwCjxdy9f7vK8zy2nD1iXPwu8eKHiRxWR7Eoqx7bT30n9dar8kWDV8kraV0sglA5pVrP3b33m/pw==}
- '@amplitude/rrdom@2.0.0-alpha.37':
- resolution: {integrity: sha512-u4dSnBtlbJ8oU5P/Ywl2RLqvjqWbkl4ScMUbvQA7in4pWcx+0NRN+VVjLZXQcd8Fn7E/rcxjeUh7e7HfwvdasQ==}
+ '@amplitude/rrdom@2.0.0-alpha.40':
+ resolution: {integrity: sha512-gmOHzCcQEHgzmlqjTiYvTrbAvlk431lmXTpSGKxrCucMg4upQHs75f+YSTFWy6w3m6LwtkUmBJuCqMF5e16Y7w==}
- '@amplitude/rrweb-packer@2.0.0-alpha.36':
- resolution: {integrity: sha512-kqKg6OGoxHZvG4jwyO4kIjLdf8MkL6JcY5iLB09PQNP7O36ysnrH+ecJfa4V1Rld99kX25Pefkw4bzKmmFAqcg==}
+ '@amplitude/rrweb-packer@2.0.0-alpha.40':
+ resolution: {integrity: sha512-Btb6b9pS1IvDMbvyYxpUdTk9NRJugSoJjRCl7R6jP/iSlPWXoveJIwHaNFAS9ZmWUEK7HhyBJ8bKGFN3giUsDg==}
- '@amplitude/rrweb-plugin-console-record@2.0.0-alpha.36':
- resolution: {integrity: sha512-7VbXu36PpJA8dSOFxpfpMaoDTuPK5uy1C8mN+Wfdm0X4ROdmrvcTdlQj+jGzhLGeK+xbTixHEy23itCNUau7hQ==}
+ '@amplitude/rrweb-plugin-console-record@2.0.0-alpha.40':
+ resolution: {integrity: sha512-vtY7T/kGFl62nC1u7ZUXQvU7ulB70cZGVHPRN/SO9fzVfsY7y6rCmBfoc2jS5KmISdlgkVzMjY2r/EE2Gk9AQA==}
peerDependencies:
- '@amplitude/rrweb': ^2.0.0-alpha.36
+ '@amplitude/rrweb': ^2.0.0-alpha.40
- '@amplitude/rrweb-record@2.0.0-alpha.36':
- resolution: {integrity: sha512-zSHvmG5NUG4jNgWNVM7Oj3+rJPagv+TiHlnSiJ1X0WWLIg1GbUnOoTqpincZS5QupqTxQchNQaUg9MNu0MM3sQ==}
+ '@amplitude/rrweb-record@2.0.0-alpha.40':
+ resolution: {integrity: sha512-5cJhQwzhymJWX5/XOtpWK0h2NLq9+t2YiO6ub0cdZ9F5AZizaRbsVH88int07DfX0YiXTKWbISezVuduCLqgSQ==}
- '@amplitude/rrweb-snapshot@2.0.0-alpha.37':
- resolution: {integrity: sha512-OPW2r8ESAguq+1R+z+WxGyzZzkMtojZ49Lpp6NrataNFyjdKaNXehDuLoNlEQkkUZGyDBiA7RSYvUw+JPSmmSQ==}
+ '@amplitude/rrweb-snapshot@2.0.0-alpha.40':
+ resolution: {integrity: sha512-S26czz9TSsmgMc0V9lJcgD+1lLi1G/77kh7pdMs3HHJ85GLn00yucoMOzVHC4O83frk7rE9cSNNdF+x21H0ZjA==}
- '@amplitude/rrweb-types@2.0.0-alpha.36':
- resolution: {integrity: sha512-Bd2r3Bs0XIJt5fgPRWVl8bhvA9FCjJn8vQlDTO8ffPxilGPIzUXLQ06+xoLYkK9v+PDKJnCapOTL4A2LilDmgA==}
+ '@amplitude/rrweb-types@2.0.0-alpha.40':
+ resolution: {integrity: sha512-rP7CBDkzXupxOA7ukvC+zDYLuCtsz54TuJKC4+5O72Jsz4YdokLznKZRG34P6zXozfhGU0261qckk87lLY6mKQ==}
- '@amplitude/rrweb-types@2.0.0-alpha.37':
- resolution: {integrity: sha512-LW9wQ85umaAW/qlemTrUC408WVoBx99hvFCjsNRnxAyUmRemWyYY7+o8xPyeUexoWGqizPMkkNnPEO8t1NFjtw==}
+ '@amplitude/rrweb-utils@2.0.0-alpha.40':
+ resolution: {integrity: sha512-i1CCt6MCjlqoeNc+1Hse5bz+ZbASaWaIJ0WdJZvnQjUCHH29Xy/QFouyOuor73RZ+UWX4s2tYSrUIdmBepXk3w==}
- '@amplitude/rrweb-utils@2.0.0-alpha.36':
- resolution: {integrity: sha512-w5RGROLU1Kyrq9j+trxcvvfkTp05MEKJ70Ig+YvHyZsE0nElh1PCF8PHAjV0/kji68+KqB03c0hoyaV99CDaDw==}
+ '@amplitude/rrweb@2.0.0-alpha.40':
+ resolution: {integrity: sha512-pFXwvQmLXTWSattr3y2ufQdZY7Is3abP0xIsHe7D7YI8D4hZ8Erf/2Z1HmuH33lHqV5e/ZItFF5gJftSiiV3TA==}
- '@amplitude/rrweb-utils@2.0.0-alpha.37':
- resolution: {integrity: sha512-40YvPj24ietFQ3BTLfvFRPriRqdNOp3DzGiPU+WDOZkI3KjInQrEsibaqNBSXzJ+kMWrm8/eEwcQ0FkLk7Achw==}
-
- '@amplitude/rrweb@2.0.0-alpha.37':
- resolution: {integrity: sha512-jJkSpPYiVgOZB422pb2jOJJn3pvb5E5f9vKK8CEmUlk2mVAl6kPQzW98mb05M65OJFj5nn9tSe9h5r5+Cl93ag==}
-
- '@amplitude/session-replay-browser@1.39.0':
- resolution: {integrity: sha512-JUVrzBNFbZKvA3QFls5fVLRvpr8F369omyIQzF5qkIpB9ExpAXqRvSXpwX+64oXlugQfW0oR9iAgRu10C66ygg==}
+ '@amplitude/session-replay-browser@1.40.0':
+ resolution: {integrity: sha512-Wlt/BCKB7MXeg6d6SaIPz3o7CdJU/rlSmFgOGqbURvNys57YLsJdqNJMnz248FjJ+cfmWuVxdbblCPhahGUkbw==}
'@amplitude/targeting@0.2.0':
resolution: {integrity: sha512-/50ywTrC4hfcfJVBbh5DFbqMPPfaIOivZeb5Gb+OGM03QrA+lsUqdvtnKLNuWtceD4H6QQ2KFzPJ5aAJLyzVDA==}
@@ -1803,14 +1803,11 @@ packages:
peerDependencies:
tailwindcss: '*'
- '@emnapi/core@1.9.2':
- resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
- '@emnapi/runtime@1.9.1':
- resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
-
- '@emnapi/runtime@1.9.2':
- resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
@@ -2050,10 +2047,6 @@ packages:
resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/config-helpers@0.5.4':
- resolution: {integrity: sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg==}
- engines: {node: ^20.19.0 || ^22.13.0 || >=24}
-
'@eslint/config-helpers@0.5.5':
resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
@@ -2062,10 +2055,6 @@ packages:
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@eslint/core@1.2.0':
- resolution: {integrity: sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A==}
- engines: {node: ^20.19.0 || ^22.13.0 || >=24}
-
'@eslint/core@1.2.1':
resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
@@ -2134,11 +2123,11 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
- '@formatjs/fast-memoize@3.1.2':
- resolution: {integrity: sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==}
+ '@formatjs/fast-memoize@3.1.4':
+ resolution: {integrity: sha512-Lbke1aOrsygKKR09Ux0NrZgbTqpDmiwXOgzyDOJ8Owr1zd5qOKTauf62hH+Seeku3ju77rHWH9I5SfX2CN0vuA==}
- '@formatjs/intl-localematcher@0.8.4':
- resolution: {integrity: sha512-J51dAnynnqJdVUEXidHoIWn+qYve+yNQEgmFk9Dyfr3p0okzm+5QhQ+9QmsMz08+BeWTVpc1HadIiLfZmRYbAQ==}
+ '@formatjs/intl-localematcher@0.8.6':
+ resolution: {integrity: sha512-AZRgUxj0q93lyF7Z5lFS85bLINXuBLX4R3tCKicO6fSWo6cvh9GQfoR3B1WlsqQwefZ1QORTivhInx7gM6HUzQ==}
'@headlessui/react@2.2.10':
resolution: {integrity: sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==}
@@ -2156,19 +2145,19 @@ packages:
resolution: {integrity: sha512-Iciv2vUCJTW9lWM/ROvyZLblmcbYJHPuXfzb1SzeDVVn4xEXu2ilLU1pq3fn+09FZ/Y0P7VyvRE47UDU6om8xA==}
engines: {node: '>=22.13.0'}
- '@hey-api/json-schema-ref-parser@1.4.1':
- resolution: {integrity: sha512-DoPJGxVApDlktP1yYLjmOrF0YBEqb32ieCbx1S1i09n8TyCgdoh4yQaQ3kp0sMTauH+bwNKPsFh7S8qiWCoKZA==}
+ '@hey-api/json-schema-ref-parser@1.4.2':
+ resolution: {integrity: sha512-ZhCFSKI2ipZHEbgmtUHdyddvRU3wJ4elgCfYUC7T7hZa4EivSrVflTQf2w+v3TuaYxR1Y2V2kq3otqTttrrK8Q==}
engines: {node: '>=22.13.0'}
- '@hey-api/openapi-ts@0.97.0':
- resolution: {integrity: sha512-WZkKgrDlZpxKlDv2HkBCzaAYeuM+EtZKFmKGBv9/JblAKpX3JQTROi7PzlCZE3eisetRPSakbcRgn+LGyB7EiQ==}
+ '@hey-api/openapi-ts@0.97.1':
+ resolution: {integrity: sha512-LksUJeXAqwf6OhcCCr3/B4YjnBs5rqSqjDUKMBvkgp4OhaCQiJrOvntctFxdnugy8jUojP4yi/eJf5xYzcYzCQ==}
engines: {node: '>=22.13.0'}
hasBin: true
peerDependencies:
typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc'
- '@hey-api/shared@0.4.2':
- resolution: {integrity: sha512-4fconS10E0Xr4/acV8G+BkApxaIStxrT0GhB9BDTQWvrFTy5/nV933SyFk8qImcbpKvgv9hpn3N+7bV8oFrbjA==}
+ '@hey-api/shared@0.4.3':
+ resolution: {integrity: sha512-3tHfZNXgGOt+3P3Kq9cvqmZ9i7e3jtrkip1uDpZTX1+hTNboHhYdjxnT8AbrDuvslTaQHoAOlP4/iCDdzd9Jag==}
engines: {node: '>=22.13.0'}
'@hey-api/spec-types@0.2.0':
@@ -2177,8 +2166,8 @@ packages:
'@hey-api/types@0.1.4':
resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==}
- '@hono/node-server@2.0.0':
- resolution: {integrity: sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ==}
+ '@hono/node-server@2.0.1':
+ resolution: {integrity: sha512-jI9yMDyFpqBeSighf/zlXnQG/nl9AyBc6aAgy4XtxJMyt/CNyJpvPfzDD+bCc2zAOmhhqtF6TnmIaY+xV4mIrw==}
engines: {node: '>=20'}
peerDependencies:
hono: ^4
@@ -2621,165 +2610,165 @@ packages:
resolution: {integrity: sha512-y3SvzjuY1ygnzWA4Krwx/WaJAsTMP11DN+e21A8Fa8PW1oDtVB5NSRW7LWurAiS2oKRkuCgcjTYMkBuBkcPCRg==}
engines: {node: '>=12.4.0'}
- '@orpc/client@1.14.0':
- resolution: {integrity: sha512-TYVcj1s5bN9adggeqIXFdIdoBBUAMUxQwMNv6YagjiaZkGtqWUYd1Y1vU0Rn/9xHWF2+0hBZNUKUmP5qrQhIAw==}
+ '@orpc/client@1.14.1':
+ resolution: {integrity: sha512-uTtM1DGQvlXVXuqHF2LXWG0Sykr3gcqJ/ufbUDvnpBbAiSYZqXAlHUBptUbjEu7xS+hjy/s+n3C7kjT2Ou+D8w==}
- '@orpc/contract@1.14.0':
- resolution: {integrity: sha512-FUxBNqWr6mOjI+w1JPzO/iHmR3M+GA53ivaxp+eOnQu7g3ZGKB0RS5gJ/oz3cGF1gvuIcCw9FVYKK/5tkB8I1Q==}
+ '@orpc/contract@1.14.1':
+ resolution: {integrity: sha512-ARcZPZzZ6x+l7LWwWjSv2mOmoUQ/VpEKIDMQPStko0H3LPKokwQbmLjeiobNSFdmz9UJ/XqEsh26oHhSxE0TnQ==}
- '@orpc/openapi-client@1.14.0':
- resolution: {integrity: sha512-joeVdSX2YYFQM+bY4SdNHmnoiw7aYfc7NDEWDncnjpho6bj3DhnDNsINgFnFX7A9by7mVYaLw45yqjDhNSMprg==}
+ '@orpc/openapi-client@1.14.1':
+ resolution: {integrity: sha512-06vrbVq4Nhbgy4mVcoZFdclYw64f0CnRBeISSrgGH7/zJSE7ILNUMi5PC9Ok0PE+cEpmgE/c+BONwzy2jub3fw==}
- '@orpc/shared@1.14.0':
- resolution: {integrity: sha512-WNzofimsE3sKbkyAAwVKMwG4P7sL0fzDLUhXqEXuJ9Yjll+phy/jSRK9TupNMtsPyz9ViKHKCQcwmsdgIgn9Sg==}
+ '@orpc/shared@1.14.1':
+ resolution: {integrity: sha512-TVF4nDacb1RBAB2gXAHmgBBnuTrZEIDJ1bP6MXLVXrbsPknH0ODk5hIkq5a6agxF48jsLK94UdAj1lj59YQQRQ==}
peerDependencies:
'@opentelemetry/api': '>=1.9.0'
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
- '@orpc/standard-server-fetch@1.14.0':
- resolution: {integrity: sha512-qg315ZVbQ+02WnLzep7YvCsXb8BdefZ7Zjt+/emu6+Ypgw4fS0O78jtMHy3r39YvdvC9U2hWt8hff1yKiVlvQA==}
+ '@orpc/standard-server-fetch@1.14.1':
+ resolution: {integrity: sha512-nSlylNKX8MfBxaTZeHGhPsKF0ZplmEvW/NSFJehBG+3DCscUuyIx/WcJHvNZKWqMD5dCeEjP71o5Yl9fhTPlXA==}
- '@orpc/standard-server-peer@1.14.0':
- resolution: {integrity: sha512-Phk8D04uxNJMLvl7JfJlWvfzDXwzfGweh4jmQI69zSV+flihp57dkZuk8gpTE7rfDClFiKCDauVsB/pQxwM09Q==}
+ '@orpc/standard-server-peer@1.14.1':
+ resolution: {integrity: sha512-2SU6aNDiGQZHt9BcH4JnTpkU1gCscB2VziBjwTnZfiodlMKI80y73fi/2hpVphGnEnz+jArmV10tU/HZNerfjw==}
- '@orpc/standard-server@1.14.0':
- resolution: {integrity: sha512-zN3Q+ajsoLoxLYmONc1RkDyhIg1wENolrTly8HfodcR3gYrfFRcGhUzShqa/KdG47mK49Nps8rdeeMj6NT8EYw==}
+ '@orpc/standard-server@1.14.1':
+ resolution: {integrity: sha512-mmdus5pW+3vFj2/Nen7zyDo3AUz0n4Dx+M0Mexz9Hy2XQQ4zIr/t1DQFE3u9w7wdFy7/Yaof5v+70fcfhkq1MQ==}
- '@orpc/tanstack-query@1.14.0':
- resolution: {integrity: sha512-Bjx29HULT5PNSaGFkt+rExTqQonZfaqrAMUOLWBBNlI8TtPMvnKtDxlzmvO5J4Aq8k5p0t+cZX1E6HTeH3mqKQ==}
+ '@orpc/tanstack-query@1.14.1':
+ resolution: {integrity: sha512-AzrUAEO3yNGg89mJvgJOK2IxZoYBjGUPMh73JzGkcTOuVDhIcL8rNfIFF71y6tqHC6J8trN1ZrHf35j4clgGjg==}
peerDependencies:
- '@orpc/client': 1.14.0
+ '@orpc/client': 1.14.1
'@tanstack/query-core': '>=5.80.2'
'@ota-meshi/ast-token-store@0.3.0':
resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@oxc-parser/binding-android-arm-eabi@0.127.0':
- resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==}
+ '@oxc-parser/binding-android-arm-eabi@0.128.0':
+ resolution: {integrity: sha512-aca6ZvzmCBUGOANQRiRQRZuRKYI3ENhcit6GisnknOOmcezfQc7xJ4dxlPU7MV7mOvrC7RNR1u3LAD7xyaiCxA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
- '@oxc-parser/binding-android-arm64@0.127.0':
- resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==}
+ '@oxc-parser/binding-android-arm64@0.128.0':
+ resolution: {integrity: sha512-BbeDmuohoJ7Rz/it5wnkj69i/OsCPS3Z51nLEzwO/Y6YshtC4JU+15oNwhY8v4LRKRYclRc7ggOikwrsJ/eOEQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@oxc-parser/binding-darwin-arm64@0.127.0':
- resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==}
+ '@oxc-parser/binding-darwin-arm64@0.128.0':
+ resolution: {integrity: sha512-tRUHPt80417QmvNpoSslJT1VY8NUbWdrWR+L14Zn+RbOTcaqB8E6PYE/ZGN8jjWBzqporiA/H4MfO50ew/NCNA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@oxc-parser/binding-darwin-x64@0.127.0':
- resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==}
+ '@oxc-parser/binding-darwin-x64@0.128.0':
+ resolution: {integrity: sha512-rWI2Hb1Nt3U/vKsjyNvZzDC8i/l144U20DKjhzaTmwIhIiSRGeroPWWiImwypmKLqrw8GuIixbWJkpGWLbkzrQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@oxc-parser/binding-freebsd-x64@0.127.0':
- resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==}
+ '@oxc-parser/binding-freebsd-x64@0.128.0':
+ resolution: {integrity: sha512-hhpdVMaNCLgQxjgNPeeFzSeJMmZPc5lKfv0NGSI3egZq9EdnEGqeC8JsYsQjK7PoQgbvZ17xlj0SO5ziH5Obkg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0':
- resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==}
+ '@oxc-parser/binding-linux-arm-gnueabihf@0.128.0':
+ resolution: {integrity: sha512-093zNw0zZ/e/obML+rhlSdmnzR0mVZluPcAkxunEc5E3F0yBVsFn24Y1ILfsEte11Ud041qn/gp2OJ1jxNqUng==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxc-parser/binding-linux-arm-musleabihf@0.127.0':
- resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==}
+ '@oxc-parser/binding-linux-arm-musleabihf@0.128.0':
+ resolution: {integrity: sha512-fq7DmKmfC+dvD97IXrgbph6Jzwe0EDu+PYMofmzZ6fv5X1k9vtaqLpDGMuICO9MmUnyKAQmVl+wIv2RNy4Dz8g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxc-parser/binding-linux-arm64-gnu@0.127.0':
- resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==}
+ '@oxc-parser/binding-linux-arm64-gnu@0.128.0':
+ resolution: {integrity: sha512-Xvm48jJah8TlIrURIjNOP/gNiGe6aKvCB+r06VliflFo8Kq7VOLE8PxtgShJzZIqubrgdMdYfvuPPozn7F6MbQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-arm64-musl@0.127.0':
- resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==}
+ '@oxc-parser/binding-linux-arm64-musl@0.128.0':
+ resolution: {integrity: sha512-M7iwBGmYJTx+pKOYFjI0buop4gJvlmcVzFGaXPt21DKpQkbQZG1f63Yg7LloIYT/t9yLxCw0Lhfx/RFlAlMSjA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@oxc-parser/binding-linux-ppc64-gnu@0.127.0':
- resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==}
+ '@oxc-parser/binding-linux-ppc64-gnu@0.128.0':
+ resolution: {integrity: sha512-21LGNIZb1Pcfk5/EGsqabrxv4yqQOWis1407JJrClS7XpFCrbvr74YAB1V+m54cYbwvO6UWwQqS4WecxiyfCRg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-riscv64-gnu@0.127.0':
- resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==}
+ '@oxc-parser/binding-linux-riscv64-gnu@0.128.0':
+ resolution: {integrity: sha512-gyHjOTFpg9bTTYjxPmQirvufb89+VdZwVfcMtAUyPr6F5H8ZswvCQshK4qOW+Q+2Xyb33hduRgY/eFHJQjU/vQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-riscv64-musl@0.127.0':
- resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==}
+ '@oxc-parser/binding-linux-riscv64-musl@0.128.0':
+ resolution: {integrity: sha512-X6Q2oKUrP5GyDd2xniuEBLk6aFQCZ97W2+aVXGgJXdjx5t4/oFuA9ri0wLOUrBIX+qdSuK581snMBio4z910eA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
- '@oxc-parser/binding-linux-s390x-gnu@0.127.0':
- resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==}
+ '@oxc-parser/binding-linux-s390x-gnu@0.128.0':
+ resolution: {integrity: sha512-BdzTmqxfxoYkpgokoLaSnOX6T+R3/goL42klre2tnG+kHbG2TXS0VN+P5BPofH1axdKOHy5ei4ENZrjmCOt2lA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-x64-gnu@0.127.0':
- resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==}
+ '@oxc-parser/binding-linux-x64-gnu@0.128.0':
+ resolution: {integrity: sha512-OO1nW2Q7sSYYvJZpDHdvyFSdRaVcQqRijZSSmWVMqFxPYy8cEF45zJ9fcdIYuzIT3jYq6YRhEFm/VMWNWhE22Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-x64-musl@0.127.0':
- resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==}
+ '@oxc-parser/binding-linux-x64-musl@0.128.0':
+ resolution: {integrity: sha512-4NehAe404MRdoZVS9DW8C5XbJwbXIc/KfVlYdpi5vE4081zc9Y0YzKVqyOYj/Puye7/Do+ohaONBFWlEHYl9hw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@oxc-parser/binding-openharmony-arm64@0.127.0':
- resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==}
+ '@oxc-parser/binding-openharmony-arm64@0.128.0':
+ resolution: {integrity: sha512-kVbqgW9xLL8bh8oc7aYOJilRKXE5G33+tE0jan+duo/9OriaFRpijcCwT2waWs2oqYROYq0GlE7/p3ywoshVeg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
- '@oxc-parser/binding-wasm32-wasi@0.127.0':
- resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==}
+ '@oxc-parser/binding-wasm32-wasi@0.128.0':
+ resolution: {integrity: sha512-L38ojghJYHmgiz6fJd7jwLB/ESDBpB02NdFxh+smqVM6P2anCEvHn0jhaSrt5eVNR1Ak8+moOeftUlofeyvniA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
- '@oxc-parser/binding-win32-arm64-msvc@0.127.0':
- resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==}
+ '@oxc-parser/binding-win32-arm64-msvc@0.128.0':
+ resolution: {integrity: sha512-xgvO35GyHBtjlQ5AEpaYr7Rll1rvY7zqIhT6ty8E3ezBW2J1SFLjIDEvI/tcgDg6oaseDAqVcM+jU1HuCekgZw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@oxc-parser/binding-win32-ia32-msvc@0.127.0':
- resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==}
+ '@oxc-parser/binding-win32-ia32-msvc@0.128.0':
+ resolution: {integrity: sha512-OY+3eM2SN72prHKRB22mPz8o5A/7dJ+f5DFLBVvggyZhEaNDAH9IB+ElMjmOkOIwf5MDCUAowCK7pAncNxzpBA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
- '@oxc-parser/binding-win32-x64-msvc@0.127.0':
- resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==}
+ '@oxc-parser/binding-win32-x64-msvc@0.128.0':
+ resolution: {integrity: sha512-NE9ny+cPUCCObXa0IKLfj0tCdPd7pe/dz9ZpkxpUOymB3miNeMPybdlYYTBSGJUalMWeBM85/4JcCErCNTqOXw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -2791,6 +2780,9 @@ packages:
'@oxc-project/types@0.127.0':
resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==}
+ '@oxc-project/types@0.128.0':
+ resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==}
+
'@oxc-resolver/binding-android-arm-eabi@11.19.1':
resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==}
cpu: [arm]
@@ -3491,32 +3483,32 @@ packages:
rollup:
optional: true
- '@sentry-internal/browser-utils@10.50.0':
- resolution: {integrity: sha512-42bxyRTxnCmYlWnvz4CxikuQNanw8UNma2WJrtxJ0f1MAJV2GhQGSHDLnA+lvFlmiz6qct3pfen/NXGyOTegTA==}
+ '@sentry-internal/browser-utils@10.51.0':
+ resolution: {integrity: sha512-lNKBS4P7RUvf1niojXQWe9bU3gnBUCbST4Dj0pSiyat1N96cXVyHkeE+uGxowD0RrVWhs+kGHiVX3FcmRWF6sA==}
engines: {node: '>=18'}
- '@sentry-internal/feedback@10.50.0':
- resolution: {integrity: sha512-0k9XZF0wn86f77mIO2U3gNNyDZooy139CnEanRzHinrN106vVzvBZ6TUEQoHtoO1fqQxr+nWWVrqV/PXUqk47w==}
+ '@sentry-internal/feedback@10.51.0':
+ resolution: {integrity: sha512-bCM95bcpphx28e6aU0bwRLxOgwosYsdNzezM1sM0pVOkb0TB3hDFRamramVDK+/Hp1o8qmRxS4c5w/A7YBZGkA==}
engines: {node: '>=18'}
- '@sentry-internal/replay-canvas@10.50.0':
- resolution: {integrity: sha512-jx6RKBmcJSWdI92qDGS/sBv1w+7Cww879Z/moX7bw7ipHa/Ts3iDcB3rgZwvhmi17U+mvYsbJeL2DXkPo3TjPw==}
+ '@sentry-internal/replay-canvas@10.51.0':
+ resolution: {integrity: sha512-8PW1Pp+Yl3lPwYqhBCr5SgkuhDanu9ZLzUqD2bPKL/ElqbM2eDVIWxq4z4ZzePrmZa6IcCjTv6sVQJ7Z4dLyLA==}
engines: {node: '>=18'}
- '@sentry-internal/replay@10.50.0':
- resolution: {integrity: sha512-51FYNfnvVLAWw1rrEWPFfwHuMRb9mkVCFGA4J9/un7SpeGBsQDziGB0Di4fsCxI7+EdSBpfLHPF0csKtCCw0oQ==}
+ '@sentry-internal/replay@10.51.0':
+ resolution: {integrity: sha512-jCpI5HXSwK6ZT2HX70+mDRciAocHzSiDk4DTgvzV69Wvd+Ei5WLgE+d39eaEPsm8lUC0Ydntb5sJIB6uG9D4bw==}
engines: {node: '>=18'}
- '@sentry/browser@10.50.0':
- resolution: {integrity: sha512-1f6rAvET6myiTaSeYqvaaBwvq1LfxqWjAPIoAW/NVC9bPMkeEcuvgDajHrnZMrBeWoJ81NMyoLkyX+iOc7MoFA==}
+ '@sentry/browser@10.51.0':
+ resolution: {integrity: sha512-Zdc0sKfenxUtW/OGhtJ7xHFN44bXR7YqxJ1zBDzlZfW0nTbeTTUZBq9z5NUw6qdS0Vs/i3V4qzAKTbRKWfqSEA==}
engines: {node: '>=18'}
- '@sentry/core@10.50.0':
- resolution: {integrity: sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg==}
+ '@sentry/core@10.51.0':
+ resolution: {integrity: sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w==}
engines: {node: '>=18'}
- '@sentry/react@10.50.0':
- resolution: {integrity: sha512-MZHYjEZAtFIa4zPrWS4oXlo+gMppRvfETqUqF920Sj2jN2U7WjboU03lDmjfDqEcH7QiwjQyl13jHd2nwAyrrw==}
+ '@sentry/react@10.51.0':
+ resolution: {integrity: sha512-RRHHqjNvjji6ebIqdlAr453AkST8Vm4cxdu1vWm772IgbzTO7Jx46Cj6Bt2/GjMyH0YLE5euDaAOQhFMmpvAOw==}
engines: {node: '>=18'}
peerDependencies:
react: ^16.14.0 || 17.x || 18.x || 19.x
@@ -3600,42 +3592,42 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
- '@storybook/addon-docs@10.3.5':
- resolution: {integrity: sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==}
+ '@storybook/addon-docs@10.3.6':
+ resolution: {integrity: sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==}
peerDependencies:
- storybook: ^10.3.5
+ storybook: ^10.3.6
- '@storybook/addon-links@10.3.5':
- resolution: {integrity: sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw==}
+ '@storybook/addon-links@10.3.6':
+ resolution: {integrity: sha512-tv9Xd68qRGBAvEubaxNo3FuFq4GwuMiBriD+gLGuFK0+/u3cnkuA264aoR1v6YCH3sT3er3+MBimuyKM3jLDxg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- storybook: ^10.3.5
+ storybook: ^10.3.6
peerDependenciesMeta:
react:
optional: true
- '@storybook/addon-onboarding@10.3.5':
- resolution: {integrity: sha512-s3/gIy9Tqxji27iclLY+KSk8kGeow1JxXMl1lPLyu8n6XVvv+tFrUPhAvUTs+fVenG6JQEWc0uzpYBdFRWbMtw==}
+ '@storybook/addon-onboarding@10.3.6':
+ resolution: {integrity: sha512-Tys9eOFzCkBygfDWVRa2hpTQI5Y1HQt9ybkJIHdR16GASQ3fhyXVULHVRmGQLEGN+3n84JGvlU8CN9S/OBB1IQ==}
peerDependencies:
- storybook: ^10.3.5
+ storybook: ^10.3.6
- '@storybook/addon-themes@10.3.5':
- resolution: {integrity: sha512-Mv+C7GuZ0MhGRx5C+rv8sCEjgYsDTLBvq68101V0s8Vwh3gKd6W9cbS31HoOeLAiIMiPPZ8C1iWudA3Oumdtlw==}
+ '@storybook/addon-themes@10.3.6':
+ resolution: {integrity: sha512-/6lPU36+nDQzoDJqwy5BrAO0zFHOHDXn6hy/aq2aKF/RTS54+KrA0fdv0sxTssaBo5tjqZ4jKs60hB0iRWKkFA==}
peerDependencies:
- storybook: ^10.3.5
+ storybook: ^10.3.6
- '@storybook/builder-vite@10.3.5':
- resolution: {integrity: sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==}
+ '@storybook/builder-vite@10.3.6':
+ resolution: {integrity: sha512-gpvR/sE4BcrFtmQZ+Ker7zD23oQzoVeqD9nF6cK6yzY+Q0svJXyX2EPmFG4y+EwygD5/vNzDpP84gGMut8VRwg==}
peerDependencies:
- storybook: ^10.3.5
+ storybook: ^10.3.6
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
- '@storybook/csf-plugin@10.3.5':
- resolution: {integrity: sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==}
+ '@storybook/csf-plugin@10.3.6':
+ resolution: {integrity: sha512-9kBf7VRdRqTSIYo+rPtVn5yjYYyK8kP2QhEYx3oiXvfwy4RexmbJnhk/tXa/lNiTqukA1TqaWQ2+5MqF4fu6YQ==}
peerDependencies:
esbuild: 0.27.2
rollup: 4.59.0
- storybook: ^10.3.5
+ storybook: ^10.3.6
vite: '*'
webpack: '*'
peerDependenciesMeta:
@@ -3657,40 +3649,40 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- '@storybook/nextjs-vite@10.3.5':
- resolution: {integrity: sha512-PdgekGAnr4m/xhrvtl+ZVh68vKTfJN/AewxmqxqxSlwk0dO7B+uVGjO79WmEZwIlLvdT+3HIThTEfC1ozfpM7A==}
+ '@storybook/nextjs-vite@10.3.6':
+ resolution: {integrity: sha512-X2ExCrrnq319LkOKM5E2ZpeEDFgeUmPNH/v5SQlYA9e3Jmtzs+esObaWEEtUF5pvoxzeszZGEV9zXNFmYaF9Dg==}
peerDependencies:
next: ^14.1.0 || ^15.0.0 || ^16.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- storybook: ^10.3.5
+ storybook: ^10.3.6
typescript: '*'
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
typescript:
optional: true
- '@storybook/react-dom-shim@10.3.5':
- resolution: {integrity: sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==}
+ '@storybook/react-dom-shim@10.3.6':
+ resolution: {integrity: sha512-/Tu1gPu+Fw+zOnAGmxRmOD30FX3a04LxcTAKflEtdpmtIMVR5bA3qpjy+f5YhoyDCecbXyKmL1OeIU2FIIZHqQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- storybook: ^10.3.5
+ storybook: ^10.3.6
- '@storybook/react-vite@10.3.5':
- resolution: {integrity: sha512-UB5sJHeh26bfd8sNMx2YPGYRYmErIdTRaLOT28m4bykQIa1l9IgVktsYg/geW7KsJU0lXd3oTbnUjLD+enpi3w==}
+ '@storybook/react-vite@10.3.6':
+ resolution: {integrity: sha512-tySQRc+8q7V2NkylQMNJjDV8zXy6tkxb8oDqw/DIhHhI9Xn77MTKVZ8Cihbo5NMm7HYTB6xDKr6wqdSMgdufYQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- storybook: ^10.3.5
+ storybook: ^10.3.6
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
- '@storybook/react@10.3.5':
- resolution: {integrity: sha512-tpLTLaVGoA6fLK3ReyGzZUricq7lyPaV2hLPpj5wqdXLV/LpRtAHClUpNoPDYSBjlnSjL81hMZijbkGC3mA+gw==}
+ '@storybook/react@10.3.6':
+ resolution: {integrity: sha512-oZQZ6xayWe5IdHmFUTL0TL8rX/gpNNh9gWhT2vzW5eeUvlkVG/RBKdsja6Ndrk2s1D9vcnwiI6r6CNXy3IEEmg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- storybook: ^10.3.5
+ storybook: ^10.3.6
typescript: '>= 4.9.x'
peerDependenciesMeta:
typescript:
@@ -3896,8 +3888,8 @@ packages:
engines: {node: '>=18'}
hasBin: true
- '@tanstack/eslint-plugin-query@5.100.6':
- resolution: {integrity: sha512-dZ2cUFe4OTTf2hLWa7la8oyj7AivK7JDecCDhUnxdAAedkn1YOL2PDr+IFF93h43zwUG2BvnFXiO59shwijyIg==}
+ '@tanstack/eslint-plugin-query@5.100.9':
+ resolution: {integrity: sha512-3jZwyxAZWSBqI7EXEdw+rktFfX1opMpqn9Lruwz52DEzQdi7kbKnqixjhR3dJ1xFfG05YxV9vsqXGxXqcLAmjA==}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ^5.4.0 || ^6.0.0
@@ -3921,11 +3913,11 @@ packages:
resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==}
engines: {node: '>=18'}
- '@tanstack/query-core@5.100.6':
- resolution: {integrity: sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg==}
+ '@tanstack/query-core@5.100.9':
+ resolution: {integrity: sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==}
- '@tanstack/query-devtools@5.100.6':
- resolution: {integrity: sha512-2SiNwlOiAdTbqktCSmwlXZH8x8mckSbES2O0bdr3qZNhdQl5DCtImZx0S3HGeNHWTIkzTaHx2Isg+bD4M3WRIg==}
+ '@tanstack/query-devtools@5.100.9':
+ resolution: {integrity: sha512-gqiptrTIhbK2PuCaPRHmWXfJG1NGYVFpAr0HqogEqiSBNB5xDz6fmesQt7w4WgMOqOQPnPHJ3ZDMuhDaXvNO8g==}
'@tanstack/react-devtools@0.10.2':
resolution: {integrity: sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==}
@@ -3957,14 +3949,14 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
- '@tanstack/react-query-devtools@5.100.6':
- resolution: {integrity: sha512-sz3ksMKA2t1rx0+Odzb0x1A3pXH/SVf7fzlzd3sKXzwXz8980f5sbOwfQD6+UfTG8G4Y2KaIg9e3sBn+uC4VTg==}
+ '@tanstack/react-query-devtools@5.100.9':
+ resolution: {integrity: sha512-mM3slaVGXJmz+pOLgXdANj75ikgQCyudyl3kmFvm6brI1JyVeY/+IeD17uDHIvZrD8hfoO2sdZ54RFsHdYAuhA==}
peerDependencies:
- '@tanstack/react-query': ^5.100.6
+ '@tanstack/react-query': ^5.100.9
react: ^18 || ^19
- '@tanstack/react-query@5.100.6':
- resolution: {integrity: sha512-uVSrps0PV16Cxmcn2rvL+dUhwTpTUtiRW347AEeYxMZXO2pZe9ja7E24PAMGoQ5u2g89DD8u4QhOviBk+RN8RA==}
+ '@tanstack/react-query@5.100.9':
+ resolution: {integrity: sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==}
peerDependencies:
react: ^18 || ^19
@@ -4028,20 +4020,20 @@ packages:
peerDependencies:
'@testing-library/dom': '>=7.21.4'
- '@tsslint/cli@3.1.0':
- resolution: {integrity: sha512-SbcBbjRyRTXWjuyXccSLjIx2TWdhQFOMPDaqdLbW8dvh/A53pAWkimENKTvEL5gpGc0aulQ2Qt0icd+TxJQiRA==}
+ '@tsslint/cli@3.1.1':
+ resolution: {integrity: sha512-rPtT/TkoRpJx9WXt8gh7K4+d0OHcxEr7qbvruJ9qolqI7Mtnw2UmDTTt+bF4Ya9GeHMSa1BjvQgZkSUR8LcNTQ==}
engines: {node: '>=22.6.0'}
hasBin: true
peerDependencies:
typescript: '*'
- '@tsslint/compat-eslint@3.1.0':
- resolution: {integrity: sha512-1gD9G9WH/KlSW3JVG6ahoMoWUy5GZqhFU2RqBOIpjw2vdDwhF+4v1PC6uMJW6LDem/FVoULiMMF9V8vKiX9Uuw==}
+ '@tsslint/compat-eslint@3.1.1':
+ resolution: {integrity: sha512-JybOXPK15s3khkyP6kGtuPao4Be5Ce13HqhJFwrhDb4rmuEeX9G3Nky1MxWdrndMQmb2rXvoA/nIXDbVtUT2sQ==}
peerDependencies:
typescript: '*'
- '@tsslint/config@3.1.0':
- resolution: {integrity: sha512-FVoIycFczf1mccZrxOpkknciSpjoWD1o5Yw5CVvsddXz4/A5q6RbWU/QD7kcXpm8Yukw+3eMSKb1/NNKZ0Mmzw==}
+ '@tsslint/config@3.1.1':
+ resolution: {integrity: sha512-yoRl7wkh/b/uo92mhYdcKbHBMztRfiXmwFMBb9FWyymBidmM/ie30RFYrfJXa8HU0gum42e83/GiTGh0lR0jJA==}
engines: {node: '>=22.6.0'}
hasBin: true
peerDependencies:
@@ -4053,12 +4045,12 @@ packages:
tsl:
optional: true
- '@tsslint/core@3.1.0':
- resolution: {integrity: sha512-WO9sL4nfYme+3u27DLMHG/abooGMTJb54b8CVS8YSJj6ldMax764mHpiaNHf48jGjWN/pbAoC47pnErwFL022w==}
+ '@tsslint/core@3.1.1':
+ resolution: {integrity: sha512-kxmh5udE8uZ9ttw5eHtHcjAQR6aBNw0XZfZr5jpy5hCKKwEY/OLD4fDMyCuUAz6Z2waMygrJH2svTzZQst//pw==}
engines: {node: '>=22.6.0'}
- '@tsslint/types@3.1.0':
- resolution: {integrity: sha512-y21o32pnDktikkRAfYCHu3bYplxyMo8dwhbBVgSyvFXJfvnc0S7K4c6gZypqSWYPzBRXHQ+sQnJ8oM9xGcwWUw==}
+ '@tsslint/types@3.1.1':
+ resolution: {integrity: sha512-kAiO1i4jsMbpBVGnReCbrcNbyGzx79rpN5B6UXPOBxF7RMUOjPy9njNL3XmQNRtqb09eHWu3w5W0DRJ8xdu0Qw==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -4272,210 +4264,115 @@ packages:
'@types/zen-observable@0.8.3':
resolution: {integrity: sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==}
- '@typescript-eslint/eslint-plugin@8.58.2':
- resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==}
+ '@typescript-eslint/eslint-plugin@8.59.2':
+ resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.58.2
+ '@typescript-eslint/parser': ^8.59.2
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/eslint-plugin@8.59.1':
- resolution: {integrity: sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- '@typescript-eslint/parser': ^8.59.1
- eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/parser@8.58.2':
- resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==}
+ '@typescript-eslint/parser@8.59.2':
+ resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/parser@8.59.1':
- resolution: {integrity: sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==}
+ '@typescript-eslint/project-service@8.59.2':
+ resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/scope-manager@8.59.2':
+ resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.59.2':
+ resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/type-utils@8.59.2':
+ resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/project-service@8.58.2':
- resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==}
+ '@typescript-eslint/types@8.59.2':
+ resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.59.2':
+ resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/project-service@8.59.0':
- resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/project-service@8.59.1':
- resolution: {integrity: sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/scope-manager@8.58.2':
- resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/scope-manager@8.59.0':
- resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/scope-manager@8.59.1':
- resolution: {integrity: sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/tsconfig-utils@8.58.2':
- resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/tsconfig-utils@8.59.0':
- resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/tsconfig-utils@8.59.1':
- resolution: {integrity: sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/type-utils@8.58.2':
- resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==}
+ '@typescript-eslint/utils@8.59.2':
+ resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/type-utils@8.59.1':
- resolution: {integrity: sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/types@8.58.2':
- resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==}
+ '@typescript-eslint/visitor-keys@8.59.2':
+ resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/types@8.59.0':
- resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/types@8.59.1':
- resolution: {integrity: sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/typescript-estree@8.58.2':
- resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/typescript-estree@8.59.0':
- resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/typescript-estree@8.59.1':
- resolution: {integrity: sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/utils@8.58.2':
- resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/utils@8.59.0':
- resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/utils@8.59.1':
- resolution: {integrity: sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
- typescript: '>=4.8.4 <6.1.0'
-
- '@typescript-eslint/visitor-keys@8.58.2':
- resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/visitor-keys@8.59.0':
- resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/visitor-keys@8.59.1':
- resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260428.1':
- resolution: {integrity: sha512-Lll6WmXfgTEj1G3QBIoHlabQwUtJiyhlRgSLksa06QFL5BoA7V+Lu1waa9PtPNZbGsXLDMHodtk/bRQABKuPiw==}
+ '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260505.1':
+ resolution: {integrity: sha512-5W94O493huwcjrAkuP9yTQVPosXjX/0fEjCZsDn2D59m7VuPLy78R9D2i3UwlnajC75ubFiLcp/sh5o6/dFZVg==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [darwin]
- '@typescript/native-preview-darwin-x64@7.0.0-dev.20260428.1':
- resolution: {integrity: sha512-WbsBNSHlo+4sGrTxDWdmI7r8x48tCtSCuKdmK62FvVOq58UWAs6sL13Z4Rev4ohLcGHdXC5E/8AIdpLPqDYQpw==}
+ '@typescript/native-preview-darwin-x64@7.0.0-dev.20260505.1':
+ resolution: {integrity: sha512-j+N/276dONuTv2mOLgZy/jLsEZ2JLrxbZ8wBS/LIsMGtvp6elaN/ZESEntpUpIUbeoc5H6nHkjicJKNxQTZ90Q==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [darwin]
- '@typescript/native-preview-linux-arm64@7.0.0-dev.20260428.1':
- resolution: {integrity: sha512-cgcBX/ZBMdepkamLT8g8jQdHe7DZS/s6zTZRof6mvcrnJHlMeUnKoC9UO8/c22IrUMV3n0XPh7R8FYjUP0ll+Q==}
+ '@typescript/native-preview-linux-arm64@7.0.0-dev.20260505.1':
+ resolution: {integrity: sha512-pP/LpkknUTeyQkIiC916BpW2R4ToXDZI7zTbkG6Llh5bGTPcTbtM/5SxXSzYH04ogrc5AP6yYRZsUxtv1GGeQA==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [linux]
- '@typescript/native-preview-linux-arm@7.0.0-dev.20260428.1':
- resolution: {integrity: sha512-/d/NnZFvEJU67L5mHh+cO3gsfwNCvJ9HGtxGq1KGz1VwTabOIcwLdpTpfsAR39WXzzfh9GJHL28n6GSGZInPow==}
+ '@typescript/native-preview-linux-arm@7.0.0-dev.20260505.1':
+ resolution: {integrity: sha512-Vo7nGP0Wbs+VafCMabS4pSDcfJj60fLAmuZ2+hfdsUMFMO0BzHIUFyKBhbaeKVgO5V0yAqvBKrWkovZy0YXxGA==}
engines: {node: '>=16.20.0'}
cpu: [arm]
os: [linux]
- '@typescript/native-preview-linux-x64@7.0.0-dev.20260428.1':
- resolution: {integrity: sha512-4gJCE7wzenx1BH2Vtx2uKWUo8rFxnhGkxNEH1zxbYy/6ASwo+PnOPYmKHAzNE1C3yB5lzw71/vR5p5zyO57Y4A==}
+ '@typescript/native-preview-linux-x64@7.0.0-dev.20260505.1':
+ resolution: {integrity: sha512-90Bpi2xCPCE3S/pcL5uXn793AKSf8qLVvQ+w87FpwKknHYXQqOQ38KBO9jX2lynoxr8YcVO1S8BS7PngkwicYg==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [linux]
- '@typescript/native-preview-win32-arm64@7.0.0-dev.20260428.1':
- resolution: {integrity: sha512-yn6Rzbn62L4QTWrp0QgG8al6l/VG7PCPRdbE0vuGDSlKhInlC+Flo4QSc1qA8KHTbpHgl+nEsq9DymiitI4G4g==}
+ '@typescript/native-preview-win32-arm64@7.0.0-dev.20260505.1':
+ resolution: {integrity: sha512-VkNazv418LbiI0X6SQPCqVFTiBBvCrIxGkdVD7WBO/M3WHZam4qhK8fF61uQclK2NqYPClI2hPbuR5i8+4s4cg==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [win32]
- '@typescript/native-preview-win32-x64@7.0.0-dev.20260428.1':
- resolution: {integrity: sha512-T9z13mcMowXmwGjprA2FIR2EEdYZxgqH8+qk7dFZVBlo5vfk41AN/qJfAdN7IsAhEb640MJ8cMN/aiczweZKmA==}
+ '@typescript/native-preview-win32-x64@7.0.0-dev.20260505.1':
+ resolution: {integrity: sha512-QhueS4Y0hxYnkQoXrAmB0JKpnXn18nNJwqxLSpyEHCEr+XnggiHBNfjT+p1LeG42TEn0w+skcfwc/Mkmk/gyCg==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [win32]
- '@typescript/native-preview@7.0.0-dev.20260428.1':
- resolution: {integrity: sha512-JiM4PYWDGs57TT0mV2KArmaW7BnTkk3XRid79NdG17tfvDbRyg4hBCpKI7vARiQPtxjKrHlxyzxOGDpv5W5T7Q==}
+ '@typescript/native-preview@7.0.0-dev.20260505.1':
+ resolution: {integrity: sha512-o82qX7L97dwQMpj6DzzokF6SQlChcxduNaL4OWzJhJkz1EP//gZOa0/xNPbPLufoJojHLQcANnpkA4JDXZDFhQ==}
engines: {node: '>=16.20.0'}
hasBin: true
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@unpic/core@1.0.3':
resolution: {integrity: sha512-aum9YNVUGso7MjGLD0Rp/08kywCGLqZ03/q6VQBFFakDBOXWEc8D4kPGcZ8v5wEnGRex3lE+++bOuucBp3KJ/w==}
@@ -5479,8 +5376,8 @@ packages:
resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==}
engines: {node: '>=20'}
- dompurify@3.4.1:
- resolution: {integrity: sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==}
+ dompurify@3.4.2:
+ resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -5582,8 +5479,8 @@ packages:
es-module-lexer@2.0.0:
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
- es-toolkit@1.46.0:
- resolution: {integrity: sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==}
+ es-toolkit@1.46.1:
+ resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==}
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -5640,8 +5537,8 @@ packages:
'@eslint/json':
optional: true
- eslint-markdown@0.7.0:
- resolution: {integrity: sha512-cqnr3BWOC7EdexODdtuKIZ4Sbot78x1PZUrdIREp1v25PXgAhz+GRyZjxhSczLnEnf/oj49IyoiBjGAmcLNCQA==}
+ eslint-markdown@0.8.0:
+ resolution: {integrity: sha512-wK1cLadFEfJAl6Hb0f8EH4Q5egWXe0uAstGZ4HPeZBD6RUCXWd4cXJtrSrLLd+WiGDkhjRL73/u7RjVkJ+MmjA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
peerDependencies:
eslint: ^9.31.0 || ^10.0.0-rc.0
@@ -5798,11 +5695,11 @@ packages:
peerDependencies:
eslint: ^8.0.0 || ^9.0.0 || ^10.0.0
- eslint-plugin-storybook@10.3.5:
- resolution: {integrity: sha512-rEFkfU3ypF44GpB4tiJ9EFDItueoGvGi3+weLHZax2ON2MB7VIDsxdSUGvIU5tMURg+oWYlpzCyLm4TpDq2deA==}
+ eslint-plugin-storybook@10.3.6:
+ resolution: {integrity: sha512-8udrL+Rmp5LFaZvgRe4J226X1MYls25bWCyHuzR5X8s2qbFTryX+wKC+o/0Ato4A1AvwnDg8OOMPc6yWJ9JpcA==}
peerDependencies:
eslint: '>=8'
- storybook: ^10.3.5
+ storybook: ^10.3.6
eslint-plugin-toml@1.3.1:
resolution: {integrity: sha512-1l00fBP03HIt9IPV7ZxBi7x0y0NMdEZmakL1jBD6N/FoKBvfKxPw5S8XkmzBecOnFBTn5Z8sNJtL5vdf9cpRMQ==}
@@ -5867,8 +5764,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- eslint@10.2.1:
- resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==}
+ eslint@10.3.0:
+ resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
@@ -6051,6 +5948,10 @@ packages:
functional-red-black-tree@1.0.1:
resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
+ fuse.js@7.2.0:
+ resolution: {integrity: sha512-zf4vdcIGpjNKTuXwug33Hm2okqX6a0t2ZEbez+o9oBJQSNhVJ5AqERfeiRD3r8HcLqP66MrjdkmzxrncbAOTUQ==}
+ engines: {node: '>=10'}
+
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -6187,8 +6088,8 @@ packages:
resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==}
engines: {node: '>=6'}
- hono@4.12.15:
- resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==}
+ hono@4.12.17:
+ resolution: {integrity: sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==}
engines: {node: '>=16.9.0'}
hosted-git-info@9.0.2:
@@ -6256,8 +6157,8 @@ packages:
engines: {node: '>=16.x'}
hasBin: true
- immer@11.1.4:
- resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
+ immer@11.1.6:
+ resolution: {integrity: sha512-uwrF08UBQfxk49i9WcUeCx045wjB1zXEHNJmbYHPVVspxmjwSeWCoKbB8DEIvs3XkBJV6lcRAyLaWJ2+u3MMCw==}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
@@ -6388,8 +6289,8 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
- jotai@2.19.1:
- resolution: {integrity: sha512-sqm9lVZiqBHZH8aSRk32DSiZDHY3yUIlulXYn9GQj7/LvoUdYXSMti7ZPJGo+6zjzKFt5a25k/I6iBCi43PJcw==}
+ jotai@2.20.0:
+ resolution: {integrity: sha512-b5GAqgmXmXzB4WPaTH26ppk9Sl7AA9WSQX7yfdM+gJ1rFROiWcVbi97gFuN/yVCojOcbcvop2sfLL+fjxW0JVg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@babel/core': '>=7.0.0'
@@ -6480,8 +6381,8 @@ packages:
khroma@2.1.0:
resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
- knip@6.7.0:
- resolution: {integrity: sha512-ckL51NDH1YJxnv1kNB0iUdDngB4f/e9Igz8uIqYfmNDoyOFmmk1V0WFv3LQ7/hzC63b2Z9X41gGUE9eOWrZpaA==}
+ knip@6.11.0:
+ resolution: {integrity: sha512-84PTlN8Q5smLpTbzs8smTVh8PMbTDXtw0tFksXq/m6auGFC/KSzJykKFmnYh3As38kiWDkoDBvdTTyKk5M1TAQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -6633,8 +6534,8 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
- loro-crdt@1.12.0:
- resolution: {integrity: sha512-+QAqhBEQ3VZqQKRYjVZElZKLMgtQoewaT1l+oZUh74WsCNqvNI5hazy5gM35NQvcOkrebskWc15a33LS6WAR7g==}
+ loro-crdt@1.12.1:
+ resolution: {integrity: sha512-iHDGq4RaTHr6CFbXqV91ngKyVlB2NuYXz0q9t0Dedr8sQnbcPNdFs4DaqzDNP1jojuygFvYv12gt407M22OeQg==}
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
@@ -6896,10 +6797,6 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
- minimatch@10.2.4:
- resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
- engines: {node: 18 || 20 || >=22}
-
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
@@ -7093,8 +6990,8 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
- oxc-parser@0.127.0:
- resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==}
+ oxc-parser@0.128.0:
+ resolution: {integrity: sha512-XkOw3eiIxAgQ19WRew/Bq9wc5Ga/guaWIzDBzq80z1PyuDNGvWBpPby9k6YGwV8A8uMw+Nlq3xqlzuDYmUFYUw==}
engines: {node: ^20.19.0 || >=22.12.0}
oxc-resolver@11.19.1:
@@ -7294,8 +7191,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
- postcss@8.5.12:
- resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==}
+ postcss@8.5.14:
+ resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
engines: {node: ^10 || ^12 || >=14}
powershell-utils@0.1.0:
@@ -7410,8 +7307,8 @@ packages:
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
- react-hotkeys-hook@5.2.4:
- resolution: {integrity: sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A==}
+ react-hotkeys-hook@5.3.2:
+ resolution: {integrity: sha512-DDDy9xK6mbTQ6aPlQvIl0dA/a90T/AWml4Rm21JXFDLlRHalIg4/Rv3equUQYs5xPTWq+oEl6RD7mi/nBpU3Uw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
@@ -7838,14 +7735,17 @@ packages:
resolution: {integrity: sha512-9SN0XIjBBXCT6ZXXVnScJN4KP2RyFg6B8sEoFlugVHMANysfaEni4LTWlvUQQ/R0wgZl1Ovt9KBQbzn21kHoZA==}
engines: {node: '>=20.19.0'}
- storybook@10.3.5:
- resolution: {integrity: sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==}
+ storybook@10.3.6:
+ resolution: {integrity: sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==}
hasBin: true
peerDependencies:
prettier: ^2 || ^3
+ vite-plus: ^0.1.15
peerDependenciesMeta:
prettier:
optional: true
+ vite-plus:
+ optional: true
streamdown@2.5.0:
resolution: {integrity: sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==}
@@ -8036,11 +7936,11 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
- tldts-core@7.0.29:
- resolution: {integrity: sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==}
+ tldts-core@7.0.30:
+ resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==}
- tldts@7.0.29:
- resolution: {integrity: sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==}
+ tldts@7.0.30:
+ resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==}
hasBin: true
to-regex-range@5.0.1:
@@ -8327,8 +8227,8 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
- vinext@0.0.45:
- resolution: {integrity: sha512-iXXRR5IMO5bZHgN9xEIzwt/+jushkoJgmWNenR++x6Tw1XnJGTEY9D5GAKMGewHl+HJ1z2GPO4fpNkT+2UowRA==}
+ vinext@0.0.47:
+ resolution: {integrity: sha512-l/c1eUUYzrEowW/7jeWfpkuWqx1pYNOk2Zr8hzen9FPqgMF83ZJHIdWchCn2xwsJ7Bm/ZJGxZlAJb2Ch9kBL8g==}
engines: {node: '>=22'}
hasBin: true
peerDependencies:
@@ -8583,8 +8483,8 @@ packages:
zen-observable@0.10.0:
resolution: {integrity: sha512-iI3lT0iojZhKwT5DaFy2Ce42n3yFcLdFyOh01G7H0flMY60P8MJuVFEoJoNwXlmAyQ45GrjL6AcZmmlv8A5rbw==}
- zod@4.3.6:
- resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
zrender@6.0.0:
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
@@ -8609,8 +8509,8 @@ packages:
react:
optional: true
- zustand@5.0.12:
- resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
+ zustand@5.0.13:
+ resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
@@ -8636,10 +8536,10 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
- '@amplitude/analytics-browser@2.42.0':
+ '@amplitude/analytics-browser@2.42.1':
dependencies:
- '@amplitude/analytics-core': 2.48.0
- '@amplitude/plugin-autocapture-browser': 1.27.0
+ '@amplitude/analytics-core': 2.48.1
+ '@amplitude/plugin-autocapture-browser': 1.27.1
'@amplitude/plugin-custom-enrichment-browser': 0.1.8
'@amplitude/plugin-event-property-attribution-browser': 0.2.0
'@amplitude/plugin-network-capture-browser': 1.10.0
@@ -8648,13 +8548,6 @@ snapshots:
'@amplitude/plugin-web-vitals-browser': 1.1.32
tslib: 2.8.1
- '@amplitude/analytics-client-common@2.4.46':
- dependencies:
- '@amplitude/analytics-connector': 1.6.4
- '@amplitude/analytics-core': 2.47.1
- '@amplitude/analytics-types': 2.11.1
- tslib: 2.8.1
-
'@amplitude/analytics-client-common@2.4.47':
dependencies:
'@amplitude/analytics-connector': 1.6.4
@@ -8664,7 +8557,7 @@ snapshots:
'@amplitude/analytics-connector@1.6.4': {}
- '@amplitude/analytics-core@2.47.1':
+ '@amplitude/analytics-core@2.48.0':
dependencies:
'@amplitude/analytics-connector': 1.6.4
'@types/zen-observable': 0.8.3
@@ -8672,7 +8565,7 @@ snapshots:
tslib: 2.8.1
zen-observable: 0.10.0
- '@amplitude/analytics-core@2.48.0':
+ '@amplitude/analytics-core@2.48.1':
dependencies:
'@amplitude/analytics-connector': 1.6.4
'@types/zen-observable': 0.8.3
@@ -8686,9 +8579,9 @@ snapshots:
dependencies:
js-base64: 3.7.8
- '@amplitude/plugin-autocapture-browser@1.27.0':
+ '@amplitude/plugin-autocapture-browser@1.27.1':
dependencies:
- '@amplitude/analytics-core': 2.48.0
+ '@amplitude/analytics-core': 2.48.1
tslib: 2.8.1
'@amplitude/plugin-custom-enrichment-browser@0.1.8':
@@ -8716,14 +8609,14 @@ snapshots:
'@amplitude/analytics-core': 2.48.0
tslib: 2.8.1
- '@amplitude/plugin-session-replay-browser@1.28.1(@amplitude/rrweb@2.0.0-alpha.37)':
+ '@amplitude/plugin-session-replay-browser@1.29.0(@amplitude/rrweb@2.0.0-alpha.40)':
dependencies:
'@amplitude/analytics-client-common': 2.4.47
- '@amplitude/analytics-core': 2.48.0
+ '@amplitude/analytics-core': 2.48.1
'@amplitude/analytics-types': 2.11.1
- '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37)
- '@amplitude/rrweb-record': 2.0.0-alpha.36
- '@amplitude/session-replay-browser': 1.39.0(@amplitude/rrweb@2.0.0-alpha.37)
+ '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.40(@amplitude/rrweb@2.0.0-alpha.40)
+ '@amplitude/rrweb-record': 2.0.0-alpha.40
+ '@amplitude/session-replay-browser': 1.40.0(@amplitude/rrweb@2.0.0-alpha.40)
idb-keyval: 6.2.2
tslib: 2.8.1
transitivePeerDependencies:
@@ -8736,58 +8629,54 @@ snapshots:
tslib: 2.8.1
web-vitals: 5.1.0
- '@amplitude/rrdom@2.0.0-alpha.37':
+ '@amplitude/rrdom@2.0.0-alpha.40':
dependencies:
- '@amplitude/rrweb-snapshot': 2.0.0-alpha.37
+ '@amplitude/rrweb-snapshot': 2.0.0-alpha.40
- '@amplitude/rrweb-packer@2.0.0-alpha.36':
+ '@amplitude/rrweb-packer@2.0.0-alpha.40':
dependencies:
- '@amplitude/rrweb-types': 2.0.0-alpha.37
+ '@amplitude/rrweb-types': 2.0.0-alpha.40
fflate: 0.4.8
- '@amplitude/rrweb-plugin-console-record@2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37)':
+ '@amplitude/rrweb-plugin-console-record@2.0.0-alpha.40(@amplitude/rrweb@2.0.0-alpha.40)':
dependencies:
- '@amplitude/rrweb': 2.0.0-alpha.37
+ '@amplitude/rrweb': 2.0.0-alpha.40
- '@amplitude/rrweb-record@2.0.0-alpha.36':
+ '@amplitude/rrweb-record@2.0.0-alpha.40':
dependencies:
- '@amplitude/rrweb': 2.0.0-alpha.37
- '@amplitude/rrweb-types': 2.0.0-alpha.37
+ '@amplitude/rrweb': 2.0.0-alpha.40
+ '@amplitude/rrweb-types': 2.0.0-alpha.40
- '@amplitude/rrweb-snapshot@2.0.0-alpha.37':
+ '@amplitude/rrweb-snapshot@2.0.0-alpha.40':
dependencies:
- postcss: 8.5.12
+ postcss: 8.5.14
- '@amplitude/rrweb-types@2.0.0-alpha.36': {}
+ '@amplitude/rrweb-types@2.0.0-alpha.40': {}
- '@amplitude/rrweb-types@2.0.0-alpha.37': {}
+ '@amplitude/rrweb-utils@2.0.0-alpha.40': {}
- '@amplitude/rrweb-utils@2.0.0-alpha.36': {}
-
- '@amplitude/rrweb-utils@2.0.0-alpha.37': {}
-
- '@amplitude/rrweb@2.0.0-alpha.37':
+ '@amplitude/rrweb@2.0.0-alpha.40':
dependencies:
- '@amplitude/rrdom': 2.0.0-alpha.37
- '@amplitude/rrweb-snapshot': 2.0.0-alpha.37
- '@amplitude/rrweb-types': 2.0.0-alpha.37
- '@amplitude/rrweb-utils': 2.0.0-alpha.37
+ '@amplitude/rrdom': 2.0.0-alpha.40
+ '@amplitude/rrweb-snapshot': 2.0.0-alpha.40
+ '@amplitude/rrweb-types': 2.0.0-alpha.40
+ '@amplitude/rrweb-utils': 2.0.0-alpha.40
'@types/css-font-loading-module': 0.0.7
'@xstate/fsm': 1.6.5
base64-arraybuffer: 1.0.2
mitt: 3.0.1
- '@amplitude/session-replay-browser@1.39.0(@amplitude/rrweb@2.0.0-alpha.37)':
+ '@amplitude/session-replay-browser@1.40.0(@amplitude/rrweb@2.0.0-alpha.40)':
dependencies:
'@amplitude/analytics-client-common': 2.4.47
- '@amplitude/analytics-core': 2.48.0
+ '@amplitude/analytics-core': 2.48.1
'@amplitude/analytics-types': 2.11.1
'@amplitude/experiment-core': 0.7.2
- '@amplitude/rrweb-packer': 2.0.0-alpha.36
- '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37)
- '@amplitude/rrweb-record': 2.0.0-alpha.36
- '@amplitude/rrweb-types': 2.0.0-alpha.36
- '@amplitude/rrweb-utils': 2.0.0-alpha.36
+ '@amplitude/rrweb-packer': 2.0.0-alpha.40
+ '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.40(@amplitude/rrweb@2.0.0-alpha.40)
+ '@amplitude/rrweb-record': 2.0.0-alpha.40
+ '@amplitude/rrweb-types': 2.0.0-alpha.40
+ '@amplitude/rrweb-utils': 2.0.0-alpha.40
'@amplitude/targeting': 0.2.0
'@rollup/plugin-replace': 6.0.3
idb: 8.0.0
@@ -8798,56 +8687,56 @@ snapshots:
'@amplitude/targeting@0.2.0':
dependencies:
- '@amplitude/analytics-client-common': 2.4.46
- '@amplitude/analytics-core': 2.47.1
+ '@amplitude/analytics-client-common': 2.4.47
+ '@amplitude/analytics-core': 2.48.1
'@amplitude/analytics-types': 2.11.1
'@amplitude/experiment-core': 0.7.2
idb: 8.0.0
tslib: 2.8.1
- '@antfu/eslint-config@8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.4)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.59.1(typescript@6.0.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.1(jiti@2.6.1)))(eslint@10.2.1(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)':
+ '@antfu/eslint-config@8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.4)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3))(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)':
dependencies:
'@antfu/install-pkg': 1.1.0
'@clack/prompts': 1.2.0
- '@e18e/eslint-plugin': 0.3.0(eslint@10.2.1(jiti@2.6.1))(oxlint@1.61.0(oxlint-tsgolint@0.22.0))
- '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.2.1(jiti@2.6.1))
+ '@e18e/eslint-plugin': 0.3.0(eslint@10.3.0(jiti@2.6.1))(oxlint@1.61.0(oxlint-tsgolint@0.22.0))
+ '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.3.0(jiti@2.6.1))
'@eslint/markdown': 8.0.1
- '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.1(jiti@2.6.1))
- '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@vitest/eslint-plugin': 1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.2.1(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)
+ '@stylistic/eslint-plugin': 5.10.0(eslint@10.3.0(jiti@2.6.1))
+ '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@vitest/eslint-plugin': 1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.3.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)
ansis: 4.2.0
cac: 7.0.0
- eslint: 10.2.1(jiti@2.6.1)
- eslint-config-flat-gitignore: 2.3.0(eslint@10.2.1(jiti@2.6.1))
+ eslint: 10.3.0(jiti@2.6.1)
+ eslint-config-flat-gitignore: 2.3.0(eslint@10.3.0(jiti@2.6.1))
eslint-flat-config-utils: 3.1.0
- eslint-merge-processors: 2.0.0(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-antfu: 3.2.2(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-command: 3.5.2(@typescript-eslint/typescript-estree@8.59.1(typescript@6.0.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-import-lite: 0.6.0(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-jsdoc: 62.9.0(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-jsonc: 3.1.2(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-n: 17.24.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ eslint-merge-processors: 2.0.0(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-antfu: 3.2.2(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-command: 3.5.2(@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3))(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-import-lite: 0.6.0(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-jsdoc: 62.9.0(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-jsonc: 3.1.2(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-n: 17.24.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
eslint-plugin-no-only-tests: 3.3.0
- eslint-plugin-perfectionist: 5.8.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint-plugin-pnpm: 1.6.0(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-regexp: 3.1.0(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-toml: 1.3.1(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-unicorn: 64.0.0(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))
- eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1)))(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1)))
- eslint-plugin-yml: 3.3.1(eslint@10.2.1(jiti@2.6.1))
- eslint-processor-vue-blocks: 2.0.0(eslint@10.2.1(jiti@2.6.1))
+ eslint-plugin-perfectionist: 5.8.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint-plugin-pnpm: 1.6.0(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-regexp: 3.1.0(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-toml: 1.3.1(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-unicorn: 64.0.0(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))
+ eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.6.1)))(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.6.1)))
+ eslint-plugin-yml: 3.3.1(eslint@10.3.0(jiti@2.6.1))
+ eslint-processor-vue-blocks: 2.0.0(eslint@10.3.0(jiti@2.6.1))
globals: 17.5.0
local-pkg: 1.1.2
parse-gitignore: 2.0.0
toml-eslint-parser: 1.0.3
- vue-eslint-parser: 10.4.0(eslint@10.2.1(jiti@2.6.1))
+ vue-eslint-parser: 10.4.0(eslint@10.3.0(jiti@2.6.1))
yaml-eslint-parser: 2.0.0
optionalDependencies:
- '@eslint-react/eslint-plugin': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/eslint-plugin': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
'@next/eslint-plugin-next': 16.2.4
- eslint-plugin-react-refresh: 0.5.2(eslint@10.2.1(jiti@2.6.1))
+ eslint-plugin-react-refresh: 0.5.2(eslint@10.3.0(jiti@2.6.1))
transitivePeerDependencies:
- '@arethetypeswrong/core'
- '@edge-runtime/vm'
@@ -9038,13 +8927,13 @@ snapshots:
'@chevrotain/utils@11.1.2': {}
- '@chromatic-com/storybook@5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@chromatic-com/storybook@5.1.2(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))':
dependencies:
'@neoconfetti/react': 1.0.0
chromatic: 13.3.5
filesize: 10.1.6
jsonfile: 6.2.0
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
strip-ansi: 7.2.0
transitivePeerDependencies:
- '@chromatic-com/cypress'
@@ -9214,11 +9103,11 @@ snapshots:
'@cucumber/tag-expressions@9.1.0': {}
- '@e18e/eslint-plugin@0.3.0(eslint@10.2.1(jiti@2.6.1))(oxlint@1.61.0(oxlint-tsgolint@0.22.0))':
+ '@e18e/eslint-plugin@0.3.0(eslint@10.3.0(jiti@2.6.1))(oxlint@1.61.0(oxlint-tsgolint@0.22.0))':
dependencies:
- eslint-plugin-depend: 1.5.0(eslint@10.2.1(jiti@2.6.1))
+ eslint-plugin-depend: 1.5.0(eslint@10.3.0(jiti@2.6.1))
optionalDependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
oxlint: 1.61.0(oxlint-tsgolint@0.22.0)
'@egoist/tailwindcss-icons@1.9.2(tailwindcss@4.2.4)':
@@ -9226,18 +9115,13 @@ snapshots:
'@iconify/utils': 3.1.0
tailwindcss: 4.2.4
- '@emnapi/core@1.9.2':
+ '@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.9.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
- '@emnapi/runtime@1.9.2':
+ '@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
@@ -9252,7 +9136,7 @@ snapshots:
'@es-joy/jsdoccomment@0.84.0':
dependencies:
'@types/estree': 1.0.8
- '@typescript-eslint/types': 8.59.0
+ '@typescript-eslint/types': 8.59.2
comment-parser: 1.4.5
esquery: 1.7.0
jsdoc-type-pratt-parser: 7.1.1
@@ -9260,7 +9144,7 @@ snapshots:
'@es-joy/jsdoccomment@0.86.0':
dependencies:
'@types/estree': 1.0.8
- '@typescript-eslint/types': 8.59.0
+ '@typescript-eslint/types': 8.59.2
comment-parser: 1.4.6
esquery: 1.7.0
jsdoc-type-pratt-parser: 7.2.0
@@ -9345,103 +9229,99 @@ snapshots:
'@esbuild/win32-x64@0.27.2':
optional: true
- '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.2.1(jiti@2.6.1))':
+ '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.3.0(jiti@2.6.1))':
dependencies:
escape-string-regexp: 4.0.0
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
ignore: 7.0.5
- '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(jiti@2.6.1))':
+ '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0(jiti@2.6.1))':
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
- '@eslint-react/ast@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@eslint-react/ast@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/types': 8.59.0
- '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3)
- '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
string-ts: 2.3.1
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@eslint-react/core@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@eslint-react/core@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
- '@eslint-react/ast': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/shared': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/var': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.59.0
- '@typescript-eslint/types': 8.59.0
- '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@eslint-react/ast': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/shared': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/var': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
ts-pattern: 5.9.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@eslint-react/eslint-plugin@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@eslint-react/eslint-plugin@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
- '@eslint-react/shared': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
- eslint-plugin-react-dom: 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint-plugin-react-naming-convention: 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint-plugin-react-rsc: 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint-plugin-react-web-api: 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint-plugin-react-x: 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/shared': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
+ eslint-plugin-react-dom: 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint-plugin-react-naming-convention: 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint-plugin-react-rsc: 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint-plugin-react-web-api: 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint-plugin-react-x: 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@eslint-react/shared@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@eslint-react/shared@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
ts-pattern: 5.9.0
typescript: 6.0.3
- zod: 4.3.6
+ zod: 4.4.3
transitivePeerDependencies:
- supports-color
- '@eslint-react/var@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@eslint-react/var@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
- '@eslint-react/ast': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/shared': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.59.0
- '@typescript-eslint/types': 8.59.0
- '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@eslint-react/ast': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/shared': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
ts-pattern: 5.9.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@eslint/compat@2.0.3(eslint@10.2.1(jiti@2.6.1))':
+ '@eslint/compat@2.0.3(eslint@10.3.0(jiti@2.6.1))':
dependencies:
- '@eslint/core': 1.2.0
+ '@eslint/core': 1.2.1
optionalDependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
'@eslint/config-array@0.23.5':
dependencies:
'@eslint/object-schema': 3.0.5
debug: 4.4.3(supports-color@8.1.1)
- minimatch: 10.2.4
+ minimatch: 10.2.5
transitivePeerDependencies:
- supports-color
- '@eslint/config-helpers@0.5.4':
- dependencies:
- '@eslint/core': 1.2.0
-
'@eslint/config-helpers@0.5.5':
dependencies:
'@eslint/core': 1.2.1
@@ -9450,10 +9330,6 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
- '@eslint/core@1.2.0':
- dependencies:
- '@types/json-schema': 7.0.15
-
'@eslint/core@1.2.1':
dependencies:
'@types/json-schema': 7.0.15
@@ -9463,9 +9339,9 @@ snapshots:
mdn-data: 2.27.1
source-map-js: 1.2.1
- '@eslint/js@10.0.1(eslint@10.2.1(jiti@2.6.1))':
+ '@eslint/js@10.0.1(eslint@10.3.0(jiti@2.6.1))':
optionalDependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
'@eslint/markdown@7.5.1':
dependencies:
@@ -9483,7 +9359,7 @@ snapshots:
'@eslint/markdown@8.0.1':
dependencies:
- '@eslint/core': 1.2.0
+ '@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.6.1
github-slugger: 2.0.0
mdast-util-from-markdown: 2.0.3
@@ -9506,7 +9382,7 @@ snapshots:
'@eslint/plugin-kit@0.6.1':
dependencies:
- '@eslint/core': 1.2.0
+ '@eslint/core': 1.2.1
levn: 0.4.1
'@eslint/plugin-kit@0.7.1':
@@ -9547,11 +9423,11 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
- '@formatjs/fast-memoize@3.1.2': {}
+ '@formatjs/fast-memoize@3.1.4': {}
- '@formatjs/intl-localematcher@0.8.4':
+ '@formatjs/intl-localematcher@0.8.6':
dependencies:
- '@formatjs/fast-memoize': 3.1.2
+ '@formatjs/fast-memoize': 3.1.4
'@headlessui/react@2.2.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
@@ -9576,17 +9452,17 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@hey-api/json-schema-ref-parser@1.4.1':
+ '@hey-api/json-schema-ref-parser@1.4.2':
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
- yaml: 2.8.3
+ js-yaml: 4.1.1
- '@hey-api/openapi-ts@0.97.0(magicast@0.5.2)(typescript@6.0.3)':
+ '@hey-api/openapi-ts@0.97.1(magicast@0.5.2)(typescript@6.0.3)':
dependencies:
'@hey-api/codegen-core': 0.8.1(magicast@0.5.2)
- '@hey-api/json-schema-ref-parser': 1.4.1
- '@hey-api/shared': 0.4.2(magicast@0.5.2)
+ '@hey-api/json-schema-ref-parser': 1.4.2
+ '@hey-api/shared': 0.4.3(magicast@0.5.2)
'@hey-api/spec-types': 0.2.0
'@hey-api/types': 0.1.4
'@lukeed/ms': 2.0.2
@@ -9598,10 +9474,10 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@hey-api/shared@0.4.2(magicast@0.5.2)':
+ '@hey-api/shared@0.4.3(magicast@0.5.2)':
dependencies:
'@hey-api/codegen-core': 0.8.1(magicast@0.5.2)
- '@hey-api/json-schema-ref-parser': 1.4.1
+ '@hey-api/json-schema-ref-parser': 1.4.2
'@hey-api/spec-types': 0.2.0
'@hey-api/types': 0.1.4
ansi-colors: 4.1.3
@@ -9617,9 +9493,9 @@ snapshots:
'@hey-api/types@0.1.4': {}
- '@hono/node-server@2.0.0(hono@4.12.15)':
+ '@hono/node-server@2.0.1(hono@4.12.17)':
dependencies:
- hono: 4.12.15
+ hono: 4.12.17
'@humanfs/core@0.19.1': {}
@@ -9759,7 +9635,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.9.1
+ '@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -10024,10 +9900,10 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
+ '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
- '@emnapi/core': 1.9.2
- '@emnapi/runtime': 1.9.2
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
'@tybys/wasm-util': 0.10.1
optional: true
@@ -10090,136 +9966,138 @@ snapshots:
'@nolyfill/side-channel@1.0.44': {}
- '@orpc/client@1.14.0':
+ '@orpc/client@1.14.1':
dependencies:
- '@orpc/shared': 1.14.0
- '@orpc/standard-server': 1.14.0
- '@orpc/standard-server-fetch': 1.14.0
- '@orpc/standard-server-peer': 1.14.0
+ '@orpc/shared': 1.14.1
+ '@orpc/standard-server': 1.14.1
+ '@orpc/standard-server-fetch': 1.14.1
+ '@orpc/standard-server-peer': 1.14.1
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/contract@1.14.0':
+ '@orpc/contract@1.14.1':
dependencies:
- '@orpc/client': 1.14.0
- '@orpc/shared': 1.14.0
+ '@orpc/client': 1.14.1
+ '@orpc/shared': 1.14.1
'@standard-schema/spec': 1.1.0
openapi-types: 12.1.3
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/openapi-client@1.14.0':
+ '@orpc/openapi-client@1.14.1':
dependencies:
- '@orpc/client': 1.14.0
- '@orpc/contract': 1.14.0
- '@orpc/shared': 1.14.0
- '@orpc/standard-server': 1.14.0
+ '@orpc/client': 1.14.1
+ '@orpc/contract': 1.14.1
+ '@orpc/shared': 1.14.1
+ '@orpc/standard-server': 1.14.1
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/shared@1.14.0':
+ '@orpc/shared@1.14.1':
dependencies:
radash: 12.1.1
type-fest: 5.5.0
- '@orpc/standard-server-fetch@1.14.0':
+ '@orpc/standard-server-fetch@1.14.1':
dependencies:
- '@orpc/shared': 1.14.0
- '@orpc/standard-server': 1.14.0
+ '@orpc/shared': 1.14.1
+ '@orpc/standard-server': 1.14.1
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/standard-server-peer@1.14.0':
+ '@orpc/standard-server-peer@1.14.1':
dependencies:
- '@orpc/shared': 1.14.0
- '@orpc/standard-server': 1.14.0
+ '@orpc/shared': 1.14.1
+ '@orpc/standard-server': 1.14.1
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/standard-server@1.14.0':
+ '@orpc/standard-server@1.14.1':
dependencies:
- '@orpc/shared': 1.14.0
+ '@orpc/shared': 1.14.1
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/tanstack-query@1.14.0(@orpc/client@1.14.0)(@tanstack/query-core@5.100.6)':
+ '@orpc/tanstack-query@1.14.1(@orpc/client@1.14.1)(@tanstack/query-core@5.100.9)':
dependencies:
- '@orpc/client': 1.14.0
- '@orpc/shared': 1.14.0
- '@tanstack/query-core': 5.100.6
+ '@orpc/client': 1.14.1
+ '@orpc/shared': 1.14.1
+ '@tanstack/query-core': 5.100.9
transitivePeerDependencies:
- '@opentelemetry/api'
'@ota-meshi/ast-token-store@0.3.0': {}
- '@oxc-parser/binding-android-arm-eabi@0.127.0':
+ '@oxc-parser/binding-android-arm-eabi@0.128.0':
optional: true
- '@oxc-parser/binding-android-arm64@0.127.0':
+ '@oxc-parser/binding-android-arm64@0.128.0':
optional: true
- '@oxc-parser/binding-darwin-arm64@0.127.0':
+ '@oxc-parser/binding-darwin-arm64@0.128.0':
optional: true
- '@oxc-parser/binding-darwin-x64@0.127.0':
+ '@oxc-parser/binding-darwin-x64@0.128.0':
optional: true
- '@oxc-parser/binding-freebsd-x64@0.127.0':
+ '@oxc-parser/binding-freebsd-x64@0.128.0':
optional: true
- '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0':
+ '@oxc-parser/binding-linux-arm-gnueabihf@0.128.0':
optional: true
- '@oxc-parser/binding-linux-arm-musleabihf@0.127.0':
+ '@oxc-parser/binding-linux-arm-musleabihf@0.128.0':
optional: true
- '@oxc-parser/binding-linux-arm64-gnu@0.127.0':
+ '@oxc-parser/binding-linux-arm64-gnu@0.128.0':
optional: true
- '@oxc-parser/binding-linux-arm64-musl@0.127.0':
+ '@oxc-parser/binding-linux-arm64-musl@0.128.0':
optional: true
- '@oxc-parser/binding-linux-ppc64-gnu@0.127.0':
+ '@oxc-parser/binding-linux-ppc64-gnu@0.128.0':
optional: true
- '@oxc-parser/binding-linux-riscv64-gnu@0.127.0':
+ '@oxc-parser/binding-linux-riscv64-gnu@0.128.0':
optional: true
- '@oxc-parser/binding-linux-riscv64-musl@0.127.0':
+ '@oxc-parser/binding-linux-riscv64-musl@0.128.0':
optional: true
- '@oxc-parser/binding-linux-s390x-gnu@0.127.0':
+ '@oxc-parser/binding-linux-s390x-gnu@0.128.0':
optional: true
- '@oxc-parser/binding-linux-x64-gnu@0.127.0':
+ '@oxc-parser/binding-linux-x64-gnu@0.128.0':
optional: true
- '@oxc-parser/binding-linux-x64-musl@0.127.0':
+ '@oxc-parser/binding-linux-x64-musl@0.128.0':
optional: true
- '@oxc-parser/binding-openharmony-arm64@0.127.0':
+ '@oxc-parser/binding-openharmony-arm64@0.128.0':
optional: true
- '@oxc-parser/binding-wasm32-wasi@0.127.0':
+ '@oxc-parser/binding-wasm32-wasi@0.128.0':
dependencies:
- '@emnapi/core': 1.9.2
- '@emnapi/runtime': 1.9.2
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
- '@oxc-parser/binding-win32-arm64-msvc@0.127.0':
+ '@oxc-parser/binding-win32-arm64-msvc@0.128.0':
optional: true
- '@oxc-parser/binding-win32-ia32-msvc@0.127.0':
+ '@oxc-parser/binding-win32-ia32-msvc@0.128.0':
optional: true
- '@oxc-parser/binding-win32-x64-msvc@0.127.0':
+ '@oxc-parser/binding-win32-x64-msvc@0.128.0':
optional: true
'@oxc-project/runtime@0.127.0': {}
'@oxc-project/types@0.127.0': {}
+ '@oxc-project/types@0.128.0': {}
+
'@oxc-resolver/binding-android-arm-eabi@11.19.1':
optional: true
@@ -10268,9 +10146,9 @@ snapshots:
'@oxc-resolver/binding-openharmony-arm64@11.19.1':
optional: true
- '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
+ '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -10635,29 +10513,29 @@ snapshots:
dependencies:
react: 19.2.5
- '@reactflow/background@11.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@reactflow/background@11.3.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
classcat: 5.0.5
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)
+ zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- immer
- '@reactflow/controls@11.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@reactflow/controls@11.2.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
classcat: 5.0.5
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)
+ zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- immer
- '@reactflow/core@11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@reactflow/core@11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@types/d3': 7.4.3
'@types/d3-drag': 3.0.7
@@ -10669,14 +10547,14 @@ snapshots:
d3-zoom: 3.0.0
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)
+ zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- immer
- '@reactflow/minimap@11.7.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@reactflow/minimap@11.7.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@types/d3-selection': 3.0.11
'@types/d3-zoom': 3.0.8
classcat: 5.0.5
@@ -10684,31 +10562,31 @@ snapshots:
d3-zoom: 3.0.0
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)
+ zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- immer
- '@reactflow/node-resizer@2.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@reactflow/node-resizer@2.2.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
classcat: 5.0.5
d3-drag: 3.0.0
d3-selection: 3.0.0
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)
+ zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- immer
- '@reactflow/node-toolbar@1.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ '@reactflow/node-toolbar@1.3.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
classcat: 5.0.5
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)
+ zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)
transitivePeerDependencies:
- '@types/react'
- immer
@@ -10736,38 +10614,38 @@ snapshots:
estree-walker: 2.0.2
picomatch: 4.0.4
- '@sentry-internal/browser-utils@10.50.0':
+ '@sentry-internal/browser-utils@10.51.0':
dependencies:
- '@sentry/core': 10.50.0
+ '@sentry/core': 10.51.0
- '@sentry-internal/feedback@10.50.0':
+ '@sentry-internal/feedback@10.51.0':
dependencies:
- '@sentry/core': 10.50.0
+ '@sentry/core': 10.51.0
- '@sentry-internal/replay-canvas@10.50.0':
+ '@sentry-internal/replay-canvas@10.51.0':
dependencies:
- '@sentry-internal/replay': 10.50.0
- '@sentry/core': 10.50.0
+ '@sentry-internal/replay': 10.51.0
+ '@sentry/core': 10.51.0
- '@sentry-internal/replay@10.50.0':
+ '@sentry-internal/replay@10.51.0':
dependencies:
- '@sentry-internal/browser-utils': 10.50.0
- '@sentry/core': 10.50.0
+ '@sentry-internal/browser-utils': 10.51.0
+ '@sentry/core': 10.51.0
- '@sentry/browser@10.50.0':
+ '@sentry/browser@10.51.0':
dependencies:
- '@sentry-internal/browser-utils': 10.50.0
- '@sentry-internal/feedback': 10.50.0
- '@sentry-internal/replay': 10.50.0
- '@sentry-internal/replay-canvas': 10.50.0
- '@sentry/core': 10.50.0
+ '@sentry-internal/browser-utils': 10.51.0
+ '@sentry-internal/feedback': 10.51.0
+ '@sentry-internal/replay': 10.51.0
+ '@sentry-internal/replay-canvas': 10.51.0
+ '@sentry/core': 10.51.0
- '@sentry/core@10.50.0': {}
+ '@sentry/core@10.51.0': {}
- '@sentry/react@10.50.0(react@19.2.5)':
+ '@sentry/react@10.51.0(react@19.2.5)':
dependencies:
- '@sentry/browser': 10.50.0
- '@sentry/core': 10.50.0
+ '@sentry/browser': 10.51.0
+ '@sentry/core': 10.51.0
react: 19.2.5
'@shikijs/core@4.0.2':
@@ -10857,15 +10735,15 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
- '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@storybook/addon-docs@10.3.6(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))':
dependencies:
'@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5)
- '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@storybook/csf-plugin': 10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
'@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@storybook/react-dom-shim': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
ts-dedent: 2.2.0
transitivePeerDependencies:
- '@types/react'
@@ -10874,26 +10752,26 @@ snapshots:
- vite
- webpack
- '@storybook/addon-links@10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@storybook/addon-links@10.3.6(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))':
dependencies:
'@storybook/global': 5.0.0
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
optionalDependencies:
react: 19.2.5
- '@storybook/addon-onboarding@10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@storybook/addon-onboarding@10.3.6(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))':
dependencies:
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
- '@storybook/addon-themes@10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@storybook/addon-themes@10.3.6(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))':
dependencies:
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
ts-dedent: 2.2.0
- '@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@storybook/builder-vite@10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))':
dependencies:
- '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@storybook/csf-plugin': 10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
ts-dedent: 2.2.0
vite: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)'
transitivePeerDependencies:
@@ -10901,9 +10779,9 @@ snapshots:
- rollup
- webpack
- '@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@storybook/csf-plugin@10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))':
dependencies:
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.27.2
@@ -10916,18 +10794,18 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)':
+ '@storybook/nextjs-vite@10.3.6(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)':
dependencies:
- '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
- '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)
- '@storybook/react-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)
+ '@storybook/builder-vite': 10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
+ '@storybook/react': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)
+ '@storybook/react-vite': 10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)
next: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5)
vite: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)'
- vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)
+ vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)
optionalDependencies:
typescript: 6.0.3
transitivePeerDependencies:
@@ -10938,25 +10816,25 @@ snapshots:
- supports-color
- webpack
- '@storybook/react-dom-shim@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@storybook/react-dom-shim@10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))':
dependencies:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
- '@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)':
+ '@storybook/react-vite@10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(typescript@6.0.3)
'@rollup/pluginutils': 5.3.0
- '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
- '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)
+ '@storybook/builder-vite': 10.3.6(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
+ '@storybook/react': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)
empathic: 2.0.0
magic-string: 0.30.21
react: 19.2.5
react-docgen: 8.0.3
react-dom: 19.2.5(react@19.2.5)
resolve: 1.22.11
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
tsconfig-paths: 4.2.0
vite: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)'
transitivePeerDependencies:
@@ -10966,15 +10844,15 @@ snapshots:
- typescript
- webpack
- '@storybook/react@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)':
+ '@storybook/react@10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3)':
dependencies:
'@storybook/global': 5.0.0
- '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@storybook/react-dom-shim': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))
react: 19.2.5
react-docgen: 8.0.3
react-docgen-typescript: 2.4.0(typescript@6.0.3)
react-dom: 19.2.5(react@19.2.5)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
optionalDependencies:
typescript: 6.0.3
transitivePeerDependencies:
@@ -10989,11 +10867,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1))':
+ '@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.6.1))':
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
- '@typescript-eslint/types': 8.59.0
- eslint: 10.2.1(jiti@2.6.1)
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
+ '@typescript-eslint/types': 8.59.2
+ eslint: 10.3.0(jiti@2.6.1)
eslint-visitor-keys: 4.2.1
espree: 10.4.0
estraverse: 5.3.0
@@ -11009,19 +10887,19 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@t3-oss/env-core@0.13.11(typescript@6.0.3)(valibot@1.3.1(typescript@6.0.3))(zod@4.3.6)':
+ '@t3-oss/env-core@0.13.11(typescript@6.0.3)(valibot@1.3.1(typescript@6.0.3))(zod@4.4.3)':
optionalDependencies:
typescript: 6.0.3
valibot: 1.3.1(typescript@6.0.3)
- zod: 4.3.6
+ zod: 4.4.3
- '@t3-oss/env-nextjs@0.13.11(typescript@6.0.3)(valibot@1.3.1(typescript@6.0.3))(zod@4.3.6)':
+ '@t3-oss/env-nextjs@0.13.11(typescript@6.0.3)(valibot@1.3.1(typescript@6.0.3))(zod@4.4.3)':
dependencies:
- '@t3-oss/env-core': 0.13.11(typescript@6.0.3)(valibot@1.3.1(typescript@6.0.3))(zod@4.3.6)
+ '@t3-oss/env-core': 0.13.11(typescript@6.0.3)(valibot@1.3.1(typescript@6.0.3))(zod@4.4.3)
optionalDependencies:
typescript: 6.0.3
valibot: 1.3.1(typescript@6.0.3)
- zod: 4.3.6
+ zod: 4.4.3
'@tailwindcss/node@4.2.4':
dependencies:
@@ -11089,7 +10967,7 @@ snapshots:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.2.4
'@tailwindcss/oxide': 4.2.4
- postcss: 8.5.12
+ postcss: 8.5.14
tailwindcss: 4.2.4
'@tailwindcss/typography@0.5.19(tailwindcss@4.2.4)':
@@ -11148,10 +11026,10 @@ snapshots:
- csstype
- utf-8-validate
- '@tanstack/eslint-plugin-query@5.100.6(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@tanstack/eslint-plugin-query@5.100.9(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
optionalDependencies:
typescript: 6.0.3
transitivePeerDependencies:
@@ -11185,9 +11063,9 @@ snapshots:
'@tanstack/pacer-lite@0.1.1': {}
- '@tanstack/query-core@5.100.6': {}
+ '@tanstack/query-core@5.100.9': {}
- '@tanstack/query-devtools@5.100.6': {}
+ '@tanstack/query-devtools@5.100.9': {}
'@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
@@ -11228,15 +11106,15 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- '@tanstack/react-query-devtools@5.100.6(@tanstack/react-query@5.100.6(react@19.2.5))(react@19.2.5)':
+ '@tanstack/react-query-devtools@5.100.9(@tanstack/react-query@5.100.9(react@19.2.5))(react@19.2.5)':
dependencies:
- '@tanstack/query-devtools': 5.100.6
- '@tanstack/react-query': 5.100.6(react@19.2.5)
+ '@tanstack/query-devtools': 5.100.9
+ '@tanstack/react-query': 5.100.9(react@19.2.5)
react: 19.2.5
- '@tanstack/react-query@5.100.6(react@19.2.5)':
+ '@tanstack/react-query@5.100.9(react@19.2.5)':
dependencies:
- '@tanstack/query-core': 5.100.6
+ '@tanstack/query-core': 5.100.9
react: 19.2.5
'@tanstack/react-store@0.11.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
@@ -11301,10 +11179,11 @@ snapshots:
dependencies:
'@testing-library/dom': 10.4.1
- '@tsslint/cli@3.1.0(@tsslint/compat-eslint@3.1.0(typescript@6.0.3))(typescript@6.0.3)':
+ '@tsslint/cli@3.1.1(@tsslint/compat-eslint@3.1.1(typescript@6.0.3))(typescript@6.0.3)':
dependencies:
- '@tsslint/config': 3.1.0(@tsslint/compat-eslint@3.1.0(typescript@6.0.3))(typescript@6.0.3)
- '@tsslint/core': 3.1.0
+ '@tsslint/config': 3.1.1(@tsslint/compat-eslint@3.1.1(typescript@6.0.3))(typescript@6.0.3)
+ '@tsslint/core': 3.1.1
+ '@tsslint/types': 3.1.1
'@volar/language-core': 2.4.28
'@volar/language-hub': 0.0.1
'@volar/typescript': 2.4.28
@@ -11314,28 +11193,28 @@ snapshots:
- '@tsslint/compat-eslint'
- tsl
- '@tsslint/compat-eslint@3.1.0(typescript@6.0.3)':
+ '@tsslint/compat-eslint@3.1.1(typescript@6.0.3)':
dependencies:
- '@tsslint/types': 3.1.0
+ '@tsslint/types': 3.1.1
esquery: 1.7.0
typescript: 6.0.3
- '@tsslint/config@3.1.0(@tsslint/compat-eslint@3.1.0(typescript@6.0.3))(typescript@6.0.3)':
+ '@tsslint/config@3.1.1(@tsslint/compat-eslint@3.1.1(typescript@6.0.3))(typescript@6.0.3)':
dependencies:
- '@tsslint/types': 3.1.0
+ '@tsslint/types': 3.1.1
minimatch: 10.2.5
ts-api-utils: 2.5.0(typescript@6.0.3)
optionalDependencies:
- '@tsslint/compat-eslint': 3.1.0(typescript@6.0.3)
+ '@tsslint/compat-eslint': 3.1.1(typescript@6.0.3)
transitivePeerDependencies:
- typescript
- '@tsslint/core@3.1.0':
+ '@tsslint/core@3.1.1':
dependencies:
- '@tsslint/types': 3.1.0
+ '@tsslint/types': 3.1.1
minimatch: 10.2.5
- '@tsslint/types@3.1.0': {}
+ '@tsslint/types@3.1.1': {}
'@tybys/wasm-util@0.10.1':
dependencies:
@@ -11572,15 +11451,15 @@ snapshots:
'@types/zen-observable@0.8.3': {}
- '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.58.2
- eslint: 10.2.1(jiti@2.6.1)
+ '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.59.2
+ eslint: 10.3.0(jiti@2.6.1)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
@@ -11588,136 +11467,56 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
- '@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.59.1
- '@typescript-eslint/type-utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.59.1
- eslint: 10.2.1(jiti@2.6.1)
- ignore: 7.0.5
- natural-compare: 1.4.0
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.59.2
+ debug: 4.4.3(supports-color@8.1.1)
+ eslint: 10.3.0(jiti@2.6.1)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/types': 8.59.2
+ debug: 4.4.3(supports-color@8.1.1)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.59.2':
+ dependencies:
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/visitor-keys': 8.59.2
+
+ '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)':
+ dependencies:
+ typescript: 6.0.3
+
+ '@typescript-eslint/type-utils@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ debug: 4.4.3(supports-color@8.1.1)
+ eslint: 10.3.0(jiti@2.6.1)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
+ '@typescript-eslint/types@8.59.2': {}
+
+ '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.58.2
- debug: 4.4.3(supports-color@8.1.1)
- eslint: 10.2.1(jiti@2.6.1)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/parser@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
- dependencies:
- '@typescript-eslint/scope-manager': 8.59.1
- '@typescript-eslint/types': 8.59.1
- '@typescript-eslint/typescript-estree': 8.59.1(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.59.1
- debug: 4.4.3(supports-color@8.1.1)
- eslint: 10.2.1(jiti@2.6.1)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/project-service@8.58.2(typescript@6.0.3)':
- dependencies:
- '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.3)
- '@typescript-eslint/types': 8.59.0
- debug: 4.4.3(supports-color@8.1.1)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/project-service@8.59.0(typescript@6.0.3)':
- dependencies:
- '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3)
- '@typescript-eslint/types': 8.59.0
- debug: 4.4.3(supports-color@8.1.1)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/project-service@8.59.1(typescript@6.0.3)':
- dependencies:
- '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@6.0.3)
- '@typescript-eslint/types': 8.59.1
- debug: 4.4.3(supports-color@8.1.1)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/scope-manager@8.58.2':
- dependencies:
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/visitor-keys': 8.58.2
-
- '@typescript-eslint/scope-manager@8.59.0':
- dependencies:
- '@typescript-eslint/types': 8.59.0
- '@typescript-eslint/visitor-keys': 8.59.0
-
- '@typescript-eslint/scope-manager@8.59.1':
- dependencies:
- '@typescript-eslint/types': 8.59.1
- '@typescript-eslint/visitor-keys': 8.59.1
-
- '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.3)':
- dependencies:
- typescript: 6.0.3
-
- '@typescript-eslint/tsconfig-utils@8.59.0(typescript@6.0.3)':
- dependencies:
- typescript: 6.0.3
-
- '@typescript-eslint/tsconfig-utils@8.59.1(typescript@6.0.3)':
- dependencies:
- typescript: 6.0.3
-
- '@typescript-eslint/type-utils@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
- dependencies:
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3)
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- debug: 4.4.3(supports-color@8.1.1)
- eslint: 10.2.1(jiti@2.6.1)
- ts-api-utils: 2.5.0(typescript@6.0.3)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/type-utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
- dependencies:
- '@typescript-eslint/types': 8.59.1
- '@typescript-eslint/typescript-estree': 8.59.1(typescript@6.0.3)
- '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- debug: 4.4.3(supports-color@8.1.1)
- eslint: 10.2.1(jiti@2.6.1)
- ts-api-utils: 2.5.0(typescript@6.0.3)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/types@8.58.2': {}
-
- '@typescript-eslint/types@8.59.0': {}
-
- '@typescript-eslint/types@8.59.1': {}
-
- '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.3)':
- dependencies:
- '@typescript-eslint/project-service': 8.58.2(typescript@6.0.3)
- '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.3)
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/visitor-keys': 8.58.2
+ '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/visitor-keys': 8.59.2
debug: 4.4.3(supports-color@8.1.1)
minimatch: 10.2.5
semver: 7.7.4
@@ -11727,114 +11526,52 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/typescript-estree@8.59.0(typescript@6.0.3)':
+ '@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/project-service': 8.59.0(typescript@6.0.3)
- '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3)
- '@typescript-eslint/types': 8.59.0
- '@typescript-eslint/visitor-keys': 8.59.0
- debug: 4.4.3(supports-color@8.1.1)
- minimatch: 10.2.5
- semver: 7.7.4
- tinyglobby: 0.2.16
- ts-api-utils: 2.5.0(typescript@6.0.3)
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/typescript-estree@8.59.1(typescript@6.0.3)':
+ '@typescript-eslint/visitor-keys@8.59.2':
dependencies:
- '@typescript-eslint/project-service': 8.59.1(typescript@6.0.3)
- '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@6.0.3)
- '@typescript-eslint/types': 8.59.1
- '@typescript-eslint/visitor-keys': 8.59.1
- debug: 4.4.3(supports-color@8.1.1)
- minimatch: 10.2.5
- semver: 7.7.4
- tinyglobby: 0.2.16
- ts-api-utils: 2.5.0(typescript@6.0.3)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/utils@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
- '@typescript-eslint/scope-manager': 8.59.0
- '@typescript-eslint/types': 8.59.0
- '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)':
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
- '@typescript-eslint/scope-manager': 8.59.1
- '@typescript-eslint/types': 8.59.1
- '@typescript-eslint/typescript-estree': 8.59.1(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
- typescript: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/visitor-keys@8.58.2':
- dependencies:
- '@typescript-eslint/types': 8.58.2
+ '@typescript-eslint/types': 8.59.2
eslint-visitor-keys: 5.0.1
- '@typescript-eslint/visitor-keys@8.59.0':
- dependencies:
- '@typescript-eslint/types': 8.59.0
- eslint-visitor-keys: 5.0.1
-
- '@typescript-eslint/visitor-keys@8.59.1':
- dependencies:
- '@typescript-eslint/types': 8.59.1
- eslint-visitor-keys: 5.0.1
-
- '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260428.1':
+ '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260505.1':
optional: true
- '@typescript/native-preview-darwin-x64@7.0.0-dev.20260428.1':
+ '@typescript/native-preview-darwin-x64@7.0.0-dev.20260505.1':
optional: true
- '@typescript/native-preview-linux-arm64@7.0.0-dev.20260428.1':
+ '@typescript/native-preview-linux-arm64@7.0.0-dev.20260505.1':
optional: true
- '@typescript/native-preview-linux-arm@7.0.0-dev.20260428.1':
+ '@typescript/native-preview-linux-arm@7.0.0-dev.20260505.1':
optional: true
- '@typescript/native-preview-linux-x64@7.0.0-dev.20260428.1':
+ '@typescript/native-preview-linux-x64@7.0.0-dev.20260505.1':
optional: true
- '@typescript/native-preview-win32-arm64@7.0.0-dev.20260428.1':
+ '@typescript/native-preview-win32-arm64@7.0.0-dev.20260505.1':
optional: true
- '@typescript/native-preview-win32-x64@7.0.0-dev.20260428.1':
+ '@typescript/native-preview-win32-x64@7.0.0-dev.20260505.1':
optional: true
- '@typescript/native-preview@7.0.0-dev.20260428.1':
+ '@typescript/native-preview@7.0.0-dev.20260505.1':
optionalDependencies:
- '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260428.1
- '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260428.1
- '@typescript/native-preview-linux-arm': 7.0.0-dev.20260428.1
- '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260428.1
- '@typescript/native-preview-linux-x64': 7.0.0-dev.20260428.1
- '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260428.1
- '@typescript/native-preview-win32-x64': 7.0.0-dev.20260428.1
+ '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260505.1
+ '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260505.1
+ '@typescript/native-preview-linux-arm': 7.0.0-dev.20260505.1
+ '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260505.1
+ '@typescript/native-preview-linux-x64': 7.0.0-dev.20260505.1
+ '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260505.1
+ '@typescript/native-preview-win32-x64': 7.0.0-dev.20260505.1
'@ungap/structured-clone@1.3.0': {}
@@ -11949,14 +11686,14 @@ snapshots:
- vite
- yaml
- '@vitest/eslint-plugin@1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.2.1(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)':
+ '@vitest/eslint-plugin@1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.3.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.59.0
- '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
vitest: '@voidzero-dev/vite-plus-test@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)'
optionalDependencies:
- '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- '@arethetypeswrong/core'
@@ -12025,7 +11762,7 @@ snapshots:
'@oxc-project/runtime': 0.127.0
'@oxc-project/types': 0.127.0
lightningcss: 1.32.0
- postcss: 8.5.12
+ postcss: 8.5.14
optionalDependencies:
'@types/node': 25.6.0
esbuild: 0.27.2
@@ -12877,7 +12614,7 @@ snapshots:
optionalDependencies:
'@types/trusted-types': 2.0.7
- dompurify@3.4.1:
+ dompurify@3.4.2:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -12975,7 +12712,7 @@ snapshots:
es-module-lexer@2.0.0: {}
- es-toolkit@1.46.0: {}
+ es-toolkit@1.46.1: {}
esast-util-from-estree@2.0.0:
dependencies:
@@ -13030,46 +12767,46 @@ snapshots:
escape-string-regexp@5.0.0: {}
- eslint-compat-utils@0.5.1(eslint@10.2.1(jiti@2.6.1)):
+ eslint-compat-utils@0.5.1(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
semver: 7.7.4
- eslint-config-flat-gitignore@2.3.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-config-flat-gitignore@2.3.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- '@eslint/compat': 2.0.3(eslint@10.2.1(jiti@2.6.1))
- eslint: 10.2.1(jiti@2.6.1)
+ '@eslint/compat': 2.0.3(eslint@10.3.0(jiti@2.6.1))
+ eslint: 10.3.0(jiti@2.6.1)
eslint-flat-config-utils@3.1.0:
dependencies:
- '@eslint/config-helpers': 0.5.4
+ '@eslint/config-helpers': 0.5.5
pathe: 2.0.3
- eslint-json-compat-utils@0.2.3(eslint@10.2.1(jiti@2.6.1))(jsonc-eslint-parser@3.1.0):
+ eslint-json-compat-utils@0.2.3(eslint@10.3.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
esquery: 1.7.0
jsonc-eslint-parser: 3.1.0
- eslint-markdown@0.7.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-markdown@0.8.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
'@eslint/markdown': 7.5.1
micromark-util-normalize-identifier: 2.0.1
parse5: 8.0.1
optionalDependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
transitivePeerDependencies:
- supports-color
- eslint-merge-processors@2.0.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-merge-processors@2.0.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
- eslint-plugin-antfu@3.2.2(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-antfu@3.2.2(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
- eslint-plugin-better-tailwindcss@4.5.0(eslint@10.2.1(jiti@2.6.1))(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tailwindcss@4.2.4)(typescript@6.0.3):
+ eslint-plugin-better-tailwindcss@4.5.0(eslint@10.3.0(jiti@2.6.1))(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(tailwindcss@4.2.4)(typescript@6.0.3):
dependencies:
'@eslint/css-tree': 4.0.1
'@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@6.0.3))
@@ -13081,42 +12818,42 @@ snapshots:
tsconfig-paths-webpack-plugin: 4.2.0
valibot: 1.3.1(typescript@6.0.3)
optionalDependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
oxlint: 1.61.0(oxlint-tsgolint@0.22.0)
transitivePeerDependencies:
- '@eslint/css'
- typescript
- eslint-plugin-command@3.5.2(@typescript-eslint/typescript-estree@8.59.1(typescript@6.0.3))(@typescript-eslint/utils@8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-command@3.5.2(@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3))(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)):
dependencies:
'@es-joy/jsdoccomment': 0.84.0
- '@typescript-eslint/typescript-estree': 8.59.1(typescript@6.0.3)
- '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
- eslint-plugin-depend@1.5.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-depend@1.5.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
empathic: 2.0.0
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
module-replacements: 2.11.0
semver: 7.7.4
- eslint-plugin-es-x@7.8.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-es-x@7.8.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2
- eslint: 10.2.1(jiti@2.6.1)
- eslint-compat-utils: 0.5.1(eslint@10.2.1(jiti@2.6.1))
+ eslint: 10.3.0(jiti@2.6.1)
+ eslint-compat-utils: 0.5.1(eslint@10.3.0(jiti@2.6.1))
- eslint-plugin-hyoban@0.14.1(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-hyoban@0.14.1(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
- eslint-plugin-import-lite@0.6.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-import-lite@0.6.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
- eslint-plugin-jsdoc@62.9.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-jsdoc@62.9.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
'@es-joy/jsdoccomment': 0.86.0
'@es-joy/resolve.exports': 1.2.0
@@ -13124,7 +12861,7 @@ snapshots:
comment-parser: 1.4.6
debug: 4.4.3(supports-color@8.1.1)
escape-string-regexp: 4.0.0
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
espree: 11.2.0
esquery: 1.7.0
html-entities: 2.6.0
@@ -13136,27 +12873,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-jsonc@3.1.2(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-jsonc@3.1.2(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
- '@eslint/core': 1.2.0
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
+ '@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.6.1
'@ota-meshi/ast-token-store': 0.3.0
diff-sequences: 29.6.3
- eslint: 10.2.1(jiti@2.6.1)
- eslint-json-compat-utils: 0.2.3(eslint@10.2.1(jiti@2.6.1))(jsonc-eslint-parser@3.1.0)
+ eslint: 10.3.0(jiti@2.6.1)
+ eslint-json-compat-utils: 0.2.3(eslint@10.3.0(jiti@2.6.1))(jsonc-eslint-parser@3.1.0)
jsonc-eslint-parser: 3.1.0
natural-compare: 1.4.0
synckit: 0.11.12
transitivePeerDependencies:
- '@eslint/json'
- eslint-plugin-markdown-preferences@0.41.1(@eslint/markdown@8.0.1)(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-markdown-preferences@0.41.1(@eslint/markdown@8.0.1)(eslint@10.3.0(jiti@2.6.1)):
dependencies:
'@eslint/markdown': 8.0.1
diff-sequences: 29.6.3
emoji-regex-xs: 2.0.1
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
mdast-util-from-markdown: 2.0.3
mdast-util-frontmatter: 2.0.1
mdast-util-gfm: 3.1.0
@@ -13171,12 +12908,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-n@17.24.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3):
+ eslint-plugin-n@17.24.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
enhanced-resolve: 5.20.1
- eslint: 10.2.1(jiti@2.6.1)
- eslint-plugin-es-x: 7.8.0(eslint@10.2.1(jiti@2.6.1))
+ eslint: 10.3.0(jiti@2.6.1)
+ eslint-plugin-es-x: 7.8.0(eslint@10.3.0(jiti@2.6.1))
get-tsconfig: 4.14.0
globals: 15.15.0
globrex: 0.1.2
@@ -13186,29 +12923,29 @@ snapshots:
transitivePeerDependencies:
- typescript
- eslint-plugin-no-barrel-files@1.3.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3):
+ eslint-plugin-no-barrel-files@1.3.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3):
dependencies:
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
transitivePeerDependencies:
- supports-color
- typescript
eslint-plugin-no-only-tests@3.3.0: {}
- eslint-plugin-perfectionist@5.8.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3):
+ eslint-plugin-perfectionist@5.8.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3):
dependencies:
- '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
natural-orderby: 5.0.0
transitivePeerDependencies:
- supports-color
- typescript
- eslint-plugin-pnpm@1.6.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-pnpm@1.6.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
empathic: 2.0.0
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
jsonc-eslint-parser: 3.1.0
pathe: 2.0.3
pnpm-workspace-yaml: 1.6.0
@@ -13216,87 +12953,87 @@ snapshots:
yaml: 2.8.3
yaml-eslint-parser: 2.0.0
- eslint-plugin-react-dom@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3):
+ eslint-plugin-react-dom@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3):
dependencies:
- '@eslint-react/ast': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/core': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/shared': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/var': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/ast': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/core': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/shared': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/var': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
compare-versions: 6.1.1
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
ts-pattern: 5.9.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-naming-convention@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3):
+ eslint-plugin-react-naming-convention@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3):
dependencies:
- '@eslint-react/ast': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/core': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/shared': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/var': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/ast': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/core': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/shared': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/var': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
compare-versions: 6.1.1
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
string-ts: 2.3.1
ts-pattern: 5.9.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-refresh@0.5.2(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-react-refresh@0.5.2(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
- eslint-plugin-react-rsc@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3):
+ eslint-plugin-react-rsc@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3):
dependencies:
- '@eslint-react/ast': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/shared': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/var': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
+ '@eslint-react/ast': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/shared': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/var': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
ts-pattern: 5.9.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-web-api@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3):
+ eslint-plugin-react-web-api@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3):
dependencies:
- '@eslint-react/ast': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/core': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/shared': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/var': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/ast': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/core': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/shared': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/var': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
birecord: 0.1.1
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
ts-pattern: 5.9.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-x@3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3):
+ eslint-plugin-react-x@3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3):
dependencies:
- '@eslint-react/ast': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/core': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/shared': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@eslint-react/var': 3.0.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.58.2
- '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/types': 8.58.2
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/ast': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/core': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/shared': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@eslint-react/var': 3.0.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.59.2
+ '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/types': 8.59.2
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
compare-versions: 6.1.1
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
string-ts: 2.3.1
ts-api-utils: 2.5.0(typescript@6.0.3)
ts-pattern: 5.9.0
@@ -13304,23 +13041,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-regexp@3.1.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-regexp@3.1.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2
comment-parser: 1.4.6
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
jsdoc-type-pratt-parser: 7.2.0
refa: 0.12.1
regexp-ast-analysis: 0.7.1
scslre: 0.3.0
- eslint-plugin-sonarjs@4.0.3(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-sonarjs@4.0.3(eslint@10.3.0(jiti@2.6.1)):
dependencies:
'@eslint-community/regexpp': 4.12.2
builtin-modules: 3.3.0
bytes: 3.1.2
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
functional-red-black-tree: 1.0.1
globals: 17.5.0
jsx-ast-utils-x: 0.1.0
@@ -13331,35 +13068,35 @@ snapshots:
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
- eslint-plugin-storybook@10.3.5(eslint@10.2.1(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3):
+ eslint-plugin-storybook@10.3.6(eslint@10.3.0(jiti@2.6.1))(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3):
dependencies:
- '@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.2.1(jiti@2.6.1)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
+ eslint: 10.3.0(jiti@2.6.1)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
transitivePeerDependencies:
- supports-color
- typescript
- eslint-plugin-toml@1.3.1(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-toml@1.3.1(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- '@eslint/core': 1.2.0
+ '@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.6.1
'@ota-meshi/ast-token-store': 0.3.0
debug: 4.4.3(supports-color@8.1.1)
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
toml-eslint-parser: 1.0.3
transitivePeerDependencies:
- supports-color
- eslint-plugin-unicorn@64.0.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-unicorn@64.0.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
'@babel/helper-validator-identifier': 7.28.5
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
change-case: 5.4.4
ci-info: 4.4.0
clean-regexp: 1.0.0
core-js-compat: 3.49.0
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
find-up-simple: 1.0.1
globals: 17.5.0
indent-string: 5.0.0
@@ -13371,43 +13108,43 @@ snapshots:
semver: 7.7.4
strip-indent: 4.1.1
- eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
optionalDependencies:
- '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
- eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1)))(@typescript-eslint/parser@8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1))):
+ eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.6.1)))(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.6.1))):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
- eslint: 10.2.1(jiti@2.6.1)
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
+ eslint: 10.3.0(jiti@2.6.1)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.1
semver: 7.7.4
- vue-eslint-parser: 10.4.0(eslint@10.2.1(jiti@2.6.1))
+ vue-eslint-parser: 10.4.0(eslint@10.3.0(jiti@2.6.1))
xml-name-validator: 4.0.0
optionalDependencies:
- '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.1(jiti@2.6.1))
- '@typescript-eslint/parser': 8.58.2(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)
+ '@stylistic/eslint-plugin': 5.10.0(eslint@10.3.0(jiti@2.6.1))
+ '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)
- eslint-plugin-yml@3.3.1(eslint@10.2.1(jiti@2.6.1)):
+ eslint-plugin-yml@3.3.1(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- '@eslint/core': 1.2.0
+ '@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.6.1
'@ota-meshi/ast-token-store': 0.3.0
debug: 4.4.3(supports-color@8.1.1)
diff-sequences: 29.6.3
escape-string-regexp: 5.0.0
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
natural-compare: 1.4.0
yaml-eslint-parser: 2.0.0
transitivePeerDependencies:
- supports-color
- eslint-processor-vue-blocks@2.0.0(eslint@10.2.1(jiti@2.6.1)):
+ eslint-processor-vue-blocks@2.0.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
eslint-scope@9.1.2:
dependencies:
@@ -13422,9 +13159,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
- eslint@10.2.1(jiti@2.6.1):
+ eslint@10.3.0(jiti@2.6.1):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.5
'@eslint/config-helpers': 0.5.5
@@ -13451,7 +13188,7 @@ snapshots:
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
- minimatch: 10.2.4
+ minimatch: 10.2.5
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
@@ -13634,6 +13371,8 @@ snapshots:
functional-red-black-tree@1.0.1: {}
+ fuse.js@7.2.0: {}
+
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
@@ -13866,7 +13605,7 @@ snapshots:
hex-rgb@4.3.0: {}
- hono@4.12.15: {}
+ hono@4.12.17: {}
hosted-git-info@9.0.2:
dependencies:
@@ -13926,7 +13665,7 @@ snapshots:
image-size@2.0.2: {}
- immer@11.1.4: {}
+ immer@11.1.6: {}
imurmurhash@0.1.4: {}
@@ -14021,7 +13760,7 @@ snapshots:
jiti@2.6.1: {}
- jotai@2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.5):
+ jotai@2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.5):
optionalDependencies:
'@babel/core': 7.29.0
'@babel/template': 7.28.6
@@ -14084,22 +13823,22 @@ snapshots:
khroma@2.1.0: {}
- knip@6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2):
+ knip@6.11.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0):
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
formatly: 0.3.0
get-tsconfig: 4.14.0
jiti: 2.6.1
minimist: 1.2.8
- oxc-parser: 0.127.0
- oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
+ oxc-parser: 0.128.0
+ oxc-resolver: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
picomatch: 4.0.4
smol-toml: 1.6.1
strip-json-comments: 5.0.3
tinyglobby: 0.2.16
unbash: 3.0.0
yaml: 2.8.3
- zod: 4.3.6
+ zod: 4.4.3
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -14225,7 +13964,7 @@ snapshots:
dependencies:
js-tokens: 4.0.0
- loro-crdt@1.12.0: {}
+ loro-crdt@1.12.1: {}
loupe@3.2.1: {}
@@ -14807,10 +14546,6 @@ snapshots:
min-indent@1.0.1: {}
- minimatch@10.2.4:
- dependencies:
- brace-expansion: 5.0.5
-
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.5
@@ -14994,32 +14729,32 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
- oxc-parser@0.127.0:
+ oxc-parser@0.128.0:
dependencies:
- '@oxc-project/types': 0.127.0
+ '@oxc-project/types': 0.128.0
optionalDependencies:
- '@oxc-parser/binding-android-arm-eabi': 0.127.0
- '@oxc-parser/binding-android-arm64': 0.127.0
- '@oxc-parser/binding-darwin-arm64': 0.127.0
- '@oxc-parser/binding-darwin-x64': 0.127.0
- '@oxc-parser/binding-freebsd-x64': 0.127.0
- '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0
- '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0
- '@oxc-parser/binding-linux-arm64-gnu': 0.127.0
- '@oxc-parser/binding-linux-arm64-musl': 0.127.0
- '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0
- '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0
- '@oxc-parser/binding-linux-riscv64-musl': 0.127.0
- '@oxc-parser/binding-linux-s390x-gnu': 0.127.0
- '@oxc-parser/binding-linux-x64-gnu': 0.127.0
- '@oxc-parser/binding-linux-x64-musl': 0.127.0
- '@oxc-parser/binding-openharmony-arm64': 0.127.0
- '@oxc-parser/binding-wasm32-wasi': 0.127.0
- '@oxc-parser/binding-win32-arm64-msvc': 0.127.0
- '@oxc-parser/binding-win32-ia32-msvc': 0.127.0
- '@oxc-parser/binding-win32-x64-msvc': 0.127.0
+ '@oxc-parser/binding-android-arm-eabi': 0.128.0
+ '@oxc-parser/binding-android-arm64': 0.128.0
+ '@oxc-parser/binding-darwin-arm64': 0.128.0
+ '@oxc-parser/binding-darwin-x64': 0.128.0
+ '@oxc-parser/binding-freebsd-x64': 0.128.0
+ '@oxc-parser/binding-linux-arm-gnueabihf': 0.128.0
+ '@oxc-parser/binding-linux-arm-musleabihf': 0.128.0
+ '@oxc-parser/binding-linux-arm64-gnu': 0.128.0
+ '@oxc-parser/binding-linux-arm64-musl': 0.128.0
+ '@oxc-parser/binding-linux-ppc64-gnu': 0.128.0
+ '@oxc-parser/binding-linux-riscv64-gnu': 0.128.0
+ '@oxc-parser/binding-linux-riscv64-musl': 0.128.0
+ '@oxc-parser/binding-linux-s390x-gnu': 0.128.0
+ '@oxc-parser/binding-linux-x64-gnu': 0.128.0
+ '@oxc-parser/binding-linux-x64-musl': 0.128.0
+ '@oxc-parser/binding-openharmony-arm64': 0.128.0
+ '@oxc-parser/binding-wasm32-wasi': 0.128.0
+ '@oxc-parser/binding-win32-arm64-msvc': 0.128.0
+ '@oxc-parser/binding-win32-ia32-msvc': 0.128.0
+ '@oxc-parser/binding-win32-x64-msvc': 0.128.0
- oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2):
+ oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0):
optionalDependencies:
'@oxc-resolver/binding-android-arm-eabi': 11.19.1
'@oxc-resolver/binding-android-arm64': 11.19.1
@@ -15037,7 +14772,7 @@ snapshots:
'@oxc-resolver/binding-linux-x64-gnu': 11.19.1
'@oxc-resolver/binding-linux-x64-musl': 11.19.1
'@oxc-resolver/binding-openharmony-arm64': 11.19.1
- '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
+ '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
'@oxc-resolver/binding-win32-arm64-msvc': 11.19.1
'@oxc-resolver/binding-win32-ia32-msvc': 11.19.1
'@oxc-resolver/binding-win32-x64-msvc': 11.19.1
@@ -15276,7 +15011,7 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- postcss@8.5.12:
+ postcss@8.5.14:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -15413,7 +15148,7 @@ snapshots:
react-fast-compare@3.2.2: {}
- react-hotkeys-hook@5.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ react-hotkeys-hook@5.3.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
@@ -15514,14 +15249,14 @@ snapshots:
react@19.2.5: {}
- reactflow@11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ reactflow@11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
- '@reactflow/background': 11.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@reactflow/controls': 11.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@reactflow/minimap': 11.7.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@reactflow/node-resizer': 2.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@reactflow/node-toolbar': 1.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/background': 11.3.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/controls': 11.2.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/minimap': 11.7.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/node-resizer': 2.2.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@reactflow/node-toolbar': 1.3.14(@types/react@19.2.14)(immer@11.1.6)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
transitivePeerDependencies:
@@ -15952,7 +15687,7 @@ snapshots:
std-semver@1.0.8: {}
- storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)):
dependencies:
'@storybook/global': 5.0.0
'@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -15967,6 +15702,8 @@ snapshots:
semver: 7.7.4
use-sync-external-store: 1.6.0(react@19.2.5)
ws: 8.20.0
+ optionalDependencies:
+ vite-plus: 0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)
transitivePeerDependencies:
- '@testing-library/dom'
- bufferutil
@@ -16158,11 +15895,11 @@ snapshots:
tinyspy@4.0.4: {}
- tldts-core@7.0.29: {}
+ tldts-core@7.0.30: {}
- tldts@7.0.29:
+ tldts@7.0.30:
dependencies:
- tldts-core: 7.0.29
+ tldts-core: 7.0.30
to-regex-range@5.0.1:
dependencies:
@@ -16424,11 +16161,12 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
- vinext@0.0.45(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3):
+ vinext@0.0.47(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3):
dependencies:
'@unpic/react': 1.0.2(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@vercel/og': 0.8.6
'@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
+ image-size: 2.0.2
magic-string: 0.30.21
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
@@ -16473,14 +16211,14 @@ snapshots:
- typescript
- ws
- vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3):
+ vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)))(typescript@6.0.3):
dependencies:
'@next/env': 16.0.0
image-size: 2.0.2
magic-string: 0.30.21
module-alias: 2.3.4
next: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite-plus@0.1.20(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))
ts-dedent: 2.2.0
vite: '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)'
vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(typescript@6.0.3)
@@ -16623,10 +16361,10 @@ snapshots:
vscode-uri@3.1.0: {}
- vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1)):
+ vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.6.1)):
dependencies:
debug: 4.4.3(supports-color@8.1.1)
- eslint: 10.2.1(jiti@2.6.1)
+ eslint: 10.3.0(jiti@2.6.1)
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
espree: 11.2.0
@@ -16731,73 +16469,217 @@ snapshots:
zen-observable@0.10.0: {}
- zod@4.3.6: {}
+ zod@4.4.3: {}
zrender@6.0.0:
dependencies:
tslib: 2.3.0
- zundo@2.3.0(zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))):
+ zundo@2.3.0(zustand@5.0.13(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))):
dependencies:
- zustand: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
+ zustand: 5.0.13(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
- zustand@4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5):
+ zustand@4.5.7(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5):
dependencies:
use-sync-external-store: 1.6.0(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
- immer: 11.1.4
+ immer: 11.1.6
react: 19.2.5
- zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)):
+ zustand@5.0.13(@types/react@19.2.14)(immer@11.1.6)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)):
optionalDependencies:
'@types/react': 19.2.14
- immer: 11.1.4
+ immer: 11.1.6
react: 19.2.5
use-sync-external-store: 1.6.0(react@19.2.5)
zwitch@2.0.4: {}
time:
- '@amplitude/analytics-browser@2.42.0': '2026-04-28T17:01:08.442Z'
- '@amplitude/plugin-session-replay-browser@1.28.1': '2026-04-28T17:01:37.145Z'
- '@hey-api/openapi-ts@0.97.0': '2026-04-28T03:33:22.380Z'
- '@hono/node-server@2.0.0': '2026-04-21T00:25:40.852Z'
+ '@amplitude/analytics-browser@2.42.1': '2026-05-05T00:10:57.955Z'
+ '@amplitude/plugin-session-replay-browser@1.29.0': '2026-05-05T00:11:17.733Z'
+ '@antfu/eslint-config@8.2.0': '2026-04-13T00:46:42.037Z'
+ '@base-ui/react@1.4.1': '2026-04-20T12:24:35.520Z'
+ '@chromatic-com/storybook@5.1.2': '2026-04-13T12:24:15.881Z'
+ '@cucumber/cucumber@12.8.2': '2026-04-25T20:32:28.804Z'
+ '@egoist/tailwindcss-icons@1.9.2': '2026-01-31T10:48:44.594Z'
+ '@emoji-mart/data@1.2.1': '2024-04-25T15:36:14.556Z'
+ '@eslint-react/eslint-plugin@3.0.0': '2026-03-15T23:41:40.655Z'
+ '@eslint/js@10.0.1': '2026-02-06T22:34:56.290Z'
+ '@floating-ui/react@0.27.19': '2026-03-03T03:02:09.664Z'
+ '@formatjs/intl-localematcher@0.8.6': '2026-05-05T17:39:39.364Z'
+ '@headlessui/react@2.2.10': '2026-04-07T17:12:43.551Z'
+ '@heroicons/react@2.2.0': '2024-11-18T15:33:27.317Z'
+ '@hey-api/openapi-ts@0.97.1': '2026-05-04T00:37:14.271Z'
+ '@hono/node-server@2.0.1': '2026-04-30T08:51:26.973Z'
+ '@iconify-json/heroicons@1.2.3': '2025-09-20T05:33:02.364Z'
+ '@iconify-json/ri@1.2.10': '2026-02-10T08:41:46.666Z'
'@lexical/link@0.44.0': '2026-04-27T14:47:45.477Z'
'@lexical/list@0.44.0': '2026-04-27T14:47:48.463Z'
'@lexical/react@0.44.0': '2026-04-27T14:48:07.316Z'
'@lexical/selection@0.44.0': '2026-04-27T14:48:15.054Z'
'@lexical/text@0.44.0': '2026-04-27T14:48:23.958Z'
'@lexical/utils@0.44.0': '2026-04-27T14:48:26.689Z'
- '@orpc/contract@1.14.0': '2026-04-22T14:03:55.170Z'
- '@tanstack/eslint-plugin-query@5.100.6': '2026-04-28T16:39:45.129Z'
+ '@mdx-js/loader@3.1.1': '2025-08-29T18:03:05.606Z'
+ '@mdx-js/react@3.1.1': '2025-08-29T18:02:56.462Z'
+ '@mdx-js/rollup@3.1.1': '2025-08-29T18:03:10.680Z'
+ '@monaco-editor/react@4.7.0': '2025-02-13T16:13:41.390Z'
+ '@next/eslint-plugin-next@16.2.4': '2026-04-15T22:33:34.415Z'
+ '@next/mdx@16.2.4': '2026-04-15T22:34:01.259Z'
+ '@orpc/client@1.14.1': '2026-05-04T01:00:05.176Z'
+ '@orpc/contract@1.14.1': '2026-05-04T01:00:11.262Z'
+ '@orpc/openapi-client@1.14.1': '2026-05-04T01:00:59.054Z'
+ '@orpc/tanstack-query@1.14.1': '2026-05-04T01:00:28.713Z'
+ '@playwright/test@1.59.1': '2026-04-01T17:59:00.155Z'
+ '@remixicon/react@4.9.0': '2026-01-29T10:53:18.993Z'
+ '@rgrove/parse-xml@4.2.0': '2024-10-25T03:58:22.145Z'
+ '@sentry/react@10.51.0': '2026-04-29T13:53:06.452Z'
+ '@storybook/addon-docs@10.3.6': '2026-04-29T14:02:41.653Z'
+ '@storybook/addon-links@10.3.6': '2026-04-29T14:02:45.236Z'
+ '@storybook/addon-onboarding@10.3.6': '2026-04-29T14:02:45.520Z'
+ '@storybook/addon-themes@10.3.6': '2026-04-29T14:02:49.188Z'
+ '@storybook/nextjs-vite@10.3.6': '2026-04-29T14:03:05.454Z'
+ '@storybook/react-vite@10.3.6': '2026-04-29T14:03:11.708Z'
+ '@storybook/react@10.3.6': '2026-04-29T14:03:45.907Z'
+ '@streamdown/math@1.0.2': '2026-02-09T17:31:31.085Z'
+ '@svgdotjs/svg.js@3.2.5': '2025-09-15T16:22:12.771Z'
+ '@t3-oss/env-nextjs@0.13.11': '2026-03-22T19:16:09.026Z'
+ '@tailwindcss/postcss@4.2.4': '2026-04-21T13:16:10.817Z'
+ '@tailwindcss/typography@0.5.19': '2025-09-24T14:49:08.735Z'
+ '@tailwindcss/vite@4.2.4': '2026-04-21T13:16:19.142Z'
+ '@tanstack/eslint-plugin-query@5.100.9': '2026-05-03T14:48:37.490Z'
+ '@tanstack/react-devtools@0.10.2': '2026-04-07T19:45:18.567Z'
+ '@tanstack/react-form-devtools@0.2.22': '2026-04-21T16:58:06.537Z'
+ '@tanstack/react-form@1.29.1': '2026-04-21T16:58:06.272Z'
'@tanstack/react-hotkeys@0.10.0': '2026-04-25T12:28:06.989Z'
- '@tanstack/react-query-devtools@5.100.6': '2026-04-28T16:39:51.334Z'
- '@tanstack/react-query@5.100.6': '2026-04-28T16:39:52.105Z'
- '@tsslint/cli@3.1.0': '2026-04-29T04:57:38.423Z'
- '@tsslint/compat-eslint@3.1.0': '2026-04-29T04:57:34.593Z'
- '@tsslint/config@3.1.0': '2026-04-29T04:57:36.446Z'
+ '@tanstack/react-query-devtools@5.100.9': '2026-05-03T14:48:42.250Z'
+ '@tanstack/react-query@5.100.9': '2026-05-03T14:48:42.837Z'
+ '@tanstack/react-virtual@3.13.24': '2026-04-17T11:51:33.949Z'
+ '@testing-library/dom@10.4.1': '2025-07-27T13:23:37.151Z'
+ '@testing-library/jest-dom@6.9.1': '2025-10-01T20:04:22.720Z'
+ '@testing-library/react@16.3.2': '2026-01-19T10:59:08.185Z'
+ '@testing-library/user-event@14.6.1': '2025-01-21T17:35:55.574Z'
+ '@tsslint/cli@3.1.1': '2026-05-03T21:19:35.929Z'
+ '@tsslint/compat-eslint@3.1.1': '2026-05-03T21:19:32.302Z'
+ '@tsslint/config@3.1.1': '2026-05-03T21:19:34.100Z'
+ '@types/js-cookie@3.0.6': '2023-11-07T08:41:16.889Z'
'@types/js-yaml@4.0.9': '2023-11-07T20:20:13.264Z'
+ '@types/negotiator@0.6.4': '2025-06-07T02:18:17.532Z'
'@types/node@25.6.0': '2026-04-10T03:39:59.421Z'
- '@typescript-eslint/eslint-plugin@8.59.1': '2026-04-27T17:31:50.020Z'
- '@typescript-eslint/parser@8.59.1': '2026-04-27T17:31:29.147Z'
- '@typescript/native-preview@7.0.0-dev.20260428.1': '2026-04-28T08:09:51.266Z'
- '@voidzero-dev/vite-plus-core@0.1.20': '2026-04-29T03:08:39.629Z'
- '@voidzero-dev/vite-plus-test@0.1.20': '2026-04-29T03:08:45.501Z'
+ '@types/qs@6.15.0': '2026-03-06T02:55:59.542Z'
+ '@types/react-dom@19.2.3': '2025-11-12T04:37:39.524Z'
+ '@types/react@19.2.14': '2026-02-11T11:44:58.515Z'
+ '@types/sortablejs@1.15.9': '2025-10-24T04:31:45.132Z'
+ '@typescript-eslint/eslint-plugin@8.59.2': '2026-05-04T17:33:26.933Z'
+ '@typescript-eslint/parser@8.59.2': '2026-05-04T17:33:05.896Z'
+ '@typescript/native-preview@7.0.0-dev.20260505.1': '2026-05-05T08:01:24.093Z'
+ '@vitejs/plugin-react@6.0.1': '2026-03-13T10:43:19.598Z'
+ '@vitejs/plugin-rsc@0.5.25': '2026-04-27T03:32:20.729Z'
+ '@vitest/coverage-v8@4.1.5': '2026-04-21T11:04:22.099Z'
+ abcjs@6.6.3: '2026-04-24T17:38:01.079Z'
+ agentation@3.0.2: '2026-03-25T16:24:19.682Z'
+ ahooks@3.9.7: '2026-03-23T15:49:13.605Z'
c12@1.10.0: '2024-03-06T13:11:04.381Z'
+ class-variance-authority@0.7.1: '2024-11-26T08:20:34.604Z'
+ client-only@0.0.1: '2022-09-03T01:07:11.981Z'
+ clsx@2.1.1: '2024-04-23T05:26:04.645Z'
+ cmdk@1.1.1: '2025-03-14T19:21:16.194Z'
+ code-inspector-plugin@1.5.1: '2026-04-03T03:44:06.420Z'
concurrently@9.2.1: '2025-08-25T09:50:49.138Z'
copy-to-clipboard@4.0.2: '2026-04-24T22:15:18.933Z'
- eslint-markdown@0.7.0: '2026-04-25T11:31:20.226Z'
+ cron-parser@5.5.0: '2026-01-16T13:14:50.225Z'
+ dayjs@1.11.20: '2026-03-12T11:30:39.315Z'
+ decimal.js@10.6.0: '2025-07-06T22:50:38.844Z'
+ dompurify@3.4.2: '2026-04-30T15:45:30.615Z'
+ echarts-for-react@3.0.6: '2026-01-21T04:38:21.243Z'
+ echarts@6.0.0: '2025-07-30T02:38:34.897Z'
+ elkjs@0.11.1: '2026-03-03T12:21:48.463Z'
+ embla-carousel-autoplay@8.6.0: '2025-04-04T17:37:46.303Z'
+ 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.46.1: '2026-04-29T09:42:09.686Z'
+ eslint-markdown@0.8.0: '2026-05-03T14:19:51.244Z'
eslint-plugin-better-tailwindcss@4.5.0: '2026-04-28T06:24:47.281Z'
- eslint@10.2.1: '2026-04-17T20:17:44.852Z'
- hono@4.12.15: '2026-04-24T06:51:10.290Z'
+ eslint-plugin-hyoban@0.14.1: '2026-03-08T02:51:00.805Z'
+ eslint-plugin-markdown-preferences@0.41.1: '2026-04-09T23:28:41.552Z'
+ eslint-plugin-no-barrel-files@1.3.1: '2026-04-12T18:28:18.653Z'
+ eslint-plugin-react-refresh@0.5.2: '2026-02-23T19:49:32.404Z'
+ eslint-plugin-sonarjs@4.0.3: '2026-04-16T08:09:42.856Z'
+ eslint-plugin-storybook@10.3.6: '2026-04-29T14:03:32.305Z'
+ eslint@10.3.0: '2026-05-01T15:39:41.045Z'
+ fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z'
+ fuse.js@7.2.0: '2026-04-02T21:14:38.087Z'
+ happy-dom@20.9.0: '2026-04-13T22:55:15.313Z'
+ hast-util-to-jsx-runtime@2.3.6: '2025-03-05T11:30:29.166Z'
+ hono@4.12.17: '2026-05-05T09:30:51.600Z'
+ html-entities@2.6.0: '2025-03-30T15:40:10.885Z'
+ html-to-image@1.11.13: '2025-02-14T01:43:48.709Z'
+ i18next-resources-to-backend@1.2.1: '2024-04-10T19:22:23.117Z'
i18next@26.0.8: '2026-04-24T19:20:14.685Z'
+ iconify-import-svg@0.2.0: '2026-04-20T06:18:25.132Z'
+ immer@11.1.6: '2026-05-04T16:24:53.113Z'
+ jotai@2.20.0: '2026-05-06T01:10:00.036Z'
+ js-audio-recorder@1.0.7: '2021-01-09T10:20:49.923Z'
+ js-cookie@3.0.5: '2023-04-24T09:23:51.443Z'
js-yaml@4.1.1: '2025-11-12T15:18:03.524Z'
+ jsonschema@1.5.0: '2025-01-07T15:09:11.287Z'
+ katex@0.16.45: '2026-04-05T13:32:39.675Z'
+ knip@6.11.0: '2026-05-02T08:25:25.211Z'
+ ky@2.0.2: '2026-04-21T08:58:46.923Z'
+ lamejs@1.2.1: '2021-12-02T15:44:40.036Z'
lexical@0.44.0: '2026-04-27T14:47:00.970Z'
- tldts@7.0.29: '2026-04-28T12:21:32.710Z'
+ loro-crdt@1.12.1: '2026-04-29T20:11:51.397Z'
+ mermaid@11.14.0: '2026-04-01T09:17:42.671Z'
+ mime@4.1.0: '2025-09-12T17:53:01.376Z'
+ mitt@3.0.1: '2023-07-04T17:31:47.638Z'
+ negotiator@1.0.0: '2024-08-31T15:42:18.280Z'
+ next-themes@0.4.6: '2025-03-11T21:02:05.882Z'
+ next@16.2.4: '2026-04-15T22:33:47.905Z'
+ nuqs@2.8.9: '2026-02-27T15:51:04.508Z'
+ pinyin-pro@3.28.1: '2026-04-10T09:18:57.903Z'
+ playwright@1.59.1: '2026-04-01T17:58:48.894Z'
+ postcss@8.5.14: '2026-05-04T16:43:35.284Z'
+ qrcode.react@4.2.0: '2024-12-11T17:22:40.569Z'
+ qs@6.15.1: '2026-04-08T19:37:55.541Z'
+ react-18-input-autosize@3.0.0: '2022-08-05T17:22:57.225Z'
+ react-dom@19.2.5: '2026-04-08T18:39:31.423Z'
+ react-easy-crop@5.5.7: '2026-03-24T09:41:01.114Z'
+ react-hotkeys-hook@5.3.2: '2026-05-05T13:01:00.987Z'
+ react-i18next@16.5.8: '2026-03-11T14:19:02.440Z'
+ react-multi-email@1.0.25: '2024-07-18T04:31:06.176Z'
+ react-papaparse@4.4.0: '2023-10-13T10:27:07.978Z'
+ react-pdf-highlighter@8.0.0-rc.0: '2024-09-14T16:57:58.673Z'
+ react-server-dom-webpack@19.2.5: '2026-04-08T18:39:35.209Z'
+ react-sortablejs@6.1.4: '2022-05-31T07:19:03.552Z'
+ react-textarea-autosize@8.5.9: '2025-03-30T22:13:11.081Z'
+ react@19.2.5: '2026-04-08T18:39:24.455Z'
+ reactflow@11.11.4: '2024-06-20T11:31:29.797Z'
+ remark-breaks@4.0.0: '2023-09-22T16:45:41.061Z'
+ remark-directive@4.0.0: '2025-02-27T15:15:20.630Z'
+ scheduler@0.27.0: '2025-10-01T21:39:15.208Z'
+ sharp@0.34.5: '2025-11-06T14:19:40.989Z'
+ shiki@4.0.2: '2026-03-09T02:23:34.958Z'
+ socket.io-client@4.8.3: '2025-12-23T16:39:16.428Z'
+ sortablejs@1.15.7: '2026-02-11T22:42:31.720Z'
+ std-semver@1.0.8: '2026-03-09T17:23:55.795Z'
+ storybook@10.3.6: '2026-04-29T14:02:57.716Z'
+ streamdown@2.5.0: '2026-03-17T17:35:05.216Z'
+ string-ts@2.3.1: '2025-11-28T17:33:10.099Z'
+ tailwind-merge@3.5.0: '2026-02-18T23:45:37.340Z'
+ tailwindcss@4.2.4: '2026-04-21T13:15:55.494Z'
+ tldts@7.0.30: '2026-05-02T12:56:41.650Z'
tsx@4.21.0: '2025-11-30T15:56:09.488Z'
typescript@6.0.3: '2026-04-16T23:38:27.905Z'
+ uglify-js@3.19.3: '2024-08-29T13:49:01.316Z'
+ unist-util-visit@5.1.0: '2026-01-22T19:02:58.977Z'
+ use-context-selector@2.0.0: '2024-05-06T11:23:59.259Z'
uuid@14.0.0: '2026-04-19T15:15:42.302Z'
- vinext@0.0.45: '2026-04-28T11:43:03.463Z'
+ vinext@0.0.47: '2026-05-03T18:57:55.900Z'
+ vite-plugin-inspect@12.0.0-beta.1: '2026-03-24T10:42:21.306Z'
vite-plus@0.1.20: '2026-04-29T03:08:50.317Z'
- zod@4.3.6: '2026-01-22T19:14:35.382Z'
+ vitest-browser-react@2.2.0: '2026-04-05T06:56:34.635Z'
+ vitest-canvas-mock@1.1.4: '2026-03-24T14:42:39.285Z'
+ zod@4.4.3: '2026-05-04T07:06:40.819Z'
+ zundo@2.3.0: '2024-11-17T16:35:11.372Z'
+ zustand@5.0.13: '2026-05-05T00:04:17.510Z'
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ee6ccf00df..6064d8d110 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -2,7 +2,7 @@ saveExact: true
catalogMode: prefer
dedupeDirectDeps: true
engineStrict: true
-minimumReleaseAge: 1440
+minimumReleaseAge: 0
optimisticRepeatInstall: true
verifyDepsBeforeRun: install
resolutionMode: time-based
@@ -54,8 +54,8 @@ overrides:
yaml@>=2.0.0 <2.8.3: 2.8.3
yauzl@<3.2.1: 3.2.1
catalog:
- '@amplitude/analytics-browser': 2.42.0
- '@amplitude/plugin-session-replay-browser': 1.28.1
+ '@amplitude/analytics-browser': 2.42.1
+ '@amplitude/plugin-session-replay-browser': 1.29.0
'@antfu/eslint-config': 8.2.0
'@base-ui/react': 1.4.1
'@chromatic-com/storybook': 5.1.2
@@ -65,11 +65,11 @@ catalog:
'@eslint-react/eslint-plugin': 3.0.0
'@eslint/js': 10.0.1
'@floating-ui/react': 0.27.19
- '@formatjs/intl-localematcher': 0.8.4
+ '@formatjs/intl-localematcher': 0.8.6
'@headlessui/react': 2.2.10
'@heroicons/react': 2.2.0
- '@hey-api/openapi-ts': 0.97.0
- '@hono/node-server': 2.0.0
+ '@hey-api/openapi-ts': 0.97.1
+ '@hono/node-server': 2.0.1
'@iconify-json/heroicons': 1.2.3
'@iconify-json/ri': 1.2.10
'@lexical/code': 0.44.0
@@ -85,42 +85,42 @@ catalog:
'@monaco-editor/react': 4.7.0
'@next/eslint-plugin-next': 16.2.4
'@next/mdx': 16.2.4
- '@orpc/client': 1.14.0
- '@orpc/contract': 1.14.0
- '@orpc/openapi-client': 1.14.0
- '@orpc/tanstack-query': 1.14.0
+ '@orpc/client': 1.14.1
+ '@orpc/contract': 1.14.1
+ '@orpc/openapi-client': 1.14.1
+ '@orpc/tanstack-query': 1.14.1
'@playwright/test': 1.59.1
'@remixicon/react': 4.9.0
'@rgrove/parse-xml': 4.2.0
- '@sentry/react': 10.50.0
- '@storybook/addon-docs': 10.3.5
- '@storybook/addon-links': 10.3.5
- '@storybook/addon-onboarding': 10.3.5
- '@storybook/addon-themes': 10.3.5
- '@storybook/nextjs-vite': 10.3.5
- '@storybook/react': 10.3.5
- '@storybook/react-vite': 10.3.5
+ '@sentry/react': 10.51.0
+ '@storybook/addon-docs': 10.3.6
+ '@storybook/addon-links': 10.3.6
+ '@storybook/addon-onboarding': 10.3.6
+ '@storybook/addon-themes': 10.3.6
+ '@storybook/nextjs-vite': 10.3.6
+ '@storybook/react': 10.3.6
+ '@storybook/react-vite': 10.3.6
'@streamdown/math': 1.0.2
'@svgdotjs/svg.js': 3.2.5
'@t3-oss/env-nextjs': 0.13.11
'@tailwindcss/postcss': 4.2.4
'@tailwindcss/typography': 0.5.19
'@tailwindcss/vite': 4.2.4
- '@tanstack/eslint-plugin-query': 5.100.6
+ '@tanstack/eslint-plugin-query': 5.100.9
'@tanstack/react-devtools': 0.10.2
'@tanstack/react-form': 1.29.1
'@tanstack/react-form-devtools': 0.2.22
'@tanstack/react-hotkeys': 0.10.0
- '@tanstack/react-query': 5.100.6
- '@tanstack/react-query-devtools': 5.100.6
+ '@tanstack/react-query': 5.100.9
+ '@tanstack/react-query-devtools': 5.100.9
'@tanstack/react-virtual': 3.13.24
'@testing-library/dom': 10.4.1
'@testing-library/jest-dom': 6.9.1
'@testing-library/react': 16.3.2
'@testing-library/user-event': 14.6.1
- '@tsslint/cli': 3.1.0
- '@tsslint/compat-eslint': 3.1.0
- '@tsslint/config': 3.1.0
+ '@tsslint/cli': 3.1.1
+ '@tsslint/compat-eslint': 3.1.1
+ '@tsslint/config': 3.1.1
'@types/js-cookie': 3.0.6
'@types/js-yaml': 4.0.9
'@types/negotiator': 0.6.4
@@ -129,9 +129,9 @@ catalog:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3
'@types/sortablejs': 1.15.9
- '@typescript-eslint/eslint-plugin': 8.59.1
- '@typescript-eslint/parser': 8.59.1
- '@typescript/native-preview': 7.0.0-dev.20260428.1
+ '@typescript-eslint/eslint-plugin': 8.59.2
+ '@typescript-eslint/parser': 8.59.2
+ '@typescript/native-preview': 7.0.0-dev.20260505.1
'@vitejs/plugin-react': 6.0.1
'@vitejs/plugin-rsc': 0.5.25
'@vitest/coverage-v8': 4.1.5
@@ -149,44 +149,45 @@ catalog:
cron-parser: 5.5.0
dayjs: 1.11.20
decimal.js: 10.6.0
- dompurify: 3.4.1
+ dompurify: 3.4.2
echarts: 6.0.0
echarts-for-react: 3.0.6
elkjs: 0.11.1
embla-carousel-autoplay: 8.6.0
embla-carousel-react: 8.6.0
emoji-mart: 5.6.0
- es-toolkit: 1.46.0
- eslint: 10.2.1
- eslint-markdown: 0.7.0
+ es-toolkit: 1.46.1
+ eslint: 10.3.0
+ eslint-markdown: 0.8.0
eslint-plugin-better-tailwindcss: 4.5.0
eslint-plugin-hyoban: 0.14.1
eslint-plugin-markdown-preferences: 0.41.1
eslint-plugin-no-barrel-files: 1.3.1
eslint-plugin-react-refresh: 0.5.2
eslint-plugin-sonarjs: 4.0.3
- eslint-plugin-storybook: 10.3.5
+ eslint-plugin-storybook: 10.3.6
fast-deep-equal: 3.1.3
+ fuse.js: 7.2.0
happy-dom: 20.9.0
hast-util-to-jsx-runtime: 2.3.6
- hono: 4.12.15
+ hono: 4.12.17
html-entities: 2.6.0
html-to-image: 1.11.13
i18next: 26.0.8
i18next-resources-to-backend: 1.2.1
iconify-import-svg: 0.2.0
- immer: 11.1.4
- jotai: 2.19.1
+ immer: 11.1.6
+ jotai: 2.20.0
js-audio-recorder: 1.0.7
js-cookie: 3.0.5
js-yaml: 4.1.1
jsonschema: 1.5.0
katex: 0.16.45
- knip: 6.7.0
+ knip: 6.11.0
ky: 2.0.2
lamejs: 1.2.1
lexical: 0.44.0
- loro-crdt: 1.12.0
+ loro-crdt: 1.12.1
mermaid: 11.14.0
mime: 4.1.0
mitt: 3.0.1
@@ -196,14 +197,14 @@ catalog:
nuqs: 2.8.9
pinyin-pro: 3.28.1
playwright: 1.59.1
- postcss: 8.5.12
+ postcss: 8.5.14
qrcode.react: 4.2.0
qs: 6.15.1
react: 19.2.5
react-18-input-autosize: 3.0.0
react-dom: 19.2.5
react-easy-crop: 5.5.7
- react-hotkeys-hook: 5.2.4
+ react-hotkeys-hook: 5.3.2
react-i18next: 16.5.8
react-multi-email: 1.0.25
react-papaparse: 4.4.0
@@ -220,25 +221,25 @@ catalog:
socket.io-client: 4.8.3
sortablejs: 1.15.7
std-semver: 1.0.8
- storybook: 10.3.5
+ storybook: 10.3.6
streamdown: 2.5.0
string-ts: 2.3.1
tailwind-merge: 3.5.0
tailwindcss: 4.2.4
- tldts: 7.0.29
+ tldts: 7.0.30
tsx: 4.21.0
typescript: 6.0.3
uglify-js: 3.19.3
unist-util-visit: 5.1.0
use-context-selector: 2.0.0
uuid: 14.0.0
- vinext: 0.0.45
+ vinext: 0.0.47
vite: npm:@voidzero-dev/vite-plus-core@0.1.20
vite-plugin-inspect: 12.0.0-beta.1
vite-plus: 0.1.20
vitest: npm:@voidzero-dev/vite-plus-test@0.1.20
vitest-browser-react: 2.2.0
vitest-canvas-mock: 1.1.4
- zod: 4.3.6
+ zod: 4.4.3
zundo: 2.3.0
- zustand: 5.0.12
+ zustand: 5.0.13
diff --git a/web/AGENTS.md b/web/AGENTS.md
index 5e9f7ed11c..dc72a293d1 100644
--- a/web/AGENTS.md
+++ b/web/AGENTS.md
@@ -5,9 +5,9 @@
## Overlay Components (Mandatory)
- `../packages/dify-ui/README.md` is the permanent contract for overlay primitives, portals, root `isolation: isolate`, and the `z-1002` / `z-1003` layering.
-- `./docs/overlay-migration.md` is the source of truth for the ongoing migration (deprecated import paths, allowlist, coexistence rules).
+- `./docs/overlay-migration.md` is the source of truth for the ongoing migration (deprecated import paths and coexistence rules).
- In new or modified code, use only overlay primitives from `@langgenius/dify-ui/*`.
-- Do not introduce deprecated overlay imports from `@/app/components/base/*`; when touching legacy callers, prefer migrating them and keep the allowlist shrinking (never expanding).
+- Do not introduce deprecated overlay imports from `@/app/components/base/*`; when touching legacy callers, prefer migrating them.
## Query & Mutation (Mandatory)
diff --git a/web/__mocks__/base-ui-dropdown-menu.tsx b/web/__mocks__/base-ui-dropdown-menu.tsx
new file mode 100644
index 0000000000..9e2bfa2d41
--- /dev/null
+++ b/web/__mocks__/base-ui-dropdown-menu.tsx
@@ -0,0 +1,172 @@
+import type { ReactNode } from 'react'
+import * as React from 'react'
+
+const DropdownMenuContext = React.createContext({
+ open: false,
+ onOpenChange: (_open: boolean) => {},
+})
+
+type DropdownMenuProps = {
+ children?: ReactNode
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}
+
+type DropdownMenuTriggerProps = React.HTMLAttributes & {
+ children?: ReactNode
+ nativeButton?: boolean
+ render?: React.ReactElement
+}
+
+type DropdownMenuContentProps = React.HTMLAttributes & {
+ children?: ReactNode
+ placement?: string
+ sideOffset?: number
+ alignOffset?: number
+ popupClassName?: string
+}
+
+export const DropdownMenu = ({
+ children,
+ open,
+ onOpenChange,
+}: DropdownMenuProps) => {
+ const [localOpen, setLocalOpen] = React.useState(false)
+ const resolvedOpen = open ?? localOpen
+ const handleOpenChange = React.useCallback((nextOpen: boolean) => {
+ setLocalOpen(nextOpen)
+ onOpenChange?.(nextOpen)
+ }, [onOpenChange])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export const DropdownMenuTrigger = ({
+ children,
+ render,
+ nativeButton: _nativeButton,
+ onClick,
+ ...props
+}: DropdownMenuTriggerProps) => {
+ const { open, onOpenChange } = React.useContext(DropdownMenuContext)
+ const node = render ?? children
+ const isNativeButton = React.isValidElement(node) && node.type === 'button'
+ const handleClick = (event: React.MouseEvent) => {
+ onClick?.(event)
+ if (!event.defaultPrevented)
+ onOpenChange(!open)
+ }
+
+ if (React.isValidElement(node)) {
+ const triggerElement = node as React.ReactElement>
+ const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes & { 'data-testid'?: string }
+ const triggerProps = props as React.HTMLAttributes & { 'data-testid'?: string }
+ const role = childProps.role ?? triggerProps.role ?? (!isNativeButton && (childProps['aria-label'] || triggerProps['aria-label']) ? 'button' : undefined)
+ return React.cloneElement(triggerElement, {
+ ...props,
+ ...childProps,
+ 'data-testid': childProps['data-testid'] ?? triggerProps['data-testid'] ?? 'dropdown-menu-trigger',
+ role,
+ 'tabIndex': childProps.tabIndex ?? triggerProps.tabIndex ?? (role === 'button' ? 0 : undefined),
+ 'onClick': (event: React.MouseEvent) => {
+ childProps.onClick?.(event)
+ handleClick(event)
+ },
+ }, render ? (children ?? childProps.children) : childProps.children)
+ }
+
+ return (
+
+ {node}
+
+ )
+}
+
+export const DropdownMenuContent = ({
+ children,
+ className,
+ popupClassName,
+ placement,
+ sideOffset,
+ alignOffset,
+ ...props
+}: DropdownMenuContentProps) => {
+ const { open } = React.useContext(DropdownMenuContext)
+ if (!open)
+ return null
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const DropdownMenuItem = ({
+ children,
+ onClick,
+ ...props
+}: React.HTMLAttributes & { children?: ReactNode }) => (
+
+ {children}
+
+)
+
+export const DropdownMenuRadioGroup = ({
+ children,
+ onValueChange,
+ ...props
+}: React.HTMLAttributes & { children?: ReactNode, value?: unknown, onValueChange?: (value: unknown) => void }) => (
+
+ {React.Children.map(children, (child) => {
+ if (!React.isValidElement(child))
+ return child
+ return React.cloneElement(child as React.ReactElement<{ __onValueChange?: (value: unknown) => void }>, { __onValueChange: onValueChange })
+ })}
+
+)
+
+export const DropdownMenuRadioItem = ({
+ children,
+ value,
+ onClick,
+ __onValueChange,
+ ...props
+}: React.HTMLAttributes & { children?: ReactNode, value?: unknown, __onValueChange?: (value: unknown) => void }) => (
+ {
+ onClick?.(event)
+ __onValueChange?.(value)
+ }}
+ {...props}
+ >
+ {children}
+
+)
+
+export const DropdownMenuRadioItemIndicator = ({ children }: { children?: ReactNode }) => <>{children}>
+export const DropdownMenuCheckboxItem = DropdownMenuItem
+export const DropdownMenuCheckboxItemIndicator = ({ children }: { children?: ReactNode }) => <>{children}>
+export const DropdownMenuLabel = ({ children }: { children?: ReactNode }) => <>{children}>
+export const DropdownMenuSeparator = (props: React.HTMLAttributes) =>
+export const DropdownMenuSub = ({ children }: { children?: ReactNode }) => <>{children}>
+export const DropdownMenuSubTrigger = DropdownMenuItem
+export const DropdownMenuSubContent = ({ children }: { children?: ReactNode }) => <>{children}>
diff --git a/web/__mocks__/base-ui-popover.tsx b/web/__mocks__/base-ui-popover.tsx
index 8818f60f4e..c4d7a23827 100644
--- a/web/__mocks__/base-ui-popover.tsx
+++ b/web/__mocks__/base-ui-popover.tsx
@@ -23,17 +23,25 @@ type PopoverContentProps = React.HTMLAttributes & {
placement?: string
sideOffset?: number
alignOffset?: number
+ popupClassName?: string
positionerProps?: React.HTMLAttributes
popupProps?: React.HTMLAttributes
}
export const Popover = ({
children,
- open = false,
+ open,
onOpenChange,
}: PopoverProps) => {
+ const [localOpen, setLocalOpen] = React.useState(false)
+ const resolvedOpen = open ?? localOpen
+ const handleOpenChange = React.useCallback((nextOpen: boolean) => {
+ setLocalOpen(nextOpen)
+ onOpenChange?.(nextOpen)
+ }, [onOpenChange])
+
React.useEffect(() => {
- if (!open)
+ if (!resolvedOpen)
return
const handleMouseDown = (event: MouseEvent) => {
@@ -41,12 +49,12 @@ export const Popover = ({
if (target?.closest?.('[data-popover-trigger="true"], [data-popover-content="true"]'))
return
- onOpenChange?.(false)
+ handleOpenChange(false)
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape')
- onOpenChange?.(false)
+ handleOpenChange(false)
}
document.addEventListener('mousedown', handleMouseDown)
@@ -56,15 +64,15 @@ export const Popover = ({
document.removeEventListener('mousedown', handleMouseDown)
document.removeEventListener('keydown', handleKeyDown)
}
- }, [open, onOpenChange])
+ }, [resolvedOpen, handleOpenChange])
return (
{}),
+ open: resolvedOpen,
+ onOpenChange: handleOpenChange,
}}
>
-
+
{children}
@@ -84,11 +92,12 @@ export const PopoverTrigger = ({
if (React.isValidElement(node)) {
const triggerElement = node as React.ReactElement
>
const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes & { 'data-testid'?: string }
+ const triggerProps = props as React.HTMLAttributes & { 'data-testid'?: string }
return React.cloneElement(triggerElement, {
...props,
...childProps,
- 'data-testid': childProps['data-testid'] ?? 'popover-trigger',
+ 'data-testid': childProps['data-testid'] ?? triggerProps['data-testid'] ?? 'popover-trigger',
'data-popover-trigger': 'true',
'onClick': (event: React.MouseEvent) => {
childProps.onClick?.(event)
@@ -97,7 +106,7 @@ export const PopoverTrigger = ({
return
onOpenChange(!open)
},
- })
+ }, render ? (children ?? childProps.children) : childProps.children)
}
return (
@@ -123,6 +132,7 @@ export const PopoverContent = ({
placement,
sideOffset,
alignOffset,
+ popupClassName,
positionerProps,
popupProps,
...props
@@ -139,7 +149,7 @@ export const PopoverContent = ({
data-placement={placement}
data-side-offset={sideOffset}
data-align-offset={alignOffset}
- className={className}
+ className={className || popupClassName}
{...positionerProps}
{...popupProps}
{...props}
diff --git a/web/__mocks__/base-ui-select.tsx b/web/__mocks__/base-ui-select.tsx
new file mode 100644
index 0000000000..7655164419
--- /dev/null
+++ b/web/__mocks__/base-ui-select.tsx
@@ -0,0 +1,65 @@
+import type { ReactNode } from 'react'
+import * as React from 'react'
+
+const SelectContext = React.createContext({
+ value: undefined as unknown,
+ onValueChange: (_value: unknown) => {},
+})
+
+type SelectProps = {
+ children?: ReactNode
+ value?: unknown
+ onValueChange?: (value: unknown) => void
+}
+
+export const Select = ({
+ children,
+ value,
+ onValueChange,
+}: SelectProps) => (
+ {}) }}>
+ {children}
+
+)
+
+export const SelectTrigger = ({
+ children,
+ ...props
+}: React.ButtonHTMLAttributes & { children?: ReactNode }) => (
+
+ {children}
+
+)
+
+export const SelectValue = ({ placeholder }: { placeholder?: ReactNode }) => <>{placeholder}>
+
+export const SelectContent = ({ children }: { children?: ReactNode }) => (
+ {children}
+)
+
+export const SelectItem = ({
+ children,
+ value,
+ onClick,
+ ...props
+}: React.HTMLAttributes & { children?: ReactNode, value?: unknown }) => {
+ const select = React.useContext(SelectContext)
+ return (
+ {
+ onClick?.(event)
+ select.onValueChange(value)
+ }}
+ {...props}
+ >
+ {children}
+
+ )
+}
+
+export const SelectItemText = ({ children }: { children?: ReactNode }) => <>{children}>
+export const SelectItemIndicator = ({ children }: { children?: ReactNode }) => <>{children}>
+export const SelectGroup = ({ children }: { children?: ReactNode }) => <>{children}>
+export const SelectLabel = ({ children }: { children?: ReactNode }) => <>{children}>
+export const SelectSeparator = (props: React.HTMLAttributes) =>
diff --git a/web/__mocks__/base-ui-tooltip.tsx b/web/__mocks__/base-ui-tooltip.tsx
new file mode 100644
index 0000000000..23e05f0864
--- /dev/null
+++ b/web/__mocks__/base-ui-tooltip.tsx
@@ -0,0 +1,95 @@
+import type { ReactNode } from 'react'
+import * as React from 'react'
+
+const TooltipContext = React.createContext({
+ open: false,
+ onOpenChange: (_open: boolean) => {},
+})
+
+type TooltipProps = {
+ children?: ReactNode
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}
+
+export const Tooltip = ({ children, open, onOpenChange }: TooltipProps) => {
+ const [localOpen, setLocalOpen] = React.useState(false)
+ const resolvedOpen = open ?? localOpen
+ const handleOpenChange = React.useCallback((nextOpen: boolean) => {
+ setLocalOpen(nextOpen)
+ onOpenChange?.(nextOpen)
+ }, [onOpenChange])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const TooltipTrigger = ({
+ children,
+ render,
+ nativeButton: _nativeButton,
+ ...props
+}: React.HTMLAttributes & { children?: ReactNode, render?: React.ReactElement, nativeButton?: boolean }) => {
+ const { open, onOpenChange } = React.useContext(TooltipContext)
+ const node = render ?? children
+
+ if (React.isValidElement(node)) {
+ const triggerElement = node as React.ReactElement>
+ const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes
+
+ return React.cloneElement(triggerElement, {
+ ...props,
+ ...childProps,
+ onMouseEnter: (event: React.MouseEvent) => {
+ childProps.onMouseEnter?.(event)
+ props.onMouseEnter?.(event)
+ onOpenChange(true)
+ },
+ onMouseLeave: (event: React.MouseEvent) => {
+ childProps.onMouseLeave?.(event)
+ props.onMouseLeave?.(event)
+ onOpenChange(false)
+ },
+ onClick: (event: React.MouseEvent) => {
+ childProps.onClick?.(event)
+ props.onClick?.(event)
+ onOpenChange(!open)
+ },
+ })
+ }
+
+ return (
+ {
+ props.onMouseEnter?.(event)
+ onOpenChange(true)
+ }}
+ onMouseLeave={(event) => {
+ props.onMouseLeave?.(event)
+ onOpenChange(false)
+ }}
+ onClick={(event) => {
+ props.onClick?.(event)
+ onOpenChange(!open)
+ }}
+ >
+ {node}
+
+ )
+}
+
+export const TooltipContent = ({
+ children,
+ ...props
+}: React.HTMLAttributes & { children?: ReactNode }) => {
+ const { open } = React.useContext(TooltipContext)
+ if (!open)
+ return null
+ return {children}
+}
+
+export const TooltipProvider = ({ children }: { children?: ReactNode }) => <>{children}>
diff --git a/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx b/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx
index a7c660105d..ef765c06f2 100644
--- a/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx
+++ b/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx
@@ -95,37 +95,8 @@ vi.mock('@/app/components/workflow/utils', () => ({
getKeyboardKeyNameBySystem: (key: string) => key,
}))
-vi.mock('@/app/components/base/portal-to-follow-elem', async () => {
- const React = await vi.importActual('react')
- const OpenContext = React.createContext(false)
-
- return {
- PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => (
-
- {children}
-
- ),
- PortalToFollowElemTrigger: ({
- children,
- onClick,
- }: {
- children: React.ReactNode
- onClick?: () => void
- }) => (
-
- {children}
-
- ),
- PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => {
- const open = React.useContext(OpenContext)
- return open ? {children}
: null
- },
- }
-})
-
-vi.mock('@/app/components/base/tooltip', () => ({
- default: ({ children }: { children?: React.ReactNode }) => <>{children}>,
-}))
+vi.mock('@langgenius/dify-ui/dropdown-menu', () => import('@/__mocks__/base-ui-dropdown-menu'))
+vi.mock('@langgenius/dify-ui/tooltip', () => import('@/__mocks__/base-ui-tooltip'))
vi.mock('@/app/components/app-sidebar/app-info', () => ({
default: ({
diff --git a/web/__tests__/app/app-publisher-flow.test.tsx b/web/__tests__/app/app-publisher-flow.test.tsx
index d4bf56e7e4..ff2e3f6f44 100644
--- a/web/__tests__/app/app-publisher-flow.test.tsx
+++ b/web/__tests__/app/app-publisher-flow.test.tsx
@@ -122,33 +122,7 @@ vi.mock('@/app/components/app/app-access-control', () => ({
default: () =>
,
}))
-vi.mock('@/app/components/base/portal-to-follow-elem', async () => {
- const React = await vi.importActual('react')
- const OpenContext = React.createContext(false)
-
- return {
- PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => (
-
- {children}
-
- ),
- PortalToFollowElemTrigger: ({
- children,
- onClick,
- }: {
- children: React.ReactNode
- onClick?: () => void
- }) => (
-
- {children}
-
- ),
- PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => {
- const open = React.useContext(OpenContext)
- return open ? {children}
: null
- },
- }
-})
+vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
vi.mock('@/app/components/workflow/utils', () => ({
getKeyboardKeyCodeBySystem: () => 'ctrl',
diff --git a/web/__tests__/apps/app-card-operations-flow.test.tsx b/web/__tests__/apps/app-card-operations-flow.test.tsx
index b0854072d2..ef3bee5167 100644
--- a/web/__tests__/apps/app-card-operations-flow.test.tsx
+++ b/web/__tests__/apps/app-card-operations-flow.test.tsx
@@ -53,6 +53,16 @@ vi.mock('@/next/navigation', () => ({
}),
}))
+vi.mock('@tanstack/react-query', async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ useQuery: () => ({
+ data: [],
+ }),
+ }
+})
+
// Mock headless UI Popover so it renders content without transition
vi.mock('@headlessui/react', async () => {
const actual = await vi.importActual('@headlessui/react')
diff --git a/web/__tests__/apps/app-list-browsing-flow.test.tsx b/web/__tests__/apps/app-list-browsing-flow.test.tsx
index e6b83bd69d..4829adacf0 100644
--- a/web/__tests__/apps/app-list-browsing-flow.test.tsx
+++ b/web/__tests__/apps/app-list-browsing-flow.test.tsx
@@ -9,7 +9,7 @@ import type { ReactElement, ReactNode } from 'react'
*/
import type { AppListResponse } from '@/models/app'
import type { App } from '@/types/app'
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
import List from '@/app/components/apps/list'
@@ -92,6 +92,9 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
+ useQuery: () => ({
+ data: [],
+ }),
useInfiniteQuery: () => ({
data: { pages: mockPages },
isLoading: mockIsLoading,
@@ -360,13 +363,18 @@ describe('App List Browsing Flow', () => {
expect(input).toBeInTheDocument()
})
- it('should allow typing in search input', () => {
+ it('should update search query when typing in search input', async () => {
mockPages = [createPage([createMockApp()])]
- renderList()
+ const { onUrlUpdate } = renderList()
- const input = document.querySelector('input')!
+ const input = screen.getByPlaceholderText('common.operation.search')
fireEvent.change(input, { target: { value: 'test search' } })
- expect(input.value).toBe('test search')
+
+ await waitFor(() => {
+ expect(onUrlUpdate).toHaveBeenCalled()
+ })
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
+ expect(lastCall.searchParams.get('keywords')).toBe('test search')
})
})
diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx
index 5df331767b..cbfd679ace 100644
--- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx
+++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx
@@ -121,25 +121,7 @@ vi.mock('../../app-access-control', () => ({
),
}))
-vi.mock('@/app/components/base/portal-to-follow-elem', async () => {
- const ReactModule = await vi.importActual('react')
- const OpenContext = ReactModule.createContext(false)
-
- return {
- PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => (
-
- {children}
-
- ),
- PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => (
- {children}
- ),
- PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => {
- const open = ReactModule.useContext(OpenContext)
- return open ? {children}
: null
- },
- }
-})
+vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
vi.mock('../sections', () => ({
PublisherSummarySection: (props: Record) => {
diff --git a/web/app/components/app/configuration/config-var/__tests__/select-var-type.spec.tsx b/web/app/components/app/configuration/config-var/__tests__/select-var-type.spec.tsx
index 611aaa1c8a..42d0de5ed7 100644
--- a/web/app/components/app/configuration/config-var/__tests__/select-var-type.spec.tsx
+++ b/web/app/components/app/configuration/config-var/__tests__/select-var-type.spec.tsx
@@ -1,8 +1,8 @@
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import SelectVarType from '../select-var-type'
describe('SelectVarType', () => {
- it('should open the menu and return the selected variable type', () => {
+ it('should open the menu and return the selected variable type', async () => {
const onChange = vi.fn()
render( )
@@ -11,6 +11,8 @@ describe('SelectVarType', () => {
fireEvent.click(screen.getByText('appDebug.variableConfig.checkbox'))
expect(onChange).toHaveBeenCalledWith('checkbox')
- expect(screen.queryByText('appDebug.variableConfig.checkbox')).not.toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.queryByText('appDebug.variableConfig.checkbox')).not.toBeInTheDocument()
+ })
})
})
diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx
index 2512aa93e8..f1356c9b61 100644
--- a/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx
+++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx
@@ -2,13 +2,7 @@
import { fireEvent, render, screen } from '@testing-library/react'
import TypeSelector from '../type-select'
-vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({ children }: { children: React.ReactNode }) => {children}
,
- PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => (
- {children}
- ),
- PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => {children}
,
-}))
+vi.mock('@langgenius/dify-ui/select', () => import('@/__mocks__/base-ui-select'))
vi.mock('@/app/components/workflow/nodes/_base/components/input-var-type-icon', () => ({
default: ({ type }: { type: string }) => {type} ,
diff --git a/web/app/components/app/configuration/config-var/config-modal/type-select.tsx b/web/app/components/app/configuration/config-var/config-modal/type-select.tsx
index 5fd7c88b82..acd3253f6b 100644
--- a/web/app/components/app/configuration/config-var/config-modal/type-select.tsx
+++ b/web/app/components/app/configuration/config-var/config-modal/type-select.tsx
@@ -1,16 +1,17 @@
'use client'
import type { FC } from 'react'
import type { InputVarType } from '@/app/components/workflow/types'
-import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { cn } from '@langgenius/dify-ui/cn'
-import * as React from 'react'
-import { useState } from 'react'
-import Badge from '@/app/components/base/badge'
import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectItemIndicator,
+ SelectItemText,
+ SelectTrigger,
+} from '@langgenius/dify-ui/select'
+import * as React from 'react'
+import Badge from '@/app/components/base/badge'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
@@ -35,21 +36,26 @@ const TypeSelector: FC = ({
popupInnerClassName,
readonly,
}) => {
- const [open, setOpen] = useState(false)
const selectedItem = value ? items.find(item => item.value === value) : undefined
return (
- {
+ const selected = items.find(item => item.value === nextValue)
+ if (selected)
+ onSelect(selected)
+ }}
>
- !readonly && setOpen(v => !v)} className="w-full">
-
+
+
= ({
{selectedItem?.name}
-
+
{inputVarTypeToVarType(selectedItem?.value as InputVarType)}
-
-
-
-
-
- {items.map((item: Item) => (
-
{
- onSelect(item)
- setOpen(false)
- }}
+
+
+ {items.map((item: Item) => (
+
+
-
-
- {item.name}
-
- {inputVarTypeToVarType(item.value)}
-
- ))}
-
-
-
+
+
{item.name}
+
+
{inputVarTypeToVarType(item.value)}
+
+
+ ))}
+
+
)
}
diff --git a/web/app/components/app/configuration/config-var/select-var-type.tsx b/web/app/components/app/configuration/config-var/select-var-type.tsx
index fc59bcd54b..4d3a96775c 100644
--- a/web/app/components/app/configuration/config-var/select-var-type.tsx
+++ b/web/app/components/app/configuration/config-var/select-var-type.tsx
@@ -1,15 +1,16 @@
'use client'
import type { FC } from 'react'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import * as React from 'react'
-import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import { ApiConnection } from '@/app/components/base/icons/src/vender/solid/development'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
import { InputVarType } from '@/app/components/workflow/types'
@@ -27,13 +28,14 @@ type ItemProps = {
const SelectItem: FC
= ({ text, type, value, Icon, onClick }) => {
return (
- onClick(value)}
>
{Icon ?
:
}
{text}
-
+
)
}
@@ -41,40 +43,36 @@ const SelectVarType: FC = ({
onChange,
}) => {
const { t } = useTranslation()
- const [open, setOpen] = useState(false)
const handleChange = (value: string) => {
onChange(value)
- setOpen(false)
}
return (
-
- setOpen(v => !v)}>
+
+ }
+ >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
)
}
export default React.memo(SelectVarType)
diff --git a/web/app/components/app/configuration/config/assistant-type-picker/__tests__/index.spec.tsx b/web/app/components/app/configuration/config/assistant-type-picker/__tests__/index.spec.tsx
index b32f14d089..7b2d3414f6 100644
--- a/web/app/components/app/configuration/config/assistant-type-picker/__tests__/index.spec.tsx
+++ b/web/app/components/app/configuration/config/assistant-type-picker/__tests__/index.spec.tsx
@@ -841,11 +841,11 @@ describe('AssistantTypePicker', () => {
it('should have proper ARIA state for dropdown', async () => {
// Arrange
const user = userEvent.setup()
- const { container } = renderComponent()
+ renderComponent()
// Act - Check initial state
- const portalContainer = container.querySelector('[data-state]')
- expect(portalContainer)!.toHaveAttribute('data-state', 'closed')
+ const triggerButton = screen.getByRole('button', { name: /chatAssistant\.name/i })
+ expect(triggerButton).toHaveAttribute('aria-expanded', 'false')
// Open dropdown
const trigger = screen.getByText(/chatAssistant.name/i)
@@ -853,23 +853,22 @@ describe('AssistantTypePicker', () => {
// Assert - State should change to open
await waitFor(() => {
- const openPortal = container.querySelector('[data-state="open"]')
- expect(openPortal)!.toBeInTheDocument()
+ expect(triggerButton).toHaveAttribute('aria-expanded', 'true')
})
})
it('should have proper data-state attribute', () => {
// Arrange & Act
- const { container } = renderComponent()
+ renderComponent()
- // Assert - Portal should have data-state for accessibility
- const portalContainer = container.querySelector('[data-state]')
- expect(portalContainer)!.toBeInTheDocument()
- expect(portalContainer)!.toHaveAttribute('data-state')
+ // Assert - Trigger should expose expanded state for accessibility
+ const triggerButton = screen.getByRole('button', { name: /chatAssistant\.name/i })
+ expect(triggerButton).toBeInTheDocument()
+ expect(triggerButton).toHaveAttribute('aria-expanded')
// Should start in closed state
// Should start in closed state
- expect(portalContainer)!.toHaveAttribute('data-state', 'closed')
+ expect(triggerButton).toHaveAttribute('aria-expanded', 'false')
})
it('should maintain accessible structure for screen readers', () => {
diff --git a/web/app/components/app/configuration/config/assistant-type-picker/index.tsx b/web/app/components/app/configuration/config/assistant-type-picker/index.tsx
index df0baa2f36..801645940f 100644
--- a/web/app/components/app/configuration/config/assistant-type-picker/index.tsx
+++ b/web/app/components/app/configuration/config/assistant-type-picker/index.tsx
@@ -2,6 +2,11 @@
import type { FC } from 'react'
import type { AgentConfig } from '@/models/debug'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import { RiArrowDownSLine } from '@remixicon/react'
import * as React from 'react'
import { useState } from 'react'
@@ -10,11 +15,6 @@ import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import { CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
import { BubbleText } from '@/app/components/base/icons/src/vender/solid/education'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import Radio from '@/app/components/base/radio/ui'
import AgentSetting from '../agent/agent-setting'
@@ -107,47 +107,48 @@ const AssistantTypePicker: FC
= ({
)
return (
<>
-
- setOpen(v => !v)}>
-
- {isAgent ?
:
}
-
{t(`assistantType.${isAgent ? 'agentAssistant' : 'chatAssistant'}.name`, { ns: 'appDebug' })}
-
-
-
-
-
-
{t('assistantType.name', { ns: 'appDebug' })}
-
-
- {!disabled && agentConfigUI}
-
-
-
+
+ )}
+ >
+ {isAgent ? : }
+ {t(`assistantType.${isAgent ? 'agentAssistant' : 'chatAssistant'}.name`, { ns: 'appDebug' })}
+
+
+
+ {t('assistantType.name', { ns: 'appDebug' })}
+
+
+ {!disabled && agentConfigUI}
+
+
{isShowAgentSetting && (
({
@@ -25,7 +25,7 @@ describe('VersionSelector', () => {
expect(onChange).not.toHaveBeenCalled()
})
- it('should open the selector and switch versions when multiple versions exist', () => {
+ it('should open the selector and switch versions when multiple versions exist', async () => {
const onChange = vi.fn()
render(
@@ -44,6 +44,8 @@ describe('VersionSelector', () => {
fireEvent.click(screen.getByText('generate.version 1'))
expect(onChange).toHaveBeenCalledWith(0)
- expect(screen.queryByText('generate.versions')).not.toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.queryByText('generate.versions')).not.toBeInTheDocument()
+ })
})
})
diff --git a/web/app/components/app/configuration/config/automatic/version-selector.tsx b/web/app/components/app/configuration/config/automatic/version-selector.tsx
index 25846e9b54..13b99dafde 100644
--- a/web/app/components/app/configuration/config/automatic/version-selector.tsx
+++ b/web/app/components/app/configuration/config/automatic/version-selector.tsx
@@ -1,10 +1,16 @@
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
-import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
type VersionSelectorProps = {
versionLen: number
@@ -16,19 +22,14 @@ const VersionSelector: React.FC = ({ versionLen, value, on
const { t } = useTranslation()
const [isOpen, {
setFalse: handleOpenFalse,
- toggle: handleOpenToggle,
set: handleOpenSet,
}] = useBoolean(false)
const moreThanOneVersion = versionLen > 1
- const handleOpen = useCallback((value: boolean) => {
+ const handleOpen = useCallback((nextOpen: boolean) => {
if (moreThanOneVersion)
- handleOpenSet(value)
- }, [moreThanOneVersion, handleOpenToggle])
- const handleToggle = useCallback(() => {
- if (moreThanOneVersion)
- handleOpenToggle()
- }, [moreThanOneVersion, handleOpenToggle])
+ handleOpenSet(nextOpen)
+ }, [moreThanOneVersion, handleOpenSet])
const versions = Array.from({ length: versionLen }, (_, index) => ({
label: `${t('generate.version', { ns: 'appDebug' })} ${index + 1}${index === versionLen - 1 ? ` · ${t('generate.latest', { ns: 'appDebug' })}` : ''}`,
@@ -38,67 +39,59 @@ const VersionSelector: React.FC = ({ versionLen, value, on
const isLatest = value === versionLen - 1
return (
-
-
+ )}
>
-
-
-
- {t('generate.version', { ns: 'appDebug' })}
- {' '}
- {value + 1}
- {isLatest && ` · ${t('generate.latest', { ns: 'appDebug' })}`}
-
- {moreThanOneVersion &&
}
+
+ {t('generate.version', { ns: 'appDebug' })}
+ {' '}
+ {value + 1}
+ {isLatest && ` · ${t('generate.latest', { ns: 'appDebug' })}`}
-
-
}
+
+
-
+ {t('generate.versions', { ns: 'appDebug' })}
+
+ {
+ onChange(nextValue)
+ handleOpenFalse()
+ }}
>
-
- {t('generate.versions', { ns: 'appDebug' })}
-
- {
- versions.map(option => (
- {
- onChange(option.value)
- handleOpenFalse()
- }}
- >
-
- {option.label}
-
- {
- value === option.value &&
- }
+ {versions.map(option => (
+
+
+ {option.label}
- ))
- }
-
-
-
+ {
+ value === option.value &&
+ }
+
+ ))}
+
+
+
)
}
diff --git a/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx
index e8b1583171..f8017c3585 100644
--- a/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx
+++ b/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx
@@ -1,7 +1,7 @@
+import type * as React from 'react'
import type { Props } from '../var-picker'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
-import * as React from 'react'
import ContextVar from '../index'
// Mock external dependencies only
@@ -76,57 +76,6 @@ vi.mock('@langgenius/dify-ui/popover', async () => {
}
})
-type PortalToFollowElemProps = {
- children: React.ReactNode
- open?: boolean
- onOpenChange?: (open: boolean) => void
-}
-type PortalToFollowElemTriggerProps = React.HTMLAttributes
& { children?: React.ReactNode, asChild?: boolean }
-type PortalToFollowElemContentProps = React.HTMLAttributes & { children?: React.ReactNode }
-
-vi.mock('@/app/components/base/portal-to-follow-elem', () => {
- const PortalContext = React.createContext({ open: false })
-
- const PortalToFollowElem = ({ children, open }: PortalToFollowElemProps) => {
- return (
-
- {children}
-
- )
- }
-
- const PortalToFollowElemContent = ({ children, ...props }: PortalToFollowElemContentProps) => {
- const { open } = React.useContext(PortalContext)
- if (!open)
- return null
- return (
-
- {children}
-
- )
- }
-
- const PortalToFollowElemTrigger = ({ children, asChild, ...props }: PortalToFollowElemTriggerProps) => {
- if (asChild && React.isValidElement(children)) {
- return React.cloneElement(children, {
- ...props,
- 'data-testid': 'portal-trigger',
- } as React.HTMLAttributes)
- }
- return (
-
- {children}
-
- )
- }
-
- return {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
- }
-})
-
describe('ContextVar', () => {
const mockOptions: Props['options'] = [
{ name: 'Variable 1', value: 'var1', type: 'string' },
diff --git a/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx b/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx
index 7890343720..5b77134468 100644
--- a/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx
+++ b/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx
@@ -10,41 +10,41 @@ vi.mock('@/next/navigation', () => ({
usePathname: () => '/test',
}))
-type PortalToFollowElemProps = {
+type PopoverProps = {
children: React.ReactNode
open?: boolean
onOpenChange?: (open: boolean) => void
}
-type PortalToFollowElemTriggerProps = React.HTMLAttributes & { children?: React.ReactNode, asChild?: boolean }
-type PortalToFollowElemContentProps = React.HTMLAttributes & { children?: React.ReactNode }
+type PopoverTriggerProps = React.HTMLAttributes & { children?: React.ReactNode, asChild?: boolean }
+type PopoverContentProps = React.HTMLAttributes & { children?: React.ReactNode }
vi.mock('@langgenius/dify-ui/popover', () => {
- const PortalContext = React.createContext({
+ const PopoverContext = React.createContext({
open: false,
onOpenChange: undefined as ((open: boolean) => void) | undefined,
})
- const Popover = ({ children, open, onOpenChange }: PortalToFollowElemProps) => {
+ const Popover = ({ children, open, onOpenChange }: PopoverProps) => {
return (
-
- {children}
-
+
+ {children}
+
)
}
- const PopoverContent = ({ children, ...props }: PortalToFollowElemContentProps) => {
- const { open } = React.useContext(PortalContext)
+ const PopoverContent = ({ children, ...props }: PopoverContentProps) => {
+ const { open } = React.useContext(PopoverContext)
if (!open)
return null
return (
-
+
{children}
)
}
- const PopoverTrigger = ({ children, asChild, render, ...props }: PortalToFollowElemTriggerProps & { render?: React.ReactNode }) => {
- const { open, onOpenChange } = React.useContext(PortalContext)
+ const PopoverTrigger = ({ children, asChild, render, ...props }: PopoverTriggerProps & { render?: React.ReactNode }) => {
+ const { open, onOpenChange } = React.useContext(PopoverContext)
const content = render ?? children
const handleClick = (e: React.MouseEvent
) => {
props.onClick?.(e)
@@ -56,7 +56,7 @@ vi.mock('@langgenius/dify-ui/popover', () => {
return React.cloneElement(content, {
...props,
'onClick': handleClick,
- 'data-testid': 'portal-trigger',
+ 'data-testid': 'popover-trigger',
} as React.HTMLAttributes)
}
@@ -64,11 +64,11 @@ vi.mock('@langgenius/dify-ui/popover', () => {
return React.cloneElement(children, {
...props,
'onClick': handleClick,
- 'data-testid': 'portal-trigger',
+ 'data-testid': 'popover-trigger',
} as React.HTMLAttributes)
}
return (
-
+
{content}
)
@@ -109,7 +109,7 @@ describe('VarPicker', () => {
// Assert
// Assert
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
expect(screen.getByText('var1'))!.toBeInTheDocument()
})
@@ -201,7 +201,7 @@ describe('VarPicker', () => {
// Assert - Trigger should be present
// Assert - Trigger should be present
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
})
})
@@ -234,7 +234,7 @@ describe('VarPicker', () => {
// Assert
// Assert
- expect(screen.getByTestId('portal-trigger'))!.toHaveClass('custom-trigger-class')
+ expect(screen.getByTestId('popover-trigger'))!.toHaveClass('custom-trigger-class')
})
it('should display selected value with proper formatting', () => {
@@ -268,11 +268,11 @@ describe('VarPicker', () => {
// Act
render(
)
- await user.click(screen.getByTestId('portal-trigger'))
+ await user.click(screen.getByTestId('popover-trigger'))
// Assert
// Assert
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
it('should call onChange and close dropdown when selecting an option', async () => {
@@ -285,8 +285,8 @@ describe('VarPicker', () => {
render(
)
// Open dropdown
- await user.click(screen.getByTestId('portal-trigger'))
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ await user.click(screen.getByTestId('popover-trigger'))
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
// Select a different option
const options = screen.getAllByText('var2')
@@ -295,7 +295,7 @@ describe('VarPicker', () => {
// Assert
expect(onChange).toHaveBeenCalledWith('var2')
- expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
})
it('should toggle dropdown when clicking trigger button multiple times', async () => {
@@ -306,15 +306,15 @@ describe('VarPicker', () => {
// Act
render(
)
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
// Open dropdown
await user.click(trigger)
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
// Close dropdown
await user.click(trigger)
- expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
})
})
@@ -359,7 +359,7 @@ describe('VarPicker', () => {
// Assert
// Assert
// Assert
- expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
})
it('should toggle dropdown state on trigger click', async () => {
@@ -370,16 +370,16 @@ describe('VarPicker', () => {
// Act
render(
)
- const trigger = screen.getByTestId('portal-trigger')
- expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
+ const trigger = screen.getByTestId('popover-trigger')
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
// Open dropdown
await user.click(trigger)
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
// Close dropdown
await user.click(trigger)
- expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
})
it('should preserve selected value when dropdown is closed without selection', async () => {
@@ -391,7 +391,7 @@ describe('VarPicker', () => {
render(
)
// Open and close dropdown without selecting anything
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
await user.click(trigger)
await user.click(trigger)
@@ -416,7 +416,7 @@ describe('VarPicker', () => {
// Assert
// Assert
expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument()
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
})
it('should handle empty options array', () => {
@@ -432,7 +432,7 @@ describe('VarPicker', () => {
// Assert
// Assert
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument()
})
@@ -485,7 +485,7 @@ describe('VarPicker', () => {
// Assert
// Assert
expect(screen.getByText('longVar'))!.toBeInTheDocument()
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
})
})
})
diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx
index 4edf5604da..c841617474 100644
--- a/web/app/components/apps/__tests__/app-card.spec.tsx
+++ b/web/app/components/apps/__tests__/app-card.spec.tsx
@@ -486,6 +486,15 @@ describe('AppCard', () => {
expect(screen.getByTestId('dropdown-menu')).toHaveAttribute('data-modal', 'false')
})
+ it('should reveal operations trigger when card receives keyboard focus', () => {
+ render(
)
+ const operationsTriggerWrapper = screen.getByTestId('dropdown-menu-trigger').closest('.absolute')
+
+ expect(operationsTriggerWrapper).toHaveClass('group-focus-within:pointer-events-auto')
+ expect(operationsTriggerWrapper).toHaveClass('group-focus-within:opacity-100')
+ expect(screen.getByTestId('dropdown-menu-trigger')).toHaveClass('focus-visible:ring-1')
+ })
+
it('should show edit option when dropdown menu is opened', async () => {
render(
)
diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx
index 06c5c8a9d8..f623f8ce53 100644
--- a/web/app/components/apps/app-card.tsx
+++ b/web/app/components/apps/app-card.tsx
@@ -425,7 +425,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
e.preventDefault()
getRedirection(isCurrentWorkspaceEditor, app, push)
}}
- className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg"
+ className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-lg"
>
@@ -524,7 +524,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
'absolute top-1/2 right-[6px] flex -translate-y-1/2 items-center transition-opacity',
isOperationsMenuOpen
? 'pointer-events-auto opacity-100'
- : 'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100',
+ : 'pointer-events-none opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100',
)}
>
@@ -533,7 +533,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
aria-label={t('operation.more', { ns: 'common' })}
className={cn(
isOperationsMenuOpen ? 'bg-state-base-hover shadow-none' : 'bg-transparent',
- 'flex h-8 w-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover',
+ 'flex h-8 w-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
)}
onClick={(e) => {
e.stopPropagation()
diff --git a/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx
index 4cbe4ce8d1..a54533b194 100644
--- a/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx
+++ b/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx
@@ -40,43 +40,24 @@ vi.mock('../../embedded-chatbot/theme/theme-context', () => ({
})),
}))
-// Mock PortalToFollowElem using React Context
-vi.mock('@/app/components/base/portal-to-follow-elem', async () => {
- const React = await import('react')
- const MockContext = React.createContext(false)
+vi.mock('@langgenius/dify-ui/dropdown-menu', () => import('@/__mocks__/base-ui-dropdown-menu'))
+vi.mock('@langgenius/dify-ui/tooltip', () => import('@/__mocks__/base-ui-tooltip'))
- return {
- PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => {
- return (
-
- {children}
-
- )
- },
- PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => {
- const open = React.useContext(MockContext)
- if (!open)
- return null
- return
{children}
- },
- PortalToFollowElemTrigger: ({ children, onClick, ...props }: { children: React.ReactNode, onClick: () => void } & React.HTMLAttributes
) => (
- {children}
- ),
- }
-})
-
-// Mock Modal to avoid Headless UI issues in tests
-vi.mock('@/app/components/base/modal', () => ({
- default: ({ children, isShow, title }: { children: React.ReactNode, isShow: boolean, title: React.ReactNode }) => {
- if (!isShow)
+// Mock Dialog to avoid Base UI focus/portal behavior in tests
+vi.mock('@langgenius/dify-ui/dialog', () => ({
+ Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) => {
+ if (!open)
return null
return (
-
- {!!title &&
{title}
}
+
{children}
)
},
+ DialogContent: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+ DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
,
}))
// Sidebar mock removed to use real component
diff --git a/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx b/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx
index b1c23a129b..b3f2f6fcb4 100644
--- a/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx
+++ b/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx
@@ -16,43 +16,24 @@ vi.mock('@/app/components/base/chat/chat-with-history/inputs-form/content', () =
default: () =>
InputsFormContent
,
}))
-// Mock PortalToFollowElem using React Context
-vi.mock('@/app/components/base/portal-to-follow-elem', async () => {
- const React = await import('react')
- const MockContext = React.createContext(false)
+vi.mock('@langgenius/dify-ui/dropdown-menu', () => import('@/__mocks__/base-ui-dropdown-menu'))
+vi.mock('@langgenius/dify-ui/tooltip', () => import('@/__mocks__/base-ui-tooltip'))
- return {
- PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => {
- return (
-
- {children}
-
- )
- },
- PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => {
- const open = React.useContext(MockContext)
- if (!open)
- return null
- return
{children}
- },
- PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
-
{children}
- ),
- }
-})
-
-// Mock Modal to avoid Headless UI issues in tests
-vi.mock('@/app/components/base/modal', () => ({
- default: ({ children, isShow, title }: { children: React.ReactNode, isShow: boolean, title: React.ReactNode }) => {
- if (!isShow)
+// Mock Dialog to avoid Base UI focus/portal behavior in tests
+vi.mock('@langgenius/dify-ui/dialog', () => ({
+ Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) => {
+ if (!open)
return null
return (
- {!!title &&
{title}
}
{children}
)
},
+ DialogContent: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+ DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
,
}))
const mockAppData: AppData = {
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx b/web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx
index 170f6d7fb5..33e25dbe01 100644
--- a/web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx
+++ b/web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx
@@ -490,7 +490,7 @@ describe('Sidebar Index', () => {
render(
)
await user.click(screen.getByTestId('rename-1'))
- expect(screen.getByTestId('modal')).toBeInTheDocument()
+ expect(screen.getByText('common.chat.renameConversation')).toBeInTheDocument()
})
it('should pass correct props to rename modal', async () => {
@@ -499,7 +499,9 @@ describe('Sidebar Index', () => {
await user.click(screen.getByTestId('rename-1'))
// The modal should have title and save/cancel
- expect(screen.getByTestId('modal')).toBeInTheDocument()
+ expect(screen.getByText('common.chat.renameConversation')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
})
it('should call handleRenameConversation with new name', async () => {
@@ -531,13 +533,13 @@ describe('Sidebar Index', () => {
render(
)
await user.click(screen.getByTestId('rename-1'))
- expect(screen.getByTestId('modal')).toBeInTheDocument()
+ expect(screen.getByText('common.chat.renameConversation')).toBeInTheDocument()
const cancelButton = screen.getByText('common.operation.cancel')
await user.click(cancelButton)
await waitFor(() => {
- expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
+ expect(screen.queryByText('common.chat.renameConversation')).not.toBeInTheDocument()
})
})
@@ -882,8 +884,7 @@ describe('RenameModal', () => {
/>,
)
- expect(screen.getByTestId('modal')).toBeInTheDocument()
- expect(screen.getByTestId('modal-title')).toHaveTextContent('common.chat.renameConversation')
+ expect(screen.getByText('common.chat.renameConversation')).toBeInTheDocument()
})
it('should handle empty placeholder translation fallback', () => {
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx b/web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx
index 5cf981363c..54822659f2 100644
--- a/web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx
+++ b/web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx
@@ -1,11 +1,15 @@
'use client'
import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
+import {
+ Dialog,
+ DialogContent,
+ DialogTitle,
+} from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
-import Modal from '@/app/components/base/modal'
type IRenameModalProps = {
isShow: boolean
@@ -27,24 +31,28 @@ const RenameModal: FC
= ({
const conversationNamePlaceholder = t('chat.conversationNamePlaceholder', { ns: 'common' }) || ''
return (
- !open && onClose()}
>
- {t('chat.conversationName', { ns: 'common' })}
- setTempName(e.target.value)}
- placeholder={conversationNamePlaceholder}
- />
+
+
+ {t('chat.renameConversation', { ns: 'common' })}
+
+ {t('chat.conversationName', { ns: 'common' })}
+ setTempName(e.target.value)}
+ placeholder={conversationNamePlaceholder}
+ />
-
- {t('operation.cancel', { ns: 'common' })}
- onSave(tempName)} loading={saveLoading}>{t('operation.save', { ns: 'common' })}
-
-
+
+ {t('operation.cancel', { ns: 'common' })}
+ onSave(tempName)} loading={saveLoading}>{t('operation.save', { ns: 'common' })}
+
+
+
)
}
export default React.memo(RenameModal)
diff --git a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx
index 9eaf5fe374..094c5c987b 100644
--- a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx
+++ b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx
@@ -441,9 +441,8 @@ describe('Operation', () => {
renderOperation()
const thumbDown = screen.getByTestId('operation-bar').querySelector('.i-ri-thumb-down-line')!.closest('button')!
await user.click(thumbDown)
- // Check if modal title/labels fallback works
- // Check if modal title/labels fallback works
- expect(screen.getByRole('tooltip'))!.toBeInTheDocument()
+ expect(screen.getByRole('dialog', { name: 'Provide Feedback' }))!.toBeInTheDocument()
+ expect(screen.getByLabelText('Feedback Content'))!.toBeInTheDocument()
mockT.mockImplementation(key => key)
})
})
diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx
index e7d1c17a3e..6804271d64 100644
--- a/web/app/components/base/chat/chat/answer/operation.tsx
+++ b/web/app/components/base/chat/chat/answer/operation.tsx
@@ -1,13 +1,17 @@
-import type { FC } from 'react'
+import type { ReactElement, ReactNode } from 'react'
import type {
ChatItem,
Feedback,
} from '../../types'
+import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
+import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import copy from 'copy-to-clipboard'
import {
memo,
+ useId,
useMemo,
useState,
} from 'react'
@@ -16,10 +20,8 @@ import EditReplyModal from '@/app/components/app/annotation/edit-annotation-moda
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import Log from '@/app/components/base/chat/chat/log'
import AnnotationCtrlButton from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button'
-import Modal from '@/app/components/base/modal/modal'
import NewAudioButton from '@/app/components/base/new-audio-button'
import Textarea from '@/app/components/base/textarea'
-import Tooltip from '@/app/components/base/tooltip'
import { useChatContext } from '../context'
type OperationProps = {
@@ -33,7 +35,25 @@ type OperationProps = {
noChatInput?: boolean
}
-const Operation: FC = ({
+type FeedbackTooltipProps = {
+ content: ReactNode
+ children: ReactElement
+}
+
+const feedbackTooltipClassName = 'max-w-[260px]'
+
+const FeedbackTooltip = ({ content, children }: FeedbackTooltipProps) => {
+ return (
+
+
+
+ {content}
+
+
+ )
+}
+
+function Operation({
item,
question,
index,
@@ -42,7 +62,7 @@ const Operation: FC = ({
contentWidth,
hasWorkflowProcess,
noChatInput,
-}) => {
+}: OperationProps) {
const { t } = useTranslation()
const {
config,
@@ -68,8 +88,8 @@ const Operation: FC = ({
const [userLocalFeedback, setUserLocalFeedback] = useState(feedback)
const [adminLocalFeedback, setAdminLocalFeedback] = useState(adminFeedback)
const [feedbackTarget, setFeedbackTarget] = useState<'user' | 'admin'>('user')
+ const feedbackTextareaId = useId()
- // Separate feedback types for display
const userFeedback = feedback
const content = useMemo(() => {
@@ -89,7 +109,11 @@ const Operation: FC = ({
const userFeedbackLabel = t('table.header.userRate', { ns: 'appLog' }) || 'User feedback'
const adminFeedbackLabel = t('table.header.adminRate', { ns: 'appLog' }) || 'Admin feedback'
- const feedbackTooltipClassName = 'max-w-[260px]'
+ const likeLabel = t('detail.operation.like', { ns: 'appLog' }) || 'Like'
+ const dislikeLabel = t('detail.operation.dislike', { ns: 'appLog' }) || 'Dislike'
+ const removeFeedbackLabel = t('operation.remove', { ns: 'common' }) || 'Remove'
+ const copyLabel = t('operation.copy', { ns: 'common' }) || 'Copy'
+ const regenerateLabel = t('operation.regenerate', { ns: 'common' }) || 'Regenerate'
const buildFeedbackTooltip = (feedbackData?: Feedback | null, label = userFeedbackLabel) => {
if (!feedbackData?.rating)
@@ -180,33 +204,35 @@ const Operation: FC = ({
>
{hasUserFeedback
? (
-
handleFeedback(null, undefined, 'user')}
>
{displayUserFeedback?.rating === 'like'
- ?
- :
}
+ ?
+ : }
-
+
)
: (
<>
handleLikeClick('user')}
>
-
+
handleDislikeClick('user')}
>
-
+
>
)}
@@ -218,68 +244,65 @@ const Operation: FC = ({
(hasAdminFeedback || hasUserFeedback) ? 'flex' : 'hidden group-hover:flex',
)}
>
- {/* User Feedback Display */}
{displayUserFeedback?.rating && (
-
{displayUserFeedback.rating === 'like'
? (
-
-
+
+
)
: (
-
-
+
+
)}
-
+
)}
- {/* Admin Feedback Controls */}
{displayUserFeedback?.rating &&
}
{hasAdminFeedback
? (
-
handleFeedback(null, undefined, 'admin')}
>
{adminLocalFeedback?.rating === 'like'
- ?
- :
}
+ ?
+ : }
-
+
)
: (
<>
-
handleLikeClick('admin')}
>
-
+
-
-
+
handleDislikeClick('admin')}
>
-
+
-
+
>
)}
@@ -300,18 +323,19 @@ const Operation: FC = ({
)}
{!humanInputFormDataList?.length && (
{
copy(content)
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
}}
data-testid="copy-btn"
>
-
+
)}
{!noChatInput && (
- onRegenerate?.(item)} data-testid="regenerate-btn">
-
+ onRegenerate?.(item)} data-testid="regenerate-btn">
+
)}
{config?.supportAnnotation && config.annotation_reply?.enabled && !humanInputFormDataList?.length && (
@@ -342,30 +366,56 @@ const Operation: FC = ({
onRemove={() => onAnnotationRemoved?.(index)}
/>
{isShowFeedbackModal && (
- {
+ if (!open)
+ handleFeedbackCancel()
+ }}
>
-
-
-
- {t('feedback.content', { ns: 'common' }) || 'Feedback Content'}
-
-
-
+
+
)}
>
)
diff --git a/web/app/components/base/chat/chat/citation/progress-tooltip.tsx b/web/app/components/base/chat/chat/citation/progress-tooltip.tsx
index fd75c2f3b0..75211b706e 100644
--- a/web/app/components/base/chat/chat/citation/progress-tooltip.tsx
+++ b/web/app/components/base/chat/chat/citation/progress-tooltip.tsx
@@ -1,11 +1,11 @@
import type { FC } from 'react'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@langgenius/dify-ui/tooltip'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
type ProgressTooltipProps = {
data: number
@@ -18,35 +18,41 @@ const ProgressTooltip: FC
= ({
const [open, setOpen] = useState(false)
return (
-
- setOpen(true)}
- onMouseLeave={() => setOpen(false)}
+ setOpen(true)}
+ onMouseLeave={() => setOpen(false)}
+ />
+ )}
>
-
-
-
-
+
-
-
-
- {t('chat.citation.hitScore', { ns: 'common' })}
- {' '}
- {data}
-
-
-
+ {data}
+
+
+ {t('chat.citation.hitScore', { ns: 'common' })}
+ {' '}
+ {data}
+
+
)
}
diff --git a/web/app/components/base/chat/chat/citation/tooltip.tsx b/web/app/components/base/chat/chat/citation/tooltip.tsx
index 16e122bc04..e1d76a9383 100644
--- a/web/app/components/base/chat/chat/citation/tooltip.tsx
+++ b/web/app/components/base/chat/chat/citation/tooltip.tsx
@@ -1,11 +1,11 @@
import type { FC } from 'react'
+import {
+ Tooltip as DifyTooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@langgenius/dify-ui/tooltip'
import * as React from 'react'
import { useState } from 'react'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
type TooltipProps = {
data: number | string
@@ -21,28 +21,34 @@ const Tooltip: FC
= ({
const [open, setOpen] = useState(false)
return (
-
- setOpen(true)}
- onMouseLeave={() => setOpen(false)}
+ setOpen(true)}
+ onMouseLeave={() => setOpen(false)}
+ />
+ )}
>
-
- {icon}
- {data}
-
-
-
-
- {text}
- {' '}
- {data}
-
-
-
+ {icon}
+ {data}
+
+
+ {text}
+ {' '}
+ {data}
+
+
)
}
diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/view-form-dropdown.spec.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/view-form-dropdown.spec.tsx
index 8e0ddedbe7..4368228070 100644
--- a/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/view-form-dropdown.spec.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/__tests__/view-form-dropdown.spec.tsx
@@ -7,7 +7,7 @@ vi.mock('../content', () => ({
default: () =>
,
}))
-// Note: PortalToFollowElem is mocked globally in vitest.setup.ts
+// Note: Popover is mocked globally in vitest.setup.ts
// to render children in the normal DOM flow when open is true.
describe('ViewFormDropdown', () => {
diff --git a/web/app/components/base/chip/__tests__/index.spec.tsx b/web/app/components/base/chip/__tests__/index.spec.tsx
index ac3a68292c..40ecbb7a33 100644
--- a/web/app/components/base/chip/__tests__/index.spec.tsx
+++ b/web/app/components/base/chip/__tests__/index.spec.tsx
@@ -40,7 +40,7 @@ describe('Chip', () => {
// Helper function to get the trigger element
const getTrigger = (container: HTMLElement) => {
- return container.querySelector('[data-state]')
+ return container.querySelector('[role="button"][aria-haspopup="menu"]') as HTMLElement | null
}
// Helper function to open dropdown panel
@@ -102,7 +102,7 @@ describe('Chip', () => {
// When showLeftIcon is false, there should be no filter icon before the text
const textElement = screen.getByText('All Items')
- const parent = textElement.closest('div[data-state]')
+ const parent = textElement.closest('[role="button"]')
const icons = parent?.querySelectorAll('svg')
// Should only have the arrow icon, not the filter icon
@@ -142,20 +142,20 @@ describe('Chip', () => {
it('should toggle dropdown panel on trigger click', () => {
const { container } = renderChip()
- // Initially closed - check data-state attribute
+ // Initially closed - check aria-expanded attribute
const trigger = getTrigger(container)
- expect(trigger)!.toHaveAttribute('data-state', 'closed')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'false')
// Open panel
openPanel(container)
- expect(trigger)!.toHaveAttribute('data-state', 'open')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'true')
// Panel items should be visible
expect(screen.getAllByText('All Items').length).toBeGreaterThan(1)
// Close panel
if (trigger)
fireEvent.click(trigger)
- expect(trigger)!.toHaveAttribute('data-state', 'closed')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'false')
})
it('should close panel after selecting an item', () => {
@@ -163,14 +163,14 @@ describe('Chip', () => {
openPanel(container)
const trigger = getTrigger(container)
- expect(trigger)!.toHaveAttribute('data-state', 'open')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'true')
// Click on an item in the dropdown panel
const activeItems = screen.getAllByText('Active')
// The second one should be in the dropdown
fireEvent.click(activeItems[activeItems.length - 1]!)
- expect(trigger)!.toHaveAttribute('data-state', 'closed')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'false')
})
})
@@ -208,7 +208,7 @@ describe('Chip', () => {
const { container } = renderChip({ value: 'active' })
const trigger = getTrigger(container)
- expect(trigger)!.toHaveAttribute('data-state', 'closed')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'false')
// Find the close icon (last SVG) and click its parent
const svgs = trigger?.querySelectorAll('svg')
@@ -220,7 +220,7 @@ describe('Chip', () => {
// Panel should remain closed
// Panel should remain closed
- expect(trigger)!.toHaveAttribute('data-state', 'closed')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'false')
expect(onClear).toHaveBeenCalledTimes(1)
})
@@ -232,17 +232,17 @@ describe('Chip', () => {
// Click 1: open
if (trigger)
fireEvent.click(trigger)
- expect(trigger)!.toHaveAttribute('data-state', 'open')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'true')
// Click 2: close
if (trigger)
fireEvent.click(trigger)
- expect(trigger)!.toHaveAttribute('data-state', 'closed')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'false')
// Click 3: open again
if (trigger)
fireEvent.click(trigger)
- expect(trigger)!.toHaveAttribute('data-state', 'open')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'true')
})
})
@@ -285,10 +285,10 @@ describe('Chip', () => {
// Closed by default
// Closed by default
- expect(trigger)!.toHaveAttribute('data-state', 'closed')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'false')
openPanel(container)
- expect(trigger)!.toHaveAttribute('data-state', 'open')
+ expect(trigger)!.toHaveAttribute('aria-expanded', 'true')
// Items should be duplicated (once in trigger, once in panel)
expect(screen.getAllByText('All Items').length).toBeGreaterThan(1)
})
@@ -327,7 +327,7 @@ describe('Chip', () => {
const { container } = renderChip({ items: [], value: '' })
// Trigger should still render
- const trigger = container.querySelector('[data-state]')
+ const trigger = getTrigger(container)
expect(trigger)!.toBeInTheDocument()
})
diff --git a/web/app/components/base/chip/index.tsx b/web/app/components/base/chip/index.tsx
index 50f4744b07..35ad48039b 100644
--- a/web/app/components/base/chip/index.tsx
+++ b/web/app/components/base/chip/index.tsx
@@ -1,12 +1,14 @@
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import { RiArrowDownSLine, RiCheckLine, RiCloseCircleFill, RiFilter3Line } from '@remixicon/react'
import { useMemo, useState } from 'react'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
export type Item = {
value: number | string
@@ -40,16 +42,14 @@ const Chip: FC = ({
}, [items, value])
return (
-
-
setOpen(v => !v)}
- className="block"
+ }
>
= ({
)}
-
-
-
-
- {items.map(item => (
-
{
- onSelect(item)
- setOpen(false)
- }}
- >
-
{item.name}
- {value === item.value &&
}
-
- ))}
-
-
-
+
+
+ {
+ const selected = items.find(item => item.value === nextValue)
+ if (selected)
+ onSelect(selected)
+ }}
+ className="max-h-72 overflow-auto p-1"
+ >
+ {items.map(item => (
+
+ {item.name}
+ {value === item.value && }
+
+ ))}
+
+
-
+
)
}
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 a4d1323e6a..8801fb62da 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
@@ -353,7 +353,9 @@ describe('BaseField', () => {
expect(screen.getByText('This is a warning')).toBeInTheDocument()
})
- it('should render tooltip when provided', async () => {
+ it('should render infotip when tooltip content is provided', async () => {
+ const user = userEvent.setup()
+
renderBaseField({
formSchema: {
type: FormTypeEnum.textInput,
@@ -366,8 +368,7 @@ describe('BaseField', () => {
expect(screen.getByText('Info')).toBeInTheDocument()
- const tooltipTrigger = screen.getByTestId('base-field-tooltip-trigger')
- fireEvent.mouseEnter(tooltipTrigger)
+ await user.click(screen.getByRole('button', { name: 'Extra info' }))
expect(screen.getByText('Extra info')).toBeInTheDocument()
})
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 c425eee574..da76e1f96f 100644
--- a/web/app/components/base/form/components/base/base-field.tsx
+++ b/web/app/components/base/form/components/base/base-field.tsx
@@ -20,10 +20,10 @@ import {
import { useTranslation } from 'react-i18next'
import CheckboxList from '@/app/components/base/checkbox-list'
import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/form/types'
+import { Infotip } from '@/app/components/base/infotip'
import Input from '@/app/components/base/input'
import Radio from '@/app/components/base/radio'
import RadioE from '@/app/components/base/radio/ui'
-import Tooltip from '@/app/components/base/tooltip'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
@@ -95,7 +95,7 @@ export type BaseFieldProps = {
formSchema: FormSchema
field: AnyFieldApi
disabled?: boolean
- onChange?: (field: string, value: any) => void
+ onChange?: (field: string, value: unknown) => void
fieldState?: FieldState
}
@@ -156,7 +156,7 @@ const BaseField = ({
}, [options])
const watchedValues = useStore(field.form.store, (s) => {
- const result: Record
= {}
+ const result: Record = {}
for (const variable of watchedVariables)
result[variable] = s.values[variable]
@@ -201,7 +201,7 @@ const BaseField = ({
}))
}, [dynamicOptionsData, renderI18nObject])
- const handleChange = useCallback((value: any) => {
+ const handleChange = useCallback((value: unknown) => {
field.handleChange(value)
onChange?.(field.name, value)
}, [field, onChange])
@@ -223,12 +223,10 @@ const BaseField = ({
*
)
}
- {tooltip && (
- {translatedTooltip} }
- triggerClassName="ml-0.5 w-4 h-4"
- />
+ {translatedTooltip && (
+
+ {translatedTooltip}
+
)}
diff --git a/web/app/components/base/modal/__tests__/modal.spec.tsx b/web/app/components/base/modal/__tests__/modal.spec.tsx
deleted file mode 100644
index 47a679c843..0000000000
--- a/web/app/components/base/modal/__tests__/modal.spec.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react'
-import Modal from '../modal'
-
-describe('Modal Component', () => {
- const defaultProps = {
- title: 'Test Modal',
- onClose: vi.fn(),
- onConfirm: vi.fn(),
- onCancel: vi.fn(),
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- describe('Render', () => {
- it('renders correctly with title and children', () => {
- render(
-
- Child Content
- ,
- )
-
- expect(screen.getByText('Test Modal')).toBeInTheDocument()
- expect(screen.getByTestId('modal-child')).toBeInTheDocument()
- expect(screen.getByText(/cancel/i)).toBeInTheDocument()
- expect(screen.getByText(/save/i)).toBeInTheDocument()
- })
-
- it('renders subTitle when provided', () => {
- render(
)
- expect(screen.getByText('Test Subtitle')).toBeInTheDocument()
- })
-
- it('renders and handles extra button', () => {
- const onExtraClick = vi.fn()
- render(
-
,
- )
-
- const extraBtn = screen.getByText('Extra Action')
- expect(extraBtn).toBeInTheDocument()
- fireEvent.click(extraBtn)
- expect(onExtraClick).toHaveBeenCalledTimes(1)
- })
-
- it('renders md size class and default extra button label', () => {
- const { container } = render(
-
,
- )
-
- expect(screen.getByText(/remove/i)).toBeInTheDocument()
- expect(container.querySelector('.w-\\[640px\\]')).toBeInTheDocument()
- })
-
- it('renders footerSlot and bottomSlot', () => {
- render(
-
Footer }
- bottomSlot={Bottom
}
- />,
- )
-
- expect(screen.getByTestId('footer-slot')).toBeInTheDocument()
- expect(screen.getByTestId('bottom-slot')).toBeInTheDocument()
- })
- })
-
- describe('Interactions', () => {
- it('calls onClose when close icon is clicked', () => {
- render( )
- const closeIcon = screen.getByTestId('close-icon').parentElement
- fireEvent.click(closeIcon!)
- expect(defaultProps.onClose).toHaveBeenCalledTimes(1)
- })
-
- it('calls onConfirm when confirm button is clicked', () => {
- render( )
- fireEvent.click(screen.getByText(/confirm/i))
- expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1)
- })
-
- it('calls onCancel when cancel button is clicked', () => {
- render( )
- fireEvent.click(screen.getByText('Cancel Me'))
- expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
- })
-
- it('handles clickOutsideNotClose logic', () => {
- const onClose = vi.fn()
- const { rerender } = render( )
-
- fireEvent.click(screen.getByRole('tooltip'))
- expect(onClose).toHaveBeenCalledTimes(1)
-
- onClose.mockClear()
- rerender( )
- fireEvent.click(screen.getByRole('tooltip'))
- expect(onClose).not.toHaveBeenCalled()
- })
-
- it('prevents propagation on internal container click', () => {
- const onClose = vi.fn()
- render( )
- fireEvent.click(screen.getByText('Test Modal'))
- expect(onClose).not.toHaveBeenCalled()
- })
- })
-
- describe('Props', () => {
- it('disables buttons when disabled prop is true', () => {
- render( )
- expect(screen.getByText(/cancel/i).closest('button')).toBeDisabled()
- expect(screen.getByText(/save/i).closest('button')).toBeDisabled()
- })
- })
-})
diff --git a/web/app/components/base/modal/modal.stories.tsx b/web/app/components/base/modal/modal.stories.tsx
deleted file mode 100644
index 7093e82f2d..0000000000
--- a/web/app/components/base/modal/modal.stories.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/nextjs-vite'
-import { useEffect, useState } from 'react'
-import Modal from './modal'
-
-const meta = {
- title: 'Base/Feedback/RichModal',
- component: Modal,
- parameters: {
- layout: 'fullscreen',
- docs: {
- description: {
- component: 'Full-featured modal with header, subtitle, customizable footer buttons, and optional extra action.',
- },
- },
- },
- tags: ['autodocs'],
- argTypes: {
- size: {
- control: 'radio',
- options: ['sm', 'md'],
- description: 'Defines the panel width.',
- },
- title: {
- control: 'text',
- description: 'Primary heading text.',
- },
- subTitle: {
- control: 'text',
- description: 'Secondary text below the title.',
- },
- confirmButtonText: {
- control: 'text',
- description: 'Label for the confirm button.',
- },
- cancelButtonText: {
- control: 'text',
- description: 'Label for the cancel button.',
- },
- showExtraButton: {
- control: 'boolean',
- description: 'Whether to render the extra button.',
- },
- extraButtonText: {
- control: 'text',
- description: 'Label for the extra button.',
- },
- extraButtonVariant: {
- control: 'select',
- options: ['primary', 'secondary', 'secondary-accent', 'ghost', 'ghost-accent', 'tertiary'],
- description: 'Visual style for the extra button.',
- },
- disabled: {
- control: 'boolean',
- description: 'Disables footer actions when true.',
- },
- footerSlot: {
- control: false,
- },
- bottomSlot: {
- control: false,
- },
- onClose: {
- control: false,
- description: 'Handler fired when the close icon or backdrop is clicked.',
- },
- onConfirm: {
- control: false,
- description: 'Handler fired when confirm is pressed.',
- },
- onCancel: {
- control: false,
- description: 'Handler fired when cancel is pressed.',
- },
- onExtraButtonClick: {
- control: false,
- description: 'Handler fired when the extra button is pressed.',
- },
- children: {
- control: false,
- },
- },
- args: {
- size: 'sm',
- title: 'Delete integration',
- subTitle: 'Disabling this integration will revoke access tokens and webhooks.',
- confirmButtonText: 'Delete integration',
- cancelButtonText: 'Cancel',
- showExtraButton: false,
- extraButtonText: 'Disable temporarily',
- extraButtonVariant: 'primary',
- disabled: false,
- onClose: () => console.log('Modal closed'),
- onConfirm: () => console.log('Confirm pressed'),
- onCancel: () => console.log('Cancel pressed'),
- onExtraButtonClick: () => console.log('Extra button pressed'),
- },
-} satisfies Meta
-
-export default meta
-type Story = StoryObj
-
-type ModalProps = React.ComponentProps
-
-const ModalDemo = (props: ModalProps) => {
- const [open, setOpen] = useState(false)
-
- useEffect(() => {
- if (props.disabled && open)
- setOpen(false)
- }, [props.disabled, open])
-
- const {
- onClose,
- onConfirm,
- onCancel,
- onExtraButtonClick,
- children,
- ...rest
- } = props
-
- const handleClose = () => {
- onClose?.()
- setOpen(false)
- }
-
- const handleConfirm = () => {
- onConfirm?.()
- setOpen(false)
- }
-
- const handleCancel = () => {
- onCancel?.()
- setOpen(false)
- }
-
- const handleExtra = () => {
- onExtraButtonClick?.()
- }
-
- return (
-
-
setOpen(true)}
- >
- Show rich modal
-
-
- {open && (
-
-
- Removing integrations immediately stops workflow automations related to this connection.
- Make sure no scheduled jobs depend on this integration before proceeding.
-
-
- All API credentials issued by this integration will be revoked.
- Historical logs remain accessible for auditing.
- You can re-enable the integration later with fresh credentials.
-
-
- )}
- />
- )}
-
- )
-}
-
-export const Default: Story = {
- render: args =>
,
-}
-
-export const WithExtraAction: Story = {
- render: args =>
,
- args: {
- showExtraButton: true,
- extraButtonVariant: 'secondary',
- extraButtonText: 'Disable only',
- footerSlot: (
-
Last synced 5 minutes ago
- ),
- },
- parameters: {
- docs: {
- description: {
- story: 'Illustrates the optional extra button and footer slot for advanced workflows.',
- },
- },
- },
-}
-
-export const MediumSized: Story = {
- render: args =>
,
- args: {
- size: 'md',
- subTitle: 'Use the larger width to surface forms with more fields or supporting descriptions.',
- bottomSlot: (
-
- Need finer control? Configure automation rules in the integration settings page.
-
- ),
- },
- parameters: {
- docs: {
- description: {
- story: 'Shows the medium sized panel and a populated `bottomSlot` for supplemental messaging.',
- },
- },
- },
-}
diff --git a/web/app/components/base/modal/modal.tsx b/web/app/components/base/modal/modal.tsx
deleted file mode 100644
index 10164e5f33..0000000000
--- a/web/app/components/base/modal/modal.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-/**
- * @deprecated Use `@langgenius/dify-ui/dialog` instead.
- * This component will be removed after migration is complete.
- * See: https://github.com/langgenius/dify/issues/32767
- */
-import type { ButtonProps } from '@langgenius/dify-ui/button'
-import { Button } from '@langgenius/dify-ui/button'
-import { cn } from '@langgenius/dify-ui/cn'
-import { noop } from 'es-toolkit/function'
-import { memo } from 'react'
-import { useTranslation } from 'react-i18next'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
-} from '@/app/components/base/portal-to-follow-elem'
-
-type ModalProps = {
- onClose?: () => void
- size?: 'sm' | 'md'
- title: string
- subTitle?: string
- children?: React.ReactNode
- confirmButtonText?: string
- onConfirm?: () => void
- cancelButtonText?: string
- onCancel?: () => void
- showExtraButton?: boolean
- extraButtonText?: string
- extraButtonVariant?: ButtonProps['variant']
- onExtraButtonClick?: () => void
- footerSlot?: React.ReactNode
- bottomSlot?: React.ReactNode
- disabled?: boolean
- containerClassName?: string
- wrapperClassName?: string
- clickOutsideNotClose?: boolean
-}
-const Modal = ({
- onClose,
- size = 'sm',
- title,
- subTitle,
- children,
- confirmButtonText,
- onConfirm,
- cancelButtonText,
- onCancel,
- showExtraButton,
- extraButtonVariant = 'primary',
- extraButtonText,
- onExtraButtonClick,
- footerSlot,
- bottomSlot,
- disabled,
- containerClassName,
- wrapperClassName,
- clickOutsideNotClose = false,
-}: ModalProps) => {
- const { t } = useTranslation()
-
- return (
-
-
- e.stopPropagation()}
- >
-
- {title}
- {
- subTitle && (
-
- {subTitle}
-
- )
- }
-
-
-
-
- {
- !!children && (
-
{children}
- )
- }
-
-
- {footerSlot}
-
-
- {
- showExtraButton && (
- <>
-
- {extraButtonText || t('operation.remove', { ns: 'common' })}
-
-
- >
- )
- }
-
- {cancelButtonText || t('operation.cancel', { ns: 'common' })}
-
-
- {confirmButtonText || t('operation.save', { ns: 'common' })}
-
-
-
- {!!bottomSlot && (
-
- {bottomSlot}
-
- )}
-
-
-
- )
-}
-
-export default memo(Modal)
diff --git a/web/app/components/base/portal-to-follow-elem/__tests__/index.spec.tsx b/web/app/components/base/portal-to-follow-elem/__tests__/index.spec.tsx
deleted file mode 100644
index 3c1b09d947..0000000000
--- a/web/app/components/base/portal-to-follow-elem/__tests__/index.spec.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import { cleanup, fireEvent, render } from '@testing-library/react'
-import * as React from 'react'
-import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '..'
-
-type MockFloatingData = {
- middlewareData?: {
- hide?: {
- referenceHidden?: boolean
- }
- }
-}
-
-let mockFloatingData: MockFloatingData = {}
-const useFloatingMock = vi.fn()
-
-vi.mock('@floating-ui/react', async (importOriginal) => {
- const actual = await importOriginal
()
- return {
- ...actual,
- useFloating: (options: unknown) => {
- useFloatingMock(options)
- const data = actual.useFloating(options as Parameters[0])
- return {
- ...data,
- ...mockFloatingData,
- middlewareData: {
- ...data.middlewareData,
- ...mockFloatingData.middlewareData,
- },
- }
- },
- }
-})
-
-afterEach(cleanup)
-
-describe('PortalToFollowElem', () => {
- describe('Context and Provider', () => {
- it('should throw error when using context outside provider', () => {
- // Suppress console.error for this test
- const originalError = console.error
- console.error = vi.fn()
-
- expect(() => {
- render(
- Trigger ,
- )
- }).toThrow('PortalToFollowElem components must be wrapped in ')
-
- console.error = originalError
- })
-
- it('should not throw when used within provider', () => {
- expect(() => {
- render(
-
- Trigger
- ,
- )
- }).not.toThrow()
- })
- })
-
- describe('PortalToFollowElemTrigger', () => {
- it('should render children correctly', () => {
- const { getByText } = render(
-
- Trigger Text
- ,
- )
- expect(getByText('Trigger Text'))!.toBeInTheDocument()
- })
-
- it('should handle asChild prop correctly', () => {
- const { getByRole } = render(
-
-
- Button Trigger
-
- ,
- )
-
- expect(getByRole('button'))!.toHaveTextContent('Button Trigger')
- })
- })
-
- describe('PortalToFollowElemContent', () => {
- it('should not render content when closed', () => {
- const { queryByText } = render(
-
- Trigger
- Popup Content
- ,
- )
-
- expect(queryByText('Popup Content')).not.toBeInTheDocument()
- })
-
- it('should render content when open', () => {
- const { getByText } = render(
-
- Trigger
- Popup Content
- ,
- )
-
- expect(getByText('Popup Content'))!.toBeInTheDocument()
- })
- })
-
- describe('Controlled behavior', () => {
- it('should call onOpenChange when interaction happens', () => {
- const handleOpenChange = vi.fn()
-
- const { getByText } = render(
-
- Hover Me
- Content
- ,
- )
-
- fireEvent.mouseEnter(getByText('Hover Me'))
- expect(handleOpenChange).toHaveBeenCalled()
-
- fireEvent.mouseLeave(getByText('Hover Me'))
- expect(handleOpenChange).toHaveBeenCalled()
- })
- })
-
- describe('Configuration options', () => {
- it('should accept placement prop', () => {
- render(
-
- Trigger
- ,
- )
-
- expect(useFloatingMock).toHaveBeenCalledWith(
- expect.objectContaining({
- placement: 'top-start',
- }),
- )
- })
-
- it('should handle triggerPopupSameWidth prop', () => {
- render(
-
- Trigger
- Content
- ,
- )
-
- type SizeMiddleware = {
- name: 'size'
- options: [{
- apply: (args: {
- elements: { floating: { style: Record } }
- rects: { reference: { width: number } }
- availableHeight: number
- }) => void
- }]
- }
-
- const sizeMiddleware = useFloatingMock.mock.calls[0]![0].middleware.find(
- (m: { name: string }) => m.name === 'size',
- ) as SizeMiddleware
- expect(sizeMiddleware).toBeDefined()
-
- // Manually trigger the apply function to cover line 81-82
- const mockElements = {
- floating: { style: {} as Record },
- }
- const mockRects = {
- reference: { width: 100 },
- }
- sizeMiddleware.options[0].apply({
- elements: mockElements,
- rects: mockRects,
- availableHeight: 500,
- })
-
- expect(mockElements.floating.style.width).toBe('100px')
- expect(mockElements.floating.style.maxHeight).toBe('500px')
- })
- })
-
- describe('PortalToFollowElemTrigger asChild', () => {
- it('should render correct data-state when open', () => {
- const { getByRole } = render(
-
-
- Trigger
-
- ,
- )
- expect(getByRole('button'))!.toHaveAttribute('data-state', 'open')
- })
-
- it('should handle missing ref on child', () => {
- const { getByRole } = render(
-
-
- Trigger
-
- ,
- )
- expect(getByRole('button'))!.toBeInTheDocument()
- })
- })
-
- describe('Visibility', () => {
- it('should hide content when reference is hidden', () => {
- mockFloatingData = {
- middlewareData: {
- hide: { referenceHidden: true },
- },
- }
-
- const { getByTestId } = render(
-
- Trigger
- Hidden Content
- ,
- )
-
- expect(getByTestId('content'))!.toHaveStyle('visibility: hidden')
- mockFloatingData = {}
- })
- })
-})
diff --git a/web/app/components/base/portal-to-follow-elem/index.stories.tsx b/web/app/components/base/portal-to-follow-elem/index.stories.tsx
deleted file mode 100644
index 8f0fc48921..0000000000
--- a/web/app/components/base/portal-to-follow-elem/index.stories.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/nextjs-vite'
-import { useState } from 'react'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '.'
-
-const TooltipCard = ({ title, description }: { title: string, description: string }) => (
-
-
- {title}
-
-
{description}
-
-)
-
-const PortalDemo = ({
- placement = 'bottom',
- triggerPopupSameWidth = false,
-}: {
- placement?: Parameters[0]['placement']
- triggerPopupSameWidth?: boolean
-}) => {
- const [controlledOpen, setControlledOpen] = useState(false)
-
- return (
-
-
-
-
- Hover me
-
-
-
-
-
-
-
-
- setControlledOpen(prev => !prev)}
- >
- Controlled toggle
-
-
-
-
-
-
-
-
- )
-}
-
-const meta = {
- title: 'Base/Feedback/PortalToFollowElem',
- component: PortalDemo,
- parameters: {
- layout: 'centered',
- docs: {
- description: {
- component: 'Floating UI based portal that tracks trigger positioning. Demonstrates both hover-driven and controlled usage.',
- },
- },
- },
- argTypes: {
- placement: {
- control: 'select',
- options: ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end'],
- },
- triggerPopupSameWidth: { control: 'boolean' },
- },
- args: {
- placement: 'bottom',
- triggerPopupSameWidth: false,
- },
- tags: ['autodocs'],
-} satisfies Meta
-
-export default meta
-type Story = StoryObj
-
-export const Playground: Story = {}
-
-export const SameWidthPanel: Story = {
- args: {
- triggerPopupSameWidth: true,
- },
-}
diff --git a/web/app/components/base/portal-to-follow-elem/index.tsx b/web/app/components/base/portal-to-follow-elem/index.tsx
deleted file mode 100644
index 866f22bcc5..0000000000
--- a/web/app/components/base/portal-to-follow-elem/index.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-'use client'
-/**
- * @deprecated Use semantic overlay primitives from `@langgenius/dify-ui/*` instead.
- * This component will be removed after migration is complete.
- * See: https://github.com/langgenius/dify/issues/32767
- *
- * Migration guide:
- * - Tooltip → `@langgenius/dify-ui/tooltip`
- * - Menu/Dropdown → `@langgenius/dify-ui/dropdown-menu`
- * - Popover → `@langgenius/dify-ui/popover`
- * - Dialog/Modal → `@langgenius/dify-ui/dialog`
- * - Select → `@langgenius/dify-ui/select`
- */
-import type { OffsetOptions, Placement } from '@floating-ui/react'
-import {
- autoUpdate,
- flip,
- FloatingPortal,
- offset,
- shift,
- size,
- useDismiss,
- useFloating,
- useFocus,
- useHover,
- useInteractions,
- useMergeRefs,
- useRole,
-} from '@floating-ui/react'
-
-import { cn } from '@langgenius/dify-ui/cn'
-import * as React from 'react'
-import { useCallback, useState } from 'react'
-
-type PortalToFollowElemOptions = {
- /*
- * top, bottom, left, right
- * start, end. Default is middle
- * combine: top-start, top-end
- */
- placement?: Placement
- open?: boolean
- offset?: number | OffsetOptions
- onOpenChange?: (open: boolean) => void
- triggerPopupSameWidth?: boolean
-}
-
-/** @deprecated Use semantic overlay primitives instead. See #32767. */
-function usePortalToFollowElem({
- placement = 'bottom',
- open: controlledOpen,
- offset: offsetValue = 0,
- onOpenChange: setControlledOpen,
- triggerPopupSameWidth,
-}: PortalToFollowElemOptions = {}) {
- const [localOpen, setLocalOpen] = useState(false)
- const open = controlledOpen ?? localOpen
- const handleOpenChange = useCallback((newOpen: boolean) => {
- setLocalOpen(newOpen)
- setControlledOpen?.(newOpen)
- }, [setControlledOpen, setLocalOpen])
-
- const data = useFloating({
- placement,
- open,
- onOpenChange: handleOpenChange,
- whileElementsMounted: autoUpdate,
- middleware: [
- offset(offsetValue),
- flip({
- crossAxis: placement.includes('-'),
- fallbackAxisSideDirection: 'start',
- padding: 5,
- }),
- shift({ padding: 5 }),
- size({
- apply({ rects, elements, availableHeight }) {
- Object.assign(elements.floating.style, {
- maxHeight: `${Math.max(0, availableHeight)}px`,
- overflowY: 'auto',
- ...(triggerPopupSameWidth && { width: `${rects.reference.width}px` }),
- })
- },
- }),
- ],
- })
-
- const context = data.context
-
- const hover = useHover(context, {
- move: false,
- enabled: controlledOpen === undefined,
- })
- const focus = useFocus(context, {
- enabled: controlledOpen === undefined,
- })
- const dismiss = useDismiss(context)
- const role = useRole(context, { role: 'tooltip' })
-
- const interactions = useInteractions([hover, focus, dismiss, role])
-
- return React.useMemo(
- () => ({
- open,
- setOpen: handleOpenChange,
- ...interactions,
- ...data,
- }),
- [open, handleOpenChange, interactions, data],
- )
-}
-
-type ContextType = ReturnType | null
-
-const PortalToFollowElemContext = React.createContext(null)
-
-function usePortalToFollowElemContext() {
- const context = React.useContext(PortalToFollowElemContext)
-
- if (context == null)
- throw new Error('PortalToFollowElem components must be wrapped in ')
-
- return context
-}
-
-/** @deprecated Use semantic overlay primitives instead. See #32767. */
-export function PortalToFollowElem({
- children,
- ...options
-}: { children: React.ReactNode } & PortalToFollowElemOptions) {
- // This can accept any props as options, e.g. `placement`,
- // or other positioning options.
- const tooltip = usePortalToFollowElem(options)
- return (
-
- {children}
-
- )
-}
-
-/** @deprecated Use semantic overlay primitives instead. See #32767. */
-export const PortalToFollowElemTrigger = (
- {
- ref: propRef,
- children,
- asChild = false,
- ...props
- }: React.HTMLProps & { ref?: React.RefObject, asChild?: boolean },
-) => {
- const context = usePortalToFollowElemContext()
- const childElement = React.isValidElement<{ ref?: React.Ref }>(children)
- ? children
- : null
- const childrenRef = childElement?.props.ref
- const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef])
-
- // `asChild` allows the user to pass any element as the anchor
- if (asChild && childElement) {
- const childProps = (childElement.props ?? {}) as Record
- return React.cloneElement(
- childElement,
- context.getReferenceProps({
- ref,
- ...props,
- ...childProps,
- 'data-state': context.open ? 'open' : 'closed',
- } as React.HTMLProps),
- )
- }
-
- return (
-
- {children}
-
- )
-}
-PortalToFollowElemTrigger.displayName = 'PortalToFollowElemTrigger'
-
-/** @deprecated Use semantic overlay primitives instead. See #32767. */
-export const PortalToFollowElemContent = (
- {
- ref: propRef,
- style,
- ...props
- }: React.HTMLProps & {
- ref?: React.RefObject
- },
-) => {
- const context = usePortalToFollowElemContext()
- const ref = useMergeRefs([context.refs.setFloating, propRef])
-
- if (!context.open)
- return null
-
- const body = document.body
-
- return (
-
-
-
- )
-}
-
-PortalToFollowElemContent.displayName = 'PortalToFollowElemContent'
diff --git a/web/app/components/base/sort/__tests__/index.spec.tsx b/web/app/components/base/sort/__tests__/index.spec.tsx
index e51ec23805..18dfd242ce 100644
--- a/web/app/components/base/sort/__tests__/index.spec.tsx
+++ b/web/app/components/base/sort/__tests__/index.spec.tsx
@@ -20,9 +20,7 @@ describe('Sort component — real portal integration', () => {
// helper: returns a non-null HTMLElement or throws with a clear message
const getTriggerWrapper = (): HTMLElement => {
- const labelNode = screen.getByText('appLog.filter.sortBy')
- // try to find a reasonable wrapper element; prefer '.block' but fallback to any ancestor div
- const wrapper = labelNode.closest('.block') ?? labelNode.closest('div')
+ const wrapper = screen.getByRole('button', { name: /appLog\.filter\.sortBy/i })
if (!wrapper)
throw new Error('Trigger wrapper element not found for "Sort by" label')
return wrapper as HTMLElement
@@ -49,32 +47,30 @@ describe('Sort component — real portal integration', () => {
expect(sortButton.querySelector('svg')).toBeInTheDocument()
})
- it('opens and closes the tooltip (portal mounts to document.body)', async () => {
+ it('opens and closes the menu', async () => {
const { user, getTriggerWrapper } = setup()
await user.click(getTriggerWrapper())
- const tooltip = await screen.findByRole('tooltip')
- expect(tooltip).toBeInTheDocument()
- expect(document.body.contains(tooltip)).toBe(true)
+ expect(await screen.findByText('Name')).toBeInTheDocument()
// clicking the trigger again should close it
await user.click(getTriggerWrapper())
- await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
+ await waitFor(() => expect(screen.queryByText('Name')).not.toBeInTheDocument())
})
it('renders options and calls onSelect with descending prefix when order is "-"', async () => {
const { user, onSelect, getTriggerWrapper } = setup({ order: '-' })
await user.click(getTriggerWrapper())
- const tooltip = await screen.findByRole('tooltip')
+ await screen.findByText('Name')
mockItems.forEach((item) => {
- expect(within(tooltip).getByText(item.name)).toBeInTheDocument()
+ expect(within(document.body).getAllByText(item.name).length).toBeGreaterThan(0)
})
- await user.click(within(tooltip).getByText('Name'))
+ await user.click(screen.getByText('Name'))
expect(onSelect).toHaveBeenCalledWith('-name')
- await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
+ await waitFor(() => expect(screen.queryByText('Name')).not.toBeInTheDocument())
})
it('toggles sorting order: ascending -> descending via right-side button', async () => {
@@ -93,10 +89,10 @@ describe('Sort component — real portal integration', () => {
const { user, getTriggerWrapper } = setup({ value: 'status' })
await user.click(getTriggerWrapper())
- const tooltip = await screen.findByRole('tooltip')
+ await screen.findByText('Name')
- const statusRow = within(tooltip).getByText('Status').closest('.flex')
- const nameRow = within(tooltip).getByText('Name').closest('.flex')
+ const statusRow = screen.getAllByText('Status').at(-1)?.closest('.flex')
+ const nameRow = screen.getByText('Name').closest('.flex')
if (!statusRow)
throw new Error('Status option row not found in menu')
@@ -120,9 +116,9 @@ describe('Sort component — real portal integration', () => {
const { user, onSelect, getTriggerWrapper } = setup({ order: undefined })
await user.click(getTriggerWrapper())
- const tooltip = await screen.findByRole('tooltip')
+ await screen.findByText('Name')
- await user.click(within(tooltip).getByText('Name'))
+ await user.click(screen.getByText('Name'))
expect(onSelect).toHaveBeenCalled()
expect(onSelect).toHaveBeenCalledWith(expect.stringMatching(/name$/))
@@ -131,11 +127,10 @@ describe('Sort component — real portal integration', () => {
it('clicking outside the open menu closes the portal', async () => {
const { user, getTriggerWrapper } = setup()
await user.click(getTriggerWrapper())
- const tooltip = await screen.findByRole('tooltip')
- expect(tooltip).toBeInTheDocument()
+ expect(await screen.findByText('Name')).toBeInTheDocument()
// click outside: body click should close the tooltip
await user.click(document.body)
- await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
+ await waitFor(() => expect(screen.queryByText('Name')).not.toBeInTheDocument())
})
})
diff --git a/web/app/components/base/sort/index.tsx b/web/app/components/base/sort/index.tsx
index ef9aebb0d7..69cf4dd220 100644
--- a/web/app/components/base/sort/index.tsx
+++ b/web/app/components/base/sort/index.tsx
@@ -1,13 +1,15 @@
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import { RiArrowDownSLine, RiCheckLine, RiSortAsc, RiSortDesc } from '@remixicon/react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
type Item = {
value: number | string
@@ -35,16 +37,14 @@ const Sort: FC = ({
return (
-
-
setOpen(v => !v)}
- className="block"
+ }
>
= ({
-
-
-
-
- {items.map(item => (
-
{
- onSelect(`${order}${item.value}`)
- setOpen(false)
- }}
- >
-
{item.name}
- {value === item.value &&
}
-
- ))}
-
-
-
+
+
+ onSelect(`${order}${nextValue}`)}
+ className="max-h-72 overflow-auto p-1"
+ >
+ {items.map(item => (
+
+ {item.name}
+ {value === item.value && }
+
+ ))}
+
+
-
+
onSelect(`${order ? '' : '-'}${value}`)}>
{!order &&
}
{order &&
}
diff --git a/web/app/components/base/tooltip/index.tsx b/web/app/components/base/tooltip/index.tsx
index 0bf7659635..85c63cdeaf 100644
--- a/web/app/components/base/tooltip/index.tsx
+++ b/web/app/components/base/tooltip/index.tsx
@@ -1,19 +1,28 @@
'use client'
+import type { Placement } from '@langgenius/dify-ui/popover'
/**
* @deprecated Use `@langgenius/dify-ui/tooltip` instead.
* This component will be removed after migration is complete.
* See: https://github.com/langgenius/dify/issues/32767
*/
-import type { OffsetOptions, Placement } from '@floating-ui/react'
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import { RiQuestionLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
-import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { tooltipManager } from './TooltipManager'
+type TooltipOffset = number | {
+ mainAxis?: number
+ crossAxis?: number
+}
+
type TooltipProps = {
position?: Placement
triggerMethod?: 'hover' | 'click'
@@ -25,7 +34,7 @@ type TooltipProps = {
popupClassName?: string
portalContentClassName?: string
noDecoration?: boolean
- offset?: OffsetOptions
+ offset?: TooltipOffset
needsDelay?: boolean
asChild?: boolean
}
@@ -46,6 +55,9 @@ const Tooltip: FC
= ({
needsDelay = true,
}) => {
const [open, setOpen] = useState(false)
+ const resolvedOffset = offset ?? 8
+ const sideOffset = typeof resolvedOffset === 'number' ? resolvedOffset : (resolvedOffset.mainAxis ?? 0)
+ const alignOffset = typeof resolvedOffset === 'number' ? 0 : (resolvedOffset.crossAxis ?? 0)
const [isHoverPopup, {
setTrue: setHoverPopup,
setFalse: setNotHoverPopup,
@@ -81,6 +93,16 @@ const Tooltip: FC = ({
}, [clearCloseTimeout])
const close = () => setOpen(false)
+ const handleOpenChange = (nextOpen: boolean) => {
+ if (disabled) {
+ setOpen(false)
+ return
+ }
+ if (triggerMethod === 'click')
+ setOpen(nextOpen)
+ else if (!nextOpen)
+ setOpen(false)
+ }
const handleLeave = (isTrigger: boolean) => {
if (isTrigger)
@@ -105,52 +127,104 @@ const Tooltip: FC = ({
tooltipManager.clear(close)
}
}
+ const handleTriggerMouseEnter = () => {
+ if (triggerMethod === 'hover') {
+ clearCloseTimeout()
+ setHoverTrigger()
+ tooltipManager.register(close)
+ setOpen(true)
+ }
+ }
+ const handleTriggerMouseLeave = () => {
+ if (triggerMethod === 'hover')
+ handleLeave(true)
+ }
+ const handlePopupMouseEnter = () => {
+ if (triggerMethod === 'hover') {
+ clearCloseTimeout()
+ setHoverPopup()
+ }
+ }
+ const handlePopupMouseLeave = () => {
+ if (triggerMethod === 'hover')
+ handleLeave(false)
+ }
+
+ const fallbackTrigger = (
+
+
+
+ )
+ const triggerContent = children || fallbackTrigger
+ const childElement = React.isValidElement>(triggerContent)
+ ? triggerContent
+ : fallbackTrigger
+ const nativeButton = typeof childElement.type !== 'string' || childElement.type === 'button'
+
+ const renderAsChildTrigger = () => {
+ const childProps = childElement.props
+ return React.cloneElement(childElement, {
+ onMouseEnter: (event: React.MouseEvent) => {
+ childProps.onMouseEnter?.(event)
+ handleTriggerMouseEnter()
+ },
+ onMouseLeave: (event: React.MouseEvent) => {
+ childProps.onMouseLeave?.(event)
+ handleTriggerMouseLeave()
+ },
+ })
+ }
+ const effectiveOpen = !disabled && open
return (
-
- triggerMethod === 'click' && setOpen(v => !v)}
- onMouseEnter={() => {
- if (triggerMethod === 'hover') {
- clearCloseTimeout()
- setHoverTrigger()
- tooltipManager.register(close)
- setOpen(true)
- }
- }}
- onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)}
- asChild={asChild}
- className={!asChild ? triggerClassName : ''}
- >
- {children ||
}
-
-
- {!!popupContent && (
- {
- if (triggerMethod === 'hover') {
- clearCloseTimeout()
- setHoverPopup()
- }
- }}
- onMouseLeave={() => triggerMethod === 'hover' && handleLeave(false)}
- >
- {popupContent}
-
- )}
-
-
+ {asChild
+ ? (
+
+ )
+ : (
+
+ )}
+ >
+ {triggerContent}
+
+ )}
+ {effectiveOpen && !!popupContent && (
+
+ {popupContent}
+
+ )}
+
)
}
diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx
index 49b0cb0789..0db8f54df4 100644
--- a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx
@@ -18,43 +18,7 @@ vi.mock('@/app/components/plugins/plugin-auth', () => ({
},
}))
-// Mock portal-to-follow-elem - use React state to properly handle open/close
-vi.mock('@/app/components/base/portal-to-follow-elem', () => {
- const MockPortalToFollowElem = ({ children, open }: { children: React.ReactNode, open: boolean }) => {
- return (
-
- {React.Children.map(children, (child) => {
- if (!React.isValidElement(child))
- return null
- return React.cloneElement(child as React.ReactElement<{ __portalOpen?: boolean }>, { __portalOpen: open })
- })}
-
- )
- }
-
- const MockPortalToFollowElemTrigger = ({ children, onClick, className, __portalOpen }: { children: React.ReactNode, onClick?: React.MouseEventHandler, className?: string, __portalOpen?: boolean }) => (
-
- {children}
-
- )
-
- const MockPortalToFollowElemContent = ({ children, className, __portalOpen }: { children: React.ReactNode, className?: string, __portalOpen?: boolean }) => {
- // Match actual behavior: returns null when not open
- if (!__portalOpen)
- return null
- return (
-
- {children}
-
- )
- }
-
- return {
- PortalToFollowElem: MockPortalToFollowElem,
- PortalToFollowElemTrigger: MockPortalToFollowElemTrigger,
- PortalToFollowElemContent: MockPortalToFollowElemContent,
- }
-})
+vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
// CredentialIcon - imported directly (not mocked)
// This is a simple UI component with no external dependencies
@@ -97,8 +61,8 @@ describe('CredentialSelector', () => {
render( )
- expect(screen.getByTestId('portal-root'))!.toBeInTheDocument()
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
})
it('should render current credential name in trigger', () => {
@@ -134,7 +98,7 @@ describe('CredentialSelector', () => {
render( )
- expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
})
it('should render all credentials in dropdown when opened', () => {
@@ -142,12 +106,12 @@ describe('CredentialSelector', () => {
render( )
// Act - Click trigger to open dropdown
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Assert - All credentials should be visible (current credential appears in both trigger and list)
// Assert - All credentials should be visible (current credential appears in both trigger and list)
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
// 3 in dropdown list + 1 in trigger (current) = 4 total
expect(screen.getAllByText(/Credential \d/)).toHaveLength(4)
})
@@ -212,7 +176,7 @@ describe('CredentialSelector', () => {
})
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Assert - 5 in dropdown + 1 in trigger (current credential appears twice)
@@ -238,7 +202,7 @@ describe('CredentialSelector', () => {
render( )
// Act - Open dropdown
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
const credential2 = screen.getByText('Credential 2')
@@ -256,11 +220,11 @@ describe('CredentialSelector', () => {
render( )
// Act - Open dropdown and select credential
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Get the dropdown item using within() to scope query to portal content
- const portalContent = screen.getByTestId('portal-content')
+ const portalContent = screen.getByTestId('popover-content')
const credentialOption = within(portalContent).getByText(credentialName)
fireEvent.click(credentialOption)
@@ -277,7 +241,7 @@ describe('CredentialSelector', () => {
render( )
// Act - Open dropdown and select Credential 1
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
const credential1 = screen.getByText('Credential 1')
@@ -326,15 +290,15 @@ describe('CredentialSelector', () => {
// Assert - Initially closed
// Assert - Initially closed
// Assert - Initially closed
- expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
// Act - Click trigger
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Assert - Now open
// Assert - Now open
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
it('should call onCredentialChange when clicking a credential item', () => {
@@ -342,7 +306,7 @@ describe('CredentialSelector', () => {
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
const credential2 = screen.getByText('Credential 2')
fireEvent.click(credential2)
@@ -357,10 +321,10 @@ describe('CredentialSelector', () => {
render( )
// Act - Open and select
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
const credential2 = screen.getByText('Credential 2')
fireEvent.click(credential2)
@@ -374,7 +338,7 @@ describe('CredentialSelector', () => {
render( )
// Act - Rapid clicks
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
fireEvent.click(trigger)
fireEvent.click(trigger)
@@ -395,7 +359,7 @@ describe('CredentialSelector', () => {
render( )
// Act & Assert - Select Credential 1 (different from current)
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
const credential1 = screen.getByText('Credential 1')
@@ -499,7 +463,7 @@ describe('CredentialSelector', () => {
render( )
// Act - Open dropdown and select
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
const credential = screen.getByText('Credential 2')
fireEvent.click(credential)
@@ -519,7 +483,7 @@ describe('CredentialSelector', () => {
rerender( )
// Open and select
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
const credential = screen.getByText('Credential 2')
fireEvent.click(credential)
@@ -651,7 +615,7 @@ describe('CredentialSelector', () => {
rerender( )
// Open and select
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
const credential = screen.getByText('Credential 2')
fireEvent.click(credential)
@@ -672,7 +636,7 @@ describe('CredentialSelector', () => {
// Assert - Should render without crashing
// Assert - Should render without crashing
- expect(screen.getByTestId('portal-root'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle undefined avatar_url in credential', () => {
@@ -713,7 +677,7 @@ describe('CredentialSelector', () => {
// Assert - Should render without crashing
// Assert - Should render without crashing
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
})
it('should handle very long credential name', () => {
@@ -788,7 +752,7 @@ describe('CredentialSelector', () => {
})
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Get all "Same Name" elements
@@ -807,7 +771,7 @@ describe('CredentialSelector', () => {
const props = createDefaultProps({ onCredentialChange: mockOnChange })
const { unmount } = render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
unmount()
@@ -832,7 +796,7 @@ describe('CredentialSelector', () => {
// Assert - Should render without crashing
// Assert - Should render without crashing
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
})
})
@@ -843,7 +807,7 @@ describe('CredentialSelector', () => {
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
expect(trigger)!.toHaveClass('overflow-hidden')
})
@@ -852,19 +816,21 @@ describe('CredentialSelector', () => {
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
expect(trigger)!.toHaveClass('grow')
})
- it('should apply z-10 class to dropdown content', () => {
+ it('should configure dropdown placement through popover props', () => {
const props = createDefaultProps()
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
- const content = screen.getByTestId('portal-content')
- expect(content)!.toHaveClass('z-10')
+ const content = screen.getByTestId('popover-content')
+ expect(content)!.toHaveAttribute('data-placement', 'bottom-start')
+ expect(content)!.toHaveAttribute('data-side-offset', '4')
+ expect(content)!.not.toHaveClass('z-10')
})
})
@@ -885,11 +851,11 @@ describe('CredentialSelector', () => {
render( )
// Assert - Initially closed
- const portalRoot = screen.getByTestId('portal-root')
+ const portalRoot = screen.getByTestId('popover')
expect(portalRoot)!.toHaveAttribute('data-open', 'false')
// Act - Open
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Assert - Now open
@@ -901,7 +867,7 @@ describe('CredentialSelector', () => {
const props = createDefaultProps()
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Assert - All credentials should be rendered in list
@@ -914,7 +880,7 @@ describe('CredentialSelector', () => {
const props = createDefaultProps({ currentCredentialId: 'cred-2' })
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Assert - Current credential (Credential 2) appears twice:
@@ -928,7 +894,7 @@ describe('CredentialSelector', () => {
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render( )
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
const credential3 = screen.getByText('Credential 3')
fireEvent.click(credential3)
@@ -938,23 +904,23 @@ describe('CredentialSelector', () => {
})
})
- // Portal Configuration
- describe('Portal Configuration', () => {
- it('should configure PortalToFollowElem with placement bottom-start', () => {
+ // Popover Configuration
+ describe('Popover Configuration', () => {
+ it('should configure Popover with placement bottom-start', () => {
// This test verifies the portal is configured correctly
// The actual placement is handled by the mock, but we verify the component renders
const props = createDefaultProps()
render( )
- expect(screen.getByTestId('portal-root'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
- it('should configure PortalToFollowElem with offset mainAxis 4', () => {
+ it('should configure Popover with offset mainAxis 4', () => {
// This test verifies the offset configuration doesn't break rendering
const props = createDefaultProps()
render( )
- expect(screen.getByTestId('portal-root'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
})
diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx
index 116b762277..b00564f9b4 100644
--- a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx
+++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx
@@ -1,12 +1,12 @@
import type { DataSourceCredential } from '@/types/pipeline'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useMemo } from 'react'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import List from './list'
import Trigger from './trigger'
@@ -21,7 +21,7 @@ const CredentialSelector = ({
onCredentialChange,
credentials,
}: CredentialSelectorProps) => {
- const [open, { toggle }] = useBoolean(false)
+ const [open, { set, setFalse }] = useBoolean(false)
const currentCredential = useMemo(() => {
return credentials.find(cred => cred.id === currentCredentialId)
@@ -34,32 +34,35 @@ const CredentialSelector = ({
const handleCredentialChange = useCallback((credentialId: string) => {
onCredentialChange(credentialId)
- toggle()
- }, [onCredentialChange, toggle])
+ setFalse()
+ }, [onCredentialChange, setFalse])
return (
-
-
+ }
+ >
-
-
+
+
-
-
+
+
)
}
diff --git a/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx b/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx
index 5e81611fc4..7aa11b4d7a 100644
--- a/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx
@@ -134,6 +134,16 @@ describe('IndexMethod', () => {
expect(input)!.toHaveValue('25')
})
+ it('should keep keyword number input visible next to steppers', () => {
+ render( )
+
+ const input = screen.getByRole('textbox')
+
+ expect(input)!.toHaveClass('w-12')
+ expect(input)!.toHaveClass('flex-none')
+ expect(input)!.toHaveClass('text-center')
+ })
+
it('should call onKeywordNumberChange when KeywordNumber changes', () => {
const handleKeywordChange = vi.fn()
render( )
@@ -147,7 +157,7 @@ describe('IndexMethod', () => {
describe('Tooltip', () => {
it('should show tooltip when hovering over disabled Economy option', () => {
- // The tooltip is shown via PortalToFollowElem when hovering
+ // The tooltip is shown via Popover when hovering
// This is controlled by useHover hook
render( )
// The tooltip content should exist in DOM but may not be visible
diff --git a/web/app/components/datasets/settings/index-method/index.tsx b/web/app/components/datasets/settings/index-method/index.tsx
index 7e925b869c..9e9b0e1759 100644
--- a/web/app/components/datasets/settings/index-method/index.tsx
+++ b/web/app/components/datasets/settings/index-method/index.tsx
@@ -1,14 +1,12 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
-import { useHover } from 'ahooks'
-import { useRef } from 'react'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import { useTranslation } from 'react-i18next'
import { Economic, HighQuality } from '@/app/components/base/icons/src/vender/knowledge'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import { IndexingType } from '../../create/step-two'
import { EffectColor } from '../chunk-structure/types'
import OptionCard from '../option-card'
@@ -32,8 +30,6 @@ const IndexMethod = ({
onKeywordNumberChange,
}: IndexMethodProps) => {
const { t } = useTranslation()
- const economyDomRef = useRef(null)
- const isHoveringEconomy = useHover(economyDomRef)
const isEconomyDisabled = currentValue === IndexingType.QUALIFIED
return (
@@ -54,14 +50,13 @@ const IndexMethod = ({
className="gap-x-2"
/>
{/* Economy */}
-
-
+
+ }
+ >
-
-
-
+
+ {isEconomyDisabled && (
+
{t('form.indexMethodChangeToEconomyDisabledTip', { ns: 'datasetSettings' })}
-
-
-
+
+ )}
+
)
}
diff --git a/web/app/components/datasets/settings/index-method/keyword-number.tsx b/web/app/components/datasets/settings/index-method/keyword-number.tsx
index 3cbc18393f..a31ab07160 100644
--- a/web/app/components/datasets/settings/index-method/keyword-number.tsx
+++ b/web/app/components/datasets/settings/index-method/keyword-number.tsx
@@ -56,14 +56,14 @@ const KeyWordNumber = ({
aria-label={t('form.numberOfKeywords', { ns: 'datasetSettings' })}
/>
-
+
diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx
index 9e05a93c7a..56a237d741 100644
--- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx
+++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx
@@ -183,18 +183,20 @@ describe('TransferOwnershipModal', () => {
})
})
- it('should show error when sending verification email fails', async () => {
+ it('should not show a modal-level toast and should stay on start step when sending verification email fails', async () => {
const user = userEvent.setup()
vi.mocked(sendOwnerEmail).mockRejectedValue(new Error('network error'))
renderModal()
await user.click(screen.getByTestId('transfer-modal-send-code'))
+ // The base service layer surfaces the real backend error. The modal itself
+ // must NOT show an additional toast (e.g. "Error sending verification code: undefined").
await waitFor(() => {
- expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
- type: 'error',
- message: expect.stringContaining('network error'),
- }))
+ expect(sendOwnerEmail).toHaveBeenCalled()
})
+ expect(mockNotify).not.toHaveBeenCalled()
+ // Should remain on the start step instead of advancing to the verify step.
+ expect(screen.getByTestId('transfer-modal-send-code')).toBeInTheDocument()
})
it('should show error when ownership transfer fails', async () => {
@@ -229,7 +231,7 @@ describe('TransferOwnershipModal', () => {
})
})
- it('should show fallback error prefix when sendOwnerEmail throws null', async () => {
+ it('should swallow null rejection from sendOwnerEmail without showing a modal-level toast', async () => {
const user = userEvent.setup()
vi.mocked(sendOwnerEmail).mockRejectedValue(null)
@@ -237,11 +239,10 @@ describe('TransferOwnershipModal', () => {
await user.click(screen.getByTestId('transfer-modal-send-code'))
await waitFor(() => {
- expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
- type: 'error',
- message: expect.stringContaining('Error sending verification code:'),
- }))
+ expect(sendOwnerEmail).toHaveBeenCalled()
})
+ expect(mockNotify).not.toHaveBeenCalled()
+ expect(screen.getByTestId('transfer-modal-send-code')).toBeInTheDocument()
})
it('should show fallback error prefix when verifyOwnerEmail throws null', async () => {
diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx
index 85a3ac3b22..cadc2dc967 100644
--- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx
+++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx
@@ -1,11 +1,10 @@
import { Button } from '@langgenius/dify-ui/button'
+import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
-import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
-import Modal from '@/app/components/base/modal'
import { useAppContext } from '@/context/app-context'
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
import MemberSelector from './member-selector'
@@ -52,15 +51,10 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
}, 1000))
}
const sendEmail = async () => {
- try {
- const res = await sendOwnerEmail({})
- startCount()
- if (res.data)
- setStepToken(res.data)
- }
- catch (error) {
- toast.error(`Error sending verification code: ${error ? (error as any).message : ''}`)
- }
+ const res = await sendOwnerEmail({})
+ startCount()
+ if (res.data)
+ setStepToken(res.data)
}
const verifyEmailAddress = async (code: string, token: string, callback?: () => void) => {
try {
@@ -81,8 +75,13 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
}
}
const sendCodeToOriginEmail = async () => {
- await sendEmail()
- setStep(STEP.verify)
+ try {
+ await sendEmail()
+ setStep(STEP.verify)
+ }
+ catch {
+ // The base service layer already surfaces the backend error (e.g. rate-limit) as a toast.
+ }
}
const handleVerifyOriginEmail = async () => {
await verifyEmailAddress(code, stepToken, () => setStep(STEP.transfer))
@@ -104,85 +103,87 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
}
}
return (
-
-
- {step === STEP.start && (
- <>
- {t('members.transferModal.title', { ns: 'common' })}
-
-
{t('members.transferModal.warning', { ns: 'common', workspace: currentWorkspace.name.replace(/'/g, '’') })}
-
{t('members.transferModal.warningTip', { ns: 'common' })}
-
-
}} values={{ email: userProfile.email }} />
+
+
+
+ {step === STEP.start && (
+ <>
+ {t('members.transferModal.title', { ns: 'common' })}
+
+
{t('members.transferModal.warning', { ns: 'common', workspace: currentWorkspace.name.replace(/'/g, '’') })}
+
{t('members.transferModal.warningTip', { ns: 'common' })}
+
+ }} values={{ email: userProfile.email }} />
+
-
-
-
-
- {t('members.transferModal.sendVerifyCode', { ns: 'common' })}
-
-
- {t('operation.cancel', { ns: 'common' })}
-
-
- >
- )}
- {step === STEP.verify && (
- <>
-
{t('members.transferModal.verifyEmail', { ns: 'common' })}
-
-
-
}} values={{ email: userProfile.email }} />
+
+
+
+ {t('members.transferModal.sendVerifyCode', { ns: 'common' })}
+
+
+ {t('operation.cancel', { ns: 'common' })}
+
- {t('members.transferModal.verifyContent2', { ns: 'common' })}
-
-
-
{t('members.transferModal.codeLabel', { ns: 'common' })}
-
setCode(e.target.value)} maxLength={6} />
-
-
-
- {t('members.transferModal.continue', { ns: 'common' })}
-
-
- {t('operation.cancel', { ns: 'common' })}
-
-
-
- {t('members.transferModal.resendTip', { ns: 'common' })}
- {time > 0 && ({t('members.transferModal.resendCount', { ns: 'common', count: time })} )}
- {!time && (
-
- {t('members.transferModal.resend', { ns: 'common' })}
-
- )}
-
- >
- )}
- {step === STEP.transfer && (
- <>
-
{t('members.transferModal.title', { ns: 'common' })}
-
-
{t('members.transferModal.warning', { ns: 'common', workspace: currentWorkspace.name.replace(/'/g, '’') })}
-
{t('members.transferModal.warningTip', { ns: 'common' })}
-
-
-
{t('members.transferModal.transferLabel', { ns: 'common' })}
-
-
-
-
- {t('members.transferModal.transfer', { ns: 'common' })}
-
-
- {t('operation.cancel', { ns: 'common' })}
-
-
- >
- )}
-
+ >
+ )}
+ {step === STEP.verify && (
+ <>
+
{t('members.transferModal.verifyEmail', { ns: 'common' })}
+
+
+ }} values={{ email: userProfile.email }} />
+
+
{t('members.transferModal.verifyContent2', { ns: 'common' })}
+
+
+
{t('members.transferModal.codeLabel', { ns: 'common' })}
+
setCode(e.target.value)} maxLength={6} />
+
+
+
+ {t('members.transferModal.continue', { ns: 'common' })}
+
+
+ {t('operation.cancel', { ns: 'common' })}
+
+
+
+ {t('members.transferModal.resendTip', { ns: 'common' })}
+ {time > 0 && ({t('members.transferModal.resendCount', { ns: 'common', count: time })} )}
+ {!time && (
+
+ {t('members.transferModal.resend', { ns: 'common' })}
+
+ )}
+
+ >
+ )}
+ {step === STEP.transfer && (
+ <>
+
{t('members.transferModal.title', { ns: 'common' })}
+
+
{t('members.transferModal.warning', { ns: 'common', workspace: currentWorkspace.name.replace(/'/g, '’') })}
+
{t('members.transferModal.warningTip', { ns: 'common' })}
+
+
+
{t('members.transferModal.transferLabel', { ns: 'common' })}
+
+
+
+
+ {t('members.transferModal.transfer', { ns: 'common' })}
+
+
+ {t('operation.cancel', { ns: 'common' })}
+
+
+ >
+ )}
+
+
)
}
export default TransferOwnershipModal
diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx
index 0232adcf4a..f9c923e6a1 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx
@@ -20,6 +20,7 @@ vi.mock('@/app/components/header/indicator', () => ({
vi.mock('@remixicon/react', () => ({
RiArrowDownSLine: () =>
,
+ RiQuestionLine: () =>
,
}))
describe('SwitchCredentialInLoadBalancing', () => {
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 be3e9edb93..8eaf18440f 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
@@ -1,4 +1,9 @@
import type { Credential } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import {
RiAddLine,
RiArrowDownSLine,
@@ -10,11 +15,6 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import Indicator from '@/app/components/header/indicator'
import CredentialItem from './authorized/credential-item'
@@ -47,68 +47,71 @@ const CredentialSelector = ({
}, [handleSelect, t])
return (
-
- !disabled && setOpen(v => !v)}>
-
+
}
+ >
+ {
+ selectedCredential && (
+
+ {
+ !selectedCredential.addNewCredential &&
+ }
+
{selectedCredential.credential_name}
+ {
+ selectedCredential.from_enterprise && (
+
Enterprise
+ )
+ }
+
+ )
+ }
+ {
+ !selectedCredential && (
+
{t('modelProvider.auth.selectModelCredential', { ns: 'common' })}
+ )
+ }
+
+
+
+
{
- selectedCredential && (
-
- {
- !selectedCredential.addNewCredential &&
- }
-
{selectedCredential.credential_name}
- {
- selectedCredential.from_enterprise && (
-
Enterprise
- )
- }
-
- )
- }
- {
- !selectedCredential && (
-
{t('modelProvider.auth.selectModelCredential', { ns: 'common' })}
- )
- }
-
-
-
-
-
-
- {
- credentials.map(credential => (
-
- ))
- }
-
- {
- !notAllowAddNewCredential && (
-
-
- {t('modelProvider.auth.addNewModelCredential', { ns: 'common' })}
-
- )
+ credentials.map(credential => (
+
+ ))
}
-
-
+ {
+ !notAllowAddNewCredential && (
+
+
+ {t('modelProvider.auth.addNewModelCredential', { ns: 'common' })}
+
+ )
+ }
+
+
)
}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx
index 385bd84f90..fe84ddea13 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx
@@ -18,7 +18,7 @@ import { useCallback, useState } from 'react'
import { Infotip } from '@/app/components/base/infotip'
import Radio from '@/app/components/base/radio'
import RadioE from '@/app/components/base/radio/ui'
-import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
+import { AppSelector } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/Form.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/Form.spec.tsx
index a71cddcd24..11ff189393 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/Form.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/Form.spec.tsx
@@ -8,6 +8,7 @@ import type {
CredentialFormSchemaTextInput,
FormValue,
} from '../../declarations'
+import type { AppSelectorValue } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@@ -29,8 +30,8 @@ vi.mock('../../hooks', () => ({
}))
vi.mock('@/app/components/plugins/plugin-detail-panel/app-selector', () => ({
- default: ({ onSelect }: { onSelect: (item: { id: string }) => void }) => (
-
onSelect({ id: 'app-1' })}>Select App
+ AppSelector: ({ onSelect }: { onSelect: (item: AppSelectorValue) => void }) => (
+
onSelect({ app_id: 'app-1', inputs: {}, files: [] })}>Select App
),
}))
@@ -408,7 +409,7 @@ describe('Form', () => {
multi_tool: [{ id: 'tool-1' }],
}))
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({
- app_selector: { id: 'app-1', type: FormTypeEnum.appSelector },
+ app_selector: { app_id: 'app-1', inputs: {}, files: [], type: FormTypeEnum.appSelector },
}))
})
diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/status-indicators.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/status-indicators.spec.tsx
index 9e28c9e204..dc7c512f78 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/status-indicators.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/status-indicators.spec.tsx
@@ -21,6 +21,12 @@ describe('StatusIndicators', () => {
installedPlugins = [{ name: 'demo-plugin', plugin_unique_identifier: 'demo@1.0.0' }]
})
+ const getTooltipTrigger = (container: HTMLElement) => {
+ const trigger = container.querySelector('[role="button"][aria-haspopup="dialog"]')
+ expect(trigger).toBeInTheDocument()
+ return trigger as HTMLElement
+ }
+
it('should render nothing when model is available and enabled', () => {
const { container } = render(
{
/>,
)
- const trigger = container.querySelector('[data-state]')
- expect(trigger).toBeInTheDocument()
- await user.hover(trigger as HTMLElement)
+ await user.hover(getTooltipTrigger(container))
expect(await screen.findByText('nodes.agent.modelSelectorTooltips.deprecated')).toBeInTheDocument()
})
@@ -68,9 +72,7 @@ describe('StatusIndicators', () => {
/>,
)
- const trigger = container.querySelector('[data-state]')
- expect(trigger).toBeInTheDocument()
- await user.hover(trigger as HTMLElement)
+ await user.hover(getTooltipTrigger(container))
expect(await screen.findByText('nodes.agent.modelNotSupport.title')).toBeInTheDocument()
})
@@ -134,9 +136,7 @@ describe('StatusIndicators', () => {
/>,
)
- const trigger = container.querySelector('[data-state]')
- expect(trigger).toBeInTheDocument()
- await user.hover(trigger as HTMLElement)
+ await user.hover(getTooltipTrigger(container))
expect(await screen.findByText('nodes.agent.modelNotInMarketplace.title')).toBeInTheDocument()
})
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx
index 2bcb7b712b..4119bca6ae 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx
@@ -25,18 +25,22 @@ vi.mock('../model-selector-trigger', () => ({
},
}))
-vi.mock('../popup', () => ({
- default: ({ onHide, onSelect }: { onHide: () => void, onSelect: (provider: string, model: ModelItem) => void }) => (
- <>
- onSelect('openai', { model: 'gpt-4' } as ModelItem)}>
- select
-
-
- hide
-
- >
- ),
-}))
+vi.mock('../popup', async () => {
+ const { ComboboxItem } = await vi.importActual('@langgenius/dify-ui/combobox')
+
+ return {
+ default: ({ onHide }: { onHide: () => void }) => (
+ <>
+
+ select
+
+
+ hide
+
+ >
+ ),
+ }
+})
const makeModelItem = (overrides: Partial = {}): ModelItem => ({
model: 'gpt-4',
@@ -82,7 +86,7 @@ describe('ModelSelector', () => {
it('should toggle popup and close it after selecting a model', () => {
renderWithQueryClient( )
- const triggerButton = screen.getByRole('button', { name: 'empty-trigger' })
+ const triggerButton = screen.getByRole('combobox')
fireEvent.click(triggerButton)
expect(triggerButton).toHaveAttribute('aria-expanded', 'true')
@@ -96,7 +100,7 @@ describe('ModelSelector', () => {
const onSelect = vi.fn()
renderWithQueryClient( )
- fireEvent.click(screen.getByText('empty-trigger'))
+ fireEvent.click(screen.getByRole('combobox'))
fireEvent.click(screen.getByText('select'))
expect(onSelect).toHaveBeenCalledWith({ provider: 'openai', model: 'gpt-4' })
@@ -105,7 +109,7 @@ describe('ModelSelector', () => {
it('should close popup when popup requests hide', () => {
renderWithQueryClient( )
- const triggerButton = screen.getByRole('button', { name: 'empty-trigger' })
+ const triggerButton = screen.getByRole('combobox')
fireEvent.click(triggerButton)
expect(triggerButton).toHaveAttribute('aria-expanded', 'true')
expect(screen.getByText('hide')).toBeInTheDocument()
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popover.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popover.spec.tsx
index d7501672f4..528a5416ee 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popover.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popover.spec.tsx
@@ -1,14 +1,6 @@
-import type { ReactNode } from 'react'
-import { act, fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, render, screen } from '@testing-library/react'
import ModelSelector from '../index'
-type PopoverProps = {
- children: ReactNode
- onOpenChange?: (open: boolean) => void
-}
-
-let latestOnOpenChange: PopoverProps['onOpenChange']
-
vi.mock('../../hooks', () => ({
useCurrentProviderAndModel: () => ({
currentProvider: undefined,
@@ -16,15 +8,6 @@ vi.mock('../../hooks', () => ({
}),
}))
-vi.mock('@langgenius/dify-ui/popover', () => ({
- Popover: ({ children, onOpenChange }: PopoverProps) => {
- latestOnOpenChange = onOpenChange
- return {children}
- },
- PopoverTrigger: ({ render }: { render: ReactNode }) => <>{render}>,
- PopoverContent: ({ children }: { children: ReactNode }) => {children}
,
-}))
-
vi.mock('../model-selector-trigger', () => ({
default: ({ open, readonly }: { open: boolean, readonly?: boolean }) => (
@@ -43,19 +26,16 @@ vi.mock('../popup', () => ({
),
}))
-describe('ModelSelector popover branches', () => {
+describe('ModelSelector combobox branches', () => {
beforeEach(() => {
vi.clearAllMocks()
- latestOnOpenChange = undefined
})
- it('should open and close through popover callbacks when editable', () => {
+ it('should open and close through combobox trigger when editable', () => {
const onHide = vi.fn()
render( )
- act(() => {
- latestOnOpenChange?.(true)
- })
+ fireEvent.click(screen.getByRole('combobox'))
expect(screen.getByText('open-editable')).toBeInTheDocument()
@@ -65,12 +45,10 @@ describe('ModelSelector popover branches', () => {
expect(onHide).toHaveBeenCalledTimes(1)
})
- it('should ignore popover open changes when readonly', () => {
+ it('should ignore combobox open requests when readonly', () => {
render( )
- act(() => {
- latestOnOpenChange?.(true)
- })
+ fireEvent.click(screen.getByRole('combobox'))
expect(screen.getByText('closed-readonly')).toBeInTheDocument()
})
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx
index 341a9c6abc..3c4fea6f51 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx
@@ -1,4 +1,6 @@
+import type { ReactElement, ReactNode } from 'react'
import type { DefaultModel, Model, ModelItem } from '../../declarations'
+import { Combobox } from '@langgenius/dify-ui/combobox'
import { fireEvent, render, screen } from '@testing-library/react'
import {
ConfigurationMethodEnum,
@@ -25,7 +27,7 @@ vi.mock('../../hooks', async () => {
})
vi.mock('../../model-badge', () => ({
- default: ({ children }: { children: React.ReactNode }) => {children} ,
+ default: ({ children }: { children: ReactNode }) => {children} ,
}))
vi.mock('../../model-icon', () => ({
@@ -41,13 +43,7 @@ vi.mock('../feature-icon', () => ({
}))
vi.mock('@/app/components/base/tooltip', () => ({
- default: ({ children }: { children: React.ReactNode }) => {children}
,
-}))
-
-vi.mock('@langgenius/dify-ui/popover', () => ({
- Popover: ({ children }: { children: React.ReactNode }) => {children}
,
- PopoverTrigger: ({ render }: { render: React.ReactNode }) => <>{render}>,
- PopoverContent: ({ children }: { children: React.ReactNode }) => {children}
,
+ default: ({ children }: { children: ReactNode }) => {children}
,
}))
const mockCredentialPanelState = vi.hoisted(() => vi.fn())
@@ -114,6 +110,24 @@ const makeProvider = (overrides: Record = {}) => ({
...overrides,
})
+const createComboboxNode = (
+ node: ReactElement,
+ onValueChange = vi.fn(),
+) => (
+
+ {node}
+
+)
+
+const renderWithCombobox = (
+ node: ReactElement,
+ onValueChange = vi.fn(),
+) => {
+ return render(
+ createComboboxNode(node, onValueChange),
+ )
+}
+
describe('PopupItem', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -141,48 +155,51 @@ describe('PopupItem', () => {
modelProviders: [],
})
- const { container } = render(
- ,
+ const { container } = renderWithCombobox(
+ ,
)
- expect(container.innerHTML).toBe('')
+ expect(container.textContent).toBe('')
})
- it('should call onSelect when clicking an active model', () => {
- const onSelect = vi.fn()
- render( )
+ it('should select the combobox value when clicking an active model', () => {
+ const onValueChange = vi.fn()
+ renderWithCombobox( , onValueChange)
fireEvent.click(screen.getByText('GPT-4'))
- expect(onSelect).toHaveBeenCalledWith('openai', expect.objectContaining({ model: 'gpt-4' }))
+ expect(onValueChange).toHaveBeenCalledWith(
+ { provider: 'openai', model: 'gpt-4' },
+ expect.objectContaining({ reason: 'item-press' }),
+ )
})
- it('should not call onSelect when model is not active', () => {
- const onSelect = vi.fn()
- render(
+ it('should not select the combobox value when model is not active', () => {
+ const onValueChange = vi.fn()
+ renderWithCombobox(
,
+ onValueChange,
)
fireEvent.click(screen.getByText('GPT-4'))
- expect(onSelect).not.toHaveBeenCalled()
+ expect(onValueChange).not.toHaveBeenCalled()
})
it('should open model modal when clicking add on unconfigured model', () => {
- const { rerender } = render(
- ,
+ const onValueChange = vi.fn()
+ const { rerender } = renderWithCombobox(
+ ,
+ onValueChange,
)
+ fireEvent.click(screen.getByText('GPT-4'))
fireEvent.click(screen.getByText('COMMON.OPERATION.ADD'))
+ expect(onValueChange).not.toHaveBeenCalled()
expect(mockSetShowModelModal).toHaveBeenCalled()
const call = mockSetShowModelModal.mock.calls[0]![0] as { onSaveCallback?: () => void }
@@ -191,15 +208,14 @@ describe('PopupItem', () => {
expect(mockUpdateModelProviders).toHaveBeenCalled()
expect(mockUpdateModelList).toHaveBeenCalledWith(ModelTypeEnum.textGeneration)
- rerender(
+ rerender(createComboboxNode(
,
- )
+ ))
fireEvent.click(screen.getByText('COMMON.OPERATION.ADD'))
const call2 = mockSetShowModelModal.mock.calls.at(-1)?.[0] as { onSaveCallback?: () => void } | undefined
@@ -211,11 +227,10 @@ describe('PopupItem', () => {
it('should show selected state when defaultModel matches', () => {
const defaultModel: DefaultModel = { provider: 'openai', model: 'gpt-4' }
- render(
+ renderWithCombobox(
,
)
@@ -226,13 +241,12 @@ describe('PopupItem', () => {
it('should fall back to english labels when the current language is unavailable', () => {
mockUseLanguage.mockReturnValue('zh_Hans')
- render(
+ renderWithCombobox(
,
)
@@ -242,7 +256,7 @@ describe('PopupItem', () => {
})
it('should toggle collapsed state when clicking provider header', () => {
- render( )
+ renderWithCombobox( )
expect(screen.getByText('GPT-4'))!.toBeInTheDocument()
@@ -256,7 +270,7 @@ describe('PopupItem', () => {
})
it('should show credential name when using custom provider', () => {
- render( )
+ renderWithCombobox( )
expect(screen.getByText('my-api-key'))!.toBeInTheDocument()
})
@@ -273,7 +287,7 @@ describe('PopupItem', () => {
credits: 200,
})
- render( )
+ renderWithCombobox( )
expect(screen.getByText('stale-key'))!.toBeInTheDocument()
expect(document.querySelector('.bg-components-badge-status-light-error-bg')).not.toBeNull()
@@ -299,7 +313,7 @@ describe('PopupItem', () => {
credits: 0,
})
- render( )
+ renderWithCombobox( )
expect(screen.getByText(/modelProvider\.selector\.configureRequired/))!.toBeInTheDocument()
})
@@ -321,7 +335,7 @@ describe('PopupItem', () => {
credits: 200,
})
- render( )
+ renderWithCombobox( )
expect(screen.getByText(/modelProvider\.selector\.aiCredits/))!.toBeInTheDocument()
})
@@ -346,7 +360,7 @@ describe('PopupItem', () => {
credits: 0,
})
- render( )
+ renderWithCombobox( )
expect(screen.getByText(/modelProvider\.selector\.creditsExhausted/))!.toBeInTheDocument()
})
@@ -354,8 +368,9 @@ describe('PopupItem', () => {
it('should close the dropdown through dropdown content callbacks', () => {
const onHide = vi.fn()
- render( )
+ renderWithCombobox( )
+ fireEvent.click(screen.getByRole('button', { name: /my-api-key/ }))
fireEvent.click(screen.getByRole('button', { name: 'close dropdown' }))
expect(onHide).toHaveBeenCalled()
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx
index 42232a71c0..4776ad2268 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx
@@ -1,7 +1,11 @@
import type { ReactElement } from 'react'
import type { Model, ModelItem, ModelProvider } from '../../declarations'
+import type { PopupProps } from '../popup'
import type { SystemFeatures } from '@/types/feature'
+import { Combobox } from '@langgenius/dify-ui/combobox'
import { fireEvent, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { useState } from 'react'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import {
ConfigurationMethodEnum,
@@ -69,10 +73,31 @@ vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({ modelProviders: mockContextModelProviders.current }),
}))
-const renderPopup = (ui: ReactElement) => renderWithSystemFeatures(ui, {
- // The Popup component never inspects trial_models beyond passing them
- // through, so an opaque string[] is enough; cast to satisfy the
- // ModelProviderQuotaGetPaid[] declared on SystemFeatures.
+type PopupTestProps = Omit
+
+function PopupHarness(props: PopupTestProps) {
+ const [inputValue, setInputValue] = useState('')
+
+ return (
+ {
+ if (details.reason !== 'item-press')
+ setInputValue(newInputValue)
+ }}
+ >
+
+
+ )
+}
+
+const renderPopup = (ui: ReactElement) => renderWithSystemFeatures(ui, {
systemFeatures: { trial_models: mockTrialModels.current as unknown as SystemFeatures['trial_models'] },
})
@@ -193,11 +218,12 @@ describe('Popup', () => {
})
})
- it('should filter models by search and allow clearing search', () => {
- const { container } = renderPopup(
- {
+ const user = userEvent.setup()
+
+ renderPopup(
+ ,
)
@@ -205,18 +231,21 @@ describe('Popup', () => {
expect(screen.getByText('openai'))!.toBeInTheDocument()
const input = screen.getByPlaceholderText('datasetSettings.form.searchModel')
- fireEvent.change(input, { target: { value: 'not-found' } })
+ await user.click(input)
+ await user.keyboard('not-found')
expect(screen.getByText('No model found for \u201Cnot-found\u201D'))!.toBeInTheDocument()
- const clearIcon = container.querySelector('.i-custom-vender-solid-general-x-circle')
- expect(clearIcon)!.toBeInTheDocument()
- fireEvent.click(clearIcon!)
+ const clearButton = screen.getByRole('button', { name: 'common.operation.clear' })
+ expect(clearButton)!.toBeInTheDocument()
+ await user.click(clearButton)
+
expect((input as HTMLInputElement).value).toBe('')
+ expect(input).toHaveFocus()
})
it('should show matching models when searching by model name', () => {
renderPopup(
- {
models: [makeModelItem({ model: 'claude-3', label: { en_US: 'Claude 3', zh_Hans: 'Claude 3' } })],
}),
]}
- onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
@@ -246,7 +274,7 @@ describe('Popup', () => {
it('should show empty search placeholder when no provider or model name matches', () => {
renderPopup(
- {
],
}),
]}
- onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
@@ -272,7 +299,7 @@ describe('Popup', () => {
it('should show all models of a provider when searching by provider label', () => {
renderPopup(
- {
],
}),
]}
- onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
@@ -307,9 +333,183 @@ describe('Popup', () => {
expect(screen.queryByText('claude-3')).not.toBeInTheDocument()
})
+ it('should fuzzy match provider labels and keep all compatible provider models visible', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'opnai' } },
+ )
+
+ expect(screen.getByText('openai'))!.toBeInTheDocument()
+ expect(screen.getByText('gpt-4'))!.toBeInTheDocument()
+ expect(screen.getByText('gpt-4o'))!.toBeInTheDocument()
+ expect(screen.queryByText('anthropic')).not.toBeInTheDocument()
+ })
+
+ it('should match model labels without expanding unmatched provider models', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'claude3' } },
+ )
+
+ expect(screen.queryByText('openai')).not.toBeInTheDocument()
+ expect(screen.getByText('anthropic'))!.toBeInTheDocument()
+ expect(screen.getByText('claude-3'))!.toBeInTheDocument()
+ expect(screen.queryByText('claude-instant')).not.toBeInTheDocument()
+ })
+
+ it('should match model names without separators', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'gpt5.4' } },
+ )
+
+ expect(screen.getByText('gpt-5.4'))!.toBeInTheDocument()
+ expect(screen.getByText('gpt-5.4-2026-03-05'))!.toBeInTheDocument()
+ expect(screen.getByText('gpt-5.4-mini'))!.toBeInTheDocument()
+ expect(screen.getByText('gpt-5.4-nano'))!.toBeInTheDocument()
+ expect(screen.queryByText('gpt-5.3-chat-latest')).not.toBeInTheDocument()
+ expect(screen.queryByText('gpt-5.2')).not.toBeInTheDocument()
+ expect(screen.queryByText('gpt-4.1')).not.toBeInTheDocument()
+ })
+
+ it('should not fuzzy match unrelated providers that share the langgenius namespace', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'openai' } },
+ )
+
+ expect(screen.getByText('langgenius/openai/openai'))!.toBeInTheDocument()
+ expect(screen.getByText('langgenius/openai_api_compatible/openai_api_compatible'))!.toBeInTheDocument()
+ expect(screen.queryByText('langgenius/openrouter/openrouter')).not.toBeInTheDocument()
+ })
+
+ it('should fuzzy match provider names without matching every langgenius provider', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'gemni' } },
+ )
+
+ expect(screen.getByText('langgenius/gemini/google'))!.toBeInTheDocument()
+ expect(screen.queryByText('langgenius/zhipuai/zhipuai')).not.toBeInTheDocument()
+ expect(screen.queryByText('langgenius/tongyi/tongyi')).not.toBeInTheDocument()
+ })
+
it('should match by model provider key when model label does not contain the search text', () => {
renderPopup(
- {
],
}),
]}
- onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
@@ -337,7 +536,7 @@ describe('Popup', () => {
mockSupportFunctionCall.mockReturnValue(false)
renderPopup(
- {
],
}),
]}
- onSelect={vi.fn()}
onHide={vi.fn()}
scopeFeatures={[ModelFeatureEnum.toolCall]}
/>,
@@ -366,9 +564,8 @@ describe('Popup', () => {
it('should not show compatible-only helper text when no scope features are applied', () => {
renderPopup(
- ,
)
@@ -378,9 +575,8 @@ describe('Popup', () => {
it('should show compatible-only helper text when scope features are applied', () => {
renderPopup(
- ,
@@ -392,9 +588,8 @@ describe('Popup', () => {
it('should keep search and footer outside the scrollable model list', () => {
renderPopup(
- ,
@@ -417,9 +612,8 @@ describe('Popup', () => {
mockSupportFunctionCall.mockReturnValue(false)
const { unmount } = renderPopup(
- ,
@@ -429,9 +623,8 @@ describe('Popup', () => {
unmount()
mockSupportFunctionCall.mockReturnValue(true)
const { unmount: unmount2 } = renderPopup(
- ,
@@ -440,9 +633,8 @@ describe('Popup', () => {
unmount2()
const { unmount: unmount3 } = renderPopup(
- ,
@@ -451,9 +643,8 @@ describe('Popup', () => {
unmount3()
renderPopup(
- ,
@@ -465,7 +656,7 @@ describe('Popup', () => {
mockLanguage = 'fr_FR'
renderPopup(
- {
],
}),
]}
- onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
@@ -504,9 +694,8 @@ describe('Popup', () => {
]
renderPopup(
- ,
)
@@ -531,9 +720,8 @@ describe('Popup', () => {
]
renderPopup(
- ,
)
@@ -561,9 +749,8 @@ describe('Popup', () => {
]
renderPopup(
- ,
)
@@ -574,9 +761,8 @@ describe('Popup', () => {
it('should open provider settings when clicking footer link', () => {
const onHide = vi.fn()
renderPopup(
- ,
)
@@ -592,9 +778,8 @@ describe('Popup', () => {
it('should show empty state when no providers are configured', () => {
const onHide = vi.fn()
renderPopup(
- ,
)
@@ -613,9 +798,8 @@ describe('Popup', () => {
mockContextModelProviders.current = [makeContextProvider({ provider: 'test-openai' })]
renderPopup(
- ,
)
@@ -635,9 +819,8 @@ describe('Popup', () => {
})]
renderPopup(
- ,
)
@@ -660,9 +843,8 @@ describe('Popup', () => {
})]
renderPopup(
- ,
)
@@ -674,9 +856,8 @@ describe('Popup', () => {
it('should toggle marketplace section collapse', () => {
renderPopup(
- ,
)
@@ -699,9 +880,8 @@ describe('Popup', () => {
mockInstallMutateAsync.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
renderPopup(
- ,
)
@@ -722,9 +902,8 @@ describe('Popup', () => {
mockInstallMutateAsync.mockRejectedValue(new Error('Install failed'))
renderPopup(
- ,
)
@@ -736,7 +915,6 @@ describe('Popup', () => {
expect(mockInstallMutateAsync).toHaveBeenCalled()
})
- // Should not crash, install buttons should still be available
expect(screen.getAllByText(/common\.modelProvider\.selector\.install/).length).toBeGreaterThan(0)
})
@@ -748,9 +926,8 @@ describe('Popup', () => {
mockCheck.mockResolvedValue(undefined)
renderPopup(
- ,
)
@@ -774,9 +951,8 @@ describe('Popup', () => {
mockMarketplacePlugins.isLoading = true
renderPopup(
- ,
)
@@ -792,9 +968,8 @@ describe('Popup', () => {
mockMarketplacePlugins.current = []
renderPopup(
- ,
)
@@ -808,13 +983,12 @@ describe('Popup', () => {
it('should sort the selected provider to the top when a default model is provided', () => {
renderPopup(
- ,
)
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx
index cc5e0154d7..69dff3a158 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx
@@ -1,17 +1,7 @@
-import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
-import {
- RiFileTextLine,
- RiFilmAiLine,
- RiImageCircleAiLine,
- RiVoiceAiFill,
-} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
-import {
- ModelFeatureEnum,
- ModelFeatureTextEnum,
-} from '../declarations'
+import { ModelFeatureEnum, ModelFeatureTextEnum } from '../declarations'
import ModelBadge from '../model-badge'
type FeatureIconProps = {
@@ -19,56 +9,20 @@ type FeatureIconProps = {
className?: string
showFeaturesLabel?: boolean
}
-const FeatureIcon: FC = ({
+function FeatureIcon({
className,
feature,
showFeaturesLabel,
-}) => {
+}: FeatureIconProps) {
const { t } = useTranslation()
- // if (feature === ModelFeatureEnum.agentThought) {
- // return (
- //
- //
- //
- //
- //
- // )
- // }
-
- // if (feature === ModelFeatureEnum.toolCall) {
- // return (
- //
- //
- //
- //
- //
- // )
- // }
-
- // if (feature === ModelFeatureEnum.multiToolCall) {
- // return (
- //
- //
- //
- //
- //
- // )
- // }
-
if (feature === ModelFeatureEnum.vision) {
if (showFeaturesLabel) {
return (
-
+
{ModelFeatureTextEnum.vision}
)
@@ -81,11 +35,11 @@ const FeatureIcon: FC = ({
-
+
)}
@@ -103,7 +57,7 @@ const FeatureIcon: FC = ({
-
+
{ModelFeatureTextEnum.document}
)
@@ -116,11 +70,11 @@ const FeatureIcon: FC = ({
-
+
)}
@@ -138,7 +92,7 @@ const FeatureIcon: FC = ({
-
+
{ModelFeatureTextEnum.audio}
)
@@ -151,11 +105,11 @@ const FeatureIcon: FC = ({
-
+
)}
@@ -173,7 +127,7 @@ const FeatureIcon: FC = ({
-
+
{ModelFeatureTextEnum.video}
)
@@ -186,11 +140,11 @@ const FeatureIcon: FC = ({
-
+
)}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx
index 835821fd59..debd06d7cd 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx
@@ -1,19 +1,15 @@
-import type { FC } from 'react'
-import type {
- DefaultModel,
- Model,
- ModelFeatureEnum,
- ModelItem,
-} from '../declarations'
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from '@langgenius/dify-ui/popover'
-import { useState } from 'react'
+import type { ComboboxRootChangeEventDetails } from '@langgenius/dify-ui/combobox'
+import type { DefaultModel, Model, ModelFeatureEnum, ModelItem } from '../declarations'
+import type { ModelSelectorValue } from './types'
+import { cn } from '@langgenius/dify-ui/cn'
+import { Combobox, ComboboxContent, ComboboxTrigger } from '@langgenius/dify-ui/combobox'
+import { useCallback, useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { ModelStatusEnum } from '../declarations'
import { useCurrentProviderAndModel } from '../hooks'
import ModelSelectorTrigger from './model-selector-trigger'
import Popup from './popup'
+import { getModelSelectorValueLabel, isSameModelSelectorValue } from './types'
type ModelSelectorProps = {
defaultModel?: DefaultModel
@@ -27,7 +23,7 @@ type ModelSelectorProps = {
deprecatedClassName?: string
showDeprecatedWarnIcon?: boolean
}
-const ModelSelector: FC = ({
+function ModelSelector({
defaultModel,
modelList,
triggerClassName,
@@ -38,8 +34,10 @@ const ModelSelector: FC = ({
scopeFeatures = [],
deprecatedClassName,
showDeprecatedWarnIcon = true,
-}) => {
+}: ModelSelectorProps) {
+ const { t } = useTranslation()
const [open, setOpen] = useState(false)
+ const [inputValue, setInputValue] = useState('')
const {
currentProvider,
currentModel,
@@ -47,62 +45,103 @@ const ModelSelector: FC = ({
modelList,
defaultModel,
)
+ const currentValue = useMemo(() => {
+ if (!currentProvider || !currentModel)
+ return null
- const handleSelect = (provider: string, model: ModelItem) => {
+ return {
+ provider: currentProvider.provider,
+ model: currentModel.model,
+ }
+ }, [currentModel, currentProvider])
+
+ const handleOpenChange = useCallback((newOpen: boolean) => {
+ if (readonly)
+ return
+
+ setOpen(newOpen)
+ if (!newOpen)
+ setInputValue('')
+ }, [readonly])
+
+ const handleSelect = useCallback((provider: string, model: ModelItem) => {
setOpen(false)
+ setInputValue('')
if (onSelect)
onSelect({ provider, model: model.model })
- }
+ }, [onSelect])
+
+ const handleValueChange = useCallback((value: ModelSelectorValue | null) => {
+ if (!value)
+ return
+
+ const provider = modelList.find(model => model.provider === value.provider)
+ const model = provider?.models.find(model => model.model === value.model)
+
+ if (!provider || !model)
+ return
+ if (model.status !== ModelStatusEnum.active)
+ return
+
+ handleSelect(provider.provider, model)
+ }, [handleSelect, modelList])
+
+ const handleInputValueChange = useCallback((inputValue: string, details: ComboboxRootChangeEventDetails) => {
+ if (details.reason !== 'item-press')
+ setInputValue(inputValue)
+ }, [])
+
+ const handleHide = useCallback(() => {
+ setOpen(false)
+ setInputValue('')
+ onHide?.()
+ }, [onHide])
return (
-
+ filter={null}
+ inputValue={inputValue}
+ isItemEqualToValue={isSameModelSelectorValue}
+ itemToStringLabel={getModelSelectorValueLabel}
open={open}
- onOpenChange={(newOpen) => {
- if (readonly)
- return
- setOpen(newOpen)
- }}
+ value={currentValue}
+ onInputValueChange={handleInputValueChange}
+ onOpenChange={handleOpenChange}
+ onValueChange={handleValueChange}
>
-
-
-
- )}
- />
-
+
+
+
{
- setOpen(false)
- onHide?.()
- }}
+ onInputValueChange={setInputValue}
+ onHide={handleHide}
/>
-
-
+
+
)
}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/marketplace-section.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/marketplace-section.tsx
index 469e3c201f..33079a80c6 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/marketplace-section.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/marketplace-section.tsx
@@ -1,4 +1,3 @@
-import type { FC } from 'react'
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
@@ -16,7 +15,7 @@ type MarketplaceSectionProps = {
onInstallPlugin: (key: ModelProviderQuotaGetPaid) => void | Promise
}
-const MarketplaceSection: FC = ({
+function MarketplaceSection({
marketplaceProviders,
marketplaceCollapsed,
installingProvider,
@@ -24,7 +23,7 @@ const MarketplaceSection: FC = ({
theme,
onMarketplaceCollapsedChange,
onInstallPlugin,
-}) => {
+}: MarketplaceSectionProps) {
const { t } = useTranslation()
if (marketplaceProviders.length === 0)
@@ -36,14 +35,15 @@ const MarketplaceSection: FC = ({
-
-
+ onMarketplaceCollapsedChange(!marketplaceCollapsed)}
>
{t('modelProvider.selector.fromMarketplace', { ns: 'common' })}
-
+
{!marketplaceCollapsed && (
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/model-search.ts b/web/app/components/header/account-setting/model-provider-page/model-selector/model-search.ts
new file mode 100644
index 0000000000..9870b70ea9
--- /dev/null
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/model-search.ts
@@ -0,0 +1,194 @@
+import type { DefaultModel, Model, ModelItem, TypeWithI18N } from '../declarations'
+import Fuse from 'fuse.js'
+import { supportFunctionCall } from '@/utils/tool-call'
+import { ModelFeatureEnum } from '../declarations'
+
+type ProviderSearchEntry = {
+ provider: string
+ labels: string[]
+ providerKeys: string[]
+}
+
+type ModelSearchEntry = {
+ provider: string
+ model: string
+ normalizedLabels: string[]
+}
+
+type SearchMatches = {
+ providers: Set
+ models: Set
+}
+
+type ModelSelectorSearchIndex = {
+ search: (query: string) => SearchMatches
+}
+
+type FilterModelSelectorModelsParams = {
+ aiCreditVisibleProviders: Set
+ defaultModel?: DefaultModel
+ inputValue: string
+ installedModelList: Model[]
+ scopeFeatures: ModelFeatureEnum[]
+ searchIndex: ModelSelectorSearchIndex
+}
+
+const providerSearchOptions = {
+ ignoreDiacritics: true,
+ ignoreLocation: true,
+ minMatchCharLength: 2,
+ shouldSort: false,
+ threshold: 0.25,
+ keys: [
+ { name: 'labels', weight: 2 },
+ { name: 'providerKeys', weight: 1 },
+ ],
+}
+
+const modelSearchOptions = {
+ ignoreDiacritics: true,
+ shouldSort: false,
+ useExtendedSearch: true,
+ keys: [
+ 'normalizedLabels',
+ ],
+}
+
+const normalizeModelSearchValue = (value: string) => (
+ value
+ .toLowerCase()
+ .normalize('NFKD')
+ .replace(/[^\p{Letter}\p{Number}]+/gu, '')
+)
+
+const looksLikeModelQuery = (value: string) => /\d/.test(value)
+
+const getLabelSearchValues = (label: TypeWithI18N, language: string) => {
+ if (label[language] !== undefined)
+ return [label[language]]
+
+ return Array.from(new Set(Object.values(label)))
+}
+
+const getProviderKeySearchValues = (provider: string) => {
+ const keys = provider
+ .split('/')
+ .filter(part => part && part !== 'langgenius')
+
+ return Array.from(new Set([
+ ...keys,
+ ...keys.map(normalizeModelSearchValue),
+ ]))
+}
+
+const createModelSearchKey = (provider: string, model: string) => `${provider}/${model}`
+
+const modelSupportsScopeFeatures = (modelItem: ModelItem, scopeFeatures: ModelFeatureEnum[]) => {
+ if (scopeFeatures.length === 0)
+ return true
+
+ return scopeFeatures.every((feature) => {
+ if (feature === ModelFeatureEnum.toolCall)
+ return supportFunctionCall(modelItem.features)
+
+ return modelItem.features?.includes(feature) ?? false
+ })
+}
+
+export const createModelSelectorSearchIndex = (installedModelList: Model[], language: string): ModelSelectorSearchIndex => {
+ const providerEntries = installedModelList.map((model) => {
+ return {
+ provider: model.provider,
+ labels: getLabelSearchValues(model.label, language),
+ providerKeys: getProviderKeySearchValues(model.provider),
+ }
+ })
+ const modelEntries = installedModelList.flatMap(model =>
+ model.models.map((modelItem) => {
+ const labels = getLabelSearchValues(modelItem.label, language)
+
+ return {
+ provider: model.provider,
+ model: modelItem.model,
+ normalizedLabels: Array.from(new Set([
+ modelItem.model,
+ ...labels,
+ ].map(normalizeModelSearchValue))),
+ }
+ }),
+ )
+ const providerFuse = new Fuse(providerEntries, providerSearchOptions)
+ const modelFuse = new Fuse(modelEntries, modelSearchOptions)
+
+ return {
+ search: (query) => {
+ const trimmedQuery = query.trim()
+
+ if (!trimmedQuery)
+ return { providers: new Set(), models: new Set() }
+
+ const normalizedQuery = normalizeModelSearchValue(trimmedQuery)
+ const providerMatches = looksLikeModelQuery(trimmedQuery)
+ ? new Set()
+ : new Set(providerFuse.search(trimmedQuery).map(({ item }) => item.provider))
+ const modelMatches = normalizedQuery
+ ? new Set(
+ modelFuse
+ .search(`'${normalizedQuery}`)
+ .map(({ item }) => createModelSearchKey(item.provider, item.model)),
+ )
+ : new Set()
+
+ return {
+ providers: providerMatches,
+ models: modelMatches,
+ }
+ },
+ }
+}
+
+export const filterModelSelectorModels = ({
+ aiCreditVisibleProviders,
+ defaultModel,
+ inputValue,
+ installedModelList,
+ scopeFeatures,
+ searchIndex,
+}: FilterModelSelectorModelsParams) => {
+ const trimmedInputValue = inputValue.trim()
+ const matches = trimmedInputValue
+ ? searchIndex.search(trimmedInputValue)
+ : { providers: new Set(), models: new Set() }
+
+ const filtered = installedModelList.map((model) => {
+ const providerMatched = matches.providers.has(model.provider)
+ const filteredModels = model.models
+ .filter((modelItem) => {
+ if (!trimmedInputValue || providerMatched)
+ return true
+
+ return matches.models.has(createModelSearchKey(model.provider, modelItem.model))
+ })
+ .filter(modelItem => modelSupportsScopeFeatures(modelItem, scopeFeatures))
+
+ if (
+ (trimmedInputValue && filteredModels.length === 0)
+ || (!trimmedInputValue && filteredModels.length === 0 && !aiCreditVisibleProviders.has(model.provider))
+ ) {
+ return null
+ }
+
+ return { ...model, models: filteredModels }
+ }).filter((model): model is Model => model !== null)
+
+ if (defaultModel?.provider) {
+ filtered.sort((a, b) => {
+ const aSelected = a.provider === defaultModel.provider ? 0 : 1
+ const bSelected = b.provider === defaultModel.provider ? 0 : 1
+
+ return aSelected - bSelected
+ })
+ }
+
+ return filtered
+}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx
index 6b9bcae8dc..e63b94aea3 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/model-selector-trigger.tsx
@@ -1,18 +1,9 @@
-import type { FC } from 'react'
-import type {
- DefaultModel,
- Model,
- ModelItem,
-} from '../declarations'
+import type { DefaultModel, Model, ModelItem } from '../declarations'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useTranslation } from 'react-i18next'
import { useProviderContext } from '@/context/provider-context'
-import {
- DERIVED_MODEL_STATUS_BADGE_I18N,
- DERIVED_MODEL_STATUS_TOOLTIP_I18N,
- deriveModelStatus,
-} from '../derive-model-status'
+import { DERIVED_MODEL_STATUS_BADGE_I18N, DERIVED_MODEL_STATUS_TOOLTIP_I18N, deriveModelStatus } from '../derive-model-status'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import { useCredentialPanelState } from '../provider-added-card/use-credential-panel-state'
@@ -28,7 +19,7 @@ type ModelSelectorTriggerProps = {
showDeprecatedWarnIcon?: boolean
}
-const ModelSelectorTrigger: FC = ({
+function ModelSelectorTrigger({
currentProvider,
currentModel,
defaultModel,
@@ -37,7 +28,7 @@ const ModelSelectorTrigger: FC = ({
className,
deprecatedClassName,
showDeprecatedWarnIcon = true,
-}) => {
+}: ModelSelectorTriggerProps) {
const { t } = useTranslation()
const { modelProviders } = useProviderContext()
@@ -100,7 +91,7 @@ const ModelSelectorTrigger: FC = ({
/>
)}
-
+
{isSelected && (
= ({
render={(
@@ -150,7 +141,7 @@ const ModelSelectorTrigger: FC
= ({
+
{deprecatedStatusLabel}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-empty-state.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-empty-state.tsx
index dafd26387b..c68e2df8fe 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-empty-state.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-empty-state.tsx
@@ -1,4 +1,3 @@
-import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { useTranslation } from 'react-i18next'
@@ -6,9 +5,9 @@ type ModelSelectorEmptyStateProps = {
onConfigure: () => void
}
-const ModelSelectorEmptyState: FC = ({
+function ModelSelectorEmptyState({
onConfigure,
-}) => {
+}: ModelSelectorEmptyStateProps) {
const { t } = useTranslation()
return (
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx
index ff9e6575bb..7a1ed57856 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx
@@ -1,60 +1,34 @@
-import type { FC } from 'react'
-import type {
- DefaultModel,
- Model,
- ModelItem,
-} from '../declarations'
+import type { DefaultModel, Model } from '../declarations'
import { cn } from '@langgenius/dify-ui/cn'
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from '@langgenius/dify-ui/popover'
-import {
- PreviewCard,
- PreviewCardContent,
- PreviewCardTrigger,
-} from '@langgenius/dify-ui/preview-card'
-import { useCallback, useState } from 'react'
+import { ComboboxGroup, ComboboxItem, ComboboxItemIndicator } from '@langgenius/dify-ui/combobox'
+import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
+import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
+import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
-import {
- ConfigurationMethodEnum,
- ModelFeatureEnum,
- ModelStatusEnum,
- ModelTypeEnum,
-} from '../declarations'
-import {
- useLanguage,
- useUpdateModelList,
- useUpdateModelProviders,
-} from '../hooks'
+import { ConfigurationMethodEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations'
+import { useLanguage, useUpdateModelList, useUpdateModelProviders } from '../hooks'
import ModelBadge from '../model-badge'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import DropdownContent from '../provider-added-card/model-auth-dropdown/dropdown-content'
import { useChangeProviderPriority } from '../provider-added-card/use-change-provider-priority'
import { useCredentialPanelState } from '../provider-added-card/use-credential-panel-state'
-import {
- modelTypeFormat,
- sizeFormat,
-} from '../utils'
+import { modelTypeFormat, sizeFormat } from '../utils'
import FeatureIcon from './feature-icon'
type PopupItemProps = {
defaultModel?: DefaultModel
model: Model
- onSelect: (provider: string, model: ModelItem) => void
onHide: () => void
}
-const PopupItem: FC = ({
+function PopupItem({
defaultModel,
model,
- onSelect,
onHide,
-}) => {
+}: PopupItemProps) {
const [collapsed, setCollapsed] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false)
const { t } = useTranslation()
@@ -64,12 +38,6 @@ const PopupItem: FC = ({
const updateModelList = useUpdateModelList()
const updateModelProviders = useUpdateModelProviders()
const currentProvider = modelProviders.find(provider => provider.provider === model.provider)
- const handleSelect = (provider: string, modelItem: ModelItem) => {
- if (modelItem.status !== ModelStatusEnum.active)
- return
-
- onSelect(provider, modelItem)
- }
const handleOpenModelModal = () => {
if (!currentProvider)
return
@@ -91,6 +59,12 @@ const PopupItem: FC = ({
const state = useCredentialPanelState(currentProvider)
const { isChangingPriority, handleChangePriority } = useChangeProviderPriority(currentProvider)
+ const groupItems = useMemo(() => model.models
+ .filter(modelItem => modelItem.status !== ModelStatusEnum.noConfigure)
+ .map(modelItem => ({
+ provider: model.provider,
+ model: modelItem.model,
+ })), [model.models, model.provider])
const isUsingCredits = state.priority === 'credits'
const hasCredits = !state.isCreditsExhausted
@@ -106,33 +80,33 @@ const PopupItem: FC = ({
return null
return (
-
- {/* Keep the sticky provider header above model rows while the list scrolls. */}
-
-
+
+ setCollapsed(prev => !prev)}
>
- {model.label[language] || model.label.en_US}
-
-
+
{model.label[language] || model.label.en_US}
+
+
+
{isUsingCredits
? (
hasCredits
? (
<>
- {t('modelProvider.selector.aiCredits', { ns: 'common' })}
+ {t('modelProvider.selector.aiCredits', { ns: 'common' })}
>
)
: (
<>
-
- {t('modelProvider.selector.creditsExhausted', { ns: 'common' })}
+
+ {t('modelProvider.selector.creditsExhausted', { ns: 'common' })}
>
)
)
@@ -140,16 +114,16 @@ const PopupItem: FC = ({
? (
<>
- {credentialName}
+ {credentialName}
>
)
: (
<>
- {t('modelProvider.selector.configureRequired', { ns: 'common' })}
+ {t('modelProvider.selector.configureRequired', { ns: 'common' })}
>
)}
-
+
)}
/>
@@ -164,100 +138,118 @@ const PopupItem: FC = ({
- {!collapsed && model.models.map(modelItem => (
- // Preview is supplementary: every field in it (name / type / mode / context size / capabilities)
- // is reachable from the model's own configuration surface once the row is selected.
- // Touch + screen reader users rely on the button's primary onClick, not the preview.
-
- handleSelect(model.provider, modelItem)}
+ {!collapsed && model.models.map((modelItem) => {
+ const rowClassName = cn(
+ 'group relative mx-1 flex h-8 min-w-0 items-center gap-1 rounded-lg px-3 py-1.5 text-left',
+ modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-not-allowed hover:bg-state-base-hover-alt',
+ )
+ const rowContent = (
+ <>
+
+
+
+
+ {
+ defaultModel?.model === modelItem.model && defaultModel.provider === currentProvider.provider && (
+
+
+
+ )
+ }
+ >
+ )
+ const itemRender = modelItem.status === ModelStatusEnum.noConfigure
+ ? (
+
+ {rowContent}
+
+ {t('operation.add', { ns: 'common' }).toLocaleUpperCase()}
+
+
+ )
+ : (
+
-
+ {rowContent}
+
+ )
+
+ return (
+
+
+
+
+
-
+
{modelItem.label[language] || modelItem.label.en_US}
- {
- defaultModel?.model === modelItem.model && defaultModel.provider === currentProvider.provider && (
-
- )
- }
- {
- modelItem.status === ModelStatusEnum.noConfigure && (
-
- {t('operation.add', { ns: 'common' }).toLocaleUpperCase()}
+
+ {!!modelItem.model_type && (
+
+ {modelTypeFormat(modelItem.model_type)}
+
+ )}
+ {!!modelItem.model_properties.mode && (
+
+ {(modelItem.model_properties.mode as string).toLocaleUpperCase()}
+
+ )}
+ {!!modelItem.model_properties.context_size && (
+
+ {sizeFormat(modelItem.model_properties.context_size as number)}
+
+ )}
+
+ {[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum)
+ && modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature))
+ && (
+
+
{t('model.capabilities', { ns: 'common' })}
+
+ {modelItem.features?.map(feature => (
+
+ ))}
+
- )
- }
-
- )}
- />
-
-
-
-
-
{modelItem.label[language] || modelItem.label.en_US}
+ )}
-
- {!!modelItem.model_type && (
-
- {modelTypeFormat(modelItem.model_type)}
-
- )}
- {!!modelItem.model_properties.mode && (
-
- {(modelItem.model_properties.mode as string).toLocaleUpperCase()}
-
- )}
- {!!modelItem.model_properties.context_size && (
-
- {sizeFormat(modelItem.model_properties.context_size as number)}
-
- )}
-
- {[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum)
- && modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature))
- && (
-
-
{t('model.capabilities', { ns: 'common' })}
-
- {modelItem.features?.map(feature => (
-
- ))}
-
-
- )}
-
-
-
- ))}
-
+
+
+ )
+ })}
+
)
}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-layout.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-layout.tsx
index 50bd098af1..95c930ef28 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-layout.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-layout.tsx
@@ -1,4 +1,6 @@
-import type { FC, ReactNode } from 'react'
+import type { ReactNode } from 'react'
+import { cn } from '@langgenius/dify-ui/cn'
+import { ComboboxInput, ComboboxInputGroup } from '@langgenius/dify-ui/combobox'
import {
ScrollAreaContent,
ScrollAreaRoot,
@@ -12,9 +14,9 @@ type ModelSelectorPopupFrameProps = {
children: ReactNode
}
-export const ModelSelectorPopupFrame: FC
= ({
+export function ModelSelectorPopupFrame({
children,
-}) => {
+}: ModelSelectorPopupFrameProps) {
return (
{children}
@@ -23,44 +25,52 @@ export const ModelSelectorPopupFrame: FC
= ({
}
type ModelSelectorSearchHeaderProps = {
- searchText: string
- onSearchTextChange: (value: string) => void
+ inputValue: string
+ onInputValueChange: (value: string) => void
}
-export const ModelSelectorSearchHeader: FC = ({
- searchText,
- onSearchTextChange,
-}) => {
+export function ModelSelectorSearchHeader({
+ inputValue,
+ onInputValueChange,
+}: ModelSelectorSearchHeaderProps) {
const { t } = useTranslation()
return (
-
- onSearchTextChange(e.target.value)}
/>
{
- searchText && (
- onSearchTextChange('')}
- />
+ inputValue && (
+ onInputValueChange('')}
+ onPointerDown={event => event.preventDefault()}
+ >
+
+
)
}
-
+
)
}
@@ -70,22 +80,19 @@ type ModelSelectorScrollBodyProps = {
label: string
}
-export const ModelSelectorScrollBody: FC = ({
+export function ModelSelectorScrollBody({
children,
label,
-}) => {
+}: ModelSelectorScrollBodyProps) {
return (
-
- {children}
-
+ {children}
- {/* Keep the overlay scrollbar above sticky provider headers inside this scroll area. */}
@@ -93,7 +100,7 @@ export const ModelSelectorScrollBody: FC = ({
)
}
-export const CompatibleModelsNotice = () => {
+export function CompatibleModelsNotice() {
const { t } = useTranslation()
return (
@@ -110,9 +117,9 @@ type ModelProviderSettingsFooterProps = {
onOpenSettings: () => void
}
-export const ModelProviderSettingsFooter: FC = ({
+export function ModelProviderSettingsFooter({
onOpenSettings,
-}) => {
+}: ModelProviderSettingsFooterProps) {
const { t } = useTranslation()
return (
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx
index 86bac84310..d8d873f26e 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx
@@ -1,10 +1,6 @@
-import type { FC } from 'react'
-import type {
- DefaultModel,
- Model,
- ModelItem,
-} from '../declarations'
+import type { DefaultModel, Model, ModelFeatureEnum } from '../declarations'
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
+import { ComboboxList } from '@langgenius/dify-ui/combobox'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useTheme } from 'next-themes'
import { useCallback, useMemo, useState } from 'react'
@@ -16,46 +12,37 @@ import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
-import { supportFunctionCall } from '@/utils/tool-call'
-import {
- CustomConfigurationStatusEnum,
- ModelFeatureEnum,
- ModelStatusEnum,
-} from '../declarations'
+import { CustomConfigurationStatusEnum, ModelStatusEnum } from '../declarations'
import { useLanguage, useMarketplaceAllPlugins } from '../hooks'
import CreditsExhaustedAlert from '../provider-added-card/model-auth-dropdown/credits-exhausted-alert'
import { useTrialCredits } from '../provider-added-card/use-trial-credits'
import { providerSupportsCredits } from '../supports-credits'
import { MODEL_PROVIDER_QUOTA_GET_PAID, providerKeyToPluginId } from '../utils'
import MarketplaceSection from './marketplace-section'
+import { createModelSelectorSearchIndex, filterModelSelectorModels } from './model-search'
import ModelSelectorEmptyState from './popup-empty-state'
import PopupItem from './popup-item'
-import {
- CompatibleModelsNotice,
- ModelProviderSettingsFooter,
- ModelSelectorPopupFrame,
- ModelSelectorScrollBody,
- ModelSelectorSearchHeader,
-} from './popup-layout'
+import { CompatibleModelsNotice, ModelProviderSettingsFooter, ModelSelectorPopupFrame, ModelSelectorScrollBody, ModelSelectorSearchHeader } from './popup-layout'
-type PopupProps = {
+export type PopupProps = {
defaultModel?: DefaultModel
+ inputValue: string
modelList: Model[]
- onSelect: (provider: string, model: ModelItem) => void
scopeFeatures?: ModelFeatureEnum[]
+ onInputValueChange: (value: string) => void
onHide: () => void
}
-const Popup: FC = ({
+function Popup({
defaultModel,
+ inputValue,
modelList,
- onSelect,
scopeFeatures = [],
+ onInputValueChange,
onHide,
-}) => {
+}: PopupProps) {
const { t } = useTranslation()
const { theme } = useTheme()
const language = useLanguage()
- const [searchText, setSearchText] = useState('')
const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
const { setShowAccountSettingModal } = useModalContext()
const { modelProviders } = useProviderContext()
@@ -142,57 +129,18 @@ const Popup: FC = ({
return [...installedMarketplaceModels, ...otherModels]
}, [aiCreditVisibleProviders, installedProviderMap, modelList])
- const filteredModelList = useMemo(() => {
- const normalizedSearch = searchText.toLowerCase()
- const matchesLabel = (label: Record) => {
- if (label[language] !== undefined)
- return label[language].toLowerCase().includes(normalizedSearch)
- return Object.values(label).some(value =>
- value.toLowerCase().includes(normalizedSearch),
- )
- }
-
- const filtered = installedModelList.map((model) => {
- const providerMatched = !!searchText && (
- matchesLabel(model.label)
- || model.provider.toLowerCase().includes(normalizedSearch)
- )
-
- const filteredModels = model.models
- .filter((modelItem) => {
- if (!searchText || providerMatched)
- return true
- return matchesLabel(modelItem.label)
- })
- .filter((modelItem) => {
- if (scopeFeatures.length === 0)
- return true
- return scopeFeatures.every((feature) => {
- if (feature === ModelFeatureEnum.toolCall)
- return supportFunctionCall(modelItem.features)
- return modelItem.features?.includes(feature) ?? false
- })
- })
- if (
- (searchText && filteredModels.length === 0)
- || (!searchText && filteredModels.length === 0 && !aiCreditVisibleProviders.has(model.provider))
- ) {
- return null
- }
-
- return { ...model, models: filteredModels }
- }).filter((model): model is Model => model !== null)
-
- if (defaultModel?.provider) {
- filtered.sort((a, b) => {
- const aSelected = a.provider === defaultModel.provider ? 0 : 1
- const bSelected = b.provider === defaultModel.provider ? 0 : 1
- return aSelected - bSelected
- })
- }
-
- return filtered
- }, [aiCreditVisibleProviders, defaultModel?.provider, installedModelList, language, scopeFeatures, searchText])
+ const searchIndex = useMemo(
+ () => createModelSelectorSearchIndex(installedModelList, language),
+ [installedModelList, language],
+ )
+ const filteredModelList = useMemo(() => filterModelSelectorModels({
+ aiCreditVisibleProviders,
+ defaultModel,
+ inputValue,
+ installedModelList,
+ scopeFeatures,
+ searchIndex,
+ }), [aiCreditVisibleProviders, defaultModel, inputValue, installedModelList, scopeFeatures, searchIndex])
const marketplaceProviders = useMemo(() => {
const installedProviders = new Set(modelProviders.map(provider => provider.provider))
@@ -207,33 +155,36 @@ const Popup: FC = ({
return (
{showCreditsExhaustedAlert && (
)}
+
+
+ {
+ filteredModelList.map(model => (
+
+ ))
+ }
+
+
- {
- filteredModelList.map(model => (
-
- ))
- }
{!filteredModelList.length && !installedModelList.length && (
)}
{!filteredModelList.length && installedModelList.length > 0 && (
-
- {`No model found for \u201C${searchText}\u201D`}
+
+ {`No model found for \u201C${inputValue}\u201D`}
)}
{scopeFeatures.length > 0 && (
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/types.ts b/web/app/components/header/account-setting/model-provider-page/model-selector/types.ts
new file mode 100644
index 0000000000..93314a6208
--- /dev/null
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/types.ts
@@ -0,0 +1,11 @@
+export type ModelSelectorValue = {
+ provider: string
+ model: string
+}
+
+export const isSameModelSelectorValue = (
+ itemValue: ModelSelectorValue,
+ value: ModelSelectorValue,
+) => itemValue.provider === value.provider && itemValue.model === value.model
+
+export const getModelSelectorValueLabel = (itemValue: ModelSelectorValue) => itemValue.model
diff --git a/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx b/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx
index d99e3514db..58bbd441ce 100644
--- a/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx
+++ b/web/app/components/plugins/plugin-auth/authorize/__tests__/oauth-client-settings.spec.tsx
@@ -225,7 +225,7 @@ describe('OAuthClientSettings', () => {
expect(mockOnClose).toHaveBeenCalled()
})
- it('should close when backdrop is clicked', async () => {
+ it('should stay open when backdrop is clicked', () => {
const mockOnClose = vi.fn()
render(
)
@@ -234,10 +234,8 @@ describe('OAuthClientSettings', () => {
fireEvent.click(backdrop!)
- await waitFor(() => {
- expect(screen.getByTestId('modal-open-state')).toHaveTextContent('false')
- })
- expect(mockOnClose).toHaveBeenCalled()
+ expect(screen.getByTestId('modal-open-state')).toHaveTextContent('true')
+ expect(mockOnClose).not.toHaveBeenCalled()
})
it('should save settings on save only button click', async () => {
diff --git a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx
index d06ecf1d60..50718d50db 100644
--- a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx
+++ b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx
@@ -138,6 +138,7 @@ const OAuthClientSettings = ({
return (
{
expect(document.querySelector('.custom-popup-class'))!.toBeInTheDocument()
})
- it('should pass placement to PortalToFollowElem', () => {
+ it('should pass placement to Popover', () => {
const pluginPayload = createPluginPayload()
const credentials = [createCredential()]
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-picker.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-picker.spec.tsx
deleted file mode 100644
index af3f97c889..0000000000
--- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-picker.spec.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import type { ReactNode } from 'react'
-import { fireEvent, render, screen } from '@testing-library/react'
-import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
-import { AppModeEnum } from '@/types/app'
-import AppPicker from '../app-picker'
-
-class MockIntersectionObserver {
- observe = vi.fn()
- disconnect = vi.fn()
- unobserve = vi.fn()
-}
-
-class MockMutationObserver {
- observe = vi.fn()
- disconnect = vi.fn()
- takeRecords = vi.fn().mockReturnValue([])
-}
-
-beforeAll(() => {
- vi.stubGlobal('IntersectionObserver', MockIntersectionObserver)
- vi.stubGlobal('MutationObserver', MockMutationObserver)
-})
-
-vi.mock('@/app/components/base/app-icon', () => ({
- default: () =>
,
-}))
-
-vi.mock('@/app/components/base/input', () => ({
- default: ({
- value,
- onChange,
- onClear,
- }: {
- value: string
- onChange: (e: { target: { value: string } }) => void
- onClear?: () => void
- }) => (
-
- onChange({ target: { value: e.target.value } })}
- />
- Clear
-
- ),
-}))
-
-vi.mock('@langgenius/dify-ui/popover', () => ({
- Popover: ({
- children,
- open,
- }: {
- children: ReactNode
- open: boolean
- }) => (
-
- {children}
-
- ),
- PopoverTrigger: ({
- children,
- render,
- onClick,
- }: {
- children: ReactNode
- render?: ReactNode
- onClick?: () => void
- }) => (
-
- {render ?? children}
-
- ),
- PopoverContent: ({ children }: { children: ReactNode }) => (
- {children}
- ),
-}))
-
-const apps = [
- {
- id: 'app-1',
- name: 'Chat App',
- mode: AppModeEnum.CHAT,
- icon_type: 'emoji',
- icon: '🤖',
- icon_background: '#fff',
- },
- {
- id: 'app-2',
- name: 'Workflow App',
- mode: AppModeEnum.WORKFLOW,
- icon_type: 'emoji',
- icon: '⚙️',
- icon_background: '#fff',
- },
-]
-
-describe('AppPicker', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- it('should open when the trigger is clicked', () => {
- const onShowChange = vi.fn()
-
- render(
- Trigger}
- isShow={false}
- onShowChange={onShowChange}
- onSelect={vi.fn()}
- apps={apps as never}
- isLoading={false}
- hasMore={false}
- onLoadMore={vi.fn()}
- searchText=""
- onSearchChange={vi.fn()}
- />,
- )
-
- fireEvent.click(screen.getByTestId('picker-trigger'))
-
- expect(onShowChange).toHaveBeenCalledWith(true)
- })
-
- it('should render apps, select one, and handle search changes', () => {
- const onSelect = vi.fn()
- const onSearchChange = vi.fn()
-
- render(
- Trigger}
- isShow
- onShowChange={vi.fn()}
- onSelect={onSelect}
- apps={apps as never}
- isLoading={false}
- hasMore={false}
- onLoadMore={vi.fn()}
- searchText="chat"
- onSearchChange={onSearchChange}
- />,
- )
-
- fireEvent.change(screen.getByTestId('search-input'), {
- target: { value: 'workflow' },
- })
- fireEvent.click(screen.getByText('Workflow App'))
- fireEvent.click(screen.getByTestId('clear-input'))
-
- expect(onSearchChange).toHaveBeenCalledWith('workflow')
- expect(onSearchChange).toHaveBeenCalledWith('')
- expect(onSelect).toHaveBeenCalledWith(apps[1])
- expect(screen.getByText('chat')).toBeInTheDocument()
- })
-
- it('should render loading text when loading more apps', () => {
- render(
- Trigger}
- isShow
- onShowChange={vi.fn()}
- onSelect={vi.fn()}
- apps={apps as never}
- isLoading
- hasMore
- onLoadMore={vi.fn()}
- searchText=""
- onSearchChange={vi.fn()}
- />,
- )
-
- expect(screen.getByText('common.loading')).toBeInTheDocument()
- })
-})
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-trigger.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-trigger.spec.tsx
deleted file mode 100644
index 1b6ac7f1f0..0000000000
--- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-trigger.spec.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { render, screen } from '@testing-library/react'
-import * as React from 'react'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-vi.mock('@/app/components/base/app-icon', () => ({
- default: ({ size }: { size: string }) =>
,
-}))
-
-vi.mock('@langgenius/dify-ui/cn', () => ({
- cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
-}))
-
-describe('AppTrigger', () => {
- let AppTrigger: (typeof import('../app-trigger'))['default']
-
- beforeEach(async () => {
- vi.clearAllMocks()
- const mod = await import('../app-trigger')
- AppTrigger = mod.default
- })
-
- it('should render placeholder when no app is selected', () => {
- render( )
-
- expect(screen.queryByTestId('app-icon')).not.toBeInTheDocument()
- })
-
- it('should render app details when appDetail is provided', () => {
- const appDetail = {
- name: 'My App',
- icon_type: 'emoji',
- icon: '🤖',
- icon_background: '#fff',
- }
- render( )
-
- expect(screen.getByTestId('app-icon')).toBeInTheDocument()
- expect(screen.getByText('My App')).toBeInTheDocument()
- })
-
- it('should render when open', () => {
- const { container } = render( )
-
- expect(container.firstChild).toBeInTheDocument()
- })
-})
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx
index 38b5324fa7..8810d35b28 100644
--- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx
@@ -1,2622 +1,201 @@
import type { ReactNode } from 'react'
+import type { AppSelectorValue } from '../index'
import type { App } from '@/types/app'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { act, fireEvent, render, screen } from '@testing-library/react'
-import * as React from 'react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { useState } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { InputVarType } from '@/app/components/workflow/types'
import { AppModeEnum } from '@/types/app'
-import AppInputsForm from '../app-inputs-form'
-import AppInputsPanel from '../app-inputs-panel'
-import AppPicker from '../app-picker'
-import AppTrigger from '../app-trigger'
+import { AppSelector } from '../index'
-import AppSelector from '../index'
-
-// ==================== Mock Setup ====================
-
-const mockAppListInfiniteOptions = vi.hoisted(() => vi.fn((options: unknown) => options))
-
-// Mock IntersectionObserver globally using class syntax
-let intersectionObserverCallback: IntersectionObserverCallback | null = null
-const mockIntersectionObserver = {
- observe: vi.fn(),
- disconnect: vi.fn(),
- unobserve: vi.fn(),
- root: null,
- rootMargin: '',
- thresholds: [],
- takeRecords: vi.fn().mockReturnValue([]),
-} as unknown as IntersectionObserver
-
-// Helper function to trigger intersection observer callback
-const triggerIntersection = (entries: IntersectionObserverEntry[]) => {
- if (intersectionObserverCallback) {
- intersectionObserverCallback(entries, mockIntersectionObserver)
- }
-}
-
-class MockIntersectionObserver {
- constructor(callback: IntersectionObserverCallback) {
- intersectionObserverCallback = callback
- }
-
- observe = vi.fn()
- disconnect = vi.fn()
- unobserve = vi.fn()
-}
-
-// Mock MutationObserver globally using class syntax
-let mutationObserverCallback: MutationCallback | null = null
-
-class MockMutationObserver {
- constructor(callback: MutationCallback) {
- mutationObserverCallback = callback
- }
-
- observe = vi.fn()
- disconnect = vi.fn()
- takeRecords = vi.fn().mockReturnValue([])
-}
-
-// Helper function to trigger mutation observer callback
-const triggerMutationObserver = () => {
- if (mutationObserverCallback) {
- mutationObserverCallback([], new MockMutationObserver(() => {}))
- }
-}
-
-// Set up global mocks before tests
-beforeAll(() => {
- vi.stubGlobal('IntersectionObserver', MockIntersectionObserver)
- vi.stubGlobal('MutationObserver', MockMutationObserver)
-})
-
-afterAll(() => {
- vi.unstubAllGlobals()
-})
-
-// Mock portal components for controlled positioning in tests
-// Use React context to properly scope open state per portal instance (for nested portals)
-vi.mock('@langgenius/dify-ui/popover', () => {
- // Context reference shared across mock components
- let sharedContext: React.Context | null = null
-
- // Lazily get or create the context
- const getContext = (): React.Context => {
- if (!sharedContext) {
- const PortalOpenContext = React.createContext(false)
- sharedContext = PortalOpenContext
- }
- return sharedContext
- }
-
- return {
- Popover: ({
- children,
- open,
- }: {
- children: ReactNode
- open?: boolean
- }) => {
- const Context = getContext()
- return React.createElement(
- Context.Provider,
- { value: open || false },
- React.createElement('div', { 'data-testid': 'portal-to-follow-elem', 'data-open': open }, children),
- )
- },
- PopoverTrigger: ({
- children,
- render,
- onClick,
- className,
- }: {
- children: ReactNode
- render?: ReactNode
- onClick?: () => void
- className?: string
- }) => (
-
- {render ?? children}
-
- ),
- PopoverContent: ({ children, className }: { children: ReactNode, className?: string }) => {
- const Context = getContext()
- const isOpen = React.useContext(Context)
- if (!isOpen)
- return null
- return (
- {children}
- )
- },
- }
-})
-
-// Mock service hooks
-let mockAppListData: { pages: Array<{ data: App[], has_more: boolean, page: number }> } | undefined
-let mockIsLoading = false
-let mockIsFetchingNextPage = false
-let mockHasNextPage = true
-const mockFetchNextPage = vi.fn()
-
-// Allow configurable mock data for useAppDetail
-let mockAppDetailData: App | undefined | null
-let mockAppDetailLoading = false
-
-// Helper to get app detail data - avoids nested ternary and hoisting issues
-const getAppDetailData = (appId: string) => {
- if (mockAppDetailData !== undefined)
- return mockAppDetailData
- if (!appId)
- return undefined
- // Extract number from appId (e.g., 'app-1' -> '1') for consistent naming with createMockApps
- const appNumber = appId.replace('app-', '')
- // Return a basic mock app structure
- return {
- id: appId,
- name: `App ${appNumber}`,
- mode: 'chat',
+const apps: App[] = [
+ {
+ id: 'app-1',
+ name: 'Support Bot',
+ mode: AppModeEnum.CHAT,
icon_type: 'emoji',
icon: '🤖',
icon_background: '#FFEAD5',
- model_config: { user_input_form: [] },
- }
-}
-
-vi.mock('@/service/use-apps', () => ({
- useAppDetail: (appId: string) => ({
- data: getAppDetailData(appId),
- isFetching: mockAppDetailLoading,
- }),
-}))
+ model_config: {
+ user_input_form: [],
+ },
+ } as unknown as App,
+ {
+ id: 'app-2',
+ name: 'Workflow App',
+ mode: AppModeEnum.WORKFLOW,
+ icon_type: 'emoji',
+ icon: '⚙️',
+ icon_background: '#E0EAFF',
+ } as unknown as App,
+]
vi.mock('@/service/client', () => ({
consoleQuery: {
apps: {
list: {
- infiniteOptions: (options: unknown) => mockAppListInfiniteOptions(options),
+ infiniteOptions: ({
+ input,
+ getNextPageParam,
+ initialPageParam,
+ placeholderData,
+ }: {
+ input: (pageParam: number) => { query: { name?: string } }
+ getNextPageParam: (lastPage: { has_more: boolean, page: number }) => number | undefined
+ initialPageParam: number
+ placeholderData: unknown
+ }) => ({
+ queryKey: ['apps', input(1).query],
+ queryFn: ({ pageParam = initialPageParam }: { pageParam?: number }) => {
+ const query = input(Number(pageParam)).query
+ const keyword = query.name?.toLowerCase() ?? ''
+ const filteredApps = keyword
+ ? apps.filter(app => app.name.toLowerCase().includes(keyword))
+ : apps
+
+ return {
+ data: filteredApps,
+ has_more: false,
+ page: Number(pageParam),
+ }
+ },
+ getNextPageParam,
+ initialPageParam,
+ placeholderData,
+ }),
},
},
},
}))
-vi.mock('@tanstack/react-query', async (importOriginal) => {
- const actual = await importOriginal()
- return {
- ...actual,
- useInfiniteQuery: () => ({
- data: mockAppListData,
- isLoading: mockIsLoading,
- isFetchingNextPage: mockIsFetchingNextPage,
- fetchNextPage: mockFetchNextPage,
- hasNextPage: mockHasNextPage,
- }),
- }
-})
+vi.mock('@/service/use-apps', () => ({
+ useAppDetail: (appId: string) => ({
+ data: apps.find(app => app.id === appId),
+ }),
+}))
-// Allow configurable mock data for useAppWorkflow
-let mockWorkflowData: Record | undefined | null
-let mockWorkflowLoading = false
-
-// Helper to get workflow data - avoids nested ternary
-const getWorkflowData = (appId: string) => {
- if (mockWorkflowData !== undefined)
- return mockWorkflowData
- if (!appId)
- return undefined
- return {
- graph: {
- nodes: [
- {
- data: {
- type: 'start',
- variables: [
- { type: 'text-input', label: 'Name', variable: 'name', required: false },
- ],
- },
- },
- ],
- },
- features: {},
- }
-}
+vi.mock('@/service/use-common', () => ({
+ useFileUploadConfig: () => ({ data: undefined }),
+}))
vi.mock('@/service/use-workflow', () => ({
- useAppWorkflow: (appId: string) => ({
- data: getWorkflowData(appId),
- isFetching: mockWorkflowLoading,
- }),
+ useAppWorkflow: () => ({ data: undefined, isFetching: false }),
}))
-// Mock common service
-vi.mock('@/service/use-common', () => ({
- useFileUploadConfig: () => ({
- data: {
- image_file_size_limit: 10,
- file_size_limit: 15,
- audio_file_size_limit: 50,
- video_file_size_limit: 100,
- workflow_file_upload_limit: 10,
- },
- }),
-}))
-
-// Mock file uploader
-vi.mock('@/app/components/base/file-uploader', () => ({
- FileUploaderInAttachmentWrapper: ({ onChange, value }: { onChange: (files: unknown[]) => void, value: unknown[] }) => (
-
- {JSON.stringify(value)}
- onChange([{ id: 'file-1', name: 'test.png' }])}
- >
- Upload
-
- onChange([{ id: 'file-1' }, { id: 'file-2' }])}
- >
- Upload Multiple
-
-
- ),
-}))
-
-// Mock Select for testing select field interactions
-vi.mock('@langgenius/dify-ui/select', async () => {
- const React = await import('react')
- const SelectContext = React.createContext<{
- onValueChange?: (value: string) => void
- }>({})
-
- return {
- Select: ({ children, onValueChange }: {
- children: React.ReactNode
- onValueChange?: (value: string) => void
- }) => (
-
- {children}
-
- ),
- SelectTrigger: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- SelectContent: ({ children }: { children: React.ReactNode }) => {children}
,
- SelectItem: ({ children, value }: { children: React.ReactNode, value: string }) => {
- const context = React.useContext(SelectContext)
- return (
- context.onValueChange?.(value)}
- >
- {children}
-
- )
- },
- SelectItemText: ({ children }: { children: React.ReactNode }) => <>{children}>,
- SelectItemIndicator: () => null,
- }
-})
-
-// Mock Input component with onClear support
-vi.mock('@/app/components/base/input', () => ({
- default: ({ onChange, onClear, value, showClearIcon, ...props }: {
- onChange: (e: { target: { value: string } }) => void
- onClear?: () => void
- value: string
- showClearIcon?: boolean
- placeholder?: string
- }) => (
-
-
- {showClearIcon && onClear && (
- Clear
- )}
-
- ),
-}))
-
-// ==================== Test Utilities ====================
-
-const createTestQueryClient = () =>
- new QueryClient({
+function renderWithQueryClient(children: ReactNode) {
+ const queryClient = new QueryClient({
defaultOptions: {
- queries: { retry: false },
- mutations: { retry: false },
+ queries: {
+ retry: false,
+ },
},
})
-const renderWithQueryClient = (ui: React.ReactElement) => {
- const queryClient = createTestQueryClient()
return render(
- {ui}
+ {children}
,
)
}
-type AppSelectorInfiniteOptions = {
- input: (pageParam: number) => { query: Record }
- getNextPageParam: (lastPage: { has_more: boolean, page: number }) => number | undefined
+function StatefulAppSelector({
+ onSelect,
+}: {
+ onSelect: (value: AppSelectorValue) => void
+}) {
+ const [value, setValue] = useState()
+
+ return (
+ {
+ setValue(nextValue)
+ onSelect(nextValue)
+ }}
+ />
+ )
}
-// Mock data factories
-const createMockApp = (overrides: Record = {}): App => ({
- id: 'app-1',
- name: 'Test App',
- description: 'A test app',
- mode: AppModeEnum.CHAT,
- icon_type: 'emoji',
- icon: '🤖',
- icon_background: '#FFEAD5',
- icon_url: null,
- use_icon_as_answer_icon: false,
- enable_site: true,
- enable_api: true,
- api_rpm: 60,
- api_rph: 3600,
- is_demo: false,
- model_config: {
- provider: 'openai',
- model_id: 'gpt-4',
- model: {
- provider: 'openai',
- name: 'gpt-4',
- mode: 'chat',
- completion_params: {},
- },
- configs: {
- prompt_template: '',
- prompt_variables: [],
- completion_params: {},
- },
- opening_statement: '',
- suggested_questions: [],
- suggested_questions_after_answer: { enabled: false },
- speech_to_text: { enabled: false },
- text_to_speech: { enabled: false, voice: '', language: '' },
- retriever_resource: { enabled: false },
- annotation_reply: { enabled: false },
- more_like_this: { enabled: false },
- sensitive_word_avoidance: { enabled: false },
- external_data_tools: [],
- dataSets: [],
- agentMode: { enabled: false, strategy: null, tools: [] },
- chatPromptConfig: {},
- completionPromptConfig: {},
- file_upload: {},
- user_input_form: [],
- },
- app_model_config: {},
- created_at: Date.now(),
- updated_at: Date.now(),
- site: {},
- api_base_url: '',
- tags: [],
- access_mode: 'public',
- ...overrides,
-} as unknown as App)
-
-// Helper function to get app mode based on index
-const getAppModeByIndex = (index: number): AppModeEnum => {
- if (index % 5 === 0)
- return AppModeEnum.ADVANCED_CHAT
- if (index % 4 === 0)
- return AppModeEnum.AGENT_CHAT
- if (index % 3 === 0)
- return AppModeEnum.WORKFLOW
- if (index % 2 === 0)
- return AppModeEnum.COMPLETION
- return AppModeEnum.CHAT
-}
-
-const createMockApps = (count: number): App[] => {
- return Array.from({ length: count }, (_, i) =>
- createMockApp({
- id: `app-${i + 1}`,
- name: `App ${i + 1}`,
- mode: getAppModeByIndex(i),
- }))
-}
-
-// ==================== AppTrigger Tests ====================
-
-describe('AppTrigger', () => {
- describe('Rendering', () => {
- it('should render placeholder when no app is selected', () => {
- render( )
- // i18n mock returns key with namespace in dot format
- // i18n mock returns key with namespace in dot format
- expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument()
- })
-
- it('should render app details when app is selected', () => {
- const app = createMockApp({ name: 'My Test App' })
- render( )
- expect(screen.getByText('My Test App'))!.toBeInTheDocument()
- })
-
- it('should apply open state styling', () => {
- const { container } = render( )
- const trigger = container.querySelector('.bg-state-base-hover-alt')
- expect(trigger)!.toBeInTheDocument()
- })
-
- it('should render AppIcon when app is provided', () => {
- const app = createMockApp()
- const { container } = render( )
- // AppIcon renders with a specific class when app is provided
- const iconContainer = container.querySelector('.mr-2')
- expect(iconContainer)!.toBeInTheDocument()
- })
- })
-
- describe('Props', () => {
- it('should handle undefined appDetail gracefully', () => {
- render( )
- expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument()
- })
-
- it('should display app name with title attribute', () => {
- const app = createMockApp({ name: 'Long App Name For Testing' })
- render( )
- const nameElement = screen.getByTitle('Long App Name For Testing')
- expect(nameElement)!.toBeInTheDocument()
- })
- })
-
- describe('Styling', () => {
- it('should have correct base classes', () => {
- const { container } = render( )
- const trigger = container.firstChild as HTMLElement
- expect(trigger)!.toHaveClass('group', 'flex', 'cursor-pointer')
- })
-
- it('should apply different padding when app is provided', () => {
- const app = createMockApp()
- const { container } = render( )
- const trigger = container.firstChild as HTMLElement
- expect(trigger)!.toHaveClass('py-1.5', 'pl-1.5')
- })
- })
-})
-
-// ==================== AppPicker Tests ====================
-
-describe('AppPicker', () => {
- const defaultProps = {
- scope: 'all',
- disabled: false,
- trigger: Select App ,
- placement: 'right-start' as const,
- offset: 0,
- isShow: false,
- onShowChange: vi.fn(),
- onSelect: vi.fn(),
- apps: createMockApps(5),
- isLoading: false,
- hasMore: false,
- onLoadMore: vi.fn(),
- searchText: '',
- onSearchChange: vi.fn(),
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- vi.useFakeTimers()
- })
-
- afterEach(() => {
- vi.useRealTimers()
- })
-
- describe('Rendering', () => {
- it('should render trigger element', () => {
- render( )
- expect(screen.getByText('Select App'))!.toBeInTheDocument()
- })
-
- it('should render app list when open', () => {
- render( )
- expect(screen.getByText('App 1'))!.toBeInTheDocument()
- expect(screen.getByText('App 2'))!.toBeInTheDocument()
- })
-
- it('should show loading indicator when isLoading is true', () => {
- render( )
- expect(screen.getByText('common.loading'))!.toBeInTheDocument()
- })
-
- it('should not render content when isShow is false', () => {
- render( )
- expect(screen.queryByText('App 1')).not.toBeInTheDocument()
- })
- })
-
- describe('User Interactions', () => {
- it('should call onSelect when app is clicked', () => {
- const onSelect = vi.fn()
- render( )
-
- fireEvent.click(screen.getByText('App 1'))
- expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'app-1' }))
- })
-
- it('should call onSearchChange when typing in search input', () => {
- const onSearchChange = vi.fn()
- render( )
-
- const input = screen.getByRole('textbox')
- fireEvent.change(input, { target: { value: 'test' } })
- expect(onSearchChange).toHaveBeenCalledWith('test')
- })
-
- it('should not call onShowChange when disabled', () => {
- const onShowChange = vi.fn()
- render( )
-
- fireEvent.click(screen.getByTestId('portal-trigger'))
- expect(onShowChange).not.toHaveBeenCalled()
- })
-
- it('should call onShowChange when trigger is clicked and not disabled', () => {
- const onShowChange = vi.fn()
- render( )
-
- fireEvent.click(screen.getByTestId('portal-trigger'))
- expect(onShowChange).toHaveBeenCalledWith(true)
- })
- })
-
- describe('App Type Display', () => {
- it('should display correct app type for CHAT', () => {
- const apps = [createMockApp({ id: 'chat-app', name: 'Chat App', mode: AppModeEnum.CHAT })]
- render( )
- expect(screen.getByText('chat'))!.toBeInTheDocument()
- })
-
- it('should display correct app type for WORKFLOW', () => {
- const apps = [createMockApp({ id: 'workflow-app', name: 'Workflow App', mode: AppModeEnum.WORKFLOW })]
- render( )
- expect(screen.getByText('workflow'))!.toBeInTheDocument()
- })
-
- it('should display correct app type for ADVANCED_CHAT', () => {
- const apps = [createMockApp({ id: 'chatflow-app', name: 'Chatflow App', mode: AppModeEnum.ADVANCED_CHAT })]
- render( )
- expect(screen.getByText('chatflow'))!.toBeInTheDocument()
- })
-
- it('should display correct app type for AGENT_CHAT', () => {
- const apps = [createMockApp({ id: 'agent-app', name: 'Agent App', mode: AppModeEnum.AGENT_CHAT })]
- render( )
- expect(screen.getByText('agent'))!.toBeInTheDocument()
- })
-
- it('should display correct app type for COMPLETION', () => {
- const apps = [createMockApp({ id: 'completion-app', name: 'Completion App', mode: AppModeEnum.COMPLETION })]
- render( )
- expect(screen.getByText('completion'))!.toBeInTheDocument()
- })
- })
-
- describe('Edge Cases', () => {
- it('should handle empty apps array', () => {
- render( )
- expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
- })
-
- it('should handle search text with value', () => {
- render( )
- const input = screen.getByTestId('input')
- expect(input)!.toHaveValue('test search')
- })
- })
-
- describe('Search Clear', () => {
- it('should call onSearchChange with empty string when clear button is clicked', () => {
- const onSearchChange = vi.fn()
- render( )
-
- const clearBtn = screen.getByTestId('clear-btn')
- fireEvent.click(clearBtn)
- expect(onSearchChange).toHaveBeenCalledWith('')
- })
- })
-
- describe('Infinite Scroll', () => {
- it('should not call onLoadMore when isLoading is true', () => {
- const onLoadMore = vi.fn()
-
- render( )
-
- // Simulate intersection
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
-
- // onLoadMore should not be called because isLoading blocks it
- expect(onLoadMore).not.toHaveBeenCalled()
- })
-
- it('should not call onLoadMore when hasMore is false', () => {
- const onLoadMore = vi.fn()
-
- render( )
-
- // Simulate intersection
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
-
- // onLoadMore should not be called when hasMore is false
- expect(onLoadMore).not.toHaveBeenCalled()
- })
-
- it('should call onLoadMore when intersection observer fires and conditions are met', () => {
- const onLoadMore = vi.fn()
-
- render( )
-
- // Simulate intersection
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
-
- expect(onLoadMore).toHaveBeenCalled()
- })
-
- it('should not call onLoadMore when target is not intersecting', () => {
- const onLoadMore = vi.fn()
-
- render( )
-
- // Simulate non-intersecting
- triggerIntersection([{ isIntersecting: false } as IntersectionObserverEntry])
-
- expect(onLoadMore).not.toHaveBeenCalled()
- })
-
- it('should handle observer target ref', () => {
- render( )
-
- // The component should render without errors
- // The component should render without errors
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle isShow toggle correctly', () => {
- const { rerender } = render( )
-
- // Change isShow to true
- rerender( )
-
- // Then back to false
- rerender( )
-
- // Should not crash
- // Should not crash
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should setup intersection observer when isShow is true', () => {
- render( )
-
- // IntersectionObserver callback should have been set
- expect(intersectionObserverCallback).not.toBeNull()
- })
-
- it('should disconnect observer when isShow changes from true to false', () => {
- const { rerender } = render( )
-
- // Verify observer was set up
- expect(intersectionObserverCallback).not.toBeNull()
-
- // Change to not shown - should disconnect observer (lines 74-75)
- rerender( )
-
- // Component should render without errors
- // Component should render without errors
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should cleanup observer on component unmount', () => {
- const { unmount } = render( )
-
- // Unmount should trigger cleanup without throwing
- expect(() => unmount()).not.toThrow()
- })
-
- it('should handle MutationObserver callback when target becomes available', () => {
- render( )
-
- // Trigger MutationObserver callback (simulates DOM change)
- triggerMutationObserver()
-
- // Component should still work correctly
- // Component should still work correctly
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should not setup IntersectionObserver when observerTarget is null', () => {
- // When isShow is false, the observer target won't be in the DOM
- render( )
-
- // The guard at line 84 should prevent setup
- // The guard at line 84 should prevent setup
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should debounce onLoadMore calls using loadingRef', () => {
- const onLoadMore = vi.fn()
-
- render( )
-
- // First intersection should trigger onLoadMore
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
- expect(onLoadMore).toHaveBeenCalledTimes(1)
-
- // Second immediate intersection should be blocked by loadingRef
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
- // Still only called once due to loadingRef debounce
- expect(onLoadMore).toHaveBeenCalledTimes(1)
-
- // After 500ms timeout, loadingRef should reset
- act(() => {
- vi.advanceTimersByTime(600)
- })
-
- // Now it can be called again
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
- expect(onLoadMore).toHaveBeenCalledTimes(2)
- })
-
- it('should reset loadingRef when the picker closes before the debounce timeout finishes', () => {
- const onLoadMore = vi.fn()
- const { rerender } = render(
- ,
- )
-
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
- expect(onLoadMore).toHaveBeenCalledTimes(1)
-
- rerender( )
- rerender( )
-
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
- expect(onLoadMore).toHaveBeenCalledTimes(2)
- })
-
- it('should reset loadingRef when the picker unmounts before the debounce timeout finishes', () => {
- const onLoadMore = vi.fn()
- const { unmount } = render(
- ,
- )
-
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
- expect(onLoadMore).toHaveBeenCalledTimes(1)
-
- unmount()
-
- render( )
-
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
- expect(onLoadMore).toHaveBeenCalledTimes(2)
- })
- })
-
- describe('Memoization', () => {
- it('should be wrapped with React.memo', () => {
- expect(AppPicker).toBeDefined()
- const onSelect = vi.fn()
- const { rerender } = render( )
- rerender( )
- })
- })
-})
-
-// ==================== AppInputsForm Tests ====================
-
-describe('AppInputsForm', () => {
- const mockInputsRef = { current: {} as Record }
-
- const defaultProps = {
- inputsForms: [],
- inputs: {} as Record,
- inputsRef: mockInputsRef,
- onFormChange: vi.fn(),
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- mockInputsRef.current = {}
- })
-
- describe('Rendering', () => {
- it('should return null when inputsForms is empty', () => {
- const { container } = render( )
- expect(container.firstChild).toBeNull()
- })
-
- it('should render text input field', () => {
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false },
- ]
- render( )
- expect(screen.getByText('Name'))!.toBeInTheDocument()
- expect(screen.getByPlaceholderText('Name'))!.toBeInTheDocument()
- })
-
- it('should render number input field', () => {
- const forms = [
- { type: InputVarType.number, label: 'Count', variable: 'count', required: false },
- ]
- render( )
- expect(screen.getByText('Count'))!.toBeInTheDocument()
- })
-
- it('should render paragraph (textarea) field', () => {
- const forms = [
- { type: InputVarType.paragraph, label: 'Description', variable: 'desc', required: false },
- ]
- render( )
- expect(screen.getByText('Description'))!.toBeInTheDocument()
- })
-
- it('should render select field', () => {
- const forms = [
- { type: InputVarType.select, label: 'Select Option', variable: 'option', options: ['a', 'b'], required: false },
- ]
- render( )
- // Label and placeholder both contain "Select Option"
- expect(screen.getAllByText(/Select Option/).length).toBeGreaterThanOrEqual(1)
- })
-
- it('should render file uploader for single file', () => {
- const forms = [
- {
- type: InputVarType.singleFile,
- label: 'Single File Upload',
- variable: 'file',
- required: false,
- allowed_file_types: ['image'],
- allowed_file_extensions: ['.png'],
- allowed_file_upload_methods: ['local_file'],
- },
- ]
- render( )
- expect(screen.getByText('Single File Upload'))!.toBeInTheDocument()
- expect(screen.getByTestId('file-uploader'))!.toBeInTheDocument()
- })
-
- it('should render file uploader for single file with existing value', () => {
- const existingFile = { id: 'existing-file-1', name: 'test.png' }
- const forms = [
- {
- type: InputVarType.singleFile,
- label: 'Single File',
- variable: 'singleFile',
- required: false,
- allowed_file_types: ['image'],
- allowed_file_extensions: ['.png'],
- allowed_file_upload_methods: ['local_file'],
- },
- ]
- render( )
- // The file uploader should receive the existing file as an array
- // The file uploader should receive the existing file as an array
- expect(screen.getByTestId('file-value'))!.toHaveTextContent(JSON.stringify([existingFile]))
- })
-
- it('should render file uploader for multi files', () => {
- const forms = [
- {
- type: InputVarType.multiFiles,
- label: 'Attachments',
- variable: 'files',
- required: false,
- max_length: 5,
- allowed_file_types: ['image'],
- allowed_file_extensions: ['.png', '.jpg'],
- allowed_file_upload_methods: ['local_file'],
- },
- ]
- render( )
- expect(screen.getByText('Attachments'))!.toBeInTheDocument()
- })
-
- it('should show optional label for non-required fields', () => {
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false },
- ]
- render( )
- expect(screen.getByText('workflow.panel.optional'))!.toBeInTheDocument()
- })
-
- it('should not show optional label for required fields', () => {
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: true },
- ]
- render( )
- expect(screen.queryByText('workflow.panel.optional')).not.toBeInTheDocument()
- })
- })
-
- describe('User Interactions', () => {
- it('should call onFormChange when text input changes', () => {
- const onFormChange = vi.fn()
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false },
- ]
- render( )
-
- const input = screen.getByPlaceholderText('Name')
- fireEvent.change(input, { target: { value: 'test value' } })
-
- expect(onFormChange).toHaveBeenCalledWith(expect.objectContaining({ name: 'test value' }))
- })
-
- it('should call onFormChange when number input changes', () => {
- const onFormChange = vi.fn()
- const forms = [
- { type: InputVarType.number, label: 'Count', variable: 'count', required: false },
- ]
- render( )
-
- const input = screen.getByPlaceholderText('Count')
- fireEvent.change(input, { target: { value: '42' } })
-
- expect(onFormChange).toHaveBeenCalledWith(expect.objectContaining({ count: '42' }))
- })
-
- it('should call onFormChange when textarea changes', () => {
- const onFormChange = vi.fn()
- const forms = [
- { type: InputVarType.paragraph, label: 'Description', variable: 'desc', required: false },
- ]
- render( )
-
- const textarea = screen.getByPlaceholderText('Description')
- fireEvent.change(textarea, { target: { value: 'long text' } })
-
- expect(onFormChange).toHaveBeenCalledWith(expect.objectContaining({ desc: 'long text' }))
- })
-
- it('should call onFormChange when file is uploaded', () => {
- const onFormChange = vi.fn()
- const forms = [
- {
- type: InputVarType.singleFile,
- label: 'Upload',
- variable: 'file',
- required: false,
- allowed_file_types: ['image'],
- allowed_file_extensions: ['.png'],
- allowed_file_upload_methods: ['local_file'],
- },
- ]
- render( )
-
- fireEvent.click(screen.getByTestId('upload-file-btn'))
- expect(onFormChange).toHaveBeenCalled()
- })
-
- it('should call onFormChange when select option is clicked', () => {
- const onFormChange = vi.fn()
- const forms = [
- { type: InputVarType.select, label: 'Color', variable: 'color', options: ['red', 'blue'], required: false },
- ]
- render( )
-
- // Click on select option
- fireEvent.click(screen.getByTestId('select-option-red'))
- expect(onFormChange).toHaveBeenCalledWith(expect.objectContaining({ color: 'red' }))
- })
-
- it('should call onFormChange when multiple files are uploaded', () => {
- const onFormChange = vi.fn()
- const forms = [
- {
- type: InputVarType.multiFiles,
- label: 'Files',
- variable: 'files',
- required: false,
- max_length: 5,
- allowed_file_types: ['image'],
- allowed_file_extensions: ['.png'],
- allowed_file_upload_methods: ['local_file'],
- },
- ]
- render( )
-
- fireEvent.click(screen.getByTestId('upload-multi-files-btn'))
- expect(onFormChange).toHaveBeenCalledWith(expect.objectContaining({
- files: [{ id: 'file-1' }, { id: 'file-2' }],
- }))
- })
- })
-
- describe('Callback Stability', () => {
- it('should preserve reference to handleFormChange with useCallback', () => {
- const onFormChange = vi.fn()
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false },
- ]
-
- const { rerender } = render(
- ,
- )
-
- // Change inputs without changing onFormChange
- rerender(
- ,
- )
-
- const input = screen.getByPlaceholderText('Name')
- fireEvent.change(input, { target: { value: 'updated' } })
-
- expect(onFormChange).toHaveBeenCalled()
- })
- })
-
- describe('Edge Cases', () => {
- it('should handle inputs with existing values', () => {
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false },
- ]
- render( )
-
- const input = screen.getByPlaceholderText('Name')
- expect(input)!.toHaveValue('existing')
- })
-
- it('should handle empty string value', () => {
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false },
- ]
- render( )
-
- const input = screen.getByPlaceholderText('Name')
- expect(input)!.toHaveValue('')
- })
-
- it('should handle undefined variable value', () => {
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false },
- ]
- render( )
-
- const input = screen.getByPlaceholderText('Name')
- expect(input)!.toHaveValue('')
- })
-
- it('should handle multiple form fields', () => {
- const forms = [
- { type: InputVarType.textInput, label: 'Name', variable: 'name', required: false },
- { type: InputVarType.number, label: 'Age', variable: 'age', required: false },
- { type: InputVarType.paragraph, label: 'Bio', variable: 'bio', required: false },
- ]
- render( )
-
- expect(screen.getByText('Name'))!.toBeInTheDocument()
- expect(screen.getByText('Age'))!.toBeInTheDocument()
- expect(screen.getByText('Bio'))!.toBeInTheDocument()
- })
-
- it('should handle unknown form type gracefully', () => {
- const forms = [
- { type: 'unknown-type' as InputVarType, label: 'Unknown', variable: 'unknown', required: false },
- ]
- // Should not throw error, just not render the field
- render( )
- expect(screen.getByText('Unknown'))!.toBeInTheDocument()
- })
- })
-})
-
-// ==================== AppInputsPanel Tests ====================
-
-describe('AppInputsPanel', () => {
- const defaultProps = {
- value: { app_id: 'app-1', inputs: {} },
- appDetail: createMockApp({ mode: AppModeEnum.CHAT }),
- onFormChange: vi.fn(),
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- mockAppDetailData = undefined
- mockAppDetailLoading = false
- mockWorkflowData = undefined
- mockWorkflowLoading = false
- })
-
- describe('Rendering', () => {
- it('should render without crashing', () => {
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should show no params message when form schema is empty', () => {
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.noParams'))!.toBeInTheDocument()
- })
-
- it('should show loading state when app is loading', () => {
- mockAppDetailLoading = true
- renderWithQueryClient( )
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- // Loading component should be rendered
- expect(screen.queryByText('app.appSelector.params')).not.toBeInTheDocument()
- })
-
- it('should show loading state when workflow is loading', () => {
- mockWorkflowLoading = true
- const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW })
- renderWithQueryClient( )
- expect(screen.queryByText('app.appSelector.params')).not.toBeInTheDocument()
- })
- })
-
- describe('Props', () => {
- it('should handle undefined value', () => {
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should handle different app modes', () => {
- const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should handle advanced chat mode', () => {
- const advancedChatApp = createMockApp({ mode: AppModeEnum.ADVANCED_CHAT })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
- })
-
- describe('Form Schema Generation - Basic App', () => {
- it('should generate schema for paragraph input', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { paragraph: { label: 'Description', variable: 'desc' } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for number input', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { number: { label: 'Count', variable: 'count' } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for checkbox input', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { checkbox: { label: 'Enabled', variable: 'enabled' } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for select input', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { select: { label: 'Option', variable: 'option', options: ['a', 'b'] } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for file-list input', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { 'file-list': { label: 'Files', variable: 'files' } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for file input', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { file: { label: 'File', variable: 'file' } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for json_object input', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { json_object: { label: 'JSON', variable: 'json' } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for text-input (default)', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { 'text-input': { label: 'Name', variable: 'name' } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should filter external_data_tool items', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { 'text-input': { label: 'Name', variable: 'name' }, 'external_data_tool': true },
- { 'text-input': { label: 'Email', variable: 'email' } },
- ],
- },
- })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
- })
-
- describe('Form Schema Generation - Workflow App', () => {
- it('should generate schema for workflow with multiFiles variable', () => {
- mockWorkflowData = {
- graph: {
- nodes: [
- {
- data: {
- type: 'start',
- variables: [
- { type: 'file-list', label: 'Files', variable: 'files' },
- ],
- },
- },
- ],
- },
- features: {},
- }
- const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for workflow with singleFile variable', () => {
- mockWorkflowData = {
- graph: {
- nodes: [
- {
- data: {
- type: 'start',
- variables: [
- { type: 'file', label: 'File', variable: 'file' },
- ],
- },
- },
- ],
- },
- features: {},
- }
- const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should generate schema for workflow with regular variable', () => {
- mockWorkflowData = {
- graph: {
- nodes: [
- {
- data: {
- type: 'start',
- variables: [
- { type: 'text-input', label: 'Name', variable: 'name' },
- ],
- },
- },
- ],
- },
- features: {},
- }
- const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
- })
-
- describe('Image Upload Schema', () => {
- it('should add image upload schema for COMPLETION mode with file upload enabled', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.COMPLETION,
- model_config: {
- ...createMockApp().model_config,
- file_upload: {
- enabled: true,
- image: { enabled: true },
- },
- user_input_form: [],
- },
- })
- const completionApp = createMockApp({ mode: AppModeEnum.COMPLETION })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should add image upload schema for WORKFLOW mode with file upload enabled', () => {
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.WORKFLOW,
- model_config: {
- ...createMockApp().model_config,
- file_upload: {
- enabled: true,
- },
- user_input_form: [],
- },
- })
- mockWorkflowData = {
- graph: { nodes: [{ data: { type: 'start', variables: [] } }] },
- features: { file_upload: { enabled: true } },
- }
- const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
- })
-
- describe('User Interactions', () => {
- it('should call onFormChange when form is updated', () => {
- const onFormChange = vi.fn()
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
-
- it('should call onFormChange with updated values when text input changes', () => {
- const onFormChange = vi.fn()
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { 'text-input': { label: 'TestField', variable: 'testField', default: '', required: false, max_length: 100 } },
- ],
- },
- })
- renderWithQueryClient( )
-
- // Find and change the text input
- const input = screen.getByPlaceholderText('TestField')
- fireEvent.change(input, { target: { value: 'new value' } })
-
- // handleFormChange should be called with the new value
- expect(onFormChange).toHaveBeenCalledWith({ testField: 'new value' })
- })
-
- it('should update inputsRef when form changes', () => {
- const onFormChange = vi.fn()
- mockAppDetailData = createMockApp({
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { 'text-input': { label: 'RefTestField', variable: 'refField', default: '', required: false, max_length: 50 } },
- ],
- },
- })
- renderWithQueryClient( )
-
- const input = screen.getByPlaceholderText('RefTestField')
- fireEvent.change(input, { target: { value: 'ref updated' } })
-
- expect(onFormChange).toHaveBeenCalledWith({ refField: 'ref updated' })
- })
- })
-
- describe('Memoization', () => {
- it('should memoize basicAppFileConfig correctly', () => {
- const { rerender } = renderWithQueryClient( )
- rerender(
-
-
- ,
- )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
- })
-
- describe('Edge Cases', () => {
- it('should return empty schema when currentApp is null', () => {
- mockAppDetailData = null
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.noParams'))!.toBeInTheDocument()
- })
-
- it('should handle workflow without start node', () => {
- mockWorkflowData = {
- graph: { nodes: [] },
- features: {},
- }
- const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW })
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.params'))!.toBeInTheDocument()
- })
- })
-})
-
-// ==================== AppSelector (Main Component) Tests ====================
-
describe('AppSelector', () => {
- const defaultProps = {
- onSelect: vi.fn(),
- }
-
beforeEach(() => {
vi.clearAllMocks()
- vi.useFakeTimers()
- mockAppListData = {
- pages: [{ data: createMockApps(5), has_more: false, page: 1 }],
- }
- mockIsLoading = false
- mockIsFetchingNextPage = false
- mockHasNextPage = false
- mockFetchNextPage.mockResolvedValue(undefined)
- mockAppDetailData = undefined
- mockAppDetailLoading = false
- mockWorkflowData = undefined
- mockWorkflowLoading = false
})
- afterEach(() => {
- vi.useRealTimers()
- })
+ it('should keep the main interaction: outer panel, inner app list, then inputs panel', async () => {
+ const onSelect = vi.fn()
- describe('Rendering', () => {
- it('should render without crashing', () => {
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ renderWithQueryClient( )
+
+ fireEvent.click(screen.getByRole('button', { name: 'app.appSelector.label' }))
+ expect(screen.getByText('app.appSelector.label')).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('combobox', { name: 'app.appSelector.label' }))
+
+ await waitFor(() => {
+ expect(screen.getByText('Support Bot')).toBeInTheDocument()
})
- it('should render trigger component', () => {
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument()
+ fireEvent.click(screen.getByText('Support Bot'))
+
+ expect(onSelect).toHaveBeenCalledWith({
+ app_id: 'app-1',
+ inputs: {},
+ files: [],
})
-
- it('should configure paged app list query options', () => {
- renderWithQueryClient( )
-
- const options = mockAppListInfiniteOptions.mock.calls.at(-1)?.[0] as AppSelectorInfiniteOptions
-
- expect(options.input(4)).toEqual({
- query: {
- page: 4,
- limit: 20,
- name: '',
- },
- })
- expect(options.getNextPageParam({ has_more: true, page: 4 })).toBe(5)
- expect(options.getNextPageParam({ has_more: false, page: 4 })).toBeUndefined()
- })
-
- it('should show selected app info when value is provided', () => {
- renderWithQueryClient(
- ,
- )
- // Should show the app trigger with app info
- // Should show the app trigger with app info
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByText('app.appSelector.label')).toBeInTheDocument()
+ expect(screen.getByRole('combobox', { name: 'app.appSelector.label' })).toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.queryByText('Workflow App')).not.toBeInTheDocument()
})
})
- describe('Props', () => {
- it('should handle different placement values', () => {
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ it('should search apps from the content input', async () => {
+ renderWithQueryClient( )
+
+ fireEvent.click(screen.getByRole('button', { name: 'app.appSelector.label' }))
+ fireEvent.click(screen.getByRole('combobox', { name: 'app.appSelector.label' }))
+ fireEvent.change(screen.getByRole('combobox', { name: 'app.appSelector.placeholder' }), {
+ target: { value: 'workflow' },
})
- it('should handle different offset values', () => {
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle disabled state', () => {
- renderWithQueryClient( )
- const trigger = screen.getByTestId('portal-trigger')
- fireEvent.click(trigger)
- // Portal should remain closed when disabled
- // Portal should remain closed when disabled
- expect(screen.getByTestId('portal-to-follow-elem'))!.toHaveAttribute('data-open', 'false')
- })
-
- it('should handle scope prop', () => {
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle value with inputs', () => {
- renderWithQueryClient(
- ,
- )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle value with files', () => {
- renderWithQueryClient(
- ,
- )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.getByText('Workflow App')).toBeInTheDocument()
})
+ expect(screen.queryByText('Support Bot')).not.toBeInTheDocument()
})
- describe('State Management', () => {
- it('should toggle isShow state when trigger is clicked', () => {
- renderWithQueryClient( )
+ it('should not keep the selected app in filtered results', async () => {
+ const user = userEvent.setup()
+ const onSelect = vi.fn()
- const trigger = screen.getAllByTestId('portal-trigger')[0]
- fireEvent.click(trigger!)
+ renderWithQueryClient( )
- // The portal state should update synchronously - get the first one (outer portal)
- // The portal state should update synchronously - get the first one (outer portal)
- expect(screen.getAllByTestId('portal-to-follow-elem')[0])!.toHaveAttribute('data-open', 'true')
+ await user.click(screen.getByRole('button', { name: 'app.appSelector.label' }))
+ await user.click(screen.getByRole('combobox', { name: 'app.appSelector.label' }))
+
+ await waitFor(() => {
+ expect(screen.getByText('Support Bot')).toBeInTheDocument()
})
- it('should not toggle isShow when disabled', () => {
- renderWithQueryClient( )
+ await user.click(screen.getByText('Support Bot'))
+ await user.click(screen.getByRole('combobox', { name: 'app.appSelector.label' }))
+ await user.type(screen.getByRole('combobox', { name: 'app.appSelector.placeholder' }), 'workflow')
- const trigger = screen.getByTestId('portal-trigger')
- fireEvent.click(trigger)
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toHaveAttribute('data-open', 'false')
+ await waitFor(() => {
+ expect(screen.queryByRole('option', { name: /Support Bot/ })).not.toBeInTheDocument()
})
+ expect(screen.getByRole('option', { name: /Workflow App/ })).toBeInTheDocument()
- it('should manage search text state', () => {
- renderWithQueryClient( )
+ await user.keyboard('{ArrowDown}')
+ await user.keyboard('{Enter}')
- const trigger = screen.getByTestId('portal-trigger')
- fireEvent.click(trigger)
-
- // Portal content should be visible after click
- // Portal content should be visible after click
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
-
- it('should render correctly during load more setup', () => {
- mockHasNextPage = true
- mockFetchNextPage.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)))
-
- renderWithQueryClient( )
-
- // Trigger should be rendered
- // Trigger should be rendered
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
- })
- })
-
- describe('Callbacks', () => {
- it('should call onSelect when app is selected', () => {
- const onSelect = vi.fn()
-
- renderWithQueryClient( )
-
- // Open the portal
- fireEvent.click(screen.getByTestId('portal-trigger'))
-
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
-
- it('should call onSelect with correct value structure', () => {
- const onSelect = vi.fn()
- renderWithQueryClient(
- ,
- )
-
- // The component should maintain the correct value structure
- // The component should maintain the correct value structure
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should clear inputs when selecting different app', () => {
- const onSelect = vi.fn()
- renderWithQueryClient(
- ,
- )
-
- // Component renders with existing value
- // Component renders with existing value
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should preserve inputs when selecting same app', () => {
- const onSelect = vi.fn()
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
- })
-
- describe('Memoization', () => {
- it('should memoize displayedApps correctly', () => {
- mockAppListData = {
- pages: [
- { data: createMockApps(3), has_more: true, page: 1 },
- { data: createMockApps(3), has_more: false, page: 2 },
- ],
- }
-
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should memoize currentAppInfo correctly', () => {
- mockAppListData = {
- pages: [{ data: createMockApps(5), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should memoize formattedValue correctly', () => {
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should be wrapped with React.memo', () => {
- // Verify the component is defined and memoized
- expect(AppSelector).toBeDefined()
-
- const onSelect = vi.fn()
- const { rerender } = renderWithQueryClient( )
-
- // Re-render with same props should not cause unnecessary updates
- rerender(
-
-
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
- })
-
- describe('Load More Functionality', () => {
- it('should handle load more when hasMore is true', async () => {
- mockHasNextPage = true
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should not trigger load more when already loading', async () => {
- mockIsFetchingNextPage = true
- mockHasNextPage = true
- renderWithQueryClient( )
- expect(mockFetchNextPage).not.toHaveBeenCalled()
- })
-
- it('should not trigger load more when no more data', () => {
- mockHasNextPage = false
- renderWithQueryClient( )
- expect(mockFetchNextPage).not.toHaveBeenCalled()
- })
-
- it('should handle fetchNextPage completion with delay', async () => {
- mockHasNextPage = true
- mockFetchNextPage.mockResolvedValue(undefined)
-
- renderWithQueryClient( )
-
- act(() => {
- vi.advanceTimersByTime(500)
- })
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should render load more area when hasMore is true', () => {
- mockHasNextPage = true
- mockIsFetchingNextPage = false
- mockFetchNextPage.mockResolvedValue(undefined)
-
- renderWithQueryClient( )
-
- // Open the portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Should render without errors
- // Should render without errors
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
-
- it('should handle fetchNextPage rejection gracefully in handleLoadMore', async () => {
- mockHasNextPage = true
- mockFetchNextPage.mockRejectedValue(new Error('Network error'))
-
- renderWithQueryClient( )
-
- // Should not crash even if fetchNextPage rejects
- // Should not crash even if fetchNextPage rejects
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should call fetchNextPage when intersection observer triggers handleLoadMore', async () => {
- mockHasNextPage = true
- mockIsFetchingNextPage = false
- mockFetchNextPage.mockResolvedValue(undefined)
-
- renderWithQueryClient( )
-
- // Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Open the inner app picker portal
- const triggers = screen.getAllByTestId('portal-trigger')
- fireEvent.click(triggers[1]!)
-
- // Simulate intersection to trigger handleLoadMore
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
-
- // fetchNextPage should be called
- expect(mockFetchNextPage).toHaveBeenCalled()
- })
-
- it('should avoid duplicate fetches while the picker debounce is active', async () => {
- mockHasNextPage = true
- mockIsFetchingNextPage = false
- mockFetchNextPage.mockResolvedValue(undefined)
-
- renderWithQueryClient( )
-
- // Open portals
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
- const triggers = screen.getAllByTestId('portal-trigger')
- fireEvent.click(triggers[1]!)
-
- // Trigger first intersection
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
-
- expect(mockFetchNextPage).toHaveBeenCalledTimes(1)
-
- // Try to trigger again immediately - should be blocked by AppPicker loadingRef
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
-
- // Still only one call due to the picker-level debounce
- expect(mockFetchNextPage).toHaveBeenCalledTimes(1)
-
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
- })
-
- it('should skip handleLoadMore when isFetchingNextPage is true', async () => {
- mockHasNextPage = true
- mockIsFetchingNextPage = true // This will block the handleLoadMore
- mockFetchNextPage.mockResolvedValue(undefined)
-
- renderWithQueryClient( )
-
- // Open portals
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
- const triggers = screen.getAllByTestId('portal-trigger')
- fireEvent.click(triggers[1]!)
-
- // Trigger intersection
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
-
- // fetchNextPage should NOT be called because isFetchingNextPage is true
- expect(mockFetchNextPage).not.toHaveBeenCalled()
- })
-
- it('should skip handleLoadMore when hasMore is false', async () => {
- mockHasNextPage = false // This will block the handleLoadMore
- mockIsFetchingNextPage = false
- mockFetchNextPage.mockResolvedValue(undefined)
-
- renderWithQueryClient( )
-
- // Open portals
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
- const triggers = screen.getAllByTestId('portal-trigger')
- fireEvent.click(triggers[1]!)
-
- // Trigger intersection
- triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
-
- // fetchNextPage should NOT be called because hasMore is false
- expect(mockFetchNextPage).not.toHaveBeenCalled()
- })
- })
- describe('Form Change Handling', () => {
- it('should handle form change with image file', () => {
- const onSelect = vi.fn()
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle form change without image file', () => {
- const onSelect = vi.fn()
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should extract #image# from inputs and add to files array', () => {
- const onSelect = vi.fn()
- // The handleFormChange function should extract #image# and add to files
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should preserve existing files when no #image# in inputs', () => {
- const onSelect = vi.fn()
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
- })
-
- describe('App Selection', () => {
- it('should clear inputs when selecting a different app', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- // Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
-
- it('should preserve inputs when selecting the same app', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle app selection with empty value', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- // Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
- })
-
- describe('Edge Cases', () => {
- it('should handle undefined value', () => {
- renderWithQueryClient( )
- expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument()
- })
-
- it('should handle empty pages array', () => {
- mockAppListData = { pages: [] }
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle undefined data', () => {
- mockAppListData = undefined
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle loading state', () => {
- mockIsLoading = true
- renderWithQueryClient( )
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle app not found in displayedApps', () => {
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle value with empty inputs and files', () => {
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
- })
-
- describe('Error Handling', () => {
- it('should handle fetchNextPage rejection gracefully', async () => {
- mockHasNextPage = true
- mockFetchNextPage.mockRejectedValue(new Error('Network error'))
-
- renderWithQueryClient( )
-
- // Should not crash
- // Should not crash
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
- })
-})
-
-// ==================== Integration Tests ====================
-
-describe('AppSelector Integration', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- vi.useFakeTimers()
- mockAppListData = {
- pages: [{ data: createMockApps(5), has_more: false, page: 1 }],
- }
- mockIsLoading = false
- mockIsFetchingNextPage = false
- mockHasNextPage = false
- mockAppDetailData = undefined
- mockAppDetailLoading = false
- mockWorkflowData = undefined
- mockWorkflowLoading = false
- })
-
- afterEach(() => {
- vi.useRealTimers()
- })
-
- describe('Full User Flow', () => {
- it('should complete full app selection flow', () => {
- const onSelect = vi.fn()
-
- renderWithQueryClient( )
-
- // 1. Click trigger to open picker - get first trigger (outer portal)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Get the first portal element (outer portal)
- // Get the first portal element (outer portal)
- expect(screen.getAllByTestId('portal-to-follow-elem')[0])!.toHaveAttribute('data-open', 'true')
- })
-
- it('should handle app change with input preservation logic', () => {
- const onSelect = vi.fn()
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
- })
-
- describe('Component Communication', () => {
- it('should pass correct props to AppTrigger', () => {
- renderWithQueryClient( )
-
- // AppTrigger should show placeholder when no app selected
- // AppTrigger should show placeholder when no app selected
- expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument()
- })
-
- it('should pass correct props to AppPicker', () => {
- renderWithQueryClient( )
-
- fireEvent.click(screen.getByTestId('portal-trigger'))
-
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
- })
-
- describe('Data Flow', () => {
- it('should properly format value with files for AppInputsPanel', () => {
- renderWithQueryClient(
- ,
- )
-
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
- })
-
- it('should handle search filtering through app list', () => {
- renderWithQueryClient( )
-
- fireEvent.click(screen.getByTestId('portal-trigger'))
-
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
- })
-
- describe('handleSelectApp Callback', () => {
- it('should call onSelect with new app when selecting different app', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- // Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // The inner AppPicker portal is closed by default (isShowChooseApp = false)
- // We need to click on the inner trigger to open it
- const innerTriggers = screen.getAllByTestId('portal-trigger')
- // The second trigger is the inner AppPicker trigger
- fireEvent.click(innerTriggers[1]!)
-
- // Now the inner portal should be open and show the app list
- // Find and click on app-2
- const app2 = screen.getByText('App 2')
- fireEvent.click(app2)
-
- // onSelect should be called with cleared inputs since it's a different app
- expect(onSelect).toHaveBeenCalledWith({
+ await waitFor(() => {
+ expect(onSelect).toHaveBeenLastCalledWith({
app_id: 'app-2',
inputs: {},
files: [],
})
})
-
- it('should preserve inputs when selecting same app', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- // Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Click on the inner trigger to open app picker
- const innerTriggers = screen.getAllByTestId('portal-trigger')
- fireEvent.click(innerTriggers[1]!)
-
- // Click on the same app - need to get the one in the app list, not the trigger
- const appItems = screen.getAllByText('App 1')
- // The last one should be in the dropdown list
- fireEvent.click(appItems[appItems.length - 1]!)
-
- // onSelect should be called with preserved inputs since it's the same app
- expect(onSelect).toHaveBeenCalledWith({
- app_id: 'app-1',
- inputs: { existing: 'value' },
- files: [{ id: 'existing-file' }],
- })
- })
-
- it('should handle app selection when value is undefined', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- // Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Click on inner trigger to open app picker
- const innerTriggers = screen.getAllByTestId('portal-trigger')
- fireEvent.click(innerTriggers[1]!)
-
- // Click on an app from the dropdown
- const app1Elements = screen.getAllByText('App 1')
- fireEvent.click(app1Elements[app1Elements.length - 1]!)
-
- // onSelect should be called with new app and empty inputs/files
- expect(onSelect).toHaveBeenCalledWith({
- app_id: 'app-1',
- inputs: {},
- files: [],
- })
- })
- })
-
- describe('handleLoadMore Callback', () => {
- it('should handle load more by calling fetchNextPage', async () => {
- mockHasNextPage = true
- mockIsFetchingNextPage = false
- mockFetchNextPage.mockResolvedValue(undefined)
-
- renderWithQueryClient( )
-
- // Open the portal to render the app picker
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
-
- it('should stay stable after fetchNextPage completes', async () => {
- mockHasNextPage = true
- mockIsFetchingNextPage = false
- mockFetchNextPage.mockResolvedValue(undefined)
-
- renderWithQueryClient( )
-
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
- })
-
- it('should not call fetchNextPage when conditions prevent it', () => {
- mockHasNextPage = false
- mockIsFetchingNextPage = true
-
- renderWithQueryClient( )
-
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // fetchNextPage should not be called
- expect(mockFetchNextPage).not.toHaveBeenCalled()
- })
- })
-
- describe('handleFormChange Callback', () => {
- it('should format value correctly with files for display', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- // Open portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // formattedValue should include #image# from files
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
- })
-
- it('should handle value with no files', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
- })
-
- it('should handle undefined value.files', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
-
- renderWithQueryClient(
- ,
- )
-
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
- })
-
- it('should call onSelect with transformed inputs when form input changes', () => {
- const onSelect = vi.fn()
- // Include app-1 in the list so currentAppInfo is found
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
- // Setup mock app detail with form fields - ensure complete form config
- mockAppDetailData = createMockApp({
- id: 'app-1',
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { 'text-input': { label: 'FormInputField', variable: 'formVar', default: '', required: false, max_length: 100 } },
- ],
- },
- })
-
- renderWithQueryClient(
- ,
- )
-
- // Open portal to render AppInputsPanel
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Find and interact with the form input (may not exist if schema is empty)
- const formInputs = screen.queryAllByPlaceholderText('FormInputField')
- if (formInputs.length > 0) {
- fireEvent.change(formInputs[0]!, { target: { value: 'test value' } })
-
- // handleFormChange in index.tsx should have been called
- expect(onSelect).toHaveBeenCalledWith({
- app_id: 'app-1',
- inputs: { formVar: 'test value' },
- files: [],
- })
- }
- else {
- // If form inputs aren't rendered, at least verify component rendered
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
- }
- })
-
- it('should extract #image# field from inputs and add to files array', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
- // Setup COMPLETION mode app with file upload enabled for #image# field
- // The #image# schema is added when basicAppFileConfig.enabled is true
- mockAppDetailData = createMockApp({
- id: 'app-1',
- mode: AppModeEnum.COMPLETION,
- model_config: {
- ...createMockApp().model_config,
- file_upload: {
- enabled: true,
- image: {
- enabled: true,
- number_limits: 1,
- detail: 'high',
- transfer_methods: ['local_file'],
- },
- },
- user_input_form: [],
- },
- })
-
- renderWithQueryClient(
- ,
- )
-
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Find file uploader and trigger upload - the #image# field will be extracted
- const uploadBtns = screen.queryAllByTestId('upload-file-btn')
- if (uploadBtns.length > 0) {
- fireEvent.click(uploadBtns[0]!)
- // handleFormChange should extract #image# and convert to files
- expect(onSelect).toHaveBeenCalled()
- }
- else {
- // Verify component rendered
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
- }
- })
-
- it('should preserve existing files when inputs do not contain #image#', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
- mockAppDetailData = createMockApp({
- id: 'app-1',
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { 'text-input': { label: 'PreserveField', variable: 'name', default: '', required: false, max_length: 50 } },
- ],
- },
- })
-
- renderWithQueryClient(
- ,
- )
-
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Find form input (may not exist if schema is empty)
- const inputs = screen.queryAllByPlaceholderText('PreserveField')
- if (inputs.length > 0) {
- fireEvent.change(inputs[0]!, { target: { value: 'updated name' } })
-
- // onSelect should be called preserving existing files (no #image# in inputs)
- expect(onSelect).toHaveBeenCalledWith({
- app_id: 'app-1',
- inputs: { name: 'updated name' },
- files: [{ id: 'preserved-file' }],
- })
- }
- else {
- // If form inputs aren't rendered, at least verify component rendered
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
- }
- })
-
- it('should handle handleFormChange with #image# field and convert to files', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
- // Setup COMPLETION app with file upload - this will add #image# to form schema
- mockAppDetailData = createMockApp({
- id: 'app-1',
- mode: AppModeEnum.COMPLETION,
- model_config: {
- ...createMockApp().model_config,
- file_upload: {
- enabled: true,
- image: {
- enabled: true,
- number_limits: 1,
- detail: 'high',
- transfer_methods: ['local_file'],
- },
- },
- user_input_form: [],
- },
- })
-
- renderWithQueryClient(
- ,
- )
-
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- // Try to find and click the upload button which triggers #image# form change
- const uploadBtn = screen.queryByTestId('upload-file-btn')
- if (uploadBtn) {
- fireEvent.click(uploadBtn)
- // handleFormChange should be called and extract #image# to files
- expect(onSelect).toHaveBeenCalled()
- }
- })
-
- it('should handle handleFormChange without #image# and preserve value files', () => {
- const onSelect = vi.fn()
- mockAppListData = {
- pages: [{ data: createMockApps(3), has_more: false, page: 1 }],
- }
- mockAppDetailData = createMockApp({
- id: 'app-1',
- mode: AppModeEnum.CHAT,
- model_config: {
- ...createMockApp().model_config,
- user_input_form: [
- { 'text-input': { label: 'SimpleInput', variable: 'simple', default: '', required: false, max_length: 100 } },
- ],
- },
- })
-
- renderWithQueryClient(
- ,
- )
-
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
-
- const inputs = screen.queryAllByPlaceholderText('SimpleInput')
- if (inputs.length > 0) {
- fireEvent.change(inputs[0]!, { target: { value: 'changed' } })
- // handleFormChange should preserve existing files when no #image# in inputs
- expect(onSelect).toHaveBeenCalledWith({
- app_id: 'app-1',
- inputs: { simple: 'changed' },
- files: [{ id: 'pre-existing-file' }],
- })
- }
- })
})
})
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx
index cf387b1715..9c1f40af90 100644
--- a/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx
@@ -1,28 +1,32 @@
'use client'
-import type {
- OffsetOptions,
- Placement,
-} from '@floating-ui/react'
-import type { FC } from 'react'
+
+import type { Placement } from '@langgenius/dify-ui/combobox'
+import type { ReactNode } from 'react'
import type { App } from '@/types/app'
+import { Button } from '@langgenius/dify-ui/button'
import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from '@langgenius/dify-ui/popover'
-import * as React from 'react'
-import { useCallback, useEffect, useRef } from 'react'
+ Combobox,
+ ComboboxContent,
+ ComboboxEmpty,
+ ComboboxInput,
+ ComboboxInputGroup,
+ ComboboxItem,
+ ComboboxItemText,
+ ComboboxList,
+ ComboboxStatus,
+ ComboboxTrigger,
+} from '@langgenius/dify-ui/combobox'
+import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
-import Input from '@/app/components/base/input'
import { AppModeEnum } from '@/types/app'
-type Props = {
- scope: string
+type AppPickerProps = {
+ scope?: string
disabled: boolean
- trigger: React.ReactNode
+ trigger: ReactNode
placement?: Placement
- offset?: OffsetOptions
+ offset?: number
isShow: boolean
onShowChange: (isShow: boolean) => void
onSelect: (app: App) => void
@@ -34,8 +38,62 @@ type Props = {
onSearchChange: (text: string) => void
}
-const AppPicker: FC = ({
- scope: _scope,
+function getAppTypeLabel(app: App) {
+ switch (app.mode) {
+ case AppModeEnum.ADVANCED_CHAT:
+ return 'chatflow'
+ case AppModeEnum.AGENT_CHAT:
+ return 'agent'
+ case AppModeEnum.CHAT:
+ return 'chat'
+ case AppModeEnum.COMPLETION:
+ return 'completion'
+ case AppModeEnum.WORKFLOW:
+ return 'workflow'
+ default:
+ return app.mode
+ }
+}
+
+function getAppSearchText(app: App) {
+ return `${app.name} ${app.id} ${getAppTypeLabel(app)}`
+}
+
+function AppPickerOption({
+ app,
+}: {
+ app: App
+}) {
+ return (
+
+
+
+
+ {app.name}
+
+ (
+ {app.id.slice(0, 8)}
+ )
+
+
+
+ {getAppTypeLabel(app)}
+
+ )
+}
+
+export function AppPicker({
disabled,
trigger,
placement = 'right-start',
@@ -49,186 +107,91 @@ const AppPicker: FC = ({
onLoadMore,
searchText,
onSearchChange,
-}) => {
+}: AppPickerProps) {
const { t } = useTranslation()
- const observerTargetRef = useRef(null)
- const observerRef = useRef(null)
- const loadingRef = useRef(false)
- const loadingResetTimerIdRef = useRef(undefined)
- const retimeLoadingReset = useCallback((timerId?: number) => {
- if (loadingResetTimerIdRef.current !== undefined)
- globalThis.clearTimeout(loadingResetTimerIdRef.current)
-
- loadingResetTimerIdRef.current = timerId
- }, [])
-
- const resetLoadingState = useCallback(() => {
- retimeLoadingReset()
- loadingRef.current = false
- }, [retimeLoadingReset])
-
- const disconnectObserver = useCallback(() => {
- if (!observerRef.current)
+ const handleValueChange = useCallback((app: App | null) => {
+ if (!app)
return
- observerRef.current.disconnect()
- observerRef.current = null
- }, [])
-
- const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
- const target = entries[0]
- if (!target!.isIntersecting || loadingRef.current || !hasMore || isLoading)
- return
-
- loadingRef.current = true
- onLoadMore()
- retimeLoadingReset(window.setTimeout(() => {
- loadingRef.current = false
- retimeLoadingReset()
- }, 500))
- }, [hasMore, isLoading, onLoadMore, retimeLoadingReset])
-
- useEffect(() => {
- if (!isShow) {
- resetLoadingState()
- disconnectObserver()
- return
- }
-
- let mutationObserver: MutationObserver | null = null
-
- const setupIntersectionObserver = () => {
- if (!observerTargetRef.current)
- return
-
- disconnectObserver()
-
- // Create new observer
- observerRef.current = new IntersectionObserver(handleIntersection, {
- root: null,
- rootMargin: '100px',
- threshold: 0.1,
- })
-
- observerRef.current.observe(observerTargetRef.current)
- }
-
- // Set up MutationObserver to watch DOM changes
- mutationObserver = new MutationObserver((_mutations) => {
- if (observerTargetRef.current) {
- setupIntersectionObserver()
- mutationObserver?.disconnect()
- }
- })
-
- // Watch body changes since Portal adds content to body
- mutationObserver.observe(document.body, {
- childList: true,
- subtree: true,
- })
-
- // If element exists, set up IntersectionObserver directly
- if (observerTargetRef.current)
- setupIntersectionObserver()
-
- return () => {
- resetLoadingState()
- disconnectObserver()
- mutationObserver?.disconnect()
- }
- }, [disconnectObserver, handleIntersection, isShow, resetLoadingState])
-
- const getAppType = (app: App) => {
- switch (app.mode) {
- case AppModeEnum.ADVANCED_CHAT:
- return 'chatflow'
- case AppModeEnum.AGENT_CHAT:
- return 'agent'
- case AppModeEnum.CHAT:
- return 'chat'
- case AppModeEnum.COMPLETION:
- return 'completion'
- case AppModeEnum.WORKFLOW:
- return 'workflow'
- }
- }
-
- const resolvedOffset = typeof offset === 'number' || typeof offset === 'function' ? undefined : offset
- const sideOffset = typeof offset === 'number' ? offset : resolvedOffset?.mainAxis ?? 0
- const alignOffset = typeof offset === 'number' ? 0 : resolvedOffset?.crossAxis ?? resolvedOffset?.alignmentAxis ?? 0
- const handleTriggerClick = useCallback((event: React.MouseEvent) => {
- event.preventDefault()
- if (disabled || isShow)
- return
-
- onShowChange(true)
- }, [disabled, isShow, onShowChange])
+ onSelect(app)
+ onShowChange(false)
+ }, [onSelect, onShowChange])
return (
-
+ items={apps}
open={isShow}
+ inputValue={searchText}
onOpenChange={onShowChange}
+ onInputValueChange={onSearchChange}
+ onValueChange={handleValueChange}
+ itemToStringLabel={app => app?.name ?? ''}
+ itemToStringValue={app => app?.id ?? ''}
+ filter={(app, query) => getAppSearchText(app).toLowerCase().includes(query.toLowerCase())}
+ disabled={disabled}
>
- {trigger} }
- onClick={handleTriggerClick}
- />
-
-
+ {trigger}
+
+
-
+
- onSearchChange(e.target.value)}
- onClear={() => onSearchChange('')}
- />
+
+
+
+ {searchText && (
+ onSearchChange('')}
+ >
+
+
+ )}
+
- {apps.map(app => (
-
onSelect(app)}
- >
-
-
- {app.name}
-
- (
- {app.id.slice(0, 8)}
- )
-
-
-
{getAppType(app)}
-
- ))}
-
- {isLoading && (
-
-
{t('loading', { ns: 'common' })}
-
+ {isLoading && (
+
+ {t('loading', { ns: 'common' })}
+
+ )}
+
+ {(app: App) => (
+
)}
-
+
+
+ {t('noData', { ns: 'common' })}
+
+ {hasMore && (
+
+ onLoadMore()}
+ >
+ {isLoading ? t('loading', { ns: 'common' }) : t('common.loadMore', { ns: 'workflow' })}
+
+
+ )}
-
-
+
+
)
}
-
-export default React.memo(AppPicker)
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/app-trigger.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/app-trigger.tsx
index aacafb3c31..d6f2c15bcf 100644
--- a/web/app/components/plugins/plugin-detail-panel/app-selector/app-trigger.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/app-selector/app-trigger.tsx
@@ -1,33 +1,32 @@
'use client'
+
import type { App } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn'
-import {
- RiArrowDownSLine,
-} from '@remixicon/react'
-import * as React from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
-type Props = {
+type AppTriggerProps = {
open: boolean
appDetail?: App
}
-const AppTrigger = ({
+export function AppTrigger({
open,
appDetail,
-}: Props) => {
+}: AppTriggerProps) {
const { t } = useTranslation()
+
return (
-
{appDetail && (
)}
- {appDetail && (
-
{appDetail.name}
- )}
- {!appDetail && (
-
{t('appSelector.placeholder', { ns: 'app' })}
- )}
-
-
+ {appDetail
+ ? (
+
+ {appDetail.name}
+
+ )
+ : (
+
+ {t('appSelector.placeholder', { ns: 'app' })}
+
+ )}
+
+
)
}
-
-export default AppTrigger
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx
index 5068143a8d..8d3094a2a0 100644
--- a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx
@@ -1,10 +1,6 @@
'use client'
-import type {
- OffsetOptions,
- Placement,
-} from '@floating-ui/react'
-import type { FC } from 'react'
-import type { AppListQuery } from '@/contract/console/apps'
+
+import type { Placement } from '@langgenius/dify-ui/popover'
import type { App } from '@/types/app'
import {
Popover,
@@ -12,48 +8,44 @@ import {
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'
-import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppInputsPanel from '@/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel'
-import AppPicker from '@/app/components/plugins/plugin-detail-panel/app-selector/app-picker'
-import AppTrigger from '@/app/components/plugins/plugin-detail-panel/app-selector/app-trigger'
+import { AppPicker } from '@/app/components/plugins/plugin-detail-panel/app-selector/app-picker'
+import { AppTrigger } from '@/app/components/plugins/plugin-detail-panel/app-selector/app-trigger'
import { consoleQuery } from '@/service/client'
import { useAppDetail } from '@/service/use-apps'
const PAGE_SIZE = 20
-type Props = {
- value?: {
- app_id: string
- inputs: Record
- files?: unknown[]
- }
+export type AppSelectorValue = {
+ app_id: string
+ inputs: Record
+ files?: unknown[]
+}
+
+type AppSelectorProps = {
+ value?: AppSelectorValue
scope?: string
disabled?: boolean
placement?: Placement
- offset?: OffsetOptions
- onSelect: (app: {
- app_id: string
- inputs: Record
- files?: unknown[]
- }) => void
- supportAddCustomTool?: boolean
+ offset?: number
+ onSelect: (app: AppSelectorValue) => void
}
-const AppSelector: FC = ({
+export function AppSelector({
value,
- scope,
disabled,
placement = 'bottom',
offset = 4,
onSelect,
-}) => {
+}: AppSelectorProps) {
const { t } = useTranslation()
const [isShow, setIsShow] = useState(false)
+ const [isShowChooseApp, setIsShowChooseApp] = useState(false)
const [searchText, setSearchText] = useState('')
- const appListQuery = useMemo(() => ({
+ const appListQuery = useMemo(() => ({
page: 1,
limit: PAGE_SIZE,
name: searchText,
@@ -80,150 +72,105 @@ const AppSelector: FC = ({
})
const displayedApps = useMemo(() => {
- const pages = data?.pages ?? []
- if (!pages.length)
- return []
- return pages.flatMap(({ data: apps }) => apps)
+ return data?.pages.flatMap(({ data: apps }) => apps) ?? []
}, [data?.pages])
- // fetch selected app by id to avoid pagination gaps
const { data: selectedAppDetail } = useAppDetail(value?.app_id || '')
- // Ensure the currently selected app is available for display and in the picker options
const currentAppInfo = useMemo(() => {
if (!value?.app_id)
return undefined
+
return selectedAppDetail || displayedApps.find(app => app.id === value.app_id)
- }, [value?.app_id, selectedAppDetail, displayedApps])
-
- const appsForPicker = useMemo(() => {
- if (!currentAppInfo)
- return displayedApps
-
- const appIndex = displayedApps.findIndex(a => a.id === currentAppInfo.id)
-
- if (appIndex === -1)
- return [currentAppInfo, ...displayedApps]
-
- const updatedApps = [...displayedApps]
- updatedApps[appIndex] = currentAppInfo
- return updatedApps
- }, [currentAppInfo, displayedApps])
+ }, [displayedApps, selectedAppDetail, value?.app_id])
const hasMore = hasNextPage ?? true
- const resolvedOffset = typeof offset === 'number' || typeof offset === 'function' ? undefined : offset
- const sideOffset = typeof offset === 'number' ? offset : resolvedOffset?.mainAxis ?? 0
- const alignOffset = typeof offset === 'number' ? 0 : resolvedOffset?.crossAxis ?? resolvedOffset?.alignmentAxis ?? 0
- const handleLoadMore = useCallback(async () => {
- if (isFetchingNextPage || !hasMore)
- return
+ const handleSelectApp = useCallback((app: App) => {
+ const shouldClearValue = app.id !== value?.app_id
- await fetchNextPage()
- }, [fetchNextPage, hasMore, isFetchingNextPage])
-
- const handleTriggerClick = useCallback((event: React.MouseEvent) => {
- event.preventDefault()
- if (disabled || isShow)
- return
-
- setIsShow(true)
- }, [disabled, isShow])
-
- const [isShowChooseApp, setIsShowChooseApp] = useState(false)
- const handleSelectApp = (app: App) => {
- const clearValue = app.id !== value?.app_id
- const appValue = {
+ onSelect({
app_id: app.id,
- inputs: clearValue ? {} : value?.inputs || {},
- files: clearValue ? [] : value?.files || [],
- }
- onSelect(appValue)
- setIsShowChooseApp(false)
- }
+ inputs: shouldClearValue ? {} : value?.inputs || {},
+ files: shouldClearValue ? [] : value?.files || [],
+ })
+ }, [onSelect, value?.app_id, value?.files, value?.inputs])
- const handleFormChange = (inputs: Record) => {
+ const handleFormChange = useCallback((inputs: Record) => {
const newFiles = inputs['#image#']
- delete inputs['#image#']
- const newValue = {
- app_id: value?.app_id || '',
- inputs,
- files: newFiles ? [newFiles] : value?.files || [],
- }
- onSelect(newValue)
- }
+ const nextInputs = { ...inputs }
+ delete nextInputs['#image#']
- const formattedValue = useMemo(() => {
- return {
+ onSelect({
app_id: value?.app_id || '',
- inputs: {
- ...value?.inputs,
- ...(value?.files?.length ? { '#image#': value.files[0] } : {}),
- },
- }
- }, [value])
+ inputs: nextInputs,
+ files: newFiles ? [newFiles] : value?.files || [],
+ })
+ }, [onSelect, value?.app_id, value?.files])
+
+ const formattedValue = useMemo(() => ({
+ app_id: value?.app_id || '',
+ inputs: {
+ ...value?.inputs,
+ ...(value?.files?.length ? { '#image#': value.files[0] } : {}),
+ },
+ }), [value])
return (
- <>
-
+ }
>
-
-
-
- )}
- onClick={handleTriggerClick}
+
-
-
-
-
{t('appSelector.label', { ns: 'app' })}
-
- )}
- isShow={isShowChooseApp}
- onShowChange={setIsShowChooseApp}
- disabled={false}
- onSelect={handleSelectApp}
- scope={scope || 'all'}
- apps={appsForPicker}
- isLoading={isLoading || isFetchingNextPage}
- hasMore={hasMore}
- onLoadMore={handleLoadMore}
- searchText={searchText}
- onSearchChange={setSearchText}
- />
-
- {/* app inputs config panel */}
- {currentAppInfo && (
-
- )}
+
+
+
+
+
{t('appSelector.label', { ns: 'app' })}
+
+ )}
+ isShow={isShowChooseApp}
+ onShowChange={setIsShowChooseApp}
+ disabled={false}
+ onSelect={handleSelectApp}
+ apps={displayedApps}
+ isLoading={isLoading || isFetchingNextPage}
+ hasMore={hasMore}
+ onLoadMore={() => {
+ void fetchNextPage()
+ }}
+ searchText={searchText}
+ onSearchChange={setSearchText}
+ />
-
-
- >
+ {currentAppInfo && (
+
+ )}
+
+
+
)
}
-
-export default React.memo(AppSelector)
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx
index 34325109eb..7ceb8607c5 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx
@@ -5,34 +5,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SupportedCreationMethods } from '@/app/components/plugins/types'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
-import { CreateButtonType, CreateSubscriptionButton, DEFAULT_METHOD } from '../index'
-
-let mockPortalOpenState = false
-
-vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => {
- mockPortalOpenState = open || false
- return (
-
- {children}
-
- )
- },
- PortalToFollowElemTrigger: ({ children, onClick, className }: { children: React.ReactNode, onClick?: () => void, className?: string }) => (
-
- {children}
-
- ),
- PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => {
- if (!mockPortalOpenState)
- return null
- return (
-
- {children}
-
- )
- },
-}))
+import { CreateSubscriptionButton } from '../index'
+import { CreateButtonType, DEFAULT_METHOD } from '../types'
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: Object.assign(vi.fn(), {
@@ -107,40 +81,47 @@ vi.mock('../common-modal', () => ({
}))
vi.mock('../oauth-client', () => ({
- OAuthClientSettingsModal: ({ oauthConfig, onClose, showOAuthCreateModal }: {
+ OAuthClientSettingsModal: ({ open, oauthConfig, onOpenChange, showOAuthCreateModal }: {
+ open: boolean
oauthConfig?: TriggerOAuthConfig
- onClose: () => void
+ onOpenChange: (open: boolean) => void
showOAuthCreateModal: (builder: TriggerSubscriptionBuilder) => void
- }) => (
-
-
Close
-
showOAuthCreateModal({
- id: 'test-builder',
- name: 'test',
- provider: 'test-provider',
- credential_type: TriggerCredentialTypeEnum.Oauth2,
- credentials: {},
- endpoint: 'https://test.com',
- parameters: {},
- properties: {},
- workflows_in_use: 0,
- })}
+ }) => {
+ if (!open)
+ return null
+
+ return (
+
- Show Create Modal
-
-
- ),
+ onOpenChange(false)}>Close
+ showOAuthCreateModal({
+ id: 'test-builder',
+ name: 'test',
+ provider: 'test-provider',
+ credential_type: TriggerCredentialTypeEnum.Oauth2,
+ credentials: {},
+ endpoint: 'https://test.com',
+ parameters: {},
+ properties: {},
+ workflows_in_use: 0,
+ })}
+ >
+ Show Create Modal
+
+
+ )
+ },
}))
vi.mock('@langgenius/dify-ui/select', async () => {
const React = await import('react')
const SelectContext = React.createContext<{
+ onOpenChange?: (open: boolean) => void
onValueChange?: (value: string) => void
}>({})
@@ -160,11 +141,13 @@ vi.mock('@langgenius/dify-ui/select', async () => {
children,
value,
open,
+ onOpenChange,
onValueChange,
}: {
children: React.ReactNode
value: string | null
open?: boolean
+ onOpenChange?: (open: boolean) => void
onValueChange?: (value: string) => void
}) => {
const currentValue = value ?? DEFAULT_METHOD
@@ -175,10 +158,11 @@ vi.mock('@langgenius/dify-ui/select', async () => {
: String(open ?? false)
return (
-
+
@@ -188,7 +172,16 @@ vi.mock('@langgenius/dify-ui/select', async () => {
)
},
SelectTrigger: ({ children, className }: { children: React.ReactNode, render?: React.ReactNode, className?: string }) => {
- return
{children}
+ const context = React.useContext(SelectContext)
+ return (
+
context.onOpenChange?.(true)}
+ >
+ {children}
+
+ )
},
SelectContent: ({ children }: { children: React.ReactNode }) => (
{children}
@@ -281,7 +274,6 @@ const setupMocks = (config: {
describe('CreateSubscriptionButton', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockPortalOpenState = false
setupMocks()
})
@@ -494,6 +486,38 @@ describe('CreateSubscriptionButton', () => {
})
})
+ it('should close dropdown when oauth settings is clicked from option extra action', async () => {
+ // Arrange
+ setupMocks({
+ storeDetail: createStoreDetail(),
+ providerInfo: createProviderInfo({
+ supported_creation_methods: [
+ SupportedCreationMethods.OAUTH,
+ SupportedCreationMethods.APIKEY,
+ SupportedCreationMethods.MANUAL,
+ ],
+ }),
+ oauthConfig: createOAuthConfig({ configured: false }),
+ })
+ const props = createDefaultProps()
+
+ // Act
+ render(
)
+
+ fireEvent.click(screen.getByTestId('custom-trigger'))
+ expect(screen.getByTestId('custom-select'))!.toHaveAttribute('data-open', 'true')
+
+ fireEvent.click(screen.getByRole('button', {
+ name: 'pluginTrigger.subscription.addType.options.oauth.clientSettings',
+ }))
+
+ // Assert
+ await waitFor(() => {
+ expect(screen.getByTestId('oauth-client-modal'))!.toBeInTheDocument()
+ expect(screen.getByTestId('custom-select'))!.toHaveAttribute('data-open', 'false')
+ })
+ })
+
it('should close OAuthClientSettingsModal and refetch config when closed', async () => {
// Arrange
const mockRefetchOAuth = vi.fn()
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx
index 46b9499027..21c76ecdca 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx
@@ -110,48 +110,6 @@ Object.defineProperty(navigator, 'clipboard', {
writable: true,
})
-vi.mock('@/app/components/base/modal/modal', () => ({
- default: ({
- children,
- onClose,
- onConfirm,
- onCancel,
- title,
- confirmButtonText,
- cancelButtonText,
- footerSlot,
- onExtraButtonClick,
- extraButtonText,
- }: {
- children: React.ReactNode
- onClose: () => void
- onConfirm: () => void
- onCancel: () => void
- title: string
- confirmButtonText: string
- cancelButtonText?: string
- footerSlot?: React.ReactNode
- onExtraButtonClick?: () => void
- extraButtonText?: string
- }) => (
-
-
{title}
-
{children}
-
- {footerSlot}
- {extraButtonText && (
- {extraButtonText}
- )}
- {cancelButtonText && (
- {cancelButtonText}
- )}
- {confirmButtonText}
- Close
-
-
- ),
-}))
-
let mockFormValues: { values: Record
, isCheckValidated: boolean } = {
values: { client_id: 'test-client-id', client_secret: 'test-client-secret' },
isCheckValidated: true,
@@ -161,10 +119,13 @@ const setMockFormValues = (values: typeof mockFormValues) => {
}
vi.mock('@/app/components/base/form/components/base', () => ({
- BaseForm: React.forwardRef((
- { formSchemas }: { formSchemas: Array<{ name: string, default?: string }> },
- ref: React.ForwardedRef<{ getFormValues: () => { values: Record, isCheckValidated: boolean } }>,
- ) => {
+ BaseForm: ({
+ formSchemas,
+ ref,
+ }: {
+ formSchemas: Array<{ name: string, default?: string }>
+ ref?: React.Ref<{ getFormValues: () => { values: Record, isCheckValidated: boolean } }>
+ }) => {
React.useImperativeHandle(ref, () => ({
getFormValues: () => mockFormValues,
}))
@@ -180,15 +141,24 @@ vi.mock('@/app/components/base/form/components/base', () => ({
))}
)
- }),
+ },
}))
describe('OAuthClientSettingsModal', () => {
const defaultProps = {
+ open: true,
oauthConfig: createMockOAuthConfig(),
- onClose: vi.fn(),
+ onOpenChange: vi.fn(),
showOAuthCreateModal: vi.fn(),
}
+ const title = 'pluginTrigger.modal.oauth.title'
+ const getDialog = () => screen.getByRole('dialog', { name: title })
+ const getCloseButton = () => screen.getByRole('button', { name: 'Close' })
+ const getCancelButton = () => screen.getByRole('button', { name: 'common.operation.cancel' })
+ const getSaveOnlyButton = () => screen.getByRole('button', { name: 'plugin.auth.saveOnly' })
+ const getConfirmButton = () => screen.getByRole('button', {
+ name: /plugin\.auth\.saveAndAuth|pluginTrigger\.modal\.common\.authorizing|pluginTrigger\.modal\.oauth\.authorization\.waitingJump/,
+ })
beforeEach(() => {
vi.clearAllMocks()
@@ -215,7 +185,7 @@ describe('OAuthClientSettingsModal', () => {
it('should render modal with correct title', () => {
render( )
- expect(screen.getByTestId('modal-title')).toHaveTextContent('pluginTrigger.modal.oauth.title')
+ expect(screen.getByRole('heading', { name: title })).toBeInTheDocument()
})
it('should render client type selector when system_configured is true', () => {
@@ -332,7 +302,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
expect(mockConfigureOAuth).toHaveBeenCalled()
})
@@ -350,7 +320,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
expect(mockOpenOAuthPopup).toHaveBeenCalledWith(
'https://oauth.example.com/authorize',
@@ -359,7 +329,7 @@ describe('OAuthClientSettingsModal', () => {
})
it('should show success toast and close modal when OAuth callback succeeds', () => {
- const mockOnClose = vi.fn()
+ const mockOnOpenChange = vi.fn()
const mockShowOAuthCreateModal = vi.fn()
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => {
@@ -379,18 +349,18 @@ describe('OAuthClientSettingsModal', () => {
render(
,
)
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
message: 'pluginTrigger.modal.oauth.authorization.authSuccess',
})
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
it('should show error toast when OAuth initiation fails', () => {
@@ -403,7 +373,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'error',
@@ -420,7 +390,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -432,20 +402,20 @@ describe('OAuthClientSettingsModal', () => {
})
it('should show success toast when save only succeeds', () => {
- const mockOnClose = vi.fn()
+ const mockOnOpenChange = vi.fn()
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => {
onSuccess()
})
- render( )
+ render( )
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
message: 'pluginTrigger.modal.oauth.save.success',
})
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
})
@@ -469,7 +439,7 @@ describe('OAuthClientSettingsModal', () => {
})
it('should show success toast when remove succeeds', () => {
- const mockOnClose = vi.fn()
+ const mockOnOpenChange = vi.fn()
const configWithCustomEnabled = createMockOAuthConfig({
system_configured: false,
custom_enabled: true,
@@ -484,7 +454,7 @@ describe('OAuthClientSettingsModal', () => {
,
)
@@ -495,7 +465,7 @@ describe('OAuthClientSettingsModal', () => {
type: 'success',
message: 'pluginTrigger.modal.oauth.remove.success',
})
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
it('should show error toast when remove fails', () => {
@@ -522,22 +492,22 @@ describe('OAuthClientSettingsModal', () => {
})
describe('Modal Actions', () => {
- it('should call onClose when close button is clicked', () => {
- const mockOnClose = vi.fn()
- render( )
+ it('should call onOpenChange when close button is clicked', () => {
+ const mockOnOpenChange = vi.fn()
+ render( )
- fireEvent.click(screen.getByTestId('modal-close'))
+ fireEvent.click(getCloseButton())
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange.mock.calls[0]?.[0]).toBe(false)
})
- it('should call onClose when extra button (cancel) is clicked', () => {
- const mockOnClose = vi.fn()
- render( )
+ it('should call onOpenChange when cancel button is clicked', () => {
+ const mockOnOpenChange = vi.fn()
+ render( )
- fireEvent.click(screen.getByTestId('modal-extra'))
+ fireEvent.click(getCancelButton())
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
})
@@ -545,13 +515,13 @@ describe('OAuthClientSettingsModal', () => {
it('should show default button text initially', () => {
render( )
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('plugin.auth.saveAndAuth')
+ expect(getConfirmButton()).toHaveTextContent('plugin.auth.saveAndAuth')
})
it('should show save only button text', () => {
render( )
- expect(screen.getByTestId('modal-cancel')).toHaveTextContent('plugin.auth.saveOnly')
+ expect(getSaveOnlyButton()).toHaveTextContent('plugin.auth.saveOnly')
})
})
@@ -591,7 +561,7 @@ describe('OAuthClientSettingsModal', () => {
it('should handle undefined oauthConfig', () => {
render( )
- expect(screen.getByTestId('modal')).toBeInTheDocument()
+ expect(getDialog()).toBeInTheDocument()
})
it('should handle missing provider', () => {
@@ -600,7 +570,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
- expect(screen.getByTestId('modal')).toBeInTheDocument()
+ expect(getDialog()).toBeInTheDocument()
})
})
@@ -618,7 +588,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// Verify OAuth flow was initiated
expect(mockInitiateOAuth).toHaveBeenCalledWith(
@@ -644,13 +614,13 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
vi.advanceTimersByTime(3000)
expect(mockVerifyBuilder).toHaveBeenCalled()
// Should still be in pending state (polling continues)
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('pluginTrigger.modal.common.authorizing')
+ expect(getConfirmButton()).toHaveTextContent('pluginTrigger.modal.common.authorizing')
vi.useRealTimers()
})
@@ -765,7 +735,7 @@ describe('OAuthClientSettingsModal', () => {
describe('OAuth callback edge cases', () => {
it('should not show success toast when OAuth callback returns falsy data', () => {
- const mockOnClose = vi.fn()
+ const mockOnOpenChange = vi.fn()
const mockShowOAuthCreateModal = vi.fn()
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => {
@@ -784,12 +754,12 @@ describe('OAuthClientSettingsModal', () => {
render(
,
)
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// Should not show success toast or call callbacks
expect(mockToastNotify).not.toHaveBeenCalledWith(
@@ -811,7 +781,7 @@ describe('OAuthClientSettingsModal', () => {
const customCard = screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')
fireEvent.click(customCard!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -829,7 +799,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
// Default is already selected
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -901,7 +871,7 @@ describe('OAuthClientSettingsModal', () => {
it('should show saveAndAuth text by default', () => {
render( )
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('plugin.auth.saveAndAuth')
+ expect(getConfirmButton()).toHaveTextContent('plugin.auth.saveAndAuth')
})
it('should show authorizing text when authorization is pending', () => {
@@ -914,9 +884,9 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('pluginTrigger.modal.common.authorizing')
+ expect(getConfirmButton()).toHaveTextContent('pluginTrigger.modal.common.authorizing')
})
})
@@ -931,10 +901,10 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// After failure, button text should return to default
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('plugin.auth.saveAndAuth')
+ expect(getConfirmButton()).toHaveTextContent('plugin.auth.saveAndAuth')
})
})
@@ -1013,7 +983,7 @@ describe('OAuthClientSettingsModal', () => {
const customCard = screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!
fireEvent.click(customCard)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
// Should not call configureOAuth because form validation failed
expect(mockConfigureOAuth).not.toHaveBeenCalled()
@@ -1035,7 +1005,7 @@ describe('OAuthClientSettingsModal', () => {
// Switch to custom type
fireEvent.click(screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1062,7 +1032,7 @@ describe('OAuthClientSettingsModal', () => {
// Switch to custom type
fireEvent.click(screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1089,7 +1059,7 @@ describe('OAuthClientSettingsModal', () => {
// Switch to custom type
fireEvent.click(screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1116,7 +1086,7 @@ describe('OAuthClientSettingsModal', () => {
// Switch to custom type
fireEvent.click(screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1148,7 +1118,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// Advance timer to trigger polling
await vi.advanceTimersByTimeAsync(3000)
@@ -1157,7 +1127,7 @@ describe('OAuthClientSettingsModal', () => {
// Button text should show waitingJump after verified
await waitFor(() => {
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('pluginTrigger.modal.oauth.authorization.waitingJump')
+ expect(getConfirmButton()).toHaveTextContent('pluginTrigger.modal.oauth.authorization.waitingJump')
})
vi.useRealTimers()
@@ -1180,7 +1150,7 @@ describe('OAuthClientSettingsModal', () => {
render( )
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// First poll
await vi.advanceTimersByTimeAsync(3000)
@@ -1191,7 +1161,7 @@ describe('OAuthClientSettingsModal', () => {
expect(mockVerifyBuilder).toHaveBeenCalledTimes(2)
// Should still be in authorizing state
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('pluginTrigger.modal.common.authorizing')
+ expect(getConfirmButton()).toHaveTextContent('pluginTrigger.modal.common.authorizing')
vi.useRealTimers()
})
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts
index 82eddf501d..4c88b1207c 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts
@@ -137,7 +137,7 @@ describe('useOAuthClientState', () => {
const defaultParams = {
oauthConfig: createMockOAuthConfig(),
providerName: 'test-provider',
- onClose: vi.fn(),
+ onOpenChange: vi.fn(),
showOAuthCreateModal: vi.fn(),
}
@@ -310,20 +310,20 @@ describe('useOAuthClientState', () => {
)
})
- it('should call onClose and show success toast on success', () => {
+ it('should call onOpenChange and show success toast on success', () => {
mockDeleteOAuth.mockImplementation((provider, { onSuccess }) => onSuccess())
- const onClose = vi.fn()
+ const onOpenChange = vi.fn()
const { result } = renderHook(() => useOAuthClientState({
...defaultParams,
- onClose,
+ onOpenChange,
}))
act(() => {
result.current.handleRemove()
})
- expect(onClose).toHaveBeenCalled()
+ expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
message: 'pluginTrigger.modal.oauth.remove.success',
@@ -398,20 +398,20 @@ describe('useOAuthClientState', () => {
)
})
- it('should show success toast and call onClose when needAuth is false', () => {
+ it('should show success toast and call onOpenChange when needAuth is false', () => {
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => onSuccess())
- const onClose = vi.fn()
+ const onOpenChange = vi.fn()
const { result } = renderHook(() => useOAuthClientState({
...defaultParams,
- onClose,
+ onOpenChange,
}))
act(() => {
result.current.handleSave(false)
})
- expect(onClose).toHaveBeenCalled()
+ expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
message: 'pluginTrigger.modal.oauth.save.success',
@@ -495,8 +495,8 @@ describe('useOAuthClientState', () => {
})
})
- it('should call onClose and showOAuthCreateModal on callback success', () => {
- const onClose = vi.fn()
+ it('should call onOpenChange and showOAuthCreateModal on callback success', () => {
+ const onOpenChange = vi.fn()
const showOAuthCreateModal = vi.fn()
const builder = createMockSubscriptionBuilder()
@@ -513,7 +513,7 @@ describe('useOAuthClientState', () => {
const { result } = renderHook(() => useOAuthClientState({
...defaultParams,
- onClose,
+ onOpenChange,
showOAuthCreateModal,
}))
@@ -521,7 +521,7 @@ describe('useOAuthClientState', () => {
result.current.handleSave(true)
})
- expect(onClose).toHaveBeenCalled()
+ expect(onOpenChange).toHaveBeenCalledWith(false)
expect(showOAuthCreateModal).toHaveBeenCalledWith(builder)
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
@@ -530,7 +530,7 @@ describe('useOAuthClientState', () => {
})
it('should not call callbacks when OAuth callback returns falsy', () => {
- const onClose = vi.fn()
+ const onOpenChange = vi.fn()
const showOAuthCreateModal = vi.fn()
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => onSuccess())
@@ -546,7 +546,7 @@ describe('useOAuthClientState', () => {
const { result } = renderHook(() => useOAuthClientState({
...defaultParams,
- onClose,
+ onOpenChange,
showOAuthCreateModal,
}))
@@ -554,7 +554,7 @@ describe('useOAuthClientState', () => {
result.current.handleSave(true)
})
- expect(onClose).not.toHaveBeenCalled()
+ expect(onOpenChange).not.toHaveBeenCalled()
expect(showOAuthCreateModal).not.toHaveBeenCalled()
})
})
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts
index 25058e529c..e2bd284d15 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts
@@ -13,16 +13,20 @@ import {
useVerifyAndUpdateTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
-export enum AuthorizationStatusEnum {
- Pending = 'pending',
- Success = 'success',
- Failed = 'failed',
-}
+export const AuthorizationStatusEnum = {
+ Pending: 'pending',
+ Success: 'success',
+ Failed: 'failed',
+} as const
-export enum ClientTypeEnum {
- Default = 'default',
- Custom = 'custom',
-}
+export type AuthorizationStatusEnum = typeof AuthorizationStatusEnum[keyof typeof AuthorizationStatusEnum]
+
+export const ClientTypeEnum = {
+ Default: 'default',
+ Custom: 'custom',
+} as const
+
+export type ClientTypeEnum = typeof ClientTypeEnum[keyof typeof ClientTypeEnum]
const POLL_INTERVAL_MS = 3000
@@ -41,7 +45,7 @@ export const getErrorMessage = (error: unknown, fallback: string): string => {
type UseOAuthClientStateParams = {
oauthConfig?: TriggerOAuthConfig
providerName: string
- onClose: () => void
+ onOpenChange: (open: boolean) => void
showOAuthCreateModal: (builder: TriggerSubscriptionBuilder) => void
}
@@ -67,7 +71,7 @@ type UseOAuthClientStateReturn = {
export const useOAuthClientState = ({
oauthConfig,
providerName,
- onClose,
+ onOpenChange,
showOAuthCreateModal,
}: UseOAuthClientStateParams): UseOAuthClientStateReturn => {
const { t } = useTranslation()
@@ -119,7 +123,7 @@ export const useOAuthClientState = ({
if (!callbackData)
return
toast.success(t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }))
- onClose()
+ onOpenChange(false)
showOAuthCreateModal(response.subscription_builder)
})
},
@@ -128,20 +132,20 @@ export const useOAuthClientState = ({
toast.error(t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }))
},
})
- }, [providerName, initiateOAuth, onClose, showOAuthCreateModal, t])
+ }, [providerName, initiateOAuth, onOpenChange, showOAuthCreateModal, t])
// Remove handler
const handleRemove = useCallback(() => {
deleteOAuth(providerName, {
onSuccess: () => {
- onClose()
+ onOpenChange(false)
toast.success(t('modal.oauth.remove.success', { ns: 'pluginTrigger' }))
},
onError: (error: unknown) => {
toast.error(getErrorMessage(error, t('modal.oauth.remove.failed', { ns: 'pluginTrigger' })))
},
})
- }, [providerName, deleteOAuth, onClose, t])
+ }, [providerName, deleteOAuth, onOpenChange, t])
// Save handler
const handleSave = useCallback((needAuth: boolean) => {
@@ -174,11 +178,11 @@ export const useOAuthClientState = ({
handleAuthorization()
return
}
- onClose()
+ onOpenChange(false)
toast.success(t('modal.oauth.save.success', { ns: 'pluginTrigger' }))
},
})
- }, [clientType, providerName, oauthClientSchema, oauthConfig?.params, configureOAuth, handleAuthorization, onClose, t])
+ }, [clientType, providerName, oauthClientSchema, oauthConfig?.params, configureOAuth, handleAuthorization, onOpenChange, t])
// Polling effect for authorization verification
useEffect(() => {
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx
index ca1194e6ed..9091cd337c 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx
@@ -68,9 +68,20 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
const onClickClientSettings = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
+ setIsMenuOpen(false)
showClientSettingsModal()
}, [showClientSettingsModal])
+ const handleClientSettingsOpenChange = useCallback((open: boolean) => {
+ if (open) {
+ showClientSettingsModal()
+ return
+ }
+
+ hideClientSettingsModal()
+ refetchOAuthConfig()
+ }, [hideClientSettingsModal, refetchOAuthConfig, showClientSettingsModal])
+
const allOptions = useMemo(() => {
const showCustomBadge = oauthConfig?.custom_enabled && oauthConfig?.custom_configured
@@ -299,11 +310,9 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
{isShowClientSettingsModal
? (
{
- hideClientSettingsModal()
- refetchOAuthConfig()
- }}
+ onOpenChange={handleClientSettingsOpenChange}
showOAuthCreateModal={(builder) => {
showCreateModal({
type: SupportedCreationMethods.OAUTH,
@@ -316,5 +325,3 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
>
)
}
-
-export { CreateButtonType, DEFAULT_METHOD } from './types'
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx
index 450324ae40..3c8b8c6aa7 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx
@@ -1,31 +1,36 @@
'use client'
import type { TriggerOAuthConfig, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import { Button } from '@langgenius/dify-ui/button'
+import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
-import {
- RiClipboardLine,
- RiInformation2Fill,
-} from '@remixicon/react'
+import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseForm } from '@/app/components/base/form/components/base'
-import Modal from '@/app/components/base/modal/modal'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { usePluginStore } from '../../store'
-import { ClientTypeEnum, useOAuthClientState } from './hooks/use-oauth-client-state'
+import { ClientTypeEnum, useOAuthClientState as useOAuthClientSettings } from './hooks/use-oauth-client-state'
type Props = {
+ open: boolean
oauthConfig?: TriggerOAuthConfig
- onClose: () => void
+ onOpenChange: (open: boolean) => void
showOAuthCreateModal: (builder: TriggerSubscriptionBuilder) => void
}
const CLIENT_TYPE_OPTIONS = [ClientTypeEnum.Default, ClientTypeEnum.Custom] as const
-export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreateModal }: Props) => {
+export const OAuthClientSettingsModal = ({ open, oauthConfig, onOpenChange, showOAuthCreateModal }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const providerName = detail?.provider || ''
+ const closeModal = useCallback(() => onOpenChange(false), [onOpenChange])
+ const oauthClientSettings = useOAuthClientSettings({
+ oauthConfig,
+ providerName,
+ onOpenChange,
+ showOAuthCreateModal,
+ })
const {
clientType,
setClientType,
@@ -34,12 +39,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
confirmButtonText,
handleRemove,
handleSave,
- } = useOAuthClientState({
- oauthConfig,
- providerName,
- onClose,
- showOAuthCreateModal,
- })
+ } = oauthClientSettings
const isCustomClient = clientType === ClientTypeEnum.Custom
const showRemoveButton = oauthConfig?.custom_enabled && oauthConfig?.params && isCustomClient
@@ -51,81 +51,116 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
}
+ const title = t('modal.oauth.title', { ns: 'pluginTrigger' })
+
return (
- handleSave(false)}
- onConfirm={() => handleSave(true)}
- footerSlot={showRemoveButton && (
-
-
- {t('operation.remove', { ns: 'common' })}
-
-
- )}
+
-
- {t('subscription.addType.options.oauth.clientTitle', { ns: 'pluginTrigger' })}
-
-
- {oauthConfig?.system_configured && (
-
- {CLIENT_TYPE_OPTIONS.map(option => (
- setClientType(option)}
- selected={clientType === option}
- className="flex-1"
- />
- ))}
-
- )}
-
- {showRedirectInfo && (
-
-
-
+
+
+
+
+ {title}
+
+
-
-
- {t('modal.oauthRedirectInfo', { ns: 'pluginTrigger' })}
+
+
+ {t('subscription.addType.options.oauth.clientTitle', { ns: 'pluginTrigger' })}
-
- {oauthConfig?.redirect_uri}
+
+ {oauthConfig?.system_configured && (
+
+ {CLIENT_TYPE_OPTIONS.map(option => (
+ setClientType(option)}
+ selected={clientType === option}
+ className="flex-1"
+ />
+ ))}
+
+ )}
+
+ {showRedirectInfo && (
+
+
+
+
+
+
+ {t('modal.oauthRedirectInfo', { ns: 'pluginTrigger' })}
+
+
+ {oauthConfig?.redirect_uri}
+
+
+
+ {t('operation.copy', { ns: 'common' })}
+
+
+
+ )}
+
+ {showClientForm && (
+
+ )}
+
+
+
+ {showRemoveButton && (
+
+ {t('operation.remove', { ns: 'common' })}
+
+ )}
+
+
+
+ {t('operation.cancel', { ns: 'common' })}
+
+
+
handleSave(false)}
+ >
+ {t('auth.saveOnly', { ns: 'plugin' })}
+
+
handleSave(true)}
+ >
+ {confirmButtonText}
+
-
-
- {t('operation.copy', { ns: 'common' })}
-
- )}
-
- {showClientForm && (
-
- )}
-
+
+
)
}
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/__tests__/index.spec.tsx
index 6cf58b8972..e1544f6649 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/__tests__/index.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/__tests__/index.spec.tsx
@@ -166,49 +166,6 @@ vi.mock('@/app/components/base/form/components/base', () => ({
}),
}))
-vi.mock('@/app/components/base/modal/modal', () => ({
- default: ({
- title,
- confirmButtonText,
- onClose,
- onCancel,
- onConfirm,
- disabled,
- children,
- showExtraButton,
- extraButtonText,
- onExtraButtonClick,
- bottomSlot,
- }: {
- title: string
- confirmButtonText: string
- onClose: () => void
- onCancel: () => void
- onConfirm: () => void
- disabled?: boolean
- children: React.ReactNode
- showExtraButton?: boolean
- extraButtonText?: string
- onExtraButtonClick?: () => void
- bottomSlot?: React.ReactNode
- }) => (
-
-
{children}
-
- {confirmButtonText}
-
-
Cancel
-
Close
- {showExtraButton && (
-
- {extraButtonText}
-
- )}
- {!!bottomSlot &&
{bottomSlot}
}
-
- ),
-}))
-
// ==================== Test Utilities ====================
const createSubscription = (overrides: Partial
= {}): TriggerSubscription => ({
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx
index f191ad41a8..f44a7db921 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx
@@ -2,6 +2,8 @@
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
+import { Button } from '@langgenius/dify-ui/button'
+import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { isEqual } from 'es-toolkit/predicate'
import { useMemo, useRef, useState } from 'react'
@@ -9,7 +11,6 @@ import { useTranslation } from 'react-i18next'
import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
import { BaseForm } from '@/app/components/base/form/components/base'
import { FormTypeEnum } from '@/app/components/base/form/types'
-import Modal from '@/app/components/base/modal/modal'
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
import { useUpdateTriggerSubscription, useVerifyTriggerSubscription } from '@/service/use-triggers'
import { parsePluginErrorMessage } from '@/utils/error-parser'
@@ -23,10 +24,12 @@ type Props = {
pluginDetail?: PluginDetail
}
-enum EditStep {
- EditCredentials = 'edit_credentials',
- EditConfiguration = 'edit_configuration',
-}
+const EditStep = {
+ EditCredentials: 'edit_credentials',
+ EditConfiguration: 'edit_configuration',
+} as const
+
+type EditStep = typeof EditStep[keyof typeof EditStep]
const normalizeFormType = (type: string): FormTypeEnum => {
switch (type) {
@@ -52,7 +55,6 @@ const normalizeFormType = (type: string): FormTypeEnum => {
const HIDDEN_SECRET_VALUE = '[__HIDDEN__]'
-// Check if all credential values are hidden (meaning nothing was changed)
const areAllCredentialsHidden = (credentials: Record): boolean => {
return Object.values(credentials).every(value => value === HIDDEN_SECRET_VALUE)
}
@@ -63,17 +65,36 @@ const StatusStep = ({ isActive, text, onClick, clickable }: {
onClick?: () => void
clickable?: boolean
}) => {
- return (
-
- {isActive && (
-
- )}
+ const className = `flex items-center gap-1 system-2xs-semibold-uppercase ${isActive
+ ? 'text-state-accent-solid'
+ : 'text-text-tertiary'} ${clickable ? 'cursor-pointer rounded bg-transparent p-0 text-left hover:text-text-secondary focus-visible:text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden' : ''}`
+
+ const content = (
+ <>
+ {isActive
+ ? (
+
+ )
+ : null}
{text}
+ >
+ )
+
+ if (clickable) {
+ return (
+
+ {content}
+
+ )
+ }
+
+ return (
+
+ {content}
)
}
@@ -260,78 +281,123 @@ export const ApiKeyEditModal = ({ onClose, subscription, pluginDetail }: Props)
})
}, [parametersSchema, subscription.parameters, subscription.id, detail?.plugin_id, detail?.provider, verifiedCredentials])
- const getConfirmButtonText = () => {
+ const confirmButtonText = (() => {
if (currentStep === EditStep.EditCredentials)
return isVerifying ? t('modal.common.verifying', { ns: 'pluginTrigger' }) : t('modal.common.verify', { ns: 'pluginTrigger' })
return isUpdating ? t('operation.saving', { ns: 'common' }) : t('operation.save', { ns: 'common' })
- }
+ })()
const handleBack = () => {
setCurrentStep(EditStep.EditCredentials)
setVerifiedCredentials(null)
}
+ const isDisabled = isUpdating || isVerifying
+ const title = t('subscription.list.item.actions.edit.title', { ns: 'pluginTrigger' })
+
return (
-
: null}
+
{
+ if (!open)
+ onClose()
+ }}
>
- {pluginDetail && (
-
- )}
+
+
+
+
+ {title}
+
+
+
+
+ {pluginDetail && (
+
+ )}
- {/* Multi-step indicator */}
-
+
- {/* Step 1: Edit Credentials */}
- {currentStep === EditStep.EditCredentials && (
-
- {credentialsFormSchemas.length > 0 && (
-
+ {currentStep === EditStep.EditCredentials
+ ? (
+
+ {credentialsFormSchemas.length > 0 && (
+
+ )}
+
+ )
+ : (
+
+
+
+ {parametersFormSchemas.length > 0 && (
+
+ )}
+
+ )}
+
+
+
+
+ {currentStep === EditStep.EditConfiguration && (
+ <>
+
+ {t('modal.common.back', { ns: 'pluginTrigger' })}
+
+
+ >
+ )}
+
+ {t('operation.cancel', { ns: 'common' })}
+
+
+ {confirmButtonText}
+
+
+
+ {currentStep === EditStep.EditCredentials && (
+
+
+
)}
- )}
-
- {/* Step 2: Edit Configuration */}
- {currentStep === EditStep.EditConfiguration && (
-
- {/* Basic form: subscription name and callback URL */}
-
-
- {/* Parameters */}
- {parametersFormSchemas.length > 0 && (
-
- )}
-
- )}
-
+
+
)
}
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx
index 3b3fa1082f..774eaa9fe9 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx
@@ -2,13 +2,14 @@
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
+import { Button } from '@langgenius/dify-ui/button'
+import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { isEqual } from 'es-toolkit/predicate'
import { useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseForm } from '@/app/components/base/form/components/base'
import { FormTypeEnum } from '@/app/components/base/form/types'
-import Modal from '@/app/components/base/modal/modal'
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
import { ReadmeShowType } from '../../../readme-panel/store'
@@ -133,26 +134,63 @@ export const ManualEditModal = ({ onClose, subscription, pluginDetail }: Props)
})),
], [t, subscription.name, subscription.endpoint, subscription.properties, propertiesSchema])
+ const title = t('subscription.list.item.actions.edit.title', { ns: 'pluginTrigger' })
+ const confirmButtonText = isUpdating ? t('operation.saving', { ns: 'common' }) : t('operation.save', { ns: 'common' })
+
return (
-
{
+ if (!open)
+ onClose()
+ }}
>
- {pluginDetail && (
-
- )}
-
-
+
+
+
+
+ {title}
+
+
+
+
+ {pluginDetail && (
+
+ )}
+
+
+
+
+
+
+ {t('operation.cancel', { ns: 'common' })}
+
+
+ {confirmButtonText}
+
+
+
+
+
+
)
}
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx
index 355a132bd2..f78933a9ff 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx
@@ -2,13 +2,14 @@
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
+import { Button } from '@langgenius/dify-ui/button'
+import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { isEqual } from 'es-toolkit/predicate'
import { useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseForm } from '@/app/components/base/form/components/base'
import { FormTypeEnum } from '@/app/components/base/form/types'
-import Modal from '@/app/components/base/modal/modal'
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
import { ReadmeShowType } from '../../../readme-panel/store'
@@ -147,26 +148,63 @@ export const OAuthEditModal = ({ onClose, subscription, pluginDetail }: Props) =
}),
], [t, subscription.name, subscription.endpoint, subscription.parameters, subscription.id, parametersSchema, detail?.plugin_id, detail?.provider])
+ const title = t('subscription.list.item.actions.edit.title', { ns: 'pluginTrigger' })
+ const confirmButtonText = isUpdating ? t('operation.saving', { ns: 'common' }) : t('operation.save', { ns: 'common' })
+
return (
-
{
+ if (!open)
+ onClose()
+ }}
>
- {pluginDetail && (
-
- )}
-
-
+
+
+
+
+ {title}
+
+
+
+
+ {pluginDetail && (
+
+ )}
+
+
+
+
+
+
+ {t('operation.cancel', { ns: 'common' })}
+
+
+ {confirmButtonText}
+
+
+
+
+
+
)
}
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx
index 45d4c486b7..66dfabb4db 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx
@@ -4,7 +4,8 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import { CreateButtonType, CreateSubscriptionButton } from './create'
+import { CreateSubscriptionButton } from './create'
+import { CreateButtonType } from './create/types'
import SubscriptionCard from './subscription-card'
import { useSubscriptionList } from './use-subscription-list'
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx
index 993c9f36cb..06b0724247 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx
@@ -7,7 +7,8 @@ import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
-import { CreateButtonType, CreateSubscriptionButton } from './create'
+import { CreateSubscriptionButton } from './create'
+import { CreateButtonType } from './create/types'
import { DeleteConfirm } from './delete-confirm'
import { useSubscriptionList } from './use-subscription-list'
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/__tests__/index.spec.tsx
index 67ade347e8..6c58405f4a 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/__tests__/index.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/__tests__/index.spec.tsx
@@ -152,20 +152,20 @@ vi.mock('@/app/components/plugins/plugin-auth', () => ({
),
}))
-// Portal components need mocking for controlled positioning in tests
-vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({
+// Popover positioning is mocked for deterministic panel tests.
+vi.mock('@langgenius/dify-ui/popover', () => ({
+ Popover: ({
children,
open,
}: {
children: ReactNode
open?: boolean
}) => (
-
+
{children}
),
- PortalToFollowElemTrigger: ({
+ PopoverTrigger: ({
children,
render,
onClick,
@@ -174,12 +174,19 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
render?: ReactNode
onClick?: () => void
}) => (
-
- {render ?? children}
+
+ {render
+ ? (
+ <>
+ {render}
+ {children}
+ >
+ )
+ : children}
),
- PortalToFollowElemContent: ({ children }: { children: ReactNode }) => (
-
{children}
+ PopoverContent: ({ children }: { children: ReactNode }) => (
+
{children}
),
}))
@@ -254,11 +261,15 @@ vi.mock('@/app/components/workflow/block-icon', () => ({
default: () =>
,
}))
-// Mock Modal - headlessui Dialog has complex behavior
-vi.mock('@/app/components/base/modal', () => ({
- default: ({ children, isShow }: { children: ReactNode, isShow: boolean }) => (
- isShow ?
{children}
: null
+// Mock Dialog to avoid Base UI focus/portal behavior in tests
+vi.mock('@langgenius/dify-ui/dialog', () => ({
+ Dialog: ({ children, open }: { children: ReactNode, open?: boolean }) => (
+ open ?
{children}
: null
),
+ DialogContent: ({ children }: { children: ReactNode }) => (
+
{children}
+ ),
+ DialogTitle: ({ children }: { children: ReactNode }) =>
{children}
,
}))
// Mock VisualEditor - complex component with many dependencies
@@ -1372,7 +1383,7 @@ describe('ToolSelector Component', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render(
, { wrapper: createWrapper() })
- expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
+ expect(screen.getByTestId('popover')).toBeInTheDocument()
})
it('should render ToolTrigger when no value and no trigger', () => {
@@ -1394,7 +1405,7 @@ describe('ToolSelector Component', () => {
it('should render panel content', () => {
render(
, { wrapper: createWrapper() })
- expect(screen.getByTestId('portal-content')).toBeInTheDocument()
+ expect(screen.getByTestId('popover-content')).toBeInTheDocument()
})
it('should render tool base form in panel', () => {
@@ -1426,7 +1437,7 @@ describe('ToolSelector Component', () => {
{ wrapper: createWrapper() },
)
// The component should receive and use the nodeId
- expect(screen.getByTestId('portal-content')).toBeInTheDocument()
+ expect(screen.getByTestId('popover-content')).toBeInTheDocument()
})
})
@@ -1442,7 +1453,7 @@ describe('ToolSelector Component', () => {
/>,
{ wrapper: createWrapper() },
)
- expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'true')
+ expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'true')
})
it('should use internal state when no trigger', () => {
@@ -1450,7 +1461,7 @@ describe('ToolSelector Component', () => {
,
{ wrapper: createWrapper() },
)
- expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
+ expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
})
})
@@ -1503,9 +1514,9 @@ describe('ToolSelector Component', () => {
)
// Click on portal trigger
- fireEvent.click(screen.getByTestId('portal-trigger'))
+ fireEvent.click(screen.getByTestId('popover-trigger'))
// State should not change when disabled
- expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
+ expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
})
})
@@ -1523,7 +1534,7 @@ describe('ToolSelector Component', () => {
rerender(
)
// Component should not trigger unnecessary re-renders
- expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
+ expect(screen.getByTestId('popover')).toBeInTheDocument()
})
})
})
@@ -1541,7 +1552,7 @@ describe('Edge Cases', () => {
,
{ wrapper: createWrapper() },
)
- expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
+ expect(screen.getByTestId('popover')).toBeInTheDocument()
})
it('should handle undefined selectedTools', () => {
@@ -1549,7 +1560,7 @@ describe('Edge Cases', () => {
,
{ wrapper: createWrapper() },
)
- expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
+ expect(screen.getByTestId('popover')).toBeInTheDocument()
})
it('should handle empty nodeOutputVars', () => {
@@ -1557,7 +1568,7 @@ describe('Edge Cases', () => {
,
{ wrapper: createWrapper() },
)
- expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
+ expect(screen.getByTestId('popover')).toBeInTheDocument()
})
it('should handle empty availableNodes', () => {
@@ -1565,7 +1576,7 @@ describe('Edge Cases', () => {
,
{ wrapper: createWrapper() },
)
- expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
+ expect(screen.getByTestId('popover')).toBeInTheDocument()
})
})
@@ -2515,11 +2526,11 @@ describe('Additional Coverage Tests', () => {
)
// Click on the trigger
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Should still be closed because disabled
- expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
+ expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
})
it('should handle trigger click when provider and tool exist', () => {
@@ -2530,10 +2541,10 @@ describe('Additional Coverage Tests', () => {
)
// Without provider/tool, clicking should not open
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
- expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
+ expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
})
it('should early return from handleTriggerClick when disabled', () => {
@@ -2546,11 +2557,11 @@ describe('Additional Coverage Tests', () => {
// Rerender with disabled=true
rerender(
)
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Verify it stays closed
- expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
+ expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
})
it('should set isShow when clicked with valid provider and tool', () => {
@@ -2584,12 +2595,12 @@ describe('Additional Coverage Tests', () => {
)
// Click on the trigger - this should call handleTriggerClick
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Now that we have provider and tool, the click should work
// This tests lines 106-108 and 148
- expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
+ expect(screen.getByTestId('popover')).toBeInTheDocument()
})
it('should not open when disabled is true even with valid provider', () => {
@@ -2622,11 +2633,11 @@ describe('Additional Coverage Tests', () => {
)
// Click should not open because disabled=true
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Verify it stays closed due to disabled
- expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
+ expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
})
})
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/reasoning-config-form.spec.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/reasoning-config-form.spec.tsx
index f7853a1fcd..016eda373d 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/reasoning-config-form.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/reasoning-config-form.spec.tsx
@@ -1,4 +1,5 @@
import type { ReactNode } from 'react'
+import type { AppSelectorValue } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -62,11 +63,11 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
}))
vi.mock('@/app/components/plugins/plugin-detail-panel/app-selector', () => ({
- default: ({ onSelect, scope }: { onSelect: (value: Record
) => void, scope?: string }) => (
+ AppSelector: ({ onSelect, scope }: { onSelect: (value: AppSelectorValue) => void, scope?: string }) => (
onSelect({ app_id: 'app-1', inputs: { topic: 'hello' } })}
+ onClick={() => onSelect({ app_id: 'app-1', inputs: { topic: 'hello' }, files: [] })}
>
Select App
@@ -275,7 +276,7 @@ describe('ReasoningConfigForm', () => {
auto: 0,
value: {
type: undefined,
- value: { app_id: 'app-1', inputs: { topic: 'hello' } },
+ value: { app_id: 'app-1', inputs: { topic: 'hello' }, files: [] },
},
},
}))
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/schema-modal.spec.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/schema-modal.spec.tsx
index 86158ab950..e8a2ee8318 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/schema-modal.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/schema-modal.spec.tsx
@@ -53,8 +53,7 @@ describe('SchemaModal', () => {
expect(screen.getByText('workflow.nodes.agent.parameterSchema')).toBeInTheDocument()
expect(screen.getByTestId('visual-editor')).toHaveTextContent('response')
- const closeButton = document.body.querySelector('div.absolute.right-5.top-5')
- fireEvent.click(closeButton!)
+ fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
expect(onClose).toHaveBeenCalled()
})
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx
index f4b582f37e..e6af05065f 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx
@@ -21,7 +21,7 @@ import Input from '@/app/components/base/input'
import Tooltip from '@/app/components/base/tooltip'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
-import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
+import { AppSelector } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import FormInputBoolean from '@/app/components/workflow/nodes/_base/components/form-input-boolean'
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx
index c01848ea59..6ea17dc565 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx
@@ -1,10 +1,13 @@
'use client'
import type { FC } from 'react'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
-import { RiCloseLine } from '@remixicon/react'
+import {
+ Dialog,
+ DialogContent,
+ DialogTitle,
+} from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import Modal from '@/app/components/base/modal'
import VisualEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor'
import { MittProvider, VisualEditorContextProvider } from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context'
@@ -23,38 +26,43 @@ const SchemaModal: FC = ({
}) => {
const { t } = useTranslation()
return (
- !open && onClose()}
>
-
- {/* Header */}
-
-
- {t('nodes.agent.parameterSchema', { ns: 'workflow' })}
+
+
+ {/* Header */}
+
+
+ {t('nodes.agent.parameterSchema', { ns: 'workflow' })}
+
+
+
+
-
-
+ {/* Content */}
+
+
+
+
+
+
+
- {/* Content */}
-
-
-
-
-
-
-
-
-
-
+
+
)
}
export default React.memo(SchemaModal)
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx
index 0ea14ecdfc..40166ff9ec 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx
@@ -1,21 +1,18 @@
'use client'
-import type {
- OffsetOptions,
- Placement,
-} from '@floating-ui/react'
+import type { OffsetOptions } from '@floating-ui/react'
+import type { Placement } from '@langgenius/dify-ui/popover'
import type { FC } from 'react'
import type { Node } from 'reactflow'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-// eslint-disable-next-line no-restricted-imports -- legacy overlay migration is handled separately from this change
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import { CollectionType } from '@/app/components/tools/types'
import Link from '@/next/link'
import {
@@ -72,6 +69,8 @@ const ToolSelector: FC
= ({
nodeId = '',
}) => {
const { t } = useTranslation()
+ const sideOffset = typeof offset === 'number' ? offset : (typeof offset === 'function' ? 0 : (offset?.mainAxis ?? 0))
+ const alignOffset = typeof offset === 'number' ? 0 : (typeof offset === 'function' ? 0 : (offset?.crossAxis ?? 0))
// Use custom hook for state management
const state = useToolSelectorState({ value, onSelect, onSelectMultiple })
@@ -103,15 +102,14 @@ const ToolSelector: FC = ({
getSettingsValue,
} = state
- const handleTriggerClick = () => {
- if (disabled)
- return
- setIsShow(true)
- }
-
// Determine portal open state based on controlled vs uncontrolled mode
const portalOpen = trigger ? controlledState : isShow
const onPortalOpenChange = trigger ? onControlledStateChange : setIsShow
+ const handlePortalOpenChange = (nextOpen: boolean) => {
+ if (nextOpen && (disabled || !currentProvider || !currentTool))
+ return
+ onPortalOpenChange?.(nextOpen)
+ }
// Build error tooltip content
const renderErrorTip = () => (
@@ -135,19 +133,13 @@ const ToolSelector: FC = ({
)
return (
-
- {
- if (!currentProvider || !currentTool)
- return
- handleTriggerClick()
- }}
+ }
>
{trigger}
@@ -183,9 +175,14 @@ const ToolSelector: FC = ({
errorTip={renderErrorTip()}
/>
)}
-
+
-
+
= ({
onParamsFormChange={handleParamsFormChange}
/>
-
-
+
+
)
}
diff --git a/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx b/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx
index b0ecc839b3..32e2e8065c 100644
--- a/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx
+++ b/web/app/components/plugins/plugin-item/__tests__/action.spec.tsx
@@ -82,15 +82,7 @@ vi.mock('../../plugin-page/plugin-info', () => ({
),
}))
-// Mock Tooltip - uses PortalToFollowElem which requires complex floating UI setup
-// Simplified mock that just renders children with tooltip content accessible
-vi.mock('../../../base/tooltip', () => ({
- default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
-
- {children}
-
- ),
-}))
+vi.mock('@langgenius/dify-ui/tooltip', () => import('@/__mocks__/base-ui-tooltip'))
// ==================== Test Utilities ====================
@@ -236,8 +228,17 @@ describe('Action Component', () => {
render( )
// Assert
- const tooltips = screen.getAllByTestId('tooltip')
- expect(tooltips).toHaveLength(3)
+ const buttons = getActionButtons()
+ fireEvent.mouseEnter(buttons[0]!)
+ expect(screen.getByText('plugin.action.checkForUpdates'))!.toBeInTheDocument()
+ fireEvent.mouseLeave(buttons[0]!)
+
+ fireEvent.mouseEnter(buttons[1]!)
+ expect(screen.getByText('plugin.action.pluginInfo'))!.toBeInTheDocument()
+ fireEvent.mouseLeave(buttons[1]!)
+
+ fireEvent.mouseEnter(buttons[2]!)
+ expect(screen.getByText('plugin.action.delete'))!.toBeInTheDocument()
})
})
@@ -256,8 +257,7 @@ describe('Action Component', () => {
fireEvent.click(getActionButtons()[0]!)
// Assert
- // Assert
- expect(screen.getByText('plugin.action.delete'))!.toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: 'plugin.action.delete' }))!.toBeInTheDocument()
})
it('should display plugin name in delete confirm content', () => {
@@ -289,13 +289,13 @@ describe('Action Component', () => {
// Act
render( )
fireEvent.click(getActionButtons()[0]!)
- expect(screen.getByText('plugin.action.delete'))!.toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: 'plugin.action.delete' }))!.toBeInTheDocument()
fireEvent.click(getDeleteCancelButton())
// Assert
return waitFor(() => {
- expect(screen.queryByText('plugin.action.delete')).not.toBeInTheDocument()
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
@@ -414,7 +414,7 @@ describe('Action Component', () => {
// Resolve and check modal closes
resolveUninstall!({ success: true })
await waitFor(() => {
- expect(screen.queryByText('plugin.action.delete')).not.toBeInTheDocument()
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
})
@@ -871,7 +871,7 @@ describe('Action Component', () => {
resolveFirst!({ success: true })
await waitFor(() => {
- expect(screen.queryByText('plugin.action.delete')).not.toBeInTheDocument()
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
diff --git a/web/app/components/plugins/plugin-item/action.tsx b/web/app/components/plugins/plugin-item/action.tsx
index fe0ba932fc..b04bc85779 100644
--- a/web/app/components/plugins/plugin-item/action.tsx
+++ b/web/app/components/plugins/plugin-item/action.tsx
@@ -11,7 +11,7 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { toast } from '@langgenius/dify-ui/toast'
-import { RiDeleteBinLine, RiInformation2Line, RiLoopLeftLine } from '@remixicon/react'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback } from 'react'
@@ -20,7 +20,6 @@ import { useModalContext } from '@/context/modal-context'
import { uninstallPlugin } from '@/service/plugins'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import ActionButton from '../../base/action-button'
-import Tooltip from '../../base/tooltip'
import { checkForUpdates, fetchReleases } from '../install-plugin/hooks'
import PluginInfo from '../plugin-page/plugin-info'
import { PluginSource } from '../types'
@@ -114,38 +113,59 @@ const Action: FC = ({
finally {
hideDeleting()
}
- }, [installationId, onDelete])
+ }, [hideDeleteConfirm, hideDeleting, installationId, onDelete, showDeleting])
return (
{/* Only plugin installed from GitHub need to check if it's the new version */}
{isShowFetchNewVersion
&& (
-
-
-
-
+
+
+
+
+ )}
+ />
+
+ {t(`${i18nPrefix}.checkForUpdates`, { ns: 'plugin' })}
+
)}
{
isShowInfo
&& (
-
-
-
-
+
+
+
+
+ )}
+ />
+
+ {t(`${i18nPrefix}.pluginInfo`, { ns: 'plugin' })}
+
)
}
{
isShowDelete
&& (
-
-
-
-
+
+
+
+
+ )}
+ />
+
+ {t(`${i18nPrefix}.delete`, { ns: 'plugin' })}
+
)
}
diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx
index 0ee9efc416..c39147e83f 100644
--- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx
+++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx
@@ -63,19 +63,19 @@ vi.mock('@/service/use-plugins', () => ({
}))
// Mock popover component for ToolPicker and StrategyPicker
-let mockPortalOpen = false
-let forcePortalContentVisible = false // Allow tests to force content visibility
-let mockPortalOnOpenChange: ((open: boolean) => void) | undefined
+let mockPopoverOpen = false
+let forcePopoverContentVisible = false // Allow tests to force content visibility
+let mockPopoverOnOpenChange: ((open: boolean) => void) | undefined
vi.mock('@langgenius/dify-ui/popover', () => ({
Popover: ({ children, open = false, onOpenChange }: {
children: React.ReactNode
open?: boolean
onOpenChange?: (open: boolean) => void
}) => {
- mockPortalOpen = open
- mockPortalOnOpenChange = onOpenChange
+ mockPopoverOpen = open
+ mockPopoverOnOpenChange = onOpenChange
return (
- {children}
+ {children}
)
},
PopoverTrigger: ({ children, render, onClick, className }: {
@@ -85,11 +85,11 @@ vi.mock('@langgenius/dify-ui/popover', () => ({
className?: string
}) => (
{
onClick?.(e)
if (!onClick)
- mockPortalOnOpenChange?.(!mockPortalOpen)
+ mockPopoverOnOpenChange?.(!mockPopoverOpen)
}}
className={className}
>
@@ -101,39 +101,9 @@ vi.mock('@langgenius/dify-ui/popover', () => ({
className?: string
popupClassName?: string
}) => {
- if (!mockPortalOpen && !forcePortalContentVisible)
+ if (!mockPopoverOpen && !forcePopoverContentVisible)
return null
- return
{children}
- },
-}))
-
-vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({ children, open = false, onOpenChange }: {
- children: React.ReactNode
- open?: boolean
- onOpenChange?: (open: boolean) => void
- }) => {
- mockPortalOpen = open
- mockPortalOnOpenChange = onOpenChange
- return
{children}
- },
- PortalToFollowElemTrigger: ({ children, onClick, className }: {
- children?: React.ReactNode
- onClick?: (e: React.MouseEvent) => void
- className?: string
- }) => (
-
- {children}
-
- ),
- PortalToFollowElemContent: ({ children, className, popupClassName }: {
- children: React.ReactNode
- className?: string
- popupClassName?: string
- }) => {
- if (!mockPortalOpen && !forcePortalContentVisible)
- return null
- return
{children}
+ return
{children}
},
}))
@@ -362,9 +332,9 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
describe('auto-update-setting', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockPortalOpen = false
- mockPortalOnOpenChange = undefined
- forcePortalContentVisible = false
+ mockPopoverOpen = false
+ mockPopoverOnOpenChange = undefined
+ forcePopoverContentVisible = false
mockPluginsData.plugins = []
})
@@ -928,12 +898,12 @@ describe('auto-update-setting', () => {
render(
)
// Assert
- expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
})
it('should render search box and tabs when isShow is true', () => {
// Arrange
- mockPortalOpen = true
+ mockPopoverOpen = true
// Act
render(
)
@@ -944,7 +914,7 @@ describe('auto-update-setting', () => {
it('should show NoDataPlaceholder when no plugins and no search query', () => {
// Arrange
- mockPortalOpen = true
+ mockPopoverOpen = true
mockPluginsData.plugins = []
// Act
@@ -986,7 +956,7 @@ describe('auto-update-setting', () => {
it('should filter out non-marketplace plugins', () => {
// Arrange
- mockPortalOpen = true
+ mockPopoverOpen = true
// Act
renderWithQueryClient(
)
@@ -997,7 +967,7 @@ describe('auto-update-setting', () => {
it('should filter by search query', () => {
// Arrange
- mockPortalOpen = true
+ mockPopoverOpen = true
// Act
renderWithQueryClient(
)
@@ -1018,7 +988,7 @@ describe('auto-update-setting', () => {
// Act
render(
)
- fireEvent.click(screen.getByTestId('portal-trigger'))
+ fireEvent.click(screen.getByTestId('popover-trigger'))
// Assert
expect(onShowChange).toHaveBeenCalledWith(true)
@@ -1026,7 +996,7 @@ describe('auto-update-setting', () => {
it('should call onChange when plugin is selected', () => {
// Arrange
- mockPortalOpen = true
+ mockPopoverOpen = true
mockPluginsData.plugins = [
createMockPluginDetail({
plugin_id: 'test-plugin',
@@ -1046,7 +1016,7 @@ describe('auto-update-setting', () => {
it('should unselect plugin when already selected', () => {
// Arrange
- mockPortalOpen = true
+ mockPopoverOpen = true
mockPluginsData.plugins = [
createMockPluginDetail({
plugin_id: 'test-plugin',
@@ -1070,7 +1040,7 @@ describe('auto-update-setting', () => {
it('handleCheckChange should be memoized with correct dependencies', () => {
// Arrange
const onChange = vi.fn()
- mockPortalOpen = true
+ mockPopoverOpen = true
mockPluginsData.plugins = [
createMockPluginDetail({
plugin_id: 'plugin-1',
diff --git a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx
index a76fd085ba..458947da6b 100644
--- a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx
+++ b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx
@@ -45,7 +45,7 @@ const PluginVersionPicker: FC
= ({
trigger,
placement = 'bottom-start',
sideOffset = 4,
- alignOffset = -16,
+ alignOffset = 0,
onSelect,
}) => {
const { t } = useTranslation()
diff --git a/web/app/components/rag-pipeline/components/panel/input-field/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/__tests__/index.spec.tsx
index 3146695cae..d362b31057 100644
--- a/web/app/components/rag-pipeline/components/panel/input-field/__tests__/index.spec.tsx
+++ b/web/app/components/rag-pipeline/components/panel/input-field/__tests__/index.spec.tsx
@@ -219,7 +219,7 @@ describe('InputFieldPanel', () => {
it('should render close button', () => {
render( )
- const closeButton = screen.getByRole('button', { name: '' })
+ const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
expect(closeButton)!.toBeInTheDocument()
})
diff --git a/web/app/components/rag-pipeline/components/panel/input-field/index.tsx b/web/app/components/rag-pipeline/components/panel/input-field/index.tsx
index 12eed4ece4..95a76d5e86 100644
--- a/web/app/components/rag-pipeline/components/panel/input-field/index.tsx
+++ b/web/app/components/rag-pipeline/components/panel/input-field/index.tsx
@@ -121,6 +121,7 @@ const InputFieldPanel = () => {
diff --git a/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx
index 7060e29f95..fc417a9528 100644
--- a/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx
+++ b/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx
@@ -113,29 +113,6 @@ vi.mock('@/app/components/tools/labels/selector', () => ({
),
}))
-// Mock PortalToFollowElem for dropdown tests
-let mockPortalOpenState = false
-vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({ children, open, onOpenChange }: { children: React.ReactNode, open: boolean, onOpenChange: (open: boolean) => void }) => {
- mockPortalOpenState = open
- return (
- onOpenChange(!open)}>
- {children}
-
- )
- },
- PortalToFollowElemTrigger: ({ children, onClick, className }: { children: React.ReactNode, onClick: () => void, className?: string }) => (
-
- {children}
-
- ),
- PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => {
- if (!mockPortalOpenState)
- return null
- return {children}
- },
-}))
-
// Test data factories
const createMockEmoji = (overrides = {}) => ({
content: '🔧',
@@ -246,7 +223,6 @@ const createDefaultModalPayload = (overrides: Partial
describe('WorkflowToolConfigureButton', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockPortalOpenState = false
mockIsCurrentWorkspaceManager.mockReturnValue(true)
mockUseWorkflowToolDetailByAppID.mockImplementation((_appId: string, enabled: boolean) => ({
data: enabled ? createMockWorkflowToolDetail() : undefined,
@@ -624,7 +600,6 @@ describe('WorkflowToolConfigureButton', () => {
describe('WorkflowToolAsModal', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockPortalOpenState = false
})
// Rendering Tests (REQUIRED)
@@ -1486,7 +1461,6 @@ describe('WorkflowToolAsModal', () => {
describe('MethodSelector', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockPortalOpenState = false
})
// Rendering Tests (REQUIRED)
@@ -1709,7 +1683,6 @@ describe('MethodSelector', () => {
describe('Integration Tests', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockPortalOpenState = false
mockIsCurrentWorkspaceManager.mockReturnValue(true)
mockUseWorkflowToolDetailByAppID.mockImplementation((_appId: string, enabled: boolean) => ({
data: enabled ? createMockWorkflowToolDetail() : undefined,
diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/__tests__/index.spec.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/__tests__/index.spec.tsx
index af38ca113f..c61882146b 100644
--- a/web/app/components/workflow-app/components/workflow-onboarding-modal/__tests__/index.spec.tsx
+++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/__tests__/index.spec.tsx
@@ -91,18 +91,12 @@ describe('WorkflowOnboardingModal', () => {
expect(getTriggerHeading()).toBeInTheDocument()
})
- it('should render ESC tip when shown', () => {
+ it('should not render ESC tip', () => {
renderComponent({ isShow: true })
- expect(screen.getByText('workflow.onboarding.escTip.press')).toBeInTheDocument()
- expect(screen.getByText('workflow.onboarding.escTip.key')).toBeInTheDocument()
- expect(screen.getByText('workflow.onboarding.escTip.toDismiss')).toBeInTheDocument()
- })
-
- it('should not render ESC tip when hidden', () => {
- renderComponent({ isShow: false })
-
expect(screen.queryByText('workflow.onboarding.escTip.press')).not.toBeInTheDocument()
+ expect(screen.queryByText('workflow.onboarding.escTip.key')).not.toBeInTheDocument()
+ expect(screen.queryByText('workflow.onboarding.escTip.toDismiss')).not.toBeInTheDocument()
})
it('should have correct styling for title', () => {
@@ -386,20 +380,6 @@ describe('WorkflowOnboardingModal', () => {
expect(mockOnClose).toHaveBeenCalledTimes(1)
})
- it('should have visible ESC key hint', () => {
- renderComponent({ isShow: true })
-
- const escKey = screen.getByText('workflow.onboarding.escTip.key')
- expect(escKey.closest('.system-kbd')).toBeInTheDocument()
- })
-
- it('should have descriptive text for ESC functionality', () => {
- renderComponent({ isShow: true })
-
- expect(screen.getByText('workflow.onboarding.escTip.press')).toBeInTheDocument()
- expect(screen.getByText('workflow.onboarding.escTip.toDismiss')).toBeInTheDocument()
- })
-
it('should have proper text color classes', () => {
renderComponent()
@@ -443,8 +423,6 @@ describe('WorkflowOnboardingModal', () => {
expect(dialog).toBeInTheDocument()
expect(screen.getByText('workflow.onboarding.title')).toBeInTheDocument()
expect(getUserInputHeading()).toBeInTheDocument()
- expect(screen.getByText('workflow.onboarding.escTip.key')).toBeInTheDocument()
- expect(dialog).not.toContainElement(screen.getByText('workflow.onboarding.escTip.key'))
})
it('should coordinate between keyboard and click interactions', async () => {
diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
index c72a515925..8f25cd00d0 100644
--- a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
+++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
@@ -1,9 +1,8 @@
'use client'
import type { FC } from 'react'
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
-import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogPortal, DialogTitle } from '@langgenius/dify-ui/dialog'
+import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useTranslation } from 'react-i18next'
-import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import { BlockEnum } from '@/app/components/workflow/types'
import StartNodeSelectionPanel from './start-node-selection-panel'
@@ -44,15 +43,6 @@ const WorkflowOnboardingModal: FC = ({
/>
-
- {/* TODO: reduce z-1002 to match @langgenius/dify-ui primitives after legacy overlay migration completes */}
-
-
- {t('onboarding.escTip.press', { ns: 'workflow' })}
-
- {t('onboarding.escTip.toDismiss', { ns: 'workflow' })}
-
-
)
}
diff --git a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx
index 1ec4ed241f..943c3649ae 100644
--- a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx
@@ -376,15 +376,15 @@ describe('ToolPicker', () => {
renderToolPicker({ onShowChange })
- await user.click(screen.getByRole('button', { name: 'open-picker' }))
- expect(onShowChange).toHaveBeenCalledWith(true)
+ await user.click(screen.getByText('open-picker').closest('[role="button"]')!)
+ expect(onShowChange.mock.calls[0]?.[0]).toBe(true)
renderToolPicker({
disabled: true,
onShowChange: disabledOnShowChange,
})
- await user.click(screen.getAllByRole('button', { name: 'open-picker' })[1]!)
+ await user.click(screen.getAllByText('open-picker')[1]!.closest('[role="button"]')!)
expect(disabledOnShowChange).not.toHaveBeenCalled()
})
diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx
index dcd3392ec6..93ee1882db 100644
--- a/web/app/components/workflow/block-selector/tool-picker.tsx
+++ b/web/app/components/workflow/block-selector/tool-picker.tsx
@@ -1,25 +1,22 @@
'use client'
-import type {
- OffsetOptions,
- Placement,
-} from '@floating-ui/react'
+import type { OffsetOptions } from '@floating-ui/react'
+import type { Placement } from '@langgenius/dify-ui/popover'
import type { FC } from 'react'
import type { ToolDefaultValue, ToolValue } from './types'
import type { CustomCollectionBackend } from '@/app/components/tools/types'
import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
-// eslint-disable-next-line no-restricted-imports -- legacy overlay migration is handled separately from this change
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import AllTools from '@/app/components/workflow/block-selector/all-tools'
@@ -71,6 +68,8 @@ const ToolPicker: FC = ({
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState([])
+ const sideOffset = typeof offset === 'number' ? offset : (typeof offset === 'function' ? 0 : (offset?.mainAxis ?? 0))
+ const alignOffset = typeof offset === 'number' ? 0 : (typeof offset === 'function' ? 0 : (offset?.crossAxis ?? 0))
const { data: enable_marketplace } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
@@ -121,10 +120,10 @@ const ToolPicker: FC = ({
const handleAddedCustomTool = invalidateCustomTools
- const handleTriggerClick = () => {
- if (disabled)
+ const handleOpenChange = (nextOpen: boolean) => {
+ if (nextOpen && disabled)
return
- onShowChange(true)
+ onShowChange(nextOpen)
}
const handleSelect = (_type: BlockEnum, tool?: ToolDefaultValue) => {
@@ -159,19 +158,23 @@ const ToolPicker: FC = ({
}
return (
-
- }
>
{trigger}
-
+
-
+
= ({
}}
/>
-
-
+
+
)
}
diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.branches.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.branches.spec.tsx
index 2e95473bb2..38fa62a728 100644
--- a/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.branches.spec.tsx
+++ b/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.branches.spec.tsx
@@ -1,5 +1,6 @@
import type { ComponentProps } from 'react'
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import type { AppSelectorValue } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
@@ -45,8 +46,8 @@ vi.mock('@/app/components/workflow/hooks', () => ({
}))
vi.mock('@/app/components/plugins/plugin-detail-panel/app-selector', () => ({
- default: ({ onSelect }: { onSelect: (value: string) => void }) => (
-
onSelect('app-1')}>app-selector
+ AppSelector: ({ onSelect }: { onSelect: (value: AppSelectorValue) => void }) => (
+
onSelect({ app_id: 'app-1', inputs: {}, files: [] })}>app-selector
),
}))
@@ -341,7 +342,11 @@ describe('FormInputItem branches', () => {
expect(app.onChange).toHaveBeenCalledWith({
field: {
type: VarKindType.constant,
- value: 'app-1',
+ value: {
+ app_id: 'app-1',
+ inputs: {},
+ files: [],
+ },
},
})
diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/__tests__/index.spec.tsx
index 9521f9b307..392f8e5a2a 100644
--- a/web/app/components/workflow/nodes/_base/components/error-handle/__tests__/index.spec.tsx
+++ b/web/app/components/workflow/nodes/_base/components/error-handle/__tests__/index.spec.tsx
@@ -149,7 +149,7 @@ describe('error-handle path', () => {
await user.click(screen.getByRole('button'))
await user.click(screen.getByText('workflow.nodes.common.errorHandle.defaultValue.title'))
- expect(onSelected).toHaveBeenCalledWith(ErrorHandleTypeEnum.defaultValue)
+ expect(onSelected.mock.calls[0]?.[0]).toBe(ErrorHandleTypeEnum.defaultValue)
})
it('should render the error tip only when a strategy exists', () => {
diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx
index 07b6519e41..c83057a6c0 100644
--- a/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx
+++ b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx
@@ -1,15 +1,16 @@
import { Button } from '@langgenius/dify-ui/button'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import {
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react'
-import { useState } from 'react'
import { useTranslation } from 'react-i18next'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import { ErrorHandleTypeEnum } from './types'
type ErrorHandleTypeSelectorProps = {
@@ -21,7 +22,6 @@ const ErrorHandleTypeSelector = ({
onSelected,
}: ErrorHandleTypeSelectorProps) => {
const { t } = useTranslation()
- const [open, setOpen] = useState(false)
const options = [
{
value: ErrorHandleTypeEnum.none,
@@ -42,37 +42,38 @@ const ErrorHandleTypeSelector = ({
const selectedOption = options.find(option => option.value === value)
return (
-
- {
- e.stopPropagation()
- e.nativeEvent.stopImmediatePropagation()
- setOpen(v => !v)
- }}
+
+ {
+ e.stopPropagation()
+ }}
+ />
+ )}
>
-
+
+
+
- {selectedOption?.label}
-
-
-
-
-
{
options.map(option => (
-
{
e.stopPropagation()
- e.nativeEvent.stopImmediatePropagation()
- onSelected(option.value)
- setOpen(false)
}}
>
@@ -86,12 +87,12 @@ const ErrorHandleTypeSelector = ({
{option.label}
{option.description}
-
+
))
}
-
-
-
+
+
+
)
}
diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx
index 33f3f8fc7d..97c4c284cd 100644
--- a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx
+++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx
@@ -11,7 +11,7 @@ import { useEffect, useMemo, useState } from 'react'
import CheckboxList from '@/app/components/base/checkbox-list'
import Input from '@/app/components/base/input'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
-import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
+import { AppSelector } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx
index 6d3cfe7cfb..14a88b4432 100644
--- a/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx
+++ b/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx
@@ -1,15 +1,15 @@
'use client'
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
-import { RiArrowDownSLine } from '@remixicon/react'
-import * as React from 'react'
-import { useCallback, useState } from 'react'
-import { Check } from '@/app/components/base/icons/src/vender/line/general'
import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectItemIndicator,
+ SelectItemText,
+ SelectTrigger,
+} from '@langgenius/dify-ui/select'
+import * as React from 'react'
import { VarType } from '@/app/components/workflow/types'
type Props = {
@@ -26,47 +26,39 @@ const VarReferencePicker: FC
= ({
value,
onChange,
}) => {
- const [open, setOpen] = useState(false)
-
- const handleChange = useCallback((type: string) => {
- return () => {
- setOpen(false)
- onChange(type)
- }
- }, [onChange])
-
return (
-
- setOpen(!open)} className="w-[120px] cursor-pointer">
-
-
- {
+ if (type)
+ onChange(type)
}}
+ >
+
-
- {TYPES.map(type => (
-
-
{type}
- {type === value &&
}
-
- ))}
-
-
-
+
{value}
+
+
+ {TYPES.map(type => (
+
+ {type}
+
+
+ ))}
+
+
)
}
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx
index 317f4853ac..d85d215923 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx
@@ -1,7 +1,8 @@
import type { FC } from 'react'
import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
import { cn } from '@langgenius/dify-ui/cn'
-import { CreateButtonType, CreateSubscriptionButton } from '@/app/components/plugins/plugin-detail-panel/subscription-list/create'
+import { CreateSubscriptionButton } from '@/app/components/plugins/plugin-detail-panel/subscription-list/create'
+import { CreateButtonType } from '@/app/components/plugins/plugin-detail-panel/subscription-list/create/types'
import { SubscriptionSelectorEntry } from '@/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry'
import { useSubscriptionList } from '@/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list'
diff --git a/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx
index 0ed61eb641..af73cd8581 100644
--- a/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx
+++ b/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx
@@ -264,8 +264,8 @@ describe('if-else path', () => {
await user.click(screen.getByText('Variable'))
fireEvent.change(screen.getByDisplayValue('12'), { target: { value: '42' } })
- expect(onSelect).toHaveBeenCalledWith(ComparisonOperator.is)
- expect(onNumberVarTypeChange).toHaveBeenCalledWith(NumberVarType.variable)
+ expect(onSelect.mock.calls[0]?.[0]).toBe(ComparisonOperator.is)
+ expect(onNumberVarTypeChange.mock.calls[0]?.[0]).toBe(NumberVarType.variable)
expect(onValueChange).toHaveBeenCalledWith('42')
})
diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx
index 83dc97f088..d343b0c8e5 100644
--- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx
+++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx
@@ -2,17 +2,18 @@ import type { ComparisonOperator } from '../../types'
import type { VarType } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import { RiArrowDownSLine } from '@remixicon/react'
import {
useMemo,
- useState,
} from 'react'
import { useTranslation } from 'react-i18next'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import { getOperators, isComparisonOperatorNeedTranslate } from '../../utils'
const i18nPrefix = 'nodes.ifElse'
@@ -34,7 +35,6 @@ const ConditionOperator = ({
onSelect,
}: ConditionOperatorProps) => {
const { t } = useTranslation()
- const [open, setOpen] = useState(false)
const options = useMemo(() => {
return getOperators(varType, file).map((o) => {
@@ -46,49 +46,48 @@ const ConditionOperator = ({
}, [t, varType, file])
const selectedOption = options.find(o => Array.isArray(value) ? o.value === value[0] : o.value === value)
return (
-
- setOpen(v => !v)}>
-
+
+ )}
+ >
+ {
+ selectedOption
+ ? selectedOption.label
+ : t(`${i18nPrefix}.select`, { ns: 'workflow' })
+ }
+
+
+
+
- {
- selectedOption
- ? selectedOption.label
- : t(`${i18nPrefix}.select`, { ns: 'workflow' })
- }
-
-
-
-
-
{
options.map(option => (
-
{
- onSelect(option.value)
- setOpen(false)
- }}
+ value={option.value}
+ closeOnClick
+ className="h-7 rounded-lg px-3 py-1.5 text-[13px] font-medium text-text-secondary"
>
{option.label}
-
+
))
}
-
-
-
+
+
+
)
}
diff --git a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx
index 954f713a78..a17d7589f1 100644
--- a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx
+++ b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx
@@ -4,6 +4,18 @@ import type {
} from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import { RiArrowDownSLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { capitalize } from 'es-toolkit/string'
@@ -14,11 +26,6 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { VarType } from '@/app/components/workflow/types'
import { variableTransformer } from '@/app/components/workflow/utils'
@@ -63,58 +70,61 @@ const ConditionNumberInput = ({
return (
-
- setNumberVarTypeVisible(v => !v)}>
-
+ )}
+ >
+ {capitalize(numberVarType)}
+
+
+
+
- {capitalize(numberVarType)}
-
-
-
-
-
{
options.map(option => (
-
{
- onNumberVarTypeChange(option)
- setNumberVarTypeVisible(false)
- }}
>
{capitalize(option)}
-
+
))
}
-
-
-
+
+
+
{
numberVarType === NumberVarType.variable && (
-
- setVariableSelectorVisible(v => !v)}
+ }
>
{
value && (
@@ -133,16 +143,20 @@ const ConditionNumberInput = ({
)
}
-
-
+
+
-
-
+
+
)
}
{
diff --git a/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/__tests__/selector.spec.tsx b/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/__tests__/selector.spec.tsx
index 617944e4ee..bd2ec4cf53 100644
--- a/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/__tests__/selector.spec.tsx
+++ b/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/__tests__/selector.spec.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { ChunkStructureEnum } from '../../../types'
import Selector from '../selector'
@@ -20,7 +20,7 @@ const options = [
]
describe('ChunkStructureSelector', () => {
- it('should open the selector panel and close it after selecting an option', () => {
+ it('should open the selector panel and close it after selecting an option', async () => {
const onChange = vi.fn()
render(
@@ -38,7 +38,9 @@ describe('ChunkStructureSelector', () => {
fireEvent.click(screen.getByText('Parent child'))
expect(onChange).toHaveBeenCalledWith(ChunkStructureEnum.parent_child)
- expect(screen.queryByText('workflow.nodes.knowledgeBase.changeChunkStructure')).not.toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.queryByText('workflow.nodes.knowledgeBase.changeChunkStructure')).not.toBeInTheDocument()
+ })
})
it('should not open the selector when readonly is enabled', () => {
@@ -51,7 +53,8 @@ describe('ChunkStructureSelector', () => {
/>,
)
- fireEvent.click(screen.getByRole('button', { name: 'custom-trigger' }))
+ const trigger = screen.getByText('custom-trigger').closest('[role="button"]')
+ fireEvent.click(trigger!)
expect(screen.queryByText('workflow.nodes.knowledgeBase.changeChunkStructure')).not.toBeInTheDocument()
})
diff --git a/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/selector.tsx b/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/selector.tsx
index 7ab8de508f..dc38729854 100644
--- a/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/selector.tsx
+++ b/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/selector.tsx
@@ -2,13 +2,13 @@ import type { ReactNode } from 'react'
import type { ChunkStructureEnum } from '../../types'
import type { Option } from './type'
import { Button } from '@langgenius/dify-ui/button'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import OptionCard from '../option-card'
type SelectorProps = {
@@ -32,37 +32,46 @@ const Selector = ({
onChange(optionId)
setOpen(false)
}, [onChange])
+ const handleOpenChange = useCallback((nextOpen: boolean) => {
+ if (readonly && nextOpen)
+ return
+ setOpen(nextOpen)
+ }, [readonly])
return (
-
- {
- if (readonly)
- return
- setOpen(!open)
- }}
+ {
+ trigger
+ ? (
+ }
+ >
+ {trigger}
+
+ )
+ : (
+
+ )}
+ >
+ {t('panel.change', { ns: 'workflow' })}
+
+ )
+ }
+
- {
- trigger || (
-
- {t('panel.change', { ns: 'workflow' })}
-
- )
- }
-
-
{t('nodes.knowledgeBase.changeChunkStructure', { ns: 'workflow' })}
@@ -86,8 +95,8 @@ const Selector = ({
}
-
-
+
+
)
}
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/integration.spec.tsx
index 81943ff86c..ea02cf5980 100644
--- a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/integration.spec.tsx
+++ b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/integration.spec.tsx
@@ -427,7 +427,7 @@ describe('knowledge-retrieval path', () => {
await user.click(screen.getByRole('button', { name: /workflow.nodes.knowledgeRetrieval.metadata.options.disabled.title/i }))
await user.click(screen.getByText('workflow.nodes.knowledgeRetrieval.metadata.options.manual.title'))
- expect(onSelect).toHaveBeenCalledWith(MetadataFilteringModeEnum.manual)
+ expect(onSelect.mock.calls[0]?.[0]).toBe(MetadataFilteringModeEnum.manual)
})
it('should remove stale metadata conditions and open the manual metadata panel', async () => {
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/metadata-filter-selector.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/metadata-filter-selector.tsx
index 396fd069e7..0d2ee187e8 100644
--- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/metadata-filter-selector.tsx
+++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/metadata-filter-selector.tsx
@@ -1,15 +1,16 @@
import { Button } from '@langgenius/dify-ui/button'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import {
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react'
-import { useState } from 'react'
import { useTranslation } from 'react-i18next'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
type MetadataFilterSelectorProps = {
@@ -21,7 +22,6 @@ const MetadataFilterSelector = ({
onSelect,
}: MetadataFilterSelectorProps) => {
const { t } = useTranslation()
- const [open, setOpen] = useState(false)
const options = [
{
key: MetadataFilteringModeEnum.disabled,
@@ -43,41 +43,35 @@ const MetadataFilterSelector = ({
const selectedOption = options.find(option => option.key === value)!
return (
-
- {
- e.stopPropagation()
- setOpen(!open)
- }}
- asChild
+
+ e.stopPropagation()}
+ />
+ )}
>
-
+
+
+
- {selectedOption.value}
-
-
-
-
-
{
options.map(option => (
-
{
- onSelect(option.key)
- setOpen(false)
- }}
+ value={option.key}
+ closeOnClick
+ className="h-auto items-start rounded-lg p-2 pr-3"
>
{
@@ -94,12 +88,12 @@ const MetadataFilterSelector = ({
{option.desc}
-
+
))
}
-
-
-
+
+
+
)
}
diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx
index 5650c65641..986116f6a0 100644
--- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx
+++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx
@@ -232,7 +232,6 @@ const EditCard: FC = ({
currentValue={currentFields.type}
items={maximumDepthReached ? MAXIMUM_DEPTH_TYPE_OPTIONS : TYPE_OPTIONS}
onSelect={handleTypeChange}
- popupClassName="z-1000"
/>
{
currentFields.required && (
diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx
index 213b6895d3..e3f637954d 100644
--- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx
+++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx
@@ -1,9 +1,16 @@
import type { FC } from 'react'
import type { ArrayType, Type } from '../../../../types'
import { cn } from '@langgenius/dify-ui/cn'
-import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectItemIndicator,
+ SelectItemText,
+ SelectTrigger,
+} from '@langgenius/dify-ui/select'
+import { RiCheckLine } from '@remixicon/react'
import { useState } from 'react'
-import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
export type TypeItem = {
value: Type | ArrayType
@@ -26,45 +33,46 @@ const TypeSelector: FC = ({
const [open, setOpen] = useState(false)
return (
-
open={open}
onOpenChange={setOpen}
- placement="bottom-start"
- offset={{
- mainAxis: 4,
+ value={currentValue}
+ onValueChange={(nextValue) => {
+ const selected = items.find(item => item.value === nextValue)
+ if (selected)
+ onSelect(selected)
}}
>
- setOpen(v => !v)}>
-
- {currentValue}
-
-
-
-
-
- {items.map((item) => {
- const isSelected = item.value === currentValue
- return (
-
{
- onSelect(item)
- setOpen(false)
- }}
- >
- {item.text}
- {isSelected && }
-
- )
- })}
-
-
-
+ >
+ {currentValue}
+
+
+ {items.map((item) => {
+ const isSelected = item.value === currentValue
+ return (
+
+ {item.text}
+ {isSelected && }
+
+
+ )
+ })}
+
+
)
}
diff --git a/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx
index a4a00fee67..99c0f9c81d 100644
--- a/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx
+++ b/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx
@@ -353,8 +353,8 @@ describe('loop path', () => {
await user.click(screen.getByText('Variable'))
fireEvent.change(screen.getByDisplayValue('12'), { target: { value: '42' } })
- expect(onSelect).toHaveBeenCalledWith(ComparisonOperator.is)
- expect(onNumberVarTypeChange).toHaveBeenCalledWith(NumberVarType.variable)
+ expect(onSelect.mock.calls[0]?.[0]).toBe(ComparisonOperator.is)
+ expect(onNumberVarTypeChange.mock.calls[0]?.[0]).toBe(NumberVarType.variable)
expect(onValueChange).toHaveBeenCalledWith('42')
})
diff --git a/web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx b/web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx
index 916326c416..d343b0c8e5 100644
--- a/web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx
+++ b/web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx
@@ -2,17 +2,18 @@ import type { ComparisonOperator } from '../../types'
import type { VarType } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import { RiArrowDownSLine } from '@remixicon/react'
import {
useMemo,
- useState,
} from 'react'
import { useTranslation } from 'react-i18next'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import { getOperators, isComparisonOperatorNeedTranslate } from '../../utils'
const i18nPrefix = 'nodes.ifElse'
@@ -34,7 +35,6 @@ const ConditionOperator = ({
onSelect,
}: ConditionOperatorProps) => {
const { t } = useTranslation()
- const [open, setOpen] = useState(false)
const options = useMemo(() => {
return getOperators(varType, file).map((o) => {
@@ -46,49 +46,48 @@ const ConditionOperator = ({
}, [t, varType, file])
const selectedOption = options.find(o => Array.isArray(value) ? o.value === value[0] : o.value === value)
return (
-
- setOpen(v => !v)}>
-
+
+ )}
+ >
+ {
+ selectedOption
+ ? selectedOption.label
+ : t(`${i18nPrefix}.select`, { ns: 'workflow' })
+ }
+
+
+
+
- {
- selectedOption
- ? selectedOption.label
- : t(`${i18nPrefix}.select`, { ns: 'workflow' })
- }
-
-
-
-
-
{
options.map(option => (
-
{
- onSelect(option.value)
- setOpen(false)
- }}
+ value={option.value}
+ closeOnClick
+ className="h-7 rounded-lg px-3 py-1.5 text-[13px] font-medium text-text-secondary"
>
{option.label}
-
+
))
}
-
-
-
+
+
+
)
}
diff --git a/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx b/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx
index 954f713a78..a17d7589f1 100644
--- a/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx
+++ b/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx
@@ -4,6 +4,18 @@ import type {
} from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@langgenius/dify-ui/popover'
import { RiArrowDownSLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { capitalize } from 'es-toolkit/string'
@@ -14,11 +26,6 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { VarType } from '@/app/components/workflow/types'
import { variableTransformer } from '@/app/components/workflow/utils'
@@ -63,58 +70,61 @@ const ConditionNumberInput = ({
return (
-
- setNumberVarTypeVisible(v => !v)}>
-
+ )}
+ >
+ {capitalize(numberVarType)}
+
+
+
+
- {capitalize(numberVarType)}
-
-
-
-
-
{
options.map(option => (
-
{
- onNumberVarTypeChange(option)
- setNumberVarTypeVisible(false)
- }}
>
{capitalize(option)}
-
+
))
}
-
-
-
+
+
+
{
numberVarType === NumberVarType.variable && (
-
- setVariableSelectorVisible(v => !v)}
+ }
>
{
value && (
@@ -133,16 +143,20 @@ const ConditionNumberInput = ({
)
}
-
-
+
+
-
-
+
+
)
}
{
diff --git a/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx b/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx
index 4ccf2b1061..11d7d33295 100644
--- a/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx
+++ b/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx
@@ -467,7 +467,6 @@ describe('InputVarList', () => {
await user.click(screen.getAllByText('app.appSelector.placeholder')[0]!)
await user.click(screen.getAllByText('app.appSelector.placeholder')[1]!)
await user.click(screen.getByTitle('Weather Assistant (app-1)'))
- await user.type(screen.getByPlaceholderText('Topic'), 'weather')
expect(onChange).toHaveBeenNthCalledWith(1, {
assistant: {
@@ -479,6 +478,10 @@ describe('InputVarList', () => {
credential_id: 'credential-1',
},
})
+
+ await user.click(screen.getByRole('combobox', { name: 'app.appSelector.label' }))
+ await user.type(screen.getByPlaceholderText('Topic'), 'weather')
+
expect(onChange).toHaveBeenLastCalledWith({
assistant: {
app_id: 'app-1',
@@ -491,8 +494,8 @@ describe('InputVarList', () => {
})
await user.click(screen.getByText('workflow:errorMsg.configureModel'))
- await user.click(await screen.findByRole('button', { name: 'plugin.detailPanel.configureModel' }))
- await user.click(await screen.findByRole('button', { name: /GPT-4o/i }))
+ await user.click(await screen.findByRole('combobox', { name: 'plugin.detailPanel.configureModel' }))
+ await user.click(await screen.findByRole('option', { name: /GPT-4o/i }))
expect(onChange).toHaveBeenLastCalledWith({
assistant: {
diff --git a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx
index f24b0cfef3..6765c92033 100644
--- a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx
+++ b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx
@@ -12,7 +12,7 @@ import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
-import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
+import { AppSelector } from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/__tests__/item.spec.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/__tests__/item.spec.tsx
index e935085fe9..42478ca34d 100644
--- a/web/app/components/workflow/nodes/tool/components/tool-form/__tests__/item.spec.tsx
+++ b/web/app/components/workflow/nodes/tool/components/tool-form/__tests__/item.spec.tsx
@@ -171,7 +171,7 @@ describe('tool/tool-form/item', () => {
} as unknown as SchemaRoot,
})
- const { container } = render(
+ render(
{
/>,
)
- fireEvent.mouseEnter(container.querySelector('svg')?.parentElement as HTMLElement)
+ const infotipTrigger = screen.getByRole('button', { name: 'Select from tools' })
+ fireEvent.click(infotipTrigger)
expect(screen.getByText('Select from tools'))!.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'JSON Schema' }))
diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx
index 6cd13984f0..1cb3179135 100644
--- a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx
+++ b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx
@@ -9,7 +9,7 @@ import {
RiBracesLine,
} from '@remixicon/react'
import { useBoolean } from 'ahooks'
-import Tooltip from '@/app/components/base/tooltip'
+import { Infotip } from '@/app/components/base/infotip'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { SchemaModal } from '@/app/components/plugins/plugin-detail-panel/tool-selector/components'
@@ -100,15 +100,13 @@ const ToolFormItem: FC = ({
*
)}
{!showDescription && tooltip && (
-
- {tooltip[language] || tooltip.en_US}
-
- )}
- triggerClassName="ml-1 w-4 h-4"
- asChild={false}
- />
+
+ {tooltip[language] || tooltip.en_US}
+
)}
{showSchemaButton && (
<>
diff --git a/web/app/components/workflow/variable-inspect/left.tsx b/web/app/components/workflow/variable-inspect/left.tsx
index 6d590eb281..4cbd6b5f63 100644
--- a/web/app/components/workflow/variable-inspect/left.tsx
+++ b/web/app/components/workflow/variable-inspect/left.tsx
@@ -9,8 +9,6 @@ import { VarInInspectType } from '@/types/workflow'
import useCurrentVars from '../hooks/use-inspect-vars-crud'
import { useNodesInteractions } from '../hooks/use-nodes-interactions'
import { useStore } from '../store'
-// import ActionButton from '@/app/components/base/action-button'
-// import Tooltip from '@/app/components/base/tooltip'
import Group from './group'
type Props = {
diff --git a/web/docs/overlay-migration.md b/web/docs/overlay-migration.md
index 1ab79511f2..4457d9cddf 100644
--- a/web/docs/overlay-migration.md
+++ b/web/docs/overlay-migration.md
@@ -2,12 +2,11 @@
This document tracks the Dify-web migration away from legacy overlay APIs.
-> **See also:** [`packages/dify-ui/README.md`] for the permanent overlay / portal / z-index contract of the replacement primitives. This document covers the one-off migration mechanics (allowlist, deprecated import paths, coexistence z-index strategy) and is expected to shrink and eventually be removed once the legacy overlays are gone.
+> **See also:** [`packages/dify-ui/README.md`] for the permanent overlay / portal / z-index contract of the replacement primitives. This document covers the one-off migration mechanics (deprecated import paths and coexistence z-index strategy) and is expected to shrink and eventually be removed once the legacy overlays are gone.
## Scope
- Deprecated imports:
- - `@/app/components/base/portal-to-follow-elem`
- `@/app/components/base/tooltip`
- `@/app/components/base/modal`
- `@/app/components/base/dialog`
@@ -18,6 +17,8 @@ This document tracks the Dify-web migration away from legacy overlay APIs.
- `@langgenius/dify-ui/popover`
- `@langgenius/dify-ui/dialog`
- `@langgenius/dify-ui/alert-dialog`
+ - `@langgenius/dify-ui/autocomplete`
+ - `@langgenius/dify-ui/combobox`
- `@langgenius/dify-ui/select`
- `@langgenius/dify-ui/toast`
- Tracking issue:
@@ -26,26 +27,18 @@ This document tracks the Dify-web migration away from legacy overlay APIs.
- `no-restricted-imports` blocks all deprecated imports listed above.
- The rule is enabled for normal source files (`.ts` / `.tsx`) and test files are excluded.
-- Legacy `app/components/base/*` callers are temporarily allowlisted in `OVERLAY_MIGRATION_LEGACY_BASE_FILES` (`web/eslint.constants.mjs`).
-- New files must not be added to the allowlist without migration owner approval.
## Migration phases
1. Business/UI features outside `app/components/base/**`
- Migrate old calls to semantic primitives from `@langgenius/dify-ui/*`.
- Keep deprecated imports out of newly touched files.
-1. Legacy base components in allowlist
- - Migrate allowlisted base callers gradually.
- - Remove migrated files from `OVERLAY_MIGRATION_LEGACY_BASE_FILES` immediately.
+1. Legacy base components
+ - Migrate legacy base callers gradually.
+ - Keep deprecated imports out of newly touched files.
1. Cleanup
- - Remove remaining allowlist entries.
- Remove legacy overlay implementations when import count reaches zero.
-## Allowlist maintenance
-
-- If a migrated file was in the allowlist, remove it from `web/eslint.constants.mjs` in the same PR.
-- Never increase allowlist scope to bypass new code.
-
## z-index strategy
All new overlay primitives in `@langgenius/dify-ui/*` share a single z-index value:
@@ -56,13 +49,12 @@ All new overlay primitives in `@langgenius/dify-ui/*` share a single z-index val
During the migration period, legacy and new overlays coexist. Legacy overlays
portal to `document.body` with explicit z-index values:
-| Layer | z-index | Components |
-| --------------------------------- | -------------- | -------------------------------------------------------- |
-| Legacy Drawer | `z-30` | `base/drawer` |
-| Legacy Modal | `z-60` | `base/modal` (default) |
-| Legacy PortalToFollowElem callers | up to `z-1001` | various business components |
-| **New UI primitives** | **`z-1002`** | `@langgenius/dify-ui/*` (Popover, Dialog, Tooltip, etc.) |
-| Toast | `z-1003` | `@langgenius/dify-ui/toast` |
+| Layer | z-index | Components |
+| --------------------- | ------------ | -------------------------------------------------------------------------------- |
+| Legacy Drawer | `z-30` | `base/drawer` |
+| Legacy Modal | `z-60` | `base/modal` (default) |
+| **New UI primitives** | **`z-1002`** | `@langgenius/dify-ui/*` (Popover, Dialog, Autocomplete, Combobox, Tooltip, etc.) |
+| Toast | `z-1003` | `@langgenius/dify-ui/toast` |
`z-1002` sits above all common legacy overlays, so new primitives always
render on top without needing per-call-site z-index hacks. Among themselves,
@@ -80,8 +72,6 @@ back to `z-9999`.
parent legacy overlay should be migrated instead.
- When migrating a legacy overlay that has a high z-index, remove the z-index
entirely — the new primitive's default `z-1002` handles it.
-- `portalToFollowElemContentClassName` with z-index values (e.g. `z-1000`)
- should be deleted when the surrounding legacy container is migrated.
### Post-migration cleanup
diff --git a/web/docs/test.md b/web/docs/test.md
index 836ab7ad56..b7c6a5f5a3 100644
--- a/web/docs/test.md
+++ b/web/docs/test.md
@@ -88,7 +88,7 @@ Use `pnpm analyze-component ` to analyze component complexity and adopt di
**Rules**:
1. **Match actual conditional rendering**: If the real component returns `null` or doesn't render under certain conditions, the mock must do the same. Always check the actual component implementation before creating mocks.
-1. **Use shared state variables when needed**: When mocking components that depend on shared context or state (e.g., `PortalToFollowElem` with `PortalToFollowElemContent`), use module-level variables to track state and reset them in `beforeEach`.
+1. **Use shared state variables when needed**: When mocking components that depend on shared context or state (for example, a parent overlay mock with a separate content component), use module-level variables to track state and reset them in `beforeEach`.
1. **Always reset shared mock state in beforeEach**: Module-level variables used in mocks must be reset in `beforeEach` to ensure test isolation, even if you set default values elsewhere.
1. **Use fake timers only when needed**: Only use `vi.useFakeTimers()` if:
- Testing components that use real `setTimeout`/`setInterval` (not mocked)
@@ -377,16 +377,16 @@ describe('ComponentName', () => {
```tsx
// ✅ CORRECT: Matches actual component behavior
-let mockPortalOpenState = false
+let mockOverlayOpenState = false
-vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({ children, open, ...props }) => {
- mockPortalOpenState = open || false // Update shared state
+vi.mock('external-overlay-library', () => ({
+ OverlayRoot: ({ children, open, ...props }) => {
+ mockOverlayOpenState = open || false // Update shared state
return {children}
},
- PortalToFollowElemContent: ({ children }) => {
+ OverlayContent: ({ children }) => {
// ✅ Matches actual: returns null when open is false
- if (!mockPortalOpenState)
+ if (!mockOverlayOpenState)
return null
return {children}
},
@@ -395,7 +395,7 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
describe('Component', () => {
beforeEach(() => {
vi.clearAllMocks() // ✅ Reset mock call history
- mockPortalOpenState = false // ✅ Reset shared state
+ mockOverlayOpenState = false // ✅ Reset shared state
})
})
```
diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs
index 2a043ac73d..864b3fa960 100644
--- a/web/eslint.config.mjs
+++ b/web/eslint.config.mjs
@@ -11,9 +11,9 @@ import noBarrelFiles from 'eslint-plugin-no-barrel-files'
import sonar from 'eslint-plugin-sonarjs'
import storybook from 'eslint-plugin-storybook'
import {
+ GENERATED_IGNORES,
HYOBAN_PREFER_TAILWIND_ICONS_OPTIONS,
NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS,
- OVERLAY_MIGRATION_LEGACY_BASE_FILES,
OVERLAY_RESTRICTED_IMPORT_PATTERNS,
WEB_RESTRICTED_IMPORT_PATTERNS,
} from './eslint.constants.mjs'
@@ -27,7 +27,7 @@ export default antfu(
'react/no-unnecessary-use-prefix': 'error',
},
},
- ignores: ['public', 'types/doc-paths.ts', 'eslint-suppressions.json'],
+ ignores: ['public', 'types/doc-paths.ts', 'eslint-suppressions.json', ...GENERATED_IGNORES],
typescript: {
overrides: {
'ts/consistent-type-definitions': ['error', 'type'],
@@ -177,7 +177,6 @@ export default antfu(
ignores: [
'next/**',
...GLOB_TESTS,
- ...OVERLAY_MIGRATION_LEGACY_BASE_FILES,
],
rules: {
'no-restricted-imports': ['error', {
diff --git a/web/eslint.constants.mjs b/web/eslint.constants.mjs
index 240d3ab06c..a55213ab49 100644
--- a/web/eslint.constants.mjs
+++ b/web/eslint.constants.mjs
@@ -1,3 +1,12 @@
+export const GENERATED_IGNORES = [
+ 'storybook-static/',
+ '.next/',
+ 'next/',
+ 'next-env.d.ts',
+ 'dist/',
+ 'coverage/',
+]
+
export const NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS = [
{
name: 'next',
@@ -36,13 +45,6 @@ export const WEB_RESTRICTED_IMPORT_PATTERNS = [
]
export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [
- {
- group: [
- '**/portal-to-follow-elem',
- '**/portal-to-follow-elem/index',
- ],
- message: 'Deprecated: use semantic overlay primitives from @langgenius/dify-ui (popover / dropdown-menu / tooltip / context-menu) instead. See issue #32767.',
- },
{
group: [
'**/base/tooltip',
@@ -54,7 +56,6 @@ export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [
group: [
'**/base/modal',
'**/base/modal/index',
- '**/base/modal/modal',
],
message: 'Deprecated: use @langgenius/dify-ui/dialog instead. See issue #32767.',
},
@@ -67,15 +68,6 @@ export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [
},
]
-export const OVERLAY_MIGRATION_LEGACY_BASE_FILES = [
- 'app/components/base/chat/chat/citation/progress-tooltip.tsx',
- 'app/components/base/chat/chat/citation/tooltip.tsx',
- 'app/components/base/chip/index.tsx',
- 'app/components/base/modal/modal.tsx',
- 'app/components/base/sort/index.tsx',
- 'app/components/base/tooltip/index.tsx',
-]
-
export const HYOBAN_PREFER_TAILWIND_ICONS_OPTIONS = {
prefix: 'i-',
propMappings: {
diff --git a/web/features/tag-management/__tests__/dataset-card-tags.spec.tsx b/web/features/tag-management/__tests__/dataset-card-tags.spec.tsx
index b79e16d5e9..7c950becc3 100644
--- a/web/features/tag-management/__tests__/dataset-card-tags.spec.tsx
+++ b/web/features/tag-management/__tests__/dataset-card-tags.spec.tsx
@@ -3,17 +3,15 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { DatasetCardTags } from '../components/dataset-card-tags'
-// Mock TagSelector as it's a complex component from base
vi.mock('@/features/tag-management/components/tag-selector', () => ({
- TagSelector: ({ selectedTagIds, selectedTags, onOpenTagManagement }: {
- selectedTagIds: string[]
- selectedTags: Tag[]
+ TagSelector: ({ value, onOpenTagManagement }: {
+ value: Tag[]
onOpenTagManagement?: () => void
}) => (
-
{selectedTagIds.join(',')}
+
{value.map(tag => tag.id).join(',')}
- {selectedTags.length}
+ {value.length}
{' '}
tags
@@ -75,7 +73,9 @@ describe('DatasetCardTags', () => {
const onClick = vi.fn()
const { container } = render(
)
- const wrapper = container.firstChild as HTMLElement
+ const wrapper = container.firstElementChild
+ if (!wrapper)
+ throw new Error('Expected dataset card tag wrapper')
fireEvent.click(wrapper)
expect(onClick).toHaveBeenCalledTimes(1)
@@ -94,13 +94,17 @@ describe('DatasetCardTags', () => {
describe('Styles', () => {
it('should have opacity class when embedding is not available', () => {
const { container } = render(
)
- const wrapper = container.firstChild as HTMLElement
+ const wrapper = container.firstElementChild
+ if (!wrapper)
+ throw new Error('Expected dataset card tag wrapper')
expect(wrapper).toHaveClass('opacity-30')
})
it('should not have opacity class when embedding is available', () => {
const { container } = render(
)
- const wrapper = container.firstChild as HTMLElement
+ const wrapper = container.firstElementChild
+ if (!wrapper)
+ throw new Error('Expected dataset card tag wrapper')
expect(wrapper).not.toHaveClass('opacity-30')
})
@@ -109,6 +113,7 @@ describe('DatasetCardTags', () => {
const maskDiv = container.querySelector('.bg-tag-selector-mask-bg')
expect(maskDiv).toBeInTheDocument()
expect(maskDiv).toHaveClass('group-hover/tag-area:hidden')
+ expect(maskDiv).toHaveClass('group-focus-within/tag-area:hidden')
expect(maskDiv).toHaveClass('group-hover:bg-tag-selector-mask-hover-bg')
})
@@ -139,10 +144,10 @@ describe('DatasetCardTags', () => {
})
it('should handle many tags', () => {
- const manyTags: Tag[] = Array.from({ length: 20 }, (_, i) => ({
+ const manyTags: Tag[] = Array.from({ length: 20 }, (_, i): Tag => ({
id: `tag-${i}`,
name: `Tag ${i}`,
- type: 'knowledge' as const,
+ type: 'knowledge',
binding_count: 0,
}))
render(
)
diff --git a/web/features/tag-management/__tests__/tag-filter.spec.tsx b/web/features/tag-management/__tests__/tag-filter.spec.tsx
index a00c9ac93d..933f55683b 100644
--- a/web/features/tag-management/__tests__/tag-filter.spec.tsx
+++ b/web/features/tag-management/__tests__/tag-filter.spec.tsx
@@ -27,6 +27,8 @@ const defaultProps = {
// Helper: the i18n mock renders "ns.key" format (dot-separated)
const i18n = {
placeholder: 'common.tag.placeholder',
+ selectorPlaceholder: 'common.tag.selectorPlaceholder',
+ operationClear: 'common.operation.clear',
noTag: 'common.tag.noTag',
manageTags: 'common.tag.manageTags',
}
@@ -101,6 +103,19 @@ describe('TagFilter', () => {
expect(onChange).toHaveBeenCalledWith(['tag-1'])
})
+ it('should select the highlighted tag with keyboard navigation', async () => {
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+ render(
)
+
+ await user.click(screen.getByText(i18n.placeholder))
+ await user.type(screen.getByRole('combobox', { name: i18n.selectorPlaceholder }), 'Back')
+ await user.keyboard('{ArrowDown}')
+ await user.keyboard('{Enter}')
+
+ expect(onChange).toHaveBeenCalledWith(['tag-2'])
+ })
+
it('should call onChange to deselect when an already-selected tag is clicked', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
@@ -158,11 +173,9 @@ describe('TagFilter', () => {
await user.click(screen.getByText('Frontend'))
// The Check icon should be rendered for the selected tag
- const tagItem = screen.getByTitle('Frontend')
+ const tagItem = screen.getByRole('option', { name: /Frontend/i })
expect(tagItem).toBeInTheDocument()
- // The parent container of the tag has a Check SVG sibling
- const checkIcons = screen.getAllByTestId('tag-filter-selected-icon')
- expect(checkIcons?.length).toBeGreaterThanOrEqual(1)
+ expect(tagItem).toHaveAttribute('aria-selected', 'true')
})
it('should clear all selected tags when clear button is clicked', async () => {
@@ -197,7 +210,7 @@ describe('TagFilter', () => {
await user.click(screen.getByText(i18n.placeholder))
- const searchInput = screen.getByRole('textbox')
+ const searchInput = screen.getByRole('combobox', { name: i18n.selectorPlaceholder })
await user.type(searchInput, 'Front')
expect(screen.getByText('Frontend')).toBeInTheDocument()
@@ -212,7 +225,7 @@ describe('TagFilter', () => {
await user.click(screen.getByText(i18n.placeholder))
- const searchInput = screen.getByRole('textbox')
+ const searchInput = screen.getByRole('combobox', { name: i18n.selectorPlaceholder })
await user.type(searchInput, 'NonExistentTag')
expect(screen.getByText(i18n.noTag)).toBeInTheDocument()
@@ -225,12 +238,12 @@ describe('TagFilter', () => {
await user.click(screen.getByText(i18n.placeholder))
- const searchInput = screen.getByRole('textbox')
+ const searchInput = screen.getByRole('combobox', { name: i18n.selectorPlaceholder })
await user.type(searchInput, 'Front')
expect(screen.queryByText('Backend')).not.toBeInTheDocument()
- const clearButton = screen.getByTestId('input-clear')
+ const clearButton = screen.getByRole('button', { name: i18n.operationClear })
await user.click(clearButton)
expect(searchInput).toHaveValue('')
diff --git a/web/features/tag-management/__tests__/tag-panel.spec.tsx b/web/features/tag-management/__tests__/tag-panel.spec.tsx
index 1f1d0a241b..65b2f0c285 100644
--- a/web/features/tag-management/__tests__/tag-panel.spec.tsx
+++ b/web/features/tag-management/__tests__/tag-panel.spec.tsx
@@ -1,82 +1,22 @@
-import type { Tag } from '@/contract/console/tags'
-import { render, screen, waitFor, within } from '@testing-library/react'
+import type { TagComboboxItem } from '../components/tag-combobox-item'
+import type { Tag, TagType } from '@/contract/console/tags'
+import { Combobox } from '@langgenius/dify-ui/combobox'
+import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
-import { act } from 'react'
-import * as ReactI18next from 'react-i18next'
+import { useMemo, useState } from 'react'
+import { isCreateTagOption } from '../components/tag-combobox-item'
import { TagPanel } from '../components/tag-panel'
-const { mockNotify, mockToast } = vi.hoisted(() => {
- const mockNotify = vi.fn()
- const mockToast = Object.assign(mockNotify, {
- success: vi.fn((message, options) => mockNotify({ type: 'success', message, ...options })),
- error: vi.fn((message, options) => mockNotify({ type: 'error', message, ...options })),
- warning: vi.fn((message, options) => mockNotify({ type: 'warning', message, ...options })),
- info: vi.fn((message, options) => mockNotify({ type: 'info', message, ...options })),
- dismiss: vi.fn(),
- update: vi.fn(),
- promise: vi.fn(),
- })
- return { mockNotify, mockToast }
-})
-
-vi.mock('@langgenius/dify-ui/toast', () => ({
- toast: mockToast,
+const { onValueChangeSpy } = vi.hoisted(() => ({
+ onValueChangeSpy: vi.fn(),
}))
-// Hoisted mocks
-const { createTag, bindTag, unBindTag } = vi.hoisted(() => ({
- createTag: vi.fn(),
- bindTag: vi.fn(),
- unBindTag: vi.fn(),
-}))
-
-vi.mock('../hooks/use-tag-mutations', () => ({
- useCreateTagMutation: () => {
- const mutation = {
- isPending: false,
- mutate: ({ body }: { body: { name: string, type: 'app' | 'knowledge' } }, options?: { onSuccess?: (tag: Tag) => void, onError?: () => void }) => {
- mutation.isPending = true
- const tag = { id: 'new-tag', name: body.name, type: body.type, binding_count: 0 } as Tag
- Promise.resolve(createTag(body.name, body.type))
- .then(() => options?.onSuccess?.(tag))
- .catch(() => options?.onError?.())
- .finally(() => {
- mutation.isPending = false
- })
- },
- }
- return mutation
- },
- useApplyTagBindingsMutation: () => ({
- mutate: (
- { currentTagIds, nextTagIds, targetId, type }: { currentTagIds: string[], nextTagIds: string[], targetId: string, type: 'app' | 'knowledge' },
- options?: { onSuccess?: () => void, onError?: () => void },
- ) => {
- const addTagIds = nextTagIds.filter(tagId => !currentTagIds.includes(tagId))
- const removeTagIds = currentTagIds.filter(tagId => !nextTagIds.includes(tagId))
- const operations: Promise
[] = []
-
- if (addTagIds.length)
- operations.push(Promise.resolve(bindTag(addTagIds, targetId, type)))
- operations.push(...removeTagIds.map(tagId => Promise.resolve(unBindTag(tagId, targetId, type))))
-
- Promise.all(operations)
- .then(() => options?.onSuccess?.())
- .catch(() => options?.onError?.())
- },
- }),
-}))
-
-// i18n mock renders "ns.key" format (dot-separated)
const i18n = {
selectorPlaceholder: 'common.tag.selectorPlaceholder',
+ operationClear: 'common.operation.clear',
create: 'common.tag.create',
- created: 'common.tag.created',
- failed: 'common.tag.failed',
noTag: 'common.tag.noTag',
manageTags: 'common.tag.manageTags',
- modifiedSuccessfully: 'common.actionMsg.modifiedSuccessfully',
- modifiedUnsuccessfully: 'common.actionMsg.modifiedUnsuccessfully',
}
const appTags: Tag[] = [
@@ -87,461 +27,171 @@ const appTags: Tag[] = [
const knowledgeTag: Tag = { id: 'tag-k1', name: 'KnowledgeDB', type: 'knowledge', binding_count: 2 }
-const defaultProps = {
- targetId: 'target-1',
- type: 'app' as const,
- selectedTagIds: ['tag-1'!], // tag-1 is already selected/bound
- selectedTags: [appTags[0]!], // pre-selected tags shown separately
- tagList: [...appTags, knowledgeTag],
+type PanelHarnessProps = {
+ type?: TagType
+ value?: Tag[]
+ tagList?: Tag[]
+ onOpenTagManagement?: () => void
}
-describe('Panel', () => {
+const tagToString = (tag: TagComboboxItem) => tag.name
+const isSameTag = (item: TagComboboxItem, value: TagComboboxItem) => item.id === value.id
+const tagFilter = (tag: TagComboboxItem, query: string) => tag.name.includes(query)
+
+const PanelHarness = ({
+ type = 'app',
+ value = [appTags[0]!],
+ tagList = [...appTags, knowledgeTag],
+ onOpenTagManagement,
+}: PanelHarnessProps) => {
+ const [selectedTags, setSelectedTags] = useState(value)
+ const [inputValue, setInputValue] = useState('')
+ const items = useMemo(() => {
+ const tags = tagList.filter(tag => tag.type === type)
+
+ if (!inputValue || tags.some(tag => tag.name === inputValue))
+ return tags
+
+ return [{
+ id: `__create_tag__:${inputValue}`,
+ name: inputValue,
+ type,
+ binding_count: 0,
+ isCreateOption: true,
+ }, ...tags]
+ }, [inputValue, tagList, type])
+
+ return (
+ {
+ onValueChangeSpy(nextTags)
+ if (nextTags.some(isCreateTagOption))
+ return
+ setSelectedTags(nextTags)
+ }}
+ inputValue={inputValue}
+ onInputValueChange={setInputValue}
+ filter={tagFilter}
+ itemToStringLabel={tagToString}
+ isItemEqualToValue={isSameTag}
+ >
+
+
+ )
+}
+
+describe('TagPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
- vi.mocked(createTag).mockResolvedValue({ id: 'new-tag', name: 'NewTag', type: 'app', binding_count: 0 })
- vi.mocked(bindTag).mockResolvedValue(undefined)
- vi.mocked(unBindTag).mockResolvedValue(undefined)
})
- describe('Rendering', () => {
- it('should render without crashing', () => {
- render( )
- expect(screen.getByPlaceholderText(i18n.selectorPlaceholder))!.toBeInTheDocument()
- })
+ it('renders search, selected tags, unselected tags, and management action', () => {
+ render( )
- it('should render the search input', () => {
- render( )
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- expect(input)!.toBeInTheDocument()
- expect(input.tagName).toBe('INPUT')
- })
-
- it('should fallback to empty placeholder when translation is empty', () => {
- const mockedTranslation = {
- t: vi.fn().mockReturnValue(''),
- i18n: {} as ReturnType['i18n'],
- ready: true,
- } as unknown as ReturnType
-
- vi.spyOn(ReactI18next, 'useTranslation').mockReturnValueOnce(mockedTranslation)
-
- render( )
-
- expect(screen.getByRole('textbox'))!.toHaveAttribute('placeholder', '')
- })
-
- it('should render selected tags from selectedTags prop', () => {
- render( )
- expect(screen.getByText('Frontend'))!.toBeInTheDocument()
- })
-
- it('should render unselected tags matching the type', () => {
- render( )
- // tag-2 and tag-3 are app type and not in value[]
- // tag-2 and tag-3 are app type and not in value[]
- expect(screen.getByText('Backend'))!.toBeInTheDocument()
- expect(screen.getByText('API'))!.toBeInTheDocument()
- })
-
- it('should not render tags of a different type', () => {
- render( )
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- // knowledgeTag is type 'knowledge', should not appear
- expect(screen.queryByText('KnowledgeDB')).not.toBeInTheDocument()
- })
-
- it('should render the manage tags button', () => {
- render( )
- expect(screen.getByText(i18n.manageTags))!.toBeInTheDocument()
- })
-
- it('should show no-tag message when there are no tags', () => {
- render( )
- expect(screen.getByText(i18n.noTag))!.toBeInTheDocument()
- })
-
- it('should not show no-tag message when tags exist', () => {
- render( )
- expect(screen.queryByText(i18n.noTag)).not.toBeInTheDocument()
- })
+ expect(screen.getByRole('combobox', { name: i18n.selectorPlaceholder })).toBeInTheDocument()
+ expect(screen.getByRole('option', { name: /Frontend/i })).toBeInTheDocument()
+ expect(screen.getByRole('option', { name: /Backend/i })).toBeInTheDocument()
+ expect(screen.queryByText('KnowledgeDB')).not.toBeInTheDocument()
+ expect(screen.getByRole('button', { name: i18n.manageTags })).toBeInTheDocument()
})
- describe('Search / Filter', () => {
- it('should filter tags by keyword', async () => {
- const user = userEvent.setup()
- render( )
+ it('filters options by the controlled combobox input value', async () => {
+ const user = userEvent.setup()
+ render( )
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'Back')
+ await user.type(screen.getByRole('combobox', { name: i18n.selectorPlaceholder }), 'Back')
- expect(screen.getByText('Backend'))!.toBeInTheDocument()
- expect(screen.queryByText('API')).not.toBeInTheDocument()
- })
-
- it('should filter selected tags by keyword', async () => {
- const user = userEvent.setup()
- render( )
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'Front')
-
- expect(screen.getByText('Frontend'))!.toBeInTheDocument()
- expect(screen.queryByText('Backend')).not.toBeInTheDocument()
- })
-
- it('should show create option when keyword does not match any tag', async () => {
- const user = userEvent.setup()
- // notExisted uses .every(tag => tag.type === type && tag.name !== keywords)
- // so store must only contain same-type tags for notExisted to be true
- render( )
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'BrandNewTag')
-
- // The create row shows "Create 'BrandNewTag'"
- // The create row shows "Create 'BrandNewTag'"
- expect(screen.getByText(/BrandNewTag/))!.toBeInTheDocument()
- expect(screen.getByText(i18n.create, { exact: false }))!.toBeInTheDocument()
- })
-
- it('should not show create option when keyword matches an existing tag name', async () => {
- const user = userEvent.setup()
- // Use only same-type tags so we can verify name matching specifically
- render( )
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'Frontend')
-
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- // 'Frontend' matches tag-1 name, so notExisted = false
- expect(screen.queryByText(i18n.create, { exact: false })).not.toBeInTheDocument()
- })
-
- it('should clear search when clear button is clicked', async () => {
- const user = userEvent.setup()
- render( )
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'Back')
- expect(input)!.toHaveValue('Back')
-
- // The Input component renders a clear icon with data-testid="input-clear"
- const clearButton = screen.getByTestId('input-clear')
- await user.click(clearButton)
-
- expect(input)!.toHaveValue('')
- // All tags should be visible again
- // All tags should be visible again
- expect(screen.getByText('Backend'))!.toBeInTheDocument()
- expect(screen.getByText('API'))!.toBeInTheDocument()
- })
+ expect(screen.getByRole('option', { name: /Backend/i })).toBeInTheDocument()
+ expect(screen.queryByRole('option', { name: /API/i })).not.toBeInTheDocument()
})
- describe('Tag Selection', () => {
- const getTagRow = (tagName: string) => {
- const row = screen.getByText(tagName).closest('[data-testid="tag-row"]')
- expect(row).not.toBeNull()
- return row as HTMLElement
- }
+ it('clears only the search input from the input clear button', async () => {
+ const user = userEvent.setup()
+ render( )
- it('should select an unselected tag when clicked', async () => {
- const user = userEvent.setup()
- render( )
+ const input = screen.getByRole('combobox', { name: i18n.selectorPlaceholder })
+ await user.type(input, 'Back')
+ expect(input).toHaveValue('Back')
+ vi.clearAllMocks()
- const backendRowBeforeSelect = getTagRow('Backend')
- expect(within(backendRowBeforeSelect).queryByTestId('check-icon-tag-2')).not.toBeInTheDocument()
+ await user.click(screen.getByRole('button', { name: i18n.operationClear }))
- await user.click(screen.getByText('Backend'))
-
- const backendRowAfterSelect = getTagRow('Backend')
- expect(within(backendRowAfterSelect).getByTestId('check-icon-tag-2'))!.toBeInTheDocument()
- })
-
- it('should deselect a selected tag when clicked', async () => {
- const user = userEvent.setup()
- render( )
-
- const frontendRowBeforeDeselect = getTagRow('Frontend')
- expect(within(frontendRowBeforeDeselect).getByTestId('check-icon-tag-1'))!.toBeInTheDocument()
-
- await user.click(screen.getByText('Frontend'))
-
- const frontendRowAfterDeselect = getTagRow('Frontend')
- expect(within(frontendRowAfterDeselect).queryByTestId('check-icon-tag-1')).not.toBeInTheDocument()
- })
-
- it('should toggle tag selection on multiple clicks', async () => {
- const user = userEvent.setup()
- render( )
-
- const backendRowBeforeToggle = getTagRow('Backend')
- expect(within(backendRowBeforeToggle).queryByTestId('check-icon-tag-2')).not.toBeInTheDocument()
-
- await user.click(screen.getByText('Backend'))
-
- const backendRowAfterFirstClick = getTagRow('Backend')
- expect(within(backendRowAfterFirstClick).getByTestId('check-icon-tag-2'))!.toBeInTheDocument()
-
- await user.click(screen.getByText('Backend'))
-
- const backendRowAfterSecondClick = getTagRow('Backend')
- expect(within(backendRowAfterSecondClick).queryByTestId('check-icon-tag-2')).not.toBeInTheDocument()
- })
+ expect(input).toHaveValue('')
+ expect(onValueChangeSpy).not.toHaveBeenCalled()
+ expect(screen.getByRole('option', { name: /Frontend/i })).toHaveAttribute('aria-selected', 'true')
})
- describe('Tag Creation', () => {
- beforeEach(() => {
- // notExisted requires all tags to be same type, so remove knowledgeTag
- })
+ it('shows a create option when the query is not an existing tag name', async () => {
+ const user = userEvent.setup()
+ render( )
- it('should create a new tag when clicking the create option', async () => {
- const user = userEvent.setup()
- render( )
+ await user.type(screen.getByRole('combobox', { name: i18n.selectorPlaceholder }), 'BrandNewTag')
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'BrandNewTag')
-
- const createOption = await screen.findByTestId('create-tag-option')
- await user.click(createOption)
-
- await waitFor(() => {
- expect(createTag).toHaveBeenCalledWith('BrandNewTag', 'app')
- })
- })
-
- it('should show success notification after tag creation', async () => {
- const user = userEvent.setup()
- render( )
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'BrandNewTag')
-
- const createOption = await screen.findByTestId('create-tag-option')
- await user.click(createOption)
-
- await waitFor(() => {
- expect(mockNotify).toHaveBeenCalledWith({
- type: 'success',
- message: i18n.created,
- })
- })
- })
-
- it('should clear keywords after successful tag creation', async () => {
- const user = userEvent.setup()
- render( )
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'BrandNewTag')
-
- const createOption = await screen.findByTestId('create-tag-option')
- await user.click(createOption)
-
- await waitFor(() => {
- expect(input)!.toHaveValue('')
- })
- })
-
- it('should show error notification when tag creation fails', async () => {
- const user = userEvent.setup()
- vi.mocked(createTag).mockRejectedValue(new Error('Creation failed'))
-
- render( )
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'FailTag')
-
- const createOption = await screen.findByTestId('create-tag-option')
- await user.click(createOption)
-
- await waitFor(() => {
- expect(mockNotify).toHaveBeenCalledWith({
- type: 'error',
- message: i18n.failed,
- })
- })
- })
-
- it('should not create tag when keywords is empty', () => {
- render( )
-
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- // The create option should not appear when no keywords
- expect(screen.queryByText(i18n.create, { exact: false })).not.toBeInTheDocument()
- expect(createTag).not.toHaveBeenCalled()
- })
+ expect(screen.getByTestId('create-tag-option')).toHaveTextContent(i18n.create)
+ expect(screen.getByTestId('create-tag-option')).toHaveTextContent('BrandNewTag')
})
- describe('Binding Selection State', () => {
- it('should not submit tag bindings on panel unmount', async () => {
- const user = userEvent.setup()
- const { unmount } = render( )
+ it('does not show a create option for an exact existing tag name', async () => {
+ const user = userEvent.setup()
+ render( )
- await user.click(screen.getByText('Backend'))
- unmount()
+ await user.type(screen.getByRole('combobox', { name: i18n.selectorPlaceholder }), 'Frontend')
- await act(async () => { })
- expect(bindTag).not.toHaveBeenCalled()
- expect(unBindTag).not.toHaveBeenCalled()
- expect(mockNotify).not.toHaveBeenCalled()
- })
+ expect(screen.queryByTestId('create-tag-option')).not.toBeInTheDocument()
})
- describe('Manage Tags Modal', () => {
- it('should open the tag management modal when manage tags is clicked', async () => {
- const user = userEvent.setup()
- const onOpenTagManagement = vi.fn()
- render( )
+ it('updates only the combobox draft value when selecting and deselecting options', async () => {
+ const user = userEvent.setup()
+ render( )
- await user.click(screen.getByText(i18n.manageTags))
+ await user.click(screen.getByRole('option', { name: /Backend/i }))
+ expect(onValueChangeSpy).toHaveBeenLastCalledWith(expect.arrayContaining([expect.objectContaining({ id: 'tag-2' })]))
- expect(onOpenTagManagement).toHaveBeenCalledTimes(1)
- })
+ await user.click(screen.getByRole('option', { name: /Backend/i }))
+ expect(onValueChangeSpy).toHaveBeenLastCalledWith([expect.objectContaining({ id: 'tag-1' })])
})
- describe('Edge Cases', () => {
- it('should handle empty value array', () => {
- render( )
- // All app-type tags should appear in the unselected list
- // All app-type tags should appear in the unselected list
- expect(screen.getByText('Frontend'))!.toBeInTheDocument()
- expect(screen.getByText('Backend'))!.toBeInTheDocument()
- expect(screen.getByText('API'))!.toBeInTheDocument()
- })
+ it('routes create option activation through the combobox value change API', async () => {
+ const user = userEvent.setup()
+ render( )
- it('should handle empty tagList', () => {
- render( )
- expect(screen.getByText(i18n.noTag))!.toBeInTheDocument()
- })
+ const input = screen.getByRole('combobox', { name: i18n.selectorPlaceholder })
+ await user.type(input, 'BrandNewTag')
+ await user.click(screen.getByTestId('create-tag-option'))
- it('should handle all tags already selected', () => {
- render(
- ,
- )
- // All app tags appear in selectedTags, filteredTagList should be empty
- // All app tags appear in selectedTags, filteredTagList should be empty
- expect(screen.getByText('Frontend'))!.toBeInTheDocument()
- expect(screen.getByText('Backend'))!.toBeInTheDocument()
- expect(screen.getByText('API'))!.toBeInTheDocument()
- })
+ expect(onValueChangeSpy).toHaveBeenLastCalledWith(expect.arrayContaining([
+ expect.objectContaining({
+ isCreateOption: true,
+ name: 'BrandNewTag',
+ }),
+ ]))
+ })
- it('should show divider between create option and tag list when both present', async () => {
- const user = userEvent.setup()
- // Only same-type tags for notExisted to work
- render( )
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'Back')
- // 'Back' matches Backend (unselected), notExisted is true (no tag named 'Back')
- // filteredTagList has items, so the conditional divider between create-option and tag-list renders
- const dividers = screen.getAllByTestId('divider')
- expect(dividers.length).toBeGreaterThanOrEqual(2)
- })
+ it('renders the empty state when no tags exist and no search is active', () => {
+ render( )
+ expect(screen.getByText(i18n.noTag)).toBeInTheDocument()
+ })
- it('should handle knowledge type tags correctly', () => {
- render(
- ,
- )
- expect(screen.getByText('KnowledgeDB'))!.toBeInTheDocument()
- })
+ it('opens tag management through a semantic button', async () => {
+ const user = userEvent.setup()
+ const onOpenTagManagement = vi.fn()
+ render( )
+
+ await user.click(screen.getByRole('button', { name: i18n.manageTags }))
+
+ expect(onOpenTagManagement).toHaveBeenCalledTimes(1)
+ })
+
+ it('renders knowledge tags when the panel type is knowledge', () => {
+ render( )
+ expect(screen.getByRole('option', { name: /KnowledgeDB/i })).toBeInTheDocument()
})
})
diff --git a/web/features/tag-management/__tests__/tag-selector.spec.tsx b/web/features/tag-management/__tests__/tag-selector.spec.tsx
index 01983dff7b..4a976e8781 100644
--- a/web/features/tag-management/__tests__/tag-selector.spec.tsx
+++ b/web/features/tag-management/__tests__/tag-selector.spec.tsx
@@ -1,5 +1,6 @@
+import type { ComponentProps } from 'react'
import type { Tag } from '@/contract/console/tags'
-import { render, screen, waitFor, within } from '@testing-library/react'
+import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TagSelector } from '../components/tag-selector'
@@ -16,16 +17,17 @@ const { mockToast } = vi.hoisted(() => {
return { mockToast }
})
-vi.mock('@langgenius/dify-ui/toast', () => ({
- toast: mockToast,
-}))
+vi.mock('@langgenius/dify-ui/toast', () => ({ toast: mockToast }))
-const { mockUseQueryData, createTag, bindTag, unBindTag } = vi.hoisted(() => ({
- mockUseQueryData: { current: [] as Tag[] },
- createTag: vi.fn(),
- bindTag: vi.fn(),
- unBindTag: vi.fn(),
-}))
+const { mockUseQueryData, createTag, bindTag, unBindTag } = vi.hoisted(() => {
+ const mockUseQueryData: { current: Tag[] } = { current: [] }
+ return {
+ mockUseQueryData,
+ createTag: vi.fn(),
+ bindTag: vi.fn(),
+ unBindTag: vi.fn(),
+ }
+})
vi.mock('@tanstack/react-query', () => ({
useQuery: () => ({ data: mockUseQueryData.current }),
@@ -35,14 +37,10 @@ vi.mock('../hooks/use-tag-mutations', () => ({
useCreateTagMutation: () => ({
isPending: false,
mutate: ({ body }: { body: { name: string, type: 'app' | 'knowledge' } }, options?: { onSuccess?: (tag: Tag) => void, onError?: () => void }) => {
- try {
- const tag = { id: 'new-tag', name: body.name, type: body.type, binding_count: 0 } as Tag
- createTag(body.name, body.type)
- options?.onSuccess?.(tag)
- }
- catch {
- options?.onError?.()
- }
+ const tag: Tag = { id: 'new-tag', name: body.name, type: body.type, binding_count: 0 }
+ Promise.resolve(createTag(body.name, body.type))
+ .then(() => options?.onSuccess?.(tag))
+ .catch(() => options?.onError?.())
},
}),
useApplyTagBindingsMutation: () => ({
@@ -66,12 +64,10 @@ vi.mock('../hooks/use-tag-mutations', () => ({
}),
}))
-// i18n keys rendered in "ns.key" format
const i18n = {
addTag: 'common.tag.addTag',
selectorPlaceholder: 'common.tag.selectorPlaceholder',
manageTags: 'common.tag.manageTags',
- noTag: 'common.tag.noTag',
modifiedSuccessfully: 'common.actionMsg.modifiedSuccessfully',
modifiedUnsuccessfully: 'common.actionMsg.modifiedUnsuccessfully',
}
@@ -83,18 +79,11 @@ const appTags: Tag[] = [
const defaultProps = {
targetId: 'target-1',
- type: 'app' as const,
- selectedTagIds: ['tag-1'!],
- selectedTags: [appTags[0]!],
-}
+ type: 'app',
+ value: [appTags[0]!],
+} satisfies ComponentProps
describe('TagSelector', () => {
- const getPanelTagRow = (tagName: string) => {
- const row = screen.getAllByTestId('tag-row').find(tagRow => within(tagRow).queryByText(tagName))
- expect(row).toBeDefined()
- return row as HTMLElement
- }
-
beforeEach(() => {
vi.clearAllMocks()
mockUseQueryData.current = appTags
@@ -103,340 +92,138 @@ describe('TagSelector', () => {
vi.mocked(unBindTag).mockResolvedValue(undefined)
})
- describe('Rendering', () => {
- it('should render TagSelector trigger with selected tag names from defaultProps when isPopover defaults to true', () => {
- render( )
- expect(screen.getByText('Frontend'))!.toBeInTheDocument()
- })
+ it('renders selected tag names in the combobox trigger', () => {
+ render( )
+ expect(screen.getByText('Frontend')).toBeInTheDocument()
+ })
- it('should render TagSelector add-tag placeholder when defaultProps are overridden with empty selectedTags and value', () => {
- render( )
- expect(screen.getByText(i18n.addTag))!.toBeInTheDocument()
- })
+ it('renders the add tag trigger when no current tag is visible in the workspace tag list', () => {
+ render( )
+ expect(screen.queryByText('Orphan')).not.toBeInTheDocument()
+ expect(screen.getByText(i18n.addTag)).toBeInTheDocument()
+ })
- it('should render nothing when isPopover is false', () => {
- const { container } = render( )
- // Only the empty fragment wrapper
- // Only the empty fragment wrapper
- expect(container)!.toBeEmptyDOMElement()
- })
+ it('opens a searchable combobox popup', async () => {
+ const user = userEvent.setup()
+ render( )
- it('should render the popover trigger button', () => {
- render( )
- // The trigger is wrapped in a PopoverButton
- // The trigger is wrapped in a PopoverButton
- expect(screen.getByRole('button'))!.toBeInTheDocument()
- })
+ await user.click(screen.getByRole('combobox', { name: /Frontend/i }))
- it('should render when minWidth is provided', () => {
- render( )
- expect(screen.getByRole('button'))!.toBeInTheDocument()
+ expect(await screen.findByRole('combobox', { name: i18n.selectorPlaceholder })).toBeInTheDocument()
+ expect(screen.getByText(i18n.manageTags)).toBeInTheDocument()
+ expect(screen.getByRole('option', { name: /Backend/i })).toBeInTheDocument()
+ })
+
+ it('applies added tags only when the popup closes', async () => {
+ const user = userEvent.setup()
+ render( )
+
+ const trigger = screen.getByRole('combobox', { name: /Frontend/i })
+ await user.click(trigger)
+ await user.click(await screen.findByRole('option', { name: /Backend/i }))
+
+ expect(bindTag).not.toHaveBeenCalled()
+
+ await user.click(trigger)
+
+ await waitFor(() => {
+ expect(bindTag).toHaveBeenCalledWith(['tag-2'], 'target-1', 'app')
+ })
+ expect(mockToast.success).toHaveBeenCalledWith(i18n.modifiedSuccessfully, {
+ id: 'tag-bindings-app-target-1',
})
})
- describe('Props', () => {
- it('should filter selectedTags to only those present in store tagList', () => {
- const unknownTag: Tag = { id: 'unknown', name: 'Unknown', type: 'app', binding_count: 0 }
- render(
- ,
- )
- // 'Frontend' is in tagList, 'Unknown' is not
- // 'Frontend' is in tagList, 'Unknown' is not
- expect(screen.getByText('Frontend'))!.toBeInTheDocument()
- expect(screen.queryByText('Unknown')).not.toBeInTheDocument()
- })
+ it('selects the highlighted tag with keyboard navigation and applies it on close', async () => {
+ const user = userEvent.setup()
+ render( )
- it('should display multiple tag names when multiple are selected', () => {
- render(
- ,
- )
- expect(screen.getByText('Frontend'))!.toBeInTheDocument()
- expect(screen.getByText('Backend'))!.toBeInTheDocument()
+ const trigger = screen.getByRole('combobox', { name: /Frontend/i })
+ await user.click(trigger)
+ await user.type(await screen.findByRole('combobox', { name: i18n.selectorPlaceholder }), 'Back')
+ await user.keyboard('{ArrowDown}')
+ await user.keyboard('{Enter}')
+ await user.click(trigger)
+
+ await waitFor(() => {
+ expect(bindTag).toHaveBeenCalledWith(['tag-2'], 'target-1', 'app')
})
})
- describe('Popover Interaction', () => {
- it('should show the panel when the trigger is clicked', async () => {
- const user = userEvent.setup()
- render( )
+ it('applies removed tags only when the popup closes', async () => {
+ const user = userEvent.setup()
+ render( )
- await user.click(screen.getByRole('button'))
+ const trigger = screen.getByRole('combobox', { name: /Frontend/i })
+ await user.click(trigger)
+ await user.click(await screen.findByRole('option', { name: /Frontend/i }))
+ await user.click(trigger)
- // Panel renders the search input and manage tags
- await waitFor(() => {
- expect(screen.getByPlaceholderText(i18n.selectorPlaceholder))!.toBeInTheDocument()
- expect(screen.getByText(i18n.manageTags))!.toBeInTheDocument()
- })
+ await waitFor(() => {
+ expect(unBindTag).toHaveBeenCalledWith('tag-1', 'target-1', 'app')
})
+ })
- it('should show unselected tags in the panel', async () => {
- const user = userEvent.setup()
- render( )
+ it('does not submit unchanged draft selections on close', async () => {
+ const user = userEvent.setup()
+ const onTagsChange = vi.fn()
+ render( )
- await user.click(screen.getByRole('button'))
+ const trigger = screen.getByRole('combobox', { name: /Frontend/i })
+ await user.click(trigger)
+ await screen.findByRole('combobox', { name: i18n.selectorPlaceholder })
+ await user.click(trigger)
- await waitFor(() => {
- expect(screen.getByText('Backend'))!.toBeInTheDocument()
- })
+ expect(bindTag).not.toHaveBeenCalled()
+ expect(unBindTag).not.toHaveBeenCalled()
+ expect(mockToast.success).not.toHaveBeenCalled()
+ expect(mockToast.error).not.toHaveBeenCalled()
+ expect(onTagsChange).not.toHaveBeenCalled()
+ })
+
+ it('notifies after apply settles with success or error', async () => {
+ const user = userEvent.setup()
+ const onTagsChange = vi.fn()
+ render( )
+
+ const trigger = screen.getByRole('combobox', { name: /Frontend/i })
+ await user.click(trigger)
+ await user.click(await screen.findByRole('option', { name: /Backend/i }))
+ await user.click(trigger)
+
+ await waitFor(() => {
+ expect(onTagsChange).toHaveBeenCalledTimes(1)
})
+ })
- it('should show the no-tag message when tag list is empty', async () => {
- const user = userEvent.setup()
- mockUseQueryData.current = []
- render( )
+ it('shows an error toast when applying bindings fails', async () => {
+ const user = userEvent.setup()
+ vi.mocked(unBindTag).mockRejectedValueOnce(new Error('Unbind failed'))
+ render( )
- await user.click(screen.getByRole('button'))
+ const trigger = screen.getByRole('combobox', { name: /Frontend/i })
+ await user.click(trigger)
+ await user.click(await screen.findByRole('option', { name: /Frontend/i }))
+ await user.click(trigger)
- await waitFor(() => {
- expect(screen.getByText(i18n.noTag))!.toBeInTheDocument()
- })
- })
-
- it('should bind a newly selected tag when closing the panel', async () => {
- const user = userEvent.setup()
- render( )
-
- const triggerButton = screen.getByRole('button', { name: /Frontend/i })
- await user.click(triggerButton)
-
- await screen.findByPlaceholderText(i18n.selectorPlaceholder)
- await user.click(getPanelTagRow('Backend'))
-
- // Close panel to trigger unmount side effects.
- await user.click(triggerButton)
-
- await waitFor(() => {
- expect(bindTag).toHaveBeenCalledTimes(1)
- expect(bindTag).toHaveBeenCalledWith(['tag-2'], 'target-1', 'app')
- })
- })
-
- it('should show one success toast when tag bindings are applied on close', async () => {
- const user = userEvent.setup()
- render( )
-
- const triggerButton = screen.getByRole('button', { name: /Frontend/i })
- await user.click(triggerButton)
-
- await screen.findByPlaceholderText(i18n.selectorPlaceholder)
- await user.click(getPanelTagRow('Backend'))
- await user.click(triggerButton)
-
- await waitFor(() => {
- expect(mockToast.success).toHaveBeenCalledWith(i18n.modifiedSuccessfully, {
- id: 'tag-bindings-app-target-1',
- })
- })
- })
-
- it('should unbind a deselected tag when closing the panel', async () => {
- const user = userEvent.setup()
- render( )
-
- const triggerButton = screen.getByRole('button', { name: /Frontend/i })
- await user.click(triggerButton)
-
- await screen.findByPlaceholderText(i18n.selectorPlaceholder)
- await user.click(getPanelTagRow('Frontend'))
-
- // Close panel to trigger unmount side effects.
- await user.click(triggerButton)
-
- await waitFor(() => {
- expect(unBindTag).toHaveBeenCalledTimes(1)
- expect(unBindTag).toHaveBeenCalledWith('tag-1', 'target-1', 'app')
- })
- })
-
- it('should show one error toast when applying tag bindings fails on close', async () => {
- const user = userEvent.setup()
- vi.mocked(unBindTag).mockRejectedValueOnce(new Error('Unbind failed'))
- render( )
-
- const triggerButton = screen.getByRole('button', { name: /Frontend/i })
- await user.click(triggerButton)
-
- await screen.findByPlaceholderText(i18n.selectorPlaceholder)
- await user.click(getPanelTagRow('Frontend'))
- await user.click(triggerButton)
-
- await waitFor(() => {
- expect(mockToast.error).toHaveBeenCalledWith(i18n.modifiedUnsuccessfully, {
- id: 'tag-bindings-app-target-1',
- })
- })
- })
-
- it('should not apply bindings when the selection is unchanged on close', async () => {
- const user = userEvent.setup()
- const onTagsChange = vi.fn()
- render( )
-
- const triggerButton = screen.getByRole('button', { name: /Frontend/i })
- await user.click(triggerButton)
- await screen.findByPlaceholderText(i18n.selectorPlaceholder)
- await user.click(triggerButton)
-
- expect(bindTag).not.toHaveBeenCalled()
- expect(unBindTag).not.toHaveBeenCalled()
- expect(mockToast.success).not.toHaveBeenCalled()
- expect(mockToast.error).not.toHaveBeenCalled()
- expect(onTagsChange).not.toHaveBeenCalled()
- })
-
- it('should notify tag changes after bindings are applied successfully', async () => {
- const user = userEvent.setup()
- const onTagsChange = vi.fn()
- render( )
-
- const triggerButton = screen.getByRole('button', { name: /Frontend/i })
- await user.click(triggerButton)
-
- await screen.findByPlaceholderText(i18n.selectorPlaceholder)
- await user.click(getPanelTagRow('Backend'))
- await user.click(triggerButton)
-
- await waitFor(() => {
- expect(onTagsChange).toHaveBeenCalledTimes(1)
- })
- })
-
- it('should notify tag changes after applying bindings settles with an error', async () => {
- const user = userEvent.setup()
- const onTagsChange = vi.fn()
- vi.mocked(unBindTag).mockRejectedValueOnce(new Error('Unbind failed'))
- render( )
-
- const triggerButton = screen.getByRole('button', { name: /Frontend/i })
- await user.click(triggerButton)
-
- await screen.findByPlaceholderText(i18n.selectorPlaceholder)
- await user.click(getPanelTagRow('Frontend'))
- await user.click(triggerButton)
-
- await waitFor(() => {
- expect(onTagsChange).toHaveBeenCalledTimes(1)
+ await waitFor(() => {
+ expect(mockToast.error).toHaveBeenCalledWith(i18n.modifiedUnsuccessfully, {
+ id: 'tag-bindings-app-target-1',
})
})
})
- describe('Data Fetching', () => {
- it('should create tags through the mutation hook', async () => {
- const user = userEvent.setup()
- vi.mocked(createTag).mockResolvedValue({ id: 'new-tag', name: 'BrandNewTag', type: 'app', binding_count: 0 })
+ it('creates a tag with the current tag type without binding it implicitly', async () => {
+ const user = userEvent.setup()
+ render( )
- render( )
+ await user.click(screen.getByRole('combobox', { name: i18n.addTag }))
+ await user.type(await screen.findByRole('combobox', { name: i18n.selectorPlaceholder }), 'NewKnowledgeTag')
+ await user.click(await screen.findByTestId('create-tag-option'))
- await user.click(screen.getByRole('button'))
-
- await waitFor(() => {
- expect(screen.getByPlaceholderText(i18n.selectorPlaceholder))!.toBeInTheDocument()
- })
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'BrandNewTag')
-
- const createOption = await screen.findByTestId('create-tag-option')
- await user.click(createOption)
-
- await waitFor(() => {
- expect(createTag).toHaveBeenCalledWith('BrandNewTag', 'app')
- })
-
- expect(mockUseQueryData.current).toEqual(appTags)
- })
- })
-
- describe('Edge Cases', () => {
- it('should handle selectedTags with no matching tags in store', () => {
- const orphanTags: Tag[] = [
- { id: 'orphan-1', name: 'Orphan', type: 'app', binding_count: 0 },
- ]
- render(
- ,
- )
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- // Orphan tag is not in store tagList, so tags memo returns []
- expect(screen.queryByText('Orphan')).not.toBeInTheDocument()
- expect(screen.getByText(i18n.addTag))!.toBeInTheDocument()
- })
-
- it('should handle knowledge type', async () => {
- const user = userEvent.setup()
- const knowledgeTags: Tag[] = [
- { id: 'k-1', name: 'KnowledgeDB', type: 'knowledge', binding_count: 2 },
- ]
- mockUseQueryData.current = knowledgeTags
-
- render(
- ,
- )
-
- expect(screen.getByText('KnowledgeDB'))!.toBeInTheDocument()
-
- // Open popover and verify panel uses knowledge type
- await user.click(screen.getByRole('button'))
-
- await waitFor(() => {
- expect(screen.getByPlaceholderText(i18n.selectorPlaceholder))!.toBeInTheDocument()
- })
-
- const input = screen.getByPlaceholderText(i18n.selectorPlaceholder)
- await user.type(input, 'NewKnowledgeTag')
-
- const createOption = await screen.findByTestId('create-tag-option')
- await user.click(createOption)
-
- await waitFor(() => {
- expect(createTag).toHaveBeenCalledWith('NewKnowledgeTag', 'knowledge')
- })
+ await waitFor(() => {
+ expect(createTag).toHaveBeenCalledWith('NewKnowledgeTag', 'knowledge')
})
+ expect(bindTag).not.toHaveBeenCalled()
})
})
diff --git a/web/features/tag-management/components/__tests__/app-card-tags.spec.tsx b/web/features/tag-management/components/__tests__/app-card-tags.spec.tsx
index 916202d6fd..26d6cf67aa 100644
--- a/web/features/tag-management/components/__tests__/app-card-tags.spec.tsx
+++ b/web/features/tag-management/components/__tests__/app-card-tags.spec.tsx
@@ -9,11 +9,10 @@ vi.mock('@/features/tag-management/components/tag-selector', () => ({
TagSelector: (props: {
onOpenTagManagement?: () => void
onTagsChange?: () => void
- position: string
- selectedTagIds: string[]
- selectedTags: Tag[]
+ placement: string
targetId: string
type: string
+ value: Tag[]
}) => {
renderTagSelector(props)
@@ -21,8 +20,8 @@ vi.mock('@/features/tag-management/components/tag-selector', () => ({