From cc9b90a5aed7e67d88be1ab8b6f912d5a28d89d0 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 20 May 2026 15:25:50 +0800 Subject: [PATCH 01/12] chore(api): cap non-dev dependency major versions (#36429) --- .../trace/trace-arize-phoenix/pyproject.toml | 2 +- .../trace/trace-langsmith/pyproject.toml | 2 +- .../trace/trace-mlflow/pyproject.toml | 2 +- .../trace/trace-weave/pyproject.toml | 2 +- .../vdb/vdb-alibabacloud-mysql/pyproject.toml | 2 +- .../vdb/vdb-analyticdb/pyproject.toml | 4 +- .../vdb/vdb-clickzetta/pyproject.toml | 2 +- api/providers/vdb/vdb-hologres/pyproject.toml | 2 +- api/providers/vdb/vdb-iris/pyproject.toml | 2 +- api/providers/vdb/vdb-lindorm/pyproject.toml | 2 +- .../vdb/vdb-matrixone/pyproject.toml | 2 +- api/providers/vdb/vdb-myscale/pyproject.toml | 2 +- .../vdb/vdb-oceanbase/pyproject.toml | 4 +- .../vdb/vdb-pgvecto-rs/pyproject.toml | 2 +- api/providers/vdb/vdb-vastbase/pyproject.toml | 2 +- api/pyproject.toml | 86 +++++----- api/uv.lock | 148 +++++++++--------- dify-agent/pyproject.toml | 22 +-- dify-agent/uv.lock | 22 +-- 19 files changed, 156 insertions(+), 156 deletions(-) diff --git a/api/providers/trace/trace-arize-phoenix/pyproject.toml b/api/providers/trace/trace-arize-phoenix/pyproject.toml index 9e756944c9..b9a10e8388 100644 --- a/api/providers/trace/trace-arize-phoenix/pyproject.toml +++ b/api/providers/trace/trace-arize-phoenix/pyproject.toml @@ -2,7 +2,7 @@ name = "dify-trace-arize-phoenix" version = "0.0.1" dependencies = [ - "arize-phoenix-otel~=0.15.0", + "arize-phoenix-otel==0.15.0", ] description = "Dify ops tracing provider (Arize / Phoenix)." diff --git a/api/providers/trace/trace-langsmith/pyproject.toml b/api/providers/trace/trace-langsmith/pyproject.toml index 2ca7ff49c5..80eb9ae323 100644 --- a/api/providers/trace/trace-langsmith/pyproject.toml +++ b/api/providers/trace/trace-langsmith/pyproject.toml @@ -2,7 +2,7 @@ name = "dify-trace-langsmith" version = "0.0.1" dependencies = [ - "langsmith>=0.8.0", + "langsmith==0.8.5", ] description = "Dify ops tracing provider (LangSmith)." diff --git a/api/providers/trace/trace-mlflow/pyproject.toml b/api/providers/trace/trace-mlflow/pyproject.toml index fad6002944..a72507d877 100644 --- a/api/providers/trace/trace-mlflow/pyproject.toml +++ b/api/providers/trace/trace-mlflow/pyproject.toml @@ -2,7 +2,7 @@ name = "dify-trace-mlflow" version = "0.0.1" dependencies = [ - "mlflow-skinny>=3.11.1", + "mlflow-skinny>=3.11.1,<4.0.0", ] description = "Dify ops tracing provider (MLflow / Databricks)." diff --git a/api/providers/trace/trace-weave/pyproject.toml b/api/providers/trace/trace-weave/pyproject.toml index ba449f2a93..8225cdbf56 100644 --- a/api/providers/trace/trace-weave/pyproject.toml +++ b/api/providers/trace/trace-weave/pyproject.toml @@ -2,7 +2,7 @@ name = "dify-trace-weave" version = "0.0.1" dependencies = [ - "weave>=0.52.36", + "weave==0.52.36", ] description = "Dify ops tracing provider (Weave)." diff --git a/api/providers/vdb/vdb-alibabacloud-mysql/pyproject.toml b/api/providers/vdb/vdb-alibabacloud-mysql/pyproject.toml index bbc0e06ffa..9103f3e4f1 100644 --- a/api/providers/vdb/vdb-alibabacloud-mysql/pyproject.toml +++ b/api/providers/vdb/vdb-alibabacloud-mysql/pyproject.toml @@ -2,7 +2,7 @@ name = "dify-vdb-alibabacloud-mysql" version = "0.0.1" dependencies = [ - "mysql-connector-python>=9.3.0", + "mysql-connector-python>=9.3.0,<10.0.0", ] description = "Dify vector store backend (dify-vdb-alibabacloud-mysql)." diff --git a/api/providers/vdb/vdb-analyticdb/pyproject.toml b/api/providers/vdb/vdb-analyticdb/pyproject.toml index af5def3061..f22e3e8e12 100644 --- a/api/providers/vdb/vdb-analyticdb/pyproject.toml +++ b/api/providers/vdb/vdb-analyticdb/pyproject.toml @@ -3,8 +3,8 @@ name = "dify-vdb-analyticdb" version = "0.0.1" dependencies = [ "alibabacloud_gpdb20160503~=5.2.0", - "alibabacloud_tea_openapi~=0.4.3", - "clickhouse-connect~=0.15.0", + "alibabacloud_tea_openapi==0.4.4", + "clickhouse-connect==0.15.1", ] description = "Dify vector store backend (dify-vdb-analyticdb)." diff --git a/api/providers/vdb/vdb-clickzetta/pyproject.toml b/api/providers/vdb/vdb-clickzetta/pyproject.toml index aea94fdb2a..fd82088cb4 100644 --- a/api/providers/vdb/vdb-clickzetta/pyproject.toml +++ b/api/providers/vdb/vdb-clickzetta/pyproject.toml @@ -3,7 +3,7 @@ name = "dify-vdb-clickzetta" version = "0.0.1" dependencies = [ - "clickzetta-connector-python>=0.8.102", + "clickzetta-connector-python==0.8.104", ] description = "Dify vector store backend (dify-vdb-clickzetta)." diff --git a/api/providers/vdb/vdb-hologres/pyproject.toml b/api/providers/vdb/vdb-hologres/pyproject.toml index 88044bf6d6..bc6cfed04e 100644 --- a/api/providers/vdb/vdb-hologres/pyproject.toml +++ b/api/providers/vdb/vdb-hologres/pyproject.toml @@ -3,7 +3,7 @@ name = "dify-vdb-hologres" version = "0.0.1" dependencies = [ - "holo-search-sdk>=0.4.2", + "holo-search-sdk==0.4.2", ] description = "Dify vector store backend (dify-vdb-hologres)." diff --git a/api/providers/vdb/vdb-iris/pyproject.toml b/api/providers/vdb/vdb-iris/pyproject.toml index 6dd7a8e073..c4da985032 100644 --- a/api/providers/vdb/vdb-iris/pyproject.toml +++ b/api/providers/vdb/vdb-iris/pyproject.toml @@ -3,7 +3,7 @@ name = "dify-vdb-iris" version = "0.0.1" dependencies = [ - "intersystems-irispython>=5.1.0", + "intersystems-irispython>=5.1.0,<6.0.0", ] description = "Dify vector store backend (dify-vdb-iris)." diff --git a/api/providers/vdb/vdb-lindorm/pyproject.toml b/api/providers/vdb/vdb-lindorm/pyproject.toml index 0cffc67491..33268cc981 100644 --- a/api/providers/vdb/vdb-lindorm/pyproject.toml +++ b/api/providers/vdb/vdb-lindorm/pyproject.toml @@ -4,7 +4,7 @@ version = "0.0.1" dependencies = [ "opensearch-py==3.1.0", - "tenacity>=8.0.0", + "tenacity>=8.0.0,<9.0.0", ] description = "Dify vector store backend (dify-vdb-lindorm)." diff --git a/api/providers/vdb/vdb-matrixone/pyproject.toml b/api/providers/vdb/vdb-matrixone/pyproject.toml index 53363ed7d9..e87b9f2ec2 100644 --- a/api/providers/vdb/vdb-matrixone/pyproject.toml +++ b/api/providers/vdb/vdb-matrixone/pyproject.toml @@ -3,7 +3,7 @@ name = "dify-vdb-matrixone" version = "0.0.1" dependencies = [ - "mo-vector~=0.1.13", + "mo-vector==0.1.13", ] description = "Dify vector store backend (dify-vdb-matrixone)." diff --git a/api/providers/vdb/vdb-myscale/pyproject.toml b/api/providers/vdb/vdb-myscale/pyproject.toml index 13e0f35d23..895d498ba7 100644 --- a/api/providers/vdb/vdb-myscale/pyproject.toml +++ b/api/providers/vdb/vdb-myscale/pyproject.toml @@ -3,7 +3,7 @@ name = "dify-vdb-myscale" version = "0.0.1" dependencies = [ - "clickhouse-connect~=0.15.0", + "clickhouse-connect==0.15.1", ] description = "Dify vector store backend (dify-vdb-myscale)." diff --git a/api/providers/vdb/vdb-oceanbase/pyproject.toml b/api/providers/vdb/vdb-oceanbase/pyproject.toml index 887869a41c..7888c89724 100644 --- a/api/providers/vdb/vdb-oceanbase/pyproject.toml +++ b/api/providers/vdb/vdb-oceanbase/pyproject.toml @@ -3,8 +3,8 @@ name = "dify-vdb-oceanbase" version = "0.0.1" dependencies = [ - "pyobvector~=0.2.17", - "mysql-connector-python>=9.3.0", + "pyobvector==0.2.25", + "mysql-connector-python>=9.3.0,<10.0.0", ] description = "Dify vector store backend (dify-vdb-oceanbase)." diff --git a/api/providers/vdb/vdb-pgvecto-rs/pyproject.toml b/api/providers/vdb/vdb-pgvecto-rs/pyproject.toml index 9a25442e9e..d1e25a31ca 100644 --- a/api/providers/vdb/vdb-pgvecto-rs/pyproject.toml +++ b/api/providers/vdb/vdb-pgvecto-rs/pyproject.toml @@ -3,7 +3,7 @@ name = "dify-vdb-pgvecto-rs" version = "0.0.1" dependencies = [ - "pgvecto-rs[sqlalchemy]~=0.2.2", + "pgvecto-rs[sqlalchemy]==0.2.2", ] description = "Dify vector store backend (dify-vdb-pgvecto-rs)." diff --git a/api/providers/vdb/vdb-vastbase/pyproject.toml b/api/providers/vdb/vdb-vastbase/pyproject.toml index 287eb147dc..8fccc3b423 100644 --- a/api/providers/vdb/vdb-vastbase/pyproject.toml +++ b/api/providers/vdb/vdb-vastbase/pyproject.toml @@ -3,7 +3,7 @@ name = "dify-vdb-vastbase" version = "0.0.1" dependencies = [ - "pyobvector~=0.2.17", + "pyobvector==0.2.25", ] description = "Dify vector store backend (dify-vdb-vastbase)." diff --git a/api/pyproject.toml b/api/pyproject.toml index aa1af71b4c..1920a9f4de 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -5,48 +5,48 @@ requires-python = "~=3.12.0" dependencies = [ # Legacy: mature and widely deployed - "bleach>=6.3.0", - "boto3>=1.43.10", - "celery>=5.6.3", - "croniter>=6.2.2", + "bleach>=6.3.0,<7.0.0", + "boto3>=1.43.10,<2.0.0", + "celery>=5.6.3,<6.0.0", + "croniter>=6.2.2,<7.0.0", "dify-agent", "flask>=3.1.3,<4.0.0", - "flask-cors>=6.0.2", - "gevent>=26.4.0", - "gevent-websocket>=0.10.1", - "gmpy2>=2.3.0", - "google-api-python-client>=2.196.0", - "gunicorn>=26.0.0", - "psycogreen>=1.0.2", - "psycopg2-binary>=2.9.12", - "python-socketio>=5.13.0", - "redis[hiredis]>=7.4.0", - "sendgrid>=6.12.5", - "sseclient-py>=1.8.0", + "flask-cors>=6.0.2,<7.0.0", + "gevent>=26.4.0,<26.5.0", + "gevent-websocket==0.10.1", + "gmpy2>=2.3.0,<3.0.0", + "google-api-python-client>=2.196.0,<3.0.0", + "gunicorn>=26.0.0,<27.0.0", + "psycogreen>=1.0.2,<2.0.0", + "psycopg2-binary>=2.9.12,<3.0.0", + "python-socketio>=5.13.0,<6.0.0", + "redis[hiredis]>=7.4.0,<8.0.0", + "sendgrid>=6.12.5,<7.0.0", + "sseclient-py>=1.8.0,<2.0.0", # Stable: production-proven, cap below the next major - "aliyun-log-python-sdk>=0.9.44,<1.0.0", + "aliyun-log-python-sdk==0.9.44", "azure-identity>=1.25.3,<2.0.0", "flask-compress>=1.24,<2.0.0", - "flask-login>=0.6.3,<1.0.0", + "flask-login==0.6.3", "flask-migrate>=4.1.0,<5.0.0", "flask-orjson>=2.0.0,<3.0.0", "flask-restx>=1.3.2,<2.0.0", "google-cloud-aiplatform>=1.151.0,<2.0.0", - "httpx[socks]>=0.28.1,<1.0.0", - "opentelemetry-distro>=0.62b1,<1.0.0", - "opentelemetry-instrumentation-celery>=0.62b0,<1.0.0", - "opentelemetry-instrumentation-flask>=0.62b0,<1.0.0", - "opentelemetry-instrumentation-httpx>=0.62b0,<1.0.0", - "opentelemetry-instrumentation-redis>=0.62b0,<1.0.0", - "opentelemetry-instrumentation-sqlalchemy>=0.62b0,<1.0.0", + "httpx[socks]==0.28.1", + "opentelemetry-distro==0.62b1", + "opentelemetry-instrumentation-celery==0.62b1", + "opentelemetry-instrumentation-flask==0.62b1", + "opentelemetry-instrumentation-httpx==0.62b1", + "opentelemetry-instrumentation-redis==0.62b1", + "opentelemetry-instrumentation-sqlalchemy==0.62b1", "opentelemetry-propagator-b3>=1.41.1,<2.0.0", - "readabilipy>=0.3.0,<1.0.0", + "readabilipy==0.3.0", "resend>=2.27.0,<3.0.0", # Emerging: newer and fast-moving, use compatible pins - "fastopenapi[flask]~=0.7.0", - "graphon~=0.4.0", - "httpx-sse~=0.4.0", - "json-repair~=0.59.4", + "fastopenapi[flask]==0.7.0", + "graphon==0.4.0", + "httpx-sse==0.4.3", + "json-repair==0.59.4", ] # Before adding new dependency, consider place it in # alphabet order (a-z) and suitable group. @@ -103,8 +103,8 @@ dify-trace-weave = { workspace = true } default-groups = ["storage", "tools", "vdb-all", "trace-all"] package = false override-dependencies = [ - "litellm>=1.83.10", - "pyarrow>=18.0.0", + "litellm>=1.83.10,<2.0.0", + "pyarrow>=23.0.1,<24.0.0", ] [dependency-groups] @@ -183,21 +183,21 @@ dev = [ # Required for storage clients ############################################################ storage = [ - "azure-storage-blob>=12.29.0", - "bce-python-sdk>=0.9.71", - "cos-python-sdk-v5>=1.9.43", - "esdk-obs-python>=3.22.2", - "google-cloud-storage>=3.10.1", - "opendal>=0.46.0", - "oss2>=2.19.1", - "supabase>=2.30.0", - "tos>=2.9.0", + "azure-storage-blob>=12.29.0,<13.0.0", + "bce-python-sdk==0.9.71", + "cos-python-sdk-v5>=1.9.43,<2.0.0", + "esdk-obs-python>=3.22.2,<4.0.0", + "google-cloud-storage>=3.10.1,<4.0.0", + "opendal==0.46.0", + "oss2>=2.19.1,<3.0.0", + "supabase>=2.30.0,<3.0.0", + "tos>=2.9.0,<3.0.0", ] ############################################################ # [ Tools ] dependency group ############################################################ -tools = ["cloudscraper>=1.2.71", "nltk>=3.9.1"] +tools = ["cloudscraper>=1.2.71,<2.0.0", "nltk>=3.9.1,<4.0.0"] ############################################################ # [ VDB ] workspace plugins — hollow packages under providers/vdb/* @@ -267,7 +267,7 @@ vdb-vastbase = ["dify-vdb-vastbase"] vdb-vikingdb = ["dify-vdb-vikingdb"] vdb-weaviate = ["dify-vdb-weaviate"] # Optional client used by some tests / integrations (not a vector backend plugin) -vdb-xinference = ["xinference-client>=2.7.0"] +vdb-xinference = ["xinference-client>=2.7.0,<3.0.0"] trace-all = [ "dify-trace-aliyun", diff --git a/api/uv.lock b/api/uv.lock index b6231698d2..5e8792207e 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -51,8 +51,8 @@ members = [ "dify-vdb-weaviate", ] overrides = [ - { name = "litellm", specifier = ">=1.83.10" }, - { name = "pyarrow", specifier = ">=18.0.0" }, + { name = "litellm", specifier = ">=1.83.10,<2.0.0" }, + { name = "pyarrow", specifier = ">=23.0.1,<24.0.0" }, ] [[package]] @@ -1291,17 +1291,17 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.136.0" }, - { name = "graphon", marker = "extra == 'server'", specifier = "~=0.2.2" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "jsonschema", marker = "extra == 'server'", specifier = ">=4.23.0" }, + { name = "fastapi", marker = "extra == 'server'", specifier = "==0.136.0" }, + { name = "graphon", marker = "extra == 'server'", specifier = "==0.2.2" }, + { name = "httpx", specifier = "==0.28.1" }, + { name = "jsonschema", marker = "extra == 'server'", specifier = ">=4.23.0,<5.0.0" }, { name = "pydantic", specifier = ">=2.12.5,<2.13" }, - { name = "pydantic-ai-slim", specifier = ">=1.85.1" }, - { name = "pydantic-ai-slim", extras = ["anthropic", "google", "openai"], marker = "extra == 'server'", specifier = ">=1.85.1" }, - { name = "pydantic-settings", marker = "extra == 'server'", specifier = ">=2.12.0" }, - { name = "redis", marker = "extra == 'server'", specifier = ">=5" }, - { name = "typing-extensions", specifier = ">=4.12.2" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.38.0" }, + { name = "pydantic-ai-slim", specifier = ">=1.85.1,<2.0.0" }, + { name = "pydantic-ai-slim", extras = ["anthropic", "google", "openai"], marker = "extra == 'server'", specifier = ">=1.85.1,<2.0.0" }, + { name = "pydantic-settings", marker = "extra == 'server'", specifier = ">=2.12.0,<3.0.0" }, + { name = "redis", marker = "extra == 'server'", specifier = ">=7.4.0,<8.0.0" }, + { name = "typing-extensions", specifier = ">=4.12.2,<5.0.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = "==0.46.0" }, ] provides-extras = ["server"] @@ -1609,46 +1609,46 @@ vdb-xinference = [ [package.metadata] requires-dist = [ - { name = "aliyun-log-python-sdk", specifier = ">=0.9.44,<1.0.0" }, + { name = "aliyun-log-python-sdk", specifier = "==0.9.44" }, { name = "azure-identity", specifier = ">=1.25.3,<2.0.0" }, - { name = "bleach", specifier = ">=6.3.0" }, - { name = "boto3", specifier = ">=1.43.10" }, - { name = "celery", specifier = ">=5.6.3" }, - { name = "croniter", specifier = ">=6.2.2" }, + { name = "bleach", specifier = ">=6.3.0,<7.0.0" }, + { name = "boto3", specifier = ">=1.43.10,<2.0.0" }, + { name = "celery", specifier = ">=5.6.3,<6.0.0" }, + { name = "croniter", specifier = ">=6.2.2,<7.0.0" }, { name = "dify-agent", directory = "../dify-agent" }, - { name = "fastopenapi", extras = ["flask"], specifier = "~=0.7.0" }, + { name = "fastopenapi", extras = ["flask"], specifier = "==0.7.0" }, { name = "flask", specifier = ">=3.1.3,<4.0.0" }, { name = "flask-compress", specifier = ">=1.24,<2.0.0" }, - { name = "flask-cors", specifier = ">=6.0.2" }, - { name = "flask-login", specifier = ">=0.6.3,<1.0.0" }, + { name = "flask-cors", specifier = ">=6.0.2,<7.0.0" }, + { name = "flask-login", specifier = "==0.6.3" }, { name = "flask-migrate", specifier = ">=4.1.0,<5.0.0" }, { name = "flask-orjson", specifier = ">=2.0.0,<3.0.0" }, { name = "flask-restx", specifier = ">=1.3.2,<2.0.0" }, - { name = "gevent", specifier = ">=26.4.0" }, - { name = "gevent-websocket", specifier = ">=0.10.1" }, - { name = "gmpy2", specifier = ">=2.3.0" }, - { name = "google-api-python-client", specifier = ">=2.196.0" }, + { name = "gevent", specifier = ">=26.4.0,<26.5.0" }, + { name = "gevent-websocket", specifier = "==0.10.1" }, + { name = "gmpy2", specifier = ">=2.3.0,<3.0.0" }, + { name = "google-api-python-client", specifier = ">=2.196.0,<3.0.0" }, { name = "google-cloud-aiplatform", specifier = ">=1.151.0,<2.0.0" }, - { name = "graphon", specifier = "~=0.4.0" }, - { name = "gunicorn", specifier = ">=26.0.0" }, - { name = "httpx", extras = ["socks"], specifier = ">=0.28.1,<1.0.0" }, - { name = "httpx-sse", specifier = "~=0.4.0" }, - { name = "json-repair", specifier = "~=0.59.4" }, - { name = "opentelemetry-distro", specifier = ">=0.62b1,<1.0.0" }, - { name = "opentelemetry-instrumentation-celery", specifier = ">=0.62b0,<1.0.0" }, - { name = "opentelemetry-instrumentation-flask", specifier = ">=0.62b0,<1.0.0" }, - { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.62b0,<1.0.0" }, - { name = "opentelemetry-instrumentation-redis", specifier = ">=0.62b0,<1.0.0" }, - { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.62b0,<1.0.0" }, + { name = "graphon", specifier = "==0.4.0" }, + { name = "gunicorn", specifier = ">=26.0.0,<27.0.0" }, + { name = "httpx", extras = ["socks"], specifier = "==0.28.1" }, + { name = "httpx-sse", specifier = "==0.4.3" }, + { name = "json-repair", specifier = "==0.59.4" }, + { name = "opentelemetry-distro", specifier = "==0.62b1" }, + { name = "opentelemetry-instrumentation-celery", specifier = "==0.62b1" }, + { name = "opentelemetry-instrumentation-flask", specifier = "==0.62b1" }, + { name = "opentelemetry-instrumentation-httpx", specifier = "==0.62b1" }, + { name = "opentelemetry-instrumentation-redis", specifier = "==0.62b1" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = "==0.62b1" }, { name = "opentelemetry-propagator-b3", specifier = ">=1.41.1,<2.0.0" }, - { name = "psycogreen", specifier = ">=1.0.2" }, - { name = "psycopg2-binary", specifier = ">=2.9.12" }, - { name = "python-socketio", specifier = ">=5.13.0" }, - { name = "readabilipy", specifier = ">=0.3.0,<1.0.0" }, - { name = "redis", extras = ["hiredis"], specifier = ">=7.4.0" }, + { name = "psycogreen", specifier = ">=1.0.2,<2.0.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.12,<3.0.0" }, + { name = "python-socketio", specifier = ">=5.13.0,<6.0.0" }, + { name = "readabilipy", specifier = "==0.3.0" }, + { name = "redis", extras = ["hiredis"], specifier = ">=7.4.0,<8.0.0" }, { name = "resend", specifier = ">=2.27.0,<3.0.0" }, - { name = "sendgrid", specifier = ">=6.12.5" }, - { name = "sseclient-py", specifier = ">=1.8.0" }, + { name = "sendgrid", specifier = ">=6.12.5,<7.0.0" }, + { name = "sseclient-py", specifier = ">=1.8.0,<2.0.0" }, ] [package.metadata.requires-dev] @@ -1716,19 +1716,19 @@ dev = [ { name = "xinference-client", specifier = ">=2.7.0" }, ] storage = [ - { name = "azure-storage-blob", specifier = ">=12.29.0" }, - { name = "bce-python-sdk", specifier = ">=0.9.71" }, - { name = "cos-python-sdk-v5", specifier = ">=1.9.43" }, - { name = "esdk-obs-python", specifier = ">=3.22.2" }, - { name = "google-cloud-storage", specifier = ">=3.10.1" }, - { name = "opendal", specifier = ">=0.46.0" }, - { name = "oss2", specifier = ">=2.19.1" }, - { name = "supabase", specifier = ">=2.30.0" }, - { name = "tos", specifier = ">=2.9.0" }, + { name = "azure-storage-blob", specifier = ">=12.29.0,<13.0.0" }, + { name = "bce-python-sdk", specifier = "==0.9.71" }, + { name = "cos-python-sdk-v5", specifier = ">=1.9.43,<2.0.0" }, + { name = "esdk-obs-python", specifier = ">=3.22.2,<4.0.0" }, + { name = "google-cloud-storage", specifier = ">=3.10.1,<4.0.0" }, + { name = "opendal", specifier = "==0.46.0" }, + { name = "oss2", specifier = ">=2.19.1,<3.0.0" }, + { name = "supabase", specifier = ">=2.30.0,<3.0.0" }, + { name = "tos", specifier = ">=2.9.0,<3.0.0" }, ] tools = [ - { name = "cloudscraper", specifier = ">=1.2.71" }, - { name = "nltk", specifier = ">=3.9.1" }, + { name = "cloudscraper", specifier = ">=1.2.71,<2.0.0" }, + { name = "nltk", specifier = ">=3.9.1,<4.0.0" }, ] trace-aliyun = [{ name = "dify-trace-aliyun", editable = "providers/trace/trace-aliyun" }] trace-all = [ @@ -1810,7 +1810,7 @@ vdb-upstash = [{ name = "dify-vdb-upstash", editable = "providers/vdb/vdb-upstas vdb-vastbase = [{ name = "dify-vdb-vastbase", editable = "providers/vdb/vdb-vastbase" }] vdb-vikingdb = [{ name = "dify-vdb-vikingdb", editable = "providers/vdb/vdb-vikingdb" }] vdb-weaviate = [{ name = "dify-vdb-weaviate", editable = "providers/vdb/vdb-weaviate" }] -vdb-xinference = [{ name = "xinference-client", specifier = ">=2.7.0" }] +vdb-xinference = [{ name = "xinference-client", specifier = ">=2.7.0,<3.0.0" }] [[package]] name = "dify-trace-aliyun" @@ -1840,7 +1840,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "arize-phoenix-otel", specifier = "~=0.15.0" }] +requires-dist = [{ name = "arize-phoenix-otel", specifier = "==0.15.0" }] [[package]] name = "dify-trace-langfuse" @@ -1862,7 +1862,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "langsmith", specifier = ">=0.8.0" }] +requires-dist = [{ name = "langsmith", specifier = "==0.8.5" }] [[package]] name = "dify-trace-mlflow" @@ -1873,7 +1873,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "mlflow-skinny", specifier = ">=3.11.1" }] +requires-dist = [{ name = "mlflow-skinny", specifier = ">=3.11.1,<4.0.0" }] [[package]] name = "dify-trace-opik" @@ -1914,7 +1914,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "weave", specifier = ">=0.52.36" }] +requires-dist = [{ name = "weave", specifier = "==0.52.36" }] [[package]] name = "dify-vdb-alibabacloud-mysql" @@ -1925,7 +1925,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "mysql-connector-python", specifier = ">=9.3.0" }] +requires-dist = [{ name = "mysql-connector-python", specifier = ">=9.3.0,<10.0.0" }] [[package]] name = "dify-vdb-analyticdb" @@ -1940,8 +1940,8 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "alibabacloud-gpdb20160503", specifier = "~=5.2.0" }, - { name = "alibabacloud-tea-openapi", specifier = "~=0.4.3" }, - { name = "clickhouse-connect", specifier = "~=0.15.0" }, + { name = "alibabacloud-tea-openapi", specifier = "==0.4.4" }, + { name = "clickhouse-connect", specifier = "==0.15.1" }, ] [[package]] @@ -1975,7 +1975,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "clickzetta-connector-python", specifier = ">=0.8.102" }] +requires-dist = [{ name = "clickzetta-connector-python", specifier = "==0.8.104" }] [[package]] name = "dify-vdb-couchbase" @@ -2008,7 +2008,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "holo-search-sdk", specifier = ">=0.4.2" }] +requires-dist = [{ name = "holo-search-sdk", specifier = "==0.4.2" }] [[package]] name = "dify-vdb-huawei-cloud" @@ -2030,7 +2030,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "intersystems-irispython", specifier = ">=5.1.0" }] +requires-dist = [{ name = "intersystems-irispython", specifier = ">=5.1.0,<6.0.0" }] [[package]] name = "dify-vdb-lindorm" @@ -2044,7 +2044,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "opensearch-py", specifier = "==3.1.0" }, - { name = "tenacity", specifier = ">=8.0.0" }, + { name = "tenacity", specifier = ">=8.0.0,<9.0.0" }, ] [[package]] @@ -2056,7 +2056,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "mo-vector", specifier = "~=0.1.13" }] +requires-dist = [{ name = "mo-vector", specifier = "==0.1.13" }] [[package]] name = "dify-vdb-milvus" @@ -2078,7 +2078,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "clickhouse-connect", specifier = "~=0.15.0" }] +requires-dist = [{ name = "clickhouse-connect", specifier = "==0.15.1" }] [[package]] name = "dify-vdb-oceanbase" @@ -2091,8 +2091,8 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "mysql-connector-python", specifier = ">=9.3.0" }, - { name = "pyobvector", specifier = "~=0.2.17" }, + { name = "mysql-connector-python", specifier = ">=9.3.0,<10.0.0" }, + { name = "pyobvector", specifier = "==0.2.25" }, ] [[package]] @@ -2131,7 +2131,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "pgvecto-rs", extras = ["sqlalchemy"], specifier = "~=0.2.2" }] +requires-dist = [{ name = "pgvecto-rs", extras = ["sqlalchemy"], specifier = "==0.2.2" }] [[package]] name = "dify-vdb-pgvector" @@ -2224,7 +2224,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "pyobvector", specifier = "~=0.2.17" }] +requires-dist = [{ name = "pyobvector", specifier = "==0.2.25" }] [[package]] name = "dify-vdb-vikingdb" @@ -6443,11 +6443,11 @@ wheels = [ [[package]] name = "tenacity" -version = "9.1.2" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" }, ] [[package]] diff --git a/dify-agent/pyproject.toml b/dify-agent/pyproject.toml index d9b2796570..7975b042d4 100644 --- a/dify-agent/pyproject.toml +++ b/dify-agent/pyproject.toml @@ -3,23 +3,23 @@ name = "dify-agent" version = "0.1.0" description = "Add your description here" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.12,<4.0" dependencies = [ - "httpx>=0.28.1", + "httpx==0.28.1", "pydantic>=2.12.5,<2.13", - "pydantic-ai-slim>=1.85.1", - "typing-extensions>=4.12.2", + "pydantic-ai-slim>=1.85.1,<2.0.0", + "typing-extensions>=4.12.2,<5.0.0", ] [project.optional-dependencies] server = [ - "fastapi>=0.136.0", - "graphon~=0.2.2", - "jsonschema>=4.23.0", - "pydantic-ai-slim[anthropic,google,openai]>=1.85.1", - "pydantic-settings>=2.12.0", - "redis>=5", - "uvicorn[standard]>=0.38.0", + "fastapi==0.136.0", + "graphon==0.2.2", + "jsonschema>=4.23.0,<5.0.0", + "pydantic-ai-slim[anthropic,google,openai]>=1.85.1,<2.0.0", + "pydantic-settings>=2.12.0,<3.0.0", + "redis>=7.4.0,<8.0.0", + "uvicorn[standard]==0.46.0", ] [tool.setuptools.packages.find] diff --git a/dify-agent/uv.lock b/dify-agent/uv.lock index d3b4b09ba0..f18d4e3e4a 100644 --- a/dify-agent/uv.lock +++ b/dify-agent/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.12" +requires-python = ">=3.12, <4.0" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", @@ -610,17 +610,17 @@ docs = [ [package.metadata] requires-dist = [ - { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.136.0" }, - { name = "graphon", marker = "extra == 'server'", specifier = "~=0.2.2" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "jsonschema", marker = "extra == 'server'", specifier = ">=4.23.0" }, + { name = "fastapi", marker = "extra == 'server'", specifier = "==0.136.0" }, + { name = "graphon", marker = "extra == 'server'", specifier = "==0.2.2" }, + { name = "httpx", specifier = "==0.28.1" }, + { name = "jsonschema", marker = "extra == 'server'", specifier = ">=4.23.0,<5.0.0" }, { name = "pydantic", specifier = ">=2.12.5,<2.13" }, - { name = "pydantic-ai-slim", specifier = ">=1.85.1" }, - { name = "pydantic-ai-slim", extras = ["anthropic", "google", "openai"], marker = "extra == 'server'", specifier = ">=1.85.1" }, - { name = "pydantic-settings", marker = "extra == 'server'", specifier = ">=2.12.0" }, - { name = "redis", marker = "extra == 'server'", specifier = ">=5" }, - { name = "typing-extensions", specifier = ">=4.12.2" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.38.0" }, + { name = "pydantic-ai-slim", specifier = ">=1.85.1,<2.0.0" }, + { name = "pydantic-ai-slim", extras = ["anthropic", "google", "openai"], marker = "extra == 'server'", specifier = ">=1.85.1,<2.0.0" }, + { name = "pydantic-settings", marker = "extra == 'server'", specifier = ">=2.12.0,<3.0.0" }, + { name = "redis", marker = "extra == 'server'", specifier = ">=7.4.0,<8.0.0" }, + { name = "typing-extensions", specifier = ">=4.12.2,<5.0.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = "==0.46.0" }, ] provides-extras = ["server"] From 5a585c86184e084a61c4fcad4d4cd0135094cb01 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 20 May 2026 15:32:11 +0800 Subject: [PATCH 02/12] refactor(web): use dropdown data attributes (#36431) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 40 -------- .../app-sidebar/dataset-info/dropdown.tsx | 2 +- .../config/automatic/version-selector.tsx | 27 ++---- .../debug-with-multiple-model/debug-item.tsx | 11 +-- .../workflow-log/__tests__/filter.spec.tsx | 14 +-- web/app/components/apps/app-card.tsx | 3 +- .../header/__tests__/operation.spec.tsx | 5 +- .../chat-with-history/header/operation.tsx | 25 ++--- .../chat/chat-with-history/sidebar/index.tsx | 14 +-- .../base/chip/__tests__/index.spec.tsx | 21 ++--- web/app/components/base/chip/index.tsx | 79 ++++++++-------- web/app/components/base/sort/index.tsx | 37 +++----- .../preview-document-picker.tsx | 2 +- .../list/template-card/actions.tsx | 2 +- .../dropdown/__tests__/index.spec.tsx | 4 +- .../dropdown/__tests__/item.spec.tsx | 18 +++- .../dropdown/__tests__/menu.spec.tsx | 20 +++- .../header/breadcrumbs/dropdown/index.tsx | 17 +--- .../header/breadcrumbs/dropdown/item.tsx | 12 +-- .../completed/__tests__/status-item.spec.tsx | 93 ------------------- .../components/__tests__/menu-bar.spec.tsx | 17 +--- .../detail/completed/components/menu-bar.tsx | 11 +-- .../detail/completed/status-item.tsx | 27 ------ .../detail/completed/style.module.css | 3 - .../segment-add/__tests__/index.spec.tsx | 12 +++ .../documents/detail/segment-add/index.tsx | 38 ++------ .../datasets/documents/style.module.css | 2 +- .../components/operations-dropdown.tsx | 2 +- .../settings/permission-selector/index.tsx | 5 +- .../item-operation/__tests__/index.spec.tsx | 48 ++++++---- .../explore/item-operation/index.tsx | 34 ++----- .../explore/item-operation/style.module.css | 1 + .../__tests__/support.spec.tsx | 7 +- .../header/account-dropdown/index.tsx | 9 +- .../header/account-dropdown/support.tsx | 7 +- .../data-source-page-new/operator.tsx | 8 +- .../members-page/operation/index.tsx | 12 +-- .../__tests__/member-selector.spec.tsx | 3 +- .../member-selector.tsx | 5 +- .../operation-dropdown.tsx | 5 +- .../install-plugin-dropdown.spec.tsx | 22 +++-- .../plugin-page/install-plugin-dropdown.tsx | 28 +++--- .../__tests__/menu-dropdown.spec.tsx | 19 ---- .../share/text-generation/menu-dropdown.tsx | 28 ++---- .../__tests__/operation-dropdown.spec.tsx | 20 +++- .../tools/mcp/detail/operation-dropdown.tsx | 10 +- .../__tests__/action.spec.tsx | 58 ++++++------ .../market-place-plugin/action.tsx | 57 +++++------- .../components/operation-selector.tsx | 4 +- .../delivery-method/method-selector.tsx | 2 +- .../__tests__/member-selector.spec.tsx | 67 ++----------- .../recipient/member-selector.tsx | 3 +- .../toolbar/__tests__/operator.spec.tsx | 12 ++- .../note-editor/toolbar/operator.tsx | 4 +- .../workflow/operator/zoom-in-out.tsx | 41 ++++---- .../components/zoom-in-out.tsx | 31 +++---- web/contract/marketplace.ts | 14 +++ web/contract/router.ts | 3 +- web/service/use-plugins.ts | 9 -- 59 files changed, 402 insertions(+), 732 deletions(-) delete mode 100644 web/app/components/datasets/documents/detail/completed/__tests__/status-item.spec.tsx delete mode 100644 web/app/components/datasets/documents/detail/completed/status-item.tsx diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 829ee56d7e..01720b8e9f 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -784,11 +784,6 @@ "count": 10 } }, - "web/app/components/base/chip/index.tsx": { - "ts/no-explicit-any": { - "count": 3 - } - }, "web/app/components/base/date-and-time-picker/hooks.ts": { "react/no-unnecessary-use-prefix": { "count": 2 @@ -1607,11 +1602,6 @@ "count": 1 } }, - "web/app/components/base/sort/index.tsx": { - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/base/svg-gallery/index.tsx": { "node/prefer-global/buffer": { "count": 1 @@ -2120,11 +2110,6 @@ "count": 2 } }, - "web/app/components/explore/item-operation/index.tsx": { - "react/set-state-in-effect": { - "count": 1 - } - }, "web/app/components/explore/try-app/tab.tsx": { "erasable-syntax-only/enums": { "count": 1 @@ -2596,11 +2581,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx": { - "react/set-state-in-effect": { - "count": 2 - } - }, "web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts": { "erasable-syntax-only/enums": { "count": 2 @@ -2778,11 +2758,6 @@ "count": 1 } }, - "web/app/components/share/text-generation/menu-dropdown.tsx": { - "react/set-state-in-effect": { - "count": 1 - } - }, "web/app/components/share/text-generation/no-data/index.tsx": { "ts/no-empty-object-type": { "count": 1 @@ -2950,11 +2925,6 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/market-place-plugin/action.tsx": { - "react/set-state-in-effect": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/market-place-plugin/item.tsx": { "erasable-syntax-only/enums": { "count": 1 @@ -4051,11 +4021,6 @@ "count": 1 } }, - "web/app/components/workflow/operator/zoom-in-out.tsx": { - "erasable-syntax-only/enums": { - "count": 1 - } - }, "web/app/components/workflow/panel/__tests__/index.spec.tsx": { "react/static-components": { "count": 2 @@ -4378,11 +4343,6 @@ "count": 1 } }, - "web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx": { - "erasable-syntax-only/enums": { - "count": 1 - } - }, "web/app/education-apply/hooks.ts": { "react/set-state-in-effect": { "count": 5 diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index fa5a40f8a4..b06d92e8d9 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -134,7 +134,7 @@ const DropDown = ({ render={( )} > diff --git a/web/app/components/app/configuration/config/automatic/version-selector.tsx b/web/app/components/app/configuration/config/automatic/version-selector.tsx index 60584f1a72..cb4f2cb9f5 100644 --- a/web/app/components/app/configuration/config/automatic/version-selector.tsx +++ b/web/app/components/app/configuration/config/automatic/version-selector.tsx @@ -7,9 +7,7 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react' -import { useBoolean } from 'ahooks' import * as React from 'react' -import { useCallback } from 'react' import { useTranslation } from 'react-i18next' type VersionSelectorProps = { @@ -20,17 +18,7 @@ type VersionSelectorProps = { const VersionSelector: React.FC = ({ versionLen, value, onChange }) => { const { t } = useTranslation() - const [isOpen, { - setFalse: handleOpenFalse, - set: handleOpenSet, - }] = useBoolean(false) - const moreThanOneVersion = versionLen > 1 - const handleOpen = useCallback((nextOpen: boolean) => { - if (moreThanOneVersion) - handleOpenSet(nextOpen) - }, [moreThanOneVersion, handleOpenSet]) - const versions = Array.from({ length: versionLen }, (_, index) => ({ label: `${t('generate.version', { ns: 'appDebug' })} ${index + 1}${index === versionLen - 1 ? ` · ${t('generate.latest', { ns: 'appDebug' })}` : ''}`, value: index, @@ -39,14 +27,12 @@ const VersionSelector: React.FC = ({ versionLen, value, on const isLatest = value === versionLen - 1 return ( - + + disabled={!moreThanOneVersion} + className={cn( + 'flex items-center border-none bg-transparent p-0 system-xs-medium text-text-tertiary', + moreThanOneVersion ? 'cursor-pointer data-popup-open:text-text-secondary' : 'cursor-default', )} >
@@ -63,14 +49,13 @@ const VersionSelector: React.FC = ({ versionLen, value, on alignOffset={-12} popupClassName="w-[208px] rounded-xl border-[0.5px] bg-components-panel-bg-blur p-1" > -
+
{t('generate.versions', { ns: 'appDebug' })}
{ onChange(nextValue) - handleOpenFalse() }} > {versions.map(option => ( diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx index 064a1fb97f..db79804755 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx @@ -1,6 +1,5 @@ import type { CSSProperties, FC } from 'react' import type { ModelAndParameter } from '../types' -import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, @@ -8,7 +7,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { memo, useState } from 'react' +import { memo } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { ModelStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -42,10 +41,8 @@ const DebugItem: FC = ({ const index = multipleModelConfigs.findIndex(v => v.id === modelAndParameter.id) const currentProvider = textGenerationModelList.find(item => item.provider === modelAndParameter.provider) const currentModel = currentProvider?.models.find(item => item.model === modelAndParameter.model) - const [open, setOpen] = useState(false) const handleDuplicate = () => { - setOpen(false) if (multipleModelConfigs.length >= 4) return @@ -63,12 +60,10 @@ const DebugItem: FC = ({ } const handleDebugAsSingleModel = () => { - setOpen(false) onDebugWithMultipleModelChange(modelAndParameter) } const handleRemove = () => { - setOpen(false) onMultipleModelConfigsChange( true, multipleModelConfigs.filter(item => item.id !== modelAndParameter.id), @@ -92,11 +87,11 @@ const DebugItem: FC = ({ - + diff --git a/web/app/components/app/workflow-log/__tests__/filter.spec.tsx b/web/app/components/app/workflow-log/__tests__/filter.spec.tsx index 59063dfb98..99443ae1e0 100644 --- a/web/app/components/app/workflow-log/__tests__/filter.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/filter.spec.tsx @@ -8,7 +8,7 @@ */ import type { QueryParam } from '../index' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' import Filter, { TIME_PERIOD_MAPPING } from '../filter' @@ -297,13 +297,13 @@ describe('Filter', () => { it('should call setQueryParams when typing in search', async () => { const user = userEvent.setup() - const setQueryParams = vi.fn() + const onSetQueryParams = vi.fn() const Wrapper = () => { - const [queryParams, updateQueryParams] = useState(createDefaultQueryParams()) + const [queryParams, setQueryParams] = useState(() => createDefaultQueryParams()) const handleSetQueryParams = (next: QueryParam) => { - updateQueryParams(next) setQueryParams(next) + onSetQueryParams(next) } return ( { await user.type(input, 'workflow') // Should call setQueryParams for each character typed - expect(setQueryParams).toHaveBeenLastCalledWith( + expect(onSetQueryParams).toHaveBeenLastCalledWith( expect.objectContaining({ keyword: 'workflow' }), ) }) @@ -335,7 +335,9 @@ describe('Filter', () => { />, ) - await user.click(screen.getByRole('button', { name: 'common.operation.clear' })) + const searchInput = screen.getByPlaceholderText('common.operation.search') + const searchField = searchInput.closest('div')! + await user.click(within(searchField).getByRole('button', { name: 'common.operation.clear' })) expect(setQueryParams).toHaveBeenCalledWith({ status: 'all', diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 262f0fee56..c4755e3835 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -532,8 +532,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () => { e.stopPropagation() diff --git a/web/app/components/base/chat/chat-with-history/header/__tests__/operation.spec.tsx b/web/app/components/base/chat/chat-with-history/header/__tests__/operation.spec.tsx index 294d5eebc5..60fc1a2b03 100644 --- a/web/app/components/base/chat/chat-with-history/header/__tests__/operation.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/header/__tests__/operation.spec.tsx @@ -95,10 +95,11 @@ describe('Operation Component', () => { const trigger = screen.getByText('Chat Title').closest('.cursor-pointer') // closed state - expect(trigger).not.toHaveClass('bg-state-base-hover') + expect(trigger).toHaveClass('data-popup-open:bg-state-base-hover') + expect(trigger).not.toHaveAttribute('data-popup-open') // open state await user.click(screen.getByText('Chat Title')) - expect(trigger).toHaveClass('bg-state-base-hover') + expect(trigger).toHaveAttribute('data-popup-open') }) }) diff --git a/web/app/components/base/chat/chat-with-history/header/operation.tsx b/web/app/components/base/chat/chat-with-history/header/operation.tsx index 066cec5183..57ac96e366 100644 --- a/web/app/components/base/chat/chat-with-history/header/operation.tsx +++ b/web/app/components/base/chat/chat-with-history/header/operation.tsx @@ -1,7 +1,6 @@ 'use client' import type { Placement } from '@langgenius/dify-ui/dropdown-menu' import type { FC } from 'react' -import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, @@ -9,7 +8,6 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import * as React from 'react' -import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' type Props = { @@ -23,6 +21,10 @@ type Props = { placement?: Placement } +const deferAction = (action: () => void) => { + queueMicrotask(action) +} + const Operation: FC = ({ title, isPinned, @@ -34,22 +36,11 @@ const Operation: FC = ({ placement = 'bottom-start', }) => { const { t } = useTranslation() - const [open, setOpen] = useState(false) - const handleDeferredAction = useCallback((action: () => void) => { - setOpen(false) - queueMicrotask(action) - }, []) return ( - + {title} @@ -65,7 +56,7 @@ const Operation: FC = ({ {isShowRenameConversation && ( onRenameConversation && handleDeferredAction(onRenameConversation)} + onClick={() => onRenameConversation && deferAction(onRenameConversation)} > {t('sidebar.action.rename', { ns: 'explore' })} @@ -74,7 +65,7 @@ const Operation: FC = ({ handleDeferredAction(onDelete)} + onClick={() => deferAction(onDelete)} > {t('sidebar.action.delete', { ns: 'explore' })} diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index 38b91e439b..d44d3f86fa 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -10,11 +10,6 @@ import { } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { - RiEditBoxLine, - RiExpandRightLine, - RiLayoutLeft2Line, -} from '@remixicon/react' import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, @@ -35,7 +30,7 @@ type Props = { panelVisible?: boolean } -const Sidebar = ({ isPanel, panelVisible }: Props) => { +const Sidebar = ({ isPanel }: Props) => { const { t } = useTranslation() const { isInstalledApp, @@ -112,18 +107,18 @@ const Sidebar = ({ isPanel, panelVisible }: Props) => {
{appData?.site.title}
{!isMobile && isSidebarCollapsed && ( handleSidebarCollapse(false)}> - + )} {!isMobile && !isSidebarCollapsed && ( handleSidebarCollapse(true)}> - + )}
@@ -156,7 +151,6 @@ const Sidebar = ({ isPanel, panelVisible }: Props) => { hideLogout={isInstalledApp} placement="top-start" data={appData?.site} - forceClose={isPanel && !panelVisible} /> {/* powered by */}
diff --git a/web/app/components/base/chip/__tests__/index.spec.tsx b/web/app/components/base/chip/__tests__/index.spec.tsx index 40ecbb7a33..826441779d 100644 --- a/web/app/components/base/chip/__tests__/index.spec.tsx +++ b/web/app/components/base/chip/__tests__/index.spec.tsx @@ -40,7 +40,7 @@ describe('Chip', () => { // Helper function to get the trigger element const getTrigger = (container: HTMLElement) => { - return container.querySelector('[role="button"][aria-haspopup="menu"]') as HTMLElement | null + return container.querySelector('button[aria-haspopup="menu"], [role="button"][aria-haspopup="menu"]') as HTMLElement | null } // Helper function to open dropdown panel @@ -98,12 +98,11 @@ describe('Chip', () => { }) it('should hide left icon when showLeftIcon is false', () => { - renderChip({ showLeftIcon: false }) + renderChip({ showLeftIcon: false, value: '' }) // When showLeftIcon is false, there should be no filter icon before the text - const textElement = screen.getByText('All Items') - const parent = textElement.closest('[role="button"]') - const icons = parent?.querySelectorAll('svg') + const trigger = getTrigger(document.body) + const icons = trigger?.querySelectorAll('svg') // Should only have the arrow icon, not the filter icon expect(icons?.length).toBe(1) @@ -190,12 +189,7 @@ describe('Chip', () => { it('should call onClear when clear button is clicked', () => { const { container } = renderChip({ value: 'active' }) - // Find the close icon (last SVG in the trigger) and click its parent - const trigger = getTrigger(container) - const svgs = trigger?.querySelectorAll('svg') - // The close icon should be the last SVG element - const closeIcon = svgs?.[svgs.length - 1] - const clearButton = closeIcon?.parentElement + const clearButton = container.querySelector('button[aria-label="common.operation.clear"]') expect(clearButton)!.toBeInTheDocument() if (clearButton) @@ -210,10 +204,7 @@ describe('Chip', () => { const trigger = getTrigger(container) expect(trigger)!.toHaveAttribute('aria-expanded', 'false') - // Find the close icon (last SVG) and click its parent - const svgs = trigger?.querySelectorAll('svg') - const closeIcon = svgs?.[svgs.length - 1] - const clearButton = closeIcon?.parentElement + const clearButton = container.querySelector('button[aria-label="common.operation.clear"]') if (clearButton) fireEvent.click(clearButton) diff --git a/web/app/components/base/chip/index.tsx b/web/app/components/base/chip/index.tsx index 6ba5d9cb44..3190fb0ccb 100644 --- a/web/app/components/base/chip/index.tsx +++ b/web/app/components/base/chip/index.tsx @@ -1,4 +1,4 @@ -import type { FC } from 'react' +import type { ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, @@ -8,24 +8,28 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { RiArrowDownSLine, RiCheckLine, RiCloseCircleFill, RiFilter3Line } from '@remixicon/react' -import { useMemo, useState } from 'react' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' -export type Item = { - value: number | string +type ItemValue = number | string + +export type Item = { + value: T name: string -} & Record +} & Record -type Props = { +type Props = { className?: string panelClassName?: string showLeftIcon?: boolean - leftIcon?: any - value: number | string - items: Item[] - onSelect: (item: any) => void + leftIcon?: ReactNode + value: T + items: Item[] + onSelect: (item: Item) => void onClear: () => void } -const Chip: FC = ({ + +function Chip({ className, panelClassName, showLeftIcon = true, @@ -34,31 +38,24 @@ const Chip: FC = ({ items, onSelect, onClear, -}) => { - const [open, setOpen] = useState(false) - +}: Props) { + const { t } = useTranslation() const triggerContent = useMemo(() => { return items.find(item => item.value === value)?.name || '' }, [items, value]) return ( - +
- } - > -
+ > + {showLeftIcon && (
{leftIcon || ( @@ -72,19 +69,21 @@ const Chip: FC = ({
{!value && } - {!!value && ( -
{ - e.stopPropagation() - onClear() - }} - > - -
- )} -
- + + {!!value && ( + + )} +
+} & Record type Props = { order?: string value: number | string items: Item[] - onSelect: (item: any) => void + onSelect: (value: string) => void } -const Sort: FC = ({ + +function Sort({ order, value, items, onSelect, -}) => { +}: Props) { const { t } = useTranslation() - const [open, setOpen] = useState(false) const triggerContent = useMemo(() => { return items.find(item => item.value === value)?.name || '' @@ -37,28 +36,18 @@ const Sort: FC = ({ return (
- +
} + className="flex min-h-8 cursor-pointer items-center rounded-l-lg border-none bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt data-popup-open:bg-state-base-hover-alt! data-popup-open:hover:bg-state-base-hover-alt" > -
-
-
{t('filter.sortBy', { ns: 'appLog' })}
-
- {triggerContent} -
+
+
{t('filter.sortBy', { ns: 'appLog' })}
+
+ {triggerContent}
-
+ = ({ +
diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/actions.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/actions.tsx index 34b0673c59..b6e3fc63a4 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/actions.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/actions.tsx @@ -59,7 +59,7 @@ const Actions = ({ aria-label={t('operation.more', { ns: 'common' })} className={cn( 'flex size-8 cursor-pointer items-center justify-center rounded-lg p-0 shadow-xs shadow-shadow-shadow-3', - isMoreOperationsOpen && 'bg-state-base-hover', + 'data-popup-open:bg-state-base-hover', )} onClick={e => e.stopPropagation()} > diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/index.spec.tsx index d57e8340e9..d93de876e7 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/index.spec.tsx @@ -368,9 +368,9 @@ describe('Dropdown', () => { // Act - Open dropdown fireEvent.click(button) - // Assert - Open state: should have bg-state-base-hover + // Assert - Open state is exposed declaratively via data-popup-open await waitFor(() => { - expect(button)!.toHaveClass('bg-state-base-hover') + expect(button).toHaveAttribute('data-popup-open') }) }) }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/item.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/item.spec.tsx index 4437305ad4..fd582a3bae 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/item.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/item.spec.tsx @@ -1,7 +1,19 @@ +import type { ReactElement } from 'react' +import { DropdownMenu, DropdownMenuContent } from '@langgenius/dify-ui/dropdown-menu' import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import Item from '../item' +const renderItem = (ui: ReactElement) => { + return render( + + + {ui} + + , + ) +} + describe('Item', () => { const defaultProps = { name: 'Documents', @@ -16,7 +28,7 @@ describe('Item', () => { // Rendering: verify the breadcrumb name is displayed describe('Rendering', () => { it('should render breadcrumb name', () => { - render() + renderItem() expect(screen.getByText('Documents')).toBeInTheDocument() }) @@ -25,7 +37,7 @@ describe('Item', () => { // User interactions: clicking triggers callback with correct index describe('User Interactions', () => { it('should call onBreadcrumbClick with correct index on click', () => { - render() + renderItem() fireEvent.click(screen.getByText('Documents')) @@ -34,7 +46,7 @@ describe('Item', () => { }) it('should pass different index values correctly', () => { - render() + renderItem() fireEvent.click(screen.getByText('Documents')) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/menu.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/menu.spec.tsx index c8c6b8fec3..80c6f7bfb0 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/menu.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/__tests__/menu.spec.tsx @@ -1,7 +1,19 @@ +import type { ReactElement } from 'react' +import { DropdownMenu, DropdownMenuContent } from '@langgenius/dify-ui/dropdown-menu' import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import Menu from '../menu' +const renderMenu = (ui: ReactElement) => { + return render( + + + {ui} + + , + ) +} + describe('Menu', () => { const defaultProps = { breadcrumbs: ['Folder A', 'Folder B', 'Folder C'], @@ -16,7 +28,7 @@ describe('Menu', () => { // Rendering: verify all breadcrumb items are displayed describe('Rendering', () => { it('should render all breadcrumb items', () => { - render() + renderMenu() expect(screen.getByText('Folder A')).toBeInTheDocument() expect(screen.getByText('Folder B')).toBeInTheDocument() @@ -36,7 +48,7 @@ describe('Menu', () => { // Index mapping: startIndex offsets are applied correctly describe('Index Mapping', () => { it('should pass correct index (startIndex + offset) to each item', () => { - render() + renderMenu() fireEvent.click(screen.getByText('Folder A')) expect(defaultProps.onBreadcrumbClick).toHaveBeenCalledWith(1) @@ -49,7 +61,7 @@ describe('Menu', () => { }) it('should offset from startIndex of zero', () => { - render( + renderMenu( { // User interactions: clicking items triggers the callback describe('User Interactions', () => { it('should call onBreadcrumbClick with correct index when item clicked', () => { - render() + renderMenu() fireEvent.click(screen.getByText('Folder B')) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/index.tsx index a77ba87ac6..17e824ff97 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/index.tsx @@ -5,7 +5,6 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import * as React from 'react' -import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Menu from './menu' @@ -21,18 +20,9 @@ const Dropdown = ({ onBreadcrumbClick, }: DropdownProps) => { const { t } = useTranslation() - const [open, setOpen] = useState(false) - - const handleBreadCrumbClick = useCallback((index: number) => { - onBreadcrumbClick(index) - setOpen(false) - }, [onBreadcrumbClick]) return ( - + @@ -56,7 +45,7 @@ const Dropdown = ({ / diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/item.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/item.tsx index 6f04ede88a..beb217f6b7 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/item.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/dropdown/item.tsx @@ -1,5 +1,5 @@ +import { DropdownMenuItem } from '@langgenius/dify-ui/dropdown-menu' import * as React from 'react' -import { useCallback } from 'react' type ItemProps = { name: string @@ -12,17 +12,13 @@ const Item = ({ index, onBreadcrumbClick, }: ItemProps) => { - const handleClick = useCallback(() => { - onBreadcrumbClick(index) - }, [index, onBreadcrumbClick]) - return ( -
onBreadcrumbClick(index)} > {name} -
+ ) } diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/status-item.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/status-item.spec.tsx deleted file mode 100644 index da7e301e4d..0000000000 --- a/web/app/components/datasets/documents/detail/completed/__tests__/status-item.spec.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import StatusItem from '../status-item' - -describe('StatusItem', () => { - const defaultItem = { - value: '1', - name: 'Test Status', - } - - describe('Rendering', () => { - it('should render without crashing', () => { - const { container } = render() - - expect(container.firstChild).toBeInTheDocument() - }) - - it('should render item name', () => { - render() - - expect(screen.getByText('Test Status')).toBeInTheDocument() - }) - - it('should render with correct styling classes', () => { - const { container } = render() - - const wrapper = container.firstChild as HTMLElement - expect(wrapper).toHaveClass('flex') - expect(wrapper).toHaveClass('items-center') - expect(wrapper).toHaveClass('justify-between') - }) - }) - - describe('Props', () => { - it('should show check icon when selected is true', () => { - const { container } = render() - - // Assert - RiCheckLine icon should be present - const checkIcon = container.querySelector('.text-text-accent') - expect(checkIcon).toBeInTheDocument() - }) - - it('should not show check icon when selected is false', () => { - const { container } = render() - - // Assert - RiCheckLine icon should not be present - const checkIcon = container.querySelector('.text-text-accent') - expect(checkIcon).not.toBeInTheDocument() - }) - - it('should render different item names', () => { - const item = { value: '2', name: 'Different Status' } - render() - - expect(screen.getByText('Different Status')).toBeInTheDocument() - }) - }) - - describe('Memoization', () => { - it('should render consistently with same props', () => { - const { container: container1 } = render() - const { container: container2 } = render() - - expect(container1.textContent).toBe(container2.textContent) - }) - }) - - describe('Edge Cases', () => { - it('should handle empty item name', () => { - const emptyItem = { value: '1', name: '' } - - const { container } = render() - - expect(container.firstChild).toBeInTheDocument() - }) - - it('should handle special characters in item name', () => { - const specialItem = { value: '1', name: 'Status <>&"' } - - render() - - expect(screen.getByText('Status <>&"')).toBeInTheDocument() - }) - - it('should maintain structure when rerendered', () => { - const { rerender } = render() - - rerender() - - expect(screen.getByText('Test Status')).toBeInTheDocument() - }) - }) -}) diff --git a/web/app/components/datasets/documents/detail/completed/components/__tests__/menu-bar.spec.tsx b/web/app/components/datasets/documents/detail/completed/components/__tests__/menu-bar.spec.tsx index 9ca5601e6a..25ecded349 100644 --- a/web/app/components/datasets/documents/detail/completed/components/__tests__/menu-bar.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/components/__tests__/menu-bar.spec.tsx @@ -11,10 +11,6 @@ vi.mock('../../display-toggle', () => ({ ), })) -vi.mock('../../status-item', () => ({ - default: ({ item }: { item: { name: string } }) =>
{item.name}
, -})) - describe('MenuBar', () => { const defaultProps = { hasSelectableSegments: true, @@ -87,22 +83,19 @@ describe('MenuBar', () => { expect(defaultProps.onInputChange).toHaveBeenCalledWith('') }) - it('should render select with status items via renderOption', () => { + it('should render the selected status in the trigger', () => { renderMenuBar() expect(screen.getByText('All')).toBeInTheDocument() }) - it('should call renderOption for each item when dropdown is opened', async () => { + it('should render status options when dropdown is opened', async () => { renderMenuBar() const selectButton = screen.getByRole('combobox') fireEvent.click(selectButton) - // After opening, renderOption is called for each item, rendering the mocked StatusItem - const statusItems = await screen.findAllByTestId('status-item') - expect(statusItems.length).toBe(3) - expect(statusItems[0]).toHaveTextContent('All') - expect(statusItems[1]).toHaveTextContent('Enabled') - expect(statusItems[2]).toHaveTextContent('Disabled') + expect(await screen.findByRole('option', { name: 'All' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'Enabled' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'Disabled' })).toBeInTheDocument() }) }) diff --git a/web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx b/web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx index 09a3db925c..8937246406 100644 --- a/web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx +++ b/web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx @@ -1,12 +1,10 @@ 'use client' import { Checkbox } from '@langgenius/dify-ui/checkbox' -import { cn } from '@langgenius/dify-ui/cn' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import Input from '@/app/components/base/input' import DisplayToggle from '../display-toggle' -import StatusItem from '../status-item' import s from '../style.module.css' type Item = { @@ -67,15 +65,14 @@ function MenuBar({ onChangeStatus(nextItem) }} > - + {selectedStatus?.name ?? ''} {statusList.map(item => ( - - {item.name} - - {item.value === selectDefaultValue && } + + {item.name} + ))} diff --git a/web/app/components/datasets/documents/detail/completed/status-item.tsx b/web/app/components/datasets/documents/detail/completed/status-item.tsx deleted file mode 100644 index ebb4b661e6..0000000000 --- a/web/app/components/datasets/documents/detail/completed/status-item.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { FC } from 'react' -import { RiCheckLine } from '@remixicon/react' -import * as React from 'react' - -type StatusOption = { - value: string | number - name: string -} - -type IStatusItemProps = { - item: StatusOption - selected: boolean -} - -const StatusItem: FC = ({ - item, - selected, -}) => { - return ( -
- {item.name} - {selected && } -
- ) -} - -export default React.memo(StatusItem) diff --git a/web/app/components/datasets/documents/detail/completed/style.module.css b/web/app/components/datasets/documents/detail/completed/style.module.css index 3009c9e41a..8cfbc178c0 100644 --- a/web/app/components/datasets/documents/detail/completed/style.module.css +++ b/web/app/components/datasets/documents/detail/completed/style.module.css @@ -33,9 +33,6 @@ background: linear-gradient(to left, white, 90%, transparent); @apply text-primary-600 font-semibold text-xs absolute right-0 hidden h-12 pl-12 items-center; } -.select { - @apply !h-8 !w-[100px] !py-0 !pr-5 !shadow-none; -} .segModalContent { @apply h-96 text-gray-800 text-base break-all overflow-y-scroll; white-space: pre-line; diff --git a/web/app/components/datasets/documents/detail/segment-add/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/segment-add/__tests__/index.spec.tsx index b31641a076..800e9d79d6 100644 --- a/web/app/components/datasets/documents/detail/segment-add/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/segment-add/__tests__/index.spec.tsx @@ -143,6 +143,18 @@ describe('SegmentAdd', () => { expect(mockShowBatchModal).toHaveBeenCalledTimes(1) }) + + it('should show plan upgrade modal instead of batch modal for sandbox users', async () => { + mockPlan = { type: Plan.sandbox } + const mockShowBatchModal = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: /list\.action\.batchAdd/i })) + fireEvent.click(await screen.findByRole('menuitem', { name: /list\.action\.batchAdd/i })) + + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(mockShowBatchModal).not.toHaveBeenCalled() + }) }) // Disabled state (embedding) diff --git a/web/app/components/datasets/documents/detail/segment-add/index.tsx b/web/app/components/datasets/documents/detail/segment-add/index.tsx index 566a14720e..7b171be45b 100644 --- a/web/app/components/datasets/documents/detail/segment-add/index.tsx +++ b/web/app/components/datasets/documents/detail/segment-add/index.tsx @@ -7,7 +7,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { useRef, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' import { Plan } from '@/app/components/billing/type' @@ -30,9 +30,7 @@ export function SegmentAdd({ embedding, }: SegmentAddProps) { const { t } = useTranslation() - const [isBatchMenuOpen, setIsBatchMenuOpen] = useState(false) const [isPlanUpgradeModalOpen, setIsPlanUpgradeModalOpen] = useState(false) - const batchMenuAnchorRef = useRef(null) const { plan, enableBilling } = useProviderContext() const canAddChunks = !enableBilling || plan.type !== Plan.sandbox @@ -40,24 +38,13 @@ export function SegmentAdd({ ? 'text-components-button-secondary-accent-text-disabled' : 'text-components-button-secondary-accent-text' - const handleAddClick = () => { + const openSegmentDialog = (openDialog: () => void) => { if (!canAddChunks) { setIsPlanUpgradeModalOpen(true) return } - showNewSegmentModal() - } - - const handleBatchAddClick = () => { - setIsBatchMenuOpen(false) - - if (!canAddChunks) { - setIsPlanUpgradeModalOpen(true) - return - } - - showBatchModal() + openDialog() } if (importStatus) { @@ -115,7 +102,6 @@ export function SegmentAdd({ return (
openSegmentDialog(showNewSegmentModal)} disabled={embedding} > @@ -133,29 +119,25 @@ export function SegmentAdd({ {t('list.action.addButton', { ns: 'datasetDocuments' })} - + -
- -
+
openSegmentDialog(showBatchModal)} > {t('list.action.batchAdd', { ns: 'datasetDocuments' })} diff --git a/web/app/components/datasets/documents/style.module.css b/web/app/components/datasets/documents/style.module.css index dccc964942..ef11b7bb45 100644 --- a/web/app/components/datasets/documents/style.module.css +++ b/web/app/components/datasets/documents/style.module.css @@ -17,7 +17,7 @@ @apply !p-2 !border-[0.5px] !border-components-button-secondary-border !bg-components-button-secondary-bg shadow-xs shadow-shadow-shadow-3 hover:!border-components-button-secondary-border-hover hover:!bg-components-button-secondary-bg-hover; } .actionItem { - @apply h-9 py-2 px-3 mx-1 flex items-center gap-2 rounded-lg border-none bg-transparent text-left hover:bg-state-base-hover cursor-pointer; + @apply h-9 w-[calc(100%-8px)] py-2 px-3 mx-1 flex items-center gap-2 rounded-lg border-none bg-transparent text-left hover:bg-state-base-hover cursor-pointer; } .deleteActionItem { @apply hover:!bg-state-destructive-hover; diff --git a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx index fc32d0b8af..481819599e 100644 --- a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx +++ b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx @@ -42,7 +42,7 @@ const OperationsDropdown = ({ 'border-components-actionbar-border bg-components-button-secondary-bg p-0 shadow-lg ring-2 shadow-shadow-shadow-5 ring-components-button-secondary-bg ring-inset', 'transition-colors hover:border-components-actionbar-border hover:bg-state-base-hover', 'focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden focus-visible:ring-inset', - open && 'bg-state-base-hover', + 'data-popup-open:bg-state-base-hover', )} aria-label="Dataset operations" > diff --git a/web/app/components/datasets/settings/permission-selector/index.tsx b/web/app/components/datasets/settings/permission-selector/index.tsx index d4611a8eda..067bf68da4 100644 --- a/web/app/components/datasets/settings/permission-selector/index.tsx +++ b/web/app/components/datasets/settings/permission-selector/index.tsx @@ -101,7 +101,7 @@ const PermissionSelector = ({
+
{ isOnlyMe && ( <> @@ -169,8 +169,7 @@ const PermissionSelector = ({ } diff --git a/web/app/components/explore/item-operation/__tests__/index.spec.tsx b/web/app/components/explore/item-operation/__tests__/index.spec.tsx index 268fe8ebea..f0c35e93f0 100644 --- a/web/app/components/explore/item-operation/__tests__/index.spec.tsx +++ b/web/app/components/explore/item-operation/__tests__/index.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' import ItemOperation from '../index' @@ -13,11 +13,21 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', () => { } return { - DropdownMenu: ({ children, open, onOpenChange }: { children: React.ReactNode, open: boolean, onOpenChange?: (open: boolean) => void }) => ( - -
{children}
-
- ), + DropdownMenu: ({ + children, + modal, + }: { + children: React.ReactNode + modal?: boolean + }) => { + const [isOpen, setIsOpen] = React.useState(false) + + return ( + +
{children}
+
+ ) + }, DropdownMenuTrigger: ({ children, onClick, @@ -158,37 +168,41 @@ describe('ItemOperation', () => { }) describe('Edge Cases', () => { - it('should close the menu when mouse leaves the panel and item is not hovering', async () => { - renderComponent() + it('should keep the menu open when item hover leaves', async () => { + const { props, rerender } = renderComponent({ isItemHovering: true }) fireEvent.click(screen.getByTestId('item-operation-trigger')) await screen.findByText('explore.sidebar.action.pin') - const menu = screen.getByTestId('dropdown-content') - fireEvent.mouseEnter(menu) - fireEvent.mouseLeave(menu) + rerender() - await waitFor(() => { - expect(screen.queryByText('explore.sidebar.action.pin')).not.toBeInTheDocument() - }) + expect(screen.getByText('explore.sidebar.action.pin')).toBeInTheDocument() }) - it('should stop propagation when clicking inside the dropdown content', async () => { + it('should render a non-modal menu', () => { + renderComponent() + + expect(screen.getByTestId('dropdown-menu')).toHaveAttribute('data-modal', 'false') + }) + + it('should stop propagation when clicking menu actions', async () => { const onParentClick = vi.fn() + const togglePin = vi.fn() render(
, ) fireEvent.click(screen.getByTestId('item-operation-trigger')) - fireEvent.click(await screen.findByTestId('dropdown-content')) + fireEvent.click(await screen.findByText('explore.sidebar.action.pin')) + expect(togglePin).toHaveBeenCalledTimes(1) expect(onParentClick).not.toHaveBeenCalled() }) }) diff --git a/web/app/components/explore/item-operation/index.tsx b/web/app/components/explore/item-operation/index.tsx index d7942170e2..11a740a131 100644 --- a/web/app/components/explore/item-operation/index.tsx +++ b/web/app/components/explore/item-operation/index.tsx @@ -7,13 +7,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { - RiDeleteBinLine, - RiEditLine, -} from '@remixicon/react' -import { useBoolean } from 'ahooks' import * as React from 'react' -import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Pin02 } from '../../base/icons/src/vender/line/general' import s from './style.module.css' @@ -41,20 +35,17 @@ const ItemOperation: FC = ({ }) => { const { t } = useTranslation('explore') const { t: tCommon } = useTranslation('common') - const [open, setOpen] = useState(false) - const [isHovering, { setTrue: setIsHovering, setFalse: setNotHovering }] = useBoolean(false) - useEffect(() => { - if (!isItemHovering && !isHovering) - setOpen(false) - }, [isItemHovering, isHovering]) + return ( - + { e.stopPropagation() }} @@ -65,11 +56,6 @@ const ItemOperation: FC = ({ placement="bottom-end" sideOffset={4} popupClassName="min-w-[120px]" - popupProps={{ - onMouseEnter: setIsHovering, - onMouseLeave: setNotHovering, - onClick: e => e.stopPropagation(), - }} > = ({ onRenameConversation?.() }} > - + {t('sidebar.action.rename')} )} @@ -101,7 +87,7 @@ const ItemOperation: FC = ({ onDelete() }} > - + {t('sidebar.action.delete')} )} diff --git a/web/app/components/explore/item-operation/style.module.css b/web/app/components/explore/item-operation/style.module.css index 62b4e28088..c84ab8e4d6 100644 --- a/web/app/components/explore/item-operation/style.module.css +++ b/web/app/components/explore/item-operation/style.module.css @@ -20,6 +20,7 @@ mask-image: url(~@/assets/action.svg); } +body .btn[data-popup-open], body .btn.open, body .btn:hover { background: url(~@/assets/action.svg) center center no-repeat transparent; diff --git a/web/app/components/header/account-dropdown/__tests__/support.spec.tsx b/web/app/components/header/account-dropdown/__tests__/support.spec.tsx index 38be78abf5..9b78e66683 100644 --- a/web/app/components/header/account-dropdown/__tests__/support.spec.tsx +++ b/web/app/components/header/account-dropdown/__tests__/support.spec.tsx @@ -42,8 +42,6 @@ vi.mock('@/config', async (importOriginal) => { }) describe('Support', () => { - const mockCloseAccountDropdown = vi.fn() - const baseAppContextValue: AppContextValue = { userProfile: { id: '1', @@ -105,7 +103,7 @@ describe('Support', () => { { }}> open - + , ) @@ -189,7 +187,7 @@ describe('Support', () => { }) describe('Interactions and Links', () => { - it('should call toggleZendeskWindow and closeAccountDropdown when "Contact Us" is clicked', () => { + it('should call toggleZendeskWindow when "Contact Us" is clicked', () => { // Act renderSupport() fireEvent.click(screen.getByText('common.userProfile.support')) @@ -197,7 +195,6 @@ describe('Support', () => { // Assert expect(window.zE).toHaveBeenCalledWith('messenger', 'open') - expect(mockCloseAccountDropdown).toHaveBeenCalled() }) it('should have correct forum and community links', () => { diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index cc1bd06641..3744db6e81 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -2,7 +2,6 @@ import type { MouseEventHandler, ReactNode } from 'react' import { Avatar } from '@langgenius/dify-ui/avatar' -import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu' import { useSuspenseQuery } from '@tanstack/react-query' import { useState } from 'react' @@ -110,7 +109,6 @@ function AccountMenuSection({ children }: AccountMenuSectionProps) { export default function AppSelector() { const router = useRouter() const [aboutVisible, setAboutVisible] = useState(false) - const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false) const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { t } = useTranslation() @@ -136,10 +134,10 @@ export default function AppSelector() { return (
- + @@ -185,7 +183,7 @@ export default function AppSelector() { label={t('userProfile.helpCenter', { ns: 'common' })} trailing={} /> - setIsAccountMenuOpen(false)} /> + {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && } @@ -214,7 +212,6 @@ export default function AppSelector() { label={t('userProfile.about', { ns: 'common' })} onClick={() => { setAboutVisible(true) - setIsAccountMenuOpen(false) }} trailing={(
diff --git a/web/app/components/header/account-dropdown/support.tsx b/web/app/components/header/account-dropdown/support.tsx index 9a7023c415..8f7dba2985 100644 --- a/web/app/components/header/account-dropdown/support.tsx +++ b/web/app/components/header/account-dropdown/support.tsx @@ -8,12 +8,8 @@ import { useProviderContext } from '@/context/provider-context' import { mailToSupport } from '../utils/util' import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content' -type SupportProps = { - closeAccountDropdown: () => void -} - // Submenu-only: this component must be rendered within an existing DropdownMenu root. -export default function Support({ closeAccountDropdown }: SupportProps) { +export default function Support() { const { t } = useTranslation() const { plan } = useProviderContext() const { userProfile, langGeniusVersionInfo } = useAppContext() @@ -37,7 +33,6 @@ export default function Support({ closeAccountDropdown }: SupportProps) { className="justify-between" onClick={() => { toggleZendeskWindow(true) - closeAccountDropdown() }} > { const { t } = useTranslation() - const [open, setOpen] = useState(false) const { type, } = credentialItem const handleAction = useCallback((action: string) => { - setOpen(false) queueMicrotask(() => { if (action === 'rename') { onRename?.() @@ -45,12 +41,12 @@ const Operator = ({ }, [credentialItem, onAction, onRename]) return ( - + diff --git a/web/app/components/header/account-setting/members-page/operation/index.tsx b/web/app/components/header/account-setting/members-page/operation/index.tsx index 19d6dfbaa6..fe72f972d1 100644 --- a/web/app/components/header/account-setting/members-page/operation/index.tsx +++ b/web/app/components/header/account-setting/members-page/operation/index.tsx @@ -1,6 +1,5 @@ 'use client' import type { Member } from '@/models/common' -import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, @@ -9,7 +8,7 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { toast } from '@langgenius/dify-ui/toast' -import { memo, useMemo, useState } from 'react' +import { memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useProviderContext } from '@/context/provider-context' import { deleteMemberOrCancelInvitation, updateMemberRole } from '@/service/common' @@ -30,7 +29,6 @@ const nonOwnerRoles = ['admin', 'editor', 'normal'] as const const isNonOwnerRole = (role: Member['role']) => role !== 'owner' const Operation = ({ member, operatorRole, onOperate }: IOperationProps) => { - const [open, setOpen] = useState(false) const { t } = useTranslation() const { datasetOperatorEnabled } = useProviderContext() const RoleMap = { @@ -59,7 +57,6 @@ const Operation = ({ member, operatorRole, onOperate }: IOperationProps) => { }, [operatorRole, datasetOperatorEnabled]) const canRemoveMember = operatorRole === 'owner' || (operatorRole === 'admin' && isNonOwnerRole(member.role)) const handleDeleteMemberOrCancelInvitation = async () => { - setOpen(false) try { await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` }) onOperate() @@ -69,7 +66,6 @@ const Operation = ({ member, operatorRole, onOperate }: IOperationProps) => { } } const handleUpdateMemberRole = async (role: string) => { - setOpen(false) try { await updateMemberRole({ url: `/workspaces/current/members/${member.id}/update-role`, @@ -82,12 +78,12 @@ const Operation = ({ member, operatorRole, onOperate }: IOperationProps) => { } } return ( - + } + className="group flex size-full cursor-pointer items-center justify-between border-none bg-transparent px-3 text-left system-sm-regular text-text-secondary hover:bg-state-base-hover data-popup-open:bg-state-base-hover" > {RoleMap[member.role] || RoleMap.normal} - + { const trigger = screen.getByTestId('member-selector-trigger') await user.click(trigger) - expect(trigger).toHaveClass('bg-state-base-hover-alt') + expect(trigger).toHaveAttribute('data-popup-open') + expect(trigger).toHaveClass('data-popup-open:bg-state-base-hover-alt') }) it('should not match account when neither name nor email contains search value', async () => { diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx index 2e6ab10e26..8c60f8244b 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import { Avatar } from '@langgenius/dify-ui/avatar' -import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, @@ -56,7 +55,7 @@ const MemberSelector: FC = ({ render={(
{!currentValue && (
{t('members.transferModal.transferPlaceholder', { ns: 'common' })}
@@ -68,7 +67,7 @@ const MemberSelector: FC = ({
{currentValue.email}
)} -
+
)} /> diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx index df6902a5a4..20c2ac3a5b 100644 --- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx +++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx @@ -39,16 +39,15 @@ const OperationDropdown: FC = ({ popupClassName, }) => { const { t } = useTranslation() - const [open, setOpen] = React.useState(false) const { data: enable_marketplace } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: s => s.enable_marketplace, }) return ( - + diff --git a/web/app/components/plugins/plugin-page/__tests__/install-plugin-dropdown.spec.tsx b/web/app/components/plugins/plugin-page/__tests__/install-plugin-dropdown.spec.tsx index bc39cfdbd4..49ac901af1 100644 --- a/web/app/components/plugins/plugin-page/__tests__/install-plugin-dropdown.spec.tsx +++ b/web/app/components/plugins/plugin-page/__tests__/install-plugin-dropdown.spec.tsx @@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import InstallPluginDropdown from '../install-plugin-dropdown' -let portalOpen = false const { mockSystemFeatures, } = vi.hoisted(() => ({ @@ -63,15 +62,22 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => { modal, children, }: { - open: boolean + open?: boolean onOpenChange?: (open: boolean) => void modal?: boolean children: React.ReactNode }) => { - portalOpen = open + const [internalOpen, setInternalOpen] = React.useState(open ?? false) + const isOpen = open ?? internalOpen + const setOpen = (nextOpen: boolean) => { + if (open === undefined) + setInternalOpen(nextOpen) + onOpenChange?.(nextOpen) + } + return ( - -
{children}
+ +
{children}
) }, @@ -99,7 +105,10 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => { children, }: { children: React.ReactNode - }) => portalOpen ?
{children}
: null, + }) => { + const { isOpen } = useDropdownMenuContext() + return isOpen ?
{children}
: null + }, DropdownMenuItem: ({ children, onClick, @@ -150,7 +159,6 @@ vi.mock('@/app/components/plugins/install-plugin/install-from-local-package', () describe('InstallPluginDropdown', () => { beforeEach(() => { vi.clearAllMocks() - portalOpen = false mockSystemFeatures.enable_marketplace = true mockSystemFeatures.plugin_installation_permission.restrict_to_marketplace_only = false }) diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index abcd1bd2b8..3582d127f2 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -1,7 +1,6 @@ 'use client' import { Button } from '@langgenius/dify-ui/button' -import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, @@ -11,7 +10,7 @@ import { import { RiAddLine, RiArrowDownSLine } from '@remixicon/react' import { useSuspenseQuery } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' -import { useEffect, useRef, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { FileZip } from '@/app/components/base/icons/src/vender/solid/files' import { Github } from '@/app/components/base/icons/src/vender/solid/general' @@ -36,7 +35,6 @@ const InstallPluginDropdown = ({ }: Props) => { const { t } = useTranslation() const fileInputRef = useRef(null) - const [isMenuOpen, setIsMenuOpen] = useState(false) const [selectedAction, setSelectedAction] = useState(null) const [selectedFile, setSelectedFile] = useState(null) const { data: enable_marketplace } = useSuspenseQuery({ @@ -54,7 +52,6 @@ const InstallPluginDropdown = ({ if (file) { setSelectedFile(file) setSelectedAction('local') - setIsMenuOpen(false) } } @@ -78,20 +75,17 @@ const InstallPluginDropdown = ({ // console.log(res) // } - const [installMethods, setInstallMethods] = useState([]) - useEffect(() => { - const methods = [] + const installMethods = useMemo(() => { + const methods: InstallMethod[] = [] if (enable_marketplace) methods.push({ icon: MagicBox, text: t('source.marketplace', { ns: 'plugin' }), action: 'marketplace' }) - if (plugin_installation_permission.restrict_to_marketplace_only) { - setInstallMethods(methods) - } - else { - methods.push({ icon: Github, text: t('source.github', { ns: 'plugin' }), action: 'github' }) - methods.push({ icon: FileZip, text: t('source.local', { ns: 'plugin' }), action: 'local' }) - setInstallMethods(methods) - } + if (plugin_installation_permission.restrict_to_marketplace_only) + return methods + + methods.push({ icon: Github, text: t('source.github', { ns: 'plugin' }), action: 'github' }) + methods.push({ icon: FileZip, text: t('source.local', { ns: 'plugin' }), action: 'local' }) + return methods }, [plugin_installation_permission, enable_marketplace, t]) const handleInstallMethodSelect = (action: string) => { @@ -111,7 +105,7 @@ const InstallPluginDropdown = ({ } return ( - +
)} > diff --git a/web/app/components/share/text-generation/__tests__/menu-dropdown.spec.tsx b/web/app/components/share/text-generation/__tests__/menu-dropdown.spec.tsx index edad871855..7c8a2c8c95 100644 --- a/web/app/components/share/text-generation/__tests__/menu-dropdown.spec.tsx +++ b/web/app/components/share/text-generation/__tests__/menu-dropdown.spec.tsx @@ -233,25 +233,6 @@ describe('MenuDropdown', () => { }) }) - describe('forceClose prop', () => { - it('should close dropdown when forceClose changes to true', async () => { - const { rerender } = render() - - const triggerButton = screen.getByRole('button') - fireEvent.click(triggerButton) - - await waitFor(() => { - expect(screen.getByText('common.theme.theme')).toBeInTheDocument() - }) - - rerender() - - await waitFor(() => { - expect(screen.queryByText('common.theme.theme')).not.toBeInTheDocument() - }) - }) - }) - describe('placement prop', () => { it('should accept custom placement', () => { render() diff --git a/web/app/components/share/text-generation/menu-dropdown.tsx b/web/app/components/share/text-generation/menu-dropdown.tsx index 004648909f..89983552f7 100644 --- a/web/app/components/share/text-generation/menu-dropdown.tsx +++ b/web/app/components/share/text-generation/menu-dropdown.tsx @@ -2,7 +2,6 @@ import type { Placement } from '@langgenius/dify-ui/dropdown-menu' import type { FC } from 'react' import type { SiteInfo } from '@/models/share' -import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, @@ -12,7 +11,7 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import * as React from 'react' -import { useCallback, useEffect, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import ThemeSwitcher from '@/app/components/base/theme-switcher' @@ -26,50 +25,37 @@ type Props = { data?: SiteInfo placement?: Placement hideLogout?: boolean - forceClose?: boolean } const MenuDropdown: FC = ({ data, placement, hideLogout, - forceClose, }) => { const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) const router = useRouter() const pathname = usePathname() const { t } = useTranslation() - const [open, setOpen] = useState(false) const shareCode = useWebAppStore(s => s.shareCode) - const handleLogout = useCallback(async () => { - setOpen(false) + const handleLogout = async () => { await webAppLogout(shareCode!) router.replace(`/webapp-signin?redirect_url=${pathname}`) - }, [pathname, router, setOpen, shareCode]) + } const [show, setShow] = useState(false) - const handleOpenInfoModal = useCallback(() => { - setOpen(false) + const handleOpenInfoModal = () => { queueMicrotask(() => { setShow(true) }) - }, []) - - useEffect(() => { - if (forceClose) - setOpen(false) - }, [forceClose, setOpen]) + } return ( <> - + + )} diff --git a/web/app/components/tools/mcp/detail/__tests__/operation-dropdown.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/operation-dropdown.spec.tsx index bb41d4f25a..e558ffbdac 100644 --- a/web/app/components/tools/mcp/detail/__tests__/operation-dropdown.spec.tsx +++ b/web/app/components/tools/mcp/detail/__tests__/operation-dropdown.spec.tsx @@ -14,11 +14,21 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => { } return { - DropdownMenu: ({ children, open, onOpenChange }: { children: React.ReactNode, open: boolean, onOpenChange?: (open: boolean) => void }) => ( - -
{children}
-
- ), + DropdownMenu: ({ children, open, onOpenChange }: { children: React.ReactNode, open?: boolean, onOpenChange?: (open: boolean) => void }) => { + const [internalOpen, setInternalOpen] = React.useState(open ?? false) + const isOpen = open ?? internalOpen + const setOpen = (nextOpen: boolean) => { + if (open === undefined) + setInternalOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + +
{children}
+
+ ) + }, DropdownMenuTrigger: ({ children, render, diff --git a/web/app/components/tools/mcp/detail/operation-dropdown.tsx b/web/app/components/tools/mcp/detail/operation-dropdown.tsx index f17391a2ab..0c21e280a1 100644 --- a/web/app/components/tools/mcp/detail/operation-dropdown.tsx +++ b/web/app/components/tools/mcp/detail/operation-dropdown.tsx @@ -13,7 +13,6 @@ import { RiMoreFill, } from '@remixicon/react' import * as React from 'react' -import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' @@ -31,16 +30,11 @@ const OperationDropdown: FC = ({ onRemove, }) => { const { t } = useTranslation() - const [open, setOpen] = useState(false) - const handleOpenChange = useCallback((nextOpen: boolean) => { - setOpen(nextOpen) - onOpenChange?.(nextOpen) - }, [onOpenChange]) return ( - + } + render={} > diff --git a/web/app/components/workflow/block-selector/market-place-plugin/__tests__/action.spec.tsx b/web/app/components/workflow/block-selector/market-place-plugin/__tests__/action.spec.tsx index 1d845dd5fc..1b0d890e53 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/__tests__/action.spec.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/__tests__/action.spec.tsx @@ -3,11 +3,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { useDownloadPlugin } from '@/service/use-plugins' import OperationDropdown from '../action' const mockDownloadBlob = vi.fn() -const mockRemoveQueries = vi.fn() +const mockDownloadPlugin = vi.fn() vi.mock('next-themes', () => ({ useTheme: () => ({ @@ -15,8 +14,15 @@ vi.mock('next-themes', () => ({ }), })) -vi.mock('@/service/use-plugins', () => ({ - useDownloadPlugin: vi.fn(), +vi.mock('@/service/client', () => ({ + marketplaceQuery: { + downloadPlugin: { + mutationOptions: (options = {}) => ({ + mutationFn: (input: unknown) => mockDownloadPlugin(input), + ...options, + }), + }, + }, })) vi.mock('@/utils/download', () => ({ @@ -37,9 +43,6 @@ const createQueryClient = () => new QueryClient({ const renderComponent = (props?: Partial>) => { const queryClient = createQueryClient() - vi.spyOn(queryClient, 'removeQueries').mockImplementation(((...args) => { - return mockRemoveQueries(...args) - }) as typeof queryClient.removeQueries) return render( @@ -58,10 +61,7 @@ const renderComponent = (props?: Partial { beforeEach(() => { vi.clearAllMocks() - vi.mocked(useDownloadPlugin).mockImplementation((_, enabled) => ({ - data: enabled ? null : null, - isLoading: false, - }) as unknown as ReturnType) + mockDownloadPlugin.mockResolvedValue(new Blob(['plugin zip'], { type: 'application/zip' })) }) it('should render download and view details actions when opened', async () => { @@ -78,28 +78,35 @@ describe('OperationDropdown', () => { await userEvent.setup().click(screen.getByText('common.operation.download')) expect(onOpenChange).toHaveBeenCalledWith(false) - expect(mockRemoveQueries).toHaveBeenCalled() + await waitFor(() => { + expect(mockDownloadPlugin).toHaveBeenCalledWith({ + params: { + organization: 'langgenius', + pluginName: 'test-plugin', + version: '1.0.0', + }, + }) + }) }) - it('should skip download when already loading', async () => { - vi.mocked(useDownloadPlugin).mockReturnValue({ - data: null, - isLoading: true, - } as unknown as ReturnType) + it('should skip duplicate downloads while pending', async () => { + mockDownloadPlugin.mockReturnValue(new Promise(() => {})) renderComponent({ open: true }) - await userEvent.setup().click(screen.getByText('common.operation.download')) + const user = userEvent.setup() + await user.click(screen.getByText('common.operation.download')) - expect(mockRemoveQueries).not.toHaveBeenCalled() + await waitFor(() => { + expect(mockDownloadPlugin).toHaveBeenCalledTimes(1) + }) + + await user.click(screen.getByText('common.operation.download')) + + expect(mockDownloadPlugin).toHaveBeenCalledTimes(1) }) - it('should download the blob when the hook returns data', async () => { - vi.mocked(useDownloadPlugin).mockImplementation((_, enabled) => ({ - data: enabled ? new Blob(['plugin zip'], { type: 'application/zip' }) : null, - isLoading: false, - }) as unknown as ReturnType) - + it('should download the blob when the request returns data', async () => { renderComponent({ open: true }) await userEvent.setup().click(screen.getByText('common.operation.download')) @@ -110,7 +117,6 @@ describe('OperationDropdown', () => { fileName: 'langgenius-test-plugin_1.0.0.zip', }) }) - expect(mockRemoveQueries).toHaveBeenCalled() }) it('should link to the marketplace detail page', () => { diff --git a/web/app/components/workflow/block-selector/market-place-plugin/action.tsx b/web/app/components/workflow/block-selector/market-place-plugin/action.tsx index eacad3de4a..fd0e87c7fc 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/action.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/action.tsx @@ -1,6 +1,5 @@ 'use client' import type { FC } from 'react' -import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, @@ -8,13 +7,12 @@ import { DropdownMenuLinkItem, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { useQueryClient } from '@tanstack/react-query' +import { useMutation } from '@tanstack/react-query' import { useTheme } from 'next-themes' import * as React from 'react' -import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import { useDownloadPlugin } from '@/service/use-plugins' +import { marketplaceQuery } from '@/service/client' import { downloadBlob } from '@/utils/download' import { getMarketplaceUrl } from '@/utils/var' @@ -35,49 +33,36 @@ const OperationDropdown: FC = ({ }) => { const { t } = useTranslation() const { theme } = useTheme() - const queryClient = useQueryClient() - const setOpen = useCallback((value: boolean) => { - onOpenChange(value) - }, [onOpenChange]) - const [needDownload, setNeedDownload] = useState(false) - const downloadInfo = useMemo(() => ({ - organization: author, - pluginName: name, - version, - }), [author, name, version]) - const { data: blob, isLoading } = useDownloadPlugin(downloadInfo, needDownload) - const handleDownload = useCallback(() => { - if (isLoading) - return - setOpen(false) - queryClient.removeQueries({ - queryKey: ['plugins', 'downloadPlugin', downloadInfo], - exact: true, - }) - setNeedDownload(true) - }, [downloadInfo, isLoading, queryClient, setOpen]) + const downloadMutation = useMutation(marketplaceQuery.downloadPlugin.mutationOptions({ + onSuccess: (blob) => { + downloadBlob({ data: blob, fileName: `${author}-${name}_${version}.zip` }) + }, + })) - useEffect(() => { - if (!needDownload || !blob) + const handleDownload = () => { + if (downloadMutation.isPending) return - const fileName = `${author}-${name}_${version}.zip` - downloadBlob({ data: blob, fileName }) - setNeedDownload(false) - queryClient.removeQueries({ - queryKey: ['plugins', 'downloadPlugin', downloadInfo], - exact: true, + + onOpenChange(false) + downloadMutation.mutate({ + params: { + organization: author, + pluginName: name, + version, + }, }) - }, [author, blob, downloadInfo, name, needDownload, queryClient, version]) + } + return ( diff --git a/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx b/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx index 780e01d949..2cdc804d8c 100644 --- a/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx +++ b/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx @@ -54,7 +54,7 @@ const OperationSelector: FC = ({ >
= ({ {selectedItem && isOperationItem(selectedItem) ? t(`nodes.assigner.operations.${selectedItem.name}`, { ns: 'workflow' }) : t('nodes.assigner.operations.title', { ns: 'workflow' })}
- +
= ({ render={( diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx index e1d959bd86..d2dfef2795 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx @@ -1,60 +1,8 @@ import type { Member } from '@/models/common' -import { fireEvent, render, screen } from '@testing-library/react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import MemberSelector from '../member-selector' -vi.mock('@langgenius/dify-ui/popover', async () => { - const React = await import('react') - const PopoverContext = React.createContext({ - open: false, - setOpen: (_open: boolean) => {}, - }) - - const Popover = ({ - children, - open: controlledOpen, - onOpenChange, - }: { - children: import('react').ReactNode - open?: boolean - onOpenChange?: (open: boolean) => void - }) => { - const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) - const isControlled = controlledOpen !== undefined - const open = isControlled ? !!controlledOpen : uncontrolledOpen - const setOpen = (nextOpen: boolean) => { - if (!isControlled) - setUncontrolledOpen(nextOpen) - onOpenChange?.(nextOpen) - } - - return ( - - {children} - - ) - } - - const PopoverTrigger = ({ render }: { render: import('react').ReactNode }) => { - const { open, setOpen } = React.useContext(PopoverContext) - return ( -
setOpen(!open)}> - {render} -
- ) - } - - const PopoverContent = ({ children }: { children: import('react').ReactNode }) => { - const { open } = React.useContext(PopoverContext) - return open ?
{children}
: null - } - - return { - Popover, - PopoverTrigger, - PopoverContent, - } -}) - const mockMemberList = vi.hoisted(() => vi.fn()) vi.mock('../member-list', () => ({ @@ -87,7 +35,9 @@ describe('human-input/delivery-method/recipient/member-selector', () => { vi.clearAllMocks() }) - it('should toggle the member list and forward selection props', () => { + it('should toggle the member list and forward selection props', async () => { + const user = userEvent.setup() + render( { expect(screen.queryByTestId('member-list')).not.toBeInTheDocument() - fireEvent.click(trigger) + await user.click(trigger) expect(screen.getByTestId('member-list')).toBeInTheDocument() - expect(trigger).toHaveClass('bg-state-accent-hover') + expect(trigger).toHaveAttribute('data-popup-open') + expect(trigger).toHaveClass('data-popup-open:bg-state-accent-hover') expect(mockMemberList).toHaveBeenCalledWith(expect.objectContaining({ searchValue: '', list: members, email: 'owner@example.com', })) - fireEvent.click(trigger) + await user.click(trigger) expect(screen.queryByTestId('member-list')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx index ad831bc0ed..1e2de58247 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import type { Recipient } from '@/app/components/workflow/nodes/human-input/types' import type { Member } from '@/models/common' import { Button } from '@langgenius/dify-ui/button' -import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, @@ -45,7 +44,7 @@ const MemberSelector: FC = ({ diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/__tests__/operator.spec.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/__tests__/operator.spec.tsx index a7959d1252..52e6c6cdf1 100644 --- a/web/app/components/workflow/note-node/note-editor/toolbar/__tests__/operator.spec.tsx +++ b/web/app/components/workflow/note-node/note-editor/toolbar/__tests__/operator.spec.tsx @@ -82,8 +82,18 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', async () => { return ( diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx index e5768af574..bb822cea99 100644 --- a/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx +++ b/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx @@ -37,12 +37,10 @@ const Operator = ({ onOpenChange={setOpen} > } aria-label={t('operation.more', { ns: 'common' })} className={cn( 'flex size-8 cursor-pointer items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', - open && 'bg-state-base-hover text-text-secondary', + 'data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary', )} onMouseDown={(event) => { event.preventDefault() diff --git a/web/app/components/workflow/operator/zoom-in-out.tsx b/web/app/components/workflow/operator/zoom-in-out.tsx index b1467508f8..48d790fd18 100644 --- a/web/app/components/workflow/operator/zoom-in-out.tsx +++ b/web/app/components/workflow/operator/zoom-in-out.tsx @@ -1,5 +1,4 @@ import type { FC } from 'react' -import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, @@ -11,7 +10,6 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { Fragment, memo, - useState, } from 'react' import { useTranslation } from 'react-i18next' import { @@ -26,17 +24,19 @@ import { import { ShortcutKbd } from '../shortcuts/shortcut-kbd' import TipPopup from './tip-popup' -enum ZoomType { - zoomToFit = 'zoomToFit', - zoomTo25 = 'zoomTo25', - zoomTo50 = 'zoomTo50', - zoomTo75 = 'zoomTo75', - zoomTo100 = 'zoomTo100', - zoomTo200 = 'zoomTo200', - toggleUserComments = 'toggleUserComments', - toggleUserCursors = 'toggleUserCursors', - toggleMiniMap = 'toggleMiniMap', -} +const ZoomType = { + zoomToFit: 'zoomToFit', + zoomTo25: 'zoomTo25', + zoomTo50: 'zoomTo50', + zoomTo75: 'zoomTo75', + zoomTo100: 'zoomTo100', + zoomTo200: 'zoomTo200', + toggleUserComments: 'toggleUserComments', + toggleUserCursors: 'toggleUserCursors', + toggleMiniMap: 'toggleMiniMap', +} as const + +type ZoomType = typeof ZoomType[keyof typeof ZoomType] type ZoomInOutProps = { showMiniMap?: boolean @@ -66,7 +66,6 @@ const ZoomInOut: FC = ({ } = useReactFlow() const { zoom } = useViewport() const { handleSyncWorkflowDraft } = useNodesSyncDraft() - const [open, setOpen] = useState(false) const { workflowReadOnly, getWorkflowReadOnly, @@ -126,12 +125,10 @@ const ZoomInOut: FC = ({ ], ] - const handleZoom = (type: string) => { + const handleZoom = (type: ZoomType) => { if (workflowReadOnly) return - setOpen(false) - if (type === ZoomType.zoomToFit) fitView() @@ -199,16 +196,10 @@ const ZoomInOut: FC = ({ - + {Number.parseFloat(`${zoom * 100}`).toFixed(0)} % diff --git a/web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx b/web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx index 6c3938b377..c46357d738 100644 --- a/web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx +++ b/web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx @@ -10,7 +10,6 @@ import { import { Fragment, memo, - useState, } from 'react' import { useTranslation } from 'react-i18next' import { @@ -20,14 +19,16 @@ import { import TipPopup from '@/app/components/workflow/operator/tip-popup' import { ShortcutKbd } from '@/app/components/workflow/shortcuts/shortcut-kbd' -enum ZoomType { - zoomToFit = 'zoomToFit', - zoomTo25 = 'zoomTo25', - zoomTo50 = 'zoomTo50', - zoomTo75 = 'zoomTo75', - zoomTo100 = 'zoomTo100', - zoomTo200 = 'zoomTo200', -} +const ZoomType = { + zoomToFit: 'zoomToFit', + zoomTo25: 'zoomTo25', + zoomTo50: 'zoomTo50', + zoomTo75: 'zoomTo75', + zoomTo100: 'zoomTo100', + zoomTo200: 'zoomTo200', +} as const + +type ZoomType = typeof ZoomType[keyof typeof ZoomType] const ZoomInOut: FC = () => { const { t } = useTranslation() @@ -38,7 +39,6 @@ const ZoomInOut: FC = () => { fitView, } = useReactFlow() const { zoom } = useViewport() - const [open, setOpen] = useState(false) const zoomOptions = [ [ @@ -71,9 +71,7 @@ const ZoomInOut: FC = () => { ], ] - const handleZoom = (type: string) => { - setOpen(false) - + const handleZoom = (type: ZoomType) => { if (type === ZoomType.zoomToFit) fitView() @@ -122,11 +120,8 @@ const ZoomInOut: FC = () => { - - + + {Number.parseFloat(`${zoom * 100}`).toFixed(0)} % diff --git a/web/contract/marketplace.ts b/web/contract/marketplace.ts index 9f2475041e..3fdb344161 100644 --- a/web/contract/marketplace.ts +++ b/web/contract/marketplace.ts @@ -67,3 +67,17 @@ export const templateDetailContract = base } }>()) .output(type<{ data: MarketplaceTemplate }>()) + +export const downloadPluginContract = base + .route({ + path: '/plugins/{organization}/{pluginName}/{version}/download', + method: 'GET', + }) + .input(type<{ + params: { + organization: string + pluginName: string + version: string + } + }>()) + .output(type()) diff --git a/web/contract/router.ts b/web/contract/router.ts index 8e198e981d..1987022551 100644 --- a/web/contract/router.ts +++ b/web/contract/router.ts @@ -52,13 +52,14 @@ import { workflowDraftUpdateFeaturesContract, } from './console/workflow' import { workflowCommentContracts } from './console/workflow-comment' -import { collectionPluginsContract, collectionsContract, searchAdvancedContract, templateDetailContract } from './marketplace' +import { collectionPluginsContract, collectionsContract, downloadPluginContract, searchAdvancedContract, templateDetailContract } from './marketplace' export const marketplaceRouterContract = { collections: collectionsContract, collectionPlugins: collectionPluginsContract, searchAdvanced: searchAdvancedContract, templateDetail: templateDetailContract, + downloadPlugin: downloadPluginContract, } export type MarketPlaceInputs = InferContractRouterInputs diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 36397c7f64..1222b4513d 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -569,15 +569,6 @@ export const usePluginManifestInfo = (pluginUID: string) => { }) } -export const useDownloadPlugin = (info: { organization: string, pluginName: string, version: string }, needDownload: boolean) => { - return useQuery({ - queryKey: [NAME_SPACE, 'downloadPlugin', info], - queryFn: () => getMarketplace(`/plugins/${info.organization}/${info.pluginName}/${info.version}/download`), - enabled: needDownload, - retry: 0, - }) -} - export const useMutationCheckDependencies = () => { return useMutation({ mutationFn: (appId: string) => { From 1fe8b7fb1d8c28f67ac86e05ad9de7e8e80e0692 Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Wed, 20 May 2026 00:59:09 -0700 Subject: [PATCH 03/12] fix(auth): use validity-returned token in ChangePasswordForm reset submit (#36415) --- .../ChangePasswordForm.spec.tsx | 86 +++++++++++++++++++ .../forgot-password/ChangePasswordForm.tsx | 6 +- web/service/use-common.ts | 2 +- 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 web/app/forgot-password/ChangePasswordForm.spec.tsx diff --git a/web/app/forgot-password/ChangePasswordForm.spec.tsx b/web/app/forgot-password/ChangePasswordForm.spec.tsx new file mode 100644 index 0000000000..b69ca1bf1e --- /dev/null +++ b/web/app/forgot-password/ChangePasswordForm.spec.tsx @@ -0,0 +1,86 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { changePasswordWithToken } from '@/service/common' +import { useVerifyForgotPasswordToken } from '@/service/use-common' +import ChangePasswordForm from './ChangePasswordForm' + +const mockReplace = vi.fn() +vi.mock('@/next/navigation', () => ({ + useSearchParams: () => new URLSearchParams('token=url-token-t1'), + useRouter: () => ({ replace: mockReplace }), +})) + +vi.mock('@/service/use-common', () => ({ + useVerifyForgotPasswordToken: vi.fn(), +})) + +vi.mock('@/service/common', () => ({ + changePasswordWithToken: vi.fn(), +})) + +vi.mock('@/utils/var', () => ({ basePath: '' })) + +type UseVerifyResult = ReturnType +const mockUseVerify = vi.mocked(useVerifyForgotPasswordToken) +const mockChangePassword = vi.mocked(changePasswordWithToken) + +const VALID_PASSWORD = 'ValidPass123!' + +describe('ChangePasswordForm', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('when token is valid', () => { + const T2 = 'verified-token-t2' + + beforeEach(() => { + mockUseVerify.mockReturnValue({ + data: { result: 'success', is_valid: true, email: 'user@example.com', token: T2 }, + refetch: vi.fn(), + } as unknown as UseVerifyResult) + }) + + it('renders the password form', () => { + render() + expect(screen.getByText('login.changePassword')).toBeInTheDocument() + }) + + it('submits with T2 (from validity response), NOT T1 (from URL)', async () => { + mockChangePassword.mockResolvedValue({ result: 'success' }) + + render() + + const inputs = Array.from(document.querySelectorAll('input[type="password"]')) as [HTMLInputElement, HTMLInputElement] + fireEvent.change(inputs[0], { target: { value: VALID_PASSWORD } }) + fireEvent.change(inputs[1], { target: { value: VALID_PASSWORD } }) + + fireEvent.click(screen.getByRole('button', { name: /common\.operation\.reset/ })) + + await waitFor(() => { + expect(mockChangePassword).toHaveBeenCalledWith({ + url: '/forgot-password/resets', + body: { + token: T2, + new_password: VALID_PASSWORD, + password_confirm: VALID_PASSWORD, + }, + }) + }) + }) + }) + + describe('when token is invalid', () => { + beforeEach(() => { + mockUseVerify.mockReturnValue({ + data: { result: 'success', is_valid: false, email: '', token: '' }, + refetch: vi.fn(), + } as unknown as UseVerifyResult) + }) + + it('shows invalid token state and no form', () => { + render() + expect(screen.getByText('login.invalid')).toBeInTheDocument() + expect(screen.queryByRole('button', { name: /common\.operation\.reset/ })).not.toBeInTheDocument() + }) + }) +}) diff --git a/web/app/forgot-password/ChangePasswordForm.tsx b/web/app/forgot-password/ChangePasswordForm.tsx index deb520c436..72baba2553 100644 --- a/web/app/forgot-password/ChangePasswordForm.tsx +++ b/web/app/forgot-password/ChangePasswordForm.tsx @@ -49,7 +49,7 @@ const ChangePasswordForm = () => { }, [password, confirmPassword, showErrorMessage, t]) const handleChangePassword = useCallback(async () => { - const token = searchParams.get('token') || '' + const resetToken = verifyTokenRes?.token ?? '' if (!valid()) return @@ -57,7 +57,7 @@ const ChangePasswordForm = () => { await changePasswordWithToken({ url: '/forgot-password/resets', body: { - token, + token: resetToken, new_password: password, password_confirm: confirmPassword, }, @@ -67,7 +67,7 @@ const ChangePasswordForm = () => { catch { await revalidateToken() } - }, [confirmPassword, password, revalidateToken, searchParams, valid]) + }, [confirmPassword, password, revalidateToken, verifyTokenRes?.token, valid]) return (
{ }) } -type ForgotPasswordValidity = CommonResponse & { is_valid: boolean, email: string } +type ForgotPasswordValidity = CommonResponse & { is_valid: boolean, email: string, token: string } export const useVerifyForgotPasswordToken = (token?: string | null) => { return useQuery({ queryKey: commonQueryKeys.forgotPasswordValidity(token), From be8627233d9853ff3a513820d548fbbcfa1b088c Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 20 May 2026 16:03:15 +0800 Subject: [PATCH 04/12] ci: show web test shard failures (#36436) --- .github/workflows/web-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 2f3f16a024..29503d7b6b 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -39,7 +39,7 @@ jobs: uses: ./.github/actions/setup-web - name: Run tests - run: vp test run --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage + run: vp test run --reporter=blob --reporter=minimal --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage - name: Upload blob report if: ${{ !cancelled() }} From 848c15a265bf4a32ec51023a735cfeaf56985b74 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 20 May 2026 16:18:26 +0800 Subject: [PATCH 05/12] chore: update to only SaaS can view template (#36440) --- .../app-card/__tests__/index.spec.tsx | 22 ++++++++++++++++++ .../app/create-app-dialog/app-card/index.tsx | 20 ++++++++-------- .../explore/app-card/__tests__/index.spec.tsx | 22 ++++++++++++++++++ web/app/components/explore/app-card/index.tsx | 20 ++++++++-------- .../explore/app-list/__tests__/index.spec.tsx | 23 ++++++++++++++++--- 5 files changed, 84 insertions(+), 23 deletions(-) diff --git a/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx index d1b7dedac3..17194796f4 100644 --- a/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx @@ -17,6 +17,20 @@ vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn(), })) +const mockConfig = vi.hoisted(() => ({ + isCloudEdition: true, +})) + +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + get IS_CLOUD_EDITION() { + return mockConfig.isCloudEdition + }, + } +}) + const mockApp: App = { can_trial: true, app: { @@ -70,6 +84,7 @@ describe('AppCard', () => { } beforeEach(() => { + mockConfig.isCloudEdition = true vi.clearAllMocks() }) @@ -261,6 +276,13 @@ describe('AppCard', () => { app: mockApp, }) }) + + it('should hide try button outside cloud edition', () => { + mockConfig.isCloudEdition = false + renderWithProvider() + + expect(screen.queryByRole('button', { name: /explore\.appCard\.try/ })).not.toBeInTheDocument() + }) }) describe('Keyboard Accessibility', () => { diff --git a/web/app/components/app/create-app-dialog/app-card/index.tsx b/web/app/components/app/create-app-dialog/app-card/index.tsx index 1b022eb961..899306c20a 100644 --- a/web/app/components/app/create-app-dialog/app-card/index.tsx +++ b/web/app/components/app/create-app-dialog/app-card/index.tsx @@ -4,14 +4,13 @@ import { PlusIcon } from '@heroicons/react/20/solid' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { RiInformation2Line } from '@remixicon/react' -import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useContextSelector } from 'use-context-selector' import { trackEvent } from '@/app/components/base/amplitude' import AppIcon from '@/app/components/base/app-icon' +import { IS_CLOUD_EDITION } from '@/config' import AppListContext from '@/context/app-list-context' -import { systemFeaturesQueryOptions } from '@/service/system-features' import { AppTypeIcon, AppTypeLabel } from '../../type-selector' type AppCardProps = { @@ -27,8 +26,7 @@ const AppCard = ({ }: AppCardProps) => { const { t } = useTranslation() const { app: appBasicInfo } = app - const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const isTrialApp = app.can_trial && systemFeatures.enable_trial_app + const canViewApp = IS_CLOUD_EDITION const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel) const handleShowTryAppPanel = useCallback(() => { trackEvent('preview_template', { @@ -69,19 +67,21 @@ const AppCard = ({ {app.description}
- {(canCreate || isTrialApp) && ( + {(canCreate || canViewApp) && (
- {isExplore && (canCreate || isTrialApp) && ( + {isExplore && (canCreate || canViewApp) && (