mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/grouping-branching
This commit is contained in:
commit
39d6383474
|
|
@ -1,4 +1,5 @@
|
|||
import concurrent.futures
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -36,6 +37,8 @@ default_retrieval_model = {
|
|||
"score_threshold_enabled": False,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RetrievalService:
|
||||
# Cache precompiled regular expressions to avoid repeated compilation
|
||||
|
|
@ -106,7 +109,12 @@ class RetrievalService:
|
|||
)
|
||||
)
|
||||
|
||||
concurrent.futures.wait(futures, timeout=3600, return_when=concurrent.futures.ALL_COMPLETED)
|
||||
if futures:
|
||||
for future in concurrent.futures.as_completed(futures, timeout=3600):
|
||||
if exceptions:
|
||||
for f in futures:
|
||||
f.cancel()
|
||||
break
|
||||
|
||||
if exceptions:
|
||||
raise ValueError(";\n".join(exceptions))
|
||||
|
|
@ -210,6 +218,7 @@ class RetrievalService:
|
|||
)
|
||||
all_documents.extend(documents)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
exceptions.append(str(e))
|
||||
|
||||
@classmethod
|
||||
|
|
@ -303,6 +312,7 @@ class RetrievalService:
|
|||
else:
|
||||
all_documents.extend(documents)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
exceptions.append(str(e))
|
||||
|
||||
@classmethod
|
||||
|
|
@ -351,6 +361,7 @@ class RetrievalService:
|
|||
else:
|
||||
all_documents.extend(documents)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
exceptions.append(str(e))
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -663,7 +674,14 @@ class RetrievalService:
|
|||
document_ids_filter=document_ids_filter,
|
||||
)
|
||||
)
|
||||
concurrent.futures.wait(futures, timeout=300, return_when=concurrent.futures.ALL_COMPLETED)
|
||||
# Use as_completed for early error propagation - cancel remaining futures on first error
|
||||
if futures:
|
||||
for future in concurrent.futures.as_completed(futures, timeout=300):
|
||||
if future.exception():
|
||||
# Cancel remaining futures to avoid unnecessary waiting
|
||||
for f in futures:
|
||||
f.cancel()
|
||||
break
|
||||
|
||||
if exceptions:
|
||||
raise ValueError(";\n".join(exceptions))
|
||||
|
|
|
|||
|
|
@ -516,6 +516,9 @@ class DatasetRetrieval:
|
|||
].embedding_model_provider
|
||||
weights["vector_setting"]["embedding_model_name"] = available_datasets[0].embedding_model
|
||||
with measure_time() as timer:
|
||||
cancel_event = threading.Event()
|
||||
thread_exceptions: list[Exception] = []
|
||||
|
||||
if query:
|
||||
query_thread = threading.Thread(
|
||||
target=self._multiple_retrieve_thread,
|
||||
|
|
@ -534,6 +537,8 @@ class DatasetRetrieval:
|
|||
"score_threshold": score_threshold,
|
||||
"query": query,
|
||||
"attachment_id": None,
|
||||
"cancel_event": cancel_event,
|
||||
"thread_exceptions": thread_exceptions,
|
||||
},
|
||||
)
|
||||
all_threads.append(query_thread)
|
||||
|
|
@ -557,12 +562,25 @@ class DatasetRetrieval:
|
|||
"score_threshold": score_threshold,
|
||||
"query": None,
|
||||
"attachment_id": attachment_id,
|
||||
"cancel_event": cancel_event,
|
||||
"thread_exceptions": thread_exceptions,
|
||||
},
|
||||
)
|
||||
all_threads.append(attachment_thread)
|
||||
attachment_thread.start()
|
||||
for thread in all_threads:
|
||||
thread.join()
|
||||
|
||||
# Poll threads with short timeout to detect errors quickly (fail-fast)
|
||||
while any(t.is_alive() for t in all_threads):
|
||||
for thread in all_threads:
|
||||
thread.join(timeout=0.1)
|
||||
if thread_exceptions:
|
||||
cancel_event.set()
|
||||
break
|
||||
if thread_exceptions:
|
||||
break
|
||||
|
||||
if thread_exceptions:
|
||||
raise thread_exceptions[0]
|
||||
self._on_query(query, attachment_ids, dataset_ids, app_id, user_from, user_id)
|
||||
|
||||
if all_documents:
|
||||
|
|
@ -1404,40 +1422,53 @@ class DatasetRetrieval:
|
|||
score_threshold: float,
|
||||
query: str | None,
|
||||
attachment_id: str | None,
|
||||
cancel_event: threading.Event | None = None,
|
||||
thread_exceptions: list[Exception] | None = None,
|
||||
):
|
||||
with flask_app.app_context():
|
||||
threads = []
|
||||
all_documents_item: list[Document] = []
|
||||
index_type = None
|
||||
for dataset in available_datasets:
|
||||
index_type = dataset.indexing_technique
|
||||
document_ids_filter = None
|
||||
if dataset.provider != "external":
|
||||
if metadata_condition and not metadata_filter_document_ids:
|
||||
continue
|
||||
if metadata_filter_document_ids:
|
||||
document_ids = metadata_filter_document_ids.get(dataset.id, [])
|
||||
if document_ids:
|
||||
document_ids_filter = document_ids
|
||||
else:
|
||||
try:
|
||||
with flask_app.app_context():
|
||||
threads = []
|
||||
all_documents_item: list[Document] = []
|
||||
index_type = None
|
||||
for dataset in available_datasets:
|
||||
# Check for cancellation signal
|
||||
if cancel_event and cancel_event.is_set():
|
||||
break
|
||||
index_type = dataset.indexing_technique
|
||||
document_ids_filter = None
|
||||
if dataset.provider != "external":
|
||||
if metadata_condition and not metadata_filter_document_ids:
|
||||
continue
|
||||
retrieval_thread = threading.Thread(
|
||||
target=self._retriever,
|
||||
kwargs={
|
||||
"flask_app": flask_app,
|
||||
"dataset_id": dataset.id,
|
||||
"query": query,
|
||||
"top_k": top_k,
|
||||
"all_documents": all_documents_item,
|
||||
"document_ids_filter": document_ids_filter,
|
||||
"metadata_condition": metadata_condition,
|
||||
"attachment_ids": [attachment_id] if attachment_id else None,
|
||||
},
|
||||
)
|
||||
threads.append(retrieval_thread)
|
||||
retrieval_thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
if metadata_filter_document_ids:
|
||||
document_ids = metadata_filter_document_ids.get(dataset.id, [])
|
||||
if document_ids:
|
||||
document_ids_filter = document_ids
|
||||
else:
|
||||
continue
|
||||
retrieval_thread = threading.Thread(
|
||||
target=self._retriever,
|
||||
kwargs={
|
||||
"flask_app": flask_app,
|
||||
"dataset_id": dataset.id,
|
||||
"query": query,
|
||||
"top_k": top_k,
|
||||
"all_documents": all_documents_item,
|
||||
"document_ids_filter": document_ids_filter,
|
||||
"metadata_condition": metadata_condition,
|
||||
"attachment_ids": [attachment_id] if attachment_id else None,
|
||||
},
|
||||
)
|
||||
threads.append(retrieval_thread)
|
||||
retrieval_thread.start()
|
||||
|
||||
# Poll threads with short timeout to respond quickly to cancellation
|
||||
while any(t.is_alive() for t in threads):
|
||||
for thread in threads:
|
||||
thread.join(timeout=0.1)
|
||||
if cancel_event and cancel_event.is_set():
|
||||
break
|
||||
if cancel_event and cancel_event.is_set():
|
||||
break
|
||||
|
||||
if reranking_enable:
|
||||
# do rerank for searched documents
|
||||
|
|
@ -1470,3 +1501,8 @@ class DatasetRetrieval:
|
|||
all_documents_item = all_documents_item[:top_k] if top_k else all_documents_item
|
||||
if all_documents_item:
|
||||
all_documents.extend(all_documents_item)
|
||||
except Exception as e:
|
||||
if cancel_event:
|
||||
cancel_event.set()
|
||||
if thread_exceptions is not None:
|
||||
thread_exceptions.append(e)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ class WorkflowToolProviderController(ToolProviderController):
|
|||
raise ValueError("app not found")
|
||||
|
||||
user = session.get(Account, db_provider.user_id) if db_provider.user_id else None
|
||||
|
||||
controller = WorkflowToolProviderController(
|
||||
entity=ToolProviderEntity(
|
||||
identity=ToolProviderIdentity(
|
||||
|
|
@ -67,7 +66,7 @@ class WorkflowToolProviderController(ToolProviderController):
|
|||
credentials_schema=[],
|
||||
plugin_id=None,
|
||||
),
|
||||
provider_id="",
|
||||
provider_id=db_provider.id,
|
||||
)
|
||||
|
||||
controller.tools = [
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
|
|
@ -31,6 +32,11 @@ class BillingService:
|
|||
|
||||
compliance_download_rate_limiter = RateLimiter("compliance_download_rate_limiter", 4, 60)
|
||||
|
||||
# Redis key prefix for tenant plan cache
|
||||
_PLAN_CACHE_KEY_PREFIX = "tenant_plan:"
|
||||
# Cache TTL: 10 minutes
|
||||
_PLAN_CACHE_TTL = 600
|
||||
|
||||
@classmethod
|
||||
def get_info(cls, tenant_id: str):
|
||||
params = {"tenant_id": tenant_id}
|
||||
|
|
@ -272,14 +278,110 @@ class BillingService:
|
|||
data = resp.get("data", {})
|
||||
|
||||
for tenant_id, plan in data.items():
|
||||
subscription_plan = subscription_adapter.validate_python(plan)
|
||||
results[tenant_id] = subscription_plan
|
||||
try:
|
||||
subscription_plan = subscription_adapter.validate_python(plan)
|
||||
results[tenant_id] = subscription_plan
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"get_plan_bulk: failed to validate subscription plan for tenant(%s)", tenant_id
|
||||
)
|
||||
continue
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch billing info batch for tenants: %s", chunk)
|
||||
logger.exception("get_plan_bulk: failed to fetch billing info batch for tenants: %s", chunk)
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def _make_plan_cache_key(cls, tenant_id: str) -> str:
|
||||
return f"{cls._PLAN_CACHE_KEY_PREFIX}{tenant_id}"
|
||||
|
||||
@classmethod
|
||||
def get_plan_bulk_with_cache(cls, tenant_ids: Sequence[str]) -> dict[str, SubscriptionPlan]:
|
||||
"""
|
||||
Bulk fetch billing subscription plan with cache to reduce billing API loads in batch job scenarios.
|
||||
|
||||
NOTE: if you want to high data consistency, use get_plan_bulk instead.
|
||||
|
||||
Returns:
|
||||
Mapping of tenant_id -> {plan: str, expiration_date: int}
|
||||
"""
|
||||
tenant_plans: dict[str, SubscriptionPlan] = {}
|
||||
|
||||
if not tenant_ids:
|
||||
return tenant_plans
|
||||
|
||||
subscription_adapter = TypeAdapter(SubscriptionPlan)
|
||||
|
||||
# Step 1: Batch fetch from Redis cache using mget
|
||||
redis_keys = [cls._make_plan_cache_key(tenant_id) for tenant_id in tenant_ids]
|
||||
try:
|
||||
cached_values = redis_client.mget(redis_keys)
|
||||
|
||||
if len(cached_values) != len(tenant_ids):
|
||||
raise Exception(
|
||||
"get_plan_bulk_with_cache: unexpected error: redis mget failed: cached values length mismatch"
|
||||
)
|
||||
|
||||
# Map cached values back to tenant_ids
|
||||
cache_misses: list[str] = []
|
||||
|
||||
for tenant_id, cached_value in zip(tenant_ids, cached_values):
|
||||
if cached_value:
|
||||
try:
|
||||
# Redis returns bytes, decode to string and parse JSON
|
||||
json_str = cached_value.decode("utf-8") if isinstance(cached_value, bytes) else cached_value
|
||||
plan_dict = json.loads(json_str)
|
||||
subscription_plan = subscription_adapter.validate_python(plan_dict)
|
||||
tenant_plans[tenant_id] = subscription_plan
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"get_plan_bulk_with_cache: process tenant(%s) failed, add to cache misses", tenant_id
|
||||
)
|
||||
cache_misses.append(tenant_id)
|
||||
else:
|
||||
cache_misses.append(tenant_id)
|
||||
|
||||
logger.info(
|
||||
"get_plan_bulk_with_cache: cache hits=%s, cache misses=%s",
|
||||
len(tenant_plans),
|
||||
len(cache_misses),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("get_plan_bulk_with_cache: redis mget failed, falling back to API")
|
||||
cache_misses = list(tenant_ids)
|
||||
|
||||
# Step 2: Fetch missing plans from billing API
|
||||
if cache_misses:
|
||||
bulk_plans = BillingService.get_plan_bulk(cache_misses)
|
||||
|
||||
if bulk_plans:
|
||||
plans_to_cache: dict[str, SubscriptionPlan] = {}
|
||||
|
||||
for tenant_id, subscription_plan in bulk_plans.items():
|
||||
tenant_plans[tenant_id] = subscription_plan
|
||||
plans_to_cache[tenant_id] = subscription_plan
|
||||
|
||||
# Step 3: Batch update Redis cache using pipeline
|
||||
if plans_to_cache:
|
||||
try:
|
||||
pipe = redis_client.pipeline()
|
||||
for tenant_id, subscription_plan in plans_to_cache.items():
|
||||
redis_key = cls._make_plan_cache_key(tenant_id)
|
||||
# Serialize dict to JSON string
|
||||
json_str = json.dumps(subscription_plan)
|
||||
pipe.setex(redis_key, cls._PLAN_CACHE_TTL, json_str)
|
||||
pipe.execute()
|
||||
|
||||
logger.info(
|
||||
"get_plan_bulk_with_cache: cached %s new tenant plans to Redis",
|
||||
len(plans_to_cache),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("get_plan_bulk_with_cache: redis pipeline failed")
|
||||
|
||||
return tenant_plans
|
||||
|
||||
@classmethod
|
||||
def get_expired_subscription_cleanup_whitelist(cls) -> Sequence[str]:
|
||||
resp = cls._send_request("GET", "/subscription/cleanup/whitelist")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,365 @@
|
|||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from extensions.ext_redis import redis_client
|
||||
from services.billing_service import BillingService
|
||||
|
||||
|
||||
class TestBillingServiceGetPlanBulkWithCache:
|
||||
"""
|
||||
Comprehensive integration tests for get_plan_bulk_with_cache using testcontainers.
|
||||
|
||||
This test class covers all major scenarios:
|
||||
- Cache hit/miss scenarios
|
||||
- Redis operation failures and fallback behavior
|
||||
- Invalid cache data handling
|
||||
- TTL expiration handling
|
||||
- Error recovery and logging
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_redis_cleanup(self, flask_app_with_containers):
|
||||
"""Clean up Redis cache before and after each test."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Clean up before test
|
||||
yield
|
||||
# Clean up after test
|
||||
# Delete all test cache keys
|
||||
pattern = f"{BillingService._PLAN_CACHE_KEY_PREFIX}*"
|
||||
keys = redis_client.keys(pattern)
|
||||
if keys:
|
||||
redis_client.delete(*keys)
|
||||
|
||||
def _create_test_plan_data(self, plan: str = "sandbox", expiration_date: int = 1735689600):
|
||||
"""Helper to create test SubscriptionPlan data."""
|
||||
return {"plan": plan, "expiration_date": expiration_date}
|
||||
|
||||
def _set_cache(self, tenant_id: str, plan_data: dict, ttl: int = 600):
|
||||
"""Helper to set cache data in Redis."""
|
||||
cache_key = BillingService._make_plan_cache_key(tenant_id)
|
||||
json_str = json.dumps(plan_data)
|
||||
redis_client.setex(cache_key, ttl, json_str)
|
||||
|
||||
def _get_cache(self, tenant_id: str):
|
||||
"""Helper to get cache data from Redis."""
|
||||
cache_key = BillingService._make_plan_cache_key(tenant_id)
|
||||
value = redis_client.get(cache_key)
|
||||
if value:
|
||||
if isinstance(value, bytes):
|
||||
return value.decode("utf-8")
|
||||
return value
|
||||
return None
|
||||
|
||||
def test_get_plan_bulk_with_cache_all_cache_hit(self, flask_app_with_containers):
|
||||
"""Test bulk plan retrieval when all tenants are in cache."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-1", "tenant-2", "tenant-3"]
|
||||
expected_plans = {
|
||||
"tenant-1": self._create_test_plan_data("sandbox", 1735689600),
|
||||
"tenant-2": self._create_test_plan_data("professional", 1767225600),
|
||||
"tenant-3": self._create_test_plan_data("team", 1798761600),
|
||||
}
|
||||
|
||||
# Pre-populate cache
|
||||
for tenant_id, plan_data in expected_plans.items():
|
||||
self._set_cache(tenant_id, plan_data)
|
||||
|
||||
# Act
|
||||
with patch.object(BillingService, "get_plan_bulk") as mock_get_plan_bulk:
|
||||
result = BillingService.get_plan_bulk_with_cache(tenant_ids)
|
||||
|
||||
# Assert
|
||||
assert len(result) == 3
|
||||
assert result["tenant-1"]["plan"] == "sandbox"
|
||||
assert result["tenant-1"]["expiration_date"] == 1735689600
|
||||
assert result["tenant-2"]["plan"] == "professional"
|
||||
assert result["tenant-2"]["expiration_date"] == 1767225600
|
||||
assert result["tenant-3"]["plan"] == "team"
|
||||
assert result["tenant-3"]["expiration_date"] == 1798761600
|
||||
|
||||
# Verify API was not called
|
||||
mock_get_plan_bulk.assert_not_called()
|
||||
|
||||
def test_get_plan_bulk_with_cache_all_cache_miss(self, flask_app_with_containers):
|
||||
"""Test bulk plan retrieval when all tenants are not in cache."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-1", "tenant-2"]
|
||||
expected_plans = {
|
||||
"tenant-1": self._create_test_plan_data("sandbox", 1735689600),
|
||||
"tenant-2": self._create_test_plan_data("professional", 1767225600),
|
||||
}
|
||||
|
||||
# Act
|
||||
with patch.object(BillingService, "get_plan_bulk", return_value=expected_plans) as mock_get_plan_bulk:
|
||||
result = BillingService.get_plan_bulk_with_cache(tenant_ids)
|
||||
|
||||
# Assert
|
||||
assert len(result) == 2
|
||||
assert result["tenant-1"]["plan"] == "sandbox"
|
||||
assert result["tenant-2"]["plan"] == "professional"
|
||||
|
||||
# Verify API was called with correct tenant_ids
|
||||
mock_get_plan_bulk.assert_called_once_with(tenant_ids)
|
||||
|
||||
# Verify data was written to cache
|
||||
cached_1 = self._get_cache("tenant-1")
|
||||
cached_2 = self._get_cache("tenant-2")
|
||||
assert cached_1 is not None
|
||||
assert cached_2 is not None
|
||||
|
||||
# Verify cache content
|
||||
cached_data_1 = json.loads(cached_1)
|
||||
cached_data_2 = json.loads(cached_2)
|
||||
assert cached_data_1 == expected_plans["tenant-1"]
|
||||
assert cached_data_2 == expected_plans["tenant-2"]
|
||||
|
||||
# Verify TTL is set
|
||||
cache_key_1 = BillingService._make_plan_cache_key("tenant-1")
|
||||
ttl_1 = redis_client.ttl(cache_key_1)
|
||||
assert ttl_1 > 0
|
||||
assert ttl_1 <= 600 # Should be <= 600 seconds
|
||||
|
||||
def test_get_plan_bulk_with_cache_partial_cache_hit(self, flask_app_with_containers):
|
||||
"""Test bulk plan retrieval when some tenants are in cache, some are not."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-1", "tenant-2", "tenant-3"]
|
||||
# Pre-populate cache for tenant-1 and tenant-2
|
||||
self._set_cache("tenant-1", self._create_test_plan_data("sandbox", 1735689600))
|
||||
self._set_cache("tenant-2", self._create_test_plan_data("professional", 1767225600))
|
||||
|
||||
# tenant-3 is not in cache
|
||||
missing_plan = {"tenant-3": self._create_test_plan_data("team", 1798761600)}
|
||||
|
||||
# Act
|
||||
with patch.object(BillingService, "get_plan_bulk", return_value=missing_plan) as mock_get_plan_bulk:
|
||||
result = BillingService.get_plan_bulk_with_cache(tenant_ids)
|
||||
|
||||
# Assert
|
||||
assert len(result) == 3
|
||||
assert result["tenant-1"]["plan"] == "sandbox"
|
||||
assert result["tenant-2"]["plan"] == "professional"
|
||||
assert result["tenant-3"]["plan"] == "team"
|
||||
|
||||
# Verify API was called only for missing tenant
|
||||
mock_get_plan_bulk.assert_called_once_with(["tenant-3"])
|
||||
|
||||
# Verify tenant-3 data was written to cache
|
||||
cached_3 = self._get_cache("tenant-3")
|
||||
assert cached_3 is not None
|
||||
cached_data_3 = json.loads(cached_3)
|
||||
assert cached_data_3 == missing_plan["tenant-3"]
|
||||
|
||||
def test_get_plan_bulk_with_cache_redis_mget_failure(self, flask_app_with_containers):
|
||||
"""Test fallback to API when Redis mget fails."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-1", "tenant-2"]
|
||||
expected_plans = {
|
||||
"tenant-1": self._create_test_plan_data("sandbox", 1735689600),
|
||||
"tenant-2": self._create_test_plan_data("professional", 1767225600),
|
||||
}
|
||||
|
||||
# Act
|
||||
with (
|
||||
patch.object(redis_client, "mget", side_effect=Exception("Redis connection error")),
|
||||
patch.object(BillingService, "get_plan_bulk", return_value=expected_plans) as mock_get_plan_bulk,
|
||||
):
|
||||
result = BillingService.get_plan_bulk_with_cache(tenant_ids)
|
||||
|
||||
# Assert
|
||||
assert len(result) == 2
|
||||
assert result["tenant-1"]["plan"] == "sandbox"
|
||||
assert result["tenant-2"]["plan"] == "professional"
|
||||
|
||||
# Verify API was called for all tenants (fallback)
|
||||
mock_get_plan_bulk.assert_called_once_with(tenant_ids)
|
||||
|
||||
# Verify data was written to cache after fallback
|
||||
cached_1 = self._get_cache("tenant-1")
|
||||
cached_2 = self._get_cache("tenant-2")
|
||||
assert cached_1 is not None
|
||||
assert cached_2 is not None
|
||||
|
||||
def test_get_plan_bulk_with_cache_invalid_json_in_cache(self, flask_app_with_containers):
|
||||
"""Test fallback to API when cache contains invalid JSON."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-1", "tenant-2", "tenant-3"]
|
||||
|
||||
# Set valid cache for tenant-1
|
||||
self._set_cache("tenant-1", self._create_test_plan_data("sandbox", 1735689600))
|
||||
|
||||
# Set invalid JSON for tenant-2
|
||||
cache_key_2 = BillingService._make_plan_cache_key("tenant-2")
|
||||
redis_client.setex(cache_key_2, 600, "invalid json {")
|
||||
|
||||
# tenant-3 is not in cache
|
||||
expected_plans = {
|
||||
"tenant-2": self._create_test_plan_data("professional", 1767225600),
|
||||
"tenant-3": self._create_test_plan_data("team", 1798761600),
|
||||
}
|
||||
|
||||
# Act
|
||||
with patch.object(BillingService, "get_plan_bulk", return_value=expected_plans) as mock_get_plan_bulk:
|
||||
result = BillingService.get_plan_bulk_with_cache(tenant_ids)
|
||||
|
||||
# Assert
|
||||
assert len(result) == 3
|
||||
assert result["tenant-1"]["plan"] == "sandbox" # From cache
|
||||
assert result["tenant-2"]["plan"] == "professional" # From API (fallback)
|
||||
assert result["tenant-3"]["plan"] == "team" # From API
|
||||
|
||||
# Verify API was called for tenant-2 and tenant-3
|
||||
mock_get_plan_bulk.assert_called_once_with(["tenant-2", "tenant-3"])
|
||||
|
||||
# Verify tenant-2's invalid JSON was replaced with correct data in cache
|
||||
cached_2 = self._get_cache("tenant-2")
|
||||
assert cached_2 is not None
|
||||
cached_data_2 = json.loads(cached_2)
|
||||
assert cached_data_2 == expected_plans["tenant-2"]
|
||||
assert cached_data_2["plan"] == "professional"
|
||||
assert cached_data_2["expiration_date"] == 1767225600
|
||||
|
||||
# Verify tenant-2 cache has correct TTL
|
||||
cache_key_2_new = BillingService._make_plan_cache_key("tenant-2")
|
||||
ttl_2 = redis_client.ttl(cache_key_2_new)
|
||||
assert ttl_2 > 0
|
||||
assert ttl_2 <= 600
|
||||
|
||||
# Verify tenant-3 data was also written to cache
|
||||
cached_3 = self._get_cache("tenant-3")
|
||||
assert cached_3 is not None
|
||||
cached_data_3 = json.loads(cached_3)
|
||||
assert cached_data_3 == expected_plans["tenant-3"]
|
||||
|
||||
def test_get_plan_bulk_with_cache_invalid_plan_data_in_cache(self, flask_app_with_containers):
|
||||
"""Test fallback to API when cache data doesn't match SubscriptionPlan schema."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-1", "tenant-2", "tenant-3"]
|
||||
|
||||
# Set valid cache for tenant-1
|
||||
self._set_cache("tenant-1", self._create_test_plan_data("sandbox", 1735689600))
|
||||
|
||||
# Set invalid plan data for tenant-2 (missing expiration_date)
|
||||
cache_key_2 = BillingService._make_plan_cache_key("tenant-2")
|
||||
invalid_data = json.dumps({"plan": "professional"}) # Missing expiration_date
|
||||
redis_client.setex(cache_key_2, 600, invalid_data)
|
||||
|
||||
# tenant-3 is not in cache
|
||||
expected_plans = {
|
||||
"tenant-2": self._create_test_plan_data("professional", 1767225600),
|
||||
"tenant-3": self._create_test_plan_data("team", 1798761600),
|
||||
}
|
||||
|
||||
# Act
|
||||
with patch.object(BillingService, "get_plan_bulk", return_value=expected_plans) as mock_get_plan_bulk:
|
||||
result = BillingService.get_plan_bulk_with_cache(tenant_ids)
|
||||
|
||||
# Assert
|
||||
assert len(result) == 3
|
||||
assert result["tenant-1"]["plan"] == "sandbox" # From cache
|
||||
assert result["tenant-2"]["plan"] == "professional" # From API (fallback)
|
||||
assert result["tenant-3"]["plan"] == "team" # From API
|
||||
|
||||
# Verify API was called for tenant-2 and tenant-3
|
||||
mock_get_plan_bulk.assert_called_once_with(["tenant-2", "tenant-3"])
|
||||
|
||||
def test_get_plan_bulk_with_cache_redis_pipeline_failure(self, flask_app_with_containers):
|
||||
"""Test that pipeline failure doesn't affect return value."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-1", "tenant-2"]
|
||||
expected_plans = {
|
||||
"tenant-1": self._create_test_plan_data("sandbox", 1735689600),
|
||||
"tenant-2": self._create_test_plan_data("professional", 1767225600),
|
||||
}
|
||||
|
||||
# Act
|
||||
with (
|
||||
patch.object(BillingService, "get_plan_bulk", return_value=expected_plans),
|
||||
patch.object(redis_client, "pipeline") as mock_pipeline,
|
||||
):
|
||||
# Create a mock pipeline that fails on execute
|
||||
mock_pipe = mock_pipeline.return_value
|
||||
mock_pipe.execute.side_effect = Exception("Pipeline execution failed")
|
||||
|
||||
result = BillingService.get_plan_bulk_with_cache(tenant_ids)
|
||||
|
||||
# Assert - Function should still return correct result despite pipeline failure
|
||||
assert len(result) == 2
|
||||
assert result["tenant-1"]["plan"] == "sandbox"
|
||||
assert result["tenant-2"]["plan"] == "professional"
|
||||
|
||||
# Verify pipeline was attempted
|
||||
mock_pipeline.assert_called_once()
|
||||
|
||||
def test_get_plan_bulk_with_cache_empty_tenant_ids(self, flask_app_with_containers):
|
||||
"""Test with empty tenant_ids list."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Act
|
||||
with patch.object(BillingService, "get_plan_bulk") as mock_get_plan_bulk:
|
||||
result = BillingService.get_plan_bulk_with_cache([])
|
||||
|
||||
# Assert
|
||||
assert result == {}
|
||||
assert len(result) == 0
|
||||
|
||||
# Verify no API calls
|
||||
mock_get_plan_bulk.assert_not_called()
|
||||
|
||||
# Verify no Redis operations (mget with empty list would return empty list)
|
||||
# But we should check that mget was not called at all
|
||||
# Since we can't easily verify this without more mocking, we just verify the result
|
||||
|
||||
def test_get_plan_bulk_with_cache_ttl_expired(self, flask_app_with_containers):
|
||||
"""Test that expired cache keys are treated as cache misses."""
|
||||
with flask_app_with_containers.app_context():
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-1", "tenant-2"]
|
||||
|
||||
# Set cache for tenant-1 with very short TTL (1 second) to simulate expiration
|
||||
self._set_cache("tenant-1", self._create_test_plan_data("sandbox", 1735689600), ttl=1)
|
||||
|
||||
# Wait for TTL to expire (key will be deleted by Redis)
|
||||
import time
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Verify cache is expired (key doesn't exist)
|
||||
cache_key_1 = BillingService._make_plan_cache_key("tenant-1")
|
||||
exists = redis_client.exists(cache_key_1)
|
||||
assert exists == 0 # Key doesn't exist (expired)
|
||||
|
||||
# tenant-2 is not in cache
|
||||
expected_plans = {
|
||||
"tenant-1": self._create_test_plan_data("sandbox", 1735689600),
|
||||
"tenant-2": self._create_test_plan_data("professional", 1767225600),
|
||||
}
|
||||
|
||||
# Act
|
||||
with patch.object(BillingService, "get_plan_bulk", return_value=expected_plans) as mock_get_plan_bulk:
|
||||
result = BillingService.get_plan_bulk_with_cache(tenant_ids)
|
||||
|
||||
# Assert
|
||||
assert len(result) == 2
|
||||
assert result["tenant-1"]["plan"] == "sandbox"
|
||||
assert result["tenant-2"]["plan"] == "professional"
|
||||
|
||||
# Verify API was called for both tenants (tenant-1 expired, tenant-2 missing)
|
||||
mock_get_plan_bulk.assert_called_once_with(tenant_ids)
|
||||
|
||||
# Verify both were written to cache with correct TTL
|
||||
cache_key_1_new = BillingService._make_plan_cache_key("tenant-1")
|
||||
cache_key_2 = BillingService._make_plan_cache_key("tenant-2")
|
||||
ttl_1_new = redis_client.ttl(cache_key_1_new)
|
||||
ttl_2 = redis_client.ttl(cache_key_2)
|
||||
assert ttl_1_new > 0
|
||||
assert ttl_1_new <= 600
|
||||
assert ttl_2 > 0
|
||||
assert ttl_2 <= 600
|
||||
|
|
@ -421,7 +421,18 @@ class TestRetrievalService:
|
|||
# In real code, this waits for all futures to complete
|
||||
# In tests, futures complete immediately, so wait is a no-op
|
||||
with patch("core.rag.datasource.retrieval_service.concurrent.futures.wait"):
|
||||
yield mock_executor
|
||||
# Mock concurrent.futures.as_completed for early error propagation
|
||||
# In real code, this yields futures as they complete
|
||||
# In tests, we yield all futures immediately since they're already done
|
||||
def mock_as_completed(futures_list, timeout=None):
|
||||
"""Mock as_completed that yields futures immediately."""
|
||||
yield from futures_list
|
||||
|
||||
with patch(
|
||||
"core.rag.datasource.retrieval_service.concurrent.futures.as_completed",
|
||||
side_effect=mock_as_completed,
|
||||
):
|
||||
yield mock_executor
|
||||
|
||||
# ==================== Vector Search Tests ====================
|
||||
|
||||
|
|
|
|||
|
|
@ -1294,6 +1294,42 @@ class TestBillingServiceSubscriptionOperations:
|
|||
# Assert
|
||||
assert result == {}
|
||||
|
||||
def test_get_plan_bulk_with_invalid_tenant_plan_skipped(self, mock_send_request):
|
||||
"""Test bulk plan retrieval when one tenant has invalid plan data (should skip that tenant)."""
|
||||
# Arrange
|
||||
tenant_ids = ["tenant-valid-1", "tenant-invalid", "tenant-valid-2"]
|
||||
|
||||
# Response with one invalid tenant plan (missing expiration_date) and two valid ones
|
||||
mock_send_request.return_value = {
|
||||
"data": {
|
||||
"tenant-valid-1": {"plan": "sandbox", "expiration_date": 1735689600},
|
||||
"tenant-invalid": {"plan": "professional"}, # Missing expiration_date field
|
||||
"tenant-valid-2": {"plan": "team", "expiration_date": 1767225600},
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
with patch("services.billing_service.logger") as mock_logger:
|
||||
result = BillingService.get_plan_bulk(tenant_ids)
|
||||
|
||||
# Assert - should only contain valid tenants
|
||||
assert len(result) == 2
|
||||
assert "tenant-valid-1" in result
|
||||
assert "tenant-valid-2" in result
|
||||
assert "tenant-invalid" not in result
|
||||
|
||||
# Verify valid tenants have correct data
|
||||
assert result["tenant-valid-1"]["plan"] == "sandbox"
|
||||
assert result["tenant-valid-1"]["expiration_date"] == 1735689600
|
||||
assert result["tenant-valid-2"]["plan"] == "team"
|
||||
assert result["tenant-valid-2"]["expiration_date"] == 1767225600
|
||||
|
||||
# Verify exception was logged for the invalid tenant
|
||||
mock_logger.exception.assert_called_once()
|
||||
log_call_args = mock_logger.exception.call_args[0]
|
||||
assert "get_plan_bulk: failed to validate subscription plan for tenant" in log_call_args[0]
|
||||
assert "tenant-invalid" in log_call_args[1]
|
||||
|
||||
def test_get_expired_subscription_cleanup_whitelist_success(self, mock_send_request):
|
||||
"""Test successful retrieval of expired subscription cleanup whitelist."""
|
||||
# Arrange
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ vi.mock('i18next', () => ({
|
|||
|
||||
// Mock the useConfig hook
|
||||
vi.mock('@/app/components/workflow/nodes/iteration/use-config', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
inputs: {
|
||||
is_parallel: true,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { noop } from 'es-toolkit/compat'
|
|||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import Picker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ const DatePicker: FC<Props> = ({
|
|||
onStartChange,
|
||||
onEndChange,
|
||||
}) => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
|
||||
const renderDate = useCallback(({ value, handleClickTrigger, isOpen }: TriggerProps) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import dayjs from 'dayjs'
|
|||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { HourglassShape } from '@/app/components/base/icons/src/vender/other'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
import DatePicker from './date-picker'
|
||||
import RangeSelector from './range-selector'
|
||||
|
|
@ -27,7 +27,7 @@ const TimeRangePicker: FC<Props> = ({
|
|||
onSelect,
|
||||
queryDateFormat,
|
||||
}) => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
|
||||
const [isCustomRange, setIsCustomRange] = useState(false)
|
||||
const [start, setStart] = useState<Dayjs>(today)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
|
||||
|
||||
export default function CheckCode() {
|
||||
|
|
@ -19,7 +19,7 @@ export default function CheckCode() {
|
|||
const token = decodeURIComponent(searchParams.get('token') as string)
|
||||
const [code, setVerifyCode] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const verify = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import Link from 'next/link'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ export default function CheckCode() {
|
|||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
|
|
@ -23,7 +23,7 @@ export default function CheckCode() {
|
|||
const token = decodeURIComponent(searchParams.get('token') as string)
|
||||
const [code, setVerifyCode] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
const codeInputRef = useRef<HTMLInputElement>(null)
|
||||
const redirectUrl = searchParams.get('redirect_url')
|
||||
const embeddedUserId = useWebAppStore(s => s.embeddedUserId)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import { noop } from 'es-toolkit/compat'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { sendWebAppEMailLoginCode } from '@/service/common'
|
||||
|
||||
export default function MailAndCodeAuth() {
|
||||
|
|
@ -18,7 +17,7 @@ export default function MailAndCodeAuth() {
|
|||
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
|
||||
const [email, setEmail] = useState(emailFromLink)
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ import Link from 'next/link'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { webAppLogin } from '@/service/common'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
|
|
@ -21,7 +20,7 @@ type MailAndPasswordAuthProps = {
|
|||
|
||||
export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
|
|
|||
|
|
@ -214,7 +214,8 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
<div className="body-md-medium text-text-warning">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey="common.account.changeEmail.content1"
|
||||
i18nKey="account.changeEmail.content1"
|
||||
ns="common"
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
|
|
@ -244,7 +245,8 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey="common.account.changeEmail.content2"
|
||||
i18nKey="account.changeEmail.content2"
|
||||
ns="common"
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
|
|
@ -333,7 +335,8 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey="common.account.changeEmail.content4"
|
||||
i18nKey="account.changeEmail.content4"
|
||||
ns="common"
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
values={{ email: mail }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import Cookies from 'js-cookie'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/constants'
|
||||
import { fetchSetupStatus } from '@/service/common'
|
||||
import { sendGAEvent } from '@/utils/gtag'
|
||||
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
|
||||
import { trackEvent } from './base/amplitude'
|
||||
|
||||
type AppInitializerProps = {
|
||||
children: ReactNode
|
||||
|
|
@ -22,6 +26,10 @@ export const AppInitializer = ({
|
|||
// Tokens are now stored in cookies, no need to check localStorage
|
||||
const pathname = usePathname()
|
||||
const [init, setInit] = useState(false)
|
||||
const [oauthNewUser, setOauthNewUser] = useQueryState(
|
||||
'oauth_new_user',
|
||||
parseAsString.withOptions({ history: 'replace' }),
|
||||
)
|
||||
|
||||
const isSetupFinished = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -45,6 +53,34 @@ export const AppInitializer = ({
|
|||
(async () => {
|
||||
const action = searchParams.get('action')
|
||||
|
||||
if (oauthNewUser === 'true') {
|
||||
let utmInfo = null
|
||||
const utmInfoStr = Cookies.get('utm_info')
|
||||
if (utmInfoStr) {
|
||||
try {
|
||||
utmInfo = JSON.parse(utmInfoStr)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to parse utm_info cookie:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Track registration event with UTM params
|
||||
trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
|
||||
method: 'oauth',
|
||||
...utmInfo,
|
||||
})
|
||||
|
||||
sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
|
||||
method: 'oauth',
|
||||
...utmInfo,
|
||||
})
|
||||
|
||||
// Clean up: remove utm_info cookie and URL params
|
||||
Cookies.remove('utm_info')
|
||||
setOauthNewUser(null)
|
||||
}
|
||||
|
||||
if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
||||
|
||||
|
|
@ -67,7 +103,7 @@ export const AppInitializer = ({
|
|||
router.replace('/signin')
|
||||
}
|
||||
})()
|
||||
}, [isSetupFinished, router, pathname, searchParams])
|
||||
}, [isSetupFinished, router, pathname, searchParams, oauthNewUser, setOauthNewUser])
|
||||
|
||||
return init ? children : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ vi.mock('@/hooks/use-knowledge', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/rename-modal', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
show,
|
||||
onClose,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ vi.mock('next/navigation', () => ({
|
|||
|
||||
// Mock classnames utility
|
||||
vi.mock('@/utils/classnames', () => ({
|
||||
__esModule: true,
|
||||
default: (...classes: any[]) => classes.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ vi.mock('@/context/provider-context', () => ({
|
|||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
notify: vi.fn(args => mockToastNotify(args)),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { Mock } from 'vitest'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import I18nContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import CSVDownload from './csv-downloader'
|
||||
|
||||
|
|
@ -17,17 +18,13 @@ vi.mock('react-papaparse', () => ({
|
|||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => 'en-US'),
|
||||
}))
|
||||
|
||||
const renderWithLocale = (locale: Locale) => {
|
||||
return render(
|
||||
<I18nContext.Provider value={{
|
||||
locale,
|
||||
i18n: {},
|
||||
setLocaleOnClient: vi.fn().mockResolvedValue(undefined),
|
||||
}}
|
||||
>
|
||||
<CSVDownload />
|
||||
</I18nContext.Provider>,
|
||||
)
|
||||
;(useLocale as Mock).mockReturnValue(locale)
|
||||
return render(<CSVDownload />)
|
||||
}
|
||||
|
||||
describe('CSVDownload', () => {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next'
|
|||
import {
|
||||
useCSVDownloader,
|
||||
} from 'react-papaparse'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
|
||||
const CSV_TEMPLATE_QA_EN = [
|
||||
|
|
@ -24,7 +24,7 @@ const CSV_TEMPLATE_QA_CN = [
|
|||
const CSVDownload: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
|
||||
const getTemplate = () => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/ser
|
|||
import BatchModal, { ProcessStatus } from './index'
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
|
|
@ -24,14 +23,12 @@ vi.mock('@/context/provider-context', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('./csv-downloader', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="csv-downloader-stub" />,
|
||||
}))
|
||||
|
||||
let lastUploadedFile: File | undefined
|
||||
|
||||
vi.mock('./csv-uploader', () => ({
|
||||
__esModule: true,
|
||||
default: ({ file, updateFile }: { file?: File, updateFile: (file?: File) => void }) => (
|
||||
<div>
|
||||
<button
|
||||
|
|
@ -49,7 +46,6 @@ vi.mock('./csv-uploader', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/annotation-full', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="annotation-full" />,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ vi.mock('@/context/provider-context', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
formatTime: () => '2023-12-01 10:30:00',
|
||||
}),
|
||||
|
|
@ -35,7 +34,6 @@ vi.mock('@/hooks/use-timestamp', () => ({
|
|||
// Note: i18n is automatically mocked by Vitest via web/vitest.setup.ts
|
||||
|
||||
vi.mock('@/app/components/billing/annotation-full', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="annotation-full" />,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import type { ComponentProps } from 'react'
|
||||
import type { Mock } from 'vitest'
|
||||
import type { AnnotationItemBasic } from '../type'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
||||
import HeaderOptions from './index'
|
||||
|
|
@ -159,16 +160,21 @@ vi.mock('@/context/provider-context', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/annotation-full', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="annotation-full" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => LanguagesSupported[0]),
|
||||
}))
|
||||
|
||||
type HeaderOptionsProps = ComponentProps<typeof HeaderOptions>
|
||||
|
||||
const renderComponent = (
|
||||
props: Partial<HeaderOptionsProps> = {},
|
||||
locale: Locale = LanguagesSupported[0],
|
||||
) => {
|
||||
;(useLocale as Mock).mockReturnValue(locale)
|
||||
|
||||
const defaultProps: HeaderOptionsProps = {
|
||||
appId: 'test-app-id',
|
||||
onAdd: vi.fn(),
|
||||
|
|
@ -177,17 +183,7 @@ const renderComponent = (
|
|||
...props,
|
||||
}
|
||||
|
||||
return render(
|
||||
<I18NContext.Provider
|
||||
value={{
|
||||
locale,
|
||||
i18n: {},
|
||||
setLocaleOnClient: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<HeaderOptions {...defaultProps} />
|
||||
</I18NContext.Provider>,
|
||||
)
|
||||
return render(<HeaderOptions {...defaultProps} />)
|
||||
}
|
||||
|
||||
const openOperationsPopover = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
|
|
@ -440,20 +436,12 @@ describe('HeaderOptions', () => {
|
|||
await waitFor(() => expect(mockedFetchAnnotations).toHaveBeenCalledTimes(1))
|
||||
|
||||
view.rerender(
|
||||
<I18NContext.Provider
|
||||
value={{
|
||||
locale: LanguagesSupported[0],
|
||||
i18n: {},
|
||||
setLocaleOnClient: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<HeaderOptions
|
||||
appId="test-app-id"
|
||||
onAdd={vi.fn()}
|
||||
onAdded={vi.fn()}
|
||||
controlUpdateList={1}
|
||||
/>
|
||||
</I18NContext.Provider>,
|
||||
<HeaderOptions
|
||||
appId="test-app-id"
|
||||
onAdd={vi.fn()}
|
||||
onAdded={vi.fn()}
|
||||
controlUpdateList={1}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(mockedFetchAnnotations).toHaveBeenCalledTimes(2))
|
||||
|
|
|
|||
|
|
@ -13,15 +13,14 @@ import { useTranslation } from 'react-i18next'
|
|||
import {
|
||||
useCSVDownloader,
|
||||
} from 'react-papaparse'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { FileDownload02, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Button from '../../../base/button'
|
||||
import AddAnnotationModal from '../add-annotation-modal'
|
||||
import BatchAddModal from '../batch-add-annotation-modal'
|
||||
|
|
@ -44,7 +43,7 @@ const HeaderOptions: FC<Props> = ({
|
|||
controlUpdateList,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
const [list, setList] = useState<AnnotationItemBasic[]>([])
|
||||
const annotationUnavailable = list.length === 0
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import Annotation from './index'
|
|||
import { JobStatus } from './type'
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import List from './list'
|
|||
const mockFormatTime = vi.fn(() => 'formatted-time')
|
||||
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
formatTime: mockFormatTime,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import ViewAnnotationModal from './index'
|
|||
const mockFormatTime = vi.fn(() => 'formatted-time')
|
||||
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
formatTime: mockFormatTime,
|
||||
}),
|
||||
|
|
@ -24,7 +23,6 @@ vi.mock('../edit-annotation-modal/edit-item', () => {
|
|||
Answer: 'answer',
|
||||
}
|
||||
return {
|
||||
__esModule: true,
|
||||
default: ({ type, content, onSave }: { type: string, content: string, onSave: (value: string) => void }) => (
|
||||
<div>
|
||||
<div data-testid={`content-${type}`}>{content}</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import * as React from 'react'
|
|||
import ConfirmAddVar from './index'
|
||||
|
||||
vi.mock('../../base/var-highlight', () => ({
|
||||
__esModule: true,
|
||||
default: ({ name }: { name: string }) => <span data-testid="var-highlight">{name}</span>,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import * as React from 'react'
|
|||
import EditModal from './edit-modal'
|
||||
|
||||
vi.mock('@/app/components/base/modal', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ vi.mock('@/context/i18n', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/app/configuration/base/operation-btn', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onClick }: { onClick: () => void }) => (
|
||||
<button type="button" data-testid="edit-button" onClick={onClick}>
|
||||
edit
|
||||
|
|
@ -17,7 +16,6 @@ vi.mock('@/app/components/app/configuration/base/operation-btn', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/app/configuration/base/feature-panel', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ const defaultPromptVariables: PromptVariable[] = [
|
|||
let mockSimplePromptInputProps: IPromptProps | null = null
|
||||
|
||||
vi.mock('./simple-prompt-input', () => ({
|
||||
__esModule: true,
|
||||
default: (props: IPromptProps) => {
|
||||
mockSimplePromptInputProps = props
|
||||
return (
|
||||
|
|
@ -67,7 +66,6 @@ type AdvancedMessageInputProps = {
|
|||
}
|
||||
|
||||
vi.mock('./advanced-prompt-input', () => ({
|
||||
__esModule: true,
|
||||
default: (props: AdvancedMessageInputProps) => {
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ vi.mock('react-i18next', () => ({
|
|||
|
||||
let latestAgentSettingProps: any
|
||||
vi.mock('./agent/agent-setting', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
latestAgentSettingProps = props
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ const ToolPickerMock = (props: ToolPickerProps) => (
|
|||
</div>
|
||||
)
|
||||
vi.mock('@/app/components/workflow/block-selector/tool-picker', () => ({
|
||||
__esModule: true,
|
||||
default: (props: ToolPickerProps) => <ToolPickerMock {...props} />,
|
||||
}))
|
||||
|
||||
|
|
@ -96,7 +95,6 @@ const SettingBuiltInToolMock = (props: SettingBuiltInToolProps) => {
|
|||
)
|
||||
}
|
||||
vi.mock('./setting-built-in-tool', () => ({
|
||||
__esModule: true,
|
||||
default: (props: SettingBuiltInToolProps) => <SettingBuiltInToolMock {...props} />,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { render, screen, waitFor } from '@testing-library/react'
|
|||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import SettingBuiltInTool from './setting-built-in-tool'
|
||||
|
||||
const fetchModelToolList = vi.fn()
|
||||
|
|
@ -36,7 +35,6 @@ const FormMock = ({ value, onChange }: MockFormProps) => {
|
|||
)
|
||||
}
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal/Form', () => ({
|
||||
__esModule: true,
|
||||
default: (props: MockFormProps) => <FormMock {...props} />,
|
||||
}))
|
||||
|
||||
|
|
@ -56,6 +54,10 @@ vi.mock('@/app/components/plugins/readme-panel/entrance', () => ({
|
|||
ReadmeEntrance: ({ className }: { className?: string }) => <div className={className}>readme</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => 'en-US'),
|
||||
}))
|
||||
|
||||
const createParameter = (overrides?: Partial<ToolParameter>): ToolParameter => ({
|
||||
name: 'settingParam',
|
||||
label: {
|
||||
|
|
@ -129,18 +131,16 @@ const renderComponent = (props?: Partial<React.ComponentProps<typeof SettingBuil
|
|||
const onSave = vi.fn()
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const utils = render(
|
||||
<I18n.Provider value={{ locale: 'en-US', i18n: {}, setLocaleOnClient: vi.fn() as any }}>
|
||||
<SettingBuiltInTool
|
||||
collection={baseCollection as any}
|
||||
toolName="search"
|
||||
isModel
|
||||
setting={{ settingParam: 'value' }}
|
||||
onHide={onHide}
|
||||
onSave={onSave}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
{...props}
|
||||
/>
|
||||
</I18n.Provider>,
|
||||
<SettingBuiltInTool
|
||||
collection={baseCollection as any}
|
||||
toolName="search"
|
||||
isModel
|
||||
setting={{ settingParam: 'value' }}
|
||||
onHide={onHide}
|
||||
onSave={onSave}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
{...props}
|
||||
/>,
|
||||
)
|
||||
return {
|
||||
...utils,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
|
|
@ -26,7 +25,7 @@ import {
|
|||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWorkflowToolList } from '@/service/tools'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -58,7 +57,7 @@ const SettingBuiltInTool: FC<Props> = ({
|
|||
credentialId,
|
||||
onAuthorizationItemClick,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const { t } = useTranslation()
|
||||
const passedTools = (collection as ToolWithProvider).tools
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ vi.mock('use-context-selector', async (importOriginal) => {
|
|||
|
||||
const mockFormattingDispatcher = vi.fn()
|
||||
vi.mock('../debug/hooks', () => ({
|
||||
__esModule: true,
|
||||
useFormattingChangedDispatcher: () => mockFormattingDispatcher,
|
||||
}))
|
||||
|
||||
let latestConfigPromptProps: any
|
||||
vi.mock('@/app/components/app/configuration/config-prompt', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
latestConfigPromptProps = props
|
||||
return <div data-testid="config-prompt" />
|
||||
|
|
@ -32,7 +30,6 @@ vi.mock('@/app/components/app/configuration/config-prompt', () => ({
|
|||
|
||||
let latestConfigVarProps: any
|
||||
vi.mock('@/app/components/app/configuration/config-var', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
latestConfigVarProps = props
|
||||
return <div data-testid="config-var" />
|
||||
|
|
@ -40,33 +37,27 @@ vi.mock('@/app/components/app/configuration/config-var', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('../dataset-config', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="dataset-config" />,
|
||||
}))
|
||||
|
||||
vi.mock('./agent/agent-tools', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="agent-tools" />,
|
||||
}))
|
||||
|
||||
vi.mock('../config-vision', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="config-vision" />,
|
||||
}))
|
||||
|
||||
vi.mock('./config-document', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="config-document" />,
|
||||
}))
|
||||
|
||||
vi.mock('./config-audio', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="config-audio" />,
|
||||
}))
|
||||
|
||||
let latestHistoryPanelProps: any
|
||||
vi.mock('../config-prompt/conversation-history/history-panel', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
latestHistoryPanelProps = props
|
||||
return <div data-testid="history-panel" />
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { RETRIEVE_METHOD } from '@/types/app'
|
|||
import Item from './index'
|
||||
|
||||
vi.mock('../settings-modal', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onSave, onCancel, currentDataset }: any) => (
|
||||
<div>
|
||||
<div>Mock settings modal</div>
|
||||
|
|
@ -24,7 +23,6 @@ vi.mock('../settings-modal', () => ({
|
|||
vi.mock('@/hooks/use-breakpoints', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/hooks/use-breakpoints')>()
|
||||
return {
|
||||
__esModule: true,
|
||||
...actual,
|
||||
default: vi.fn(() => actual.MediaType.pc),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ vi.mock('uuid', () => ({
|
|||
|
||||
// Mock child components
|
||||
vi.mock('./card-item', () => ({
|
||||
__esModule: true,
|
||||
default: ({ config, onRemove, onSave, editable }: any) => (
|
||||
<div data-testid={`card-item-${config.id}`}>
|
||||
<span>{config.name}</span>
|
||||
|
|
@ -91,7 +90,6 @@ vi.mock('./card-item', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('./params-config', () => ({
|
||||
__esModule: true,
|
||||
default: ({ disabled, selectedDatasets }: any) => (
|
||||
<button data-testid="params-config" disabled={disabled}>
|
||||
Params (
|
||||
|
|
@ -102,7 +100,6 @@ vi.mock('./params-config', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('./context-var', () => ({
|
||||
__esModule: true,
|
||||
default: ({ value, options, onChange }: any) => (
|
||||
<select data-testid="context-var" value={value} onChange={e => onChange(e.target.value)}>
|
||||
<option value="">Select context variable</option>
|
||||
|
|
@ -114,7 +111,6 @@ vi.mock('./context-var', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
metadataList,
|
||||
metadataFilterMode,
|
||||
|
|
@ -198,7 +194,6 @@ const mockConfigContext: any = {
|
|||
}
|
||||
|
||||
vi.mock('@/context/debug-configuration', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children }: any) => (
|
||||
<div data-testid="config-context-provider">
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -30,13 +30,11 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
|
|||
)
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: MockModelSelector,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="model-parameter-modal" />,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -65,13 +65,11 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
|
|||
)
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: MockModelSelector,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="model-parameter-modal" />,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { RETRIEVE_METHOD } from '@/types/app'
|
|||
import SelectDataSet from './index'
|
||||
|
||||
vi.mock('@/i18n-config/i18next-config', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
changeLanguage: vi.fn(),
|
||||
addResourceBundle: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ vi.mock('ky', () => {
|
|||
})
|
||||
|
||||
vi.mock('@/app/components/datasets/create/step-two', () => ({
|
||||
__esModule: true,
|
||||
IndexingType: {
|
||||
QUALIFIED: 'high_quality',
|
||||
ECONOMICAL: 'economy',
|
||||
|
|
@ -45,7 +44,6 @@ vi.mock('@/service/datasets', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/service/use-common', async () => ({
|
||||
__esModule: true,
|
||||
...(await vi.importActual('@/service/use-common')),
|
||||
useMembers: vi.fn(),
|
||||
}))
|
||||
|
|
@ -86,7 +84,6 @@ vi.mock('@/context/provider-context', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
__esModule: true,
|
||||
useModelList: (...args: unknown[]) => mockUseModelList(...args),
|
||||
useModelListAndDefaultModel: (...args: unknown[]) => mockUseModelListAndDefaultModel(...args),
|
||||
useModelListAndDefaultModelAndCurrentProviderAndModel: (...args: unknown[]) =>
|
||||
|
|
@ -95,7 +92,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
|
||||
__esModule: true,
|
||||
default: ({ defaultModel }: { defaultModel?: { provider: string, model: string } }) => (
|
||||
<div data-testid="model-selector">
|
||||
{defaultModel ? `${defaultModel.provider}/${defaultModel.model}` : 'no-model'}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ vi.mock('@/context/provider-context', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
__esModule: true,
|
||||
useModelListAndDefaultModelAndCurrentProviderAndModel: (...args: unknown[]) =>
|
||||
mockUseModelListAndDefaultModelAndCurrentProviderAndModel(...args),
|
||||
useModelListAndDefaultModel: (...args: unknown[]) => mockUseModelListAndDefaultModel(...args),
|
||||
|
|
@ -43,7 +42,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
|
||||
__esModule: true,
|
||||
default: ({ defaultModel }: { defaultModel?: { provider: string, model: string } }) => (
|
||||
<div data-testid="model-selector">
|
||||
{defaultModel ? `${defaultModel.provider}/${defaultModel.model}` : 'no-model'}
|
||||
|
|
@ -52,7 +50,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/create/step-two', () => ({
|
||||
__esModule: true,
|
||||
IndexingType: {
|
||||
QUALIFIED: 'high_quality',
|
||||
ECONOMICAL: 'economy',
|
||||
|
|
|
|||
|
|
@ -52,27 +52,22 @@ const mockFiles: FileEntity[] = [
|
|||
]
|
||||
|
||||
vi.mock('@/context/debug-configuration', () => ({
|
||||
__esModule: true,
|
||||
useDebugConfigurationContext: () => mockUseDebugConfigurationContext(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
__esModule: true,
|
||||
useFeatures: (selector: (state: FeatureStoreState) => unknown) => mockUseFeaturesSelector(selector),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
__esModule: true,
|
||||
useEventEmitterContextContext: () => mockUseEventEmitterContext(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
__esModule: true,
|
||||
useStore: (selector: (state: { setShowAppConfigureFeaturesModal: typeof mockSetShowAppConfigureFeaturesModal }) => unknown) => mockUseAppStoreSelector(selector),
|
||||
}))
|
||||
|
||||
vi.mock('./debug-item', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
modelAndParameter,
|
||||
className,
|
||||
|
|
@ -95,7 +90,6 @@ vi.mock('./debug-item', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/chat/chat-input-area', () => ({
|
||||
__esModule: true,
|
||||
default: (props: MockChatInputAreaProps) => {
|
||||
capturedChatInputProps = props
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -403,7 +403,6 @@ vi.mock('@/app/components/base/toast', () => ({
|
|||
|
||||
// Mock hooks/use-timestamp
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn(() => ({
|
||||
formatTime: vi.fn((timestamp: number) => new Date(timestamp).toLocaleString()),
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ vi.mock('@/app/components/app/store', () => ({
|
|||
useStore: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/app/components/base/features/new-feature-panel/feature-bar', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onFeatureBarClick }: { onFeatureBarClick: () => void }) => (
|
||||
<button type="button" onClick={onFeatureBarClick}>
|
||||
feature bar
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import type {
|
|||
import { noop } from 'es-toolkit/compat'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
|
|
@ -16,7 +15,7 @@ import Modal from '@/app/components/base/modal'
|
|||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import I18n, { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { useCodeBasedExtensions } from '@/service/use-common'
|
||||
|
||||
|
|
@ -41,7 +40,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
|||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useToastContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' })
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const { data: codeBasedExtensionList } = useCodeBasedExtensions('external_data_tool')
|
||||
|
|
|
|||
|
|
@ -28,13 +28,11 @@ vi.mock('@/service/use-explore', () => ({
|
|||
useExploreAppList: () => mockUseExploreAppList(),
|
||||
}))
|
||||
vi.mock('@/app/components/app/type-selector', () => ({
|
||||
__esModule: true,
|
||||
default: ({ value, onChange }: { value: AppModeEnum[], onChange: (value: AppModeEnum[]) => void }) => (
|
||||
<button data-testid="type-selector" onClick={() => onChange([...value, 'chat' as AppModeEnum])}>{value.join(',')}</button>
|
||||
),
|
||||
}))
|
||||
vi.mock('../app-card', () => ({
|
||||
__esModule: true,
|
||||
default: ({ app, onCreate }: { app: any, onCreate: () => void }) => (
|
||||
<div
|
||||
data-testid="app-card"
|
||||
|
|
@ -46,7 +44,6 @@ vi.mock('../app-card', () => ({
|
|||
),
|
||||
}))
|
||||
vi.mock('@/app/components/explore/create-app-modal', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="create-from-template-modal" />,
|
||||
}))
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ vi.mock('@/context/i18n', () => ({
|
|||
useDocLink: () => () => '/guides',
|
||||
}))
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({ theme: 'light' }),
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import DuplicateAppModal from './index'
|
|||
|
||||
const appsFullRenderSpy = vi.fn()
|
||||
vi.mock('@/app/components/billing/apps-full-in-dialog', () => ({
|
||||
__esModule: true,
|
||||
default: ({ loc }: { loc: string }) => {
|
||||
appsFullRenderSpy(loc)
|
||||
return <div data-testid="apps-full">AppsFull</div>
|
||||
|
|
|
|||
|
|
@ -14,21 +14,18 @@ vi.mock('next/navigation', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/app/annotation', () => ({
|
||||
__esModule: true,
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="annotation" data-app-id={appDetail.id} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/log', () => ({
|
||||
__esModule: true,
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="log" data-app-id={appDetail.id} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/workflow-log', () => ({
|
||||
__esModule: true,
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="workflow-log" data-app-id={appDetail.id} />
|
||||
),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ const EmptyElement: FC<{ appDetail: App }> = ({ appDetail }) => {
|
|||
</span>
|
||||
<div className="system-sm-regular mt-2 text-text-tertiary">
|
||||
<Trans
|
||||
i18nKey="appLog.table.empty.element.content"
|
||||
i18nKey="table.empty.element.content"
|
||||
ns="appLog"
|
||||
components={{
|
||||
shareLink: <Link href={`${appDetail.site.app_base_url}${basePath}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} className="text-util-colors-blue-blue-600" target="_blank" rel="noopener noreferrer" />,
|
||||
testLink: <Link href={getRedirectionPath(true, appDetail)} className="text-util-colors-blue-blue-600" />,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
|
|||
import Embedded from './index'
|
||||
|
||||
vi.mock('./style.module.css', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
option: 'option',
|
||||
active: 'active',
|
||||
|
|
@ -37,7 +36,6 @@ const mockUseAppContext = vi.fn(() => ({
|
|||
}))
|
||||
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/app/components/base/chat/embedded-chatbot/theme/theme-context', () => ({
|
||||
|
|
|
|||
|
|
@ -413,6 +413,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||
<p className={cn('body-xs-regular pb-0.5 text-text-tertiary')}>
|
||||
<Trans
|
||||
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
|
||||
ns="appOverview"
|
||||
components={{ privacyPolicyLink: <Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer" className="text-text-accent" /> }}
|
||||
/>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ vi.mock('@/context/provider-context', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/apps-full-in-dialog', () => ({
|
||||
__esModule: true,
|
||||
default: ({ loc }: { loc: string }) => (
|
||||
<div data-testid="apps-full">
|
||||
AppsFull
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import Toast from '@/app/components/base/toast'
|
|||
import SavedItems from './index'
|
||||
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn(),
|
||||
}))
|
||||
vi.mock('next/navigation', () => ({
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ vi.mock('next/navigation', () => ({
|
|||
|
||||
// Mock the Run component as it has complex dependencies
|
||||
vi.mock('@/app/components/workflow/run', () => ({
|
||||
__esModule: true,
|
||||
default: ({ runDetailUrl, tracingListUrl }: { runDetailUrl: string, tracingListUrl: string }) => (
|
||||
<div data-testid="workflow-run">
|
||||
<span data-testid="run-detail-url">{runDetailUrl}</span>
|
||||
|
|
|
|||
|
|
@ -54,13 +54,11 @@ vi.mock('next/navigation', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, href }: { children: React.ReactNode, href: string }) => <a href={href}>{children}</a>,
|
||||
}))
|
||||
|
||||
// Mock the Run component to avoid complex dependencies
|
||||
vi.mock('@/app/components/workflow/run', () => ({
|
||||
__esModule: true,
|
||||
default: ({ runDetailUrl, tracingListUrl }: { runDetailUrl: string, tracingListUrl: string }) => (
|
||||
<div data-testid="workflow-run">
|
||||
<span data-testid="run-detail-url">{runDetailUrl}</span>
|
||||
|
|
@ -75,7 +73,6 @@ vi.mock('@/app/components/base/amplitude/utils', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
return { theme: 'light' }
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ vi.mock('next/navigation', () => ({
|
|||
|
||||
// Mock useTimestamp hook
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
formatTime: (timestamp: number, _format: string) => `formatted-${timestamp}`,
|
||||
}),
|
||||
|
|
@ -39,7 +38,6 @@ vi.mock('@/hooks/use-timestamp', () => ({
|
|||
|
||||
// Mock useBreakpoints hook
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
__esModule: true,
|
||||
default: () => 'pc', // Return desktop by default
|
||||
MediaType: {
|
||||
mobile: 'mobile',
|
||||
|
|
@ -49,7 +47,6 @@ vi.mock('@/hooks/use-breakpoints', () => ({
|
|||
|
||||
// Mock the Run component
|
||||
vi.mock('@/app/components/workflow/run', () => ({
|
||||
__esModule: true,
|
||||
default: ({ runDetailUrl, tracingListUrl }: { runDetailUrl: string, tracingListUrl: string }) => (
|
||||
<div data-testid="workflow-run">
|
||||
<span data-testid="run-detail-url">{runDetailUrl}</span>
|
||||
|
|
@ -67,13 +64,11 @@ vi.mock('@/app/components/workflow/context', () => ({
|
|||
|
||||
// Mock BlockIcon
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="block-icon">BlockIcon</div>,
|
||||
}))
|
||||
|
||||
// Mock useTheme
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
return { theme: 'light' }
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ import TriggerByDisplay from './trigger-by-display'
|
|||
|
||||
let mockTheme = Theme.light
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({ theme: mockTheme }),
|
||||
}))
|
||||
|
||||
// Mock BlockIcon as it has complex dependencies
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
__esModule: true,
|
||||
default: ({ type, toolIcon }: { type: string, toolIcon?: string }) => (
|
||||
<div data-testid="block-icon" data-type={type} data-tool-icon={toolIcon || ''}>
|
||||
BlockIcon
|
||||
|
|
|
|||
|
|
@ -188,13 +188,11 @@ vi.mock('@/app/components/base/popover', () => {
|
|||
|
||||
// Tooltip uses portals - minimal mock preserving popup content as title attribute
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, popupContent }: any) => React.createElement('div', { title: popupContent }, children),
|
||||
}))
|
||||
|
||||
// TagSelector has API dependency (service/tag) - mock for isolated testing
|
||||
vi.mock('@/app/components/base/tag-management/selector', () => ({
|
||||
__esModule: true,
|
||||
default: ({ tags }: any) => {
|
||||
return React.createElement('div', { 'aria-label': 'tag-selector' }, tags?.map((tag: any) => React.createElement('span', { key: tag.id }, tag.name)))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ let educationInitCalls: number = 0
|
|||
|
||||
// Mock useDocumentTitle hook
|
||||
vi.mock('@/hooks/use-document-title', () => ({
|
||||
__esModule: true,
|
||||
default: (title: string) => {
|
||||
documentTitleCalls.push(title)
|
||||
},
|
||||
|
|
@ -25,7 +24,6 @@ vi.mock('@/app/education-apply/hooks', () => ({
|
|||
|
||||
// Mock List component
|
||||
vi.mock('./list', () => ({
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
return React.createElement('div', { 'data-testid': 'apps-list' }, 'Apps List')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ const mockQueryState = {
|
|||
isCreatedByMe: false,
|
||||
}
|
||||
vi.mock('./hooks/use-apps-query-state', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
query: mockQueryState,
|
||||
setQuery: mockSetQuery,
|
||||
|
|
@ -144,7 +143,6 @@ vi.mock('@/service/tag', () => ({
|
|||
// Store TagFilter onChange callback for testing
|
||||
let mockTagFilterOnChange: ((value: string[]) => void) | null = null
|
||||
vi.mock('@/app/components/base/tag-management/filter', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onChange }: { onChange: (value: string[]) => void }) => {
|
||||
mockTagFilterOnChange = onChange
|
||||
return React.createElement('div', { 'data-testid': 'tag-filter' }, 'common.tag.placeholder')
|
||||
|
|
@ -200,7 +198,6 @@ vi.mock('next/dynamic', () => ({
|
|||
* Each child component (AppCard, NewAppCard, Empty, Footer) has its own dedicated tests.
|
||||
*/
|
||||
vi.mock('./app-card', () => ({
|
||||
__esModule: true,
|
||||
default: ({ app }: any) => {
|
||||
return React.createElement('div', { 'data-testid': `app-card-${app.id}`, 'role': 'article' }, app.name)
|
||||
},
|
||||
|
|
@ -213,14 +210,12 @@ vi.mock('./new-app-card', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('./empty', () => ({
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
return React.createElement('div', { 'data-testid': 'empty-state', 'role': 'status' }, 'No apps found')
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./footer', () => ({
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
return React.createElement('footer', { 'data-testid': 'footer', 'role': 'contentinfo' }, 'Footer')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
RiErrorWarningLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -26,7 +26,7 @@ type Props = {
|
|||
|
||||
const ToolCallItem: FC<Props> = ({ toolCall, isLLM = false, isFinal, tokens, observation, finalAnswer }) => {
|
||||
const [collapseState, setCollapseState] = useState<boolean>(true)
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const toolName = isLLM ? 'LLM' : (toolCall.tool_label[locale] || toolCall.tool_label[locale.replaceAll('-', '_')])
|
||||
|
||||
const getTime = (time: number) => {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ const AmplitudeProvider: FC<IAmplitudeProps> = ({
|
|||
pageViews: true,
|
||||
formInteractions: true,
|
||||
fileDownloads: true,
|
||||
attribution: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -18,12 +18,19 @@ const BasicContent: FC<BasicContentProps> = ({
|
|||
if (annotation?.logAnnotation)
|
||||
return <Markdown content={annotation?.logAnnotation.content || ''} />
|
||||
|
||||
// Preserve Windows UNC paths and similar backslash-heavy strings by
|
||||
// wrapping them in inline code so Markdown renders backslashes verbatim.
|
||||
let displayContent = content
|
||||
if (typeof content === 'string' && /^\\\\\S.*/.test(content) && !/^`.*`$/.test(content)) {
|
||||
displayContent = `\`${content}\``
|
||||
}
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
className={cn(
|
||||
item.isError && '!text-[#F04438]',
|
||||
)}
|
||||
content={content}
|
||||
content={displayContent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ const Answer: FC<AnswerProps> = ({
|
|||
}
|
||||
}, [switchSibling, item.prevSibling, item.nextSibling])
|
||||
|
||||
const contentIsEmpty = content.trim() === ''
|
||||
const contentIsEmpty = typeof content === 'string' && content.trim() === ''
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex last:mb-0">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import type { FC } from 'react'
|
||||
import type { CodeBasedExtensionForm } from '@/models/common'
|
||||
import type { ModerationConfig } from '@/models/debug'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
|
||||
type FormGenerationProps = {
|
||||
forms: CodeBasedExtensionForm[]
|
||||
|
|
@ -16,7 +15,7 @@ const FormGeneration: FC<FormGenerationProps> = ({
|
|||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
|
||||
const handleFormChange = (type: string, v: string) => {
|
||||
onChange({ ...value, [type]: v })
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import { FeatureEnum } from '@/app/components/base/features/types'
|
||||
import { ContentModeration } from '@/app/components/base/icons/src/vender/features'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useCodeBasedExtensions } from '@/service/use-common'
|
||||
|
||||
|
|
@ -25,7 +23,7 @@ const Moderation = ({
|
|||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowModerationSettingModal } = useModalContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const moderation = useFeatures(s => s.features.moderation)
|
||||
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { RiCloseLine } from '@remixicon/react'
|
|||
import { noop } from 'es-toolkit/compat'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
|
|
@ -15,7 +14,7 @@ import { useToastContext } from '@/app/components/base/toast'
|
|||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import I18n, { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { useCodeBasedExtensions, useModelProviders } from '@/service/use-common'
|
||||
|
|
@ -45,7 +44,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
|||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useToastContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { data: modelProviders, isPending: isLoading, refetch: refetchModelProviders } = useModelProviders()
|
||||
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import {
|
|||
} from './utils'
|
||||
|
||||
vi.mock('mime', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getAllExtensions: vi.fn(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { UnsafeUnwrappedHeaders } from 'next/headers'
|
||||
import type { FC } from 'react'
|
||||
import { headers } from 'next/headers'
|
||||
import Script from 'next/script'
|
||||
|
|
@ -18,45 +19,54 @@ export type IGAProps = {
|
|||
gaType: GaType
|
||||
}
|
||||
|
||||
const GA: FC<IGAProps> = async ({
|
||||
const extractNonceFromCSP = (cspHeader: string | null): string | undefined => {
|
||||
if (!cspHeader)
|
||||
return undefined
|
||||
const nonceMatch = cspHeader.match(/'nonce-([^']+)'/)
|
||||
return nonceMatch ? nonceMatch[1] : undefined
|
||||
}
|
||||
|
||||
const GA: FC<IGAProps> = ({
|
||||
gaType,
|
||||
}) => {
|
||||
if (IS_CE_EDITION)
|
||||
return null
|
||||
|
||||
const nonce = process.env.NODE_ENV === 'production' ? (await headers()).get('x-nonce') ?? '' : ''
|
||||
const cspHeader = process.env.NODE_ENV === 'production'
|
||||
? (headers() as unknown as UnsafeUnwrappedHeaders).get('content-security-policy')
|
||||
: null
|
||||
const nonce = extractNonceFromCSP(cspHeader)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
strategy="beforeInteractive"
|
||||
async
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${gaIdMaps[gaType]}`}
|
||||
nonce={nonce ?? undefined}
|
||||
>
|
||||
</Script>
|
||||
{/* Initialize dataLayer first */}
|
||||
<Script
|
||||
id="ga-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gaIdMaps[gaType]}');
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.gtag = function gtag(){window.dataLayer.push(arguments);};
|
||||
window.gtag('js', new Date());
|
||||
window.gtag('config', '${gaIdMaps[gaType]}');
|
||||
`,
|
||||
}}
|
||||
nonce={nonce ?? undefined}
|
||||
>
|
||||
</Script>
|
||||
nonce={nonce}
|
||||
/>
|
||||
{/* Load GA script */}
|
||||
<Script
|
||||
strategy="afterInteractive"
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${gaIdMaps[gaType]}`}
|
||||
nonce={nonce}
|
||||
/>
|
||||
{/* Cookie banner */}
|
||||
<Script
|
||||
id="cookieyes"
|
||||
strategy="lazyOnload"
|
||||
src="https://cdn-cookieyes.com/client_data/2a645945fcae53f8e025a2b1/script.js"
|
||||
nonce={nonce ?? undefined}
|
||||
>
|
||||
</Script>
|
||||
nonce={nonce}
|
||||
/>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(GA)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react'
|
|||
import AnnotationFull from './index'
|
||||
|
||||
vi.mock('./usage', () => ({
|
||||
__esModule: true,
|
||||
default: (props: { className?: string }) => {
|
||||
return (
|
||||
<div data-testid="usage-component" data-classname={props.className ?? ''}>
|
||||
|
|
@ -13,7 +12,6 @@ vi.mock('./usage', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('../upgrade-btn', () => ({
|
||||
__esModule: true,
|
||||
default: (props: { loc?: string }) => {
|
||||
return (
|
||||
<button type="button" data-testid="upgrade-btn">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
|||
import AnnotationFullModal from './modal'
|
||||
|
||||
vi.mock('./usage', () => ({
|
||||
__esModule: true,
|
||||
default: (props: { className?: string }) => {
|
||||
return (
|
||||
<div data-testid="usage-component" data-classname={props.className ?? ''}>
|
||||
|
|
@ -14,7 +13,6 @@ vi.mock('./usage', () => ({
|
|||
|
||||
let mockUpgradeBtnProps: { loc?: string } | null = null
|
||||
vi.mock('../upgrade-btn', () => ({
|
||||
__esModule: true,
|
||||
default: (props: { loc?: string }) => {
|
||||
mockUpgradeBtnProps = props
|
||||
return (
|
||||
|
|
@ -32,7 +30,6 @@ type ModalSnapshot = {
|
|||
}
|
||||
let mockModalProps: ModalSnapshot | null = null
|
||||
vi.mock('../../base/modal', () => ({
|
||||
__esModule: true,
|
||||
default: ({ isShow, children, onClose, closable, className }: { isShow: boolean, children: React.ReactNode, onClose: () => void, closable?: boolean, className?: string }) => {
|
||||
mockModalProps = {
|
||||
isShow,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ vi.mock('@/context/provider-context', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('../plan', () => ({
|
||||
__esModule: true,
|
||||
default: ({ loc }: { loc: string }) => <div data-testid="plan-component" data-loc={loc} />,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ vi.mock('@/context/provider-context', () => {
|
|||
})
|
||||
|
||||
vi.mock('../upgrade-btn', () => ({
|
||||
__esModule: true,
|
||||
default: () => <button data-testid="upgrade-btn" type="button">Upgrade</button>,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ vi.mock('@/config', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('./use-ps-info', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
saveOrUpdate,
|
||||
bind,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ vi.mock('js-cookie', () => {
|
|||
globals.__partnerStackCookieMocks = { get, set, remove }
|
||||
const cookieApi = { get, set, remove }
|
||||
return {
|
||||
__esModule: true,
|
||||
default: cookieApi,
|
||||
get,
|
||||
set,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ vi.mock('@/app/components/base/modal', () => {
|
|||
isShow ? <div data-testid="plan-upgrade-modal">{children}</div> : null
|
||||
)
|
||||
return {
|
||||
__esModule: true,
|
||||
default: MockModal,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -47,13 +47,11 @@ const verifyStateModalMock = vi.fn(props => (
|
|||
</div>
|
||||
))
|
||||
vi.mock('@/app/education-apply/verify-state-modal', () => ({
|
||||
__esModule: true,
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
default: (props: any) => verifyStateModalMock(props),
|
||||
}))
|
||||
|
||||
vi.mock('../upgrade-btn', () => ({
|
||||
__esModule: true,
|
||||
default: () => <button data-testid="plan-upgrade-btn" type="button">Upgrade</button>,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { PlanRange } from '../../plan-switcher/plan-range-switcher'
|
|||
import CloudPlanItem from './index'
|
||||
|
||||
vi.mock('../../../../base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import Plans from './index'
|
|||
import selfHostedPlanItem from './self-hosted-plan-item'
|
||||
|
||||
vi.mock('./cloud-plan-item', () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn(props => (
|
||||
<div data-testid={`cloud-plan-${props.plan}`} data-current-plan={props.currentPlan}>
|
||||
Cloud
|
||||
|
|
@ -20,7 +19,6 @@ vi.mock('./cloud-plan-item', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('./self-hosted-plan-item', () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn(props => (
|
||||
<div data-testid={`self-plan-${props.plan}`}>
|
||||
Self
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ vi.mock('react-i18next', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('../../../../base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ const planUpgradeModalMock = vi.fn((props: { show: boolean, title: string, descr
|
|||
))
|
||||
|
||||
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||
__esModule: true,
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
default: (props: any) => planUpgradeModalMock(props),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ vi.mock('@/context/provider-context', () => {
|
|||
})
|
||||
|
||||
vi.mock('../upgrade-btn', () => ({
|
||||
__esModule: true,
|
||||
default: () => <button data-testid="vector-upgrade-btn" type="button">Upgrade</button>,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ vi.mock('@/context/modal-context', () => ({
|
|||
// Mock the complex CustomWebAppBrand component to avoid dependency issues
|
||||
// This is acceptable because it has complex dependencies (fetch, APIs)
|
||||
vi.mock('../custom-web-app-brand', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="custom-web-app-brand">CustomWebAppBrand</div>,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
|
|||
|
||||
// Mock child component RetrievalParamConfig to simplify testing
|
||||
vi.mock('../retrieval-param-config', () => ({
|
||||
__esModule: true,
|
||||
default: ({ type, value, onChange, showMultiModalTip }: {
|
||||
type: RETRIEVE_METHOD
|
||||
value: RetrievalConfig
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useMemo } from 'react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { usePipelineTemplateList } from '@/service/use-pipeline'
|
||||
import CreateCard from './create-card'
|
||||
import TemplateCard from './template-card'
|
||||
|
||||
const BuiltInPipelineList = () => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
const language = useMemo(() => {
|
||||
if (['zh-Hans', 'ja-JP'].includes(locale))
|
||||
return locale
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
|||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { upload } from '@/service/base'
|
||||
|
|
@ -40,7 +40,7 @@ const FileUploader = ({
|
|||
}: IFileUploaderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const dropRef = useRef<HTMLDivElement>(null)
|
||||
const dragRef = useRef<HTMLDivElement>(null)
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ let stepThreeProps: Record<string, any> = {}
|
|||
let _topBarProps: Record<string, any> = {}
|
||||
|
||||
vi.mock('./step-one', () => ({
|
||||
__esModule: true,
|
||||
default: (props: Record<string, any>) => {
|
||||
stepOneProps = props
|
||||
return (
|
||||
|
|
@ -165,7 +164,6 @@ vi.mock('./step-one', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('./step-two', () => ({
|
||||
__esModule: true,
|
||||
default: (props: Record<string, any>) => {
|
||||
stepTwoProps = props
|
||||
return (
|
||||
|
|
@ -200,7 +198,6 @@ vi.mock('./step-two', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('./step-three', () => ({
|
||||
__esModule: true,
|
||||
default: (props: Record<string, any>) => {
|
||||
stepThreeProps = props
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import StepThree from './index'
|
|||
|
||||
// Mock the EmbeddingProcess component since it has complex async logic
|
||||
vi.mock('../embedding-process', () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn(({ datasetId, batchId, documents, indexingType, retrievalMethod }) => (
|
||||
<div data-testid="embedding-process">
|
||||
<span data-testid="ep-dataset-id">{datasetId}</span>
|
||||
|
|
@ -20,7 +19,6 @@ vi.mock('../embedding-process', () => ({
|
|||
// Mock useBreakpoints hook
|
||||
let mockMediaType = 'pc'
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
__esModule: true,
|
||||
MediaType: {
|
||||
mobile: 'mobile',
|
||||
tablet: 'tablet',
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@ import {
|
|||
import { noop } from 'es-toolkit/compat'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
|
@ -38,7 +36,7 @@ import { useDefaultModel, useModelList, useModelListAndDefaultModelAndCurrentPro
|
|||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import { FULL_DOC_PREVIEW_LENGTH, IS_CE_EDITION } from '@/config'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import I18n, { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
|
|
@ -151,7 +149,7 @@ const StepTwo = ({
|
|||
}: StepTwoProps) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/u
|
|||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { upload } from '@/service/base'
|
||||
|
|
@ -33,7 +33,7 @@ const LocalFile = ({
|
|||
}: LocalFileProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const localFileList = useDataSourceStoreWithSelector(state => state.localFileList)
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ const { mockToastNotify } = vi.hoisted(() => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
notify: mockToastNotify,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ const { mockToastNotify } = vi.hoisted(() => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
notify: mockToastNotify,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ vi.mock('@/context/dataset-detail', () => ({
|
|||
|
||||
// Mock document picker - needs mock for simplified interaction testing
|
||||
vi.mock('../../../common/document-picker/preview-document-picker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ files, onChange, value }: {
|
||||
files: Array<{ id: string, name: string, extension: string }>
|
||||
onChange: (selected: { id: string, name: string, extension: string }) => void
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import RuleDetail from './rule-detail'
|
|||
|
||||
// Mock next/image (using img element for simplicity in tests)
|
||||
vi.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: function MockImage({ src, alt, className }: { src: string, alt: string, className?: string }) {
|
||||
// eslint-disable-next-line next/no-img-element
|
||||
return <img src={src} alt={alt} className={className} data-testid="next-image" />
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ vi.mock('@/context/dataset-detail', () => ({
|
|||
// Mock the EmbeddingProcess component to track props
|
||||
let embeddingProcessProps: Record<string, unknown> = {}
|
||||
vi.mock('./embedding-process', () => ({
|
||||
__esModule: true,
|
||||
default: (props: Record<string, unknown>) => {
|
||||
embeddingProcessProps = props
|
||||
return (
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue