Compare commits

..

4 Commits

107 changed files with 585 additions and 3038 deletions

View File

@ -10,14 +10,10 @@ updates:
directory: "/api"
open-pull-requests-limit: 2
patterns: ["*"]
schedule:
interval: "weekly"
- package-ecosystem: "uv"
directory: "/api"
open-pull-requests-limit: 2
patterns: ["*"]
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/web"
schedule:

View File

@ -115,9 +115,6 @@ ignore_imports =
core.workflow.nodes.datasource.datasource_node -> models.tools
core.workflow.nodes.datasource.datasource_node -> services.datasource_provider_service
core.workflow.nodes.document_extractor.node -> core.helper.ssrf_proxy
core.workflow.nodes.http_request.entities -> configs
core.workflow.nodes.http_request.executor -> configs
core.workflow.nodes.http_request.node -> configs
core.workflow.nodes.http_request.node -> core.tools.tool_file_manager
core.workflow.nodes.iteration.iteration_node -> core.app.workflow.node_factory
core.workflow.nodes.knowledge_index.knowledge_index_node -> core.rag.index_processor.index_processor_factory

View File

@ -17,7 +17,7 @@ from core.workflow.nodes.base.node import Node
from core.workflow.nodes.code.code_node import CodeNode
from core.workflow.nodes.code.limits import CodeNodeLimits
from core.workflow.nodes.document_extractor import DocumentExtractorNode, UnstructuredApiConfig
from core.workflow.nodes.http_request.node import HttpRequestNode
from core.workflow.nodes.http_request import HttpRequestNode, HttpRequestNodeConfig
from core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node import KnowledgeRetrievalNode
from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING
from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtocol
@ -45,6 +45,7 @@ class DifyNodeFactory(NodeFactory):
self,
graph_init_params: "GraphInitParams",
graph_runtime_state: "GraphRuntimeState",
*,
code_executor: type[CodeExecutor] | None = None,
code_providers: Sequence[type[CodeNodeProvider]] | None = None,
code_limits: CodeNodeLimits | None = None,
@ -54,6 +55,7 @@ class DifyNodeFactory(NodeFactory):
http_request_tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager,
http_request_file_manager: FileManagerProtocol | None = None,
document_extractor_unstructured_api_config: UnstructuredApiConfig | None = None,
http_request_config: HttpRequestNodeConfig | None = None,
) -> None:
self.graph_init_params = graph_init_params
self.graph_runtime_state = graph_runtime_state
@ -86,6 +88,15 @@ class DifyNodeFactory(NodeFactory):
api_key=dify_config.UNSTRUCTURED_API_KEY or "",
)
)
self._http_request_config = http_request_config or HttpRequestNodeConfig(
max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
max_write_timeout=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
max_binary_size=dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE,
max_text_size=dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE,
ssl_verify=dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
ssrf_default_max_retries=dify_config.SSRF_DEFAULT_MAX_RETRIES,
)
@override
def create_node(self, node_config: NodeConfigDict) -> Node:
@ -146,6 +157,7 @@ class DifyNodeFactory(NodeFactory):
config=node_config,
graph_init_params=self.graph_init_params,
graph_runtime_state=self.graph_runtime_state,
http_request_config=self._http_request_config,
http_client=self._http_request_http_client,
tool_file_manager_factory=self._http_request_tool_file_manager_factory,
file_manager=self._http_request_file_manager,

View File

@ -1,4 +1,19 @@
from .entities import BodyData, HttpRequestNodeAuthorization, HttpRequestNodeBody, HttpRequestNodeData
from .entities import (
HTTP_REQUEST_CONFIG_FILTER_KEY,
BodyData,
HttpRequestNodeAuthorization,
HttpRequestNodeBody,
HttpRequestNodeConfig,
HttpRequestNodeData,
)
from .node import HttpRequestNode
__all__ = ["BodyData", "HttpRequestNode", "HttpRequestNodeAuthorization", "HttpRequestNodeBody", "HttpRequestNodeData"]
__all__ = [
"HTTP_REQUEST_CONFIG_FILTER_KEY",
"BodyData",
"HttpRequestNode",
"HttpRequestNodeAuthorization",
"HttpRequestNodeBody",
"HttpRequestNodeConfig",
"HttpRequestNodeData",
]

View File

@ -1,5 +1,6 @@
import mimetypes
from collections.abc import Sequence
from dataclasses import dataclass
from email.message import Message
from typing import Any, Literal
@ -7,9 +8,10 @@ import charset_normalizer
import httpx
from pydantic import BaseModel, Field, ValidationInfo, field_validator
from configs import dify_config
from core.workflow.nodes.base import BaseNodeData
HTTP_REQUEST_CONFIG_FILTER_KEY = "http_request_config"
class HttpRequestNodeAuthorizationConfig(BaseModel):
type: Literal["basic", "bearer", "custom"]
@ -59,9 +61,27 @@ class HttpRequestNodeBody(BaseModel):
class HttpRequestNodeTimeout(BaseModel):
connect: int = dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT
read: int = dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT
write: int = dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT
connect: int | None = None
read: int | None = None
write: int | None = None
@dataclass(frozen=True, slots=True)
class HttpRequestNodeConfig:
max_connect_timeout: int
max_read_timeout: int
max_write_timeout: int
max_binary_size: int
max_text_size: int
ssl_verify: bool
ssrf_default_max_retries: int
def default_timeout(self) -> "HttpRequestNodeTimeout":
return HttpRequestNodeTimeout(
connect=self.max_connect_timeout,
read=self.max_read_timeout,
write=self.max_write_timeout,
)
class HttpRequestNodeData(BaseNodeData):
@ -91,7 +111,7 @@ class HttpRequestNodeData(BaseNodeData):
params: str
body: HttpRequestNodeBody | None = None
timeout: HttpRequestNodeTimeout | None = None
ssl_verify: bool | None = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY
ssl_verify: bool | None = None
class Response:

View File

@ -10,7 +10,6 @@ from urllib.parse import urlencode, urlparse
import httpx
from json_repair import repair_json
from configs import dify_config
from core.helper.ssrf_proxy import ssrf_proxy
from core.variables.segments import ArrayFileSegment, FileSegment
from core.workflow.file.enums import FileTransferMethod
@ -20,6 +19,7 @@ from core.workflow.runtime import VariablePool
from ..protocols import FileManagerProtocol, HttpClientProtocol
from .entities import (
HttpRequestNodeAuthorization,
HttpRequestNodeConfig,
HttpRequestNodeData,
HttpRequestNodeTimeout,
Response,
@ -78,10 +78,13 @@ class Executor:
node_data: HttpRequestNodeData,
timeout: HttpRequestNodeTimeout,
variable_pool: VariablePool,
max_retries: int = dify_config.SSRF_DEFAULT_MAX_RETRIES,
http_request_config: HttpRequestNodeConfig,
max_retries: int | None = None,
ssl_verify: bool | None = None,
http_client: HttpClientProtocol | None = None,
file_manager: FileManagerProtocol | None = None,
):
self._http_request_config = http_request_config
# If authorization API key is present, convert the API key using the variable pool
if node_data.authorization.type == "api-key":
if node_data.authorization.config is None:
@ -99,14 +102,21 @@ class Executor:
self.method = node_data.method
self.auth = node_data.authorization
self.timeout = timeout
self.ssl_verify = node_data.ssl_verify
resolved_ssl_verify = ssl_verify if ssl_verify is not None else node_data.ssl_verify
if resolved_ssl_verify is None:
resolved_ssl_verify = self._http_request_config.ssl_verify
if not isinstance(resolved_ssl_verify, bool):
raise ValueError("ssl_verify must be a boolean")
self.ssl_verify = resolved_ssl_verify
self.params = None
self.headers = {}
self.content = None
self.files = None
self.data = None
self.json = None
self.max_retries = max_retries
self.max_retries = (
max_retries if max_retries is not None else self._http_request_config.ssrf_default_max_retries
)
self._http_client = http_client or ssrf_proxy
self._file_manager = file_manager or default_file_manager
@ -319,9 +329,9 @@ class Executor:
executor_response = Response(response)
threshold_size = (
dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE
self._http_request_config.max_binary_size
if executor_response.is_file
else dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE
else self._http_request_config.max_text_size
)
if executor_response.size > threshold_size:
raise ResponseSizeError(
@ -366,9 +376,7 @@ class Executor:
**request_args,
max_retries=self.max_retries,
)
except self._http_client.max_retries_exceeded_error as e:
raise HttpRequestNodeError(f"Reached maximum retries for URL {self.url}") from e
except self._http_client.request_error as e:
except (self._http_client.max_retries_exceeded_error, self._http_client.request_error) as e:
raise HttpRequestNodeError(str(e)) from e
return response

View File

@ -3,7 +3,6 @@ import mimetypes
from collections.abc import Callable, Mapping, Sequence
from typing import TYPE_CHECKING, Any
from configs import dify_config
from core.helper.ssrf_proxy import ssrf_proxy
from core.tools.tool_file_manager import ToolFileManager
from core.variables.segments import ArrayFileSegment
@ -19,18 +18,14 @@ from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtoco
from factories import file_factory
from .entities import (
HTTP_REQUEST_CONFIG_FILTER_KEY,
HttpRequestNodeConfig,
HttpRequestNodeData,
HttpRequestNodeTimeout,
Response,
)
from .exc import HttpRequestNodeError, RequestBodyError
HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeTimeout(
connect=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
read=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
write=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
)
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
@ -38,6 +33,15 @@ if TYPE_CHECKING:
from core.workflow.runtime import GraphRuntimeState
def _resolve_http_request_config(filters: Mapping[str, object] | None) -> HttpRequestNodeConfig:
if not filters:
raise ValueError("http_request_config is required to build HTTP request default config")
config = filters.get(HTTP_REQUEST_CONFIG_FILTER_KEY)
if not isinstance(config, HttpRequestNodeConfig):
raise ValueError("http_request_config must be an HttpRequestNodeConfig instance")
return config
class HttpRequestNode(Node[HttpRequestNodeData]):
node_type = NodeType.HTTP_REQUEST
@ -48,6 +52,7 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
graph_init_params: "GraphInitParams",
graph_runtime_state: "GraphRuntimeState",
*,
http_request_config: HttpRequestNodeConfig,
http_client: HttpClientProtocol | None = None,
tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager,
file_manager: FileManagerProtocol | None = None,
@ -58,12 +63,15 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
graph_init_params=graph_init_params,
graph_runtime_state=graph_runtime_state,
)
self._http_request_config = http_request_config
self._http_client = http_client or ssrf_proxy
self._tool_file_manager_factory = tool_file_manager_factory
self._file_manager = file_manager or default_file_manager
@classmethod
def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]:
http_request_config = _resolve_http_request_config(filters)
default_timeout = http_request_config.default_timeout()
return {
"type": "http-request",
"config": {
@ -73,15 +81,15 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
},
"body": {"type": "none"},
"timeout": {
**HTTP_REQUEST_DEFAULT_TIMEOUT.model_dump(),
"max_connect_timeout": dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
"max_read_timeout": dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
"max_write_timeout": dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
**default_timeout.model_dump(),
"max_connect_timeout": http_request_config.max_connect_timeout,
"max_read_timeout": http_request_config.max_read_timeout,
"max_write_timeout": http_request_config.max_write_timeout,
},
"ssl_verify": dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
"ssl_verify": http_request_config.ssl_verify,
},
"retry_config": {
"max_retries": dify_config.SSRF_DEFAULT_MAX_RETRIES,
"max_retries": http_request_config.ssrf_default_max_retries,
"retry_interval": 0.5 * (2**2),
"retry_enabled": True,
},
@ -98,7 +106,9 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
node_data=self.node_data,
timeout=self._get_request_timeout(self.node_data),
variable_pool=self.graph_runtime_state.variable_pool,
http_request_config=self._http_request_config,
max_retries=0,
ssl_verify=self._resolve_ssl_verify(self.node_data),
http_client=self._http_client,
file_manager=self._file_manager,
)
@ -142,15 +152,18 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
error_type=type(e).__name__,
)
@staticmethod
def _get_request_timeout(node_data: HttpRequestNodeData) -> HttpRequestNodeTimeout:
def _resolve_ssl_verify(self, node_data: HttpRequestNodeData) -> bool:
return self._http_request_config.ssl_verify if node_data.ssl_verify is None else node_data.ssl_verify
def _get_request_timeout(self, node_data: HttpRequestNodeData) -> HttpRequestNodeTimeout:
default_timeout = self._http_request_config.default_timeout()
timeout = node_data.timeout
if timeout is None:
return HTTP_REQUEST_DEFAULT_TIMEOUT
return default_timeout
timeout.connect = timeout.connect or HTTP_REQUEST_DEFAULT_TIMEOUT.connect
timeout.read = timeout.read or HTTP_REQUEST_DEFAULT_TIMEOUT.read
timeout.write = timeout.write or HTTP_REQUEST_DEFAULT_TIMEOUT.write
timeout.connect = timeout.connect or default_timeout.connect
timeout.read = timeout.read or default_timeout.read
timeout.write = timeout.write or default_timeout.write
return timeout
@classmethod

View File

@ -21,7 +21,7 @@ dependencies = [
"flask-orjson~=2.0.0",
"flask-sqlalchemy~=3.1.1",
"gevent~=25.9.1",
"gmpy2~=2.3.0",
"gmpy2~=2.2.1",
"google-api-core==2.18.0",
"google-api-python-client==2.189.0",
"google-auth==2.29.0",
@ -138,7 +138,7 @@ dev = [
"types-gevent~=25.9.0",
"types-greenlet~=3.3.0",
"types-html5lib~=1.1.11",
"types-markdown~=3.10.2",
"types-markdown~=3.7.0",
"types-oauthlib~=3.2.0",
"types-objgraph~=3.6.0",
"types-olefile~=0.47.0",
@ -211,7 +211,7 @@ vdb = [
"clickzetta-connector-python>=0.8.102",
"couchbase~=4.3.0",
"elasticsearch==8.14.0",
"opensearch-py==3.1.0",
"opensearch-py==2.4.0",
"oracledb==3.3.0",
"pgvecto-rs[sqlalchemy]~=0.2.1",
"pgvector==0.2.5",

View File

@ -3,15 +3,13 @@ from collections.abc import Mapping, Sequence
from mimetypes import guess_type
from pydantic import BaseModel
from sqlalchemy import delete, select, update
from sqlalchemy.orm import Session
from sqlalchemy import select
from yarl import URL
from configs import dify_config
from core.helper import marketplace
from core.helper.download import download_with_size_limit
from core.helper.marketplace import download_plugin_pkg
from core.helper.model_provider_cache import ProviderCredentialsCache, ProviderCredentialsCacheType
from core.plugin.entities.bundle import PluginBundleDependency
from core.plugin.entities.plugin import (
PluginDeclaration,
@ -30,7 +28,7 @@ from core.plugin.impl.debugging import PluginDebuggingClient
from core.plugin.impl.plugin import PluginInstaller
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from models.provider import Provider, ProviderCredential
from models.provider import ProviderCredential
from models.provider_ids import GenericProviderID
from services.errors.plugin import PluginInstallationForbiddenError
from services.feature_service import FeatureService, PluginInstallationScope
@ -513,55 +511,30 @@ class PluginService:
manager = PluginInstaller()
# Get plugin info before uninstalling to delete associated credentials
plugins = manager.list_plugins(tenant_id)
plugin = next((p for p in plugins if p.installation_id == plugin_installation_id), None)
try:
plugins = manager.list_plugins(tenant_id)
plugin = next((p for p in plugins if p.installation_id == plugin_installation_id), None)
if not plugin:
return manager.uninstall(tenant_id, plugin_installation_id)
if plugin:
plugin_id = plugin.plugin_id
logger.info("Deleting credentials for plugin: %s", plugin_id)
with Session(db.engine) as session, session.begin():
plugin_id = plugin.plugin_id
logger.info("Deleting credentials for plugin: %s", plugin_id)
# Delete provider credentials that match this plugin
credentials = db.session.scalars(
select(ProviderCredential).where(
ProviderCredential.tenant_id == tenant_id,
ProviderCredential.provider_name.like(f"{plugin_id}/%"),
)
).all()
# Delete provider credentials that match this plugin
credential_ids = session.scalars(
select(ProviderCredential.id).where(
ProviderCredential.tenant_id == tenant_id,
ProviderCredential.provider_name.like(f"{plugin_id}/%"),
)
).all()
for cred in credentials:
db.session.delete(cred)
if not credential_ids:
logger.info("No credentials found for plugin: %s", plugin_id)
return manager.uninstall(tenant_id, plugin_installation_id)
provider_ids = session.scalars(
select(Provider.id).where(
Provider.tenant_id == tenant_id,
Provider.provider_name.like(f"{plugin_id}/%"),
Provider.credential_id.in_(credential_ids),
)
).all()
session.execute(update(Provider).where(Provider.id.in_(provider_ids)).values(credential_id=None))
for provider_id in provider_ids:
ProviderCredentialsCache(
tenant_id=tenant_id,
identity_id=provider_id,
cache_type=ProviderCredentialsCacheType.PROVIDER,
).delete()
session.execute(
delete(ProviderCredential).where(
ProviderCredential.id.in_(credential_ids),
)
)
logger.info(
"Completed deleting credentials and cleaning provider associations for plugin: %s",
plugin_id,
)
db.session.commit()
logger.info("Deleted %d credentials for plugin: %s", len(credentials), plugin_id)
except Exception as e:
logger.warning("Failed to delete credentials: %s", e)
# Continue with uninstall even if credential deletion fails
return manager.uninstall(tenant_id, plugin_installation_id)

View File

@ -47,6 +47,7 @@ from core.workflow.graph_events import NodeRunFailedEvent, NodeRunSucceededEvent
from core.workflow.graph_events.base import GraphNodeEventBase
from core.workflow.node_events.base import NodeRunResult
from core.workflow.nodes.base.node import Node
from core.workflow.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNodeConfig
from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING
from core.workflow.repositories.workflow_node_execution_repository import OrderConfig
from core.workflow.runtime import VariablePool
@ -86,6 +87,18 @@ from services.workflow_draft_variable_service import DraftVariableSaver, DraftVa
logger = logging.getLogger(__name__)
def _build_http_request_config() -> HttpRequestNodeConfig:
return HttpRequestNodeConfig(
max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
max_write_timeout=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
max_binary_size=dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE,
max_text_size=dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE,
ssl_verify=dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
ssrf_default_max_retries=dify_config.SSRF_DEFAULT_MAX_RETRIES,
)
class RagPipelineService:
def __init__(self, session_maker: sessionmaker | None = None):
"""Initialize RagPipelineService with repository dependencies."""
@ -380,9 +393,12 @@ class RagPipelineService:
"""
# return default block config
default_block_configs: list[dict[str, Any]] = []
for node_class_mapping in NODE_TYPE_CLASSES_MAPPING.values():
for node_type, node_class_mapping in NODE_TYPE_CLASSES_MAPPING.items():
node_class = node_class_mapping[LATEST_VERSION]
default_config = node_class.get_default_config()
filters = None
if node_type is NodeType.HTTP_REQUEST:
filters = {HTTP_REQUEST_CONFIG_FILTER_KEY: _build_http_request_config()}
default_config = node_class.get_default_config(filters=filters)
if default_config:
default_block_configs.append(dict(default_config))
@ -402,7 +418,10 @@ class RagPipelineService:
return None
node_class = NODE_TYPE_CLASSES_MAPPING[node_type_enum][LATEST_VERSION]
default_config = node_class.get_default_config(filters=filters)
resolved_filters = dict(filters) if filters else {}
if node_type_enum is NodeType.HTTP_REQUEST and HTTP_REQUEST_CONFIG_FILTER_KEY not in resolved_filters:
resolved_filters[HTTP_REQUEST_CONFIG_FILTER_KEY] = _build_http_request_config()
default_config = node_class.get_default_config(filters=resolved_filters or None)
if not default_config:
return None

View File

@ -26,6 +26,7 @@ from core.workflow.graph_events import GraphNodeEventBase, NodeRunFailedEvent, N
from core.workflow.node_events import NodeRunResult
from core.workflow.nodes import NodeType
from core.workflow.nodes.base.node import Node
from core.workflow.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNodeConfig
from core.workflow.nodes.human_input.entities import (
DeliveryChannelConfig,
HumanInputNodeData,
@ -70,6 +71,18 @@ from .human_input_delivery_test_service import (
from .workflow_draft_variable_service import DraftVariableSaver, DraftVarLoader, WorkflowDraftVariableService
def _build_http_request_config() -> HttpRequestNodeConfig:
return HttpRequestNodeConfig(
max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
max_write_timeout=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
max_binary_size=dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE,
max_text_size=dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE,
ssl_verify=dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
ssrf_default_max_retries=dify_config.SSRF_DEFAULT_MAX_RETRIES,
)
class WorkflowService:
"""
Workflow Service
@ -618,9 +631,12 @@ class WorkflowService:
"""
# return default block config
default_block_configs: list[Mapping[str, object]] = []
for node_class_mapping in NODE_TYPE_CLASSES_MAPPING.values():
for node_type, node_class_mapping in NODE_TYPE_CLASSES_MAPPING.items():
node_class = node_class_mapping[LATEST_VERSION]
default_config = node_class.get_default_config()
filters = None
if node_type is NodeType.HTTP_REQUEST:
filters = {HTTP_REQUEST_CONFIG_FILTER_KEY: _build_http_request_config()}
default_config = node_class.get_default_config(filters=filters)
if default_config:
default_block_configs.append(default_config)
@ -642,7 +658,10 @@ class WorkflowService:
return {}
node_class = NODE_TYPE_CLASSES_MAPPING[node_type_enum][LATEST_VERSION]
default_config = node_class.get_default_config(filters=filters)
resolved_filters = dict(filters) if filters else {}
if node_type_enum is NodeType.HTTP_REQUEST and HTTP_REQUEST_CONFIG_FILTER_KEY not in resolved_filters:
resolved_filters[HTTP_REQUEST_CONFIG_FILTER_KEY] = _build_http_request_config()
default_config = node_class.get_default_config(filters=resolved_filters or None)
if not default_config:
return {}

View File

@ -4,17 +4,28 @@ from urllib.parse import urlencode
import pytest
from configs import dify_config
from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.workflow.node_factory import DifyNodeFactory
from core.workflow.entities import GraphInitParams
from core.workflow.enums import WorkflowNodeExecutionStatus
from core.workflow.graph import Graph
from core.workflow.nodes.http_request.node import HttpRequestNode
from core.workflow.nodes.http_request import HttpRequestNode, HttpRequestNodeConfig
from core.workflow.runtime import GraphRuntimeState, VariablePool
from core.workflow.system_variable import SystemVariable
from models.enums import UserFrom
from tests.integration_tests.workflow.nodes.__mock.http import setup_http_mock
HTTP_REQUEST_CONFIG = HttpRequestNodeConfig(
max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
max_write_timeout=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
max_binary_size=dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE,
max_text_size=dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE,
ssl_verify=dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
ssrf_default_max_retries=dify_config.SSRF_DEFAULT_MAX_RETRIES,
)
def init_http_node(config: dict):
graph_config = {
@ -64,6 +75,7 @@ def init_http_node(config: dict):
config=config,
graph_init_params=init_params,
graph_runtime_state=graph_runtime_state,
http_request_config=HTTP_REQUEST_CONFIG,
)
return node
@ -215,6 +227,7 @@ def test_custom_auth_with_empty_api_key_raises_error(setup_http_mock):
Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=10),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -702,6 +715,7 @@ def test_nested_object_variable_selector(setup_http_mock):
config=graph_config["nodes"][1],
graph_init_params=init_params,
graph_runtime_state=graph_runtime_state,
http_request_config=HTTP_REQUEST_CONFIG,
)
result = node._run()

View File

@ -114,6 +114,15 @@ class MockNodeFactory(DifyNodeFactory):
code_providers=self._code_providers,
code_limits=self._code_limits,
)
elif node_type == NodeType.HTTP_REQUEST:
mock_instance = mock_class(
id=node_id,
config=node_config,
graph_init_params=self.graph_init_params,
graph_runtime_state=self.graph_runtime_state,
mock_config=self.mock_config,
http_request_config=self._http_request_config,
)
else:
mock_instance = mock_class(
id=node_id,

View File

@ -1,9 +1,11 @@
import pytest
from configs import dify_config
from core.workflow.nodes.http_request import (
BodyData,
HttpRequestNodeAuthorization,
HttpRequestNodeBody,
HttpRequestNodeConfig,
HttpRequestNodeData,
)
from core.workflow.nodes.http_request.entities import HttpRequestNodeTimeout
@ -12,6 +14,16 @@ from core.workflow.nodes.http_request.executor import Executor
from core.workflow.runtime import VariablePool
from core.workflow.system_variable import SystemVariable
HTTP_REQUEST_CONFIG = HttpRequestNodeConfig(
max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
max_write_timeout=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
max_binary_size=dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE,
max_text_size=dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE,
ssl_verify=dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
ssrf_default_max_retries=dify_config.SSRF_DEFAULT_MAX_RETRIES,
)
def test_executor_with_json_body_and_number_variable():
# Prepare the variable pool
@ -45,6 +57,7 @@ def test_executor_with_json_body_and_number_variable():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -98,6 +111,7 @@ def test_executor_with_json_body_and_object_variable():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -153,6 +167,7 @@ def test_executor_with_json_body_and_nested_object_variable():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -196,6 +211,7 @@ def test_extract_selectors_from_template_with_newline():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -240,6 +256,7 @@ def test_executor_with_form_data():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -290,6 +307,7 @@ def test_init_headers():
return Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=VariablePool(system_variables=SystemVariable.default()),
)
@ -324,6 +342,7 @@ def test_init_params():
return Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=VariablePool(system_variables=SystemVariable.default()),
)
@ -373,6 +392,7 @@ def test_empty_api_key_raises_error_bearer():
Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -397,6 +417,7 @@ def test_empty_api_key_raises_error_basic():
Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -421,6 +442,7 @@ def test_empty_api_key_raises_error_custom():
Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -445,6 +467,7 @@ def test_whitespace_only_api_key_raises_error():
Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -468,6 +491,7 @@ def test_valid_api_key_works():
executor = Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -515,6 +539,7 @@ def test_executor_with_json_body_and_unquoted_uuid_variable():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -559,6 +584,7 @@ def test_executor_with_json_body_and_unquoted_uuid_with_newlines():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -597,6 +623,7 @@ def test_executor_with_json_body_preserves_numbers_and_strings():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)

View File

@ -1005,7 +1005,7 @@ class TestWorkflowService:
mock_node_class = MagicMock()
mock_node_class.get_default_config.return_value = {"type": "llm", "config": {}}
mock_mapping.values.return_value = [{"latest": mock_node_class}]
mock_mapping.items.return_value = [(NodeType.LLM, {"latest": mock_node_class})]
with patch("services.workflow_service.LATEST_VERSION", "latest"):
result = workflow_service.get_default_block_configs()

186
api/uv.lock generated
View File

@ -1590,7 +1590,7 @@ requires-dist = [
{ name = "flask-restx", specifier = "~=1.3.2" },
{ name = "flask-sqlalchemy", specifier = "~=3.1.1" },
{ name = "gevent", specifier = "~=25.9.1" },
{ name = "gmpy2", specifier = "~=2.3.0" },
{ name = "gmpy2", specifier = "~=2.2.1" },
{ name = "google-api-core", specifier = "==2.18.0" },
{ name = "google-api-python-client", specifier = "==2.189.0" },
{ name = "google-auth", specifier = "==2.29.0" },
@ -1698,7 +1698,7 @@ dev = [
{ name = "types-html5lib", specifier = "~=1.1.11" },
{ name = "types-jmespath", specifier = ">=1.0.2.20240106" },
{ name = "types-jsonschema", specifier = "~=4.23.0" },
{ name = "types-markdown", specifier = "~=3.10.2" },
{ name = "types-markdown", specifier = "~=3.7.0" },
{ name = "types-oauthlib", specifier = "~=3.2.0" },
{ name = "types-objgraph", specifier = "~=3.6.0" },
{ name = "types-olefile", specifier = "~=0.47.0" },
@ -1750,7 +1750,7 @@ vdb = [
{ name = "intersystems-irispython", specifier = ">=5.1.0" },
{ name = "mo-vector", specifier = "~=0.1.13" },
{ name = "mysql-connector-python", specifier = ">=9.3.0" },
{ name = "opensearch-py", specifier = "==3.1.0" },
{ name = "opensearch-py", specifier = "==2.4.0" },
{ name = "oracledb", specifier = "==3.3.0" },
{ name = "pgvecto-rs", extras = ["sqlalchemy"], specifier = "~=0.2.1" },
{ name = "pgvector", specifier = "==0.2.5" },
@ -1896,14 +1896,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/d8/2a1c638d9e0aa7e269269a1a1bf423ddd94267f1a01bbe3ad03432b67dd4/eval_type_backport-0.3.0-py3-none-any.whl", hash = "sha256:975a10a0fe333c8b6260d7fdb637698c9a16c3a9e3b6eb943fee6a6f67a37fe8", size = 6061, upload-time = "2025-11-13T20:56:49.499Z" },
]
[[package]]
name = "events"
version = "0.5"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/ed/e47dec0626edd468c84c04d97769e7ab4ea6457b7f54dcb3f72b17fcd876/Events-0.5-py3-none-any.whl", hash = "sha256:a7286af378ba3e46640ac9825156c93bdba7502174dd696090fdfcd4d80a1abd", size = 6758, upload-time = "2023-07-31T08:23:13.645Z" },
]
[[package]]
name = "execnet"
version = "2.1.2"
@ -2019,7 +2011,7 @@ wheels = [
[[package]]
name = "flask"
version = "3.1.3"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
@ -2029,9 +2021,9 @@ dependencies = [
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
]
[[package]]
@ -2256,31 +2248,24 @@ wheels = [
[[package]]
name = "gmpy2"
version = "2.3.0"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/57/86fd2ed7722cddfc7b1aa87cc768ef89944aa759b019595765aff5ad96a7/gmpy2-2.3.0.tar.gz", hash = "sha256:2d943cc9051fcd6b15b2a09369e2f7e18c526bc04c210782e4da61b62495eb4a", size = 302252, upload-time = "2026-02-08T00:57:42.808Z" }
sdist = { url = "https://files.pythonhosted.org/packages/07/bd/c6c154ce734a3e6187871b323297d8e5f3bdf9feaafc5212381538bc19e4/gmpy2-2.2.1.tar.gz", hash = "sha256:e83e07567441b78cb87544910cb3cc4fe94e7da987e93ef7622e76fb96650432", size = 234228, upload-time = "2024-07-21T05:33:00.715Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/70/0b5bde5f8e960c25ee18a352eb12bf5078d7fff3367c86d04985371de3f5/gmpy2-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2792ec96b2c4ee5af9f72409cd5b786edaf8277321f7022ce80ddff265815b01", size = 858392, upload-time = "2026-02-08T00:56:06.264Z" },
{ url = "https://files.pythonhosted.org/packages/c7/9b/2b52e92d0f1f36428e93ad7980634156fb5a1c88044984b0c03988951dc7/gmpy2-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3770aa5e44c5650d18232a0b8b8ed3d12db530d8278d4c800e4de5eef24cac5", size = 708753, upload-time = "2026-02-08T00:56:07.539Z" },
{ url = "https://files.pythonhosted.org/packages/e8/74/dac71b2f9f7844c40b38b6e43e3f793193420fd65573258147792cc069ce/gmpy2-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b4cee1fa3647505f53b81dc3b60ac49034768117f6295a04aaf4d3f216b821", size = 1674005, upload-time = "2026-02-08T00:56:10.932Z" },
{ url = "https://files.pythonhosted.org/packages/2c/29/16548784d70b2a58919720cb976a968b9b14a1b8ccebfe4a21d21647ecec/gmpy2-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd9f4124d7dc39d50896ba08820049a95f9f3952dcd6e072cc3a9d07361b7f1f", size = 1774200, upload-time = "2026-02-08T00:56:13.167Z" },
{ url = "https://files.pythonhosted.org/packages/75/c5/ef9efb075388e91c166f74234cd54897af7a2d3b93c66a9c3a266c796c99/gmpy2-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2f6b38e1b6d2aeb553c936c136c3a12cf983c9f9ce3e211b8632744a15f2bce7", size = 1693346, upload-time = "2026-02-08T00:56:14.999Z" },
{ url = "https://files.pythonhosted.org/packages/13/7e/1a1d6f50bb428434ca6930df0df6d9f8ad914c103106e60574b5df349f36/gmpy2-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:089229ef18b8d804a76fec9bd7e7d653f598a977e8354f7de8850731a48adb37", size = 1731821, upload-time = "2026-02-08T00:56:16.524Z" },
{ url = "https://files.pythonhosted.org/packages/49/47/f1140943bed78da59261edb377b9497b74f6e583d7accc9dc20592753a25/gmpy2-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:f1843f2ca5a1643fac7563a12a6a7d68e539d93de4afe5812355d32fb1613891", size = 1234877, upload-time = "2026-02-08T00:56:17.919Z" },
{ url = "https://files.pythonhosted.org/packages/64/44/a19e4a1628067bf7d27eeda2a1a874b1a5e750e2f5847cc2c49e90946eb5/gmpy2-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:cd5b92fa675dde5151ebe8d89814c78d573e5210cdc162016080782778f15654", size = 855570, upload-time = "2026-02-08T00:56:19.415Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/f70385e41b265b4f3534c7f41e78eefcf78dfe3a0d490816c697bb0703a9/gmpy2-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f35d6b1a8f067323a0a0d7034699284baebef498b030bbb29ab31d2ec13d1068", size = 857355, upload-time = "2026-02-08T00:56:20.674Z" },
{ url = "https://files.pythonhosted.org/packages/52/31/637015bd02bc74c6d854fc92ca1c24109a91691df07bc5e10bd14e09fd15/gmpy2-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:392d0560526dfa377c54c5c001d507fbbdea6cf54574895b90a97fc3587fa51e", size = 708996, upload-time = "2026-02-08T00:56:22.058Z" },
{ url = "https://files.pythonhosted.org/packages/f4/21/7f8bf79c486cff140aca76d958cdecfd1986cf989d28e14791a6e09004d8/gmpy2-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e900f41cc46700a5f49a4fbdcd5cd895e00bd0c2b9889fb2504ac1d594c21ac2", size = 1667404, upload-time = "2026-02-08T00:56:25.199Z" },
{ url = "https://files.pythonhosted.org/packages/86/1a/6efe94b7eb963362a7023b5c31157de703398d77320273a6dd7492736fff/gmpy2-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:713ba9b7a0a9098591f202e8f24f27ac5dd5001baf088ece1762852608a04b95", size = 1768643, upload-time = "2026-02-08T00:56:27.094Z" },
{ url = "https://files.pythonhosted.org/packages/5b/cf/9e9790f55b076d2010e282fc9a80bb4888c54b5e7fe359ae06a1d4bb76ea/gmpy2-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d2ed7b6d557b5d47068e889e2db204321ac855e001316a12928e4e7435f98637", size = 1683858, upload-time = "2026-02-08T00:56:28.422Z" },
{ url = "https://files.pythonhosted.org/packages/0f/02/1644480dc9f499f510979033a09069bb5a4fb3e75cf8f79c894d4ba17eed/gmpy2-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d135dcef824e26e1b3af544004d8f98564d090e7cf1001c50cc93d9dc1dc047", size = 1722019, upload-time = "2026-02-08T00:56:29.973Z" },
{ url = "https://files.pythonhosted.org/packages/5a/3f/5a74a2c9ac2e6076819649707293e16fd0384bee9f065f097d0f2fb89b0c/gmpy2-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:9dcbb628f9c806f0e6789f2c5e056e67e949b317af0e9ea0c3f0e0488c56e2a8", size = 1236149, upload-time = "2026-02-08T00:56:31.734Z" },
{ url = "https://files.pythonhosted.org/packages/59/34/e9157d26278462feca182515fd58de1e7a2bb5da0ee7ba80aeed0363776c/gmpy2-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:19022e0103aa76803b666720f107d8ab1941c597fd3fe70fadf7c49bac82a097", size = 856534, upload-time = "2026-02-08T00:56:33.059Z" },
{ url = "https://files.pythonhosted.org/packages/a1/10/f95d0103be9c1c458d5d92a72cca341a4ce0f1ca3ae6f79839d0f171f7ea/gmpy2-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:71dc3734104fa1f300d35ac6f55c7e98f7b0e1c7fd96f27b409110ed1c0c47d2", size = 840903, upload-time = "2026-02-08T00:57:34.192Z" },
{ url = "https://files.pythonhosted.org/packages/5b/50/677daeb75c038cdd773d575eefd34e96dbdd7b03c91166e56e6f8ed7acc2/gmpy2-2.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4623e700423396ef3d1658efa83b6feb0615fb68cb0b850e9ac0cba966db34c8", size = 691637, upload-time = "2026-02-08T00:57:35.495Z" },
{ url = "https://files.pythonhosted.org/packages/bd/cf/f1eb022f61c7bcc2dc428d345a7c012f0fabe1acb8db0d8216f23a46a915/gmpy2-2.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:692289a37442468856328986e0fab7e7e71c514bc470e1abae82d3bc54ca4cd2", size = 939209, upload-time = "2026-02-08T00:57:37.19Z" },
{ url = "https://files.pythonhosted.org/packages/db/ae/c651b8d903f4d8a65e4f959e2fd39c963d36cb2c6bfc452aa6d7db0fc5b3/gmpy2-2.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb379412033b52c3ec6bc44c6eaa134c88a068b6f1f360e6c13ca962082478ee", size = 1039433, upload-time = "2026-02-08T00:57:38.841Z" },
{ url = "https://files.pythonhosted.org/packages/53/1a/72844930f855d50b831a899f53365404ec81c165a68dea6ea3fa1668ba46/gmpy2-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8d087b262a0356c318a56fbb5c718e4e56762d861b2f9d581adc90a180264db9", size = 1233930, upload-time = "2026-02-08T00:57:40.228Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ec/ab67751ac0c4088ed21cf9a2a7f9966bf702ca8ebfc3204879cf58c90179/gmpy2-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98e947491c67523d3147a500f377bb64d0b115e4ab8a12d628fb324bb0e142bf", size = 880346, upload-time = "2024-07-21T05:31:25.531Z" },
{ url = "https://files.pythonhosted.org/packages/97/7c/bdc4a7a2b0e543787a9354e80fdcf846c4e9945685218cef4ca938d25594/gmpy2-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ccd319a3a87529484167ae1391f937ac4a8724169fd5822bbb541d1eab612b0", size = 694518, upload-time = "2024-07-21T05:31:27.78Z" },
{ url = "https://files.pythonhosted.org/packages/fc/44/ea903003bb4c3af004912fb0d6488e346bd76968f11a7472a1e60dee7dd7/gmpy2-2.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:827bcd433e5d62f1b732f45e6949419da4a53915d6c80a3c7a5a03d5a783a03a", size = 1653491, upload-time = "2024-07-21T05:31:29.968Z" },
{ url = "https://files.pythonhosted.org/packages/c9/70/5bce281b7cd664c04f1c9d47a37087db37b2be887bce738340e912ad86c8/gmpy2-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7131231fc96f57272066295c81cbf11b3233a9471659bca29ddc90a7bde9bfa", size = 1706487, upload-time = "2024-07-21T05:31:32.476Z" },
{ url = "https://files.pythonhosted.org/packages/2a/52/1f773571f21cf0319fc33218a1b384f29de43053965c05ed32f7e6729115/gmpy2-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1cc6f2bb68ee00c20aae554e111dc781a76140e00c31e4eda5c8f2d4168ed06c", size = 1637415, upload-time = "2024-07-21T05:31:34.591Z" },
{ url = "https://files.pythonhosted.org/packages/99/4c/390daf67c221b3f4f10b5b7d9293e61e4dbd48956a38947679c5a701af27/gmpy2-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae388fe46e3d20af4675451a4b6c12fc1bb08e6e0e69ee47072638be21bf42d8", size = 1657781, upload-time = "2024-07-21T05:31:36.81Z" },
{ url = "https://files.pythonhosted.org/packages/61/cd/86e47bccb3636389e29c4654a0e5ac52926d832897f2f64632639b63ffc1/gmpy2-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8b472ee3c123b77979374da2293ebf2c170b88212e173d64213104956d4678fb", size = 1203346, upload-time = "2024-07-21T05:31:39.344Z" },
{ url = "https://files.pythonhosted.org/packages/9a/ee/8f9f65e2bac334cfe13b3fc3f8962d5fc2858ebcf4517690d2d24afa6d0e/gmpy2-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90d03a1be1b1ad3944013fae5250316c3f4e6aec45ecdf189a5c7422d640004d", size = 885231, upload-time = "2024-07-21T05:31:41.471Z" },
{ url = "https://files.pythonhosted.org/packages/07/1c/bf29f6bf8acd72c3cf85d04e7db1bb26dd5507ee2387770bb787bc54e2a5/gmpy2-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd09dd43d199908c1d1d501c5de842b3bf754f99b94af5b5ef0e26e3b716d2d5", size = 696569, upload-time = "2024-07-21T05:31:43.768Z" },
{ url = "https://files.pythonhosted.org/packages/7c/cc/38d33eadeccd81b604a95b67d43c71b246793b7c441f1d7c3b41978cd1cf/gmpy2-2.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3232859fda3e96fd1aecd6235ae20476ed4506562bcdef6796a629b78bb96acd", size = 1655776, upload-time = "2024-07-21T05:31:46.272Z" },
{ url = "https://files.pythonhosted.org/packages/96/8d/d017599d6db8e9b96d6e84ea5102c33525cb71c82876b1813a2ece5d94ec/gmpy2-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30fba6f7cf43fb7f8474216701b5aaddfa5e6a06d560e88a67f814062934e863", size = 1707529, upload-time = "2024-07-21T05:31:48.732Z" },
{ url = "https://files.pythonhosted.org/packages/d0/93/91b4a0af23ae4216fd7ebcfd955dcbe152c5ef170598aee421310834de0a/gmpy2-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9b33cae533ede8173bc7d4bb855b388c5b636ca9f22a32c949f2eb7e0cc531b2", size = 1634195, upload-time = "2024-07-21T05:31:50.99Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ba/08ee99f19424cd33d5f0f17b2184e34d2fa886eebafcd3e164ccba15d9f2/gmpy2-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:954e7e1936c26e370ca31bbd49729ebeeb2006a8f9866b1e778ebb89add2e941", size = 1656779, upload-time = "2024-07-21T05:31:53.657Z" },
{ url = "https://files.pythonhosted.org/packages/14/e1/7b32ae2b23c8363d87b7f4bbac9abe9a1f820c2417d2e99ca3b4afd9379b/gmpy2-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c929870137b20d9c3f7dd97f43615b2d2c1a2470e50bafd9a5eea2e844f462e9", size = 1204668, upload-time = "2024-07-21T05:31:56.264Z" },
]
[[package]]
@ -2570,51 +2555,51 @@ wheels = [
[[package]]
name = "grimp"
version = "3.14"
version = "3.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/46/79764cfb61a3ac80dadae5d94fb10acdb7800e31fecf4113cf3d345e4952/grimp-3.14.tar.gz", hash = "sha256:645fbd835983901042dae4e1b24fde3a89bf7ac152f9272dd17a97e55cb4f871", size = 830882, upload-time = "2025-12-10T17:55:01.287Z" }
sdist = { url = "https://files.pythonhosted.org/packages/80/b3/ff0d704cdc5cf399d74aabd2bf1694d4c4c3231d4d74b011b8f39f686a86/grimp-3.13.tar.gz", hash = "sha256:759bf6e05186e6473ee71af4119ec181855b2b324f4fcdd78dee9e5b59d87874", size = 847508, upload-time = "2025-10-29T13:04:57.704Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/31/d4a86207c38954b6c3d859a1fc740a80b04bbe6e3b8a39f4e66f9633dfa4/grimp-3.14-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f1c91e3fa48c2196bf62e3c71492140d227b2bfcd6d15e735cbc0b3e2d5308e0", size = 2185572, upload-time = "2025-12-10T17:53:41.287Z" },
{ url = "https://files.pythonhosted.org/packages/f5/61/ed4cba5bd75d37fe46e17a602f616619a9e4f74ad8adfcf560ce4b2a1697/grimp-3.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6291c8f1690a9fe21b70923c60b075f4a89676541999e3d33084cbc69ac06a1", size = 2118002, upload-time = "2025-12-10T17:53:18.546Z" },
{ url = "https://files.pythonhosted.org/packages/77/6a/688f6144d0b207d7845bd8ab403820a83630ce3c9420cbbc7c9e9282f9c0/grimp-3.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ec312383935c2d09e4085c8435780ada2e13ebef14e105609c2988a02a5b2ce", size = 2283939, upload-time = "2025-12-10T17:52:06.228Z" },
{ url = "https://files.pythonhosted.org/packages/a5/98/4c540de151bf3fd58d6d7b3fe2269b6a6af6c61c915de1bc991802bfaff8/grimp-3.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f43cbf640e73ee703ad91639591046828d20103a1c363a02516e77a66a4ac07", size = 2233693, upload-time = "2025-12-10T17:52:18.938Z" },
{ url = "https://files.pythonhosted.org/packages/3e/7b/84b4b52b6c6dd5bf083cb1a72945748f56ea2e61768bbebf87e8d9d0ef75/grimp-3.14-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a93c9fddccb9ff16f5c6b5fca44227f5f86cba7cffc145d2176119603d2d7c7", size = 2389745, upload-time = "2025-12-10T17:53:00.659Z" },
{ url = "https://files.pythonhosted.org/packages/a7/33/31b96907c7dd78953df5e1ce67c558bd6057220fa1203d28d52566315a2e/grimp-3.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5653a2769fdc062cb7598d12200352069c9c6559b6643af6ada3639edb98fcc3", size = 2569055, upload-time = "2025-12-10T17:52:33.556Z" },
{ url = "https://files.pythonhosted.org/packages/b2/24/ce1a8110f3d5b178153b903aafe54b6a9216588b5bff3656e30af43e9c29/grimp-3.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:071c7ddf5e5bb7b2fdf79aefdf6e1c237cd81c095d6d0a19620e777e85bf103c", size = 2358044, upload-time = "2025-12-10T17:52:47.545Z" },
{ url = "https://files.pythonhosted.org/packages/05/7f/16d98c02287bc99884843478b9a68b04a2ef13b5cb8b9f36a9ca7daea75b/grimp-3.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e01b7a4419f535b667dfdcb556d3815b52981474f791fb40d72607228389a31", size = 2310304, upload-time = "2025-12-10T17:53:09.679Z" },
{ url = "https://files.pythonhosted.org/packages/a5/8c/0fde9781b0f6b4f9227d485685f48f6bcc70b95af22e2f85ff7f416cbfc1/grimp-3.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c29682f336151d1d018d0c3aa9eeaa35734b970e4593fa396b901edca7ef5c79", size = 2463682, upload-time = "2025-12-10T17:53:49.185Z" },
{ url = "https://files.pythonhosted.org/packages/51/cb/2baff301c2c2cc2792b6e225ea0784793ca587c81b97572be0bad122cfc8/grimp-3.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a5c4fd71f363ea39e8aab0630010ced77a8de9789f27c0acdd0d7e6269d4a8ef", size = 2500573, upload-time = "2025-12-10T17:54:03.899Z" },
{ url = "https://files.pythonhosted.org/packages/96/69/797e4242f42d6665da5fe22cb250cae3f14ece4cb22ad153e9cd97158179/grimp-3.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766911e3ba0b13d833fdd03ad1f217523a8a2b2527b5507335f71dca1153183d", size = 2503005, upload-time = "2025-12-10T17:54:32.993Z" },
{ url = "https://files.pythonhosted.org/packages/fd/45/da1a27a6377807ca427cd56534231f0920e1895e16630204f382a0df14c5/grimp-3.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:154e84a2053e9f858ae48743de23a5ad4eb994007518c29371276f59b8419036", size = 2515776, upload-time = "2025-12-10T17:54:47.962Z" },
{ url = "https://files.pythonhosted.org/packages/4f/8d/b918a29ce98029cd7a9e33a584be43a93288d5283fb7ccef5b6b2ba39ede/grimp-3.14-cp311-cp311-win32.whl", hash = "sha256:3189c86c3e73016a1907ee3ba9f7a6ca037e3601ad09e60ce9bf12b88877f812", size = 1873189, upload-time = "2025-12-10T17:55:11.872Z" },
{ url = "https://files.pythonhosted.org/packages/90/d7/2327c203f83a25766fbd62b0df3b24230d422b6e53518ff4d1c5e69793f1/grimp-3.14-cp311-cp311-win_amd64.whl", hash = "sha256:201f46a6a4e5ee9dfba4a2f7d043f7deab080d1d84233f4a1aee812678c25307", size = 2014277, upload-time = "2025-12-10T17:55:04.144Z" },
{ url = "https://files.pythonhosted.org/packages/75/d6/a35ff62f35aa5fd148053506eddd7a8f2f6afaed31870dc608dd0eb38e4f/grimp-3.14-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ffabc6940301214753bad89ec0bfe275892fa1f64b999e9a101f6cebfc777133", size = 2178573, upload-time = "2025-12-10T17:53:42.836Z" },
{ url = "https://files.pythonhosted.org/packages/93/e2/bd2e80273da4d46110969fc62252e5372e0249feb872bc7fe76fdc7f1818/grimp-3.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:075d9a1c78d607792d0ed8d4d3d7754a621ef04c8a95eaebf634930dc9232bb2", size = 2110452, upload-time = "2025-12-10T17:53:19.831Z" },
{ url = "https://files.pythonhosted.org/packages/44/c3/7307249c657d34dca9d250d73ba027d6cfe15a98fb3119b6e5210bc388b7/grimp-3.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ff52addeb20955a4d6aa097bee910573ffc9ef0d3c8a860844f267ad958156", size = 2283064, upload-time = "2025-12-10T17:52:07.673Z" },
{ url = "https://files.pythonhosted.org/packages/c7/d2/cae4cf32dc8d4188837cc4ab183300d655f898969b0f169e240f3b7c25be/grimp-3.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d10e0663e961fcbe8d0f54608854af31f911f164c96a44112d5173050132701f", size = 2235893, upload-time = "2025-12-10T17:52:20.418Z" },
{ url = "https://files.pythonhosted.org/packages/04/92/3f58bc3064fc305dac107d08003ba65713a5bc89a6d327f1c06b30cce752/grimp-3.14-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ab874d7ddddc7a1291259cf7c31a4e7b5c612e9da2e24c67c0eb1a44a624e67", size = 2393376, upload-time = "2025-12-10T17:53:02.397Z" },
{ url = "https://files.pythonhosted.org/packages/06/b8/f476f30edf114f04cb58e8ae162cb4daf52bda0ab01919f3b5b7edb98430/grimp-3.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54fec672ec83355636a852177f5a470c964bede0f6730f9ba3c7b5c8419c9eab", size = 2571342, upload-time = "2025-12-10T17:52:35.214Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ae/2e44d3c4f591f95f86322a8f4dbb5aac17001d49e079f3a80e07e7caaf09/grimp-3.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9e221b5e8070a916c780e88c877fee2a61c95a76a76a2a076396e459511b0bb", size = 2359022, upload-time = "2025-12-10T17:52:49.063Z" },
{ url = "https://files.pythonhosted.org/packages/69/ac/42b4d6bc0ea119ce2e91e1788feabf32c5433e9617dbb495c2a3d0dc7f12/grimp-3.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea6b495f9b4a8d82f5ce544921e76d0d12017f5d1ac3a3bd2f5ac88ab055b1c", size = 2309424, upload-time = "2025-12-10T17:53:11.069Z" },
{ url = "https://files.pythonhosted.org/packages/e8/c7/6a731989625c1790f4da7602dcbf9d6525512264e853cda77b3b3602d5e0/grimp-3.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:655e8d3f79cd99bb859e09c9dd633515150e9d850879ca71417d5ac31809b745", size = 2462754, upload-time = "2025-12-10T17:53:50.886Z" },
{ url = "https://files.pythonhosted.org/packages/cd/4d/3d1571c0a39a59dd68be4835f766da64fe64cbab0d69426210b716a8bdf0/grimp-3.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a14f10b1b71c6c37647a76e6a49c226509648107abc0f48c1e3ecd158ba05531", size = 2501356, upload-time = "2025-12-10T17:54:06.014Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d1/8950b8229095ebda5c54c8784e4d1f0a6e19423f2847289ef9751f878798/grimp-3.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:81685111ee24d3e25f8ed9e77ed00b92b58b2414e1a1c2937236026900972744", size = 2504631, upload-time = "2025-12-10T17:54:34.441Z" },
{ url = "https://files.pythonhosted.org/packages/0a/e6/23bed3da9206138d36d01890b656c7fb7adfb3a37daac8842d84d8777ade/grimp-3.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce8352a8ea0e27b143136ea086582fc6653419aa8a7c15e28ed08c898c42b185", size = 2514751, upload-time = "2025-12-10T17:54:49.384Z" },
{ url = "https://files.pythonhosted.org/packages/eb/45/6f1f55c97ee982f133ec5ccb22fc99bf5335aee70c208f4fb86cd833b8d5/grimp-3.14-cp312-cp312-win32.whl", hash = "sha256:3fc0f98b3c60d88e9ffa08faff3200f36604930972f8b29155f323b76ea25a06", size = 1875041, upload-time = "2025-12-10T17:55:13.326Z" },
{ url = "https://files.pythonhosted.org/packages/cf/cf/03ba01288e2a41a948bc8526f32c2eeaddd683ed34be1b895e31658d5a4c/grimp-3.14-cp312-cp312-win_amd64.whl", hash = "sha256:6bca77d1d50c8dc402c96af21f4e28e2f1e9938eeabd7417592a22bd83cde3c3", size = 2013868, upload-time = "2025-12-10T17:55:05.907Z" },
{ url = "https://files.pythonhosted.org/packages/65/cc/dbc00210d0324b8fc1242d8e857757c7e0b62ff0fc0c1bc8dcc42342da85/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c8a8aab9b4310a7e69d7d845cac21cf14563aa0520ea322b948eadeae56d303", size = 2284804, upload-time = "2025-12-10T17:52:16.379Z" },
{ url = "https://files.pythonhosted.org/packages/80/89/851d3d345342e9bcec3fe85d3997db29501fa59f958c1566bf3e24d9d7d9/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d781943b27e5875a41c8f9cfc80f8f0a349f864379192b8c3faa0e6a22593313", size = 2235176, upload-time = "2025-12-10T17:52:30.795Z" },
{ url = "https://files.pythonhosted.org/packages/58/78/5f94702a8d5c121cafcdc9664de34c34f19d0d91a1127bf3946a2631f7a3/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9630d4633607aff94d0ac84b9c64fef1382cdb05b00d9acbde47f8745e264871", size = 2391258, upload-time = "2025-12-10T17:53:06.906Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a2/df8c79de5c9e227856d048cc1551c4742a5f97660c40304ac278bd48607f/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb00e1bcca583668554a8e9e1e4229a1d11b0620969310aae40148829ff6a32", size = 2571443, upload-time = "2025-12-10T17:52:43.853Z" },
{ url = "https://files.pythonhosted.org/packages/f0/21/747b7ed9572bbdc34a76dfec12ce510e80164b1aa06d3b21b34994e5f567/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3389da4ceaaa7f7de24a668c0afc307a9f95997bd90f81ec359a828a9bd1d270", size = 2357767, upload-time = "2025-12-10T17:52:57.84Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e6/485c5e3b64933e71f72f0cc45b0d7130418a6a5a13cedc2e8411bd76f290/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd7a32970ef97e42d4e7369397c7795287d84a736d788ccb90b6c14f0561d975", size = 2309069, upload-time = "2025-12-10T17:53:15.203Z" },
{ url = "https://files.pythonhosted.org/packages/31/bd/12024a8cba1c77facc1422a7b48cd0d04c252fc9178fd6f99dc05a8af57b/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:fd1278623fa09f62abc0fd8a6500f31b421a1fd479980f44c2926020a0becf02", size = 2466429, upload-time = "2025-12-10T17:54:00.286Z" },
{ url = "https://files.pythonhosted.org/packages/ee/7f/0e5977887e1c8f00f84bb4125217534806ffdcef9cf52f3580aa3b151f4b/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:9cfa52c89333d3d8fe9dc782529e888270d060231c3783e036d424044671dde0", size = 2501190, upload-time = "2025-12-10T17:54:30.107Z" },
{ url = "https://files.pythonhosted.org/packages/42/6b/06acb94b6d0d8c7277bb3e33f93224aa3be5b04643f853479d3bf7b23ace/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:48a5be4a12fca6587e6885b4fc13b9e242ab8bf874519292f0f13814aecf52cc", size = 2503440, upload-time = "2025-12-10T17:54:44.444Z" },
{ url = "https://files.pythonhosted.org/packages/5b/4d/2e531370d12e7a564f67f680234710bbc08554238a54991cd244feb61fb6/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3fcc332466783a12a42cd317fd344c30fe734ba4fa2362efff132dc3f8d36da7", size = 2516525, upload-time = "2025-12-10T17:54:58.987Z" },
{ url = "https://files.pythonhosted.org/packages/45/cc/d272cf87728a7e6ddb44d3c57c1d3cbe7daf2ffe4dc76e3dc9b953b69ab1/grimp-3.13-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:57745996698932768274a2ed9ba3e5c424f60996c53ecaf1c82b75be9e819ee9", size = 2074518, upload-time = "2025-10-29T13:03:58.51Z" },
{ url = "https://files.pythonhosted.org/packages/06/11/31dc622c5a0d1615b20532af2083f4bba2573aebbba5b9d6911dfd60a37d/grimp-3.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca29f09710342b94fa6441f4d1102a0e49f0b463b1d91e43223baa949c5e9337", size = 1988182, upload-time = "2025-10-29T13:03:50.129Z" },
{ url = "https://files.pythonhosted.org/packages/aa/83/a0e19beb5c42df09e9a60711b227b4f910ba57f46bea258a9e1df883976c/grimp-3.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adda25aa158e11d96dd27166300b955c8ec0c76ce2fd1a13597e9af012aada06", size = 2145832, upload-time = "2025-10-29T13:02:35.218Z" },
{ url = "https://files.pythonhosted.org/packages/bc/f5/13752205e290588e970fdc019b4ab2c063ca8da352295c332e34df5d5842/grimp-3.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03e17029d75500a5282b40cb15cdae030bf14df9dfaa6a2b983f08898dfe74b6", size = 2106762, upload-time = "2025-10-29T13:02:51.681Z" },
{ url = "https://files.pythonhosted.org/packages/ff/30/c4d62543beda4b9a483a6cd5b7dd5e4794aafb511f144d21a452467989a1/grimp-3.13-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cbfc9d2d0ebc0631fb4012a002f3d8f4e3acb8325be34db525c0392674433b8", size = 2256674, upload-time = "2025-10-29T13:03:27.923Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ea/d07ed41b7121719c3f7bf30c9881dbde69efeacfc2daf4e4a628efe5f123/grimp-3.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:161449751a085484608c5b9f863e41e8fb2a98e93f7312ead5d831e487a94518", size = 2442699, upload-time = "2025-10-29T13:03:04.451Z" },
{ url = "https://files.pythonhosted.org/packages/fe/a0/1923f0480756effb53c7e6cef02a3918bb519a86715992720838d44f0329/grimp-3.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:119628fbe7f941d1e784edac98e8ced7e78a0b966a4ff2c449e436ee860bd507", size = 2317145, upload-time = "2025-10-29T13:03:15.941Z" },
{ url = "https://files.pythonhosted.org/packages/0d/d9/aef4c8350090653e34bc755a5d9e39cc300f5c46c651c1d50195f69bf9ab/grimp-3.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ca1ac776baf1fa105342b23c72f2e7fdd6771d4cce8d2903d28f92fd34a9e8f", size = 2180288, upload-time = "2025-10-29T13:03:41.023Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2e/a206f76eccffa56310a1c5d5950ed34923a34ae360cb38e297604a288837/grimp-3.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:941ff414cc66458f56e6af93c618266091ea70bfdabe7a84039be31d937051ee", size = 2328696, upload-time = "2025-10-29T13:04:06.888Z" },
{ url = "https://files.pythonhosted.org/packages/40/3b/88ff1554409b58faf2673854770e6fc6e90167a182f5166147b7618767d7/grimp-3.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:87ad9bcd1caaa2f77c369d61a04b9f2f1b87f4c3b23ae6891b2c943193c4ec62", size = 2367574, upload-time = "2025-10-29T13:04:21.404Z" },
{ url = "https://files.pythonhosted.org/packages/b6/b3/e9c99ecd94567465a0926ae7136e589aed336f6979a4cddcb8dfba16d27c/grimp-3.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:751fe37104a4f023d5c6556558b723d843d44361245c20f51a5d196de00e4774", size = 2358842, upload-time = "2025-10-29T13:04:34.26Z" },
{ url = "https://files.pythonhosted.org/packages/74/65/a5fffeeb9273e06dfbe962c8096331ba181ca8415c5f9d110b347f2c0c34/grimp-3.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b561f79ec0b3a4156937709737191ad57520f2d58fa1fc43cd79f67839a3cd7", size = 2382268, upload-time = "2025-10-29T13:04:46.864Z" },
{ url = "https://files.pythonhosted.org/packages/d9/79/2f3b4323184329b26b46de2b6d1bd64ba1c26e0a9c3cfa0aaecec237b75e/grimp-3.13-cp311-cp311-win32.whl", hash = "sha256:52405ea8c8f20cf5d2d1866c80ee3f0243a38af82bd49d1464c5e254bf2e1f8f", size = 1759345, upload-time = "2025-10-29T13:05:10.435Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ce/e86cf73e412a6bf531cbfa5c733f8ca48b28ebea23a037338be763f24849/grimp-3.13-cp311-cp311-win_amd64.whl", hash = "sha256:6a45d1d3beeefad69717b3718e53680fb3579fe67696b86349d6f39b75e850bf", size = 1859382, upload-time = "2025-10-29T13:05:01.071Z" },
{ url = "https://files.pythonhosted.org/packages/1d/06/ff7e3d72839f46f0fccdc79e1afe332318986751e20f65d7211a5e51366c/grimp-3.13-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3e715c56ffdd055e5c84d27b4c02d83369b733e6a24579d42bbbc284bd0664a9", size = 2070161, upload-time = "2025-10-29T13:03:59.755Z" },
{ url = "https://files.pythonhosted.org/packages/58/2f/a95bdf8996db9400fd7e288f32628b2177b8840fe5f6b7cd96247b5fa173/grimp-3.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f794dea35a4728b948ab8fec970ffbdf2589b34209f3ab902cf8a9148cf1eaad", size = 1984365, upload-time = "2025-10-29T13:03:51.805Z" },
{ url = "https://files.pythonhosted.org/packages/1f/45/cc3d7f3b7b4d93e0b9d747dc45ed73a96203ba083dc857f24159eb6966b4/grimp-3.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69571270f2c27e8a64b968195aa7ecc126797112a9bf1e804ff39ba9f42d6f6d", size = 2145486, upload-time = "2025-10-29T13:02:36.591Z" },
{ url = "https://files.pythonhosted.org/packages/16/92/a6e493b71cb5a9145ad414cc4790c3779853372b840a320f052b22879606/grimp-3.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f7b226398ae476762ef0afb5ef8f838d39c8e0e2f6d1a4378ce47059b221a4a", size = 2106747, upload-time = "2025-10-29T13:02:53.084Z" },
{ url = "https://files.pythonhosted.org/packages/db/8d/36a09f39fe14ad8843ef3ff81090ef23abbd02984c1fcc1cef30e5713d82/grimp-3.13-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5498aeac4df0131a1787fcbe9bb460b52fc9b781ec6bba607fd6a7d6d3ea6fce", size = 2257027, upload-time = "2025-10-29T13:03:29.44Z" },
{ url = "https://files.pythonhosted.org/packages/a1/7a/90f78787f80504caeef501f1bff47e8b9f6058d45995f1d4c921df17bfef/grimp-3.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4be702bb2b5c001a6baf709c452358470881e15e3e074cfc5308903603485dcb", size = 2441208, upload-time = "2025-10-29T13:03:05.733Z" },
{ url = "https://files.pythonhosted.org/packages/61/71/0fbd3a3e914512b9602fa24c8ebc85a8925b101f04f8a8c1d1e220e0a717/grimp-3.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fcf988f3e3d272a88f7be68f0c1d3719fee8624d902e9c0346b9015a0ea6a65", size = 2318758, upload-time = "2025-10-29T13:03:17.454Z" },
{ url = "https://files.pythonhosted.org/packages/34/e9/29c685e88b3b0688f0a2e30c0825e02076ecdf22bc0e37b1468562eaa09a/grimp-3.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ede36d104ff88c208140f978de3345f439345f35b8ef2b4390c59ef6984deba", size = 2180523, upload-time = "2025-10-29T13:03:42.3Z" },
{ url = "https://files.pythonhosted.org/packages/86/bc/7cc09574b287b8850a45051e73272f365259d9b6ca58d7b8773265c6fe35/grimp-3.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b35e44bb8dc80e0bd909a64387f722395453593a1884caca9dc0748efea33764", size = 2328855, upload-time = "2025-10-29T13:04:08.111Z" },
{ url = "https://files.pythonhosted.org/packages/34/86/3b0845900c8f984a57c6afe3409b20638065462d48b6afec0fd409fd6118/grimp-3.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:becb88e9405fc40896acd6e2b9bbf6f242a5ae2fd43a1ec0a32319ab6c10a227", size = 2367756, upload-time = "2025-10-29T13:04:22.736Z" },
{ url = "https://files.pythonhosted.org/packages/06/2d/4e70e8c06542db92c3fffaecb43ebfc4114a411505bff574d4da7d82c7db/grimp-3.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a66585b4af46c3fbadbef495483514bee037e8c3075ed179ba4f13e494eb7899", size = 2358595, upload-time = "2025-10-29T13:04:35.595Z" },
{ url = "https://files.pythonhosted.org/packages/dd/06/c511d39eb6c73069af277f4e74991f1f29a05d90cab61f5416b9fc43932f/grimp-3.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:29f68c6e2ff70d782ca0e989ec4ec44df73ba847937bcbb6191499224a2f84e2", size = 2381464, upload-time = "2025-10-29T13:04:48.265Z" },
{ url = "https://files.pythonhosted.org/packages/86/f5/42197d69e4c9e2e7eed091d06493da3824e07c37324155569aa895c3b5f7/grimp-3.13-cp312-cp312-win32.whl", hash = "sha256:cc996dcd1a44ae52d257b9a3e98838f8ecfdc42f7c62c8c82c2fcd3828155c98", size = 1758510, upload-time = "2025-10-29T13:05:11.74Z" },
{ url = "https://files.pythonhosted.org/packages/30/dd/59c5f19f51e25f3dbf1c9e88067a88165f649ba1b8e4174dbaf1c950f78b/grimp-3.13-cp312-cp312-win_amd64.whl", hash = "sha256:e2966435947e45b11568f04a65863dcf836343c11ae44aeefdaa7f07eb1a0576", size = 1859530, upload-time = "2025-10-29T13:05:02.638Z" },
{ url = "https://files.pythonhosted.org/packages/e5/81/82de1b5d82701214b1f8e32b2e71fde8e1edbb4f2cdca9beb22ee6c8796d/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6a3c76525b018c85c0e3a632d94d72be02225f8ada56670f3f213cf0762be4", size = 2145955, upload-time = "2025-10-29T13:02:47.559Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ae/ada18cb73bdf97094af1c60070a5b85549482a57c509ee9a23fdceed4fc3/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239e9b347af4da4cf69465bfa7b2901127f6057bc73416ba8187fb1eabafc6ea", size = 2107150, upload-time = "2025-10-29T13:02:59.891Z" },
{ url = "https://files.pythonhosted.org/packages/10/5e/6d8c65643ad5a1b6e00cc2cd8f56fc063923485f07c59a756fa61eefe7f2/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6db85ce2dc2f804a2edd1c1e9eaa46d282e1f0051752a83ca08ca8b87f87376", size = 2257515, upload-time = "2025-10-29T13:03:36.705Z" },
{ url = "https://files.pythonhosted.org/packages/b2/62/72cbfd7d0f2b95a53edd01d5f6b0d02bde38db739a727e35b76c13e0d0a8/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e000f3590bcc6ff7c781ebbc1ac4eb919f97180f13cc4002c868822167bd9aed", size = 2441262, upload-time = "2025-10-29T13:03:12.158Z" },
{ url = "https://files.pythonhosted.org/packages/18/00/b9209ab385567c3bddffb5d9eeecf9cb432b05c30ca8f35904b06e206a89/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2374c217c862c1af933a430192d6a7c6723ed1d90303f1abbc26f709bbb9263", size = 2318557, upload-time = "2025-10-29T13:03:23.925Z" },
{ url = "https://files.pythonhosted.org/packages/11/4d/a3d73c11d09da00a53ceafe2884a71c78f5a76186af6d633cadd6c85d850/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ed0ff17d559ff2e7fa1be8ae086bc4fedcace5d7b12017f60164db8d9a8d806", size = 2180811, upload-time = "2025-10-29T13:03:47.461Z" },
{ url = "https://files.pythonhosted.org/packages/c1/9a/1cdfaa7d7beefd8859b190dfeba11d5ec074e8702b2903e9f182d662ed63/grimp-3.13-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:43960234aabce018c8d796ec8b77c484a1c9cbb6a3bc036a0d307c8dade9874c", size = 2329205, upload-time = "2025-10-29T13:04:15.845Z" },
{ url = "https://files.pythonhosted.org/packages/86/73/b36f86ef98df96e7e8a6166dfa60c8db5d597f051e613a3112f39a870b4c/grimp-3.13-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:44420b638b3e303f32314bd4d309f15de1666629035acd1cdd3720c15917ac85", size = 2368745, upload-time = "2025-10-29T13:04:29.706Z" },
{ url = "https://files.pythonhosted.org/packages/02/2f/0ce37872fad5c4b82d727f6e435fd5bc76f701279bddc9666710318940cf/grimp-3.13-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:f6127fdb982cf135612504d34aa16b841f421e54751fcd54f80b9531decb2b3f", size = 2358753, upload-time = "2025-10-29T13:04:42.632Z" },
{ url = "https://files.pythonhosted.org/packages/bb/23/935c888ac9ee71184fe5adf5ea86648746739be23c85932857ac19fc1d17/grimp-3.13-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:69893a9ef1edea25226ed17e8e8981e32900c59703972e0780c0e927ce624f75", size = 2383066, upload-time = "2025-10-29T13:04:55.073Z" },
]
[[package]]
@ -2953,19 +2938,17 @@ wheels = [
[[package]]
name = "import-linter"
version = "2.10"
version = "2.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "fastapi" },
{ name = "grimp" },
{ name = "rich" },
{ name = "typing-extensions" },
{ name = "uvicorn" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/c4/a83cc1ea9ed0171725c0e2edc11fd929994d4f026028657e8b30d62bca37/import_linter-2.10.tar.gz", hash = "sha256:c6a5057d2dbd32e1854c4d6b60e90dfad459b7ab5356230486d8521f25872963", size = 1149263, upload-time = "2026-02-06T17:57:24.779Z" }
sdist = { url = "https://files.pythonhosted.org/packages/50/20/cc371a35123cd6afe4c8304cf199a53530a05f7437eda79ce84d9c6f6949/import_linter-2.7.tar.gz", hash = "sha256:7bea754fac9cde54182c81eeb48f649eea20b865219c39f7ac2abd23775d07d2", size = 219914, upload-time = "2025-11-19T11:44:28.193Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/e5/4b7b9435eac78ecfd537fa1004a0bcf0f4eac17d3a893f64d38a7bacb51b/import_linter-2.10-py3-none-any.whl", hash = "sha256:cc2ddd7ec0145cbf83f3b25391d2a5dbbf138382aaf80708612497fa6ebc8f60", size = 637081, upload-time = "2026-02-06T17:57:23.386Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b5/26a1d198f3de0676354a628f6e2a65334b744855d77e25eea739287eea9a/import_linter-2.7-py3-none-any.whl", hash = "sha256:be03bbd467b3f0b4535fb3ee12e07995d9837864b307df2e78888364e0ba012d", size = 46197, upload-time = "2025-11-19T11:44:27.023Z" },
]
[[package]]
@ -3938,33 +3921,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
]
[[package]]
name = "opensearch-protobufs"
version = "0.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio" },
{ name = "protobuf" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e2/8a09dbdbfe51e30dfecb625a0f5c524a53bfa4b1fba168f73ac85621dba2/opensearch_protobufs-0.19.0-py3-none-any.whl", hash = "sha256:5137c9c2323cc7debb694754b820ca4cfb5fc8eb180c41ff125698c3ee11bfc2", size = 39778, upload-time = "2025-09-29T20:05:52.379Z" },
]
[[package]]
name = "opensearch-py"
version = "3.1.0"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "events" },
{ name = "opensearch-protobufs" },
{ name = "python-dateutil" },
{ name = "requests" },
{ name = "six" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/65/9f/d4969f7e8fa221bfebf254cc3056e7c743ce36ac9874e06110474f7c947d/opensearch_py-3.1.0.tar.gz", hash = "sha256:883573af13175ff102b61c80b77934a9e937bdcc40cda2b92051ad53336bc055", size = 258616, upload-time = "2025-11-20T16:37:36.777Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/dc/acb182db6bb0c71f1e6e41c49260e01d68e52a03efb64e44aed3cc7f483f/opensearch-py-2.4.0.tar.gz", hash = "sha256:7eba2b6ed2ddcf33225bfebfba2aee026877838cc39f760ec80f27827308cc4b", size = 182924, upload-time = "2023-11-15T21:41:37.329Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/a1/293c8ad81768ad625283d960685bde07c6302abf20a685e693b48ab6eb91/opensearch_py-3.1.0-py3-none-any.whl", hash = "sha256:e5af83d0454323e6ea9ddee8c0dcc185c0181054592d23cb701da46271a3b65b", size = 385729, upload-time = "2025-11-20T16:37:34.941Z" },
{ url = "https://files.pythonhosted.org/packages/c1/98/178aacf07ece7f95d1948352778702898d57c286053813deb20ebb409923/opensearch_py-2.4.0-py2.py3-none-any.whl", hash = "sha256:316077235437c8ceac970232261f3393c65fb92a80f33c5b106f50f1dab24fd9", size = 258405, upload-time = "2023-11-15T21:41:35.59Z" },
]
[[package]]
@ -5043,11 +5013,11 @@ wheels = [
[[package]]
name = "pypdf"
version = "6.7.1"
version = "6.6.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/63/3437c4363483f2a04000a48f1cd48c40097f69d580363712fa8b0b4afe45/pypdf-6.7.1.tar.gz", hash = "sha256:6b7a63be5563a0a35d54c6d6b550d75c00b8ccf36384be96365355e296e6b3b0", size = 5302208, upload-time = "2026-02-17T17:00:48.88Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/bb/a44bab1ac3c54dbcf653d7b8bcdee93dddb2d3bf025a3912cacb8149a2f2/pypdf-6.6.2.tar.gz", hash = "sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016", size = 5281850, upload-time = "2026-01-26T11:57:55.964Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/77/38bd7744bb9e06d465b0c23879e6d2c187d93a383f8fa485c862822bb8a3/pypdf-6.7.1-py3-none-any.whl", hash = "sha256:a02ccbb06463f7c334ce1612e91b3e68a8e827f3cee100b9941771e6066b094e", size = 331048, upload-time = "2026-02-17T17:00:46.991Z" },
{ url = "https://files.pythonhosted.org/packages/7d/be/549aaf1dfa4ab4aed29b09703d2fb02c4366fc1f05e880948c296c5764b9/pypdf-6.6.2-py3-none-any.whl", hash = "sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba", size = 329132, upload-time = "2026-01-26T11:57:54.099Z" },
]
[[package]]
@ -6473,11 +6443,11 @@ wheels = [
[[package]]
name = "types-markdown"
version = "3.10.2.20260211"
version = "3.7.0.20250322"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/2e/35b30a09f6ee8a69142408d3ceb248c4454aa638c0a414d8704a3ef79563/types_markdown-3.10.2.20260211.tar.gz", hash = "sha256:66164310f88c11a58c6c706094c6f8c537c418e3525d33b76276a5fbd66b01ce", size = 19768, upload-time = "2026-02-11T04:19:29.497Z" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/fd/b4bd01b8c46f021c35a07aa31fe1dc45d21adc9fc8d53064bfa577aae73d/types_markdown-3.7.0.20250322.tar.gz", hash = "sha256:a48ed82dfcb6954592a10f104689d2d44df9125ce51b3cee20e0198a5216d55c", size = 18052, upload-time = "2025-03-22T02:48:46.193Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/c9/659fa2df04b232b0bfcd05d2418e683080e91ec68f636f3c0a5a267350e7/types_markdown-3.10.2.20260211-py3-none-any.whl", hash = "sha256:2d94d08587e3738203b3c4479c449845112b171abe8b5cadc9b0c12fcf3e99da", size = 25854, upload-time = "2026-02-11T04:19:28.647Z" },
{ url = "https://files.pythonhosted.org/packages/56/59/ee46617bc2b5e43bc06a000fdcd6358a013957e30ad545bed5e3456a4341/types_markdown-3.7.0.20250322-py3-none-any.whl", hash = "sha256:7e855503027b4290355a310fb834871940d9713da7c111f3e98a5e1cbc77acfb", size = 23699, upload-time = "2025-03-22T02:48:45.001Z" },
]
[[package]]
@ -7207,14 +7177,14 @@ wheels = [
[[package]]
name = "werkzeug"
version = "3.1.6"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
]
[[package]]

View File

@ -588,7 +588,7 @@ export default translation
const trimmedKeyLine = keyLine.trim()
// If key line ends with ":" (not complete value), it's likely multiline
if (trimmedKeyLine.endsWith(':') && !trimmedKeyLine.includes('{') && !/:\s*['"`]/.exec(trimmedKeyLine)) {
if (trimmedKeyLine.endsWith(':') && !trimmedKeyLine.includes('{') && !trimmedKeyLine.match(/:\s*['"`]/)) {
// Find the value lines that belong to this key
let currentLine = targetLineIndex + 1
let foundValue = false
@ -604,7 +604,7 @@ export default translation
}
// Check if this line starts a new key (indicates end of current value)
if (/^\w+\s*:/.exec(trimmed))
if (trimmed.match(/^\w+\s*:/))
break
// Check if this line is part of the value

View File

@ -94,7 +94,7 @@ const ConfigPopup: FC<PopupProps> = ({
const switchContent = (
<Switch
className="ml-3"
value={enabled}
defaultValue={enabled}
onChange={onStatusChange}
disabled={providerAllNotConfigured}
/>

View File

@ -144,7 +144,7 @@ const Annotation: FC<Props> = (props) => {
return (
<div className="flex h-full flex-col">
<p className="text-text-tertiary system-sm-regular">{t('description', { ns: 'appLog' })}</p>
<p className="system-sm-regular text-text-tertiary">{t('description', { ns: 'appLog' })}</p>
<div className="relative flex h-full flex-1 flex-col py-4">
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}>
<div className="flex items-center space-x-2">
@ -152,10 +152,10 @@ const Annotation: FC<Props> = (props) => {
<>
<div className={cn(!annotationConfig?.enabled && 'pr-2', 'flex h-7 items-center space-x-1 rounded-lg border border-components-panel-border bg-components-panel-bg-blur pl-2')}>
<MessageFast className="h-4 w-4 text-util-colors-indigo-indigo-600" />
<div className="text-text-primary system-sm-medium">{t('name', { ns: 'appAnnotation' })}</div>
<div className="system-sm-medium text-text-primary">{t('name', { ns: 'appAnnotation' })}</div>
<Switch
key={controlRefreshSwitch}
value={annotationConfig?.enabled ?? false}
defaultValue={annotationConfig?.enabled}
size="md"
onChange={async (value) => {
if (value) {

View File

@ -121,7 +121,7 @@ const ConfigVision: FC = () => {
<ParamConfig />
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-regular"></div>
<Switch
value={isImageEnabled}
defaultValue={isImageEnabled}
onChange={handleChange}
size="md"
/>

View File

@ -298,7 +298,7 @@ const AgentTools: FC = () => {
<div className={cn(item.isDeleted && 'opacity-50')}>
{!item.notAuthor && (
<Switch
value={item.isDeleted ? false : item.enabled}
defaultValue={item.isDeleted ? false : item.enabled}
disabled={item.isDeleted || readonly}
size="md"
onChange={(enabled) => {

View File

@ -69,7 +69,7 @@ const ConfigAudio: FC = () => {
<div className="flex shrink-0 items-center">
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle"></div>
<Switch
value={isAudioEnabled}
defaultValue={isAudioEnabled}
onChange={handleChange}
size="md"
/>

View File

@ -69,7 +69,7 @@ const ConfigDocument: FC = () => {
<div className="flex shrink-0 items-center">
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle"></div>
<Switch
value={isDocumentEnabled}
defaultValue={isDocumentEnabled}
onChange={handleChange}
size="md"
/>

View File

@ -188,14 +188,14 @@ const ConfigContent: FC<Props> = ({
return (
<div>
<div className="text-text-primary system-xl-semibold">{t('retrievalSettings', { ns: 'dataset' })}</div>
<div className="text-text-tertiary system-xs-regular">
<div className="system-xl-semibold text-text-primary">{t('retrievalSettings', { ns: 'dataset' })}</div>
<div className="system-xs-regular text-text-tertiary">
{t('defaultRetrievalTip', { ns: 'dataset' })}
</div>
{type === RETRIEVE_TYPE.multiWay && (
<>
<div className="my-2 flex flex-col items-center py-1">
<div className="mb-2 mr-2 shrink-0 text-text-secondary system-xs-semibold-uppercase">
<div className="system-xs-semibold-uppercase mb-2 mr-2 shrink-0 text-text-secondary">
{t('rerankSettings', { ns: 'dataset' })}
</div>
<Divider bgStyle="gradient" className="m-0 !h-px" />
@ -203,21 +203,21 @@ const ConfigContent: FC<Props> = ({
{
selectedDatasetsMode.inconsistentEmbeddingModel
&& (
<div className="mt-4 text-text-warning system-xs-medium">
<div className="system-xs-medium mt-4 text-text-warning">
{t('inconsistentEmbeddingModelTip', { ns: 'dataset' })}
</div>
)
}
{
selectedDatasetsMode.mixtureInternalAndExternal && (
<div className="mt-4 text-text-warning system-xs-medium">
<div className="system-xs-medium mt-4 text-text-warning">
{t('mixtureInternalAndExternalTip', { ns: 'dataset' })}
</div>
)
}
{
selectedDatasetsMode.allExternal && (
<div className="mt-4 text-text-warning system-xs-medium">
<div className="system-xs-medium mt-4 text-text-warning">
{t('allExternalTip', { ns: 'dataset' })}
</div>
)
@ -225,7 +225,7 @@ const ConfigContent: FC<Props> = ({
{
selectedDatasetsMode.mixtureHighQualityAndEconomic
&& (
<div className="mt-4 text-text-warning system-xs-medium">
<div className="system-xs-medium mt-4 text-text-warning">
{t('mixtureHighQualityAndEconomicTip', { ns: 'dataset' })}
</div>
)
@ -238,7 +238,7 @@ const ConfigContent: FC<Props> = ({
<div
key={option.value}
className={cn(
'flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary system-sm-medium',
'system-sm-medium flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary',
selectedRerankMode === option.value && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
)}
onClick={() => handleRerankModeChange(option.value)}
@ -267,12 +267,12 @@ const ConfigContent: FC<Props> = ({
canManuallyToggleRerank && (
<Switch
size="md"
value={showRerankModel ?? false}
defaultValue={showRerankModel}
onChange={handleManuallyToggleRerank}
/>
)
}
<div className="ml-1 leading-[32px] text-text-secondary system-sm-semibold">{t('modelProvider.rerankModel.key', { ns: 'common' })}</div>
<div className="system-sm-semibold ml-1 leading-[32px] text-text-secondary">{t('modelProvider.rerankModel.key', { ns: 'common' })}</div>
<Tooltip
popupContent={(
<div className="w-[200px]">

View File

@ -109,7 +109,7 @@ const Configuration: FC = () => {
const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
const isLoading = !hasFetchedDetail
const pathname = usePathname()
const matched = /\/app\/([^/]+)/.exec(pathname)
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const [mode, setMode] = useState<AppModeEnum>(AppModeEnum.CHAT)
const [publishedConfig, setPublishedConfig] = useState<PublishConfig | null>(null)

View File

@ -130,7 +130,7 @@ const Tools = () => {
className="flex h-7 cursor-pointer items-center px-3 text-xs font-medium text-gray-700"
onClick={() => handleOpenExternalDataToolModal({}, -1)}
>
<RiAddLine className="mr-[5px] h-3.5 w-3.5" />
<RiAddLine className="mr-[5px] h-3.5 w-3.5 " />
{t('operation.add', { ns: 'common' })}
</div>
</div>
@ -180,7 +180,7 @@ const Tools = () => {
<div className="ml-2 mr-3 hidden h-3.5 w-[1px] bg-gray-200 group-hover:block" />
<Switch
size="l"
value={item.enabled ?? false}
defaultValue={item.enabled}
onChange={(enabled: boolean) => handleSaveExternalDataToolModal({ ...item, enabled }, index)}
/>
</div>

View File

@ -260,7 +260,7 @@ function AppCard({
offset={24}
>
<div>
<Switch value={runningStatus} onChange={onChangeStatus} disabled={toggleDisabled} />
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={toggleDisabled} />
</div>
</Tooltip>
</div>

View File

@ -281,7 +281,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<div className="flex items-center justify-between">
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t('answerIcon.title', { ns: 'app' })}</div>
<Switch
value={inputInfo.use_icon_as_answer_icon}
defaultValue={inputInfo.use_icon_as_answer_icon}
onChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
/>
</div>
@ -315,7 +315,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
/>
<div className="flex items-center justify-between">
<p className={cn('body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.chatColorThemeInverted`, { ns: 'appOverview' })}</p>
<Switch value={inputInfo.chatColorThemeInverted} onChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
<Switch defaultValue={inputInfo.chatColorThemeInverted} onChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
</div>
</div>
</div>
@ -326,7 +326,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t(`${prefixSettings}.workflow.subTitle`, { ns: 'appOverview' })}</div>
<Switch
disabled={!(appInfo.mode === AppModeEnum.WORKFLOW || appInfo.mode === AppModeEnum.ADVANCED_CHAT)}
value={inputInfo.show_workflow_steps}
defaultValue={inputInfo.show_workflow_steps}
onChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
/>
</div>
@ -380,7 +380,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
>
<Switch
disabled={!webappCopyrightEnabled}
value={inputInfo.copyrightSwitchValue}
defaultValue={inputInfo.copyrightSwitchValue}
onChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/>
</Tooltip>

View File

@ -192,7 +192,7 @@ function TriggerCard({ appInfo, onToggleResult }: ITriggerCardProps) {
</div>
<div className="shrink-0">
<Switch
value={trigger.status === 'enabled'}
defaultValue={trigger.status === 'enabled'}
onChange={enabled => onToggleTrigger(trigger, enabled)}
disabled={!isCurrentWorkspaceEditor}
/>

View File

@ -70,7 +70,7 @@ const BlockInput: FC<IBlockInputProps> = ({
const renderSafeContent = (value: string) => {
const parts = value.split(/(\{\{[^}]+\}\}|\n)/g)
return parts.map((part, index) => {
const variableMatch = /^\{\{([^}]+)\}\}$/.exec(part)
const variableMatch = part.match(/^\{\{([^}]+)\}\}$/)
if (variableMatch) {
return (
<VarHighlight

View File

@ -17,7 +17,7 @@ const ContentItem = ({
const extractFieldName = (str: string): string => {
const outputVarRegex = /\{\{#\$output\.([^#]+)#\}\}/
const match = outputVarRegex.exec(str)
const match = str.match(outputVarRegex)
return match ? match[1] : ''
}

View File

@ -1,24 +0,0 @@
import { render, screen } from '@testing-library/react'
import { DaysOfWeek } from './days-of-week'
describe('DaysOfWeek', () => {
// Rendering test
describe('Rendering', () => {
it('should render 7 day labels', () => {
render(<DaysOfWeek />)
// The global i18n mock returns keys like "time.daysInWeek.Sun"
const dayElements = screen.getAllByText(/daysInWeek/)
expect(dayElements).toHaveLength(7)
})
it('should render each day of the week', () => {
render(<DaysOfWeek />)
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
days.forEach((day) => {
expect(screen.getByText(new RegExp(day))).toBeInTheDocument()
})
})
})
})

View File

@ -1,114 +0,0 @@
import type { CalendarProps, Day } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import dayjs from '../utils/dayjs'
import Calendar from './index'
// Mock scrollIntoView since jsdom doesn't implement it
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn()
})
// Factory for creating mock days
const createMockDays = (count: number = 7): Day[] => {
return Array.from({ length: count }, (_, i) => ({
date: dayjs('2024-06-01').add(i, 'day'),
isCurrentMonth: true,
}))
}
// Factory for Calendar props
const createCalendarProps = (overrides: Partial<CalendarProps> = {}): CalendarProps => ({
days: createMockDays(),
selectedDate: undefined,
onDateClick: vi.fn(),
...overrides,
})
describe('Calendar', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render days of week header', () => {
const props = createCalendarProps()
render(<Calendar {...props} />)
// DaysOfWeek component renders day labels
const dayLabels = screen.getAllByText(/daysInWeek/)
expect(dayLabels).toHaveLength(7)
})
it('should render all calendar day items', () => {
const days = createMockDays(7)
const props = createCalendarProps({ days })
render(<Calendar {...props} />)
const buttons = screen.getAllByRole('button')
expect(buttons).toHaveLength(7)
})
it('should accept wrapperClassName prop without errors', () => {
const props = createCalendarProps({ wrapperClassName: 'custom-class' })
const { container } = render(<Calendar {...props} />)
// Verify the component renders successfully with wrapperClassName
const dayLabels = screen.getAllByText(/daysInWeek/)
expect(dayLabels).toHaveLength(7)
expect(container.firstChild).toHaveClass('custom-class')
})
})
// Interaction tests
describe('Interactions', () => {
it('should call onDateClick when a day is clicked', () => {
const onDateClick = vi.fn()
const days = createMockDays(3)
const props = createCalendarProps({ days, onDateClick })
render(<Calendar {...props} />)
const dayButtons = screen.getAllByRole('button')
fireEvent.click(dayButtons[1])
expect(onDateClick).toHaveBeenCalledTimes(1)
expect(onDateClick).toHaveBeenCalledWith(days[1].date)
})
})
// Disabled dates tests
describe('Disabled Dates', () => {
it('should not call onDateClick for disabled dates', () => {
const onDateClick = vi.fn()
const days = createMockDays(3)
// Disable all dates
const getIsDateDisabled = vi.fn().mockReturnValue(true)
const props = createCalendarProps({ days, onDateClick, getIsDateDisabled })
render(<Calendar {...props} />)
const dayButtons = screen.getAllByRole('button')
fireEvent.click(dayButtons[0])
expect(onDateClick).not.toHaveBeenCalled()
})
it('should pass getIsDateDisabled to CalendarItem', () => {
const getIsDateDisabled = vi.fn().mockReturnValue(false)
const days = createMockDays(2)
const props = createCalendarProps({ days, getIsDateDisabled })
render(<Calendar {...props} />)
expect(getIsDateDisabled).toHaveBeenCalled()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should render empty calendar when days array is empty', () => {
const props = createCalendarProps({ days: [] })
render(<Calendar {...props} />)
expect(screen.queryAllByRole('button')).toHaveLength(0)
})
})
})

View File

@ -1,137 +0,0 @@
import type { CalendarItemProps, Day } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import dayjs from '../utils/dayjs'
import Item from './item'
const createMockDay = (overrides: Partial<Day> = {}): Day => ({
date: dayjs('2024-06-15'),
isCurrentMonth: true,
...overrides,
})
const createItemProps = (overrides: Partial<CalendarItemProps> = {}): CalendarItemProps => ({
day: createMockDay(),
selectedDate: undefined,
onClick: vi.fn(),
isDisabled: false,
...overrides,
})
describe('CalendarItem', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render the day number', () => {
const props = createItemProps()
render(<Item {...props} />)
expect(screen.getByRole('button', { name: '15' })).toBeInTheDocument()
})
})
describe('Visual States', () => {
it('should have selected styles when date matches selectedDate', () => {
const selectedDate = dayjs('2024-06-15')
const props = createItemProps({ selectedDate })
render(<Item {...props} />)
const button = screen.getByRole('button', { name: '15' })
expect(button).toHaveClass('bg-components-button-primary-bg', 'text-components-button-primary-text')
})
it('should not have selected styles when date does not match selectedDate', () => {
const selectedDate = dayjs('2024-06-16')
const props = createItemProps({ selectedDate })
render(<Item {...props} />)
const button = screen.getByRole('button', { name: '15' })
expect(button).not.toHaveClass('bg-components-button-primary-bg', 'text-components-button-primary-text')
})
it('should have different styles when day is not in current month', () => {
const props = createItemProps({
day: createMockDay({ isCurrentMonth: false }),
})
render(<Item {...props} />)
const button = screen.getByRole('button', { name: '15' })
expect(button).toHaveClass('text-text-quaternary')
})
it('should have different styles when day is in current month', () => {
const props = createItemProps({
day: createMockDay({ isCurrentMonth: true }),
})
render(<Item {...props} />)
const button = screen.getByRole('button', { name: '15' })
expect(button).toHaveClass('text-text-secondary')
})
})
describe('Click Behavior', () => {
it('should call onClick with the date when clicked', () => {
const onClick = vi.fn()
const day = createMockDay()
const props = createItemProps({ day, onClick })
render(<Item {...props} />)
fireEvent.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalledTimes(1)
expect(onClick).toHaveBeenCalledWith(day.date)
})
it('should not call onClick when isDisabled is true', () => {
const onClick = vi.fn()
const props = createItemProps({ onClick, isDisabled: true })
render(<Item {...props} />)
fireEvent.click(screen.getByRole('button'))
expect(onClick).not.toHaveBeenCalled()
})
})
describe('Today Indicator', () => {
it('should render today indicator when date is today', () => {
const today = dayjs()
const props = createItemProps({
day: createMockDay({ date: today }),
})
render(<Item {...props} />)
const button = screen.getByRole('button')
expect(button).toBeInTheDocument()
// Today's button should contain a child indicator element
expect(button.children.length).toBeGreaterThan(0)
})
it('should not render today indicator when date is not today', () => {
const notToday = dayjs('2020-01-01')
const props = createItemProps({
day: createMockDay({ date: notToday }),
})
render(<Item {...props} />)
const button = screen.getByRole('button')
// Non-today button should only contain the day number text, no extra children
expect(button.children.length).toBe(0)
})
})
describe('Edge Cases', () => {
it('should handle undefined selectedDate', () => {
const props = createItemProps({ selectedDate: undefined })
render(<Item {...props} />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
})
})

View File

@ -1,137 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react'
import OptionListItem from './option-list-item'
describe('OptionListItem', () => {
let originalScrollIntoView: Element['scrollIntoView']
beforeEach(() => {
vi.clearAllMocks()
originalScrollIntoView = Element.prototype.scrollIntoView
Element.prototype.scrollIntoView = vi.fn()
})
afterEach(() => {
Element.prototype.scrollIntoView = originalScrollIntoView
})
describe('Rendering', () => {
it('should render children content', () => {
render(
<OptionListItem isSelected={false} onClick={vi.fn()}>
Test Item
</OptionListItem>,
)
expect(screen.getByText('Test Item')).toBeInTheDocument()
})
it('should render as a list item element', () => {
render(
<OptionListItem isSelected={false} onClick={vi.fn()}>
Item
</OptionListItem>,
)
expect(screen.getByRole('listitem')).toBeInTheDocument()
})
})
describe('Selection State', () => {
it('should have selected styles when isSelected is true', () => {
render(
<OptionListItem isSelected={true} onClick={vi.fn()}>
Selected
</OptionListItem>,
)
const item = screen.getByRole('listitem')
expect(item).toHaveClass('bg-components-button-ghost-bg-hover')
})
it('should not have selected styles when isSelected is false', () => {
render(
<OptionListItem isSelected={false} onClick={vi.fn()}>
Not Selected
</OptionListItem>,
)
const item = screen.getByRole('listitem')
expect(item).not.toHaveClass('bg-components-button-ghost-bg-hover')
})
})
describe('Auto-Scroll', () => {
it('should scroll into view on mount when isSelected is true', () => {
render(
<OptionListItem isSelected={true} onClick={vi.fn()}>
Selected
</OptionListItem>,
)
expect(Element.prototype.scrollIntoView).toHaveBeenCalledWith({ behavior: 'instant' })
})
it('should not scroll into view on mount when isSelected is false', () => {
render(
<OptionListItem isSelected={false} onClick={vi.fn()}>
Not Selected
</OptionListItem>,
)
expect(Element.prototype.scrollIntoView).not.toHaveBeenCalled()
})
it('should not scroll into view on mount when noAutoScroll is true', () => {
render(
<OptionListItem isSelected={true} noAutoScroll onClick={vi.fn()}>
No Scroll
</OptionListItem>,
)
expect(Element.prototype.scrollIntoView).not.toHaveBeenCalled()
})
})
describe('Click Behavior', () => {
it('should call onClick when clicked', () => {
const handleClick = vi.fn()
render(
<OptionListItem isSelected={false} onClick={handleClick}>
Clickable
</OptionListItem>,
)
fireEvent.click(screen.getByRole('listitem'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should scroll into view with smooth behavior on click', () => {
render(
<OptionListItem isSelected={false} onClick={vi.fn()}>
Item
</OptionListItem>,
)
fireEvent.click(screen.getByRole('listitem'))
expect(Element.prototype.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' })
})
})
describe('Edge Cases', () => {
it('should handle rapid clicks without errors', () => {
const handleClick = vi.fn()
render(
<OptionListItem isSelected={false} onClick={handleClick}>
Rapid Click
</OptionListItem>,
)
const item = screen.getByRole('listitem')
fireEvent.click(item)
fireEvent.click(item)
fireEvent.click(item)
expect(handleClick).toHaveBeenCalledTimes(3)
})
})
})

View File

@ -1,97 +0,0 @@
import type { DatePickerFooterProps } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import { ViewType } from '../types'
import Footer from './footer'
// Factory for Footer props
const createFooterProps = (overrides: Partial<DatePickerFooterProps> = {}): DatePickerFooterProps => ({
needTimePicker: true,
displayTime: '02:30 PM',
view: ViewType.date,
handleClickTimePicker: vi.fn(),
handleSelectCurrentDate: vi.fn(),
handleConfirmDate: vi.fn(),
...overrides,
})
describe('DatePicker Footer', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render Now button and confirm button', () => {
const props = createFooterProps()
render(<Footer {...props} />)
expect(screen.getByText(/operation\.now/)).toBeInTheDocument()
expect(screen.getByText(/operation\.ok/)).toBeInTheDocument()
})
it('should show time picker button when needTimePicker is true', () => {
const props = createFooterProps({ needTimePicker: true, displayTime: '02:30 PM' })
render(<Footer {...props} />)
expect(screen.getByText('02:30 PM')).toBeInTheDocument()
})
it('should not show time picker button when needTimePicker is false', () => {
const props = createFooterProps({ needTimePicker: false })
render(<Footer {...props} />)
expect(screen.queryByText('02:30 PM')).not.toBeInTheDocument()
})
})
// View-dependent rendering tests
describe('View States', () => {
it('should show display time when view is date', () => {
const props = createFooterProps({ view: ViewType.date, displayTime: '10:00 AM' })
render(<Footer {...props} />)
expect(screen.getByText('10:00 AM')).toBeInTheDocument()
})
it('should show pickDate text when view is time', () => {
const props = createFooterProps({ view: ViewType.time })
render(<Footer {...props} />)
expect(screen.getByText(/operation\.pickDate/)).toBeInTheDocument()
})
})
// Interaction tests
describe('Interactions', () => {
it('should call handleClickTimePicker when time picker button is clicked', () => {
const handleClickTimePicker = vi.fn()
const props = createFooterProps({ handleClickTimePicker })
render(<Footer {...props} />)
// Click the time picker toggle button (has the time display)
fireEvent.click(screen.getByText('02:30 PM'))
expect(handleClickTimePicker).toHaveBeenCalledTimes(1)
})
it('should call handleSelectCurrentDate when Now button is clicked', () => {
const handleSelectCurrentDate = vi.fn()
const props = createFooterProps({ handleSelectCurrentDate })
render(<Footer {...props} />)
fireEvent.click(screen.getByText(/operation\.now/))
expect(handleSelectCurrentDate).toHaveBeenCalledTimes(1)
})
it('should call handleConfirmDate when OK button is clicked', () => {
const handleConfirmDate = vi.fn()
const props = createFooterProps({ handleConfirmDate })
render(<Footer {...props} />)
fireEvent.click(screen.getByText(/operation\.ok/))
expect(handleConfirmDate).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -1,78 +0,0 @@
import type { DatePickerHeaderProps } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import dayjs from '../utils/dayjs'
import Header from './header'
// Factory for Header props
const createHeaderProps = (overrides: Partial<DatePickerHeaderProps> = {}): DatePickerHeaderProps => ({
handleOpenYearMonthPicker: vi.fn(),
currentDate: dayjs('2024-06-15'),
onClickNextMonth: vi.fn(),
onClickPrevMonth: vi.fn(),
...overrides,
})
describe('DatePicker Header', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render month and year display', () => {
const props = createHeaderProps({ currentDate: dayjs('2024-06-15') })
render(<Header {...props} />)
// The useMonths hook returns translated keys; check for year
expect(screen.getByText(/2024/)).toBeInTheDocument()
})
it('should render navigation buttons', () => {
const props = createHeaderProps()
render(<Header {...props} />)
// There are 3 buttons: month/year display, prev, next
const buttons = screen.getAllByRole('button')
expect(buttons).toHaveLength(3)
})
})
// Interaction tests
describe('Interactions', () => {
it('should call handleOpenYearMonthPicker when month/year button is clicked', () => {
const handleOpenYearMonthPicker = vi.fn()
const props = createHeaderProps({ handleOpenYearMonthPicker })
render(<Header {...props} />)
// First button is the month/year display
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[0])
expect(handleOpenYearMonthPicker).toHaveBeenCalledTimes(1)
})
it('should call onClickPrevMonth when previous button is clicked', () => {
const onClickPrevMonth = vi.fn()
const props = createHeaderProps({ onClickPrevMonth })
render(<Header {...props} />)
// Second button is prev month
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[1])
expect(onClickPrevMonth).toHaveBeenCalledTimes(1)
})
it('should call onClickNextMonth when next button is clicked', () => {
const onClickNextMonth = vi.fn()
const props = createHeaderProps({ onClickNextMonth })
render(<Header {...props} />)
// Third button is next month
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[2])
expect(onClickNextMonth).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -1,616 +0,0 @@
import type { DatePickerProps } from '../types'
import { act, fireEvent, render, screen, within } from '@testing-library/react'
import dayjs from '../utils/dayjs'
import DatePicker from './index'
// Mock scrollIntoView
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn()
})
// Factory for DatePicker props
const createDatePickerProps = (overrides: Partial<DatePickerProps> = {}): DatePickerProps => ({
value: undefined,
onChange: vi.fn(),
onClear: vi.fn(),
...overrides,
})
// Helper to open the picker
const openPicker = () => {
const input = screen.getByRole('textbox')
fireEvent.click(input)
}
describe('DatePicker', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render with default placeholder', () => {
const props = createDatePickerProps()
render(<DatePicker {...props} />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should render with custom placeholder', () => {
const props = createDatePickerProps({ placeholder: 'Select date' })
render(<DatePicker {...props} />)
expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'Select date')
})
it('should display formatted date value when value is provided', () => {
const value = dayjs('2024-06-15T14:30:00')
const props = createDatePickerProps({ value })
render(<DatePicker {...props} />)
expect(screen.getByRole('textbox').getAttribute('value')).not.toBe('')
})
it('should render with empty value when no value is provided', () => {
const props = createDatePickerProps()
render(<DatePicker {...props} />)
expect(screen.getByRole('textbox')).toHaveValue('')
})
it('should normalize value with timezone applied', () => {
const value = dayjs('2024-06-15T14:30:00')
const props = createDatePickerProps({ value, timezone: 'America/New_York' })
render(<DatePicker {...props} />)
expect(screen.getByRole('textbox').getAttribute('value')).not.toBe('')
})
})
// Open/close behavior
describe('Open/Close Behavior', () => {
it('should open the picker when trigger is clicked', () => {
const props = createDatePickerProps()
render(<DatePicker {...props} />)
openPicker()
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
})
it('should close when trigger is clicked while open', () => {
const props = createDatePickerProps()
render(<DatePicker {...props} />)
openPicker()
openPicker() // second click closes
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should restore selected date from value when reopening', () => {
const value = dayjs('2024-06-15')
const props = createDatePickerProps({ value })
render(<DatePicker {...props} />)
openPicker()
// Calendar should be showing June 2024
expect(screen.getByText(/2024/)).toBeInTheDocument()
})
it('should close when clicking outside the container', () => {
const props = createDatePickerProps()
render(<DatePicker {...props} />)
openPicker()
// Simulate a mousedown event outside the container
act(() => {
document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
})
// The picker should now be closed - input shows its value
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
})
// Time Picker Integration
describe('Time Picker Integration', () => {
it('should show time display in footer when needTimePicker is true', () => {
const props = createDatePickerProps({ needTimePicker: true })
render(<DatePicker {...props} />)
openPicker()
expect(screen.getByText('--:-- --')).toBeInTheDocument()
})
it('should not show time toggle when needTimePicker is false', () => {
const props = createDatePickerProps({ needTimePicker: false })
render(<DatePicker {...props} />)
openPicker()
expect(screen.queryByText('--:-- --')).not.toBeInTheDocument()
})
it('should switch to time view when time picker button is clicked', () => {
const props = createDatePickerProps({ needTimePicker: true })
render(<DatePicker {...props} />)
openPicker()
// Click the time display button to switch to time view
fireEvent.click(screen.getByText('--:-- --'))
// In time view, the "pickDate" text should appear instead of the time
expect(screen.getByText(/operation\.pickDate/)).toBeInTheDocument()
})
it('should switch back to date view when pickDate is clicked in time view', () => {
const props = createDatePickerProps({ needTimePicker: true })
render(<DatePicker {...props} />)
openPicker()
// Switch to time view
fireEvent.click(screen.getByText('--:-- --'))
// Switch back to date view
fireEvent.click(screen.getByText(/operation\.pickDate/))
// Days of week should be visible again
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
})
it('should render time picker options in time view', () => {
const props = createDatePickerProps({ needTimePicker: true, value: dayjs('2024-06-15T14:30:00') })
render(<DatePicker {...props} />)
openPicker()
// Switch to time view
fireEvent.click(screen.getByText(/\d{2}:\d{2}\s(AM|PM)/))
// Should show AM/PM options (TimePickerOptions renders these)
expect(screen.getByText('AM')).toBeInTheDocument()
expect(screen.getByText('PM')).toBeInTheDocument()
})
it('should update selected time when hour is selected in time view', () => {
const props = createDatePickerProps({ needTimePicker: true, value: dayjs('2024-06-15T14:30:00') })
render(<DatePicker {...props} />)
openPicker()
// Switch to time view
fireEvent.click(screen.getByText(/\d{2}:\d{2}\s(AM|PM)/))
// Click hour "05" from the time options
const allLists = screen.getAllByRole('list')
const hourItems = within(allLists[0]).getAllByRole('listitem')
fireEvent.click(hourItems[4])
// The picker should still be in time view
expect(screen.getByText(/operation\.pickDate/)).toBeInTheDocument()
})
it('should update selected time when minute is selected in time view', () => {
const props = createDatePickerProps({ needTimePicker: true, value: dayjs('2024-06-15T14:30:00') })
render(<DatePicker {...props} />)
openPicker()
// Switch to time view
fireEvent.click(screen.getByText(/\d{2}:\d{2}\s(AM|PM)/))
// Click minute "45" from the time options
const allLists = screen.getAllByRole('list')
const minuteItems = within(allLists[1]).getAllByRole('listitem')
fireEvent.click(minuteItems[45])
expect(screen.getByText(/operation\.pickDate/)).toBeInTheDocument()
})
it('should update selected time when period is changed in time view', () => {
const props = createDatePickerProps({ needTimePicker: true, value: dayjs('2024-06-15T14:30:00') })
render(<DatePicker {...props} />)
openPicker()
// Switch to time view
fireEvent.click(screen.getByText(/\d{2}:\d{2}\s(AM|PM)/))
// Click AM to switch period
fireEvent.click(screen.getByText('AM'))
expect(screen.getByText(/operation\.pickDate/)).toBeInTheDocument()
})
it('should update time when no selectedDate exists and hour is selected', () => {
const props = createDatePickerProps({ needTimePicker: true })
render(<DatePicker {...props} />)
openPicker()
// Switch to time view (click on the "--:-- --" text)
fireEvent.click(screen.getByText('--:-- --'))
// Click hour "03" from the time options
const allLists = screen.getAllByRole('list')
const hourItems = within(allLists[0]).getAllByRole('listitem')
fireEvent.click(hourItems[2])
expect(screen.getByText(/operation\.pickDate/)).toBeInTheDocument()
})
})
// Date selection
describe('Date Selection', () => {
it('should call onChange when Now button is clicked', () => {
const onChange = vi.fn()
const props = createDatePickerProps({ onChange })
render(<DatePicker {...props} />)
openPicker()
fireEvent.click(screen.getByText(/operation\.now/))
expect(onChange).toHaveBeenCalledTimes(1)
})
it('should call onChange when OK button is clicked with a value', () => {
const onChange = vi.fn()
const props = createDatePickerProps({ onChange, value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
fireEvent.click(screen.getByText(/operation\.ok/))
expect(onChange).toHaveBeenCalledTimes(1)
})
it('should select a calendar day when clicked', () => {
const onChange = vi.fn()
const props = createDatePickerProps({ onChange, value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
// Click on a day in the calendar - day "20"
const dayButton = screen.getByRole('button', { name: '20' })
fireEvent.click(dayButton)
// The date should now appear in the header/display
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should immediately confirm when noConfirm is true and a date is clicked', () => {
const onChange = vi.fn()
const props = createDatePickerProps({ onChange, noConfirm: true, value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
// Click on a day
const dayButton = screen.getByRole('button', { name: '20' })
fireEvent.click(dayButton)
expect(onChange).toHaveBeenCalledTimes(1)
})
it('should call onChange with undefined when OK is clicked without a selected date', () => {
const onChange = vi.fn()
const props = createDatePickerProps({ onChange })
render(<DatePicker {...props} />)
openPicker()
// Clear selected date then confirm
fireEvent.click(screen.getByText(/operation\.ok/))
expect(onChange).toHaveBeenCalledTimes(1)
})
})
// Clear behavior
describe('Clear Behavior', () => {
it('should call onClear when clear is clicked while picker is closed', () => {
const onClear = vi.fn()
const renderTrigger = vi.fn(({ handleClear }) => (
<button data-testid="clear-trigger" onClick={handleClear}>
Clear
</button>
))
const props = createDatePickerProps({
value: dayjs('2024-06-15'),
onClear,
renderTrigger,
})
render(<DatePicker {...props} />)
fireEvent.click(screen.getByTestId('clear-trigger'))
expect(onClear).toHaveBeenCalledTimes(1)
})
it('should clear selected date without calling onClear when picker is open', () => {
const onClear = vi.fn()
const onChange = vi.fn()
const renderTrigger = vi.fn(({ handleClickTrigger, handleClear }) => (
<div>
<button data-testid="open-trigger" onClick={handleClickTrigger}>
Open
</button>
<button data-testid="clear-trigger" onClick={handleClear}>
Clear
</button>
</div>
))
const props = createDatePickerProps({
value: dayjs('2024-06-15'),
onClear,
onChange,
renderTrigger,
})
render(<DatePicker {...props} />)
fireEvent.click(screen.getByTestId('open-trigger'))
fireEvent.click(screen.getByTestId('clear-trigger'))
fireEvent.click(screen.getByText(/operation\.ok/))
expect(onClear).not.toHaveBeenCalled()
expect(onChange).toHaveBeenCalledWith(undefined)
})
})
// Month navigation
describe('Month Navigation', () => {
it('should navigate to next month when next arrow is clicked', () => {
const props = createDatePickerProps({ value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
// Find navigation buttons in the header
const allButtons = screen.getAllByRole('button')
// The header has: month/year button, prev button, next button
// Then calendar days are also buttons. We need the 3rd button (next month).
// Header buttons come first in DOM order.
fireEvent.click(allButtons[2]) // next month button
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
})
it('should navigate to previous month when prev arrow is clicked', () => {
const props = createDatePickerProps({ value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
const allButtons = screen.getAllByRole('button')
fireEvent.click(allButtons[1]) // prev month button
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
})
})
// Year/Month picker
describe('Year/Month Picker', () => {
it('should open year/month picker when month/year header is clicked', () => {
const props = createDatePickerProps({ value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
const headerButton = screen.getByText(/2024/)
fireEvent.click(headerButton)
// Cancel button visible in year/month picker footer
expect(screen.getByText(/operation\.cancel/)).toBeInTheDocument()
})
it('should close year/month picker when cancel is clicked', () => {
const props = createDatePickerProps({ value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
fireEvent.click(screen.getByText(/2024/))
// Cancel
fireEvent.click(screen.getByText(/operation\.cancel/))
// Should be back to date view with days of week
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
})
it('should confirm year/month selection when OK is clicked', () => {
const props = createDatePickerProps({ value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
fireEvent.click(screen.getByText(/2024/))
// Select a different year
fireEvent.click(screen.getByText('2023'))
// Confirm - click the last OK button (year/month footer)
const okButtons = screen.getAllByText(/operation\.ok/)
fireEvent.click(okButtons[okButtons.length - 1])
// Should return to date view
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
})
it('should close year/month picker by clicking header button', () => {
const props = createDatePickerProps({ value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
// Open year/month picker
fireEvent.click(screen.getByText(/2024/))
// The header in year/month view shows selected month/year with an up arrow
// Clicking it closes the year/month picker
const headerButtons = screen.getAllByRole('button')
fireEvent.click(headerButtons[0]) // First button in year/month view is the header
// Should return to date view
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
})
it('should update month selection in year/month picker', () => {
const props = createDatePickerProps({ value: dayjs('2024-06-15') })
render(<DatePicker {...props} />)
openPicker()
fireEvent.click(screen.getByText(/2024/))
// Select a different month using RTL queries
const allLists = screen.getAllByRole('list')
const monthItems = within(allLists[0]).getAllByRole('listitem')
fireEvent.click(monthItems[0])
// Confirm the selection - click the last OK button (year/month footer)
const okButtons = screen.getAllByText(/operation\.ok/)
fireEvent.click(okButtons[okButtons.length - 1])
// Should return to date view
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
})
})
// noConfirm mode
describe('noConfirm Mode', () => {
it('should not show footer when noConfirm is true', () => {
const props = createDatePickerProps({ noConfirm: true })
render(<DatePicker {...props} />)
openPicker()
expect(screen.queryByText(/operation\.ok/)).not.toBeInTheDocument()
})
})
// Custom trigger
describe('Custom Trigger', () => {
it('should use renderTrigger when provided', () => {
const renderTrigger = vi.fn(({ handleClickTrigger }) => (
<button data-testid="custom-trigger" onClick={handleClickTrigger}>
Custom
</button>
))
const props = createDatePickerProps({ renderTrigger })
render(<DatePicker {...props} />)
expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()
})
it('should open picker when custom trigger is clicked', () => {
const renderTrigger = vi.fn(({ handleClickTrigger }) => (
<button data-testid="custom-trigger" onClick={handleClickTrigger}>
Custom
</button>
))
const props = createDatePickerProps({ renderTrigger })
render(<DatePicker {...props} />)
fireEvent.click(screen.getByTestId('custom-trigger'))
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
})
})
// Disabled dates
describe('Disabled Dates', () => {
it('should pass getIsDateDisabled to calendar', () => {
const getIsDateDisabled = vi.fn().mockReturnValue(false)
const props = createDatePickerProps({
value: dayjs('2024-06-15'),
getIsDateDisabled,
})
render(<DatePicker {...props} />)
openPicker()
expect(getIsDateDisabled).toHaveBeenCalled()
})
})
// Timezone
describe('Timezone', () => {
it('should render with timezone', () => {
const props = createDatePickerProps({
value: dayjs('2024-06-15'),
timezone: 'UTC',
})
render(<DatePicker {...props} />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should call onChange when timezone changes with a value', () => {
const onChange = vi.fn()
const props = createDatePickerProps({
value: dayjs('2024-06-15T14:30:00'),
timezone: 'UTC',
onChange,
})
const { rerender } = render(<DatePicker {...props} />)
// Change timezone
rerender(<DatePicker {...props} timezone="America/New_York" />)
expect(onChange).toHaveBeenCalled()
})
it('should update currentDate when timezone changes without a value', () => {
const onChange = vi.fn()
const props = createDatePickerProps({
timezone: 'UTC',
onChange,
})
const { rerender } = render(<DatePicker {...props} />)
// Change timezone with no value
rerender(<DatePicker {...props} timezone="America/New_York" />)
// onChange should NOT be called when there is no value
expect(onChange).not.toHaveBeenCalled()
})
it('should update selectedDate when timezone changes and value is present', () => {
const onChange = vi.fn()
const value = dayjs('2024-06-15T14:30:00')
const props = createDatePickerProps({
value,
timezone: 'UTC',
onChange,
})
const { rerender } = render(<DatePicker {...props} />)
// Change timezone
rerender(<DatePicker {...props} timezone="Asia/Tokyo" />)
// Should have been called with the new timezone-adjusted value
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(emitted.isValid()).toBe(true)
})
})
// Display time when selected date exists
describe('Time Display', () => {
it('should show formatted time when selectedDate exists', () => {
const value = dayjs('2024-06-15T14:30:00')
const props = createDatePickerProps({ value, needTimePicker: true })
render(<DatePicker {...props} />)
openPicker()
// The footer should show the time from selectedDate (02:30 PM)
expect(screen.getByText(/\d{2}:\d{2}\s(AM|PM)/)).toBeInTheDocument()
})
})
})

View File

@ -1,94 +0,0 @@
import { renderHook } from '@testing-library/react'
import { useDaysOfWeek, useMonths, useTimeOptions, useYearOptions } from './hooks'
import { Period } from './types'
import dayjs from './utils/dayjs'
describe('date-and-time-picker hooks', () => {
// Tests for useDaysOfWeek hook
describe('useDaysOfWeek', () => {
it('should return 7 days of the week', () => {
const { result } = renderHook(() => useDaysOfWeek())
expect(result.current).toHaveLength(7)
})
it('should return translated day keys with namespace prefix', () => {
const { result } = renderHook(() => useDaysOfWeek())
// Global i18n mock returns "time.daysInWeek.<day>" format
expect(result.current[0]).toContain('daysInWeek.Sun')
expect(result.current[6]).toContain('daysInWeek.Sat')
})
})
// Tests for useMonths hook
describe('useMonths', () => {
it('should return 12 months', () => {
const { result } = renderHook(() => useMonths())
expect(result.current).toHaveLength(12)
})
it('should return translated month keys with namespace prefix', () => {
const { result } = renderHook(() => useMonths())
expect(result.current[0]).toContain('months.January')
expect(result.current[11]).toContain('months.December')
})
})
// Tests for useYearOptions hook
describe('useYearOptions', () => {
it('should return 200 year options', () => {
const { result } = renderHook(() => useYearOptions())
expect(result.current).toHaveLength(200)
})
it('should center around the current year', () => {
const { result } = renderHook(() => useYearOptions())
const currentYear = dayjs().year()
expect(result.current).toContain(currentYear)
// First year should be currentYear - 50 (YEAR_RANGE/2 = 50)
expect(result.current[0]).toBe(currentYear - 50)
// Last year should be currentYear + 149
expect(result.current[199]).toBe(currentYear + 149)
})
})
// Tests for useTimeOptions hook
describe('useTimeOptions', () => {
it('should return 12 hour options', () => {
const { result } = renderHook(() => useTimeOptions())
expect(result.current.hourOptions).toHaveLength(12)
})
it('should return hours from 01 to 12 zero-padded', () => {
const { result } = renderHook(() => useTimeOptions())
expect(result.current.hourOptions[0]).toBe('01')
expect(result.current.hourOptions[11]).toBe('12')
})
it('should return 60 minute options', () => {
const { result } = renderHook(() => useTimeOptions())
expect(result.current.minuteOptions).toHaveLength(60)
})
it('should return minutes from 00 to 59 zero-padded', () => {
const { result } = renderHook(() => useTimeOptions())
expect(result.current.minuteOptions[0]).toBe('00')
expect(result.current.minuteOptions[59]).toBe('59')
})
it('should return AM and PM period options', () => {
const { result } = renderHook(() => useTimeOptions())
expect(result.current.periodOptions).toEqual([Period.AM, Period.PM])
})
})
})

View File

@ -1,50 +0,0 @@
import type { TimePickerFooterProps } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import Footer from './footer'
// Factory for TimePickerFooter props
const createFooterProps = (overrides: Partial<TimePickerFooterProps> = {}): TimePickerFooterProps => ({
handleSelectCurrentTime: vi.fn(),
handleConfirm: vi.fn(),
...overrides,
})
describe('TimePicker Footer', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render Now and OK buttons', () => {
const props = createFooterProps()
render(<Footer {...props} />)
expect(screen.getByText(/operation\.now/)).toBeInTheDocument()
expect(screen.getByText(/operation\.ok/)).toBeInTheDocument()
})
})
// Interaction tests
describe('Interactions', () => {
it('should call handleSelectCurrentTime when Now button is clicked', () => {
const handleSelectCurrentTime = vi.fn()
const props = createFooterProps({ handleSelectCurrentTime })
render(<Footer {...props} />)
fireEvent.click(screen.getByText(/operation\.now/))
expect(handleSelectCurrentTime).toHaveBeenCalledTimes(1)
})
it('should call handleConfirm when OK button is clicked', () => {
const handleConfirm = vi.fn()
const props = createFooterProps({ handleConfirm })
render(<Footer {...props} />)
fireEvent.click(screen.getByText(/operation\.ok/))
expect(handleConfirm).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -1,30 +0,0 @@
import { render, screen } from '@testing-library/react'
import Header from './header'
describe('TimePicker Header', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render default title when no title prop is provided', () => {
render(<Header />)
// Global i18n mock returns the key with namespace prefix
expect(screen.getByText(/title\.pickTime/)).toBeInTheDocument()
})
it('should render custom title when title prop is provided', () => {
render(<Header title="Custom Title" />)
expect(screen.getByText('Custom Title')).toBeInTheDocument()
})
it('should not render default title when custom title is provided', () => {
render(<Header title="Custom Title" />)
expect(screen.queryByText(/title\.pickTime/)).not.toBeInTheDocument()
})
})
})

View File

@ -1,12 +1,42 @@
import type { TimePickerProps } from '../types'
import { fireEvent, render, screen, within } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import dayjs, { isDayjsObject } from '../utils/dayjs'
import TimePicker from './index'
// Mock scrollIntoView since jsdom doesn't implement it
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn()
})
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
if (key === 'defaultPlaceholder')
return 'Pick a time...'
if (key === 'operation.now')
return 'Now'
if (key === 'operation.ok')
return 'OK'
if (key === 'operation.clear')
return 'Clear'
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
PortalToFollowElem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: (e: React.MouseEvent) => void }) => (
<div onClick={onClick}>{children}</div>
),
PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="timepicker-content">{children}</div>
),
}))
vi.mock('./options', () => ({
default: () => <div data-testid="time-options" />,
}))
vi.mock('./header', () => ({
default: () => <div data-testid="time-header" />,
}))
describe('TimePicker', () => {
const baseProps: Pick<TimePickerProps, 'onChange' | 'onClear' | 'value'> = {
@ -43,10 +73,10 @@ describe('TimePicker', () => {
const input = screen.getByRole('textbox')
fireEvent.click(input)
const clearButton = screen.getByRole('button', { name: /operation\.clear/i })
const clearButton = screen.getByRole('button', { name: /clear/i })
fireEvent.click(clearButton)
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
const confirmButton = screen.getByRole('button', { name: 'OK' })
fireEvent.click(confirmButton)
expect(baseProps.onChange).toHaveBeenCalledTimes(1)
@ -64,10 +94,7 @@ describe('TimePicker', () => {
/>,
)
// Open the picker first to access content
fireEvent.click(screen.getByRole('textbox'))
const nowButton = screen.getByRole('button', { name: /operation\.now/i })
const nowButton = screen.getByRole('button', { name: 'Now' })
fireEvent.click(nowButton)
expect(onChange).toHaveBeenCalledTimes(1)
@ -76,601 +103,6 @@ describe('TimePicker', () => {
expect(emitted?.utcOffset()).toBe(dayjs().tz('America/New_York').utcOffset())
})
// Opening and closing behavior tests
describe('Open/Close Behavior', () => {
it('should show placeholder when no value is provided', () => {
render(<TimePicker {...baseProps} />)
const input = screen.getByRole('textbox')
expect(input).toHaveAttribute('placeholder', expect.stringMatching(/defaultPlaceholder/i))
})
it('should toggle open state when trigger is clicked', () => {
render(<TimePicker {...baseProps} value="10:00 AM" timezone="UTC" />)
const input = screen.getByRole('textbox')
// Open
fireEvent.click(input)
expect(input).toHaveValue('')
// Close by clicking again
fireEvent.click(input)
expect(input).toHaveValue('10:00 AM')
})
it('should call onClear when clear is clicked while picker is closed', () => {
const onClear = vi.fn()
render(
<TimePicker
{...baseProps}
onClear={onClear}
value="10:00 AM"
timezone="UTC"
/>,
)
const clearButton = screen.getByRole('button', { name: /operation\.clear/i })
fireEvent.click(clearButton)
expect(onClear).toHaveBeenCalledTimes(1)
})
it('should not call onClear when clear is clicked while picker is open', () => {
const onClear = vi.fn()
render(
<TimePicker
{...baseProps}
onClear={onClear}
value="10:00 AM"
timezone="UTC"
/>,
)
// Open picker first
fireEvent.click(screen.getByRole('textbox'))
// Then clear
const clearButton = screen.getByRole('button', { name: /operation\.clear/i })
fireEvent.click(clearButton)
expect(onClear).not.toHaveBeenCalled()
})
it('should register click outside listener on mount', () => {
const addEventSpy = vi.spyOn(document, 'addEventListener')
render(<TimePicker {...baseProps} value="10:00 AM" timezone="UTC" />)
expect(addEventSpy).toHaveBeenCalledWith('mousedown', expect.any(Function))
addEventSpy.mockRestore()
})
it('should sync selectedTime from value when opening with stale state', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
value="10:00 AM"
timezone="UTC"
/>,
)
const input = screen.getByRole('textbox')
// Open - this triggers handleClickTrigger which syncs selectedTime from value
fireEvent.click(input)
// Confirm to verify selectedTime was synced from value prop ("10:00 AM")
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true)
expect(emitted.hour()).toBe(10)
expect(emitted.minute()).toBe(0)
})
it('should resync selectedTime when opening after internal clear', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T10:30:00Z')}
timezone="UTC"
/>,
)
const input = screen.getByRole('textbox')
// Open
fireEvent.click(input)
// Clear selected time internally
const clearButton = screen.getByRole('button', { name: /operation\.clear/i })
fireEvent.click(clearButton)
// Close
fireEvent.click(input)
// Open again - should resync selectedTime from value prop
fireEvent.click(input)
// Confirm to verify the value was resynced
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true)
// Resynced from value prop: dayjs('2024-01-01T10:30:00Z') in UTC = 10:30 AM
expect(emitted.hour()).toBe(10)
expect(emitted.minute()).toBe(30)
})
})
// Props tests
describe('Props', () => {
it('should show custom placeholder when provided', () => {
render(
<TimePicker
{...baseProps}
placeholder="Select time"
/>,
)
const input = screen.getByRole('textbox')
expect(input).toHaveAttribute('placeholder', 'Select time')
})
it('should render with triggerFullWidth prop without errors', () => {
render(
<TimePicker
{...baseProps}
triggerFullWidth={true}
/>,
)
// Verify the component renders successfully with triggerFullWidth
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should use renderTrigger when provided', () => {
const renderTrigger = vi.fn(({ inputElem, onClick }) => (
<div data-testid="custom-trigger" onClick={onClick}>
{inputElem}
</div>
))
render(
<TimePicker
{...baseProps}
renderTrigger={renderTrigger}
/>,
)
expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()
expect(renderTrigger).toHaveBeenCalled()
})
it('should render with notClearable prop without errors', () => {
render(
<TimePicker
{...baseProps}
notClearable={true}
value="10:00 AM"
timezone="UTC"
/>,
)
// In test env the icon stays in DOM, but must remain hidden when notClearable is set
expect(screen.getByRole('button', { name: /clear/i })).toHaveClass('hidden')
})
})
// Confirm behavior tests
describe('Confirm Behavior', () => {
it('should emit selected time when confirm is clicked with a value', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T10:30:00Z')}
timezone="UTC"
/>,
)
// Open the picker first to access content
fireEvent.click(screen.getByRole('textbox'))
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true)
expect(emitted.hour()).toBe(10)
expect(emitted.minute()).toBe(30)
})
})
// Time selection handler tests
describe('Time Selection', () => {
const openPicker = () => {
fireEvent.click(screen.getByRole('textbox'))
}
const getHourAndMinuteLists = () => {
const allLists = screen.getAllByRole('list')
const hourList = allLists.find(list =>
within(list).queryByText('01')
&& within(list).queryByText('12')
&& !within(list).queryByText('59'))
const minuteList = allLists.find(list =>
within(list).queryByText('00')
&& within(list).queryByText('59'))
expect(hourList).toBeTruthy()
expect(minuteList).toBeTruthy()
return {
hourList: hourList!,
minuteList: minuteList!,
}
}
it('should update selectedTime when hour is selected', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T10:30:00Z')}
timezone="UTC"
/>,
)
openPicker()
// Click hour "05" from the time options
const { hourList } = getHourAndMinuteLists()
fireEvent.click(within(hourList).getByText('05'))
// Now confirm to verify the selectedTime was updated
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true)
// Hour 05 in AM (since original was 10:30 AM) = 5
expect(emitted.hour()).toBe(5)
})
it('should update selectedTime when minute is selected', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T10:30:00Z')}
timezone="UTC"
/>,
)
openPicker()
// Click minute "45" from the time options
const { minuteList } = getHourAndMinuteLists()
fireEvent.click(within(minuteList).getByText('45'))
// Confirm
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(emitted.minute()).toBe(45)
})
it('should update selectedTime when period is changed', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T10:30:00Z')}
timezone="UTC"
/>,
)
openPicker()
// Click PM to switch period
fireEvent.click(screen.getByText('PM'))
// Confirm
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
// Original was 10:30 AM, switching to PM makes it 22:30
expect(emitted.hour()).toBe(22)
})
it('should create new time when selecting hour without prior selectedTime', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
timezone="UTC"
/>,
)
openPicker()
// Click hour "03" with no existing selectedTime
const { hourList } = getHourAndMinuteLists()
fireEvent.click(within(hourList).getByText('03'))
// Confirm
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true)
expect(emitted.hour()).toBe(3)
})
it('should handle minute selection without prior selectedTime', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
timezone="UTC"
/>,
)
openPicker()
// Click minute "15" with no existing selectedTime
const { minuteList } = getHourAndMinuteLists()
fireEvent.click(within(minuteList).getByText('15'))
// Confirm
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(emitted.minute()).toBe(15)
})
it('should handle period selection without prior selectedTime', () => {
const onChange = vi.fn()
render(
<TimePicker
{...baseProps}
onChange={onChange}
timezone="UTC"
/>,
)
openPicker()
// Click PM with no existing selectedTime
fireEvent.click(screen.getByText('PM'))
// Confirm
const confirmButton = screen.getByRole('button', { name: /operation\.ok/i })
fireEvent.click(confirmButton)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true)
expect(emitted.hour()).toBeGreaterThanOrEqual(12)
})
})
// Timezone change effect tests
describe('Timezone Changes', () => {
it('should call onChange when timezone changes with an existing value', () => {
const onChange = vi.fn()
const value = dayjs('2024-01-01T10:30:00Z')
const { rerender } = render(
<TimePicker
{...baseProps}
onChange={onChange}
value={value}
timezone="UTC"
/>,
)
// Change timezone without changing value (same reference)
rerender(
<TimePicker
{...baseProps}
onChange={onChange}
value={value}
timezone="America/New_York"
/>,
)
expect(onChange).toHaveBeenCalledTimes(1)
const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true)
// 10:30 UTC converted to America/New_York (UTC-5 in Jan) = 05:30
expect(emitted.utcOffset()).toBe(dayjs().tz('America/New_York').utcOffset())
expect(emitted.hour()).toBe(5)
expect(emitted.minute()).toBe(30)
})
it('should update selectedTime when value changes', () => {
const onChange = vi.fn()
const { rerender } = render(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T10:30:00Z')}
timezone="UTC"
/>,
)
// Change value
rerender(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T14:00:00Z')}
timezone="UTC"
/>,
)
// onChange should not be called when only value changes (no timezone change)
expect(onChange).not.toHaveBeenCalled()
// But the display should update
expect(screen.getByDisplayValue('02:00 PM')).toBeInTheDocument()
})
it('should handle timezone change when value is undefined', () => {
const onChange = vi.fn()
const { rerender } = render(
<TimePicker
{...baseProps}
onChange={onChange}
timezone="UTC"
/>,
)
// Change timezone without a value
rerender(
<TimePicker
{...baseProps}
onChange={onChange}
timezone="America/New_York"
/>,
)
// onChange should not be called when value is undefined
expect(onChange).not.toHaveBeenCalled()
})
it('should handle timezone change when selectedTime exists but value becomes undefined', () => {
const onChange = vi.fn()
const { rerender } = render(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T10:30:00Z')}
timezone="UTC"
/>,
)
// Remove value and change timezone
rerender(
<TimePicker
{...baseProps}
onChange={onChange}
value={undefined}
timezone="America/New_York"
/>,
)
// Input should be empty now
expect(screen.getByRole('textbox')).toHaveValue('')
// onChange should not fire when value is undefined, even if selectedTime was set
expect(onChange).not.toHaveBeenCalled()
})
it('should not update when neither timezone nor value changes', () => {
const onChange = vi.fn()
const value = dayjs('2024-01-01T10:30:00Z')
const { rerender } = render(
<TimePicker
{...baseProps}
onChange={onChange}
value={value}
timezone="UTC"
/>,
)
// Rerender with same props
rerender(
<TimePicker
{...baseProps}
onChange={onChange}
value={value}
timezone="UTC"
/>,
)
expect(onChange).not.toHaveBeenCalled()
})
it('should update display when both value and timezone change', () => {
const onChange = vi.fn()
const { rerender } = render(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T10:30:00Z')}
timezone="UTC"
/>,
)
// Change both value and timezone simultaneously
rerender(
<TimePicker
{...baseProps}
onChange={onChange}
value={dayjs('2024-01-01T15:00:00Z')}
timezone="America/New_York"
/>,
)
// onChange should not be called since both changed (timezoneChanged && !valueChanged is false)
expect(onChange).not.toHaveBeenCalled()
// 15:00 UTC in America/New_York (UTC-5) = 10:00 AM
expect(screen.getByDisplayValue('10:00 AM')).toBeInTheDocument()
})
})
// Format time value tests
describe('Format Time Value', () => {
it('should return empty string when value is undefined', () => {
render(<TimePicker {...baseProps} />)
expect(screen.getByRole('textbox')).toHaveValue('')
})
it('should format dayjs value correctly', () => {
render(
<TimePicker
{...baseProps}
value={dayjs('2024-01-01T14:30:00Z')}
timezone="UTC"
/>,
)
expect(screen.getByDisplayValue('02:30 PM')).toBeInTheDocument()
})
it('should format string value correctly', () => {
render(
<TimePicker
{...baseProps}
value="09:15"
timezone="UTC"
/>,
)
expect(screen.getByDisplayValue('09:15 AM')).toBeInTheDocument()
})
})
describe('Timezone Label Integration', () => {
it('should not display timezone label by default', () => {
render(

View File

@ -1,97 +0,0 @@
import type { TimeOptionsProps } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import dayjs from '../utils/dayjs'
import Options from './options'
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn()
})
const createOptionsProps = (overrides: Partial<TimeOptionsProps> = {}): TimeOptionsProps => ({
selectedTime: undefined,
handleSelectHour: vi.fn(),
handleSelectMinute: vi.fn(),
handleSelectPeriod: vi.fn(),
...overrides,
})
describe('TimePickerOptions', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render hour options', () => {
const props = createOptionsProps()
render(<Options {...props} />)
const allItems = screen.getAllByRole('listitem')
expect(allItems.length).toBeGreaterThan(12)
})
it('should render all hour, minute, and period options by default', () => {
const props = createOptionsProps()
render(<Options {...props} />)
const allItems = screen.getAllByRole('listitem')
// 12 hours + 60 minutes + 2 periods
expect(allItems).toHaveLength(74)
})
it('should render AM and PM period options', () => {
const props = createOptionsProps()
render(<Options {...props} />)
expect(screen.getByText('AM')).toBeInTheDocument()
expect(screen.getByText('PM')).toBeInTheDocument()
})
})
describe('Minute Filter', () => {
it('should apply minuteFilter when provided', () => {
const minuteFilter = (minutes: string[]) => minutes.filter(m => Number(m) % 15 === 0)
const props = createOptionsProps({ minuteFilter })
render(<Options {...props} />)
const allItems = screen.getAllByRole('listitem')
expect(allItems).toHaveLength(18)
})
})
describe('Interactions', () => {
it('should render selected hour in the list', () => {
const props = createOptionsProps({ selectedTime: dayjs('2024-01-01 05:30:00') })
render(<Options {...props} />)
const selectedHour = screen.getAllByRole('listitem').find(item => item.textContent === '05')
expect(selectedHour).toHaveClass('bg-components-button-ghost-bg-hover')
})
it('should render selected minute in the list', () => {
const props = createOptionsProps({ selectedTime: dayjs('2024-01-01 05:30:00') })
render(<Options {...props} />)
const selectedMinute = screen.getAllByRole('listitem').find(item => item.textContent === '30')
expect(selectedMinute).toHaveClass('bg-components-button-ghost-bg-hover')
})
it('should call handleSelectPeriod when AM is clicked', () => {
const handleSelectPeriod = vi.fn()
const props = createOptionsProps({ handleSelectPeriod })
render(<Options {...props} />)
fireEvent.click(screen.getAllByText('AM')[0])
expect(handleSelectPeriod).toHaveBeenCalledWith('AM')
})
it('should call handleSelectPeriod when PM is clicked', () => {
const handleSelectPeriod = vi.fn()
const props = createOptionsProps({ handleSelectPeriod })
render(<Options {...props} />)
fireEvent.click(screen.getAllByText('PM')[0])
expect(handleSelectPeriod).toHaveBeenCalledWith('PM')
})
})
})

View File

@ -1,366 +0,0 @@
import dayjs, {
clearMonthMapCache,
cloneTime,
formatDateForOutput,
getDateWithTimezone,
getDaysInMonth,
getHourIn12Hour,
parseDateWithFormat,
toDayjs,
} from './dayjs'
describe('dayjs extended utilities', () => {
// Tests for cloneTime
describe('cloneTime', () => {
it('should copy hour and minute from source to target', () => {
const target = dayjs('2024-01-15')
const source = dayjs('2024-06-20 14:30')
const result = cloneTime(target, source)
expect(result.hour()).toBe(14)
expect(result.minute()).toBe(30)
expect(result.date()).toBe(15)
expect(result.month()).toBe(0) // January
})
it('should not mutate the original target date', () => {
const target = dayjs('2024-01-15 08:00')
const source = dayjs('2024-06-20 14:30')
cloneTime(target, source)
expect(target.hour()).toBe(8)
expect(target.minute()).toBe(0)
})
})
// Tests for getDaysInMonth
describe('getDaysInMonth', () => {
beforeEach(() => {
clearMonthMapCache()
})
it('should return an array of Day objects', () => {
const date = dayjs('2024-06-15')
const days = getDaysInMonth(date)
expect(days.length).toBeGreaterThan(0)
days.forEach((day) => {
expect(day).toHaveProperty('date')
expect(day).toHaveProperty('isCurrentMonth')
})
})
it('should include days from previous and next month to fill the grid', () => {
const date = dayjs('2024-06-15') // June 2024 starts on Saturday
const days = getDaysInMonth(date)
const prevMonthDays = days.filter(d => !d.isCurrentMonth && d.date.month() < date.month())
const nextMonthDays = days.filter(d => !d.isCurrentMonth && d.date.month() > date.month())
// June 2024 starts on Saturday (6), so there are 6 days from previous month
expect(prevMonthDays.length).toBeGreaterThan(0)
expect(nextMonthDays.length).toBeGreaterThan(0)
})
it('should mark current month days correctly', () => {
const date = dayjs('2024-06-15')
const days = getDaysInMonth(date)
const currentMonthDays = days.filter(d => d.isCurrentMonth)
// June has 30 days
expect(currentMonthDays).toHaveLength(30)
})
it('should cache results for the same month', () => {
const date1 = dayjs('2024-06-15')
const date2 = dayjs('2024-06-20')
const days1 = getDaysInMonth(date1)
const days2 = getDaysInMonth(date2)
// Same reference since it's cached
expect(days1).toBe(days2)
})
it('should return different results for different months', () => {
const june = dayjs('2024-06-15')
const july = dayjs('2024-07-15')
const juneDays = getDaysInMonth(june)
const julyDays = getDaysInMonth(july)
expect(juneDays).not.toBe(julyDays)
})
})
// Tests for clearMonthMapCache
describe('clearMonthMapCache', () => {
it('should clear the cache so new days are generated', () => {
const date = dayjs('2024-06-15')
const days1 = getDaysInMonth(date)
clearMonthMapCache()
const days2 = getDaysInMonth(date)
// After clearing cache, a new array should be created
expect(days1).not.toBe(days2)
// But should have the same length
expect(days1.length).toBe(days2.length)
})
})
// Tests for getHourIn12Hour
describe('getHourIn12Hour', () => {
it('should return 12 for midnight (hour 0)', () => {
const date = dayjs('2024-01-01 00:00')
expect(getHourIn12Hour(date)).toBe(12)
})
it('should return hour as-is for 1-11 AM', () => {
expect(getHourIn12Hour(dayjs('2024-01-01 01:00'))).toBe(1)
expect(getHourIn12Hour(dayjs('2024-01-01 11:00'))).toBe(11)
})
it('should return 0 for noon (hour 12)', () => {
const date = dayjs('2024-01-01 12:00')
expect(getHourIn12Hour(date)).toBe(0)
})
it('should return hour - 12 for PM hours (13-23)', () => {
expect(getHourIn12Hour(dayjs('2024-01-01 13:00'))).toBe(1)
expect(getHourIn12Hour(dayjs('2024-01-01 23:00'))).toBe(11)
})
})
// Tests for getDateWithTimezone
describe('getDateWithTimezone', () => {
it('should return a cloned date when no timezone is provided', () => {
const date = dayjs('2024-06-15')
const result = getDateWithTimezone({ date })
expect(result.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should return current date clone when neither date nor timezone is provided', () => {
const result = getDateWithTimezone({})
const now = dayjs()
expect(result.format('YYYY-MM-DD')).toBe(now.format('YYYY-MM-DD'))
})
it('should apply timezone to provided date', () => {
const date = dayjs('2024-06-15T12:00:00')
const result = getDateWithTimezone({ date, timezone: 'America/New_York' })
// dayjs.tz converts the date to the given timezone
expect(result).toBeDefined()
expect(result.isValid()).toBe(true)
})
it('should return current time in timezone when only timezone is provided', () => {
const result = getDateWithTimezone({ timezone: 'Asia/Tokyo' })
expect(result.utcOffset()).toBe(dayjs().tz('Asia/Tokyo').utcOffset())
})
})
// Tests for toDayjs additional edge cases
describe('toDayjs edge cases', () => {
it('should return undefined for empty string', () => {
expect(toDayjs('')).toBeUndefined()
})
it('should return undefined for undefined', () => {
expect(toDayjs(undefined)).toBeUndefined()
})
it('should handle Dayjs object input', () => {
const date = dayjs('2024-06-15')
const result = toDayjs(date)
expect(result).toBeDefined()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should handle Dayjs object with timezone', () => {
const date = dayjs('2024-06-15T12:00:00')
const result = toDayjs(date, { timezone: 'UTC' })
expect(result).toBeDefined()
})
it('should parse with custom format when format matches common formats', () => {
// Uses a format from COMMON_PARSE_FORMATS
const result = toDayjs('2024-06-15', { format: 'YYYY-MM-DD' })
expect(result).toBeDefined()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should fall back when custom format does not match', () => {
// dayjs strict mode with format requires customParseFormat plugin
// which is not loaded, so invalid format falls through to other parsing
const result = toDayjs('2024-06-15', { format: 'INVALID', timezone: 'UTC' })
// It will still be parsed by fallback mechanisms
expect(result).toBeDefined()
})
it('should parse time with seconds', () => {
const result = toDayjs('14:30:45', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(14)
expect(result?.minute()).toBe(30)
expect(result?.second()).toBe(45)
})
it('should parse time with milliseconds', () => {
const result = toDayjs('14:30:45.123', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.millisecond()).toBe(123)
})
it('should normalize short milliseconds by padding', () => {
const result = toDayjs('14:30:45.1', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.millisecond()).toBe(100)
})
it('should truncate long milliseconds to 3 digits', () => {
// The time regex only captures up to 3 digits for ms, so 4+ digit values
// don't match the regex and fall through to common format parsing
const result = toDayjs('14:30:45.12', { timezone: 'UTC' })
expect(result).toBeDefined()
// 2-digit ms "12" gets padded to "120"
expect(result?.millisecond()).toBe(120)
})
it('should parse 12-hour AM time', () => {
const result = toDayjs('07:15 AM', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(7)
expect(result?.minute()).toBe(15)
})
it('should parse 12-hour time with seconds', () => {
const result = toDayjs('07:15:30 PM', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(19)
expect(result?.second()).toBe(30)
})
it('should handle 12 PM correctly', () => {
const result = toDayjs('12:00 PM', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(12)
})
it('should handle 12 AM correctly', () => {
const result = toDayjs('12:00 AM', { timezone: 'UTC' })
expect(result).toBeDefined()
expect(result?.hour()).toBe(0)
})
it('should use custom formats array when provided', () => {
const result = toDayjs('2024.06.15', { formats: ['YYYY.MM.DD'] })
expect(result).toBeDefined()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should fall back to native parsing for ISO strings', () => {
const result = toDayjs('2024-06-15T12:00:00.000Z')
expect(result).toBeDefined()
})
it('should return undefined for completely unparseable value', () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = toDayjs('not-a-date')
expect(result).toBeUndefined()
consoleSpy.mockRestore()
})
})
// Tests for parseDateWithFormat
describe('parseDateWithFormat', () => {
it('should return null for empty string', () => {
expect(parseDateWithFormat('')).toBeNull()
})
it('should parse with provided format from common formats', () => {
// Uses YYYY-MM-DD which is in COMMON_PARSE_FORMATS
const result = parseDateWithFormat('2024-06-15', 'YYYY-MM-DD')
expect(result).not.toBeNull()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should return null for invalid date with format', () => {
const result = parseDateWithFormat('not-a-date', 'YYYY-MM-DD')
expect(result).toBeNull()
})
it('should try common formats when no format is specified', () => {
const result = parseDateWithFormat('2024-06-15')
expect(result).not.toBeNull()
expect(result?.format('YYYY-MM-DD')).toBe('2024-06-15')
})
it('should parse ISO datetime format', () => {
const result = parseDateWithFormat('2024-06-15T12:00:00')
expect(result).not.toBeNull()
})
it('should return null for unparseable string without format', () => {
const result = parseDateWithFormat('gibberish')
expect(result).toBeNull()
})
})
// Tests for formatDateForOutput
describe('formatDateForOutput', () => {
it('should return empty string for invalid date', () => {
const invalidDate = dayjs('invalid')
expect(formatDateForOutput(invalidDate)).toBe('')
})
it('should format date-only output without time', () => {
const date = dayjs('2024-06-15T12:00:00')
const result = formatDateForOutput(date)
expect(result).toBe('2024-06-15')
})
it('should format with time when includeTime is true', () => {
const date = dayjs('2024-06-15T12:00:00')
const result = formatDateForOutput(date, true)
expect(result).toContain('2024-06-15')
expect(result).toContain('12:00:00')
})
it('should default to date-only format', () => {
const date = dayjs('2024-06-15T14:30:00')
const result = formatDateForOutput(date)
expect(result).toBe('2024-06-15')
expect(result).not.toContain('14:30')
})
})
})

View File

@ -111,7 +111,7 @@ export const convertTimezoneToOffsetStr = (timezone?: string) => {
return DEFAULT_OFFSET_STR
// Extract offset from name format like "-11:00 Niue Time" or "+05:30 India Time"
// Name format is always "{offset}:{minutes} {timezone name}"
const offsetMatch = /^([+-]?\d{1,2}):(\d{2})/.exec(tzItem.name)
const offsetMatch = tzItem.name.match(/^([+-]?\d{1,2}):(\d{2})/)
if (!offsetMatch)
return DEFAULT_OFFSET_STR
// Parse hours and minutes separately

View File

@ -1,50 +0,0 @@
import type { YearAndMonthPickerFooterProps } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import Footer from './footer'
// Factory for Footer props
const createFooterProps = (overrides: Partial<YearAndMonthPickerFooterProps> = {}): YearAndMonthPickerFooterProps => ({
handleYearMonthCancel: vi.fn(),
handleYearMonthConfirm: vi.fn(),
...overrides,
})
describe('YearAndMonthPicker Footer', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render Cancel and OK buttons', () => {
const props = createFooterProps()
render(<Footer {...props} />)
expect(screen.getByText(/operation\.cancel/)).toBeInTheDocument()
expect(screen.getByText(/operation\.ok/)).toBeInTheDocument()
})
})
// Interaction tests
describe('Interactions', () => {
it('should call handleYearMonthCancel when Cancel button is clicked', () => {
const handleYearMonthCancel = vi.fn()
const props = createFooterProps({ handleYearMonthCancel })
render(<Footer {...props} />)
fireEvent.click(screen.getByText(/operation\.cancel/))
expect(handleYearMonthCancel).toHaveBeenCalledTimes(1)
})
it('should call handleYearMonthConfirm when OK button is clicked', () => {
const handleYearMonthConfirm = vi.fn()
const props = createFooterProps({ handleYearMonthConfirm })
render(<Footer {...props} />)
fireEvent.click(screen.getByText(/operation\.ok/))
expect(handleYearMonthConfirm).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -1,47 +0,0 @@
import type { YearAndMonthPickerHeaderProps } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import Header from './header'
// Factory for Header props
const createHeaderProps = (overrides: Partial<YearAndMonthPickerHeaderProps> = {}): YearAndMonthPickerHeaderProps => ({
selectedYear: 2024,
selectedMonth: 5, // June (0-indexed)
onClick: vi.fn(),
...overrides,
})
describe('YearAndMonthPicker Header', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should display the selected year', () => {
const props = createHeaderProps({ selectedYear: 2024 })
render(<Header {...props} />)
expect(screen.getByText(/2024/)).toBeInTheDocument()
})
it('should render a clickable button', () => {
const props = createHeaderProps()
render(<Header {...props} />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
})
// Interaction tests
describe('Interactions', () => {
it('should call onClick when the header button is clicked', () => {
const onClick = vi.fn()
const props = createHeaderProps({ onClick })
render(<Header {...props} />)
fireEvent.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -1,81 +0,0 @@
import type { YearAndMonthPickerOptionsProps } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import Options from './options'
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn()
})
const createOptionsProps = (overrides: Partial<YearAndMonthPickerOptionsProps> = {}): YearAndMonthPickerOptionsProps => ({
selectedMonth: 5,
selectedYear: 2024,
handleMonthSelect: vi.fn(),
handleYearSelect: vi.fn(),
...overrides,
})
describe('YearAndMonthPicker Options', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render month options', () => {
const props = createOptionsProps()
render(<Options {...props} />)
const monthItems = screen.getAllByText(/months\./)
expect(monthItems).toHaveLength(12)
})
it('should render year options', () => {
const props = createOptionsProps()
render(<Options {...props} />)
const allItems = screen.getAllByRole('listitem')
expect(allItems).toHaveLength(212)
})
})
describe('Interactions', () => {
it('should call handleMonthSelect when a month is clicked', () => {
const handleMonthSelect = vi.fn()
const props = createOptionsProps({ handleMonthSelect })
render(<Options {...props} />)
// The mock returns 'time.months.January' for the first month
fireEvent.click(screen.getByText(/months\.January/))
expect(handleMonthSelect).toHaveBeenCalledWith(0)
})
it('should call handleYearSelect when a year is clicked', () => {
const handleYearSelect = vi.fn()
const props = createOptionsProps({ handleYearSelect })
render(<Options {...props} />)
fireEvent.click(screen.getByText('2024'))
expect(handleYearSelect).toHaveBeenCalledWith(2024)
})
})
describe('Selection', () => {
it('should render selected month in the list', () => {
const props = createOptionsProps({ selectedMonth: 0 })
render(<Options {...props} />)
const monthItems = screen.getAllByText(/months\./)
expect(monthItems.length).toBeGreaterThan(0)
})
it('should render selected year in the list', () => {
const props = createOptionsProps({ selectedYear: 2024 })
render(<Options {...props} />)
expect(screen.getByText('2024')).toBeInTheDocument()
})
})
})

View File

@ -27,7 +27,7 @@ const AnnotationReply = ({
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const matched = /\/app\/([^/]+)/.exec(pathname)
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const featuresStore = useFeaturesStore()
const annotationReply = useFeatures(s => s.features.annotationReply)

View File

@ -48,7 +48,7 @@ const FeatureCard = ({
</Tooltip>
)}
</div>
<Switch disabled={disabled} className="shrink-0" onChange={state => onChange?.(state)} value={value} />
<Switch disabled={disabled} className="shrink-0" onChange={state => onChange?.(state)} defaultValue={value} />
</div>
{description && (
<div className="system-xs-regular line-clamp-2 min-h-8 text-text-tertiary">{description}</div>

View File

@ -38,7 +38,7 @@ const ModerationContent: FC<ModerationContentProps> = ({
}
<Switch
size="l"
value={config.enabled}
defaultValue={config.enabled}
onChange={v => handleConfigChange('enabled', v)}
/>
</div>

View File

@ -29,7 +29,7 @@ const VoiceParamConfig = ({
}: VoiceParamConfigProps) => {
const { t } = useTranslation()
const pathname = usePathname()
const matched = /\/app\/([^/]+)/.exec(pathname)
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const text2speech = useFeatures(state => state.features.text2speech)
const featuresStore = useFeaturesStore()
@ -232,7 +232,7 @@ const VoiceParamConfig = ({
</div>
<Switch
className="shrink-0"
value={text2speech?.autoPlay === TtsAutoPlay.enabled}
defaultValue={text2speech?.autoPlay === TtsAutoPlay.enabled}
onChange={(value: boolean) => {
handleChange({
autoPlay: value ? TtsAutoPlay.enabled : TtsAutoPlay.disabled,

View File

@ -21,7 +21,7 @@ export type IGAProps = {
const extractNonceFromCSP = (cspHeader: string | null): string | undefined => {
if (!cspHeader)
return undefined
const nonceMatch = /'nonce-([^']+)'/.exec(cspHeader)
const nonceMatch = cspHeader.match(/'nonce-([^']+)'/)
return nonceMatch ? nonceMatch[1] : undefined
}

View File

@ -239,7 +239,7 @@ const Flowchart = (props: FlowchartProps) => {
.split('\n')
.map((line) => {
// Gantt charts have specific syntax needs.
const taskMatch = /^\s*([^:]+?)\s*:\s*(.*)/.exec(line)
const taskMatch = line.match(/^\s*([^:]+?)\s*:\s*(.*)/)
if (!taskMatch)
return line // Not a task line, return as is.

View File

@ -185,7 +185,7 @@ export function isMermaidCodeComplete(code: string): boolean {
const hasNoSyntaxErrors = !trimmedCode.includes('undefined')
&& !trimmedCode.includes('[object Object]')
&& trimmedCode.split('\n').every(line =>
!(line.includes('-->') && !/\S+\s*-->\s*\S+/.exec(line)))
!(line.includes('-->') && !line.match(/\S+\s*-->\s*\S+/)))
return hasValidStart && isBalanced && hasNoSyntaxErrors
}

View File

@ -30,7 +30,7 @@ const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1,
<Switch
size="md"
className="mr-2"
value={enable}
defaultValue={enable}
onChange={async (val) => {
onSwitchChange?.(id, val)
}}

View File

@ -4,54 +4,41 @@ import { describe, expect, it, vi } from 'vitest'
import Switch from './index'
describe('Switch', () => {
it('should render in unchecked state when value is false', () => {
render(<Switch value={false} />)
it('should render in unchecked state by default', () => {
render(<Switch />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toBeInTheDocument()
expect(switchElement).toHaveAttribute('aria-checked', 'false')
})
it('should render in checked state when value is true', () => {
render(<Switch value={true} />)
it('should render in checked state when defaultValue is true', () => {
render(<Switch defaultValue={true} />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveAttribute('aria-checked', 'true')
})
it('should call onChange with next value when clicked', async () => {
it('should toggle state and call onChange when clicked', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
render(<Switch value={false} onChange={onChange} />)
render(<Switch onChange={onChange} />)
const switchElement = screen.getByRole('switch')
await user.click(switchElement)
expect(switchElement).toHaveAttribute('aria-checked', 'true')
expect(onChange).toHaveBeenCalledWith(true)
expect(onChange).toHaveBeenCalledTimes(1)
// Controlled component stays the same until parent updates value.
expect(switchElement).toHaveAttribute('aria-checked', 'false')
})
it('should work in controlled mode with value prop', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
const { rerender } = render(<Switch value={false} onChange={onChange} />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveAttribute('aria-checked', 'false')
await user.click(switchElement)
expect(onChange).toHaveBeenCalledWith(true)
expect(switchElement).toHaveAttribute('aria-checked', 'false')
rerender(<Switch value={true} onChange={onChange} />)
expect(switchElement).toHaveAttribute('aria-checked', 'true')
expect(onChange).toHaveBeenCalledWith(false)
expect(onChange).toHaveBeenCalledTimes(2)
})
it('should not call onChange when disabled', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
render(<Switch value={false} disabled onChange={onChange} />)
render(<Switch disabled onChange={onChange} />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('!cursor-not-allowed', '!opacity-50')
@ -61,36 +48,37 @@ describe('Switch', () => {
})
it('should apply correct size classes', () => {
const { rerender } = render(<Switch value={false} size="xs" />)
const { rerender } = render(<Switch size="xs" />)
// We only need to find the element once
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('h-2.5', 'w-3.5', 'rounded-sm')
rerender(<Switch value={false} size="sm" />)
rerender(<Switch size="sm" />)
expect(switchElement).toHaveClass('h-3', 'w-5')
rerender(<Switch value={false} size="md" />)
rerender(<Switch size="md" />)
expect(switchElement).toHaveClass('h-4', 'w-7')
rerender(<Switch value={false} size="l" />)
rerender(<Switch size="l" />)
expect(switchElement).toHaveClass('h-5', 'w-9')
rerender(<Switch value={false} size="lg" />)
rerender(<Switch size="lg" />)
expect(switchElement).toHaveClass('h-6', 'w-11')
})
it('should apply custom className', () => {
render(<Switch value={false} className="custom-test-class" />)
render(<Switch className="custom-test-class" />)
expect(screen.getByRole('switch')).toHaveClass('custom-test-class')
})
it('should apply correct background colors based on value prop', () => {
const { rerender } = render(<Switch value={false} />)
it('should apply correct background colors based on state', async () => {
const user = userEvent.setup()
render(<Switch />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('bg-components-toggle-bg-unchecked')
rerender(<Switch value={true} />)
await user.click(switchElement)
expect(switchElement).toHaveClass('bg-components-toggle-bg')
})
})

View File

@ -14,18 +14,15 @@ const meta = {
},
},
tags: ['autodocs'],
args: {
value: false,
},
argTypes: {
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'l'],
description: 'Switch size',
},
value: {
defaultValue: {
control: 'boolean',
description: 'Checked state (controlled)',
description: 'Default checked state',
},
disabled: {
control: 'boolean',
@ -39,14 +36,14 @@ type Story = StoryObj<typeof meta>
// Interactive demo wrapper
const SwitchDemo = (args: any) => {
const [enabled, setEnabled] = useState(args.value ?? false)
const [enabled, setEnabled] = useState(args.defaultValue || false)
return (
<div style={{ width: '300px' }}>
<div className="flex items-center gap-3">
<Switch
{...args}
value={enabled}
defaultValue={enabled}
onChange={(value) => {
setEnabled(value)
console.log('Switch toggled:', value)
@ -65,7 +62,7 @@ export const Default: Story = {
render: args => <SwitchDemo {...args} />,
args: {
size: 'md',
value: false,
defaultValue: false,
disabled: false,
},
}
@ -75,7 +72,7 @@ export const DefaultOn: Story = {
render: args => <SwitchDemo {...args} />,
args: {
size: 'md',
value: true,
defaultValue: true,
disabled: false,
},
}
@ -85,7 +82,7 @@ export const DisabledOff: Story = {
render: args => <SwitchDemo {...args} />,
args: {
size: 'md',
value: false,
defaultValue: false,
disabled: true,
},
}
@ -95,7 +92,7 @@ export const DisabledOn: Story = {
render: args => <SwitchDemo {...args} />,
args: {
size: 'md',
value: true,
defaultValue: true,
disabled: true,
},
}
@ -114,31 +111,31 @@ const SizeComparisonDemo = () => {
<div style={{ width: '400px' }} className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Switch size="xs" value={states.xs} onChange={v => setStates({ ...states, xs: v })} />
<Switch size="xs" defaultValue={states.xs} onChange={v => setStates({ ...states, xs: v })} />
<span className="text-sm text-gray-700">Extra Small (xs)</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Switch size="sm" value={states.sm} onChange={v => setStates({ ...states, sm: v })} />
<Switch size="sm" defaultValue={states.sm} onChange={v => setStates({ ...states, sm: v })} />
<span className="text-sm text-gray-700">Small (sm)</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Switch size="md" value={states.md} onChange={v => setStates({ ...states, md: v })} />
<Switch size="md" defaultValue={states.md} onChange={v => setStates({ ...states, md: v })} />
<span className="text-sm text-gray-700">Medium (md)</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Switch size="l" value={states.l} onChange={v => setStates({ ...states, l: v })} />
<Switch size="l" defaultValue={states.l} onChange={v => setStates({ ...states, l: v })} />
<span className="text-sm text-gray-700">Large (l)</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Switch size="lg" value={states.lg} onChange={v => setStates({ ...states, lg: v })} />
<Switch size="lg" defaultValue={states.lg} onChange={v => setStates({ ...states, lg: v })} />
<span className="text-sm text-gray-700">Extra Large (lg)</span>
</div>
</div>
@ -163,7 +160,7 @@ const WithLabelsDemo = () => {
</div>
<Switch
size="md"
value={enabled}
defaultValue={enabled}
onChange={setEnabled}
/>
</div>
@ -200,7 +197,7 @@ const SettingsPanelDemo = () => {
</div>
<Switch
size="md"
value={settings.notifications}
defaultValue={settings.notifications}
onChange={v => updateSetting('notifications', v)}
/>
</div>
@ -212,7 +209,7 @@ const SettingsPanelDemo = () => {
</div>
<Switch
size="md"
value={settings.autoSave}
defaultValue={settings.autoSave}
onChange={v => updateSetting('autoSave', v)}
/>
</div>
@ -224,7 +221,7 @@ const SettingsPanelDemo = () => {
</div>
<Switch
size="md"
value={settings.darkMode}
defaultValue={settings.darkMode}
onChange={v => updateSetting('darkMode', v)}
/>
</div>
@ -236,7 +233,7 @@ const SettingsPanelDemo = () => {
</div>
<Switch
size="md"
value={settings.analytics}
defaultValue={settings.analytics}
onChange={v => updateSetting('analytics', v)}
/>
</div>
@ -248,7 +245,7 @@ const SettingsPanelDemo = () => {
</div>
<Switch
size="md"
value={settings.emailUpdates}
defaultValue={settings.emailUpdates}
onChange={v => updateSetting('emailUpdates', v)}
/>
</div>
@ -282,7 +279,7 @@ const PrivacyControlsDemo = () => {
</div>
<Switch
size="md"
value={privacy.profilePublic}
defaultValue={privacy.profilePublic}
onChange={v => setPrivacy({ ...privacy, profilePublic: v })}
/>
</div>
@ -294,7 +291,7 @@ const PrivacyControlsDemo = () => {
</div>
<Switch
size="md"
value={privacy.showEmail}
defaultValue={privacy.showEmail}
onChange={v => setPrivacy({ ...privacy, showEmail: v })}
/>
</div>
@ -306,7 +303,7 @@ const PrivacyControlsDemo = () => {
</div>
<Switch
size="md"
value={privacy.allowMessages}
defaultValue={privacy.allowMessages}
onChange={v => setPrivacy({ ...privacy, allowMessages: v })}
/>
</div>
@ -318,7 +315,7 @@ const PrivacyControlsDemo = () => {
</div>
<Switch
size="md"
value={privacy.shareActivity}
defaultValue={privacy.shareActivity}
onChange={v => setPrivacy({ ...privacy, shareActivity: v })}
/>
</div>
@ -354,7 +351,7 @@ const FeatureTogglesDemo = () => {
</div>
<Switch
size="md"
value={features.betaFeatures}
defaultValue={features.betaFeatures}
onChange={v => setFeatures({ ...features, betaFeatures: v })}
/>
</div>
@ -369,7 +366,7 @@ const FeatureTogglesDemo = () => {
</div>
<Switch
size="md"
value={features.experimentalUI}
defaultValue={features.experimentalUI}
onChange={v => setFeatures({ ...features, experimentalUI: v })}
/>
</div>
@ -384,7 +381,7 @@ const FeatureTogglesDemo = () => {
</div>
<Switch
size="md"
value={features.advancedMode}
defaultValue={features.advancedMode}
onChange={v => setFeatures({ ...features, advancedMode: v })}
/>
</div>
@ -399,7 +396,7 @@ const FeatureTogglesDemo = () => {
</div>
<Switch
size="md"
value={features.developerTools}
defaultValue={features.developerTools}
onChange={v => setFeatures({ ...features, developerTools: v })}
/>
</div>
@ -443,7 +440,7 @@ const NotificationPreferencesDemo = () => {
</div>
<Switch
size="md"
value={notifications.email}
defaultValue={notifications.email}
onChange={v => setNotifications({ ...notifications, email: v })}
/>
</div>
@ -458,7 +455,7 @@ const NotificationPreferencesDemo = () => {
</div>
<Switch
size="md"
value={notifications.push}
defaultValue={notifications.push}
onChange={v => setNotifications({ ...notifications, push: v })}
/>
</div>
@ -473,7 +470,7 @@ const NotificationPreferencesDemo = () => {
</div>
<Switch
size="md"
value={notifications.sms}
defaultValue={notifications.sms}
onChange={v => setNotifications({ ...notifications, sms: v })}
/>
</div>
@ -488,7 +485,7 @@ const NotificationPreferencesDemo = () => {
</div>
<Switch
size="md"
value={notifications.desktop}
defaultValue={notifications.desktop}
onChange={v => setNotifications({ ...notifications, desktop: v })}
/>
</div>
@ -526,7 +523,7 @@ const APIAccessControlDemo = () => {
</div>
<Switch
size="md"
value={access.readAccess}
defaultValue={access.readAccess}
onChange={v => setAccess({ ...access, readAccess: v })}
/>
</div>
@ -542,7 +539,7 @@ const APIAccessControlDemo = () => {
</div>
<Switch
size="md"
value={access.writeAccess}
defaultValue={access.writeAccess}
onChange={v => setAccess({ ...access, writeAccess: v })}
/>
</div>
@ -558,7 +555,7 @@ const APIAccessControlDemo = () => {
</div>
<Switch
size="md"
value={access.deleteAccess}
defaultValue={access.deleteAccess}
onChange={v => setAccess({ ...access, deleteAccess: v })}
/>
</div>
@ -574,7 +571,7 @@ const APIAccessControlDemo = () => {
</div>
<Switch
size="md"
value={access.adminAccess}
defaultValue={access.adminAccess}
onChange={v => setAccess({ ...access, adminAccess: v })}
/>
</div>
@ -612,7 +609,7 @@ const CompactListDemo = () => {
<span className="text-sm text-gray-700">{item.name}</span>
<Switch
size="sm"
value={item.enabled}
defaultValue={item.enabled}
onChange={() => toggleItem(item.id)}
/>
</div>
@ -631,7 +628,7 @@ export const Playground: Story = {
render: args => <SwitchDemo {...args} />,
args: {
size: 'md',
value: false,
defaultValue: false,
disabled: false,
},
}

View File

@ -1,12 +1,13 @@
'use client'
import { Switch as OriginalSwitch } from '@headlessui/react'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { cn } from '@/utils/classnames'
type SwitchProps = {
value: boolean
onChange?: (value: boolean) => void
size?: 'xs' | 'sm' | 'md' | 'lg' | 'l'
defaultValue?: boolean
disabled?: boolean
className?: string
}
@ -14,15 +15,19 @@ type SwitchProps = {
const Switch = (
{
ref: propRef,
value,
onChange,
size = 'md',
defaultValue = false,
disabled = false,
className,
}: SwitchProps & {
ref?: React.RefObject<HTMLButtonElement>
},
) => {
const [enabled, setEnabled] = useState(defaultValue)
useEffect(() => {
setEnabled(defaultValue)
}, [defaultValue])
const wrapStyle = {
lg: 'h-6 w-11',
l: 'h-5 w-9',
@ -49,17 +54,18 @@ const Switch = (
return (
<OriginalSwitch
ref={propRef}
checked={value}
checked={enabled}
onChange={(checked: boolean) => {
if (disabled)
return
setEnabled(checked)
onChange?.(checked)
}}
className={cn(wrapStyle[size], value ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked', 'relative inline-flex shrink-0 cursor-pointer rounded-[5px] border-2 border-transparent transition-colors duration-200 ease-in-out', disabled ? '!cursor-not-allowed !opacity-50' : '', size === 'xs' && 'rounded-sm', className)}
className={cn(wrapStyle[size], enabled ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked', 'relative inline-flex shrink-0 cursor-pointer rounded-[5px] border-2 border-transparent transition-colors duration-200 ease-in-out', disabled ? '!cursor-not-allowed !opacity-50' : '', size === 'xs' && 'rounded-sm', className)}
>
<span
aria-hidden="true"
className={cn(circleStyle[size], value ? translateLeft[size] : 'translate-x-0', size === 'xs' && 'rounded-[1px]', 'pointer-events-none inline-block rounded-[3px] bg-components-toggle-knob shadow ring-0 transition duration-200 ease-in-out')}
className={cn(circleStyle[size], enabled ? translateLeft[size] : 'translate-x-0', size === 'xs' && 'rounded-[1px]', 'pointer-events-none inline-block rounded-[3px] bg-components-toggle-knob shadow ring-0 transition duration-200 ease-in-out')}
/>
</OriginalSwitch>
)

View File

@ -24,12 +24,12 @@ const PlanRangeSwitcher: FC<PlanRangeSwitcherProps> = ({
<div className="flex items-center justify-end gap-x-3 pr-5">
<Switch
size="l"
value={value === PlanRange.yearly}
defaultValue={value === PlanRange.yearly}
onChange={(v) => {
onChange(v ? PlanRange.yearly : PlanRange.monthly)
}}
/>
<span className="text-text-tertiary system-md-regular">
<span className="system-md-regular text-text-tertiary">
{t('plansCommon.annualBilling', { ns: 'billing', percent: 17 })}
</span>
</div>

View File

@ -7,7 +7,7 @@ import { ALL_PLANS, NUM_INFINITE } from '@/app/components/billing/config'
* @example "50MB" -> 50, "5GB" -> 5120, "20GB" -> 20480
*/
export const parseVectorSpaceToMB = (vectorSpace: string): number => {
const match = /^(\d+)(MB|GB)$/i.exec(vectorSpace)
const match = vectorSpace.match(/^(\d+)(MB|GB)$/i)
if (!match)
return 0

View File

@ -116,19 +116,19 @@ const CustomWebAppBrand = () => {
return (
<div className="py-4">
<div className="mb-2 flex items-center justify-between rounded-xl bg-background-section-burn p-4 text-text-primary system-md-medium">
<div className="system-md-medium mb-2 flex items-center justify-between rounded-xl bg-background-section-burn p-4 text-text-primary">
{t('webapp.removeBrand', { ns: 'custom' })}
<Switch
size="l"
value={webappBrandRemoved ?? false}
defaultValue={webappBrandRemoved}
disabled={isSandbox || !isCurrentWorkspaceManager}
onChange={handleSwitch}
/>
</div>
<div className={cn('flex h-14 items-center justify-between rounded-xl bg-background-section-burn px-4', webappBrandRemoved && 'opacity-30')}>
<div>
<div className="text-text-primary system-md-medium">{t('webapp.changeLogo', { ns: 'custom' })}</div>
<div className="text-text-tertiary system-xs-regular">{t('webapp.changeLogoTip', { ns: 'custom' })}</div>
<div className="system-md-medium text-text-primary">{t('webapp.changeLogo', { ns: 'custom' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('webapp.changeLogoTip', { ns: 'custom' })}</div>
</div>
<div className="flex items-center">
{(!uploadDisabled && webappLogo && !webappBrandRemoved) && (
@ -204,7 +204,7 @@ const CustomWebAppBrand = () => {
<div className="mt-2 text-xs text-[#D92D20]">{t('uploadedFail', { ns: 'custom' })}</div>
)}
<div className="mb-2 mt-5 flex items-center gap-2">
<div className="shrink-0 text-text-tertiary system-xs-medium-uppercase">{t('overview.appInfo.preview', { ns: 'appOverview' })}</div>
<div className="system-xs-medium-uppercase shrink-0 text-text-tertiary">{t('overview.appInfo.preview', { ns: 'appOverview' })}</div>
<Divider bgStyle="gradient" className="grow" />
</div>
<div className="relative mb-2 flex items-center gap-3">
@ -215,7 +215,7 @@ const CustomWebAppBrand = () => {
<div className={cn('inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-blue-light-solid')}>
<BubbleTextMod className="h-4 w-4 text-components-avatar-shape-fill-stop-100" />
</div>
<div className="grow text-text-secondary system-md-semibold">Chatflow App</div>
<div className="system-md-semibold grow text-text-secondary">Chatflow App</div>
<div className="p-1.5">
<RiLayoutLeft2Line className="h-4 w-4 text-text-tertiary" />
</div>
@ -246,7 +246,7 @@ const CustomWebAppBrand = () => {
<div className="flex items-center gap-1.5">
{!webappBrandRemoved && (
<>
<div className="text-text-tertiary system-2xs-medium-uppercase">POWERED BY</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">POWERED BY</div>
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt="logo" className="block h-5 w-auto" />
@ -262,12 +262,12 @@ const CustomWebAppBrand = () => {
<div className="flex w-[138px] grow flex-col justify-between p-2 pr-0">
<div className="flex grow flex-col justify-between rounded-l-2xl border-[0.5px] border-r-0 border-components-panel-border-subtle bg-chatbot-bg pb-4 pl-[22px] pt-16">
<div className="w-[720px] rounded-2xl border border-divider-subtle bg-chat-bubble-bg px-4 py-3">
<div className="mb-1 text-text-primary body-md-regular">Hello! How can I assist you today?</div>
<div className="body-md-regular mb-1 text-text-primary">Hello! How can I assist you today?</div>
<Button size="small">
<div className="h-2 w-[144px] rounded-sm bg-text-quaternary opacity-20"></div>
</Button>
</div>
<div className="flex h-[52px] w-[578px] items-center rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pl-3.5 text-text-placeholder shadow-md backdrop-blur-sm body-lg-regular">Talk to Dify</div>
<div className="body-lg-regular flex h-[52px] w-[578px] items-center rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pl-3.5 text-text-placeholder shadow-md backdrop-blur-sm">Talk to Dify</div>
</div>
</div>
</div>
@ -278,14 +278,14 @@ const CustomWebAppBrand = () => {
<div className={cn('inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-indigo-solid')}>
<RiExchange2Fill className="h-4 w-4 text-components-avatar-shape-fill-stop-100" />
</div>
<div className="grow text-text-secondary system-md-semibold">Workflow App</div>
<div className="system-md-semibold grow text-text-secondary">Workflow App</div>
<div className="p-1.5">
<RiLayoutLeft2Line className="h-4 w-4 text-text-tertiary" />
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex h-10 shrink-0 items-center border-b-2 border-components-tab-active text-text-primary system-md-semibold-uppercase">RUN ONCE</div>
<div className="flex h-10 grow items-center border-b-2 border-transparent text-text-tertiary system-md-semibold-uppercase">RUN BATCH</div>
<div className="system-md-semibold-uppercase flex h-10 shrink-0 items-center border-b-2 border-components-tab-active text-text-primary">RUN ONCE</div>
<div className="system-md-semibold-uppercase flex h-10 grow items-center border-b-2 border-transparent text-text-tertiary">RUN BATCH</div>
</div>
</div>
<div className="grow bg-components-panel-bg">
@ -293,7 +293,7 @@ const CustomWebAppBrand = () => {
<div className="mb-1 py-2">
<div className="h-2 w-20 rounded-sm bg-text-quaternary opacity-20"></div>
</div>
<div className="h-16 w-full rounded-lg bg-components-input-bg-normal"></div>
<div className="h-16 w-full rounded-lg bg-components-input-bg-normal "></div>
</div>
<div className="flex items-center justify-between px-4 py-3">
<Button size="small">
@ -308,7 +308,7 @@ const CustomWebAppBrand = () => {
<div className="flex h-12 shrink-0 items-center gap-1.5 bg-components-panel-bg p-4 pt-3">
{!webappBrandRemoved && (
<>
<div className="text-text-tertiary system-2xs-medium-uppercase">POWERED BY</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">POWERED BY</div>
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt="logo" className="block h-5 w-auto" />

View File

@ -98,8 +98,8 @@ describe('CredentialIcon', () => {
const classes1 = wrapper1.className
const classes2 = wrapper2.className
const bgClass1 = /bg-components-icon-bg-\S+/.exec(classes1)?.[0]
const bgClass2 = /bg-components-icon-bg-\S+/.exec(classes2)?.[0]
const bgClass1 = classes1.match(/bg-components-icon-bg-\S+/)?.[0]
const bgClass2 = classes2.match(/bg-components-icon-bg-\S+/)?.[0]
expect(bgClass1).toBe(bgClass2)
})
@ -112,8 +112,8 @@ describe('CredentialIcon', () => {
const wrapper1 = container1.firstChild as HTMLElement
const wrapper2 = container2.firstChild as HTMLElement
const bgClass1 = /bg-components-icon-bg-\S+/.exec(wrapper1.className)?.[0]
const bgClass2 = /bg-components-icon-bg-\S+/.exec(wrapper2.className)?.[0]
const bgClass1 = wrapper1.className.match(/bg-components-icon-bg-\S+/)?.[0]
const bgClass2 = wrapper2.className.match(/bg-components-icon-bg-\S+/)?.[0]
expect(bgClass1).toBeDefined()
expect(bgClass2).toBeDefined()

View File

@ -123,11 +123,11 @@ vi.mock('@/app/components/base/radio-card', () => ({
}))
vi.mock('@/app/components/base/switch', () => ({
default: ({ value, onChange }: { value: boolean, onChange?: (v: boolean) => void }) => (
default: ({ defaultValue, onChange }: { defaultValue: boolean, onChange: (v: boolean) => void }) => (
<button
data-testid="rerank-switch"
data-checked={value}
onClick={() => onChange?.(!value)}
data-checked={defaultValue}
onClick={() => onChange(!defaultValue)}
>
Switch
</button>

View File

@ -122,7 +122,7 @@ const RetrievalParamConfig: FC<Props> = ({
{canToggleRerankModalEnable && (
<Switch
size="md"
value={value.reranking_enable}
defaultValue={value.reranking_enable}
onChange={handleToggleRerankEnable}
/>
)}

View File

@ -191,7 +191,7 @@ const Operations = ({
return (
<div className="flex items-center" onClick={e => e.stopPropagation()}>
{isListScene && !embeddingAvailable && (
<Switch value={false} onChange={noop} disabled={true} size="md" />
<Switch defaultValue={false} onChange={noop} disabled={true} size="md" />
)}
{isListScene && embeddingAvailable && (
<>
@ -202,11 +202,11 @@ const Operations = ({
popupClassName="!font-semibold"
>
<div>
<Switch value={false} onChange={noop} disabled={true} size="md" />
<Switch defaultValue={false} onChange={noop} disabled={true} size="md" />
</div>
</Tooltip>
)
: <Switch value={enabled} onChange={v => handleSwitch(v ? 'enable' : 'disable')} size="md" />}
: <Switch defaultValue={enabled} onChange={v => handleSwitch(v ? 'enable' : 'disable')} size="md" />}
<Divider className="!ml-4 !mr-2 !h-3" type="vertical" />
</>
)}

View File

@ -216,7 +216,7 @@ const SegmentCard: FC<ISegmentCardProps> = ({
<Switch
size="md"
disabled={archived || detail?.status !== 'completed'}
value={enabled}
defaultValue={enabled}
onChange={async (val) => {
await onChangeSwitch?.(val, id)
}}

View File

@ -119,7 +119,7 @@ const StatusItem = ({
disabled={!archived}
>
<Switch
value={archived ? false : enabled}
defaultValue={archived ? false : enabled}
onChange={v => !archived && handleSwitch(v ? 'enable' : 'disable')}
disabled={embedding || archived}
size="md"

View File

@ -60,7 +60,7 @@ const Card = ({
</div>
</div>
<Switch
value={apiEnabled}
defaultValue={apiEnabled}
onChange={onToggle}
disabled={!isCurrentWorkspaceManager}
/>

View File

@ -204,7 +204,7 @@ const DatasetMetadataDrawer: FC<Props> = ({
<div className="mt-3 flex h-6 items-center">
<Switch
value={isBuiltInEnabled}
defaultValue={isBuiltInEnabled}
onChange={onIsBuiltInEnabledChange}
/>
<div className="system-sm-semibold ml-2 mr-0.5 text-text-secondary">{t(`${i18nPrefix}.builtIn`, { ns: 'dataset' })}</div>

View File

@ -72,7 +72,7 @@ const SummaryIndexSetting = ({
</Tooltip>
</div>
<Switch
value={summaryIndexSetting?.enable ?? false}
defaultValue={summaryIndexSetting?.enable ?? false}
onChange={handleSummaryIndexEnableChange}
size="md"
/>
@ -119,7 +119,7 @@ const SummaryIndexSetting = ({
<div className="system-sm-semibold flex items-center text-text-secondary">
<Switch
className="mr-2"
value={summaryIndexSetting?.enable ?? false}
defaultValue={summaryIndexSetting?.enable ?? false}
onChange={handleSummaryIndexEnableChange}
size="md"
/>
@ -184,7 +184,7 @@ const SummaryIndexSetting = ({
<div className="flex h-6 items-center">
<Switch
className="mr-2"
value={summaryIndexSetting?.enable ?? false}
defaultValue={summaryIndexSetting?.enable ?? false}
onChange={handleSummaryIndexEnableChange}
size="md"
/>

View File

@ -166,7 +166,7 @@ const CreateAppModal = ({
<div className="flex items-center justify-between">
<div className="py-2 text-sm font-medium leading-[20px] text-text-primary">{t('answerIcon.title', { ns: 'app' })}</div>
<Switch
value={useIconAsAnswerIcon}
defaultValue={useIconAsAnswerIcon}
onChange={v => setUseIconAsAnswerIcon(v)}
/>
</div>

View File

@ -257,7 +257,7 @@ const ParameterItem: FC<ParameterItemProps> = ({
!parameterRule.required && parameterRule.name !== 'stop' && (
<div className="mr-2 w-7">
<Switch
value={!isNullOrUndefined(value)}
defaultValue={!isNullOrUndefined(value)}
onChange={handleSwitch}
size="md"
/>

View File

@ -92,13 +92,13 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad
}
offset={{ mainAxis: 4 }}
>
<Switch value={false} disabled size="md" />
<Switch defaultValue={false} disabled size="md" />
</Tooltip>
)
: (isCurrentWorkspaceManager && (
<Switch
className="ml-2"
value={model?.status === ModelStatusEnum.active}
defaultValue={model?.status === ModelStatusEnum.active}
disabled={![ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)}
size="md"
onChange={onEnablingStateChange}

View File

@ -167,7 +167,7 @@ const ModelLoadBalancingConfigs = ({
{
withSwitch && (
<Switch
value={Boolean(draftConfig.enabled)}
defaultValue={Boolean(draftConfig.enabled)}
size="l"
className="ml-3 justify-self-end"
disabled={!modelLoadBalancingEnabled && !draftConfig.enabled}
@ -227,7 +227,7 @@ const ModelLoadBalancingConfigs = ({
<>
<span className="mr-2 h-3 border-r border-r-divider-subtle" />
<Switch
value={credential?.not_allowed_to_use ? false : Boolean(config.enabled)}
defaultValue={credential?.not_allowed_to_use ? false : Boolean(config.enabled)}
size="md"
className="justify-self-end"
onChange={value => toggleConfigEntryEnabled(index, value)}

View File

@ -181,7 +181,7 @@ const EndpointCard = ({
)}
<Switch
className="ml-3"
value={active}
defaultValue={active}
onChange={handleSwitch}
size="sm"
/>

View File

@ -258,7 +258,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
<span className="system-xs-medium text-text-secondary">{t('detailPanel.toolSelector.auto', { ns: 'plugin' })}</span>
<Switch
size="xs"
value={!!auto}
defaultValue={!!auto}
onChange={val => handleAutomatic(variable, val, type)}
/>
</div>

View File

@ -95,8 +95,8 @@ const ToolItem = ({
</div>
)}
<div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}>
<div className="text-text-tertiary system-2xs-medium-uppercase">{providerNameText}</div>
<div className="text-text-secondary system-xs-medium">{toolLabel}</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">{providerNameText}</div>
<div className="system-xs-medium text-text-secondary">{toolLabel}</div>
</div>
<div className="hidden items-center gap-1 group-hover:flex">
{!noAuth && !isError && !uninstalled && !versionMismatch && !isShowCanNotChooseMCPTip && (
@ -120,7 +120,7 @@ const ToolItem = ({
<div className="mr-1" onClick={e => e.stopPropagation()}>
<Switch
size="md"
value={switchValue ?? false}
defaultValue={switchValue}
onChange={onSwitchChange}
/>
</div>

View File

@ -12,7 +12,7 @@ import { uploadRemoteFileInfo } from '@/service/common'
const DEFAULT_ICON = { type: 'emoji', icon: '🔗', background: '#6366F1' }
const extractFileId = (url: string) => {
const match = /files\/(.+?)\/file-preview/.exec(url)
const match = url.match(/files\/(.+?)\/file-preview/)
return match ? match[1] : null
}

View File

@ -250,7 +250,7 @@ const MCPServiceCard: FC<IAppCardProps> = ({
offset={24}
>
<div>
<Switch value={activated} onChange={onChangeStatus} disabled={toggleDisabled} />
<Switch defaultValue={activated} onChange={onChangeStatus} disabled={toggleDisabled} />
</div>
</Tooltip>
</div>

View File

@ -32,7 +32,7 @@ const AuthenticationSection: FC<AuthenticationSectionProps> = ({
<div className="mb-1 flex h-6 items-center">
<Switch
className="mr-2"
value={isDynamicRegistration}
defaultValue={isDynamicRegistration}
onChange={onDynamicRegistrationChange}
/>
<span className="system-sm-medium text-text-secondary">{t('mcp.modal.useDynamicClientRegistration', { ns: 'tools' })}</span>

View File

@ -65,7 +65,7 @@ const ConfigVision: FC<Props> = ({
popupContent={t('vision.onlySupportVisionModelTip', { ns: 'appDebug' })!}
disabled={isVisionModel}
>
<Switch disabled={readOnly || !isVisionModel} size="md" value={!isVisionModel ? false : enabled} onChange={onEnabledChange} />
<Switch disabled={readOnly || !isVisionModel} size="md" defaultValue={!isVisionModel ? false : enabled} onChange={onEnabledChange} />
</Tooltip>
)}
>

View File

@ -136,7 +136,7 @@ const MemoryConfig: FC<Props> = ({
tooltip={t(`${i18nPrefix}.memoryTip`, { ns: 'workflow' })!}
operations={(
<Switch
value={!!payload}
defaultValue={!!payload}
onChange={handleMemoryEnabledChange}
size="md"
disabled={readonly}
@ -149,7 +149,7 @@ const MemoryConfig: FC<Props> = ({
<div className="flex justify-between">
<div className="flex h-8 items-center space-x-2">
<Switch
value={payload?.window?.enabled}
defaultValue={payload?.window?.enabled}
onChange={handleWindowEnabledChange}
size="md"
disabled={readonly}

View File

@ -196,7 +196,7 @@ const Editor: FC<Props> = ({
<Jinja className="h-3 w-6 text-text-quaternary" />
<Switch
size="sm"
value={editionType === EditionType.jinja2}
defaultValue={editionType === EditionType.jinja2}
onChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}

View File

@ -55,10 +55,10 @@ const RetryOnPanel = ({
<div className="pt-2">
<div className="flex h-10 items-center justify-between px-4 py-2">
<div className="flex items-center">
<div className="mr-0.5 text-text-secondary system-sm-semibold-uppercase">{t('nodes.common.retry.retryOnFailure', { ns: 'workflow' })}</div>
<div className="system-sm-semibold-uppercase mr-0.5 text-text-secondary">{t('nodes.common.retry.retryOnFailure', { ns: 'workflow' })}</div>
</div>
<Switch
value={retry_config?.retry_enabled ?? false}
defaultValue={retry_config?.retry_enabled}
onChange={v => handleRetryEnabledChange(v)}
/>
</div>
@ -66,7 +66,7 @@ const RetryOnPanel = ({
retry_config?.retry_enabled && (
<div className="px-4 pb-2">
<div className="mb-1 flex w-full items-center">
<div className="mr-2 grow text-text-secondary system-xs-medium-uppercase">{t('nodes.common.retry.maxRetries', { ns: 'workflow' })}</div>
<div className="system-xs-medium-uppercase mr-2 grow text-text-secondary">{t('nodes.common.retry.maxRetries', { ns: 'workflow' })}</div>
<Slider
className="mr-3 w-[108px]"
value={retry_config?.max_retries || 3}
@ -87,7 +87,7 @@ const RetryOnPanel = ({
/>
</div>
<div className="flex items-center">
<div className="mr-2 grow text-text-secondary system-xs-medium-uppercase">{t('nodes.common.retry.retryInterval', { ns: 'workflow' })}</div>
<div className="system-xs-medium-uppercase mr-2 grow text-text-secondary">{t('nodes.common.retry.retryInterval', { ns: 'workflow' })}</div>
<Slider
className="mr-3 w-[108px]"
value={retry_config?.retry_interval || 1000}

View File

@ -131,7 +131,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
tooltip={t(`${i18nPrefix}.verifySSL.warningTooltip`, { ns: 'workflow' })}
operations={(
<Switch
value={!!inputs.ssl_verify}
defaultValue={!!inputs.ssl_verify}
onChange={handleSSLVerifyChange}
size="md"
disabled={readOnly}

View File

@ -149,7 +149,7 @@ const EmailConfigureModal = ({
</div>
</div>
<Switch
value={debugMode}
defaultValue={debugMode}
onChange={checked => setDebugMode(checked)}
/>
</div>

View File

@ -160,7 +160,7 @@ const DeliveryMethodItem: FC<DeliveryMethodItemProps> = ({
)}
{(method.config || method.type === DeliveryMethodType.WebApp) && (
<Switch
value={method.enabled}
defaultValue={method.enabled}
onChange={handleEnableStatusChange}
disabled={readonly}
/>

View File

@ -91,7 +91,7 @@ const Recipient = ({
</div>
<div className={cn('system-sm-medium grow text-text-secondary')}>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.allMembers`, { workspaceName: currentWorkspace.name.replace(/'/g, ''), ns: 'workflow' })}</div>
<Switch
value={data.whole_workspace}
defaultValue={data.whole_workspace}
onChange={checked => onChange({ ...data, whole_workspace: checked })}
/>
</div>

View File

@ -92,7 +92,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
</div>
<div className="px-4 pb-2">
<Field title={t(`${i18nPrefix}.parallelMode`, { ns: 'workflow' })} tooltip={<div className="w-[230px]">{t(`${i18nPrefix}.parallelPanelDesc`, { ns: 'workflow' })}</div>} inline>
<Switch value={inputs.is_parallel} onChange={changeParallel} />
<Switch defaultValue={inputs.is_parallel} onChange={changeParallel} />
</Field>
</div>
{
@ -130,7 +130,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
tooltip={<div className="w-[230px]">{t(`${i18nPrefix}.flattenOutputDesc`, { ns: 'workflow' })}</div>}
inline
>
<Switch value={inputs.flatten_output} onChange={changeFlattenOutput} />
<Switch defaultValue={inputs.flatten_output} onChange={changeFlattenOutput} />
</Field>
</div>
</div>

View File

@ -166,10 +166,10 @@ const SearchMethodOption = ({
<div>
{
showRerankModelSelectorSwitch && (
<div className="mb-1 flex items-center text-text-secondary system-sm-semibold">
<div className="system-sm-semibold mb-1 flex items-center text-text-secondary">
<Switch
className="mr-1"
value={rerankingModelEnabled ?? false}
defaultValue={rerankingModelEnabled}
onChange={onRerankingModelEnabledChange}
disabled={readonly}
/>
@ -192,7 +192,7 @@ const SearchMethodOption = ({
<div className="p-1">
<AlertTriangle className="size-4 text-text-warning-secondary" />
</div>
<span className="text-text-primary system-xs-medium">
<span className="system-xs-medium text-text-primary">
{t('form.retrievalSetting.multiModalTip', { ns: 'datasetSettings' })}
</span>
</div>

View File

@ -56,7 +56,7 @@ const TopKAndScoreThreshold = ({
return (
<div className="grid grid-cols-2 gap-4">
<div>
<div className="mb-0.5 flex h-6 items-center text-text-secondary system-xs-medium">
<div className="system-xs-medium mb-0.5 flex h-6 items-center text-text-secondary">
{t('datasetConfig.top_k', { ns: 'appDebug' })}
<Tooltip
triggerClassName="ml-0.5 shrink-0 w-3.5 h-3.5"
@ -78,11 +78,11 @@ const TopKAndScoreThreshold = ({
<div className="mb-0.5 flex h-6 items-center">
<Switch
className="mr-2"
value={isScoreThresholdEnabled ?? false}
defaultValue={isScoreThresholdEnabled}
onChange={onScoreThresholdEnabledChange}
disabled={readonly}
/>
<div className="grow truncate text-text-secondary system-sm-medium">
<div className="system-sm-medium grow truncate text-text-secondary">
{t('datasetConfig.score_threshold', { ns: 'appDebug' })}
</div>
<Tooltip

View File

@ -56,7 +56,7 @@ const LimitConfig: FC<Props> = ({
title={t(`${i18nPrefix}.limit`, { ns: 'workflow' })}
operations={(
<Switch
value={payload.enabled}
defaultValue={payload.enabled}
onChange={handleLimitEnabledChange}
size="md"
disabled={readonly}

View File

@ -65,7 +65,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
title={t(`${i18nPrefix}.filterCondition`, { ns: 'workflow' })}
operations={(
<Switch
value={inputs.filter_by?.enabled}
defaultValue={inputs.filter_by?.enabled}
onChange={handleFilterEnabledChange}
size="md"
disabled={readOnly}
@ -90,7 +90,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
title={t(`${i18nPrefix}.extractsCondition`, { ns: 'workflow' })}
operations={(
<Switch
value={inputs.extract_by?.enabled}
defaultValue={inputs.extract_by?.enabled}
onChange={handleExtractsEnabledChange}
size="md"
disabled={readOnly}
@ -123,7 +123,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
title={t(`${i18nPrefix}.orderBy`, { ns: 'workflow' })}
operations={(
<Switch
value={inputs.order_by?.enabled}
defaultValue={inputs.order_by?.enabled}
onChange={handleOrderByEnabledChange}
size="md"
disabled={readOnly}

View File

@ -17,7 +17,7 @@ const RequiredSwitch: FC<RequiredSwitchProps> = ({
return (
<div className="flex items-center gap-x-1 rounded-[5px] border border-divider-subtle bg-background-default-lighter px-1.5 py-1">
<span className="system-2xs-medium-uppercase text-text-secondary">{t('nodes.llm.jsonSchema.required', { ns: 'workflow' })}</span>
<Switch size="xs" value={defaultValue} onChange={toggleRequired} />
<Switch size="xs" defaultValue={defaultValue} onChange={toggleRequired} />
</div>
)
}

View File

@ -24,7 +24,7 @@ const ReasoningFormatConfig: FC<ReasoningFormatConfigProps> = ({
operations={(
// ON = separated, OFF = tagged
<Switch
value={value === 'separated'}
defaultValue={value === 'separated'}
onChange={enabled => onChange(enabled ? 'separated' : 'tagged')}
size="md"
disabled={readonly}

View File

@ -285,7 +285,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
</Tooltip>
<Switch
className="ml-2"
value={!!inputs.structured_output_enabled}
defaultValue={!!inputs.structured_output_enabled}
onChange={handleStructureOutputEnableChange}
size="md"
disabled={readOnly}

View File

@ -174,7 +174,7 @@ const AddExtractParameter: FC<Props> = ({
<Field title={t(`${i18nPrefix}.addExtractParameterContent.required`, { ns: 'workflow' })}>
<>
<div className="mb-1.5 text-xs font-normal leading-[18px] text-text-tertiary">{t(`${i18nPrefix}.addExtractParameterContent.requiredContent`, { ns: 'workflow' })}</div>
<Switch size="l" value={param.required ?? false} onChange={handleParamChange('required')} />
<Switch size="l" defaultValue={param.required} onChange={handleParamChange('required')} />
</>
</Field>
</div>

View File

@ -90,7 +90,7 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
tooltip={t(`${i18nPrefix}.aggregationGroupTip`, { ns: 'workflow' })!}
operations={(
<Switch
value={isEnableGroup}
defaultValue={isEnableGroup}
onChange={handleGroupEnabledChange}
size="md"
disabled={readOnly}

View File

@ -80,7 +80,7 @@ const Operator = ({
<div>{t('nodes.note.editor.showAuthor', { ns: 'workflow' })}</div>
<Switch
size="l"
value={showAuthor}
defaultValue={showAuthor}
onChange={onShowAuthorChange}
/>
</div>

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