Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142f94e27a | ||
|
|
608958de1c | ||
|
|
7eb632eb34 | ||
|
|
33d4fd357c | ||
|
|
e55bd61c17 | ||
|
|
f2fc213d52 | ||
|
|
f814579ed2 | ||
|
|
71d299d0d3 | ||
|
|
e178451d04 | ||
|
|
9a6222f245 | ||
|
|
affe5ed30b | ||
|
|
4cc5401d7e | ||
|
|
36e840cd87 | ||
|
|
985b41c40b | ||
|
|
2e29ac2829 | ||
|
|
dbfb474eab | ||
|
|
d243de26ec | ||
|
|
894826771a | ||
|
|
a1bd929b3c | ||
|
|
ffb9ee3e36 | ||
|
|
485586f49a |
@@ -89,6 +89,12 @@ if $web_modified; then
|
||||
echo "No staged TypeScript changes detected, skipping type-check:tsgo"
|
||||
fi
|
||||
|
||||
echo "Running knip"
|
||||
if ! pnpm run knip; then
|
||||
echo "Knip check failed. Please run 'pnpm run knip' to fix the errors."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running unit tests check"
|
||||
modified_files=$(git diff --cached --name-only -- utils | grep -v '\.spec\.ts$' || true)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from flask_restx import Resource, fields, marshal_with
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
@@ -71,7 +71,7 @@ class AppImportApi(Resource):
|
||||
args = AppImportPayload.model_validate(console_ns.payload)
|
||||
|
||||
# Create service with session
|
||||
with sessionmaker(db.engine).begin() as session:
|
||||
with Session(db.engine) as session:
|
||||
import_service = AppDslService(session)
|
||||
# Import app
|
||||
account = current_user
|
||||
|
||||
@@ -158,10 +158,11 @@ class DataSourceApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def patch(self, binding_id, action: Literal["enable", "disable"]):
|
||||
_, current_tenant_id = current_account_with_tenant()
|
||||
binding_id = str(binding_id)
|
||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
||||
data_source_binding = session.execute(
|
||||
select(DataSourceOauthBinding).filter_by(id=binding_id)
|
||||
select(DataSourceOauthBinding).filter_by(id=binding_id, tenant_id=current_tenant_id)
|
||||
).scalar_one_or_none()
|
||||
if data_source_binding is None:
|
||||
raise NotFound("Data source binding not found.")
|
||||
|
||||
@@ -7,7 +7,7 @@ from werkzeug.exceptions import NotFound, RequestEntityTooLarge
|
||||
from controllers.trigger import bp
|
||||
from core.trigger.debug.event_bus import TriggerDebugEventBus
|
||||
from core.trigger.debug.events import WebhookDebugEvent, build_webhook_pool_key
|
||||
from services.trigger.webhook_service import WebhookService
|
||||
from services.trigger.webhook_service import RawWebhookDataDict, WebhookService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,6 +23,7 @@ def _prepare_webhook_execution(webhook_id: str, is_debug: bool = False):
|
||||
webhook_id, is_debug=is_debug
|
||||
)
|
||||
|
||||
webhook_data: RawWebhookDataDict
|
||||
try:
|
||||
# Use new unified extraction and validation
|
||||
webhook_data = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import orjson
|
||||
|
||||
from configs import dify_config
|
||||
|
||||
|
||||
class IdentityDict(TypedDict, total=False):
|
||||
tenant_id: str
|
||||
user_id: str
|
||||
user_type: str
|
||||
|
||||
|
||||
class StructuredJSONFormatter(logging.Formatter):
|
||||
"""
|
||||
JSON log formatter following the specified schema:
|
||||
@@ -84,7 +90,7 @@ class StructuredJSONFormatter(logging.Formatter):
|
||||
|
||||
return log_dict
|
||||
|
||||
def _extract_identity(self, record: logging.LogRecord) -> dict[str, str] | None:
|
||||
def _extract_identity(self, record: logging.LogRecord) -> IdentityDict | None:
|
||||
tenant_id = getattr(record, "tenant_id", None)
|
||||
user_id = getattr(record, "user_id", None)
|
||||
user_type = getattr(record, "user_type", None)
|
||||
@@ -92,7 +98,7 @@ class StructuredJSONFormatter(logging.Formatter):
|
||||
if not any([tenant_id, user_id, user_type]):
|
||||
return None
|
||||
|
||||
identity: dict[str, str] = {}
|
||||
identity: IdentityDict = {}
|
||||
if tenant_id:
|
||||
identity["tenant_id"] = tenant_id
|
||||
if user_id:
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Callable
|
||||
from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError
|
||||
from datetime import timedelta
|
||||
from types import TracebackType
|
||||
from typing import Any, Self, cast
|
||||
from typing import Any, Self
|
||||
|
||||
from httpx import HTTPStatusError
|
||||
from pydantic import BaseModel
|
||||
@@ -338,12 +338,11 @@ class BaseSession[
|
||||
validated_request = self._receive_request_type.model_validate(
|
||||
message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
)
|
||||
validated_request = cast(ReceiveRequestT, validated_request)
|
||||
|
||||
responder = RequestResponder[ReceiveRequestT, SendResultT](
|
||||
request_id=message.message.root.id,
|
||||
request_meta=validated_request.root.params.meta if validated_request.root.params else None,
|
||||
request=validated_request,
|
||||
request=validated_request, # type: ignore[arg-type] # mypy can't narrow constrained TypeVar from model_validate
|
||||
session=self,
|
||||
on_complete=lambda r: self._in_flight.pop(r.request_id, None),
|
||||
)
|
||||
@@ -359,15 +358,14 @@ class BaseSession[
|
||||
notification = self._receive_notification_type.model_validate(
|
||||
message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||
)
|
||||
notification = cast(ReceiveNotificationT, notification)
|
||||
# Handle cancellation notifications
|
||||
if isinstance(notification.root, CancelledNotification):
|
||||
cancelled_id = notification.root.params.requestId
|
||||
if cancelled_id in self._in_flight:
|
||||
self._in_flight[cancelled_id].cancel()
|
||||
else:
|
||||
self._received_notification(notification)
|
||||
self._handle_incoming(notification)
|
||||
self._received_notification(notification) # type: ignore[arg-type]
|
||||
self._handle_incoming(notification) # type: ignore[arg-type]
|
||||
except Exception as e:
|
||||
# For other validation errors, log and continue
|
||||
logger.warning("Failed to validate notification: %s. Message was: %s", e, message.message.root)
|
||||
|
||||
@@ -144,22 +144,26 @@ class AccountService:
|
||||
|
||||
@staticmethod
|
||||
def load_user(user_id: str) -> None | Account:
|
||||
account = db.session.query(Account).filter_by(id=user_id).first()
|
||||
account = db.session.get(Account, user_id)
|
||||
if not account:
|
||||
return None
|
||||
|
||||
if account.status == AccountStatus.BANNED:
|
||||
raise Unauthorized("Account is banned.")
|
||||
|
||||
current_tenant = db.session.query(TenantAccountJoin).filter_by(account_id=account.id, current=True).first()
|
||||
current_tenant = db.session.scalar(
|
||||
select(TenantAccountJoin)
|
||||
.where(TenantAccountJoin.account_id == account.id, TenantAccountJoin.current == True)
|
||||
.limit(1)
|
||||
)
|
||||
if current_tenant:
|
||||
account.set_tenant_id(current_tenant.tenant_id)
|
||||
else:
|
||||
available_ta = (
|
||||
db.session.query(TenantAccountJoin)
|
||||
.filter_by(account_id=account.id)
|
||||
available_ta = db.session.scalar(
|
||||
select(TenantAccountJoin)
|
||||
.where(TenantAccountJoin.account_id == account.id)
|
||||
.order_by(TenantAccountJoin.id.asc())
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if not available_ta:
|
||||
return None
|
||||
@@ -195,7 +199,7 @@ class AccountService:
|
||||
def authenticate(email: str, password: str, invite_token: str | None = None) -> Account:
|
||||
"""authenticate account with email and password"""
|
||||
|
||||
account = db.session.query(Account).filter_by(email=email).first()
|
||||
account = db.session.scalar(select(Account).where(Account.email == email).limit(1))
|
||||
if not account:
|
||||
raise AccountPasswordError("Invalid email or password.")
|
||||
|
||||
@@ -371,8 +375,10 @@ class AccountService:
|
||||
"""Link account integrate"""
|
||||
try:
|
||||
# Query whether there is an existing binding record for the same provider
|
||||
account_integrate: AccountIntegrate | None = (
|
||||
db.session.query(AccountIntegrate).filter_by(account_id=account.id, provider=provider).first()
|
||||
account_integrate: AccountIntegrate | None = db.session.scalar(
|
||||
select(AccountIntegrate)
|
||||
.where(AccountIntegrate.account_id == account.id, AccountIntegrate.provider == provider)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if account_integrate:
|
||||
@@ -416,7 +422,9 @@ class AccountService:
|
||||
def update_account_email(account: Account, email: str) -> Account:
|
||||
"""Update account email"""
|
||||
account.email = email
|
||||
account_integrate = db.session.query(AccountIntegrate).filter_by(account_id=account.id).first()
|
||||
account_integrate = db.session.scalar(
|
||||
select(AccountIntegrate).where(AccountIntegrate.account_id == account.id).limit(1)
|
||||
)
|
||||
if account_integrate:
|
||||
db.session.delete(account_integrate)
|
||||
db.session.add(account)
|
||||
@@ -818,7 +826,7 @@ class AccountService:
|
||||
)
|
||||
)
|
||||
|
||||
account = db.session.query(Account).where(Account.email == email).first()
|
||||
account = db.session.scalar(select(Account).where(Account.email == email).limit(1))
|
||||
if not account:
|
||||
return None
|
||||
|
||||
@@ -1018,7 +1026,7 @@ class AccountService:
|
||||
|
||||
@staticmethod
|
||||
def check_email_unique(email: str) -> bool:
|
||||
return db.session.query(Account).filter_by(email=email).first() is None
|
||||
return db.session.scalar(select(Account).where(Account.email == email).limit(1)) is None
|
||||
|
||||
|
||||
class TenantService:
|
||||
|
||||
@@ -32,6 +32,11 @@ class SubscriptionPlan(TypedDict):
|
||||
expiration_date: int
|
||||
|
||||
|
||||
class KnowledgeRateLimitDict(TypedDict):
|
||||
limit: int
|
||||
subscription_plan: str
|
||||
|
||||
|
||||
class BillingService:
|
||||
base_url = os.environ.get("BILLING_API_URL", "BILLING_API_URL")
|
||||
secret_key = os.environ.get("BILLING_API_SECRET_KEY", "BILLING_API_SECRET_KEY")
|
||||
@@ -58,7 +63,7 @@ class BillingService:
|
||||
return usage_info
|
||||
|
||||
@classmethod
|
||||
def get_knowledge_rate_limit(cls, tenant_id: str):
|
||||
def get_knowledge_rate_limit(cls, tenant_id: str) -> KnowledgeRateLimitDict:
|
||||
params = {"tenant_id": tenant_id}
|
||||
|
||||
knowledge_rate_limit = cls._send_request("GET", "/subscription/knowledge-rate-limit", params=params)
|
||||
|
||||
@@ -5,7 +5,7 @@ from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from graphon.nodes.http_request.exc import InvalidHttpMethodError
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from constants import HIDDEN_VALUE
|
||||
from core.helper import ssrf_proxy
|
||||
@@ -103,8 +103,10 @@ class ExternalDatasetService:
|
||||
|
||||
@staticmethod
|
||||
def get_external_knowledge_api(external_knowledge_api_id: str, tenant_id: str) -> ExternalKnowledgeApis:
|
||||
external_knowledge_api: ExternalKnowledgeApis | None = (
|
||||
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
|
||||
external_knowledge_api: ExternalKnowledgeApis | None = db.session.scalar(
|
||||
select(ExternalKnowledgeApis)
|
||||
.where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
if external_knowledge_api is None:
|
||||
raise ValueError("api template not found")
|
||||
@@ -112,8 +114,10 @@ class ExternalDatasetService:
|
||||
|
||||
@staticmethod
|
||||
def update_external_knowledge_api(tenant_id, user_id, external_knowledge_api_id, args) -> ExternalKnowledgeApis:
|
||||
external_knowledge_api: ExternalKnowledgeApis | None = (
|
||||
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
|
||||
external_knowledge_api: ExternalKnowledgeApis | None = db.session.scalar(
|
||||
select(ExternalKnowledgeApis)
|
||||
.where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
if external_knowledge_api is None:
|
||||
raise ValueError("api template not found")
|
||||
@@ -132,8 +136,10 @@ class ExternalDatasetService:
|
||||
|
||||
@staticmethod
|
||||
def delete_external_knowledge_api(tenant_id: str, external_knowledge_api_id: str):
|
||||
external_knowledge_api = (
|
||||
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
|
||||
external_knowledge_api = db.session.scalar(
|
||||
select(ExternalKnowledgeApis)
|
||||
.where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
if external_knowledge_api is None:
|
||||
raise ValueError("api template not found")
|
||||
@@ -144,9 +150,12 @@ class ExternalDatasetService:
|
||||
@staticmethod
|
||||
def external_knowledge_api_use_check(external_knowledge_api_id: str) -> tuple[bool, int]:
|
||||
count = (
|
||||
db.session.query(ExternalKnowledgeBindings)
|
||||
.filter_by(external_knowledge_api_id=external_knowledge_api_id)
|
||||
.count()
|
||||
db.session.scalar(
|
||||
select(func.count(ExternalKnowledgeBindings.id)).where(
|
||||
ExternalKnowledgeBindings.external_knowledge_api_id == external_knowledge_api_id
|
||||
)
|
||||
)
|
||||
or 0
|
||||
)
|
||||
if count > 0:
|
||||
return True, count
|
||||
@@ -154,8 +163,10 @@ class ExternalDatasetService:
|
||||
|
||||
@staticmethod
|
||||
def get_external_knowledge_binding_with_dataset_id(tenant_id: str, dataset_id: str) -> ExternalKnowledgeBindings:
|
||||
external_knowledge_binding: ExternalKnowledgeBindings | None = (
|
||||
db.session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id, tenant_id=tenant_id).first()
|
||||
external_knowledge_binding: ExternalKnowledgeBindings | None = db.session.scalar(
|
||||
select(ExternalKnowledgeBindings)
|
||||
.where(ExternalKnowledgeBindings.dataset_id == dataset_id, ExternalKnowledgeBindings.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
if not external_knowledge_binding:
|
||||
raise ValueError("external knowledge binding not found")
|
||||
@@ -163,8 +174,10 @@ class ExternalDatasetService:
|
||||
|
||||
@staticmethod
|
||||
def document_create_args_validate(tenant_id: str, external_knowledge_api_id: str, process_parameter: dict):
|
||||
external_knowledge_api = (
|
||||
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
|
||||
external_knowledge_api = db.session.scalar(
|
||||
select(ExternalKnowledgeApis)
|
||||
.where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
if external_knowledge_api is None or external_knowledge_api.settings is None:
|
||||
raise ValueError("api template not found")
|
||||
@@ -238,12 +251,17 @@ class ExternalDatasetService:
|
||||
@staticmethod
|
||||
def create_external_dataset(tenant_id: str, user_id: str, args: dict) -> Dataset:
|
||||
# check if dataset name already exists
|
||||
if db.session.query(Dataset).filter_by(name=args.get("name"), tenant_id=tenant_id).first():
|
||||
if db.session.scalar(
|
||||
select(Dataset).where(Dataset.name == args.get("name"), Dataset.tenant_id == tenant_id).limit(1)
|
||||
):
|
||||
raise DatasetNameDuplicateError(f"Dataset with name {args.get('name')} already exists.")
|
||||
external_knowledge_api = (
|
||||
db.session.query(ExternalKnowledgeApis)
|
||||
.filter_by(id=args.get("external_knowledge_api_id"), tenant_id=tenant_id)
|
||||
.first()
|
||||
external_knowledge_api = db.session.scalar(
|
||||
select(ExternalKnowledgeApis)
|
||||
.where(
|
||||
ExternalKnowledgeApis.id == args.get("external_knowledge_api_id"),
|
||||
ExternalKnowledgeApis.tenant_id == tenant_id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if external_knowledge_api is None:
|
||||
@@ -286,16 +304,18 @@ class ExternalDatasetService:
|
||||
external_retrieval_parameters: dict,
|
||||
metadata_condition: MetadataCondition | None = None,
|
||||
):
|
||||
external_knowledge_binding = (
|
||||
db.session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id, tenant_id=tenant_id).first()
|
||||
external_knowledge_binding = db.session.scalar(
|
||||
select(ExternalKnowledgeBindings)
|
||||
.where(ExternalKnowledgeBindings.dataset_id == dataset_id, ExternalKnowledgeBindings.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
if not external_knowledge_binding:
|
||||
raise ValueError("external knowledge binding not found")
|
||||
|
||||
external_knowledge_api = (
|
||||
db.session.query(ExternalKnowledgeApis)
|
||||
.filter_by(id=external_knowledge_binding.external_knowledge_api_id)
|
||||
.first()
|
||||
external_knowledge_api = db.session.scalar(
|
||||
select(ExternalKnowledgeApis)
|
||||
.where(ExternalKnowledgeApis.id == external_knowledge_binding.external_knowledge_api_id)
|
||||
.limit(1)
|
||||
)
|
||||
if external_knowledge_api is None or external_knowledge_api.settings is None:
|
||||
raise ValueError("external api template not found")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from graphon.model_runtime.entities import LLMMode
|
||||
|
||||
@@ -18,6 +18,16 @@ from models.enums import CreatorUserRole, DatasetQuerySource
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QueryDict(TypedDict):
|
||||
content: str
|
||||
|
||||
|
||||
class RetrieveResponseDict(TypedDict):
|
||||
query: QueryDict
|
||||
records: list[dict[str, Any]]
|
||||
|
||||
|
||||
default_retrieval_model = {
|
||||
"search_method": RetrievalMethod.SEMANTIC_SEARCH,
|
||||
"reranking_enable": False,
|
||||
@@ -150,7 +160,7 @@ class HitTestingService:
|
||||
return dict(cls.compact_external_retrieve_response(dataset, query, all_documents))
|
||||
|
||||
@classmethod
|
||||
def compact_retrieve_response(cls, query: str, documents: list[Document]) -> dict[Any, Any]:
|
||||
def compact_retrieve_response(cls, query: str, documents: list[Document]) -> RetrieveResponseDict:
|
||||
records = RetrievalService.format_retrieval_documents(documents)
|
||||
|
||||
return {
|
||||
@@ -161,7 +171,7 @@ class HitTestingService:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def compact_external_retrieve_response(cls, dataset: Dataset, query: str, documents: list) -> dict[Any, Any]:
|
||||
def compact_external_retrieve_response(cls, dataset: Dataset, query: str, documents: list) -> RetrieveResponseDict:
|
||||
records = []
|
||||
if dataset.provider == "external":
|
||||
for document in documents:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
|
||||
from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
@@ -25,10 +27,14 @@ class MetadataService:
|
||||
raise ValueError("Metadata name cannot exceed 255 characters.")
|
||||
current_user, current_tenant_id = current_account_with_tenant()
|
||||
# check if metadata name already exists
|
||||
if (
|
||||
db.session.query(DatasetMetadata)
|
||||
.filter_by(tenant_id=current_tenant_id, dataset_id=dataset_id, name=metadata_args.name)
|
||||
.first()
|
||||
if db.session.scalar(
|
||||
select(DatasetMetadata)
|
||||
.where(
|
||||
DatasetMetadata.tenant_id == current_tenant_id,
|
||||
DatasetMetadata.dataset_id == dataset_id,
|
||||
DatasetMetadata.name == metadata_args.name,
|
||||
)
|
||||
.limit(1)
|
||||
):
|
||||
raise ValueError("Metadata name already exists.")
|
||||
for field in BuiltInField:
|
||||
@@ -54,10 +60,14 @@ class MetadataService:
|
||||
lock_key = f"dataset_metadata_lock_{dataset_id}"
|
||||
# check if metadata name already exists
|
||||
current_user, current_tenant_id = current_account_with_tenant()
|
||||
if (
|
||||
db.session.query(DatasetMetadata)
|
||||
.filter_by(tenant_id=current_tenant_id, dataset_id=dataset_id, name=name)
|
||||
.first()
|
||||
if db.session.scalar(
|
||||
select(DatasetMetadata)
|
||||
.where(
|
||||
DatasetMetadata.tenant_id == current_tenant_id,
|
||||
DatasetMetadata.dataset_id == dataset_id,
|
||||
DatasetMetadata.name == name,
|
||||
)
|
||||
.limit(1)
|
||||
):
|
||||
raise ValueError("Metadata name already exists.")
|
||||
for field in BuiltInField:
|
||||
@@ -65,7 +75,11 @@ class MetadataService:
|
||||
raise ValueError("Metadata name already exists in Built-in fields.")
|
||||
try:
|
||||
MetadataService.knowledge_base_metadata_lock_check(dataset_id, None)
|
||||
metadata = db.session.query(DatasetMetadata).filter_by(id=metadata_id, dataset_id=dataset_id).first()
|
||||
metadata = db.session.scalar(
|
||||
select(DatasetMetadata)
|
||||
.where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id)
|
||||
.limit(1)
|
||||
)
|
||||
if metadata is None:
|
||||
raise ValueError("Metadata not found.")
|
||||
old_name = metadata.name
|
||||
@@ -74,9 +88,9 @@ class MetadataService:
|
||||
metadata.updated_at = naive_utc_now()
|
||||
|
||||
# update related documents
|
||||
dataset_metadata_bindings = (
|
||||
db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata_id).all()
|
||||
)
|
||||
dataset_metadata_bindings = db.session.scalars(
|
||||
select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id)
|
||||
).all()
|
||||
if dataset_metadata_bindings:
|
||||
document_ids = [binding.document_id for binding in dataset_metadata_bindings]
|
||||
documents = DocumentService.get_document_by_ids(document_ids)
|
||||
@@ -101,15 +115,19 @@ class MetadataService:
|
||||
lock_key = f"dataset_metadata_lock_{dataset_id}"
|
||||
try:
|
||||
MetadataService.knowledge_base_metadata_lock_check(dataset_id, None)
|
||||
metadata = db.session.query(DatasetMetadata).filter_by(id=metadata_id, dataset_id=dataset_id).first()
|
||||
metadata = db.session.scalar(
|
||||
select(DatasetMetadata)
|
||||
.where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id)
|
||||
.limit(1)
|
||||
)
|
||||
if metadata is None:
|
||||
raise ValueError("Metadata not found.")
|
||||
db.session.delete(metadata)
|
||||
|
||||
# deal related documents
|
||||
dataset_metadata_bindings = (
|
||||
db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata_id).all()
|
||||
)
|
||||
dataset_metadata_bindings = db.session.scalars(
|
||||
select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id)
|
||||
).all()
|
||||
if dataset_metadata_bindings:
|
||||
document_ids = [binding.document_id for binding in dataset_metadata_bindings]
|
||||
documents = DocumentService.get_document_by_ids(document_ids)
|
||||
@@ -224,16 +242,23 @@ class MetadataService:
|
||||
|
||||
# deal metadata binding (in the same transaction as the doc_metadata update)
|
||||
if not operation.partial_update:
|
||||
db.session.query(DatasetMetadataBinding).filter_by(document_id=operation.document_id).delete()
|
||||
db.session.execute(
|
||||
delete(DatasetMetadataBinding).where(
|
||||
DatasetMetadataBinding.document_id == operation.document_id
|
||||
)
|
||||
)
|
||||
|
||||
current_user, current_tenant_id = current_account_with_tenant()
|
||||
for metadata_value in operation.metadata_list:
|
||||
# check if binding already exists
|
||||
if operation.partial_update:
|
||||
existing_binding = (
|
||||
db.session.query(DatasetMetadataBinding)
|
||||
.filter_by(document_id=operation.document_id, metadata_id=metadata_value.id)
|
||||
.first()
|
||||
existing_binding = db.session.scalar(
|
||||
select(DatasetMetadataBinding)
|
||||
.where(
|
||||
DatasetMetadataBinding.document_id == operation.document_id,
|
||||
DatasetMetadataBinding.metadata_id == metadata_value.id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if existing_binding:
|
||||
continue
|
||||
@@ -275,9 +300,13 @@ class MetadataService:
|
||||
"id": item.get("id"),
|
||||
"name": item.get("name"),
|
||||
"type": item.get("type"),
|
||||
"count": db.session.query(DatasetMetadataBinding)
|
||||
.filter_by(metadata_id=item.get("id"), dataset_id=dataset.id)
|
||||
.count(),
|
||||
"count": db.session.scalar(
|
||||
select(func.count(DatasetMetadataBinding.id)).where(
|
||||
DatasetMetadataBinding.metadata_id == item.get("id"),
|
||||
DatasetMetadataBinding.dataset_id == dataset.id,
|
||||
)
|
||||
)
|
||||
or 0,
|
||||
}
|
||||
for item in dataset.doc_metadata or []
|
||||
if item.get("id") != "built-in"
|
||||
|
||||
@@ -156,27 +156,27 @@ class RagPipelineService:
|
||||
:param template_id: template id
|
||||
:param template_info: template info
|
||||
"""
|
||||
customized_template: PipelineCustomizedTemplate | None = (
|
||||
db.session.query(PipelineCustomizedTemplate)
|
||||
customized_template: PipelineCustomizedTemplate | None = db.session.scalar(
|
||||
select(PipelineCustomizedTemplate)
|
||||
.where(
|
||||
PipelineCustomizedTemplate.id == template_id,
|
||||
PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if not customized_template:
|
||||
raise ValueError("Customized pipeline template not found.")
|
||||
# check template name is exist
|
||||
template_name = template_info.name
|
||||
if template_name:
|
||||
template = (
|
||||
db.session.query(PipelineCustomizedTemplate)
|
||||
template = db.session.scalar(
|
||||
select(PipelineCustomizedTemplate)
|
||||
.where(
|
||||
PipelineCustomizedTemplate.name == template_name,
|
||||
PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id,
|
||||
PipelineCustomizedTemplate.id != template_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if template:
|
||||
raise ValueError("Template name is already exists")
|
||||
@@ -192,13 +192,13 @@ class RagPipelineService:
|
||||
"""
|
||||
Delete customized pipeline template.
|
||||
"""
|
||||
customized_template: PipelineCustomizedTemplate | None = (
|
||||
db.session.query(PipelineCustomizedTemplate)
|
||||
customized_template: PipelineCustomizedTemplate | None = db.session.scalar(
|
||||
select(PipelineCustomizedTemplate)
|
||||
.where(
|
||||
PipelineCustomizedTemplate.id == template_id,
|
||||
PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if not customized_template:
|
||||
raise ValueError("Customized pipeline template not found.")
|
||||
@@ -210,14 +210,14 @@ class RagPipelineService:
|
||||
Get draft workflow
|
||||
"""
|
||||
# fetch draft workflow by rag pipeline
|
||||
workflow = (
|
||||
db.session.query(Workflow)
|
||||
workflow = db.session.scalar(
|
||||
select(Workflow)
|
||||
.where(
|
||||
Workflow.tenant_id == pipeline.tenant_id,
|
||||
Workflow.app_id == pipeline.id,
|
||||
Workflow.version == "draft",
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
# return draft workflow
|
||||
@@ -232,28 +232,28 @@ class RagPipelineService:
|
||||
return None
|
||||
|
||||
# fetch published workflow by workflow_id
|
||||
workflow = (
|
||||
db.session.query(Workflow)
|
||||
workflow = db.session.scalar(
|
||||
select(Workflow)
|
||||
.where(
|
||||
Workflow.tenant_id == pipeline.tenant_id,
|
||||
Workflow.app_id == pipeline.id,
|
||||
Workflow.id == pipeline.workflow_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
return workflow
|
||||
|
||||
def get_published_workflow_by_id(self, pipeline: Pipeline, workflow_id: str) -> Workflow | None:
|
||||
"""Fetch a published workflow snapshot by ID for restore operations."""
|
||||
workflow = (
|
||||
db.session.query(Workflow)
|
||||
workflow = db.session.scalar(
|
||||
select(Workflow)
|
||||
.where(
|
||||
Workflow.tenant_id == pipeline.tenant_id,
|
||||
Workflow.app_id == pipeline.id,
|
||||
Workflow.id == workflow_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if workflow and workflow.version == Workflow.VERSION_DRAFT:
|
||||
raise IsDraftWorkflowError("source workflow must be published")
|
||||
@@ -974,7 +974,7 @@ class RagPipelineService:
|
||||
if invoke_from.value == InvokeFrom.PUBLISHED_PIPELINE:
|
||||
document_id = get_system_segment(variable_pool, SystemVariableKey.DOCUMENT_ID)
|
||||
if document_id:
|
||||
document = db.session.query(Document).where(Document.id == document_id.value).first()
|
||||
document = db.session.get(Document, document_id.value)
|
||||
if document:
|
||||
document.indexing_status = IndexingStatus.ERROR
|
||||
document.error = error
|
||||
@@ -1178,12 +1178,12 @@ class RagPipelineService:
|
||||
"""
|
||||
Publish customized pipeline template
|
||||
"""
|
||||
pipeline = db.session.query(Pipeline).where(Pipeline.id == pipeline_id).first()
|
||||
pipeline = db.session.get(Pipeline, pipeline_id)
|
||||
if not pipeline:
|
||||
raise ValueError("Pipeline not found")
|
||||
if not pipeline.workflow_id:
|
||||
raise ValueError("Pipeline workflow not found")
|
||||
workflow = db.session.query(Workflow).where(Workflow.id == pipeline.workflow_id).first()
|
||||
workflow = db.session.get(Workflow, pipeline.workflow_id)
|
||||
if not workflow:
|
||||
raise ValueError("Workflow not found")
|
||||
with Session(db.engine) as session:
|
||||
@@ -1194,21 +1194,21 @@ class RagPipelineService:
|
||||
# check template name is exist
|
||||
template_name = args.get("name")
|
||||
if template_name:
|
||||
template = (
|
||||
db.session.query(PipelineCustomizedTemplate)
|
||||
template = db.session.scalar(
|
||||
select(PipelineCustomizedTemplate)
|
||||
.where(
|
||||
PipelineCustomizedTemplate.name == template_name,
|
||||
PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if template:
|
||||
raise ValueError("Template name is already exists")
|
||||
|
||||
max_position = (
|
||||
db.session.query(func.max(PipelineCustomizedTemplate.position))
|
||||
.where(PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id)
|
||||
.scalar()
|
||||
max_position = db.session.scalar(
|
||||
select(func.max(PipelineCustomizedTemplate.position)).where(
|
||||
PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id
|
||||
)
|
||||
)
|
||||
|
||||
from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineDslService
|
||||
@@ -1239,13 +1239,14 @@ class RagPipelineService:
|
||||
|
||||
def is_workflow_exist(self, pipeline: Pipeline) -> bool:
|
||||
return (
|
||||
db.session.query(Workflow)
|
||||
.where(
|
||||
Workflow.tenant_id == pipeline.tenant_id,
|
||||
Workflow.app_id == pipeline.id,
|
||||
Workflow.version == Workflow.VERSION_DRAFT,
|
||||
db.session.scalar(
|
||||
select(func.count(Workflow.id)).where(
|
||||
Workflow.tenant_id == pipeline.tenant_id,
|
||||
Workflow.app_id == pipeline.id,
|
||||
Workflow.version == Workflow.VERSION_DRAFT,
|
||||
)
|
||||
)
|
||||
.count()
|
||||
or 0
|
||||
) > 0
|
||||
|
||||
def get_node_last_run(
|
||||
@@ -1353,11 +1354,11 @@ class RagPipelineService:
|
||||
|
||||
def get_recommended_plugins(self, type: str) -> dict:
|
||||
# Query active recommended plugins
|
||||
query = db.session.query(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True)
|
||||
stmt = select(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True)
|
||||
if type and type != "all":
|
||||
query = query.where(PipelineRecommendedPlugin.type == type)
|
||||
stmt = stmt.where(PipelineRecommendedPlugin.type == type)
|
||||
|
||||
pipeline_recommended_plugins = query.order_by(PipelineRecommendedPlugin.position.asc()).all()
|
||||
pipeline_recommended_plugins = db.session.scalars(stmt.order_by(PipelineRecommendedPlugin.position.asc())).all()
|
||||
|
||||
if not pipeline_recommended_plugins:
|
||||
return {
|
||||
@@ -1396,14 +1397,12 @@ class RagPipelineService:
|
||||
"""
|
||||
Retry error document
|
||||
"""
|
||||
document_pipeline_execution_log = (
|
||||
db.session.query(DocumentPipelineExecutionLog)
|
||||
.where(DocumentPipelineExecutionLog.document_id == document.id)
|
||||
.first()
|
||||
document_pipeline_execution_log = db.session.scalar(
|
||||
select(DocumentPipelineExecutionLog).where(DocumentPipelineExecutionLog.document_id == document.id).limit(1)
|
||||
)
|
||||
if not document_pipeline_execution_log:
|
||||
raise ValueError("Document pipeline execution log not found")
|
||||
pipeline = db.session.query(Pipeline).where(Pipeline.id == document_pipeline_execution_log.pipeline_id).first()
|
||||
pipeline = db.session.get(Pipeline, document_pipeline_execution_log.pipeline_id)
|
||||
if not pipeline:
|
||||
raise ValueError("Pipeline not found")
|
||||
# convert to app config
|
||||
@@ -1432,23 +1431,23 @@ class RagPipelineService:
|
||||
"""
|
||||
Get datasource plugins
|
||||
"""
|
||||
dataset: Dataset | None = (
|
||||
db.session.query(Dataset)
|
||||
dataset: Dataset | None = db.session.scalar(
|
||||
select(Dataset)
|
||||
.where(
|
||||
Dataset.id == dataset_id,
|
||||
Dataset.tenant_id == tenant_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if not dataset:
|
||||
raise ValueError("Dataset not found")
|
||||
pipeline: Pipeline | None = (
|
||||
db.session.query(Pipeline)
|
||||
pipeline: Pipeline | None = db.session.scalar(
|
||||
select(Pipeline)
|
||||
.where(
|
||||
Pipeline.id == dataset.pipeline_id,
|
||||
Pipeline.tenant_id == tenant_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if not pipeline:
|
||||
raise ValueError("Pipeline not found")
|
||||
@@ -1530,23 +1529,23 @@ class RagPipelineService:
|
||||
"""
|
||||
Get pipeline
|
||||
"""
|
||||
dataset: Dataset | None = (
|
||||
db.session.query(Dataset)
|
||||
dataset: Dataset | None = db.session.scalar(
|
||||
select(Dataset)
|
||||
.where(
|
||||
Dataset.id == dataset_id,
|
||||
Dataset.tenant_id == tenant_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if not dataset:
|
||||
raise ValueError("Dataset not found")
|
||||
pipeline: Pipeline | None = (
|
||||
db.session.query(Pipeline)
|
||||
pipeline: Pipeline | None = db.session.scalar(
|
||||
select(Pipeline)
|
||||
.where(
|
||||
Pipeline.id == dataset.pipeline_id,
|
||||
Pipeline.tenant_id == tenant_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
if not pipeline:
|
||||
raise ValueError("Pipeline not found")
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from datetime import datetime
|
||||
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy import delete, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.tools.__base.tool_provider import ToolProviderController
|
||||
@@ -42,20 +42,22 @@ class WorkflowToolManageService:
|
||||
labels: list[str] | None = None,
|
||||
):
|
||||
# check if the name is unique
|
||||
existing_workflow_tool_provider = (
|
||||
db.session.query(WorkflowToolProvider)
|
||||
existing_workflow_tool_provider = db.session.scalar(
|
||||
select(WorkflowToolProvider)
|
||||
.where(
|
||||
WorkflowToolProvider.tenant_id == tenant_id,
|
||||
# name or app_id
|
||||
or_(WorkflowToolProvider.name == name, WorkflowToolProvider.app_id == workflow_app_id),
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if existing_workflow_tool_provider is not None:
|
||||
raise ValueError(f"Tool with name {name} or app_id {workflow_app_id} already exists")
|
||||
|
||||
app: App | None = db.session.query(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).first()
|
||||
app: App | None = db.session.scalar(
|
||||
select(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).limit(1)
|
||||
)
|
||||
|
||||
if app is None:
|
||||
raise ValueError(f"App {workflow_app_id} not found")
|
||||
@@ -122,30 +124,30 @@ class WorkflowToolManageService:
|
||||
:return: the updated tool
|
||||
"""
|
||||
# check if the name is unique
|
||||
existing_workflow_tool_provider = (
|
||||
db.session.query(WorkflowToolProvider)
|
||||
existing_workflow_tool_provider = db.session.scalar(
|
||||
select(WorkflowToolProvider)
|
||||
.where(
|
||||
WorkflowToolProvider.tenant_id == tenant_id,
|
||||
WorkflowToolProvider.name == name,
|
||||
WorkflowToolProvider.id != workflow_tool_id,
|
||||
)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if existing_workflow_tool_provider is not None:
|
||||
raise ValueError(f"Tool with name {name} already exists")
|
||||
|
||||
workflow_tool_provider: WorkflowToolProvider | None = (
|
||||
db.session.query(WorkflowToolProvider)
|
||||
workflow_tool_provider: WorkflowToolProvider | None = db.session.scalar(
|
||||
select(WorkflowToolProvider)
|
||||
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if workflow_tool_provider is None:
|
||||
raise ValueError(f"Tool {workflow_tool_id} not found")
|
||||
|
||||
app: App | None = (
|
||||
db.session.query(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).first()
|
||||
app: App | None = db.session.scalar(
|
||||
select(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).limit(1)
|
||||
)
|
||||
|
||||
if app is None:
|
||||
@@ -234,9 +236,11 @@ class WorkflowToolManageService:
|
||||
:param tenant_id: the tenant id
|
||||
:param workflow_tool_id: the workflow tool id
|
||||
"""
|
||||
db.session.query(WorkflowToolProvider).where(
|
||||
WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id
|
||||
).delete()
|
||||
db.session.execute(
|
||||
delete(WorkflowToolProvider).where(
|
||||
WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id
|
||||
)
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@@ -251,10 +255,10 @@ class WorkflowToolManageService:
|
||||
:param workflow_tool_id: the workflow tool id
|
||||
:return: the tool
|
||||
"""
|
||||
db_tool: WorkflowToolProvider | None = (
|
||||
db.session.query(WorkflowToolProvider)
|
||||
db_tool: WorkflowToolProvider | None = db.session.scalar(
|
||||
select(WorkflowToolProvider)
|
||||
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
return cls._get_workflow_tool(tenant_id, db_tool)
|
||||
|
||||
@@ -267,10 +271,10 @@ class WorkflowToolManageService:
|
||||
:param workflow_app_id: the workflow app id
|
||||
:return: the tool
|
||||
"""
|
||||
db_tool: WorkflowToolProvider | None = (
|
||||
db.session.query(WorkflowToolProvider)
|
||||
db_tool: WorkflowToolProvider | None = db.session.scalar(
|
||||
select(WorkflowToolProvider)
|
||||
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.app_id == workflow_app_id)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
return cls._get_workflow_tool(tenant_id, db_tool)
|
||||
|
||||
@@ -284,8 +288,8 @@ class WorkflowToolManageService:
|
||||
if db_tool is None:
|
||||
raise ValueError("Tool not found")
|
||||
|
||||
workflow_app: App | None = (
|
||||
db.session.query(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).first()
|
||||
workflow_app: App | None = db.session.scalar(
|
||||
select(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).limit(1)
|
||||
)
|
||||
|
||||
if workflow_app is None:
|
||||
@@ -331,10 +335,10 @@ class WorkflowToolManageService:
|
||||
:param workflow_tool_id: the workflow tool id
|
||||
:return: the list of tools
|
||||
"""
|
||||
db_tool: WorkflowToolProvider | None = (
|
||||
db.session.query(WorkflowToolProvider)
|
||||
db_tool: WorkflowToolProvider | None = db.session.scalar(
|
||||
select(WorkflowToolProvider)
|
||||
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if db_tool is None:
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
import mimetypes
|
||||
import secrets
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from typing import Any
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import orjson
|
||||
from flask import request
|
||||
@@ -50,6 +50,14 @@ logger = logging.getLogger(__name__)
|
||||
_file_access_controller = DatabaseFileAccessController()
|
||||
|
||||
|
||||
class RawWebhookDataDict(TypedDict):
|
||||
method: str
|
||||
headers: dict[str, str]
|
||||
query_params: dict[str, str]
|
||||
body: dict[str, Any]
|
||||
files: dict[str, Any]
|
||||
|
||||
|
||||
class WebhookService:
|
||||
"""Service for handling webhook operations."""
|
||||
|
||||
@@ -145,7 +153,7 @@ class WebhookService:
|
||||
@classmethod
|
||||
def extract_and_validate_webhook_data(
|
||||
cls, webhook_trigger: WorkflowWebhookTrigger, node_config: NodeConfigDict
|
||||
) -> dict[str, Any]:
|
||||
) -> RawWebhookDataDict:
|
||||
"""Extract and validate webhook data in a single unified process.
|
||||
|
||||
Args:
|
||||
@@ -173,7 +181,7 @@ class WebhookService:
|
||||
return processed_data
|
||||
|
||||
@classmethod
|
||||
def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> dict[str, Any]:
|
||||
def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> RawWebhookDataDict:
|
||||
"""Extract raw data from incoming webhook request without type conversion.
|
||||
|
||||
Args:
|
||||
@@ -189,7 +197,7 @@ class WebhookService:
|
||||
"""
|
||||
cls._validate_content_length()
|
||||
|
||||
data = {
|
||||
data: RawWebhookDataDict = {
|
||||
"method": request.method,
|
||||
"headers": dict(request.headers),
|
||||
"query_params": dict(request.args),
|
||||
@@ -223,7 +231,7 @@ class WebhookService:
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _process_and_validate_data(cls, raw_data: dict[str, Any], node_data: WebhookData) -> dict[str, Any]:
|
||||
def _process_and_validate_data(cls, raw_data: RawWebhookDataDict, node_data: WebhookData) -> RawWebhookDataDict:
|
||||
"""Process and validate webhook data according to node configuration.
|
||||
|
||||
Args:
|
||||
@@ -664,7 +672,7 @@ class WebhookService:
|
||||
raise ValueError(f"Required header missing: {header_name}")
|
||||
|
||||
@classmethod
|
||||
def _validate_http_metadata(cls, webhook_data: dict[str, Any], node_data: WebhookData) -> dict[str, Any]:
|
||||
def _validate_http_metadata(cls, webhook_data: RawWebhookDataDict, node_data: WebhookData) -> dict[str, Any]:
|
||||
"""Validate HTTP method and content-type.
|
||||
|
||||
Args:
|
||||
@@ -729,7 +737,7 @@ class WebhookService:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def build_workflow_inputs(cls, webhook_data: dict[str, Any]) -> dict[str, Any]:
|
||||
def build_workflow_inputs(cls, webhook_data: RawWebhookDataDict) -> dict[str, Any]:
|
||||
"""Construct workflow inputs payload from webhook data.
|
||||
|
||||
Args:
|
||||
@@ -747,7 +755,7 @@ class WebhookService:
|
||||
|
||||
@classmethod
|
||||
def trigger_workflow_execution(
|
||||
cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: dict[str, Any], workflow: Workflow
|
||||
cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: RawWebhookDataDict, workflow: Workflow
|
||||
) -> None:
|
||||
"""Trigger workflow execution via AsyncWorkflowService.
|
||||
|
||||
|
||||
+266
-747
File diff suppressed because it is too large
Load Diff
@@ -401,10 +401,7 @@ class TestMetadataServiceCreateMetadata:
|
||||
metadata_args = MetadataTestDataFactory.create_metadata_args_mock(name="category", metadata_type="string")
|
||||
|
||||
# Mock query to return None (no existing metadata with same name)
|
||||
mock_query = Mock()
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db_session.query.return_value = mock_query
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
# Mock BuiltInField enum iteration
|
||||
with patch("services.metadata_service.BuiltInField") as mock_builtin:
|
||||
@@ -417,10 +414,6 @@ class TestMetadataServiceCreateMetadata:
|
||||
assert result is not None
|
||||
assert isinstance(result, DatasetMetadata)
|
||||
|
||||
# Verify query was made to check for duplicates
|
||||
mock_db_session.query.assert_called()
|
||||
mock_query.filter_by.assert_called()
|
||||
|
||||
# Verify metadata was added and committed
|
||||
mock_db_session.add.assert_called_once()
|
||||
mock_db_session.commit.assert_called_once()
|
||||
@@ -468,10 +461,7 @@ class TestMetadataServiceCreateMetadata:
|
||||
|
||||
# Mock existing metadata with same name
|
||||
existing_metadata = MetadataTestDataFactory.create_metadata_mock(name="category")
|
||||
mock_query = Mock()
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_metadata
|
||||
mock_db_session.query.return_value = mock_query
|
||||
mock_db_session.scalar.return_value = existing_metadata
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="Metadata name already exists"):
|
||||
@@ -500,10 +490,7 @@ class TestMetadataServiceCreateMetadata:
|
||||
)
|
||||
|
||||
# Mock query to return None (no duplicate in database)
|
||||
mock_query = Mock()
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db_session.query.return_value = mock_query
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
# Mock BuiltInField to include the conflicting name
|
||||
with patch("services.metadata_service.BuiltInField") as mock_builtin:
|
||||
@@ -597,27 +584,11 @@ class TestMetadataServiceUpdateMetadataName:
|
||||
|
||||
existing_metadata = MetadataTestDataFactory.create_metadata_mock(metadata_id=metadata_id, name="category")
|
||||
|
||||
# Mock query for duplicate check (no duplicate)
|
||||
mock_query = Mock()
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db_session.query.return_value = mock_query
|
||||
|
||||
# Mock metadata retrieval
|
||||
def query_side_effect(model):
|
||||
if model == DatasetMetadata:
|
||||
mock_meta_query = Mock()
|
||||
mock_meta_query.filter_by.return_value = mock_meta_query
|
||||
mock_meta_query.first.return_value = existing_metadata
|
||||
return mock_meta_query
|
||||
return mock_query
|
||||
|
||||
mock_db_session.query.side_effect = query_side_effect
|
||||
# Mock scalar calls: first for duplicate check (None), second for metadata retrieval
|
||||
mock_db_session.scalar.side_effect = [None, existing_metadata]
|
||||
|
||||
# Mock no metadata bindings (no documents to update)
|
||||
mock_binding_query = Mock()
|
||||
mock_binding_query.filter_by.return_value = mock_binding_query
|
||||
mock_binding_query.all.return_value = []
|
||||
mock_db_session.scalars.return_value.all.return_value = []
|
||||
|
||||
# Mock BuiltInField enum
|
||||
with patch("services.metadata_service.BuiltInField") as mock_builtin:
|
||||
@@ -655,22 +626,8 @@ class TestMetadataServiceUpdateMetadataName:
|
||||
metadata_id = "non-existent-metadata"
|
||||
new_name = "updated_category"
|
||||
|
||||
# Mock query for duplicate check (no duplicate)
|
||||
mock_query = Mock()
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db_session.query.return_value = mock_query
|
||||
|
||||
# Mock metadata retrieval to return None
|
||||
def query_side_effect(model):
|
||||
if model == DatasetMetadata:
|
||||
mock_meta_query = Mock()
|
||||
mock_meta_query.filter_by.return_value = mock_meta_query
|
||||
mock_meta_query.first.return_value = None # Not found
|
||||
return mock_meta_query
|
||||
return mock_query
|
||||
|
||||
mock_db_session.query.side_effect = query_side_effect
|
||||
# Mock scalar calls: first for duplicate check (None), second for metadata retrieval (None = not found)
|
||||
mock_db_session.scalar.side_effect = [None, None]
|
||||
|
||||
# Mock BuiltInField enum
|
||||
with patch("services.metadata_service.BuiltInField") as mock_builtin:
|
||||
@@ -746,15 +703,10 @@ class TestMetadataServiceDeleteMetadata:
|
||||
existing_metadata = MetadataTestDataFactory.create_metadata_mock(metadata_id=metadata_id, name="category")
|
||||
|
||||
# Mock metadata retrieval
|
||||
mock_query = Mock()
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_metadata
|
||||
mock_db_session.query.return_value = mock_query
|
||||
mock_db_session.scalar.return_value = existing_metadata
|
||||
|
||||
# Mock no metadata bindings (no documents to update)
|
||||
mock_binding_query = Mock()
|
||||
mock_binding_query.filter_by.return_value = mock_binding_query
|
||||
mock_binding_query.all.return_value = []
|
||||
mock_db_session.scalars.return_value.all.return_value = []
|
||||
|
||||
# Act
|
||||
result = MetadataService.delete_metadata(dataset_id, metadata_id)
|
||||
@@ -788,10 +740,7 @@ class TestMetadataServiceDeleteMetadata:
|
||||
metadata_id = "non-existent-metadata"
|
||||
|
||||
# Mock metadata retrieval to return None
|
||||
mock_query = Mock()
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db_session.query.return_value = mock_query
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="Metadata not found"):
|
||||
@@ -1013,10 +962,7 @@ class TestMetadataServiceGetDatasetMetadatas:
|
||||
)
|
||||
|
||||
# Mock usage count queries
|
||||
mock_query = Mock()
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.count.return_value = 5 # 5 documents use this metadata
|
||||
mock_db_session.query.return_value = mock_query
|
||||
mock_db_session.scalar.return_value = 5 # 5 documents use this metadata
|
||||
|
||||
# Act
|
||||
result = MetadataService.get_dataset_metadatas(dataset)
|
||||
|
||||
@@ -292,7 +292,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
|
||||
"""
|
||||
|
||||
api = Mock(spec=ExternalKnowledgeApis)
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = api
|
||||
mock_db_session.scalar.return_value = api
|
||||
|
||||
result = ExternalDatasetService.get_external_knowledge_api("api-id", "tenant-id")
|
||||
assert result is api
|
||||
@@ -302,7 +302,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
|
||||
When the record is absent, a ``ValueError`` is raised.
|
||||
"""
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="api template not found"):
|
||||
ExternalDatasetService.get_external_knowledge_api("missing-id", "tenant-id")
|
||||
@@ -320,7 +320,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
|
||||
existing_api = Mock(spec=ExternalKnowledgeApis)
|
||||
existing_api.settings_dict = {"api_key": "stored-key"}
|
||||
existing_api.settings = '{"api_key":"stored-key"}'
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_api
|
||||
mock_db_session.scalar.return_value = existing_api
|
||||
|
||||
args = {
|
||||
"name": "New Name",
|
||||
@@ -340,7 +340,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
|
||||
Updating a non‑existent API template should raise ``ValueError``.
|
||||
"""
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="api template not found"):
|
||||
ExternalDatasetService.update_external_knowledge_api(
|
||||
@@ -356,7 +356,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
|
||||
"""
|
||||
|
||||
api = Mock(spec=ExternalKnowledgeApis)
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = api
|
||||
mock_db_session.scalar.return_value = api
|
||||
|
||||
ExternalDatasetService.delete_external_knowledge_api("tenant-1", "api-1")
|
||||
|
||||
@@ -368,7 +368,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
|
||||
Deletion of a missing template should raise ``ValueError``.
|
||||
"""
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="api template not found"):
|
||||
ExternalDatasetService.delete_external_knowledge_api("tenant-1", "missing")
|
||||
@@ -394,7 +394,7 @@ class TestExternalDatasetServiceUsageAndBindings:
|
||||
When there are bindings, ``external_knowledge_api_use_check`` returns True and count.
|
||||
"""
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.count.return_value = 3
|
||||
mock_db_session.scalar.return_value = 3
|
||||
|
||||
in_use, count = ExternalDatasetService.external_knowledge_api_use_check("api-1")
|
||||
|
||||
@@ -406,7 +406,7 @@ class TestExternalDatasetServiceUsageAndBindings:
|
||||
Zero bindings should return ``(False, 0)``.
|
||||
"""
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.count.return_value = 0
|
||||
mock_db_session.scalar.return_value = 0
|
||||
|
||||
in_use, count = ExternalDatasetService.external_knowledge_api_use_check("api-1")
|
||||
|
||||
@@ -419,7 +419,7 @@ class TestExternalDatasetServiceUsageAndBindings:
|
||||
"""
|
||||
|
||||
binding = Mock(spec=ExternalKnowledgeBindings)
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = binding
|
||||
mock_db_session.scalar.return_value = binding
|
||||
|
||||
result = ExternalDatasetService.get_external_knowledge_binding_with_dataset_id("tenant-1", "ds-1")
|
||||
assert result is binding
|
||||
@@ -429,7 +429,7 @@ class TestExternalDatasetServiceUsageAndBindings:
|
||||
Missing binding should result in a ``ValueError``.
|
||||
"""
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="external knowledge binding not found"):
|
||||
ExternalDatasetService.get_external_knowledge_binding_with_dataset_id("tenant-1", "ds-1")
|
||||
@@ -460,7 +460,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate:
|
||||
'[{"document_process_setting":[{"name":"foo","required":true},{"name":"bar","required":false}]}]'
|
||||
)
|
||||
# Raw string; the service itself calls json.loads on it
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = external_api
|
||||
mock_db_session.scalar.return_value = external_api
|
||||
|
||||
process_parameter = {"foo": "value", "bar": "optional"}
|
||||
|
||||
@@ -474,7 +474,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate:
|
||||
When the referenced API template is missing, a ``ValueError`` is raised.
|
||||
"""
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="api template not found"):
|
||||
ExternalDatasetService.document_create_args_validate("tenant-1", "missing", {})
|
||||
@@ -488,7 +488,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate:
|
||||
external_api.settings = (
|
||||
'[{"document_process_setting":[{"name":"foo","required":true},{"name":"bar","required":false}]}]'
|
||||
)
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = external_api
|
||||
mock_db_session.scalar.return_value = external_api
|
||||
|
||||
process_parameter = {"bar": "present"} # missing "foo"
|
||||
|
||||
@@ -702,7 +702,7 @@ class TestExternalDatasetServiceCreateExternalDataset:
|
||||
}
|
||||
|
||||
# No existing dataset with same name.
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
|
||||
mock_db_session.scalar.side_effect = [
|
||||
None, # duplicate‑name check
|
||||
Mock(spec=ExternalKnowledgeApis), # external knowledge api
|
||||
]
|
||||
@@ -724,7 +724,7 @@ class TestExternalDatasetServiceCreateExternalDataset:
|
||||
"""
|
||||
|
||||
existing_dataset = Mock(spec=Dataset)
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_dataset
|
||||
mock_db_session.scalar.return_value = existing_dataset
|
||||
|
||||
args = {
|
||||
"name": "Existing",
|
||||
@@ -744,7 +744,7 @@ class TestExternalDatasetServiceCreateExternalDataset:
|
||||
"""
|
||||
|
||||
# First call: duplicate name check – not found.
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
|
||||
mock_db_session.scalar.side_effect = [
|
||||
None,
|
||||
None, # external knowledge api lookup
|
||||
]
|
||||
@@ -763,8 +763,10 @@ class TestExternalDatasetServiceCreateExternalDataset:
|
||||
``external_knowledge_id`` and ``external_knowledge_api_id`` are mandatory.
|
||||
"""
|
||||
|
||||
# duplicate name check
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
|
||||
# duplicate name check — two calls to create_external_dataset, each does 2 scalar calls
|
||||
mock_db_session.scalar.side_effect = [
|
||||
None,
|
||||
Mock(spec=ExternalKnowledgeApis),
|
||||
None,
|
||||
Mock(spec=ExternalKnowledgeApis),
|
||||
]
|
||||
@@ -826,7 +828,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval:
|
||||
api.settings = '{"endpoint":"https://example.com","api_key":"secret"}'
|
||||
|
||||
# First query: binding; second query: api.
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
|
||||
mock_db_session.scalar.side_effect = [
|
||||
binding,
|
||||
api,
|
||||
]
|
||||
@@ -861,7 +863,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval:
|
||||
Missing binding should raise ``ValueError``.
|
||||
"""
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
|
||||
mock_db_session.scalar.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="external knowledge binding not found"):
|
||||
ExternalDatasetService.fetch_external_knowledge_retrieval(
|
||||
@@ -878,7 +880,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval:
|
||||
"""
|
||||
|
||||
binding = ExternalDatasetTestDataFactory.create_external_binding()
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
|
||||
mock_db_session.scalar.side_effect = [
|
||||
binding,
|
||||
None,
|
||||
]
|
||||
@@ -901,7 +903,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval:
|
||||
api = Mock(spec=ExternalKnowledgeApis)
|
||||
api.settings = '{"endpoint":"https://example.com","api_key":"secret"}'
|
||||
|
||||
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
|
||||
mock_db_session.scalar.side_effect = [
|
||||
binding,
|
||||
api,
|
||||
]
|
||||
|
||||
@@ -117,9 +117,7 @@ def test_get_all_published_workflow_applies_limit_and_has_more(rag_pipeline_serv
|
||||
|
||||
|
||||
def test_get_pipeline_raises_when_dataset_not_found(mocker, rag_pipeline_service) -> None:
|
||||
first_query = mocker.Mock()
|
||||
first_query.where.return_value.first.return_value = None
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=first_query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
|
||||
|
||||
with pytest.raises(ValueError, match="Dataset not found"):
|
||||
rag_pipeline_service.get_pipeline("tenant-1", "dataset-1")
|
||||
@@ -131,12 +129,8 @@ def test_get_pipeline_raises_when_dataset_not_found(mocker, rag_pipeline_service
|
||||
def test_update_customized_pipeline_template_success(mocker) -> None:
|
||||
template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None)
|
||||
|
||||
# First query finds the template, second query (duplicate check) returns None
|
||||
query_mock_1 = mocker.Mock()
|
||||
query_mock_1.where.return_value.first.return_value = template
|
||||
query_mock_2 = mocker.Mock()
|
||||
query_mock_2.where.return_value.first.return_value = None
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", side_effect=[query_mock_1, query_mock_2])
|
||||
# First scalar finds the template, second scalar (duplicate check) returns None
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[template, None])
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit")
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
|
||||
|
||||
@@ -152,9 +146,7 @@ def test_update_customized_pipeline_template_success(mocker) -> None:
|
||||
|
||||
|
||||
def test_update_customized_pipeline_template_not_found(mocker) -> None:
|
||||
query_mock = mocker.Mock()
|
||||
query_mock.where.return_value.first.return_value = None
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
|
||||
|
||||
info = PipelineTemplateInfoEntity(name="x", description="d", icon_info=IconInfo(icon="i"))
|
||||
@@ -166,9 +158,7 @@ def test_update_customized_pipeline_template_duplicate_name(mocker) -> None:
|
||||
template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None)
|
||||
duplicate = SimpleNamespace(name="dup")
|
||||
|
||||
query_mock = mocker.Mock()
|
||||
query_mock.where.return_value.first.side_effect = [template, duplicate]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[template, duplicate])
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
|
||||
|
||||
info = PipelineTemplateInfoEntity(name="dup", description="d", icon_info=IconInfo(icon="i"))
|
||||
@@ -181,9 +171,7 @@ def test_update_customized_pipeline_template_duplicate_name(mocker) -> None:
|
||||
|
||||
def test_delete_customized_pipeline_template_success(mocker) -> None:
|
||||
template = SimpleNamespace(id="tpl-1")
|
||||
query_mock = mocker.Mock()
|
||||
query_mock.where.return_value.first.return_value = template
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template)
|
||||
delete_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.delete")
|
||||
commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit")
|
||||
|
||||
@@ -196,9 +184,7 @@ def test_delete_customized_pipeline_template_success(mocker) -> None:
|
||||
|
||||
|
||||
def test_delete_customized_pipeline_template_not_found(mocker) -> None:
|
||||
query_mock = mocker.Mock()
|
||||
query_mock.where.return_value.first.return_value = None
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
|
||||
|
||||
with pytest.raises(ValueError, match="Customized pipeline template not found"):
|
||||
@@ -397,18 +383,14 @@ def test_get_rag_pipeline_workflow_run_delegates(mocker, rag_pipeline_service) -
|
||||
|
||||
|
||||
def test_is_workflow_exist_returns_true_when_draft_exists(mocker, rag_pipeline_service) -> None:
|
||||
query_mock = mocker.Mock()
|
||||
query_mock.where.return_value.count.return_value = 1
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=1)
|
||||
|
||||
pipeline = SimpleNamespace(tenant_id="t1", id="p1")
|
||||
assert rag_pipeline_service.is_workflow_exist(pipeline) is True
|
||||
|
||||
|
||||
def test_is_workflow_exist_returns_false_when_no_draft(mocker, rag_pipeline_service) -> None:
|
||||
query_mock = mocker.Mock()
|
||||
query_mock.where.return_value.count.return_value = 0
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=0)
|
||||
|
||||
pipeline = SimpleNamespace(tenant_id="t1", id="p1")
|
||||
assert rag_pipeline_service.is_workflow_exist(pipeline) is False
|
||||
@@ -738,8 +720,7 @@ def test_get_second_step_parameters_success(mocker, rag_pipeline_service) -> Non
|
||||
|
||||
|
||||
def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_service) -> None:
|
||||
from models.dataset import Dataset, Pipeline, PipelineCustomizedTemplate
|
||||
from models.workflow import Workflow
|
||||
from models.dataset import Pipeline
|
||||
|
||||
# 1. Setup mocks
|
||||
pipeline = mocker.Mock(spec=Pipeline)
|
||||
@@ -754,36 +735,15 @@ def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_servi
|
||||
# Mock db itself to avoid app context errors
|
||||
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
|
||||
|
||||
# Improved mocking for session.query
|
||||
def mock_query_side_effect(model):
|
||||
m = mocker.Mock()
|
||||
if model == Pipeline:
|
||||
m.where.return_value.first.return_value = pipeline
|
||||
elif model == Workflow:
|
||||
m.where.return_value.first.return_value = workflow
|
||||
elif model == PipelineCustomizedTemplate:
|
||||
m.where.return_value.first.return_value = None
|
||||
elif model == Dataset:
|
||||
m.where.return_value.first.return_value = mocker.Mock()
|
||||
else:
|
||||
# For func.max cases
|
||||
m.where.return_value.scalar.return_value = 5
|
||||
m.where.return_value.first.return_value = mocker.Mock()
|
||||
return m
|
||||
|
||||
mock_db.session.query.side_effect = mock_query_side_effect
|
||||
# Mock get() for Pipeline and Workflow PK lookups
|
||||
mock_db.session.get.side_effect = [pipeline, workflow]
|
||||
# Mock scalar() for template name check (None) and max position (5)
|
||||
mock_db.session.scalar.side_effect = [None, 5]
|
||||
|
||||
# Mock retrieve_dataset
|
||||
dataset = mocker.Mock()
|
||||
pipeline.retrieve_dataset.return_value = dataset
|
||||
|
||||
# Mock max position
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.func.max", return_value=1)
|
||||
mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline.db.session.query.return_value.where.return_value.scalar",
|
||||
return_value=5,
|
||||
)
|
||||
|
||||
# Mock RagPipelineDslService
|
||||
mock_dsl_service = mocker.Mock()
|
||||
mock_dsl_service.export_rag_pipeline_dsl.return_value = {"dsl": "content"}
|
||||
@@ -839,9 +799,7 @@ def test_get_datasource_plugins_success(mocker, rag_pipeline_service) -> None:
|
||||
workflow.rag_pipeline_variables = []
|
||||
|
||||
# Mock queries
|
||||
mock_query = mocker.Mock()
|
||||
mock_query.where.return_value.first.side_effect = [dataset, pipeline]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=mock_query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
|
||||
|
||||
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow)
|
||||
|
||||
@@ -881,11 +839,9 @@ def test_retry_error_document_success(mocker, rag_pipeline_service) -> None:
|
||||
|
||||
workflow = mocker.Mock()
|
||||
|
||||
# Mock queries
|
||||
mock_query = mocker.Mock()
|
||||
# Log lookup, then Pipeline lookup
|
||||
mock_query.where.return_value.first.side_effect = [log, pipeline]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=mock_query)
|
||||
# Mock queries: Log lookup via scalar, Pipeline lookup via get
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=log)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline)
|
||||
|
||||
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow)
|
||||
|
||||
@@ -913,7 +869,7 @@ def test_set_datasource_variables_success(mocker, rag_pipeline_service) -> None:
|
||||
# Mock db aggressively
|
||||
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
|
||||
mock_db.engine = mocker.Mock()
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = mocker.Mock()
|
||||
mock_db.session.scalar.return_value = mocker.Mock()
|
||||
|
||||
pipeline = mocker.Mock(spec=Pipeline)
|
||||
pipeline.id = "p-1"
|
||||
@@ -976,7 +932,7 @@ def test_get_draft_workflow_success(mocker, rag_pipeline_service) -> None:
|
||||
workflow = mocker.Mock(spec=Workflow)
|
||||
|
||||
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = workflow
|
||||
mock_db.session.scalar.return_value = workflow
|
||||
|
||||
# 2. Run test
|
||||
result = rag_pipeline_service.get_draft_workflow(pipeline)
|
||||
@@ -998,7 +954,7 @@ def test_get_published_workflow_success(mocker, rag_pipeline_service) -> None:
|
||||
workflow = mocker.Mock(spec=Workflow)
|
||||
|
||||
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
|
||||
mock_db.session.query.return_value.where.return_value.first.return_value = workflow
|
||||
mock_db.session.scalar.return_value = workflow
|
||||
|
||||
# 2. Run test
|
||||
result = rag_pipeline_service.get_published_workflow(pipeline)
|
||||
@@ -1319,11 +1275,8 @@ def test_get_rag_pipeline_workflow_run_node_executions_returns_sorted_executions
|
||||
|
||||
|
||||
def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, rag_pipeline_service) -> None:
|
||||
query = mocker.Mock()
|
||||
query.where.return_value = query
|
||||
query.order_by.return_value.all.return_value = []
|
||||
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
|
||||
mock_db.session.query.return_value = query
|
||||
mock_db.session.scalars.return_value.all.return_value = []
|
||||
|
||||
result = rag_pipeline_service.get_recommended_plugins("all")
|
||||
|
||||
@@ -1336,11 +1289,8 @@ def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, ra
|
||||
def test_get_recommended_plugins_returns_installed_and_uninstalled(mocker, rag_pipeline_service) -> None:
|
||||
plugin_a = SimpleNamespace(plugin_id="plugin-a")
|
||||
plugin_b = SimpleNamespace(plugin_id="plugin-b")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value = query
|
||||
query.order_by.return_value.all.return_value = [plugin_a, plugin_b]
|
||||
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
|
||||
mock_db.session.query.return_value = query
|
||||
mock_db.session.scalars.return_value.all.return_value = [plugin_a, plugin_b]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
|
||||
mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools",
|
||||
@@ -1568,9 +1518,7 @@ def test_get_second_step_parameters_filters_first_step_variables(mocker, rag_pip
|
||||
|
||||
|
||||
def test_retry_error_document_raises_when_execution_log_not_found(mocker, rag_pipeline_service) -> None:
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.return_value = None
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
|
||||
|
||||
with pytest.raises(ValueError, match="Document pipeline execution log not found"):
|
||||
rag_pipeline_service.retry_error_document(
|
||||
@@ -1581,9 +1529,7 @@ def test_retry_error_document_raises_when_execution_log_not_found(mocker, rag_pi
|
||||
def test_get_datasource_plugins_raises_when_workflow_not_found(mocker, rag_pipeline_service) -> None:
|
||||
dataset = SimpleNamespace(pipeline_id="p1")
|
||||
pipeline = SimpleNamespace(id="p1", tenant_id="t1")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [dataset, pipeline]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
|
||||
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None)
|
||||
|
||||
with pytest.raises(ValueError, match="Pipeline or workflow not found"):
|
||||
@@ -1656,8 +1602,7 @@ def test_handle_node_run_result_marks_document_error_for_published_invoke(mocker
|
||||
|
||||
document = SimpleNamespace(indexing_status="waiting", error=None)
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.return_value = document
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=document)
|
||||
add_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.add")
|
||||
commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit")
|
||||
|
||||
@@ -1712,9 +1657,7 @@ def test_run_datasource_node_preview_raises_for_unsupported_provider(mocker, rag
|
||||
|
||||
|
||||
def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker, rag_pipeline_service) -> None:
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.return_value = None
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None)
|
||||
|
||||
with pytest.raises(ValueError, match="Pipeline not found"):
|
||||
rag_pipeline_service.publish_customized_pipeline_template("p1", {})
|
||||
@@ -1722,9 +1665,7 @@ def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker
|
||||
|
||||
def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(mocker, rag_pipeline_service) -> None:
|
||||
pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id=None)
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.return_value = pipeline
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline)
|
||||
|
||||
with pytest.raises(ValueError, match="Pipeline workflow not found"):
|
||||
rag_pipeline_service.publish_customized_pipeline_template("p1", {"name": "template-name"})
|
||||
@@ -1732,8 +1673,7 @@ def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(moc
|
||||
|
||||
def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None:
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.return_value = None
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
|
||||
|
||||
with pytest.raises(ValueError, match="Dataset not found"):
|
||||
rag_pipeline_service.get_pipeline("t1", "d1")
|
||||
@@ -1742,8 +1682,7 @@ def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service)
|
||||
def test_get_pipeline_raises_when_pipeline_missing(mocker, rag_pipeline_service) -> None:
|
||||
dataset = SimpleNamespace(pipeline_id="p1")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [dataset, None]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, None])
|
||||
|
||||
with pytest.raises(ValueError, match="Pipeline not found"):
|
||||
rag_pipeline_service.get_pipeline("t1", "d1")
|
||||
@@ -1783,8 +1722,7 @@ def test_get_pipeline_templates_builtin_en_us_no_fallback(mocker) -> None:
|
||||
def test_update_customized_pipeline_template_commits_when_name_empty(mocker) -> None:
|
||||
template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None)
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.return_value = template
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template)
|
||||
commit = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit")
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
|
||||
|
||||
@@ -2011,8 +1949,7 @@ def test_run_free_workflow_node_delegates_to_handle_result(mocker, rag_pipeline_
|
||||
def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None:
|
||||
pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [pipeline, None]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", side_effect=[pipeline, None])
|
||||
|
||||
with pytest.raises(ValueError, match="Workflow not found"):
|
||||
rag_pipeline_service.publish_customized_pipeline_template("p1", {})
|
||||
@@ -2021,11 +1958,9 @@ def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocke
|
||||
def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None:
|
||||
pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1")
|
||||
workflow = SimpleNamespace(id="wf-1")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [pipeline, workflow]
|
||||
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
|
||||
mock_db.engine = mocker.Mock()
|
||||
mock_db.session.query.return_value = query
|
||||
mock_db.session.get.side_effect = [pipeline, workflow]
|
||||
session_ctx = mocker.MagicMock()
|
||||
session_ctx.__enter__.return_value = SimpleNamespace()
|
||||
session_ctx.__exit__.return_value = False
|
||||
@@ -2038,11 +1973,8 @@ def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker
|
||||
|
||||
def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipeline_service) -> None:
|
||||
plugin = SimpleNamespace(plugin_id="plugin-a")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value = query
|
||||
query.order_by.return_value.all.return_value = [plugin]
|
||||
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
|
||||
mock_db.session.query.return_value = query
|
||||
mock_db.session.scalars.return_value.all.return_value = [plugin]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools", return_value=[])
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.marketplace.batch_fetch_plugin_by_ids", return_value=[])
|
||||
@@ -2056,8 +1988,8 @@ def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipelin
|
||||
def test_retry_error_document_raises_when_pipeline_missing(mocker, rag_pipeline_service) -> None:
|
||||
exec_log = SimpleNamespace(pipeline_id="p1")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [exec_log, None]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=exec_log)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None)
|
||||
|
||||
with pytest.raises(ValueError, match="Pipeline not found"):
|
||||
rag_pipeline_service.retry_error_document(
|
||||
@@ -2069,8 +2001,8 @@ def test_retry_error_document_raises_when_workflow_missing(mocker, rag_pipeline_
|
||||
exec_log = SimpleNamespace(pipeline_id="p1")
|
||||
pipeline = SimpleNamespace(id="p1")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [exec_log, pipeline]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=exec_log)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline)
|
||||
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None)
|
||||
|
||||
with pytest.raises(ValueError, match="Workflow not found"):
|
||||
@@ -2086,8 +2018,7 @@ def test_get_datasource_plugins_returns_empty_for_non_datasource_nodes(mocker, r
|
||||
graph_dict={"nodes": [{"id": "n1", "data": {"type": "start"}}]}, rag_pipeline_variables=[]
|
||||
)
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [dataset, pipeline]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
|
||||
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow)
|
||||
|
||||
assert rag_pipeline_service.get_datasource_plugins("t1", "d1", True) == []
|
||||
@@ -2250,8 +2181,7 @@ def test_get_datasource_plugins_handles_empty_datasource_data_and_non_published(
|
||||
rag_pipeline_variables=[{"variable": "v1", "belong_to_node_id": "shared"}],
|
||||
)
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [dataset, pipeline]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
|
||||
mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=workflow)
|
||||
mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials", return_value=[]
|
||||
@@ -2291,8 +2221,7 @@ def test_get_datasource_plugins_extracts_user_inputs_and_credentials(mocker, rag
|
||||
],
|
||||
)
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [dataset, pipeline]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
|
||||
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow)
|
||||
mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials",
|
||||
@@ -2310,8 +2239,7 @@ def test_get_pipeline_returns_pipeline_when_found(mocker, rag_pipeline_service)
|
||||
dataset = SimpleNamespace(pipeline_id="p1")
|
||||
pipeline = SimpleNamespace(id="p1")
|
||||
query = mocker.Mock()
|
||||
query.where.return_value.first.side_effect = [dataset, pipeline]
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
|
||||
|
||||
result = rag_pipeline_service.get_pipeline("t1", "d1")
|
||||
|
||||
|
||||
@@ -173,9 +173,7 @@ class TestAccountService:
|
||||
# Setup test data
|
||||
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
|
||||
|
||||
# Setup smart database query mock
|
||||
query_results = {("Account", "email", "test@example.com"): mock_account}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.scalar.return_value = mock_account
|
||||
|
||||
mock_password_dependencies["compare_password"].return_value = True
|
||||
|
||||
@@ -188,9 +186,7 @@ class TestAccountService:
|
||||
|
||||
def test_authenticate_account_not_found(self, mock_db_dependencies):
|
||||
"""Test authentication when account does not exist."""
|
||||
# Setup smart database query mock - no matching results
|
||||
query_results = {("Account", "email", "notfound@example.com"): None}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.scalar.return_value = None
|
||||
|
||||
# Execute test and verify exception
|
||||
self._assert_exception_raised(
|
||||
@@ -202,9 +198,7 @@ class TestAccountService:
|
||||
# Setup test data
|
||||
mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned")
|
||||
|
||||
# Setup smart database query mock
|
||||
query_results = {("Account", "email", "banned@example.com"): mock_account}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.scalar.return_value = mock_account
|
||||
|
||||
# Execute test and verify exception
|
||||
self._assert_exception_raised(AccountLoginError, AccountService.authenticate, "banned@example.com", "password")
|
||||
@@ -214,9 +208,7 @@ class TestAccountService:
|
||||
# Setup test data
|
||||
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
|
||||
|
||||
# Setup smart database query mock
|
||||
query_results = {("Account", "email", "test@example.com"): mock_account}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.scalar.return_value = mock_account
|
||||
|
||||
mock_password_dependencies["compare_password"].return_value = False
|
||||
|
||||
@@ -230,9 +222,7 @@ class TestAccountService:
|
||||
# Setup test data
|
||||
mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="pending")
|
||||
|
||||
# Setup smart database query mock
|
||||
query_results = {("Account", "email", "pending@example.com"): mock_account}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.scalar.return_value = mock_account
|
||||
|
||||
mock_password_dependencies["compare_password"].return_value = True
|
||||
|
||||
@@ -422,12 +412,8 @@ class TestAccountService:
|
||||
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
|
||||
mock_tenant_join = TestAccountAssociatedDataFactory.create_tenant_join_mock()
|
||||
|
||||
# Setup smart database query mock
|
||||
query_results = {
|
||||
("Account", "id", "user-123"): mock_account,
|
||||
("TenantAccountJoin", "account_id", "user-123"): mock_tenant_join,
|
||||
}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.get.return_value = mock_account
|
||||
mock_db_dependencies["db"].session.scalar.return_value = mock_tenant_join
|
||||
|
||||
# Mock datetime
|
||||
with patch("services.account_service.datetime") as mock_datetime:
|
||||
@@ -444,9 +430,7 @@ class TestAccountService:
|
||||
|
||||
def test_load_user_not_found(self, mock_db_dependencies):
|
||||
"""Test user loading when user does not exist."""
|
||||
# Setup smart database query mock - no matching results
|
||||
query_results = {("Account", "id", "non-existent-user"): None}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.get.return_value = None
|
||||
|
||||
# Execute test
|
||||
result = AccountService.load_user("non-existent-user")
|
||||
@@ -459,9 +443,7 @@ class TestAccountService:
|
||||
# Setup test data
|
||||
mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned")
|
||||
|
||||
# Setup smart database query mock
|
||||
query_results = {("Account", "id", "user-123"): mock_account}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.get.return_value = mock_account
|
||||
|
||||
# Execute test and verify exception
|
||||
self._assert_exception_raised(
|
||||
@@ -476,13 +458,9 @@ class TestAccountService:
|
||||
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
|
||||
mock_available_tenant = TestAccountAssociatedDataFactory.create_tenant_join_mock(current=False)
|
||||
|
||||
# Setup smart database query mock for complex scenario
|
||||
query_results = {
|
||||
("Account", "id", "user-123"): mock_account,
|
||||
("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant
|
||||
("TenantAccountJoin", "order_by", "first_available"): mock_available_tenant, # First available tenant
|
||||
}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.get.return_value = mock_account
|
||||
# First scalar: current tenant (None), second scalar: available tenant
|
||||
mock_db_dependencies["db"].session.scalar.side_effect = [None, mock_available_tenant]
|
||||
|
||||
# Mock datetime
|
||||
with patch("services.account_service.datetime") as mock_datetime:
|
||||
@@ -503,13 +481,9 @@ class TestAccountService:
|
||||
# Setup test data
|
||||
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
|
||||
|
||||
# Setup smart database query mock for no tenants scenario
|
||||
query_results = {
|
||||
("Account", "id", "user-123"): mock_account,
|
||||
("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant
|
||||
("TenantAccountJoin", "order_by", "first_available"): None, # No available tenants
|
||||
}
|
||||
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
|
||||
mock_db_dependencies["db"].session.get.return_value = mock_account
|
||||
# First scalar: current tenant (None), second scalar: available tenant (None)
|
||||
mock_db_dependencies["db"].session.scalar.side_effect = [None, None]
|
||||
|
||||
# Mock datetime
|
||||
with patch("services.account_service.datetime") as mock_datetime:
|
||||
|
||||
@@ -799,10 +799,7 @@ class TestExternalDatasetServiceGetAPI:
|
||||
api_id = "api-123"
|
||||
expected_api = factory.create_external_knowledge_api_mock(api_id=api_id)
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = expected_api
|
||||
mock_db.session.scalar.return_value = expected_api
|
||||
|
||||
# Act
|
||||
tenant_id = "tenant-123"
|
||||
@@ -810,16 +807,12 @@ class TestExternalDatasetServiceGetAPI:
|
||||
|
||||
# Assert
|
||||
assert result.id == api_id
|
||||
mock_query.filter_by.assert_called_once_with(id=api_id, tenant_id=tenant_id)
|
||||
|
||||
@patch("services.external_knowledge_service.db")
|
||||
def test_get_external_knowledge_api_not_found(self, mock_db, factory):
|
||||
"""Test error when API is not found."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db.session.scalar.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="api template not found"):
|
||||
@@ -848,10 +841,7 @@ class TestExternalDatasetServiceUpdateAPI:
|
||||
"settings": {"endpoint": "https://new.example.com", "api_key": "new-key"},
|
||||
}
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_api
|
||||
mock_db.session.scalar.return_value = existing_api
|
||||
|
||||
# Act
|
||||
result = ExternalDatasetService.update_external_knowledge_api(tenant_id, user_id, api_id, args)
|
||||
@@ -881,10 +871,7 @@ class TestExternalDatasetServiceUpdateAPI:
|
||||
"settings": {"endpoint": "https://api.example.com", "api_key": HIDDEN_VALUE},
|
||||
}
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_api
|
||||
mock_db.session.scalar.return_value = existing_api
|
||||
|
||||
# Act
|
||||
result = ExternalDatasetService.update_external_knowledge_api(tenant_id, "user-123", api_id, args)
|
||||
@@ -897,10 +884,7 @@ class TestExternalDatasetServiceUpdateAPI:
|
||||
def test_update_external_knowledge_api_not_found(self, mock_db, factory):
|
||||
"""Test error when API is not found."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db.session.scalar.return_value = None
|
||||
|
||||
args = {"name": "Updated API"}
|
||||
|
||||
@@ -912,10 +896,7 @@ class TestExternalDatasetServiceUpdateAPI:
|
||||
def test_update_external_knowledge_api_tenant_mismatch(self, mock_db, factory):
|
||||
"""Test error when tenant ID doesn't match."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db.session.scalar.return_value = None
|
||||
|
||||
args = {"name": "Updated API"}
|
||||
|
||||
@@ -934,10 +915,7 @@ class TestExternalDatasetServiceUpdateAPI:
|
||||
|
||||
args = {"name": "New Name Only"}
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_api
|
||||
mock_db.session.scalar.return_value = existing_api
|
||||
|
||||
# Act
|
||||
result = ExternalDatasetService.update_external_knowledge_api("tenant-123", "user-123", "api-123", args)
|
||||
@@ -958,10 +936,7 @@ class TestExternalDatasetServiceDeleteAPI:
|
||||
|
||||
existing_api = factory.create_external_knowledge_api_mock(api_id=api_id, tenant_id=tenant_id)
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_api
|
||||
mock_db.session.scalar.return_value = existing_api
|
||||
|
||||
# Act
|
||||
ExternalDatasetService.delete_external_knowledge_api(tenant_id, api_id)
|
||||
@@ -974,10 +949,7 @@ class TestExternalDatasetServiceDeleteAPI:
|
||||
def test_delete_external_knowledge_api_not_found(self, mock_db, factory):
|
||||
"""Test error when API is not found."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db.session.scalar.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="api template not found"):
|
||||
@@ -987,10 +959,7 @@ class TestExternalDatasetServiceDeleteAPI:
|
||||
def test_delete_external_knowledge_api_tenant_mismatch(self, mock_db, factory):
|
||||
"""Test error when tenant ID doesn't match."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db.session.scalar.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="api template not found"):
|
||||
@@ -1006,10 +975,7 @@ class TestExternalDatasetServiceAPIUseCheck:
|
||||
# Arrange
|
||||
api_id = "api-123"
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.count.return_value = 1
|
||||
mock_db.session.scalar.return_value = 1
|
||||
|
||||
# Act
|
||||
in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id)
|
||||
@@ -1024,10 +990,7 @@ class TestExternalDatasetServiceAPIUseCheck:
|
||||
# Arrange
|
||||
api_id = "api-123"
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.count.return_value = 10
|
||||
mock_db.session.scalar.return_value = 10
|
||||
|
||||
# Act
|
||||
in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id)
|
||||
@@ -1042,10 +1005,7 @@ class TestExternalDatasetServiceAPIUseCheck:
|
||||
# Arrange
|
||||
api_id = "api-123"
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.count.return_value = 0
|
||||
mock_db.session.scalar.return_value = 0
|
||||
|
||||
# Act
|
||||
in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id)
|
||||
@@ -1067,10 +1027,7 @@ class TestExternalDatasetServiceGetBinding:
|
||||
|
||||
expected_binding = factory.create_external_knowledge_binding_mock(tenant_id=tenant_id, dataset_id=dataset_id)
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = expected_binding
|
||||
mock_db.session.scalar.return_value = expected_binding
|
||||
|
||||
# Act
|
||||
result = ExternalDatasetService.get_external_knowledge_binding_with_dataset_id(tenant_id, dataset_id)
|
||||
@@ -1083,10 +1040,7 @@ class TestExternalDatasetServiceGetBinding:
|
||||
def test_get_external_knowledge_binding_not_found(self, mock_db, factory):
|
||||
"""Test error when binding is not found."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db.session.scalar.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="external knowledge binding not found"):
|
||||
@@ -1113,10 +1067,7 @@ class TestExternalDatasetServiceDocumentValidate:
|
||||
|
||||
api = factory.create_external_knowledge_api_mock(api_id=api_id, settings=[settings])
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = api
|
||||
mock_db.session.scalar.return_value = api
|
||||
|
||||
process_parameter = {"param1": "value1", "param2": "value2"}
|
||||
|
||||
@@ -1134,10 +1085,7 @@ class TestExternalDatasetServiceDocumentValidate:
|
||||
|
||||
api = factory.create_external_knowledge_api_mock(api_id=api_id, settings=[settings])
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = api
|
||||
mock_db.session.scalar.return_value = api
|
||||
|
||||
process_parameter = {}
|
||||
|
||||
@@ -1149,10 +1097,7 @@ class TestExternalDatasetServiceDocumentValidate:
|
||||
def test_document_create_args_validate_api_not_found(self, mock_db, factory):
|
||||
"""Test validation fails when API is not found."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db.session.scalar.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="api template not found"):
|
||||
@@ -1165,10 +1110,7 @@ class TestExternalDatasetServiceDocumentValidate:
|
||||
settings = {}
|
||||
api = factory.create_external_knowledge_api_mock(settings=[settings])
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = api
|
||||
mock_db.session.scalar.return_value = api
|
||||
|
||||
# Act & Assert - should not raise
|
||||
ExternalDatasetService.document_create_args_validate("tenant-123", "api-123", {})
|
||||
@@ -1186,10 +1128,7 @@ class TestExternalDatasetServiceDocumentValidate:
|
||||
|
||||
api = factory.create_external_knowledge_api_mock(settings=[settings])
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = api
|
||||
mock_db.session.scalar.return_value = api
|
||||
|
||||
process_parameter = {"required_param": "value"}
|
||||
|
||||
@@ -1498,24 +1437,7 @@ class TestExternalDatasetServiceCreateDataset:
|
||||
|
||||
api = factory.create_external_knowledge_api_mock(api_id="api-123")
|
||||
|
||||
# Mock database queries
|
||||
mock_dataset_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == Dataset:
|
||||
return mock_dataset_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_dataset_query.filter_by.return_value = mock_dataset_query
|
||||
mock_dataset_query.first.return_value = None
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [None, api]
|
||||
|
||||
# Act
|
||||
result = ExternalDatasetService.create_external_dataset(tenant_id, user_id, args)
|
||||
@@ -1534,10 +1456,7 @@ class TestExternalDatasetServiceCreateDataset:
|
||||
# Arrange
|
||||
existing_dataset = factory.create_dataset_mock(name="Duplicate Dataset")
|
||||
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = existing_dataset
|
||||
mock_db.session.scalar.return_value = existing_dataset
|
||||
|
||||
args = {"name": "Duplicate Dataset"}
|
||||
|
||||
@@ -1549,23 +1468,7 @@ class TestExternalDatasetServiceCreateDataset:
|
||||
def test_create_external_dataset_api_not_found_error(self, mock_db, factory):
|
||||
"""Test error when external knowledge API is not found."""
|
||||
# Arrange
|
||||
mock_dataset_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == Dataset:
|
||||
return mock_dataset_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_dataset_query.filter_by.return_value = mock_dataset_query
|
||||
mock_dataset_query.first.return_value = None
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = None
|
||||
mock_db.session.scalar.side_effect = [None, None]
|
||||
|
||||
args = {"name": "Test Dataset", "external_knowledge_api_id": "nonexistent-api"}
|
||||
|
||||
@@ -1579,23 +1482,7 @@ class TestExternalDatasetServiceCreateDataset:
|
||||
# Arrange
|
||||
api = factory.create_external_knowledge_api_mock()
|
||||
|
||||
mock_dataset_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == Dataset:
|
||||
return mock_dataset_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_dataset_query.filter_by.return_value = mock_dataset_query
|
||||
mock_dataset_query.first.return_value = None
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [None, api]
|
||||
|
||||
args = {"name": "Test Dataset", "external_knowledge_api_id": "api-123"}
|
||||
|
||||
@@ -1609,23 +1496,7 @@ class TestExternalDatasetServiceCreateDataset:
|
||||
# Arrange
|
||||
api = factory.create_external_knowledge_api_mock()
|
||||
|
||||
mock_dataset_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == Dataset:
|
||||
return mock_dataset_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_dataset_query.filter_by.return_value = mock_dataset_query
|
||||
mock_dataset_query.first.return_value = None
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [None, api]
|
||||
|
||||
args = {"name": "Test Dataset", "external_knowledge_id": "knowledge-123"}
|
||||
|
||||
@@ -1651,23 +1522,7 @@ class TestExternalDatasetServiceFetchRetrieval:
|
||||
)
|
||||
api = factory.create_external_knowledge_api_mock(api_id="api-123")
|
||||
|
||||
mock_binding_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == ExternalKnowledgeBindings:
|
||||
return mock_binding_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_binding_query.filter_by.return_value = mock_binding_query
|
||||
mock_binding_query.first.return_value = binding
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [binding, api]
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
@@ -1695,10 +1550,7 @@ class TestExternalDatasetServiceFetchRetrieval:
|
||||
def test_fetch_external_knowledge_retrieval_binding_not_found_error(self, mock_db, factory):
|
||||
"""Test error when external knowledge binding is not found."""
|
||||
# Arrange
|
||||
mock_query = MagicMock()
|
||||
mock_db.session.query.return_value = mock_query
|
||||
mock_query.filter_by.return_value = mock_query
|
||||
mock_query.first.return_value = None
|
||||
mock_db.session.scalar.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="external knowledge binding not found"):
|
||||
@@ -1712,23 +1564,7 @@ class TestExternalDatasetServiceFetchRetrieval:
|
||||
binding = factory.create_external_knowledge_binding_mock()
|
||||
api = factory.create_external_knowledge_api_mock()
|
||||
|
||||
mock_binding_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == ExternalKnowledgeBindings:
|
||||
return mock_binding_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_binding_query.filter_by.return_value = mock_binding_query
|
||||
mock_binding_query.first.return_value = binding
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [binding, api]
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
@@ -1751,23 +1587,7 @@ class TestExternalDatasetServiceFetchRetrieval:
|
||||
binding = factory.create_external_knowledge_binding_mock()
|
||||
api = factory.create_external_knowledge_api_mock()
|
||||
|
||||
mock_binding_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == ExternalKnowledgeBindings:
|
||||
return mock_binding_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_binding_query.filter_by.return_value = mock_binding_query
|
||||
mock_binding_query.first.return_value = binding
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [binding, api]
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
@@ -1799,23 +1619,7 @@ class TestExternalDatasetServiceFetchRetrieval:
|
||||
binding = factory.create_external_knowledge_binding_mock()
|
||||
api = factory.create_external_knowledge_api_mock()
|
||||
|
||||
mock_binding_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == ExternalKnowledgeBindings:
|
||||
return mock_binding_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_binding_query.filter_by.return_value = mock_binding_query
|
||||
mock_binding_query.first.return_value = binding
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [binding, api]
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 500
|
||||
@@ -1856,23 +1660,7 @@ class TestExternalDatasetServiceFetchRetrieval:
|
||||
)
|
||||
api = factory.create_external_knowledge_api_mock(api_id="api-123")
|
||||
|
||||
mock_binding_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == ExternalKnowledgeBindings:
|
||||
return mock_binding_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_binding_query.filter_by.return_value = mock_binding_query
|
||||
mock_binding_query.first.return_value = binding
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [binding, api]
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = status_code
|
||||
@@ -1891,23 +1679,7 @@ class TestExternalDatasetServiceFetchRetrieval:
|
||||
binding = factory.create_external_knowledge_binding_mock()
|
||||
api = factory.create_external_knowledge_api_mock()
|
||||
|
||||
mock_binding_query = MagicMock()
|
||||
mock_api_query = MagicMock()
|
||||
|
||||
def query_side_effect(model):
|
||||
if model == ExternalKnowledgeBindings:
|
||||
return mock_binding_query
|
||||
elif model == ExternalKnowledgeApis:
|
||||
return mock_api_query
|
||||
return MagicMock()
|
||||
|
||||
mock_db.session.query.side_effect = query_side_effect
|
||||
|
||||
mock_binding_query.filter_by.return_value = mock_binding_query
|
||||
mock_binding_query.first.return_value = binding
|
||||
|
||||
mock_api_query.filter_by.return_value = mock_api_query
|
||||
mock_api_query.first.return_value = api
|
||||
mock_db.session.scalar.side_effect = [binding, api]
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 503
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
node_modules/
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "@langgenius/dify-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0-private",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"exports": {
|
||||
"./context-menu": {
|
||||
"types": "./dist/context-menu/index.d.ts",
|
||||
"import": "./dist/context-menu/index.js",
|
||||
"default": "./dist/context-menu/index.js"
|
||||
},
|
||||
"./dropdown-menu": {
|
||||
"types": "./dist/dropdown-menu/index.d.ts",
|
||||
"import": "./dist/dropdown-menu/index.js",
|
||||
"default": "./dist/dropdown-menu/index.js"
|
||||
},
|
||||
"./tailwind-preset": {
|
||||
"types": "./dist/tailwind-preset.d.ts",
|
||||
"import": "./dist/tailwind-preset.js",
|
||||
"default": "./dist/tailwind-preset.js"
|
||||
},
|
||||
"./styles.css": "./dist/styles.css",
|
||||
"./markdown.css": "./dist/markdown.css",
|
||||
"./themes/light.css": "./dist/themes/light.css",
|
||||
"./themes/dark.css": "./dist/themes/dark.css",
|
||||
"./themes/manual-light.css": "./dist/themes/manual-light.css",
|
||||
"./themes/manual-dark.css": "./dist/themes/manual-dark.css",
|
||||
"./themes/markdown-light.css": "./dist/themes/markdown-light.css",
|
||||
"./themes/markdown-dark.css": "./dist/themes/markdown-dark.css",
|
||||
"./tokens/tailwind-theme-var-define": {
|
||||
"types": "./dist/tokens/tailwind-theme-var-define.d.ts",
|
||||
"import": "./dist/tokens/tailwind-theme-var-define.js",
|
||||
"default": "./dist/tokens/tailwind-theme-var-define.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ./scripts/build.mjs",
|
||||
"prepack": "pnpm build",
|
||||
"test": "vp test",
|
||||
"test:watch": "vp test --watch",
|
||||
"type-check": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
"@dify/iconify-collections": "workspace:*",
|
||||
"@egoist/tailwindcss-icons": "catalog:",
|
||||
"@iconify-json/heroicons": "catalog:",
|
||||
"@iconify-json/ri": "catalog:",
|
||||
"@remixicon/react": "catalog:",
|
||||
"@tailwindcss/typography": "catalog:",
|
||||
"clsx": "catalog:",
|
||||
"tailwind-merge": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/react": "catalog:",
|
||||
"@testing-library/jest-dom": "catalog:",
|
||||
"@testing-library/react": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@vitejs/plugin-react": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plus": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { cp, mkdir, rm } from 'node:fs/promises'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const distDir = resolve(packageRoot, 'dist')
|
||||
|
||||
await rm(distDir, { recursive: true, force: true })
|
||||
|
||||
const tsc = spawnSync('pnpm', ['exec', 'tsc', '-p', 'tsconfig.build.json'], {
|
||||
cwd: packageRoot,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
if (tsc.status !== 0)
|
||||
process.exit(tsc.status ?? 1)
|
||||
|
||||
await mkdir(distDir, { recursive: true })
|
||||
|
||||
await cp(resolve(packageRoot, 'src/styles.css'), resolve(packageRoot, 'dist/styles.css'))
|
||||
await cp(resolve(packageRoot, 'src/markdown.css'), resolve(packageRoot, 'dist/markdown.css'))
|
||||
await cp(resolve(packageRoot, 'src/styles'), resolve(packageRoot, 'dist/styles'), {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
await cp(resolve(packageRoot, 'src/themes'), resolve(packageRoot, 'dist/themes'), {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
+12
-6
@@ -1,4 +1,10 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiFileCopyLine,
|
||||
RiPencilLine,
|
||||
RiShareLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -17,7 +23,7 @@ import {
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
} from '.'
|
||||
} from './index'
|
||||
|
||||
const TriggerArea = ({ label = 'Right-click inside this area' }: { label?: string }) => (
|
||||
<ContextMenuTrigger
|
||||
@@ -185,17 +191,17 @@ export const Complex: Story = {
|
||||
<TriggerArea label="Right-click to inspect all menu capabilities" />
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem>
|
||||
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem>
|
||||
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Duplicate
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger>
|
||||
<span aria-hidden className="i-ri-share-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiShareLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Share
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
@@ -206,7 +212,7 @@ export const Complex: Story = {
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem destructive>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
|
||||
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
+23
-23
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import type { Placement } from '../internal/placement.js'
|
||||
import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu'
|
||||
import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { cn } from '../internal/cn.js'
|
||||
import {
|
||||
menuBackdropClassName,
|
||||
menuGroupLabelClassName,
|
||||
@@ -11,14 +13,11 @@ import {
|
||||
menuPopupBaseClassName,
|
||||
menuRowClassName,
|
||||
menuSeparatorClassName,
|
||||
} from '@/app/components/base/ui/menu-shared'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
import { cn } from '@/utils/classnames'
|
||||
} from '../internal/menu-shared.js'
|
||||
import { parsePlacement } from '../internal/placement.js'
|
||||
|
||||
export const ContextMenu = BaseContextMenu.Root
|
||||
export const ContextMenuTrigger = BaseContextMenu.Trigger
|
||||
export const ContextMenuPortal = BaseContextMenu.Portal
|
||||
export const ContextMenuBackdrop = BaseContextMenu.Backdrop
|
||||
export const ContextMenuSub = BaseContextMenu.SubmenuRoot
|
||||
export const ContextMenuGroup = BaseContextMenu.Group
|
||||
export const ContextMenuRadioGroup = BaseContextMenu.RadioGroup
|
||||
@@ -44,11 +43,11 @@ type ContextMenuPopupRenderProps = Required<Pick<ContextMenuContentProps, 'child
|
||||
placement: Placement
|
||||
sideOffset: number
|
||||
alignOffset: number
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
positionerProps?: ContextMenuContentProps['positionerProps']
|
||||
popupProps?: ContextMenuContentProps['popupProps']
|
||||
withBackdrop?: boolean
|
||||
className?: string | undefined
|
||||
popupClassName?: string | undefined
|
||||
positionerProps?: ContextMenuContentProps['positionerProps'] | undefined
|
||||
popupProps?: ContextMenuContentProps['popupProps'] | undefined
|
||||
withBackdrop?: boolean | undefined
|
||||
}
|
||||
|
||||
function renderContextMenuPopup({
|
||||
@@ -190,11 +189,10 @@ export function ContextMenuItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <span aria-hidden className="i-ri-check-line h-4 w-4" />}
|
||||
{children ?? <RiCheckLine aria-hidden className="h-4 w-4" />}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContextMenuCheckboxItemIndicator({
|
||||
className,
|
||||
...props
|
||||
@@ -204,7 +202,7 @@ export function ContextMenuCheckboxItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4" />
|
||||
<RiCheckLine aria-hidden className="h-4 w-4" />
|
||||
</BaseContextMenu.CheckboxItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -218,7 +216,7 @@ export function ContextMenuRadioItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4" />
|
||||
<RiCheckLine aria-hidden className="h-4 w-4" />
|
||||
</BaseContextMenu.RadioItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -239,20 +237,20 @@ export function ContextMenuSubTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span aria-hidden className="i-ri-arrow-right-s-line ml-auto size-4 shrink-0 text-text-tertiary" />
|
||||
<RiArrowRightSLine aria-hidden className="ml-auto size-4 shrink-0 text-text-tertiary" />
|
||||
</BaseContextMenu.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
type ContextMenuSubContentProps = {
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
positionerProps?: ContextMenuContentProps['positionerProps']
|
||||
popupProps?: ContextMenuContentProps['popupProps']
|
||||
placement?: Placement | undefined
|
||||
sideOffset?: number | undefined
|
||||
alignOffset?: number | undefined
|
||||
className?: string | undefined
|
||||
popupClassName?: string | undefined
|
||||
positionerProps?: ContextMenuContentProps['positionerProps'] | undefined
|
||||
popupProps?: ContextMenuContentProps['popupProps'] | undefined
|
||||
}
|
||||
|
||||
export function ContextMenuSubContent({
|
||||
@@ -300,3 +298,5 @@ export function ContextMenuSeparator({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type { Placement }
|
||||
+12
-13
@@ -1,7 +1,6 @@
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Link from '@/next/link'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -14,20 +13,20 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../index'
|
||||
|
||||
vi.mock('@/next/link', () => ({
|
||||
default: ({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
href: string
|
||||
children?: ReactNode
|
||||
} & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) => (
|
||||
function MockLink({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
href: string
|
||||
children?: ReactNode
|
||||
} & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) {
|
||||
return (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
describe('dropdown-menu wrapper', () => {
|
||||
describe('DropdownMenuContent', () => {
|
||||
@@ -301,7 +300,7 @@ describe('dropdown-menu wrapper', () => {
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLinkItem
|
||||
render={<Link href="/account" />}
|
||||
render={<MockLink href="/account" />}
|
||||
aria-label="account link"
|
||||
>
|
||||
Account settings
|
||||
+26
-15
@@ -1,4 +1,15 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import {
|
||||
RiArchiveLine,
|
||||
RiChat1Line,
|
||||
RiDeleteBinLine,
|
||||
RiFileCopyLine,
|
||||
RiLink,
|
||||
RiLockLine,
|
||||
RiMailLine,
|
||||
RiPencilLine,
|
||||
RiShareLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -17,7 +28,7 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '.'
|
||||
} from './index'
|
||||
|
||||
const TriggerButton = ({ label = 'Open Menu' }: { label?: string }) => (
|
||||
<DropdownMenuTrigger
|
||||
@@ -214,20 +225,20 @@ export const WithIcons: Story = {
|
||||
<TriggerButton />
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-archive-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiArchiveLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem destructive>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
|
||||
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -262,35 +273,35 @@ const ComplexDemo = () => {
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuGroupLabel>Edit</DropdownMenuGroupLabel>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled>
|
||||
<span aria-hidden className="i-ri-lock-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiLockLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Move to Workspace
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<span aria-hidden className="i-ri-share-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiShareLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Share
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-mail-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiMailLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-chat-1-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiChat1Line aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Slack
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-link size-4 shrink-0 text-text-tertiary" />
|
||||
<RiLink aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Copy Link
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
@@ -315,13 +326,13 @@ const ComplexDemo = () => {
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem checked={showArchived} onCheckedChange={setShowArchived}>
|
||||
<span aria-hidden className="i-ri-archive-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiArchiveLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Show Archived
|
||||
<DropdownMenuCheckboxItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem destructive>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
|
||||
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
+21
-19
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import type { Placement } from '../internal/placement.js'
|
||||
import { Menu } from '@base-ui/react/menu'
|
||||
import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { cn } from '../internal/cn.js'
|
||||
import {
|
||||
menuGroupLabelClassName,
|
||||
menuIndicatorClassName,
|
||||
@@ -10,12 +12,10 @@ import {
|
||||
menuPopupBaseClassName,
|
||||
menuRowClassName,
|
||||
menuSeparatorClassName,
|
||||
} from '@/app/components/base/ui/menu-shared'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
import { cn } from '@/utils/classnames'
|
||||
} from '../internal/menu-shared.js'
|
||||
import { parsePlacement } from '../internal/placement.js'
|
||||
|
||||
export const DropdownMenu = Menu.Root
|
||||
export const DropdownMenuPortal = Menu.Portal
|
||||
export const DropdownMenuTrigger = Menu.Trigger
|
||||
export const DropdownMenuSub = Menu.SubmenuRoot
|
||||
export const DropdownMenuGroup = Menu.Group
|
||||
@@ -42,7 +42,7 @@ export function DropdownMenuRadioItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4" />
|
||||
<RiCheckLine aria-hidden className="h-4 w-4" />
|
||||
</Menu.RadioItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export function DropdownMenuCheckboxItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4" />
|
||||
<RiCheckLine aria-hidden className="h-4 w-4" />
|
||||
</Menu.CheckboxItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -106,10 +106,10 @@ type DropdownMenuPopupRenderProps = Required<Pick<DropdownMenuContentProps, 'chi
|
||||
placement: Placement
|
||||
sideOffset: number
|
||||
alignOffset: number
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
positionerProps?: DropdownMenuContentProps['positionerProps']
|
||||
popupProps?: DropdownMenuContentProps['popupProps']
|
||||
className?: string | undefined
|
||||
popupClassName?: string | undefined
|
||||
positionerProps?: DropdownMenuContentProps['positionerProps'] | undefined
|
||||
popupProps?: DropdownMenuContentProps['popupProps'] | undefined
|
||||
}
|
||||
|
||||
function renderDropdownMenuPopup({
|
||||
@@ -187,20 +187,20 @@ export function DropdownMenuSubTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span aria-hidden className="i-ri-arrow-right-s-line ml-auto size-4 shrink-0 text-text-tertiary" />
|
||||
<RiArrowRightSLine aria-hidden className="ml-auto size-4 shrink-0 text-text-tertiary" />
|
||||
</Menu.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
type DropdownMenuSubContentProps = {
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
positionerProps?: DropdownMenuContentProps['positionerProps']
|
||||
popupProps?: DropdownMenuContentProps['popupProps']
|
||||
placement?: Placement | undefined
|
||||
sideOffset?: number | undefined
|
||||
alignOffset?: number | undefined
|
||||
className?: string | undefined
|
||||
popupClassName?: string | undefined
|
||||
positionerProps?: DropdownMenuContentProps['positionerProps'] | undefined
|
||||
popupProps?: DropdownMenuContentProps['popupProps'] | undefined
|
||||
}
|
||||
|
||||
export function DropdownMenuSubContent({
|
||||
@@ -272,3 +272,5 @@ export function DropdownMenuSeparator({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type { Placement }
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ClassValue } from 'clsx'
|
||||
import { clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
type Side = 'top' | 'bottom' | 'left' | 'right'
|
||||
type Align = 'start' | 'center' | 'end'
|
||||
|
||||
export type Placement
|
||||
= 'top'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'right'
|
||||
| 'right-start'
|
||||
| 'right-end'
|
||||
| 'bottom'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left'
|
||||
| 'left-start'
|
||||
| 'left-end'
|
||||
|
||||
export function parsePlacement(placement: Placement): { side: Side, align: Align } {
|
||||
const [side, align] = placement.split('-') as [Side, Align | undefined]
|
||||
|
||||
return {
|
||||
side,
|
||||
align: align ?? 'center',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@import './themes/markdown-light.css';
|
||||
@import './themes/markdown-dark.css';
|
||||
@@ -0,0 +1,7 @@
|
||||
@import './themes/light.css' layer(base);
|
||||
@import './themes/dark.css' layer(base);
|
||||
@import './themes/manual-light.css' layer(base);
|
||||
@import './themes/manual-dark.css' layer(base);
|
||||
@import './styles/tokens.css';
|
||||
|
||||
@source './**/*.{js,mjs}';
|
||||
@@ -0,0 +1,713 @@
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
@utility system-kbd {
|
||||
/* font define start */
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-regular-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-regular {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-medium {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-medium-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-semibold-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-regular-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-medium {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-medium-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-semibold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-semibold-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-medium {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-medium-uppercase {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-semibold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-semibold-uppercase {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-medium {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-semibold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-semibold-uppercase {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-regular {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-medium {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-semibold {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-xs-semibold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-sm-semibold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-md-semibold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-light {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-medium {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-light {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-medium {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-light {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-medium {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-light {
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-regular {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-medium {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-regular {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-medium {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-light {
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-light {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-regular {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-medium {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xs-semi-bold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xs-bold {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-sm-semi-bold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-sm-bold {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-md-semi-bold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-md-bold {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-lg-semi-bold {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-lg-bold {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xl-semi-bold {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xl-bold {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-2xl-semi-bold {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-2xl-bold {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-3xl-semi-bold {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-3xl-bold {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-4xl-semi-bold {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-4xl-bold {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-5xl-semi-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-5xl-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-6xl-semi-bold {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-6xl-bold {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-7xl-semi-bold {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-7xl-bold {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-8xl-semi-bold {
|
||||
font-size: 60px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-8xl-bold {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-2xs {
|
||||
/* font define end */
|
||||
|
||||
/* border radius start */
|
||||
border-radius: 2px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-xs {
|
||||
border-radius: 4px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-sm {
|
||||
border-radius: 6px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-md {
|
||||
border-radius: 8px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-lg {
|
||||
border-radius: 10px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-xl {
|
||||
border-radius: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-2xl {
|
||||
border-radius: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-3xl {
|
||||
border-radius: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-4xl {
|
||||
border-radius: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-5xl {
|
||||
border-radius: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-6xl {
|
||||
border-radius: 28px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-7xl {
|
||||
border-radius: 32px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-8xl {
|
||||
border-radius: 40px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-9xl {
|
||||
border-radius: 48px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-full {
|
||||
border-radius: 64px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility no-scrollbar {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@utility no-spinner {
|
||||
/* Hide arrows from number input */
|
||||
&::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { icons as customPublicIcons } from '@dify/iconify-collections/custom-public'
|
||||
import { icons as customVenderIcons } from '@dify/iconify-collections/custom-vender'
|
||||
import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons'
|
||||
import { icons as heroicons } from '@iconify-json/heroicons'
|
||||
import { icons as remixIcons } from '@iconify-json/ri'
|
||||
import { iconsPlugin } from '@egoist/tailwindcss-icons'
|
||||
import tailwindTypography from '@tailwindcss/typography'
|
||||
import tailwindThemeVarDefine from './themes/tailwind-theme-var-define'
|
||||
import tailwindThemeVarDefine from './tokens/tailwind-theme-var-define.js'
|
||||
import typography from './typography.js'
|
||||
|
||||
const config = {
|
||||
@@ -151,7 +153,8 @@ const config = {
|
||||
tailwindTypography,
|
||||
iconsPlugin({
|
||||
collections: {
|
||||
...getIconCollections(['heroicons', 'ri']),
|
||||
heroicons,
|
||||
ri: remixIcons,
|
||||
'custom-public': customPublicIcons,
|
||||
'custom-vender': customVenderIcons,
|
||||
},
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const typography: (helpers: { theme: (path: string) => unknown }) => Record<string, unknown>
|
||||
|
||||
export default typography
|
||||
@@ -0,0 +1,8 @@
|
||||
import difyUiTailwindPreset from './src/tailwind-preset'
|
||||
|
||||
const config = {
|
||||
content: [],
|
||||
...difyUiTailwindPreset,
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.js"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.stories.tsx",
|
||||
"src/**/__tests__/**"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"jsx": "react-jsx",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es2022"
|
||||
],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
"vitest/globals",
|
||||
"@testing-library/jest-dom"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.js",
|
||||
"scripts/**/*.mjs",
|
||||
"vite.config.ts",
|
||||
"vitest.setup.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite-plus'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
globals: true,
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { afterEach } from 'vitest'
|
||||
|
||||
if (typeof globalThis.ResizeObserver === 'undefined') {
|
||||
globalThis.ResizeObserver = class {
|
||||
observe() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
unobserve() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof globalThis.IntersectionObserver === 'undefined') {
|
||||
globalThis.IntersectionObserver = class {
|
||||
readonly root: Element | Document | null = null
|
||||
readonly rootMargin = ''
|
||||
readonly scrollMargin = ''
|
||||
readonly thresholds: ReadonlyArray<number> = []
|
||||
constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {}
|
||||
observe(_target: Element) {}
|
||||
unobserve(_target: Element) {}
|
||||
disconnect() {}
|
||||
takeRecords(): IntersectionObserverEntry[] {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
Generated
+1351
-1341
File diff suppressed because it is too large
Load Diff
+50
-47
@@ -1,5 +1,9 @@
|
||||
catalogMode: prefer
|
||||
trustPolicy: no-downgrade
|
||||
minimumReleaseAge: 2880
|
||||
trustPolicyExclude:
|
||||
- chokidar@4.0.3
|
||||
- reselect@5.1.1
|
||||
- semver@6.3.1
|
||||
blockExoticSubdeps: true
|
||||
strictDepBuilds: true
|
||||
allowBuilds:
|
||||
@@ -23,7 +27,7 @@ overrides:
|
||||
array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44
|
||||
array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44
|
||||
assert: npm:@nolyfill/assert@^1.0.26
|
||||
brace-expansion@<2.0.2: 2.0.2
|
||||
brace-expansion@>=2.0.0 <2.0.3: 2.0.3
|
||||
canvas: ^3.2.2
|
||||
devalue@<5.3.2: 5.3.2
|
||||
dompurify@>=3.1.3 <=3.3.1: 3.3.2
|
||||
@@ -37,6 +41,7 @@ overrides:
|
||||
is-generator-function: npm:@nolyfill/is-generator-function@^1.0.44
|
||||
is-typed-array: npm:@nolyfill/is-typed-array@^1.0.44
|
||||
isarray: npm:@nolyfill/isarray@^1.0.44
|
||||
lodash-es@>=4.0.0 <= 4.17.23: 4.18.0
|
||||
object.assign: npm:@nolyfill/object.assign@^1.0.44
|
||||
object.entries: npm:@nolyfill/object.entries@^1.0.44
|
||||
object.fromentries: npm:@nolyfill/object.fromentries@^1.0.44
|
||||
@@ -64,15 +69,15 @@ overrides:
|
||||
tar@<=7.5.10: 7.5.11
|
||||
typed-array-buffer: npm:@nolyfill/typed-array-buffer@^1.0.44
|
||||
undici@>=7.0.0 <7.24.0: 7.24.0
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.1.14
|
||||
vitest: npm:@voidzero-dev/vite-plus-test@0.1.14
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.1.15
|
||||
vitest: npm:@voidzero-dev/vite-plus-test@0.1.15
|
||||
which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44
|
||||
yaml@>=2.0.0 <2.8.3: 2.8.3
|
||||
yauzl@<3.2.1: 3.2.1
|
||||
catalog:
|
||||
"@amplitude/analytics-browser": 2.38.0
|
||||
"@amplitude/plugin-session-replay-browser": 1.27.5
|
||||
"@antfu/eslint-config": 7.7.3
|
||||
"@amplitude/analytics-browser": 2.38.1
|
||||
"@amplitude/plugin-session-replay-browser": 1.27.6
|
||||
"@antfu/eslint-config": 8.0.0
|
||||
"@base-ui/react": 1.3.0
|
||||
"@chromatic-com/storybook": 5.1.1
|
||||
"@cucumber/cucumber": 12.7.0
|
||||
@@ -84,7 +89,7 @@ catalog:
|
||||
"@formatjs/intl-localematcher": 0.8.2
|
||||
"@headlessui/react": 2.2.9
|
||||
"@heroicons/react": 2.2.0
|
||||
"@hono/node-server": 1.19.11
|
||||
"@hono/node-server": 1.19.12
|
||||
"@iconify-json/heroicons": 1.2.3
|
||||
"@iconify-json/ri": 1.2.10
|
||||
"@lexical/code": 0.42.0
|
||||
@@ -98,34 +103,34 @@ catalog:
|
||||
"@mdx-js/react": 3.1.1
|
||||
"@mdx-js/rollup": 3.1.1
|
||||
"@monaco-editor/react": 4.7.0
|
||||
"@next/eslint-plugin-next": 16.2.1
|
||||
"@next/mdx": 16.2.1
|
||||
"@next/eslint-plugin-next": 16.2.2
|
||||
"@next/mdx": 16.2.2
|
||||
"@orpc/client": 1.13.13
|
||||
"@orpc/contract": 1.13.13
|
||||
"@orpc/openapi-client": 1.13.13
|
||||
"@orpc/tanstack-query": 1.13.13
|
||||
"@playwright/test": 1.58.2
|
||||
"@playwright/test": 1.59.1
|
||||
"@remixicon/react": 4.9.0
|
||||
"@rgrove/parse-xml": 4.2.0
|
||||
"@sentry/react": 10.46.0
|
||||
"@storybook/addon-docs": 10.3.3
|
||||
"@storybook/addon-links": 10.3.3
|
||||
"@storybook/addon-onboarding": 10.3.3
|
||||
"@storybook/addon-themes": 10.3.3
|
||||
"@storybook/nextjs-vite": 10.3.3
|
||||
"@storybook/react": 10.3.3
|
||||
"@sentry/react": 10.47.0
|
||||
"@storybook/addon-docs": 10.3.4
|
||||
"@storybook/addon-links": 10.3.4
|
||||
"@storybook/addon-onboarding": 10.3.4
|
||||
"@storybook/addon-themes": 10.3.4
|
||||
"@storybook/nextjs-vite": 10.3.4
|
||||
"@storybook/react": 10.3.4
|
||||
"@streamdown/math": 1.0.2
|
||||
"@svgdotjs/svg.js": 3.2.5
|
||||
"@t3-oss/env-nextjs": 0.13.11
|
||||
"@tailwindcss/postcss": 4.2.2
|
||||
"@tailwindcss/typography": 0.5.19
|
||||
"@tailwindcss/vite": 4.2.2
|
||||
"@tanstack/eslint-plugin-query": 5.95.2
|
||||
"@tanstack/react-devtools": 0.10.0
|
||||
"@tanstack/react-form": 1.28.5
|
||||
"@tanstack/react-form-devtools": 0.2.19
|
||||
"@tanstack/react-query": 5.95.2
|
||||
"@tanstack/react-query-devtools": 5.95.2
|
||||
"@tanstack/eslint-plugin-query": 5.96.1
|
||||
"@tanstack/react-devtools": 0.10.1
|
||||
"@tanstack/react-form": 1.28.6
|
||||
"@tanstack/react-form-devtools": 0.2.20
|
||||
"@tanstack/react-query": 5.96.1
|
||||
"@tanstack/react-query-devtools": 5.96.1
|
||||
"@testing-library/dom": 10.4.1
|
||||
"@testing-library/jest-dom": 6.9.1
|
||||
"@testing-library/react": 16.3.2
|
||||
@@ -144,12 +149,12 @@ catalog:
|
||||
"@types/react-syntax-highlighter": 15.5.13
|
||||
"@types/react-window": 1.8.8
|
||||
"@types/sortablejs": 1.15.9
|
||||
"@typescript-eslint/eslint-plugin": 8.57.2
|
||||
"@typescript-eslint/parser": 8.57.2
|
||||
"@typescript/native-preview": 7.0.0-dev.20260329.1
|
||||
"@typescript-eslint/eslint-plugin": 8.58.0
|
||||
"@typescript-eslint/parser": 8.58.0
|
||||
"@typescript/native-preview": 7.0.0-dev.20260401.1
|
||||
"@vitejs/plugin-react": 6.0.1
|
||||
"@vitejs/plugin-rsc": 0.5.21
|
||||
"@vitest/coverage-v8": 4.1.1
|
||||
"@vitest/coverage-v8": 4.1.2
|
||||
abcjs: 6.6.2
|
||||
agentation: 3.0.2
|
||||
ahooks: 3.9.7
|
||||
@@ -157,7 +162,7 @@ catalog:
|
||||
class-variance-authority: 0.7.1
|
||||
clsx: 2.1.1
|
||||
cmdk: 1.1.1
|
||||
code-inspector-plugin: 1.4.5
|
||||
code-inspector-plugin: 1.5.0
|
||||
copy-to-clipboard: 3.3.3
|
||||
cron-parser: 5.5.0
|
||||
dayjs: 1.11.20
|
||||
@@ -174,19 +179,18 @@ catalog:
|
||||
eslint-markdown: 0.6.0
|
||||
eslint-plugin-better-tailwindcss: 4.3.2
|
||||
eslint-plugin-hyoban: 0.14.1
|
||||
eslint-plugin-markdown-preferences: 0.40.3
|
||||
eslint-plugin-markdown-preferences: 0.41.0
|
||||
eslint-plugin-no-barrel-files: 1.2.2
|
||||
eslint-plugin-react-hooks: 7.0.1
|
||||
eslint-plugin-react-refresh: 0.5.2
|
||||
eslint-plugin-sonarjs: 4.0.2
|
||||
eslint-plugin-storybook: 10.3.3
|
||||
eslint-plugin-storybook: 10.3.4
|
||||
fast-deep-equal: 3.1.3
|
||||
foxact: 0.3.0
|
||||
happy-dom: 20.8.9
|
||||
hono: 4.12.9
|
||||
hono: 4.12.10
|
||||
html-entities: 2.6.0
|
||||
html-to-image: 1.11.13
|
||||
i18next: 25.10.10
|
||||
i18next: 26.0.3
|
||||
i18next-resources-to-backend: 1.2.1
|
||||
iconify-import-svg: 0.1.2
|
||||
immer: 11.1.4
|
||||
@@ -196,15 +200,15 @@ catalog:
|
||||
js-yaml: 4.1.1
|
||||
jsonschema: 1.5.0
|
||||
katex: 0.16.44
|
||||
knip: 6.1.0
|
||||
knip: 6.2.0
|
||||
ky: 1.14.3
|
||||
lamejs: 1.2.1
|
||||
lexical: 0.42.0
|
||||
mermaid: 11.13.0
|
||||
mermaid: 11.14.0
|
||||
mime: 4.1.0
|
||||
mitt: 3.0.1
|
||||
negotiator: 1.0.0
|
||||
next: 16.2.1
|
||||
next: 16.2.2
|
||||
next-themes: 0.4.6
|
||||
nuqs: 2.8.9
|
||||
pinyin-pro: 3.28.0
|
||||
@@ -217,7 +221,7 @@ catalog:
|
||||
react-dom: 19.2.4
|
||||
react-easy-crop: 5.5.7
|
||||
react-hotkeys-hook: 5.2.4
|
||||
react-i18next: 16.6.6
|
||||
react-i18next: 17.0.2
|
||||
react-multi-email: 1.0.25
|
||||
react-papaparse: 4.4.0
|
||||
react-pdf-highlighter: 8.0.0-rc.0
|
||||
@@ -229,30 +233,29 @@ catalog:
|
||||
reactflow: 11.11.4
|
||||
remark-breaks: 4.0.0
|
||||
remark-directive: 4.0.0
|
||||
sass: 1.98.0
|
||||
scheduler: 0.27.0
|
||||
sharp: 0.34.5
|
||||
sortablejs: 1.15.7
|
||||
std-semver: 1.0.8
|
||||
storybook: 10.3.3
|
||||
storybook: 10.3.4
|
||||
streamdown: 2.5.0
|
||||
string-ts: 2.3.1
|
||||
tailwind-merge: 3.5.0
|
||||
tailwindcss: 4.2.2
|
||||
taze: 19.10.0
|
||||
taze: 19.11.0
|
||||
tldts: 7.0.27
|
||||
tsup: ^8.5.1
|
||||
tsdown: 0.21.7
|
||||
tsx: 4.21.0
|
||||
typescript: 5.9.3
|
||||
typescript: 6.0.2
|
||||
uglify-js: 3.19.3
|
||||
unist-util-visit: 5.1.0
|
||||
use-context-selector: 2.0.0
|
||||
uuid: 13.0.0
|
||||
vinext: 0.0.38
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.1.14
|
||||
vinext: 0.0.39
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.1.15
|
||||
vite-plugin-inspect: 12.0.0-beta.1
|
||||
vite-plus: 0.1.14
|
||||
vitest: npm:@voidzero-dev/vite-plus-test@0.1.14
|
||||
vite-plus: 0.1.15
|
||||
vitest: npm:@voidzero-dev/vite-plus-test@0.1.15
|
||||
vitest-canvas-mock: 1.1.4
|
||||
zod: 4.3.6
|
||||
zundo: 2.3.0
|
||||
|
||||
@@ -45,12 +45,12 @@
|
||||
"homepage": "https://dify.ai",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build": "vp pack",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"type-check": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test": "vp test",
|
||||
"test:coverage": "vp test --coverage",
|
||||
"publish:check": "./scripts/publish.sh --dry-run",
|
||||
"publish:npm": "./scripts/publish.sh"
|
||||
},
|
||||
@@ -61,8 +61,8 @@
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite-plus": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
outDir: "dist",
|
||||
});
|
||||
@@ -1,6 +1,17 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { defineConfig } from "vite-plus";
|
||||
|
||||
export default defineConfig({
|
||||
pack: {
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
// splitting: false,
|
||||
treeshake: true,
|
||||
outDir: "dist",
|
||||
target: false,
|
||||
},
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["**/*.test.ts"],
|
||||
@@ -6,10 +6,5 @@ export default defineConfig({
|
||||
'react-syntax-highlighter',
|
||||
'react-window',
|
||||
'@types/react-window',
|
||||
|
||||
// We can not upgrade these yet
|
||||
'typescript',
|
||||
],
|
||||
|
||||
maturityPeriod: 2,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { StorybookConfig } from '@storybook/nextjs-vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
stories: [
|
||||
'../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
'../../packages/dify-ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
// Not working with Storybook Vite framework
|
||||
// '@storybook/addon-onboarding',
|
||||
|
||||
@@ -35,7 +35,7 @@ const TagManagementModal = dynamic(() => import('@/app/components/base/tag-manag
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
appId: string
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
|
||||
export type ICardViewProps = {
|
||||
type ICardViewProps = {
|
||||
appId: string
|
||||
isInPanel?: boolean
|
||||
className?: string
|
||||
|
||||
@@ -27,7 +27,7 @@ const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [
|
||||
|
||||
const queryDateFormat = 'YYYY-MM-DD HH:mm'
|
||||
|
||||
export type IChartViewProps = {
|
||||
type IChartViewProps = {
|
||||
appId: string
|
||||
headerRight: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@reference "../../../../styles/globals.css";
|
||||
|
||||
.app {
|
||||
flex-grow: 1;
|
||||
height: 0;
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
@reference "../../../../../styles/globals.css";
|
||||
|
||||
.logTable td {
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import { usePathname } from '@/next/navigation'
|
||||
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
datasetId: string
|
||||
}
|
||||
|
||||
@@ -13,10 +13,6 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useLogout, useUserProfile } from '@/service/use-common'
|
||||
|
||||
export type IAppSelector = {
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
export default function AppSelector() {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -5,7 +5,7 @@ import AppInfoModals from './app-info-modals'
|
||||
import AppInfoTrigger from './app-info-trigger'
|
||||
import { useAppInfoActions } from './use-app-info-actions'
|
||||
|
||||
export type IAppInfoProps = {
|
||||
type IAppInfoProps = {
|
||||
expand: boolean
|
||||
onlyShowDetail?: boolean
|
||||
openState?: boolean
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import AppIcon from '../base/app-icon'
|
||||
|
||||
export type IAppBasicProps = {
|
||||
type IAppBasicProps = {
|
||||
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
|
||||
icon?: string
|
||||
icon_background?: string | null
|
||||
|
||||
@@ -17,7 +17,7 @@ import DatasetSidebarDropdown from './dataset-sidebar-dropdown'
|
||||
import NavLink from './nav-link'
|
||||
import ToggleButton from './toggle-button'
|
||||
|
||||
export type IAppDetailNavProps = {
|
||||
type IAppDetailNavProps = {
|
||||
iconType?: 'app' | 'dataset'
|
||||
navigation: Array<{
|
||||
name: string
|
||||
|
||||
@@ -39,7 +39,5 @@ export enum AnnotationEnableStatus {
|
||||
}
|
||||
|
||||
export enum JobStatus {
|
||||
waiting = 'waiting',
|
||||
processing = 'processing',
|
||||
completed = 'completed',
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { HTMLProps, PropsWithChildren } from 'react'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement> & {
|
||||
type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement> & {
|
||||
icon?: React.ReactNode
|
||||
link?: string
|
||||
disabled?: boolean
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { FC, ReactNode } from 'react'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type IFeaturePanelProps = {
|
||||
type IFeaturePanelProps = {
|
||||
className?: string
|
||||
headerIcon?: ReactNode
|
||||
title: ReactNode
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
|
||||
export type IGroupNameProps = {
|
||||
type IGroupNameProps = {
|
||||
name: string
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type IOperationBtnProps = {
|
||||
type IOperationBtnProps = {
|
||||
className?: string
|
||||
type: 'add' | 'edit'
|
||||
actionName?: string
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export type IVarHighlightProps = {
|
||||
type IVarHighlightProps = {
|
||||
name: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@reference "../../../../../styles/globals.css";
|
||||
|
||||
.item {
|
||||
background-color: rgba(21, 94, 239, 0.05);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import WarningMask from '.'
|
||||
|
||||
export type IFormattingChangedProps = {
|
||||
type IFormattingChangedProps = {
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import WarningMask from '.'
|
||||
|
||||
export type IFormattingChangedProps = {
|
||||
type IFormattingChangedProps = {
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type IHasNotSetAPIProps = {
|
||||
type IHasNotSetAPIProps = {
|
||||
onSetting: () => void
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export type IWarningMaskProps = {
|
||||
type IWarningMaskProps = {
|
||||
title: string
|
||||
description: string
|
||||
footer: React.ReactNode
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@reference "../../../../../styles/globals.css";
|
||||
|
||||
.mask {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import VarHighlight from '../../base/var-highlight'
|
||||
|
||||
export type IConfirmAddVarProps = {
|
||||
type IConfirmAddVarProps = {
|
||||
varNameArr: string[]
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
|
||||
@@ -33,7 +33,7 @@ import { getNewVar, getVars } from '@/utils/var'
|
||||
import ConfirmAddVar from './confirm-add-var'
|
||||
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
|
||||
|
||||
export type ISimplePromptInput = {
|
||||
type ISimplePromptInput = {
|
||||
mode: AppModeEnum
|
||||
promptTemplate: string
|
||||
promptVariables: PromptVariable[]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@reference "../../../../styles/globals.css";
|
||||
|
||||
.gradientBorder {
|
||||
background: radial-gradient(circle at 100% 100%, #fcfcfd 0, #fcfcfd 10px, transparent 10px) 0% 0%/12px 12px no-repeat,
|
||||
radial-gradient(circle at 0 100%, #fcfcfd 0, #fcfcfd 10px, transparent 10px) 100% 0%/12px 12px no-repeat,
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
export const jsonObjectWrap = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: true,
|
||||
}
|
||||
|
||||
export const jsonConfigPlaceHolder = JSON.stringify(
|
||||
{
|
||||
type: 'object',
|
||||
|
||||
@@ -52,7 +52,7 @@ const normalizeSelectDefaultValue = (inputVar: InputVar) => {
|
||||
return inputVar
|
||||
}
|
||||
|
||||
export type IConfigModalProps = {
|
||||
type IConfigModalProps = {
|
||||
isCreate?: boolean
|
||||
payload?: InputVar
|
||||
isShow: boolean
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ReactSortable } from 'react-sortablejs'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type Options = string[]
|
||||
export type IConfigSelectProps = {
|
||||
type IConfigSelectProps = {
|
||||
options: Options
|
||||
onChange: (options: Options) => void
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type IModalFootProps = {
|
||||
type IModalFootProps = {
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type ISelectTypeItemProps = {
|
||||
type ISelectTypeItemProps = {
|
||||
type: InputVarType
|
||||
selected: boolean
|
||||
onClick: () => void
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import s from '@/app/components/app/configuration/config-prompt/style.module.css'
|
||||
import {
|
||||
Copy,
|
||||
CopyCheck,
|
||||
} from '@/app/components/base/icons/src/vender/line/files'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
type: 'first-prompt' | 'next-iteration'
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const Editor: FC<Props> = ({
|
||||
className,
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
const {
|
||||
modelConfig,
|
||||
hasSetBlockStatus,
|
||||
dataSets,
|
||||
showSelectDataSet,
|
||||
externalDataToolsConfig,
|
||||
setExternalDataToolsConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
const { setShowExternalDataToolModal } = useModalContext()
|
||||
const isFirstPrompt = type === 'first-prompt'
|
||||
const editorHeight = isFirstPrompt ? 'h-[336px]' : 'h-[52px]'
|
||||
|
||||
const handleOpenExternalDataToolModal = () => {
|
||||
setShowExternalDataToolModal({
|
||||
payload: {},
|
||||
onSaveCallback: (newExternalDataTool?: ExternalDataTool) => {
|
||||
if (!newExternalDataTool)
|
||||
return
|
||||
setExternalDataToolsConfig([...externalDataToolsConfig, newExternalDataTool])
|
||||
},
|
||||
onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
|
||||
for (let i = 0; i < promptVariables.length; i++) {
|
||||
if (promptVariables[i].key === newExternalDataTool.variable) {
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < externalDataToolsConfig.length; i++) {
|
||||
if (externalDataToolsConfig[i].variable === newExternalDataTool.variable) {
|
||||
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: externalDataToolsConfig[i].variable }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className={cn(className, s.gradientBorder, 'relative')}>
|
||||
<div className="rounded-xl bg-white">
|
||||
<div className={cn(s.boxHeader, 'flex h-11 items-center justify-between rounded-tl-xl rounded-tr-xl bg-white pb-1 pl-4 pr-3 pt-2 hover:shadow-xs')}>
|
||||
<div className="text-sm font-semibold uppercase text-indigo-800">{t(`agent.${isFirstPrompt ? 'firstPrompt' : 'nextIteration'}`, { ns: 'appDebug' })}</div>
|
||||
<div className={cn(s.optionWrap, 'items-center space-x-1')}>
|
||||
{!isCopied
|
||||
? (
|
||||
<Copy
|
||||
className="h-6 w-6 cursor-pointer p-1 text-gray-500"
|
||||
onClick={() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<CopyCheck className="h-6 w-6 p-1 text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(editorHeight, 'min-h-[102px] overflow-y-auto px-4 text-sm text-gray-700')}>
|
||||
<PromptEditor
|
||||
className={editorHeight}
|
||||
value={value}
|
||||
contextBlock={{
|
||||
show: true,
|
||||
selectable: !hasSetBlockStatus.context,
|
||||
datasets: dataSets.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
type: item.data_source_type,
|
||||
})),
|
||||
onAddContext: showSelectDataSet,
|
||||
}}
|
||||
variableBlock={{
|
||||
show: true,
|
||||
variables: modelConfig.configs.prompt_variables.filter(item => item.key && item.key.trim() && item.name && item.name.trim()).map(item => ({
|
||||
name: item.name,
|
||||
value: item.key,
|
||||
})),
|
||||
}}
|
||||
externalToolBlock={{
|
||||
show: true,
|
||||
externalTools: externalDataToolsConfig.map(item => ({
|
||||
name: item.label!,
|
||||
variableName: item.variable!,
|
||||
icon: item.icon,
|
||||
icon_background: item.icon_background,
|
||||
})),
|
||||
onAddExternalTool: handleOpenExternalDataToolModal,
|
||||
}}
|
||||
historyBlock={{
|
||||
show: false,
|
||||
selectable: false,
|
||||
history: {
|
||||
user: '',
|
||||
assistant: '',
|
||||
},
|
||||
onEditRole: noop,
|
||||
}}
|
||||
queryBlock={{
|
||||
show: false,
|
||||
selectable: false,
|
||||
}}
|
||||
onChange={onChange}
|
||||
onBlur={noop}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex pb-2 pl-4">
|
||||
<div className="h-[18px] rounded-md bg-gray-100 px-1 text-xs leading-[18px] text-gray-500">{value.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Editor)
|
||||
@@ -7,7 +7,7 @@ import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type IAutomaticBtnProps = {
|
||||
type IAutomaticBtnProps = {
|
||||
onClick: () => void
|
||||
}
|
||||
const AutomaticBtn: FC<IAutomaticBtnProps> = ({
|
||||
|
||||
@@ -43,7 +43,7 @@ import useGenData from './use-gen-data'
|
||||
|
||||
const i18nPrefix = 'generate'
|
||||
|
||||
export type IGetAutomaticResProps = {
|
||||
type IGetAutomaticResProps = {
|
||||
mode: AppModeEnum
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@reference "../../../../../styles/globals.css";
|
||||
|
||||
.textGradient {
|
||||
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
|
||||
-webkit-background-clip: text;
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ import { GeneratorType } from '../automatic/types'
|
||||
import useGenData from '../automatic/use-gen-data'
|
||||
|
||||
const i18nPrefix = 'generate'
|
||||
export type IGetCodeGeneratorResProps = {
|
||||
type IGetCodeGeneratorResProps = {
|
||||
flowId: string
|
||||
nodeId: string
|
||||
currentCode?: string
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import s from './style.module.css'
|
||||
|
||||
export type IContrlBtnGroupProps = {
|
||||
type IContrlBtnGroupProps = {
|
||||
onSave: () => void
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@reference "../../../../styles/globals.css";
|
||||
|
||||
.ctrlBtn {
|
||||
left: -16px;
|
||||
right: -16px;
|
||||
|
||||
@@ -17,7 +17,7 @@ import Link from '@/next/link'
|
||||
import { useInfiniteDatasets } from '@/service/knowledge/use-dataset'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type ISelectDataSetProps = {
|
||||
type ISelectDataSetProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
selectedIds: string[]
|
||||
|
||||
@@ -18,5 +18,3 @@ export const DebugWithMultipleModelContext = createContext<DebugWithMultipleMode
|
||||
})
|
||||
|
||||
export const useDebugWithMultipleModelContext = () => useContext(DebugWithMultipleModelContext)
|
||||
|
||||
export default DebugWithMultipleModelContext
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
@reference "../../../styles/globals.css";
|
||||
|
||||
.advancedPromptMode {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.advancedPromptMode::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -1px;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: rgba(68, 76, 231, 0.18);
|
||||
transform: skewX(-30deg);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user