diff --git a/api/controllers/common/fields.py b/api/controllers/common/fields.py index 7cc0dabc5a8e5d..590e45eccad028 100644 --- a/api/controllers/common/fields.py +++ b/api/controllers/common/fields.py @@ -8,7 +8,7 @@ from graphon.file import helpers as file_helpers from models.model import IconType -type JSONValue = str | int | float | bool | None | dict[str, Any] | list[Any] +type ShallowJSONValue = str | int | float | bool | None | dict[str, Any] | list[Any] type JSONObject = dict[str, Any] URLString = Annotated[str, WithJsonSchema({"format": "url", "type": "string"})] UUIDString = Annotated[str, WithJsonSchema({"format": "uuid", "type": "string"})] @@ -335,8 +335,8 @@ class SimpleResultResponse(ResponseModel): result: str = Field(description="Operation result.") -class GeneratedAppResponse(RootModel[JSONValue]): - root: JSONValue +class GeneratedAppResponse(RootModel[ShallowJSONValue]): + root: ShallowJSONValue class EventStreamResponse(RootModel[str]): @@ -360,7 +360,7 @@ class AudioBinaryResponse(RootModel[bytes]): class AudioTranscriptResponse(ResponseModel): - text: str + text: str = Field(description="Transcribed text from audio") class ValidationResultResponse(ResponseModel): diff --git a/api/controllers/common/schema.py b/api/controllers/common/schema.py index 0fb2f3c5884334..726b35d663d968 100644 --- a/api/controllers/common/schema.py +++ b/api/controllers/common/schema.py @@ -6,13 +6,16 @@ `register_schema_models` emit resolvable OpenAPI 3 references. """ +import re from collections.abc import Iterable, Mapping from enum import StrEnum -from typing import Any, Literal, NotRequired, Protocol, TypedDict +from typing import Any, Literal, NotRequired, Protocol, TypedDict, override +from weakref import WeakKeyDictionary from flask import request from flask_restx import Namespace from pydantic import BaseModel, TypeAdapter +from pydantic.json_schema import CoreModeRef, DefsRef, GenerateJsonSchema DEFAULT_REF_TEMPLATE_OPENAPI_3_0 = "#/components/schemas/{model}" @@ -51,14 +54,97 @@ def to_dict(self, flat: bool = True) -> dict[str, str]: ... def getlist(self, key: str) -> list[str]: ... -def _register_json_schema(namespace: Namespace, name: str, schema: dict) -> None: - """Register a JSON schema and promote any nested Pydantic `$defs`.""" +class SchemaNameCollisionError(ValueError): + """Raised when two different schemas would be published under the same OpenAPI component name.""" + + +# Pydantic names `$defs` after the bare class name and only qualifies them when two +# same-named types meet inside one schema, so the same type can surface under different +# names depending on which model pulled it in. Types that share a class name with +# another documented type get a fixed, unique component name here instead. +SCHEMA_NAME_OVERRIDES: Mapping[str, str] = { + "graphon.model_runtime.entities.common_entities.I18nObject": "ModelRuntimeI18nObject", + "services.knowledge.entities.knowledge_entities.FileInfo": "KnowledgeFileInfo", + "core.plugin.entities.plugin.PluginDependency.Github": "PluginDependencyGithub", + "core.plugin.entities.plugin.PluginDependency.Marketplace": "PluginDependencyMarketplace", + "core.plugin.entities.plugin.PluginDependency.Package": "PluginDependencyPackage", + "core.plugin.entities.bundle.PluginBundleDependency.Github": "PluginBundleDependencyGithub", + "core.plugin.entities.bundle.PluginBundleDependency.Marketplace": "PluginBundleDependencyMarketplace", + "core.plugin.entities.bundle.PluginBundleDependency.Package": "PluginBundleDependencyPackage", + "core.app.entities.task_entities.WorkflowAppBlockingResponse.Data": "WorkflowAppBlockingResponseData", +} + +_REF_ID_SUFFIX = re.compile(r":\d+(?=$|[\[\],])") +_MODULE_PREFIX = re.compile(r"(?:[^.[\],]+\.)+([^.[\],]+)") + +# Normalized component name -> (component name, fully qualified source type, schema), per namespace. +_registered_schemas: WeakKeyDictionary[Namespace, dict[str, tuple[str, str, dict]]] = WeakKeyDictionary() +_NON_ALPHANUMERIC = re.compile(r"[^a-z0-9]") + + +def _normalized_schema_name(name: str) -> str: + """Fold names that client code generators turn into the same identifier (`JSONValue` / `JsonValue`).""" + + return _NON_ALPHANUMERIC.sub("", name.lower()) + + +class _OpenApiJsonSchemaGenerator(GenerateJsonSchema): + """Emit stable component names and remember which type produced each of them.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.schema_sources: dict[str, str] = {} + + @override + def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef: + core_ref, _mode = core_mode_ref + source = _REF_ID_SUFFIX.sub("", core_ref) + name = SCHEMA_NAME_OVERRIDES.get(source) or self.normalize_name(_MODULE_PREFIX.sub(r"\1", source)) + existing_source = self.schema_sources.setdefault(name, source) + if existing_source != source: + raise SchemaNameCollisionError( + f"OpenAPI schema name {name!r} is produced by both {existing_source} and {source}; " + "rename one of them or add it to SCHEMA_NAME_OVERRIDES." + ) + defs_ref = DefsRef(name) + self._prioritized_defsref_choices[defs_ref] = [defs_ref] + return defs_ref + + +def _register_json_schema(namespace: Namespace, name: str, schema: dict, sources: Mapping[str, str]) -> None: + """Register a JSON schema and promote any nested Pydantic `$defs`. + + `sources` maps each component name to the type that produced it. Flask-RESTX keeps + one flat model namespace, so a second registration under an existing name would + silently replace the first. Re-registering the same type with the same schema is a + no-op; a different type, or the same type with a different shape (for example a + response model also registered in validation mode), raises instead. + """ nested_definitions = schema.get("$defs") schema_to_register = dict(schema) if isinstance(nested_definitions, dict): schema_to_register.pop("$defs") + source = sources.get(name, name) + registered = _registered_schemas.setdefault(namespace, {}) + normalized_name = _normalized_schema_name(name) + existing = registered.get(normalized_name) + if existing is not None and existing != (name, source, schema_to_register): + existing_name, existing_source, _ = existing + if existing_name != name: + reason = f"collides with {existing_name!r} from {existing_source} once normalized by client generators" + elif existing_source != source: + reason = f"is produced by both {existing_source} and {source}" + else: + reason = f"is registered by {source} with two different shapes (validation vs serialization mode?)" + raise SchemaNameCollisionError( + f"OpenAPI schema name {name!r} in namespace {namespace.name!r} {reason}; give each shape its own " + "class name, add it to SCHEMA_NAME_OVERRIDES, or register response models only with " + "register_response_schema_models." + ) + registered[normalized_name] = (name, source, schema_to_register) + namespace.schema_model(name, schema_to_register) if not isinstance(nested_definitions, dict): @@ -66,24 +152,27 @@ def _register_json_schema(namespace: Namespace, name: str, schema: dict) -> None for nested_name, nested_schema in nested_definitions.items(): if isinstance(nested_schema, dict): - _register_json_schema(namespace, nested_name, nested_schema) + _register_json_schema(namespace, nested_name, nested_schema, sources) JsonSchemaMode = Literal["validation", "serialization"] -def _register_schema_model(namespace: Namespace, model: type[BaseModel], *, mode: JsonSchemaMode) -> None: +def _register_type_schema(namespace: Namespace, source_type: type, *, mode: JsonSchemaMode) -> None: + generator = _OpenApiJsonSchemaGenerator(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0) + schema = generator.generate(TypeAdapter(source_type).core_schema, mode=mode) _register_json_schema( namespace, - model.__name__, - model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0, mode=mode), + source_type.__name__, + schema, + {**generator.schema_sources, source_type.__name__: f"{source_type.__module__}.{source_type.__qualname__}"}, ) def register_schema_model(namespace: Namespace, model: type[BaseModel]) -> None: """Register a BaseModel and its nested component schemas for OpenAPI documentation.""" - _register_schema_model(namespace, model, mode="validation") + _register_type_schema(namespace, model, mode="validation") def register_schema_models(namespace: Namespace, *models: type[BaseModel]) -> None: @@ -96,7 +185,7 @@ def register_schema_models(namespace: Namespace, *models: type[BaseModel]) -> No def register_response_schema_model(namespace: Namespace, model: type[BaseModel]) -> None: """Register a BaseModel using its serialized response shape.""" - _register_schema_model(namespace, model, mode="serialization") + _register_type_schema(namespace, model, mode="serialization") def register_response_schema_models(namespace: Namespace, *models: type[BaseModel]) -> None: @@ -119,11 +208,7 @@ def get_or_create_model(model_name: str, field_def): def register_enum_models(namespace: Namespace, *models: type[StrEnum]) -> None: """Register multiple StrEnum with a namespace.""" for model in models: - _register_json_schema( - namespace, - model.__name__, - TypeAdapter(model).json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0), - ) + _register_type_schema(namespace, model, mode="validation") def query_params_from_model(model: type[BaseModel]) -> dict[str, QueryParamDoc]: @@ -334,6 +419,8 @@ def _nullable_property_schema(property_schema: Mapping[str, Any]) -> Mapping[str __all__ = [ "DEFAULT_REF_TEMPLATE_OPENAPI_3_0", + "SCHEMA_NAME_OVERRIDES", + "SchemaNameCollisionError", "get_or_create_model", "query_params_from_model", "query_params_from_request", diff --git a/api/controllers/console/app/advanced_prompt_template.py b/api/controllers/console/app/advanced_prompt_template.py index 267ac25c0c944b..f7b1efaae04dc6 100644 --- a/api/controllers/console/app/advanced_prompt_template.py +++ b/api/controllers/console/app/advanced_prompt_template.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Literal from flask_restx import Resource from pydantic import BaseModel, Field @@ -23,9 +23,33 @@ class AdvancedPromptTemplateQuery(BaseModel): model_name: str = Field(..., description="Model name") +class PromptTemplateMessageResponse(ResponseModel): + role: Literal["system", "user", "assistant"] + text: str + + +class ChatPromptTemplateResponse(ResponseModel): + prompt: list[PromptTemplateMessageResponse] + + +class CompletionPromptTemplateTextResponse(ResponseModel): + text: str + + +class ConversationHistoriesRoleResponse(ResponseModel): + user_prefix: str + assistant_prefix: str + + +class CompletionPromptTemplateResponse(ResponseModel): + prompt: CompletionPromptTemplateTextResponse + conversation_histories_role: ConversationHistoriesRoleResponse | None = None + + class AdvancedPromptTemplateResponse(ResponseModel): - chat_prompt_config: dict[str, Any] | None = Field(default=None) - completion_prompt_config: dict[str, Any] | None = Field(default=None) + chat_prompt_config: ChatPromptTemplateResponse | None = Field(default=None) + completion_prompt_config: CompletionPromptTemplateResponse | None = Field(default=None) + stop: list[str] | None = Field(default=None, description="Stop words for completion prompts.") console_ns.schema_model( diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index 619d7da6371c3e..3d213ab1fffbbb 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -48,11 +48,11 @@ class AgentToolCallResponse(ResponseModel): error: str | None = None time_cost: float | int tool_name: str - tool_label: str + tool_label: str | dict[str, str] = Field(description="Tool name or its i18n labels keyed by locale") tool_input: dict[str, Any] tool_output: dict[str, Any] tool_parameters: dict[str, Any] - tool_icon: Any = Field(default=None) + tool_icon: str | dict[str, str] | None = Field(default=None, description="Icon URL or emoji icon") class AgentIterationLogResponse(ResponseModel): diff --git a/api/controllers/console/app/annotation.py b/api/controllers/console/app/annotation.py index 897ba51d58ee69..a304ef7b27bc6e 100644 --- a/api/controllers/console/app/annotation.py +++ b/api/controllers/console/app/annotation.py @@ -76,6 +76,13 @@ class AnnotationListQuery(BaseModel): keyword: str = Field(default="", description="Search keyword") +class AnnotationBatchDeleteQuery(BaseModel): + annotation_id: list[str] = Field( + default_factory=list, + description="Annotation IDs to delete; omitting it clears all annotations of the app", + ) + + class CreateAnnotationPayload(BaseModel): message_id: str | None = Field(default=None, description="Message ID") question: str | None = Field(default=None, description="Question text") @@ -140,11 +147,6 @@ class AnnotationBatchImportResponse(ResponseModel): register_schema_models( console_ns, - Annotation, - AnnotationList, - AnnotationExportList, - AnnotationHitHistory, - AnnotationHitHistoryList, AnnotationReplyPayload, AnnotationSettingUpdatePayload, AnnotationListQuery, @@ -349,6 +351,7 @@ def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID @account_initialization_required @edit_permission_required @rbac_permission_required(RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp())) + @console_ns.doc(params=query_params_from_model(AnnotationBatchDeleteQuery)) @console_ns.response(204, "Annotations deleted successfully") @with_session def delete(self, session: Session, app_id: UUID): diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 63975480757109..8e3faec0f3faa7 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -32,7 +32,6 @@ from extensions.ext_application_services import application_services from fields.app_fields import ( AppDetail, - AppDetailSiteResponse, AppDetailWithSite, AppExportResponse, AppImportResponse, @@ -41,7 +40,6 @@ AppPartial, AppTraceResponse, DeletedTool, - ModelConfigPartial, RecentAppListResponse, RecentAppResponse, Tag, @@ -269,8 +267,6 @@ def validate_tracing_provider(cls, value: str | None, info) -> str | None: AppTracePayload, Tag, WorkflowPartial, - ModelConfigPartial, - AppDetailSiteResponse, DeletedTool, AppExportResponse, Segmentation, diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index b5c6e6598d5f3c..65c14b57ed30de 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -9,6 +9,7 @@ from werkzeug.exceptions import HTTPException, InternalServerError import services +from controllers.common.fields import AudioTranscriptResponse from controllers.common.rbac import AgentId, PlainApp, RBACCheck, enforce_rbac_checks from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns @@ -79,10 +80,6 @@ class TextToSpeechVoiceQuery(BaseModel): language: str = Field(..., description="Language code") -class AudioTranscriptResponse(ResponseModel): - text: str = Field(description="Transcribed text from audio") - - class AgentAudioTranscriptFormPayload(BaseModel): model_config = ConfigDict(extra="forbid") diff --git a/api/controllers/console/app/generator.py b/api/controllers/console/app/generator.py index eab25fc7e8fe2d..d52ec962ac3bfc 100644 --- a/api/controllers/console/app/generator.py +++ b/api/controllers/console/app/generator.py @@ -211,6 +211,18 @@ class GeneratorResponse(RootModel[Any]): root: Any +class RuleGenerateResponse(ResponseModel): + prompt: str + variables: list[str] + opening_statement: str + error: str + + +class StructuredOutputGenerateResponse(ResponseModel): + output: str = Field(description="Generated JSON Schema, empty when generation failed") + error: str = Field(description="Failure reason, empty on success") + + register_enum_models(console_ns, LLMMode) register_schema_models( console_ns, @@ -226,6 +238,8 @@ class GeneratorResponse(RootModel[Any]): register_response_schema_models( console_ns, GeneratorResponse, + RuleGenerateResponse, + StructuredOutputGenerateResponse, SimpleDataResponse, WorkflowGenerateResponse, WorkflowGenerateRejectedResponse, @@ -251,7 +265,7 @@ class RuleGenerateApi(Resource): @console_ns.response( 200, "Rule configuration generated successfully", - console_ns.models[GeneratorResponse.__name__], + console_ns.models[RuleGenerateResponse.__name__], ) @console_ns.response(400, "Invalid request parameters") @setup_required @@ -272,7 +286,7 @@ def post(self, req_data: RuleGeneratePayload, current_tenant_id: str): except InvokeError as e: raise CompletionRequestError(e.description) - return rules + return dump_response(RuleGenerateResponse, rules) @console_ns.route("/rule-code-generate") @@ -323,7 +337,11 @@ class RuleStructuredOutputGenerateApi(Resource): @console_ns.doc("generate_structured_output") @console_ns.doc(description="Generate structured output rules using LLM") @console_ns.expect(console_ns.models[RuleStructuredOutputPayload.__name__]) - @console_ns.response(200, "Structured output generated successfully", console_ns.models[GeneratorResponse.__name__]) + @console_ns.response( + 200, + "Structured output generated successfully", + console_ns.models[StructuredOutputGenerateResponse.__name__], + ) @console_ns.response(400, "Invalid request parameters") @setup_required @login_required diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index e2587abd70b556..2c52db1de20f05 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -45,6 +45,7 @@ MessageDetail as BaseMessageDetailResponse, ) from fields.conversation_fields import MessageResponseSource +from fields.message_fields import SuggestedQuestionsResponse from graphon.model_runtime.errors.invoke import InvokeError from libs.external_api import error_responses from libs.helper import dump_response, uuid_value @@ -87,7 +88,7 @@ def validate_uuid(cls, value: str | None) -> str | None: return uuid_value(value) -class MessageFeedbackPayload(_MessageFeedbackPayloadBase): +class AppMessageFeedbackPayload(_MessageFeedbackPayloadBase): message_id: str = Field(..., description="Message ID") @field_validator("message_id") @@ -100,10 +101,6 @@ class AnnotationCountResponse(ResponseModel): count: int = Field(description="Number of annotations") -class SuggestedQuestionsResponse(ResponseModel): - data: list[str] = Field(description="Suggested question") - - class MessageDetailResponse(BaseMessageDetailResponse): extra_contents: list[ExecutionExtraContentDomainModel] = Field(default_factory=list) @@ -117,7 +114,7 @@ class MessageInfiniteScrollPaginationResponse(ResponseModel): register_schema_models( console_ns, ChatMessagesQuery, - MessageFeedbackPayload, + AppMessageFeedbackPayload, ) register_response_schema_models( console_ns, @@ -198,7 +195,7 @@ class MessageFeedbackApi(Resource): @console_ns.doc("create_message_feedback") @console_ns.doc(description="Create or update message feedback (like/dislike)") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[MessageFeedbackPayload.__name__]) + @console_ns.expect(console_ns.models[AppMessageFeedbackPayload.__name__]) @console_ns.response(200, "Feedback updated successfully", console_ns.models[SimpleResultResponse.__name__]) @console_ns.response(404, "Message not found") @setup_required @@ -207,8 +204,8 @@ class MessageFeedbackApi(Resource): @with_current_user @with_session @get_app_model - @model_validate(MessageFeedbackPayload) - def post(self, req_data: MessageFeedbackPayload, session: Session, current_user: Account, app_model: App): + @model_validate(AppMessageFeedbackPayload) + def post(self, req_data: AppMessageFeedbackPayload, session: Session, current_user: Account, app_model: App): return _update_message_feedback( args=req_data, session=session, @@ -223,7 +220,7 @@ class AgentMessageFeedbackApi(Resource): @console_ns.doc("create_agent_message_feedback") @console_ns.doc(description="Create or update Agent App message feedback") @console_ns.doc(params={"agent_id": "Agent ID"}) - @console_ns.expect(console_ns.models[MessageFeedbackPayload.__name__]) + @console_ns.expect(console_ns.models[AppMessageFeedbackPayload.__name__]) @console_ns.response(200, "Feedback updated successfully", console_ns.models[SimpleResultResponse.__name__]) @console_ns.response(404, "Agent or message not found") @setup_required @@ -233,10 +230,10 @@ class AgentMessageFeedbackApi(Resource): @with_current_user @with_current_tenant_id @with_session - @model_validate(MessageFeedbackPayload) + @model_validate(AppMessageFeedbackPayload) def post( self, - req_data: MessageFeedbackPayload, + req_data: AppMessageFeedbackPayload, session: Session, current_tenant_id: str, current_user: Account, @@ -476,7 +473,7 @@ def _list_chat_messages( def _update_message_feedback( *, - args: MessageFeedbackPayload, + args: AppMessageFeedbackPayload, session: Session, current_user: Account, app_model: App, diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 156d5be5f4d054..fd4a65f38ced78 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -968,7 +968,7 @@ class HumanInputFormPreviewPayload(BaseModel): ) -class HumanInputFormSubmitPayload(BaseModel): +class HumanInputFormPreviewSubmitPayload(BaseModel): form_inputs: dict[str, Any] = Field( ..., description="Values the user provides for the form's own fields", @@ -991,7 +991,7 @@ class HumanInputDeliveryTestPayload(BaseModel): register_schema_models( console_ns, HumanInputFormPreviewPayload, - HumanInputFormSubmitPayload, + HumanInputFormPreviewSubmitPayload, HumanInputDeliveryTestPayload, ) @@ -1033,7 +1033,7 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource): @console_ns.doc("submit_advanced_chat_draft_human_input_form") @console_ns.doc(description="Submit human input form preview for advanced chat workflow") @console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"}) - @console_ns.expect(console_ns.models[HumanInputFormSubmitPayload.__name__]) + @console_ns.expect(console_ns.models[HumanInputFormPreviewSubmitPayload.__name__]) @console_ns.response( 200, "Human input form submission result", @@ -1046,8 +1046,8 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource): @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @with_current_user @edit_permission_required - @model_validate(HumanInputFormSubmitPayload) - def post(self, args: HumanInputFormSubmitPayload, current_user: Account, app_model: App, node_id: str): + @model_validate(HumanInputFormPreviewSubmitPayload) + def post(self, args: HumanInputFormPreviewSubmitPayload, current_user: Account, app_model: App, node_id: str): """ Submit human input form preview """ @@ -1101,7 +1101,7 @@ class WorkflowDraftHumanInputFormRunApi(Resource): @console_ns.doc("submit_workflow_draft_human_input_form") @console_ns.doc(description="Submit human input form preview for workflow") @console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"}) - @console_ns.expect(console_ns.models[HumanInputFormSubmitPayload.__name__]) + @console_ns.expect(console_ns.models[HumanInputFormPreviewSubmitPayload.__name__]) @console_ns.response( 200, "Human input form submission result", @@ -1114,8 +1114,8 @@ class WorkflowDraftHumanInputFormRunApi(Resource): @get_app_model(mode=[AppMode.WORKFLOW]) @with_current_user @edit_permission_required - @model_validate(HumanInputFormSubmitPayload) - def post(self, args: HumanInputFormSubmitPayload, current_user: Account, app_model: App, node_id: str): + @model_validate(HumanInputFormPreviewSubmitPayload) + def post(self, args: HumanInputFormPreviewSubmitPayload, current_user: Account, app_model: App, node_id: str): """ Submit human input form preview """ diff --git a/api/controllers/console/app/workflow_trigger.py b/api/controllers/console/app/workflow_trigger.py index 1b9d1d0ebf174c..ab684a6d8f0925 100644 --- a/api/controllers/console/app/workflow_trigger.py +++ b/api/controllers/console/app/workflow_trigger.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) -class Parser(BaseModel): +class WebhookTriggerQuery(BaseModel): node_id: str @@ -82,7 +82,7 @@ def _normalize_string_fields(cls, value: object) -> str: register_schema_models( console_ns, - Parser, + WebhookTriggerQuery, ParserEnable, WorkflowTriggerResponse, WorkflowTriggerListResponse, @@ -94,7 +94,7 @@ def _normalize_string_fields(cls, value: object) -> str: class WebhookTriggerApi(Resource): """Webhook Trigger API""" - @console_ns.doc(params=query_params_from_model(Parser)) + @console_ns.doc(params=query_params_from_model(WebhookTriggerQuery)) @setup_required @login_required @account_initialization_required @@ -102,8 +102,8 @@ class WebhookTriggerApi(Resource): @rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp())) @with_session(write=False) @get_app_model(mode=AppMode.WORKFLOW) - @model_validate(Parser) - def get(self, req_data: Parser, session: Session, app_model: App): + @model_validate(WebhookTriggerQuery) + def get(self, req_data: WebhookTriggerQuery, session: Session, app_model: App): """Get webhook trigger for a node""" node_id = req_data.node_id diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index 20da94a535e784..67e04dd7afecf5 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -29,6 +29,7 @@ ) from core.entities.knowledge_entities import IndexingEstimate from core.rag.index_processor.constant.index_type import IndexStructureType +from core.rag.index_processor.constant.query_type import QueryType from extensions.ext_application_services import application_services from fields.api_key_fields import ApiKeyItem, ApiKeyList, build_masked_api_key_list from fields.base import ResponseModel @@ -36,11 +37,14 @@ DatasetDetailResponse, NotionEstimateWorkspacePayload, ) +from fields.document_fields import DocumentStatusListResponse from libs.external_api import error_responses from libs.helper import build_icon_url, dump_response, to_timestamp from machinery.context import RequestContext from models.account import TenantAccountRole +from models.enums import CreatorUserRole, DatasetQuerySource from models.enums import PermissionEnum as DatasetPermissionEnum +from models.model import AppMode, IconType from services.errors.dataset import DatasetNameDuplicateError as DatasetNameDuplicateFailure from services.knowledge.dataset_access import DatasetAccessDeniedError, DatasetNotFoundError from services.knowledge.datasets.application import DatasetListFilter @@ -247,7 +251,7 @@ class DatasetQueryFileInfoResponse(ResponseModel): class DatasetQueryContentResponse(ResponseModel): - content_type: str + content_type: QueryType content: str file_info: DatasetQueryFileInfoResponse | None = None @@ -255,9 +259,9 @@ class DatasetQueryContentResponse(ResponseModel): class DatasetQueryDetailResponse(ResponseModel): id: str queries: list[DatasetQueryContentResponse] - source: str + source: DatasetQuerySource source_app_id: str | None - created_by_role: str + created_by_role: CreatorUserRole created_by: str created_at: int @@ -279,8 +283,8 @@ class RelatedAppResponse(ResponseModel): id: str name: str description: str - mode: str = Field(validation_alias="mode_compatible_with_agent") - icon_type: str | None + mode: AppMode = Field(validation_alias="mode_compatible_with_agent") + icon_type: IconType | None icon: str | None icon_background: str | None icon_url: str | None = None @@ -296,39 +300,6 @@ class RelatedAppListResponse(ResponseModel): total: int -class DocumentStatusResponse(ResponseModel): - id: str - indexing_status: str - processing_started_at: int | None - parsing_completed_at: int | None - cleaning_completed_at: int | None - splitting_completed_at: int | None - completed_at: int | None - paused_at: int | None - error: str | None - stopped_at: int | None - completed_segments: int | None = None - total_segments: int | None = None - - @field_validator( - "processing_started_at", - "parsing_completed_at", - "cleaning_completed_at", - "splitting_completed_at", - "completed_at", - "paused_at", - "stopped_at", - mode="before", - ) - @classmethod - def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: - return to_timestamp(value) - - -class DocumentStatusListResponse(ResponseModel): - data: list[DocumentStatusResponse] - - class ErrorDocsResponse(DocumentStatusListResponse): total: int @@ -496,11 +467,16 @@ def get(self, request_context: RequestContext, dataset_id: UUID): return dump_response(UsageCheckResponse, {"is_using": result}), 200 +class DatasetQueryListQuery(BaseModel): + page: int = Field(default=1, description="Page number") + limit: int = Field(default=20, description="Number of items per page") + + @console_ns.route("/datasets//queries") class DatasetQueryApi(Resource): @console_ns.doc("get_dataset_queries") @console_ns.doc(description="Get dataset query history") - @console_ns.doc(params={"dataset_id": "Dataset ID"}) + @console_ns.doc(params={"dataset_id": "Dataset ID", **query_params_from_model(DatasetQueryListQuery)}) @console_ns.response( 200, "Query history retrieved successfully", diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index 0fd1e2225f24ce..1469db61600922 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -12,13 +12,14 @@ from controllers.common.errors import InvalidArgumentError, NotFoundError from controllers.common.fields import SimpleResultMessageResponse, SimpleResultResponse, UrlResponse from controllers.common.rbac import DatasetByDocument, DatasetId, RBACCheck, enforce_rbac_checks -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( ProviderModelCurrentlyNotSupportError, ProviderNotInitializeError, ProviderQuotaExceededError, ) +from controllers.console.datasets.datasets import IndexingEstimateResponse from controllers.console.datasets.error import ( ArchivedDocumentImmutableError, DatasetAccessDeniedRequestError, @@ -42,7 +43,9 @@ from fields.base import ResponseModel from fields.document_fields import ( DocumentMetadataResponse, + DocumentProcessRuleResponse, DocumentResponse, + DocumentSourceDetailResponse, DocumentStatusListResponse, DocumentStatusResponse, normalize_enum, @@ -147,7 +150,7 @@ def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: class DocumentWithSegmentsResponse(DocumentResponse): - process_rule_dict: Any = None + process_rule_dict: DocumentProcessRuleResponse | None = None completed_segments: int | None = Field(default=None, exclude_if=lambda value: value is None) total_segments: int | None = Field(default=None, exclude_if=lambda value: value is None) @@ -184,6 +187,10 @@ class DocumentDatasetListParam(BaseModel): fetch_val: str = Field("false", alias="fetch") +class DocumentIdListQuery(BaseModel): + document_id: list[str] = Field(default_factory=list, description="Document IDs") + + class DocumentWithSegmentsListResponse(ResponseModel): data: list[DocumentWithSegmentsResponse] has_more: bool @@ -192,12 +199,6 @@ class DocumentWithSegmentsListResponse(ResponseModel): page: int -class IndexingEstimateResponse(IndexingEstimate): - tokens: int - total_price: float | int - currency: str - - def _serialize_indexing_estimate(estimate: IndexingEstimate) -> tuple[dict[str, object], int]: return ( IndexingEstimateResponse( @@ -216,11 +217,11 @@ class DocumentDetailResponse(ResponseModel): id: str position: int | None = None data_source_type: str | None = None - data_source_info: Any = None - data_source_detail_dict: Any = None + data_source_info: dict[str, JsonValue] | None = None + data_source_detail_dict: DocumentSourceDetailResponse | None = None dataset_process_rule_id: str | None = None - dataset_process_rule: Any = None - document_process_rule: Any = None + dataset_process_rule: DocumentProcessRuleResponse | None = None + document_process_rule: DocumentProcessRuleResponse | None = None name: str | None = None created_from: str | None = None created_by: str | None = None @@ -280,10 +281,14 @@ class DocumentSummaryStatusResponse(ResponseModel): summaries: list[SummaryEntryResponse] +class ProcessRuleLimitsResponse(ResponseModel): + indexing_max_segmentation_tokens_length: int + + class ProcessRuleResponse(ResponseModel): mode: ProcessRuleMode rules: Rule | None = None - limits: dict[str, Any] + limits: ProcessRuleLimitsResponse class DocumentPipelineExecutionLogResponse(ResponseModel): @@ -352,17 +357,7 @@ def get(self, request_context: RequestContext): class DatasetDocumentListApi(Resource): @console_ns.doc("get_dataset_documents") @console_ns.doc(description="Get documents in a dataset") - @console_ns.doc( - params={ - "dataset_id": "Dataset ID", - "page": "Page number (default: 1)", - "limit": "Number of items per page (default: 20)", - "keyword": "Search keyword", - "sort": "Sort order (default: -created_at)", - "fetch": "Fetch full details (default: false)", - "status": "Filter documents by display status", - } - ) + @console_ns.doc(params={"dataset_id": "Dataset ID", **query_params_from_model(DocumentDatasetListParam)}) @console_ns.response( 200, "Documents retrieved successfully", @@ -406,6 +401,7 @@ def post(self, request_context: RequestContext, dataset_id: UUID): _raise_document_indexing_error(error) return dump_response(DatasetAndDocumentResponse, result) + @console_ns.doc(params=query_params_from_model(DocumentIdListQuery)) @console_ns.response(204, "Documents deleted successfully") @console_account_admission( allowed_roles=_DATASET_EDIT_ROLES, @@ -750,6 +746,7 @@ def put( @console_ns.route("/datasets//documents/status//batch") class DocumentStatusApi(Resource): + @console_ns.doc(params=query_params_from_model(DocumentIdListQuery)) @error_responses(InvalidActionError) @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @console_account_admission( diff --git a/api/controllers/console/datasets/external.py b/api/controllers/console/datasets/external.py index c9961b50a30ea8..3f86a1c61f70fe 100644 --- a/api/controllers/console/datasets/external.py +++ b/api/controllers/console/datasets/external.py @@ -76,12 +76,19 @@ class ExternalKnowledgeApiBindingResponse(ResponseModel): name: str +class ExternalKnowledgeApiSettingsResponse(ResponseModel): + endpoint: str | None = None + api_key: str | None = None + + class ExternalKnowledgeApiResponse(ResponseModel): id: str tenant_id: str name: str description: str - settings: dict[str, Any] | None = Field(validation_alias=AliasChoices("settings_dict", "settings")) + settings: ExternalKnowledgeApiSettingsResponse | None = Field( + validation_alias=AliasChoices("settings_dict", "settings") + ) dataset_bindings: list[ExternalKnowledgeApiBindingResponse] created_by: str created_at: str diff --git a/api/controllers/console/datasets/rag_pipeline/datasource_content_preview.py b/api/controllers/console/datasets/rag_pipeline/datasource_content_preview.py index af4993d9d51513..048f5cf858db96 100644 --- a/api/controllers/console/datasets/rag_pipeline/datasource_content_preview.py +++ b/api/controllers/console/datasets/rag_pipeline/datasource_content_preview.py @@ -17,26 +17,26 @@ from services.rag_pipeline.rag_pipeline import RagPipelineService -class Parser(BaseModel): +class DatasourceContentPreviewPayload(BaseModel): inputs: dict[str, Any] datasource_type: str credential_id: str | None = None -register_schema_models(console_ns, Parser) +register_schema_models(console_ns, DatasourceContentPreviewPayload) @console_ns.route("/rag/pipelines//workflows/published/datasource/nodes//preview") class DataSourceContentPreviewApi(Resource): - @console_ns.expect(console_ns.models[Parser.__name__]) + @console_ns.expect(console_ns.models[DatasourceContentPreviewPayload.__name__]) @console_ns.response(200, "Success") @setup_required @login_required @account_initialization_required @get_rag_pipeline @with_current_user - @model_validate(Parser) - def post(self, req_data: Parser, current_user: Account, pipeline: Pipeline, node_id: str): + @model_validate(DatasourceContentPreviewPayload) + def post(self, req_data: DatasourceContentPreviewPayload, current_user: Account, pipeline: Pipeline, node_id: str): """ Run datasource content preview """ diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index c02db46f393bb2..29ae07485d138a 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -26,6 +26,7 @@ RESTORE_SOURCE_WORKFLOW_MUST_BE_PUBLISHED_MESSAGE, DefaultBlockConfigResponse, DefaultBlockConfigsResponse, + PipelineVariableResponse, WorkflowPaginationResponse, WorkflowResponse, WorkflowResponseSource, @@ -33,6 +34,7 @@ from controllers.console.app.wraps import with_session from controllers.console.datasets.error import PipelineNotFoundError from controllers.console.datasets.wraps import get_rag_pipeline, load_rag_pipeline +from controllers.console.workspace.tool_providers import ToolProviderApiEntityResponse from controllers.console.wraps import ( RBACPermission, account_initialization_required, @@ -47,6 +49,8 @@ from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.pipeline.pipeline_generator import PipelineGenerator from core.app.entities.app_invoke_entities import InvokeFrom +from core.app.entities.task_entities import WorkflowAppBlockingResponse +from core.entities.knowledge_entities import PipelineGenerateResponse from core.plugin.entities.plugin_daemon import PluginDatasourceProviderEntity from extensions.ext_application_services import application_services from extensions.ext_database import db @@ -107,14 +111,14 @@ class DatasourceNodeRunPayload(BaseModel): credential_id: str | None = None -class DraftWorkflowRunPayload(BaseModel): +class RagPipelineDraftWorkflowRunPayload(BaseModel): inputs: dict[str, Any] datasource_type: str datasource_info_list: list[dict[str, Any]] start_node_id: str -class PublishedWorkflowRunPayload(DraftWorkflowRunPayload): +class PublishedWorkflowRunPayload(RagPipelineDraftWorkflowRunPayload): is_preview: bool = False response_mode: Literal["streaming", "blocking"] = "streaming" original_document_id: str | None = None @@ -124,7 +128,7 @@ class NodeIdQuery(BaseModel): node_id: str -class WorkflowRunQuery(BaseModel): +class RagPipelineWorkflowRunQuery(BaseModel): last_id: UUIDStrOrEmpty | None = None limit: int = Field(default=20, ge=1, le=100) @@ -164,7 +168,23 @@ class RagPipelineDatasourceListResponse(RootModel[list[RagPipelineDatasourceProv class RagPipelineStepParametersResponse(ResponseModel): - variables: Any + variables: list[PipelineVariableResponse] + + +class RagPipelinePublishedRunResponse(RootModel[PipelineGenerateResponse | WorkflowAppBlockingResponse]): + """Blocking run result: a preview run returns the workflow result, a processing run the queued batch.""" + + +class RagPipelineTransformResponse(ResponseModel): + pipeline_id: str + dataset_id: str + status: str + + +class RagPipelineRecommendedPluginsResponse(ResponseModel): + installed_recommended_plugins: list[ToolProviderApiEntityResponse] + # Marketplace plugin manifests, proxied as the marketplace returns them. + uninstalled_recommended_plugins: list[dict[str, Any]] register_schema_models( @@ -173,13 +193,13 @@ class RagPipelineStepParametersResponse(ResponseModel): NodeRunPayload, NodeRunRequiredPayload, DatasourceNodeRunPayload, - DraftWorkflowRunPayload, + RagPipelineDraftWorkflowRunPayload, PublishedWorkflowRunPayload, DefaultBlockConfigQuery, WorkflowListQuery, WorkflowUpdatePayload, NodeIdQuery, - WorkflowRunQuery, + RagPipelineWorkflowRunQuery, DatasourceVariablesPayload, RagPipelineRecommendedPluginQuery, ) @@ -190,7 +210,10 @@ class RagPipelineStepParametersResponse(ResponseModel): RagPipelineDatasourceListResponse, RagPipelineDatasourceProviderResponse, RagPipelineOpaqueResponse, + RagPipelinePublishedRunResponse, + RagPipelineRecommendedPluginsResponse, RagPipelineStepParametersResponse, + RagPipelineTransformResponse, RagPipelineWorkflowPublishResponse, RagPipelineWorkflowSyncResponse, SimpleResultResponse, @@ -374,7 +397,7 @@ def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeli @console_ns.route("/rag/pipelines//workflows/draft/run") class DraftRagPipelineRunApi(Resource): @error_responses(InvokeRateLimitHttpError, PipelineNotFoundError) - @console_ns.expect(console_ns.models[DraftWorkflowRunPayload.__name__]) + @console_ns.expect(console_ns.models[RagPipelineDraftWorkflowRunPayload.__name__]) @console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__]) @setup_required @login_required @@ -383,8 +406,10 @@ class DraftRagPipelineRunApi(Resource): @rbac_permission_required(RBACCheck(RBACPermission.DATASET_EDIT, DatasetByPipeline())) @with_current_user @with_session - @model_validate(DraftWorkflowRunPayload) - def post(self, req_data: DraftWorkflowRunPayload, session: Session, current_user: Account, pipeline_id: UUID): + @model_validate(RagPipelineDraftWorkflowRunPayload) + def post( + self, req_data: RagPipelineDraftWorkflowRunPayload, session: Session, current_user: Account, pipeline_id: UUID + ): """ Run draft workflow """ @@ -411,7 +436,7 @@ def post(self, req_data: DraftWorkflowRunPayload, session: Session, current_user class PublishedRagPipelineRunApi(Resource): @error_responses(InvokeRateLimitHttpError, PipelineNotFoundError) @console_ns.expect(console_ns.models[PublishedWorkflowRunPayload.__name__]) - @console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__]) + @console_ns.response(200, "Success", console_ns.models[RagPipelinePublishedRunResponse.__name__]) @setup_required @login_required @account_initialization_required @@ -862,9 +887,7 @@ def get(self, req_data: NodeIdQuery, pipeline: Pipeline): node_id = req_data.node_id rag_pipeline_service = RagPipelineService(db.session()) variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False) - return { - "variables": variables, - } + return dump_response(RagPipelineStepParametersResponse, {"variables": variables}, exclude_unset=True) @console_ns.route("/rag/pipelines//workflows/published/pre-processing/parameters") @@ -885,9 +908,7 @@ def get(self, req_data: NodeIdQuery, pipeline: Pipeline): node_id = req_data.node_id rag_pipeline_service = RagPipelineService(db.session()) variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False) - return { - "variables": variables, - } + return dump_response(RagPipelineStepParametersResponse, {"variables": variables}, exclude_unset=True) @console_ns.route("/rag/pipelines//workflows/draft/pre-processing/parameters") @@ -908,9 +929,7 @@ def get(self, req_data: NodeIdQuery, pipeline: Pipeline): node_id = req_data.node_id rag_pipeline_service = RagPipelineService(db.session()) variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True) - return { - "variables": variables, - } + return dump_response(RagPipelineStepParametersResponse, {"variables": variables}, exclude_unset=True) @console_ns.route("/rag/pipelines//workflows/draft/processing/parameters") @@ -932,14 +951,12 @@ def get(self, req_data: NodeIdQuery, pipeline: Pipeline): rag_pipeline_service = RagPipelineService(db.session()) variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True) - return { - "variables": variables, - } + return dump_response(RagPipelineStepParametersResponse, {"variables": variables}, exclude_unset=True) @console_ns.route("/rag/pipelines//workflow-runs") class RagPipelineWorkflowRunListApi(Resource): - @console_ns.doc(params=query_params_from_model(WorkflowRunQuery)) + @console_ns.doc(params=query_params_from_model(RagPipelineWorkflowRunQuery)) @console_ns.response( 200, "Workflow runs retrieved successfully", @@ -954,7 +971,7 @@ def get(self, session: Session, pipeline: Pipeline): """ Get workflow run list """ - query = WorkflowRunQuery.model_validate( + query = RagPipelineWorkflowRunQuery.model_validate( { "last_id": request.args.get("last_id"), "limit": request.args.get("limit", type=int, default=20), @@ -1076,7 +1093,7 @@ def get(self, pipeline: Pipeline, node_id: str): @console_ns.route("/rag/pipelines/transform/datasets/") class RagPipelineTransformApi(Resource): - @console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__]) + @console_ns.response(200, "Success", console_ns.models[RagPipelineTransformResponse.__name__]) @console_ns.response(404, "Dataset or pipeline not found") @setup_required @login_required @@ -1099,7 +1116,10 @@ def post(self, session: Session, current_tenant_id: str, current_user: Account, raise Forbidden(str(exc)) from exc try: - return RagPipelineTransformService().transform_dataset(dataset, current_user.id, session) + return dump_response( + RagPipelineTransformResponse, + RagPipelineTransformService().transform_dataset(dataset, current_user.id, session), + ) except RagPipelineResourceNotFoundError as exc: raise NotFound(str(exc)) from exc @@ -1140,7 +1160,7 @@ def post(self, req_data: DatasourceVariablesPayload, current_user: Account, pipe @console_ns.route("/rag/pipelines/recommended-plugins") class RagPipelineRecommendedPluginApi(Resource): @console_ns.doc(params=query_params_from_model(RagPipelineRecommendedPluginQuery)) - @console_ns.response(200, "Success", console_ns.models[RagPipelineOpaqueResponse.__name__]) + @console_ns.response(200, "Success", console_ns.models[RagPipelineRecommendedPluginsResponse.__name__]) @setup_required @login_required @account_initialization_required @@ -1153,4 +1173,4 @@ def get(self, req_data: RagPipelineRecommendedPluginQuery, current_tenant_id: st recommended_plugins = rag_pipeline_service.get_recommended_plugins( req_data.type, current_user, current_tenant_id ) - return recommended_plugins + return dump_response(RagPipelineRecommendedPluginsResponse, recommended_plugins, exclude_unset=True) diff --git a/api/controllers/console/explore/completion.py b/api/controllers/console/explore/completion.py index b8a1e1b76b81a2..87b80345f7bad2 100644 --- a/api/controllers/console/explore/completion.py +++ b/api/controllers/console/explore/completion.py @@ -54,7 +54,7 @@ class CompletionMessageExplorePayload(BaseModel): retriever_from: str = Field(default="explore_app") -class ChatMessagePayload(BaseModel): +class ChatMessageExplorePayload(BaseModel): inputs: dict[str, Any] query: str files: list[dict[str, Any]] | None = Field(default=None) @@ -77,7 +77,7 @@ def normalize_uuid(cls, value: str | UUID | None) -> str | None: raise ValueError("must be a valid UUID") from exc -register_schema_models(console_ns, CompletionMessageExplorePayload, ChatMessagePayload) +register_schema_models(console_ns, CompletionMessageExplorePayload, ChatMessageExplorePayload) register_response_schema_models(console_ns, SimpleResultResponse) @@ -185,14 +185,14 @@ class ChatApi(Resource): ProviderNotInitializeError, ProviderQuotaExceededError, ) - @console_ns.expect(console_ns.models[ChatMessagePayload.__name__]) + @console_ns.expect(console_ns.models[ChatMessageExplorePayload.__name__]) @console_ns.response(HTTPStatus.OK, "Success") @console_account_admission() @get_installed_app - @model_validate(ChatMessagePayload) + @model_validate(ChatMessageExplorePayload) def post( self, - req_data: ChatMessagePayload, + req_data: ChatMessageExplorePayload, request_context: RequestContext, installed_app: InstalledAppRef, ) -> Response: diff --git a/api/controllers/console/explore/recommended_app.py b/api/controllers/console/explore/recommended_app.py index b09b7e2ca8ce79..c5adb9e86aca72 100644 --- a/api/controllers/console/explore/recommended_app.py +++ b/api/controllers/console/explore/recommended_app.py @@ -12,7 +12,7 @@ from controllers.console.flask_admission import console_account_admission from controllers.console.wraps import model_validate from extensions.ext_application_services import application_services -from fields.app_export_fields import AppExportResponse +from fields.app_fields import AppExportResponse from fields.base import ResponseModel from libs.external_api import error_responses from libs.flask_restx_compat import BINARY_RESPONSE_MEDIA_TYPES_VENDOR_KEY diff --git a/api/controllers/console/extension.py b/api/controllers/console/extension.py index 2b064daf85578c..164202f9dc7e97 100644 --- a/api/controllers/console/extension.py +++ b/api/controllers/console/extension.py @@ -35,9 +35,15 @@ class APIBasedExtensionPayload(BaseModel): api_key: str = Field(description="API key for authentication") +class CodeBasedExtensionItemResponse(ResponseModel): + name: str + label: dict[str, Any] | None = None + form_schema: list[dict[str, Any]] | None = None + + class CodeBasedExtensionResponse(ResponseModel): module: str = Field(description="Module name") - data: Any = Field(description="Extension data") + data: list[CodeBasedExtensionItemResponse] = Field(description="Non-builtin extensions of the module") def _mask_api_key(api_key: str) -> str: diff --git a/api/controllers/console/snippets/payloads.py b/api/controllers/console/snippets/payloads.py index 86b7735de6fffd..4bd3e061c8db5d 100644 --- a/api/controllers/console/snippets/payloads.py +++ b/api/controllers/console/snippets/payloads.py @@ -110,7 +110,7 @@ class SnippetWorkflowListQuery(BaseModel): limit: int = Field(default=10, ge=1, le=100) -class WorkflowRunQuery(BaseModel): +class SnippetWorkflowRunQuery(BaseModel): """Query parameters for workflow runs.""" last_id: str | None = None @@ -144,7 +144,7 @@ class SnippetLoopNodeRunPayload(BaseModel): inputs: dict[str, Any] | None = Field(default=None) -class PublishWorkflowPayload(BaseModel): +class SnippetPublishWorkflowPayload(BaseModel): """Payload for publishing snippet workflow.""" knowledge_base_setting: dict[str, Any] | None = Field(default=None) diff --git a/api/controllers/console/snippets/snippet_workflow.py b/api/controllers/console/snippets/snippet_workflow.py index 6c837abd7d74a0..fdaa90f6f097e6 100644 --- a/api/controllers/console/snippets/snippet_workflow.py +++ b/api/controllers/console/snippets/snippet_workflow.py @@ -26,14 +26,14 @@ WorkflowRestoreResponse, ) from controllers.console.snippets.payloads import ( - PublishWorkflowPayload, SnippetDraftNodeRunPayload, SnippetDraftRunPayload, SnippetDraftSyncPayload, SnippetIterationNodeRunPayload, SnippetLoopNodeRunPayload, + SnippetPublishWorkflowPayload, SnippetWorkflowListQuery, - WorkflowRunQuery, + SnippetWorkflowRunQuery, ) from controllers.console.wraps import ( RBACPermission, @@ -102,9 +102,9 @@ class SnippetWorkflowPaginationResponse(BaseModel): SnippetIterationNodeRunPayload, SnippetLoopNodeRunPayload, SnippetWorkflowListQuery, - WorkflowRunQuery, + SnippetWorkflowRunQuery, WorkflowUpdatePayload, - PublishWorkflowPayload, + SnippetPublishWorkflowPayload, ) register_response_schema_models( console_ns, @@ -288,7 +288,7 @@ def get(self, session: Session, snippet: CustomizedSnippet): return response @console_ns.doc("publish_snippet_workflow") - @console_ns.expect(console_ns.models.get(PublishWorkflowPayload.__name__)) + @console_ns.expect(console_ns.models.get(SnippetPublishWorkflowPayload.__name__)) @console_ns.response(200, "Workflow published successfully", console_ns.models[WorkflowPublishResponse.__name__]) @console_ns.response(400, "No draft workflow found") @setup_required @@ -514,7 +514,7 @@ def delete(self, snippet: CustomizedSnippet, workflow_id: str): @console_ns.route("/snippets//workflow-runs") class SnippetWorkflowRunsApi(Resource): @console_ns.doc("list_snippet_workflow_runs") - @console_ns.doc(params=query_params_from_model(WorkflowRunQuery)) + @console_ns.doc(params=query_params_from_model(SnippetWorkflowRunQuery)) @console_ns.response( 200, "Workflow runs retrieved successfully", @@ -527,7 +527,7 @@ class SnippetWorkflowRunsApi(Resource): @with_session(write=False) def get(self, session: Session, snippet: CustomizedSnippet): """List workflow runs for snippet.""" - query = WorkflowRunQuery.model_validate( + query = SnippetWorkflowRunQuery.model_validate( { "last_id": request.args.get("last_id"), "limit": request.args.get("limit", type=int, default=20), diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py index f32e64ad59ac1e..6226af4051a4ea 100644 --- a/api/controllers/console/workspace/members.py +++ b/api/controllers/console/workspace/members.py @@ -12,7 +12,12 @@ from controllers.common.errors import ForbiddenError from controllers.common.fields import SimpleResultDataResponse, SimpleResultResponse, VerificationTokenResponse from controllers.common.rbac import rbac_language -from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models +from controllers.common.schema import ( + query_params_from_model, + register_enum_models, + register_response_schema_models, + register_schema_models, +) from controllers.console import console_ns from controllers.console.auth.error import ( CannotTransferOwnerToSelfError, @@ -32,6 +37,7 @@ MemberNotFoundError, MemberRoleAlreadyAssignedError, ) +from controllers.console.workspace.rbac.schemas import _AccessControlLanguageQuery from enums.account import TenantAccountRole from extensions.ext_application_services import application_services from fields.base import ResponseModel @@ -147,6 +153,7 @@ class MemberInviteErrorResponse(ResponseModel): class MemberListApi(Resource): """List all members of current tenant.""" + @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) @console_ns.response(HTTPStatus.OK, "Success", console_ns.models[AccountWithRoleListResponse.__name__]) @console_account_admission() def get(self, request_context: RequestContext): diff --git a/api/controllers/console/workspace/rbac/members.py b/api/controllers/console/workspace/rbac/members.py index 223070778e6ec7..d32dadaca2988b 100644 --- a/api/controllers/console/workspace/rbac/members.py +++ b/api/controllers/console/workspace/rbac/members.py @@ -6,9 +6,11 @@ from controllers.common.errors import InvalidArgumentError from controllers.common.rbac import rbac_language +from controllers.common.schema import query_params_from_model from controllers.console import console_ns from controllers.console.flask_admission import console_account_admission from controllers.console.workspace.rbac.schemas import ( + _AccessControlLanguageQuery, _ReplaceMemberRolesRequest, ) from extensions.ext_application_services import application_services @@ -46,6 +48,7 @@ def get(self, context: RequestContext): @console_ns.route("/workspaces/current/rbac/members//rbac-roles") class RBACMemberRolesApi(Resource): @console_account_admission() + @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) @console_ns.response(200, "Success", console_ns.models[dto.MemberRolesResponse.__name__]) def get(self, context: RequestContext, member_id): return dump_response( diff --git a/api/controllers/console/workspace/rbac/roles.py b/api/controllers/console/workspace/rbac/roles.py index 955d06836c78f6..786e395b2bb179 100644 --- a/api/controllers/console/workspace/rbac/roles.py +++ b/api/controllers/console/workspace/rbac/roles.py @@ -5,11 +5,14 @@ from controllers.common.fields import SimpleResultResponse from controllers.common.rbac import RBACCheck, Workspace, rbac_language +from controllers.common.schema import query_params_from_model from controllers.console import console_ns from controllers.console.flask_admission import console_account_admission from controllers.console.workspace.rbac.schemas import ( CopyRoleParam, + _AccessControlLanguageQuery, _MembersInRoleList, + _PaginationQuery, _RBACRoleList, _RolesListQuery, _RoleUpsertRequest, @@ -35,6 +38,12 @@ def get(self, context: RequestContext): @console_ns.route("/workspaces/current/rbac/roles") class RBACRolesApi(Resource): @console_account_admission(rbac_checks=[RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())]) + @console_ns.doc( + params={ + **query_params_from_model(_RolesListQuery), + **query_params_from_model(_AccessControlLanguageQuery), + } + ) @console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__]) def get(self, context: RequestContext): req_data = _RolesListQuery.model_validate(request.args.to_dict(flat=True)) @@ -44,6 +53,8 @@ def get(self, context: RequestContext): return dump_response(_RBACRoleList, result) @console_account_admission(rbac_checks=[RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())]) + @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) + @console_ns.expect(console_ns.models[_RoleUpsertRequest.__name__]) @console_ns.response(201, "Role created", console_ns.models[dto.RBACRole.__name__]) def post(self, context: RequestContext): request = _RoleUpsertRequest.model_validate(console_ns.payload or {}) @@ -54,6 +65,7 @@ def post(self, context: RequestContext): @console_ns.route("/workspaces/current/rbac/roles/") class RBACRoleItemApi(Resource): @console_account_admission(rbac_checks=[RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())]) + @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) @console_ns.response(200, "Success", console_ns.models[dto.RBACRole.__name__]) def get(self, context: RequestContext, role_id): return dump_response( @@ -61,6 +73,8 @@ def get(self, context: RequestContext, role_id): ) @console_account_admission(rbac_checks=[RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())]) + @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) + @console_ns.expect(console_ns.models[_RoleUpsertRequest.__name__]) @console_ns.response(200, "Success", console_ns.models[dto.RBACRole.__name__]) def put(self, context: RequestContext, role_id): request = _RoleUpsertRequest.model_validate(console_ns.payload or {}) @@ -79,6 +93,8 @@ def delete(self, context: RequestContext, role_id): @console_ns.route("/workspaces/current/rbac/roles//copy") class RBACRoleCopyApi(Resource): @console_account_admission(rbac_checks=[RBACCheck(RBACPermission.WORKSPACE_ROLE_MANAGE, Workspace())]) + @console_ns.doc(params=query_params_from_model(_AccessControlLanguageQuery)) + @console_ns.expect(console_ns.models[CopyRoleParam.__name__]) @console_ns.response(201, "Role copied", console_ns.models[dto.RBACRole.__name__]) def post(self, context: RequestContext, role_id): request = CopyRoleParam.model_validate(console_ns.payload or {}) @@ -91,6 +107,12 @@ def post(self, context: RequestContext, role_id): @console_ns.route("/workspaces/current/rbac/roles//members") class ListMembersByRole(Resource): @console_account_admission() + @console_ns.doc( + params={ + **query_params_from_model(_PaginationQuery), + **query_params_from_model(_AccessControlLanguageQuery), + } + ) @console_ns.response(200, "Success", console_ns.models[_MembersInRoleList.__name__]) def get(self, context: RequestContext, role_id): return dump_response( diff --git a/api/controllers/console/workspace/rbac/schemas.py b/api/controllers/console/workspace/rbac/schemas.py index d276aaaacf1f7b..3a8c5c3ef590db 100644 --- a/api/controllers/console/workspace/rbac/schemas.py +++ b/api/controllers/console/workspace/rbac/schemas.py @@ -156,7 +156,7 @@ def _coerce_role_ids(cls, value: Any) -> list[str]: return value -register_schema_models(console_ns, _ReplaceMemberRolesRequest) +register_schema_models(console_ns, _ReplaceMemberRolesRequest, _RoleUpsertRequest, CopyRoleParam) def pagination_options() -> dto.ListOption: diff --git a/api/controllers/inner_api/app/dsl.py b/api/controllers/inner_api/app/dsl.py index 301a754f2bbfe8..4169a7320b46eb 100644 --- a/api/controllers/inner_api/app/dsl.py +++ b/api/controllers/inner_api/app/dsl.py @@ -24,7 +24,7 @@ from controllers.inner_api.wraps import enterprise_inner_api_only from core.logging.context import get_request_id, get_trace_id from extensions.ext_application_services import application_services -from fields.app_export_fields import AppExportResponse +from fields.app_fields import AppExportResponse from fields.base import ResponseModel from libs.helper import dump_response from services.entities.app_entities import AppExportOptions diff --git a/api/controllers/service_api/app/annotation.py b/api/controllers/service_api/app/annotation.py index 0d4317541898c1..da26ad37f0d335 100644 --- a/api/controllers/service_api/app/annotation.py +++ b/api/controllers/service_api/app/annotation.py @@ -64,8 +64,6 @@ class AnnotationListQuery(BaseModel): AnnotationCreatePayload, AnnotationReplyActionPayload, AnnotationListQuery, - Annotation, - AnnotationList, ) register_response_schema_models( service_api_ns, diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py index 50a311079ab7ca..086638c5710749 100644 --- a/api/controllers/service_api/dataset/dataset.py +++ b/api/controllers/service_api/dataset/dataset.py @@ -95,11 +95,12 @@ class DatasetDetailResponse(BaseDatasetDetailResponse): - # The Service API dump helpers exclude Console permission metadata. + # The Service API dump helpers exclude Console permission metadata and form settings. permission_keys: list[str] = Field(default_factory=list, exclude=True) + keyword_number: int | None = Field(default=None, exclude=True) -_SERVICE_DATASET_DETAIL_EXCLUDE = {"permission_keys"} +_SERVICE_DATASET_DETAIL_EXCLUDE = {"permission_keys", "keyword_number"} _SERVICE_DATASET_LIST_EXCLUDE = {"data": {"__all__": _SERVICE_DATASET_DETAIL_EXCLUDE}} diff --git a/api/controllers/web/human_input_file_upload.py b/api/controllers/web/human_input_file_upload.py index 5befd9a2f7ef19..3966f9fee3905e 100644 --- a/api/controllers/web/human_input_file_upload.py +++ b/api/controllers/web/human_input_file_upload.py @@ -28,7 +28,7 @@ TooManyFilesError, UnsupportedFileTypeError, ) -from controllers.common.schema import JsonResponseWithStatus, register_schema_models +from controllers.common.schema import JsonResponseWithStatus, register_response_schema_models, register_schema_models from controllers.web import web_ns from extensions.ext_application_services import application_services from fields.file_fields import FileResponse, FileWithSignedUrl @@ -80,7 +80,8 @@ class HumanInputFileUploadFormPayload(BaseModel): url: HttpUrl | None = Field(default=None, description="Remote file URL") -register_schema_models(web_ns, HumanInputFileUploadFormPayload, FileResponse, FileWithSignedUrl) +register_schema_models(web_ns, HumanInputFileUploadFormPayload) +register_response_schema_models(web_ns, FileResponse, FileWithSignedUrl) def _extract_hitl_upload_token() -> str: diff --git a/api/fields/app_export_fields.py b/api/fields/app_export_fields.py deleted file mode 100644 index 91322b8cca8a81..00000000000000 --- a/api/fields/app_export_fields.py +++ /dev/null @@ -1,5 +0,0 @@ -from fields.base import ResponseModel - - -class AppExportResponse(ResponseModel): - data: str diff --git a/api/fields/conversation_fields.py b/api/fields/conversation_fields.py index bb016b9ca53b22..02d3e898cbe337 100644 --- a/api/fields/conversation_fields.py +++ b/api/fields/conversation_fields.py @@ -24,7 +24,7 @@ Message as MessageModel, ) -type JSONValue = Any +type AnyJSONValue = Any UUIDString = Annotated[str, WithJsonSchema({"format": "uuid", "type": "string"})] Int64 = Annotated[int, WithJsonSchema({"format": "int64", "type": "integer"})] URLString = Annotated[str, WithJsonSchema({"format": "url", "type": "string"})] @@ -155,7 +155,7 @@ def _normalize_transfer_method(cls, value: object) -> str: class SimpleConversation(ResponseModel): id: UUIDString name: str - inputs: dict[str, JSONValue] + inputs: dict[str, AnyJSONValue] status: str introduction: str | None = None created_at: Int64 | None = None @@ -163,7 +163,7 @@ class SimpleConversation(ResponseModel): @field_validator("inputs", mode="before") @classmethod - def _normalize_inputs(cls, value: JSONValue) -> JSONValue: + def _normalize_inputs(cls, value: AnyJSONValue) -> AnyJSONValue: return format_files_contained(value) @field_validator("created_at", "updated_at", mode="before") @@ -233,7 +233,7 @@ class AgentThought(ResponseModel): thought: str | None = None answer: str | None = None tool: str | None = None - tool_labels: JSONValue = Field(description="Labels for tools used.") + tool_labels: AnyJSONValue = Field(description="Labels for tools used.") tool_input: str | None = None created_at: int | None = None observation: str | None = None @@ -254,9 +254,9 @@ def _fallback_chain_id(self): class MessageDetail(ResponseModel): id: str conversation_id: str - inputs: dict[str, JSONValue] + inputs: dict[str, AnyJSONValue] query: str - message: JSONValue + message: AnyJSONValue message_tokens: int answer: str = Field(validation_alias="re_sign_file_url_answer") answer_tokens: int @@ -271,14 +271,14 @@ class MessageDetail(ResponseModel): created_at: int | None = None agent_thoughts: list[AgentThought] message_files: list[MessageFile] - metadata: JSONValue = Field(validation_alias="message_metadata_dict") + metadata: AnyJSONValue = Field(validation_alias="message_metadata_dict") status: str error: str | None = None parent_message_id: str | None = None @field_validator("inputs", mode="before") @classmethod - def _normalize_inputs(cls, value: JSONValue) -> JSONValue: + def _normalize_inputs(cls, value: AnyJSONValue) -> AnyJSONValue: return format_files_contained(value) @field_validator("created_at", mode="before") @@ -299,17 +299,17 @@ class StatusCount(ResponseModel): paused: int -class ModelConfig(ResponseModel): +class ConversationModelConfig(ResponseModel): opening_statement: str | None = None - suggested_questions: JSONValue | None = Field(default=None) - model: JSONValue | None = Field(default=None) - user_input_form: JSONValue | None = Field(default=None) + suggested_questions: AnyJSONValue | None = Field(default=None) + model: AnyJSONValue | None = Field(default=None) + user_input_form: AnyJSONValue | None = Field(default=None) pre_prompt: str | None = None - agent_mode: JSONValue | None = Field(default=None) + agent_mode: AnyJSONValue | None = Field(default=None) class SimpleModelConfig(ResponseModel): - model: JSONValue | None = Field( + model: AnyJSONValue | None = Field( default=None, validation_alias="model_dict", ) @@ -317,19 +317,19 @@ class SimpleModelConfig(ResponseModel): class SimpleMessageDetail(ResponseModel): - inputs: dict[str, JSONValue] + inputs: dict[str, AnyJSONValue] query: str message: str answer: str @field_validator("inputs", mode="before") @classmethod - def _normalize_inputs(cls, value: JSONValue) -> JSONValue: + def _normalize_inputs(cls, value: AnyJSONValue) -> AnyJSONValue: return format_files_contained(value) @field_validator("message", mode="before") @classmethod - def _normalize_message(cls, value: JSONValue) -> str: + def _normalize_message(cls, value: AnyJSONValue) -> str: return message_text(value) @@ -371,7 +371,7 @@ class ConversationMessageDetail(ResponseModel): from_end_user_id: str | None = None from_account_id: str | None = None created_at: int | None = None - model_config_: ModelConfig | None = Field(default=None, alias="model_config") + model_config_: ConversationModelConfig | None = Field(default=None, alias="model_config") message: MessageDetail | None = Field(default=None, validation_alias="first_message") @field_validator("created_at", mode="before") @@ -424,7 +424,7 @@ class ConversationDetail(ResponseModel): updated_at: int | None = None annotated: bool introduction: str | None = None - model_config_: ModelConfig | None = Field(default=None, alias="model_config") + model_config_: ConversationModelConfig | None = Field(default=None, alias="model_config") message_count: int user_feedback_stats: FeedbackStat | None = None admin_feedback_stats: FeedbackStat | None = None @@ -435,7 +435,7 @@ def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) -def format_files_contained(value: JSONValue) -> JSONValue: +def format_files_contained(value: AnyJSONValue) -> AnyJSONValue: if isinstance(value, File): # Response payloads must preserve legacy file keys like `related_id`/`url` # while still exposing the new graph-layer `reference` field. @@ -447,7 +447,7 @@ def format_files_contained(value: JSONValue) -> JSONValue: return value -def message_text(value: JSONValue) -> str: +def message_text(value: AnyJSONValue) -> str: if isinstance(value, list) and value: first = value[0] if isinstance(first, dict): @@ -457,7 +457,7 @@ def message_text(value: JSONValue) -> str: return "" -def extract_model_config(value: object | None) -> dict[str, JSONValue]: +def extract_model_config(value: object | None) -> dict[str, AnyJSONValue]: if value is None: return {} if isinstance(value, dict): diff --git a/api/fields/dataset_fields.py b/api/fields/dataset_fields.py index e6902f1cab25a5..6624a32aac8a70 100644 --- a/api/fields/dataset_fields.py +++ b/api/fields/dataset_fields.py @@ -1,7 +1,10 @@ from datetime import datetime +from typing import Literal from pydantic import BaseModel, Field, field_validator +from core.rag.index_processor.constant.index_type import IndexTechniqueType +from core.rag.retrieval.retrieval_methods import RetrievalMethod from fields.base import ResponseModel from libs.helper import to_timestamp from services.data_source.entities.notion_import import NotionPageType @@ -18,16 +21,26 @@ class NotionEstimateWorkspacePayload(BaseModel): pages: list[NotionEstimatePagePayload] = Field(min_length=1) +MetadataValueType = Literal["string", "number", "time"] +# Mirror models.enums.PermissionEnum / DataSourceType / DatasetRuntimeMode; response schemas do not +# import models. +DatasetPermission = Literal["only_me", "all_team_members", "partial_members"] +DatasetDataSourceType = Literal[ + "upload_file", "notion_import", "website_crawl", "local_file", "online_document", "online_drive" +] +DatasetRuntimeMode = Literal["general", "rag_pipeline"] + + class DatasetMetadataResponse(ResponseModel): id: str - type: str + type: MetadataValueType name: str class DatasetMetadataListItemResponse(ResponseModel): id: str name: str - type: str + type: MetadataValueType count: int = 0 @@ -38,7 +51,7 @@ class DatasetMetadataListResponse(ResponseModel): class DatasetMetadataBuiltInFieldResponse(ResponseModel): name: str - type: str + type: MetadataValueType class DatasetMetadataBuiltInFieldsResponse(ResponseModel): @@ -82,7 +95,7 @@ def _expand_null_nested(cls, value: object) -> object: class DatasetRetrievalModelResponse(ResponseModel): - search_method: str + search_method: RetrievalMethod reranking_enable: bool reranking_mode: str | None = None reranking_model: DatasetRerankingModelResponse = Field( @@ -129,7 +142,7 @@ class DatasetExternalRetrievalModelResponse(ResponseModel): class DatasetDocMetadataResponse(ResponseModel): id: str name: str - type: str + type: MetadataValueType class DatasetIconInfoResponse(ResponseModel): @@ -144,9 +157,10 @@ class DatasetDetailResponse(ResponseModel): name: str description: str | None provider: str - permission: str - data_source_type: str | None - indexing_technique: str | None + permission: DatasetPermission + data_source_type: DatasetDataSourceType | None + indexing_technique: IndexTechniqueType | None + keyword_number: int | None = Field(default=None, description="Keywords extracted per chunk by the economy index.") app_count: int document_count: int word_count: int @@ -178,7 +192,7 @@ class DatasetDetailResponse(ResponseModel): doc_metadata: list[DatasetDocMetadataResponse] built_in_field_enabled: bool pipeline_id: str | None - runtime_mode: str | None + runtime_mode: DatasetRuntimeMode | None chunk_structure: str | None icon_info: DatasetIconInfoResponse = Field( default_factory=DatasetIconInfoResponse, diff --git a/api/fields/document_fields.py b/api/fields/document_fields.py index d0122c36e08f25..2a831c99b8f4db 100644 --- a/api/fields/document_fields.py +++ b/api/fields/document_fields.py @@ -1,13 +1,17 @@ """Response schemas for dataset document endpoints.""" from datetime import datetime -from typing import Any +from typing import Any, Literal -from pydantic import Field, field_validator +from pydantic import AliasChoices, ConfigDict, Field, JsonValue, field_validator from fields.base import ResponseModel +from fields.dataset_fields import MetadataValueType from libs.helper import to_timestamp +# Mirrors models.enums.IndexingStatus; response schemas do not import models. +IndexingStatus = Literal["waiting", "parsing", "cleaning", "splitting", "indexing", "paused", "completed", "error"] + def normalize_enum(value: Any) -> Any: if isinstance(value, str) or value is None: @@ -15,19 +19,62 @@ def normalize_enum(value: Any) -> Any: return getattr(value, "value", value) +class DocumentPreProcessingRuleResponse(ResponseModel): + id: str + enabled: bool + + +class DocumentSegmentationResponse(ResponseModel): + # Older rules store the separator as `delimiter`. + separator: str = Field(default="\n", validation_alias=AliasChoices("separator", "delimiter")) + max_tokens: int + chunk_overlap: int = 0 + + +class DocumentProcessRulesResponse(ResponseModel): + pre_processing_rules: list[DocumentPreProcessingRuleResponse] | None = None + segmentation: DocumentSegmentationResponse | None = None + parent_mode: Literal["full-doc", "paragraph"] | None = None + subchunk_segmentation: DocumentSegmentationResponse | None = None + + +class DocumentProcessRuleResponse(ResponseModel): + mode: str + rules: DocumentProcessRulesResponse | None = None + + +class DocumentUploadFileDetailResponse(ResponseModel): + id: str + name: str + size: int + extension: str + mime_type: str | None = None + created_by: str + created_at: float + + +class DocumentSourceDetailResponse(ResponseModel): + """The uploaded file for file documents; Notion and website documents repeat their source info.""" + + model_config = ConfigDict(extra="allow") + + upload_file: DocumentUploadFileDetailResponse | None = Field(default=None, exclude_if=lambda value: value is None) + + class DocumentMetadataResponse(ResponseModel): id: str name: str - type: str - value: str | int | float | bool | None = None + type: MetadataValueType + value: str | int | float | None class DocumentResponse(ResponseModel): id: str position: int | None = None data_source_type: str | None = None - data_source_info: dict[str, Any] | None = Field(default=None, validation_alias="data_source_info_dict") - data_source_detail_dict: dict[str, Any] + # Source-specific settings, written by the data source that imported the document. + data_source_info: dict[str, JsonValue] | None = Field(default=None, validation_alias="data_source_info_dict") + data_source_detail_dict: DocumentSourceDetailResponse dataset_process_rule_id: str | None = None name: str created_from: str | None = None @@ -76,7 +123,7 @@ class DocumentListResponse(ResponseModel): class DocumentStatusResponse(ResponseModel): id: str - indexing_status: str + indexing_status: IndexingStatus processing_started_at: int | None parsing_completed_at: int | None cleaning_completed_at: int | None @@ -84,7 +131,7 @@ class DocumentStatusResponse(ResponseModel): completed_at: int | None paused_at: int | None error: str | None - error_code: str | None = None + error_code: Literal["vector_space_estimate_exceeded"] | None = None estimated_vector_space_mb: int | None = None vector_space_limit_mb: int | None = None stopped_at: int | None diff --git a/api/fields/member_fields.py b/api/fields/member_fields.py index 5108522af01400..5c81d9c5951b6c 100644 --- a/api/fields/member_fields.py +++ b/api/fields/member_fields.py @@ -41,6 +41,11 @@ def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) +class MemberRoleSummaryResponse(ResponseModel): + id: str + name: str + + class AccountWithRoleResponse(_AccountAvatarResponseMixin): id: str name: str @@ -49,7 +54,7 @@ class AccountWithRoleResponse(_AccountAvatarResponseMixin): last_active_at: int | None = None created_at: int | None = None role: str - roles: list[dict[str, str]] = Field(default_factory=list) + roles: list[MemberRoleSummaryResponse] = Field(default_factory=list) status: str @field_validator("last_login_at", "last_active_at", "created_at", mode="before") diff --git a/api/fields/message_fields.py b/api/fields/message_fields.py index 8bb91dee3d77b6..6e3f6b396415a1 100644 --- a/api/fields/message_fields.py +++ b/api/fields/message_fields.py @@ -9,11 +9,11 @@ from core.entities.execution_extra_content import ExecutionExtraContentDomainModel from fields.base import ResponseModel -from fields.conversation_fields import AgentThought, JSONValue, MessageFile +from fields.conversation_fields import AgentThought, AnyJSONValue, MessageFile from graphon.file import File from libs.helper import to_timestamp -type JSONValueType = JSONValue +type JSONValueType = AnyJSONValue UUIDString = Annotated[str, WithJsonSchema({"format": "uuid", "type": "string"})] Int64 = Annotated[int, WithJsonSchema({"format": "int64", "type": "integer"})] FloatNumber = Annotated[float, WithJsonSchema({"format": "float", "type": "number"})] diff --git a/api/fields/segment_fields.py b/api/fields/segment_fields.py index c4f73c3c08cb1d..76765e981cb8b0 100644 --- a/api/fields/segment_fields.py +++ b/api/fields/segment_fields.py @@ -1,10 +1,15 @@ from datetime import datetime +from typing import Literal from pydantic import field_serializer from fields.base import ResponseModel from libs.helper import to_timestamp +# Mirrors models.enums.SegmentStatus / SegmentType; response schemas do not import models. +SegmentStatus = Literal["waiting", "indexing", "completed", "error", "paused", "re_segment"] +SegmentType = Literal["automatic", "customized"] + class SegmentAttachmentResponse(ResponseModel): id: str @@ -21,7 +26,7 @@ class ChildChunkResponse(ResponseModel): content: str position: int word_count: int - type: str + type: SegmentType created_at: datetime | int updated_at: datetime | int @@ -46,7 +51,7 @@ class SegmentResponse(ResponseModel): enabled: bool disabled_at: datetime | int | None disabled_by: str | None - status: str + status: SegmentStatus created_by: str created_at: datetime | int updated_at: datetime | int diff --git a/api/fields/workflow_draft_variable_fields.py b/api/fields/workflow_draft_variable_fields.py index 354082edc1935a..ba3b754a078060 100644 --- a/api/fields/workflow_draft_variable_fields.py +++ b/api/fields/workflow_draft_variable_fields.py @@ -10,10 +10,10 @@ from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment from models.workflow import WorkflowDraftVariable -type JSONValue = str | int | float | bool | dict[str, "JSONValue"] | list["JSONValue"] | None +type RecursiveJSONValue = str | int | float | bool | dict[str, "RecursiveJSONValue"] | list["RecursiveJSONValue"] | None -def _convert_values_to_json_serializable_object(value: Segment) -> JSONValue: +def _convert_values_to_json_serializable_object(value: Segment) -> RecursiveJSONValue: match value: case FileSegment(): return value.value.model_dump() @@ -25,7 +25,7 @@ def _convert_values_to_json_serializable_object(value: Segment) -> JSONValue: return value.value -def _serialize_var_value(variable: WorkflowDraftVariable) -> JSONValue: +def _serialize_var_value(variable: WorkflowDraftVariable) -> RecursiveJSONValue: value = variable.get_value() # Create a copy to avoid mutating the model's cached deserialized value. value = value.model_copy(deep=True) @@ -98,7 +98,7 @@ def _from_workflow_draft_variable(cls, value: Any) -> Any: class WorkflowDraftVariableResponse(WorkflowDraftVariableWithoutValueResponse): - value: JSONValue + value: RecursiveJSONValue full_content: WorkflowDraftVariableFullContentResponse | None @model_validator(mode="before") diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 4ffaeee4227980..f2f2d4b02d88e9 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -1185,7 +1185,7 @@ Create or update Agent App message feedback | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [MessageFeedbackPayload](#messagefeedbackpayload)
| +| Yes | **application/json**: [AppMessageFeedbackPayload](#appmessagefeedbackpayload)
| #### Responses @@ -1976,7 +1976,7 @@ Submit human input form preview for advanced chat workflow | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)
| +| Yes | **application/json**: [HumanInputFormPreviewSubmitPayload](#humaninputformpreviewsubmitpayload)
| #### Responses @@ -2466,6 +2466,7 @@ Update annotation settings for an app | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string (uuid) | +| annotation_id | query | Annotation IDs to delete; omitting it clears all annotations of the app | No | [ string ] | #### Responses @@ -3032,7 +3033,7 @@ Create or update message feedback (like/dislike) | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [MessageFeedbackPayload](#messagefeedbackpayload)
| +| Yes | **application/json**: [AppMessageFeedbackPayload](#appmessagefeedbackpayload)
| #### Responses @@ -4474,7 +4475,7 @@ Submit human input form preview for workflow | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)
| +| Yes | **application/json**: [HumanInputFormPreviewSubmitPayload](#humaninputformpreviewsubmitpayload)
| #### Responses @@ -6038,6 +6039,7 @@ Get dataset auto disable logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string (uuid) | +| document_id | query | Document IDs | No | [ string ] | #### Responses @@ -6054,12 +6056,12 @@ Get documents in a dataset | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string (uuid) | -| fetch | query | Fetch full details (default: false) | No | string | -| keyword | query | Search keyword | No | string | -| limit | query | Number of items per page (default: 20) | No | string | -| page | query | Page number (default: 1) | No | string | -| sort | query | Sort order (default: -created_at) | No | string | -| status | query | Filter documents by display status | No | string | +| fetch | query | | No | string,
**Default:** false | +| keyword | query | Search keyword. | No | string | +| limit | query | Page size. | No | integer,
**Default:** 20 | +| page | query | Page number. | No | integer,
**Default:** 1 | +| sort | query | Sort by field. | No | string,
**Default:** -created_at | +| status | query | Document status. | No | string | #### Responses @@ -6169,6 +6171,7 @@ then asynchronously generates summary indexes for the provided documents. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| document_id | query | Document IDs | No | [ string ] | | action | path | | Yes | string | | dataset_id | path | | Yes | string (uuid) | @@ -6907,6 +6910,8 @@ Get dataset query history | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string (uuid) | +| limit | query | Number of items per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | #### Responses @@ -7407,7 +7412,7 @@ Request body: | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [ChatMessagePayload](#chatmessagepayload)
| +| Yes | **application/json**: [ChatMessageExplorePayload](#chatmessageexplorepayload)
| #### Responses @@ -8430,7 +8435,7 @@ Update account-level Step-by-step Tour state | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [RagPipelineOpaqueResponse](#ragpipelineopaqueresponse)
| +| 200 | Success | **application/json**: [RagPipelineRecommendedPluginsResponse](#ragpipelinerecommendedpluginsresponse)
| | default | Error | **application/json**: [ErrorResponse](#errorresponse)
| ### [POST] /rag/pipelines/transform/datasets/{dataset_id} @@ -8444,7 +8449,7 @@ Update account-level Step-by-step Tour state | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [RagPipelineOpaqueResponse](#ragpipelineopaqueresponse)
| +| 200 | Success | **application/json**: [RagPipelineTransformResponse](#ragpipelinetransformresponse)
| | 404 | Dataset or pipeline not found | **application/json**: [ErrorResponse](#errorresponse)
| | default | Error | **application/json**: [ErrorResponse](#errorresponse)
| @@ -8890,7 +8895,7 @@ Update account-level Step-by-step Tour state | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [DraftWorkflowRunPayload](#draftworkflowrunpayload)
| +| Yes | **application/json**: [RagPipelineDraftWorkflowRunPayload](#ragpipelinedraftworkflowrunpayload)
| #### Responses @@ -9069,7 +9074,7 @@ Update account-level Step-by-step Tour state | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [Parser](#parser)
| +| Yes | **application/json**: [DatasourceContentPreviewPayload](#datasourcecontentpreviewpayload)
| #### Responses @@ -9158,7 +9163,7 @@ Update account-level Step-by-step Tour state | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [RagPipelineOpaqueResponse](#ragpipelineopaqueresponse)
| +| 200 | Success | **application/json**: [RagPipelinePublishedRunResponse](#ragpipelinepublishedrunresponse)
| | 404 | Not Found | **application/json**: [PipelineNotFoundError](#pipelinenotfounderror)
[ErrorResponse](#errorresponse)
| | 429 | Too Many Requests | **application/json**: [RateLimitError](#ratelimiterror)
[ErrorResponse](#errorresponse)
| | default | Error | **application/json**: [ErrorResponse](#errorresponse)
| @@ -9382,7 +9387,7 @@ Generate rule configuration using LLM | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Rule configuration generated successfully | **application/json**: [GeneratorResponse](#generatorresponse)
| +| 200 | Rule configuration generated successfully | **application/json**: [RuleGenerateResponse](#rulegenerateresponse)
| | 400 | Invalid request parameters | **application/json**: [CompletionRequestError](#completionrequesterror)
[ModelCurrentlyNotSupportError](#modelcurrentlynotsupporterror)
[ProviderNotInitializeError](#providernotinitializeerror)
[ProviderQuotaExceededError](#providerquotaexceedederror)
[ErrorResponse](#errorresponse)
| | default | Error | **application/json**: [ErrorResponse](#errorresponse)
| @@ -9399,7 +9404,7 @@ Generate structured output rules using LLM | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Structured output generated successfully | **application/json**: [GeneratorResponse](#generatorresponse)
| +| 200 | Structured output generated successfully | **application/json**: [StructuredOutputGenerateResponse](#structuredoutputgenerateresponse)
| | 400 | Invalid request parameters | **application/json**: [CompletionRequestError](#completionrequesterror)
[ModelCurrentlyNotSupportError](#modelcurrentlynotsupporterror)
[ProviderNotInitializeError](#providernotinitializeerror)
[ProviderQuotaExceededError](#providerquotaexceedederror)
[ErrorResponse](#errorresponse)
| | default | Error | **application/json**: [ErrorResponse](#errorresponse)
| @@ -10076,7 +10081,7 @@ Reset a draft workflow variable to its default value (snippet scope) | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [PublishWorkflowPayload](#publishworkflowpayload)
| +| Yes | **application/json**: [SnippetPublishWorkflowPayload](#snippetpublishworkflowpayload)
| #### Responses @@ -11493,6 +11498,12 @@ Update a plugin endpoint | default | Error | **application/json**: [ErrorResponse](#errorresponse)
| ### [GET] /workspaces/current/members +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | + #### Responses | Code | Description | Schema | @@ -13116,6 +13127,7 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | | member_id | path | | Yes | string (uuid) | #### Responses @@ -13186,6 +13198,16 @@ Update a plugin endpoint | default | Error | **application/json**: [ErrorResponse](#errorresponse)
| ### [GET] /workspaces/current/rbac/roles +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| include_owner | query | | No | integer | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | +| limit | query | | No | integer | +| page | query | | No | integer | +| reverse | query | | No | boolean | + #### Responses | Code | Description | Schema | @@ -13194,6 +13216,18 @@ Update a plugin endpoint | default | Error | **application/json**: [ErrorResponse](#errorresponse)
| ### [POST] /workspaces/current/rbac/roles +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [_RoleUpsertRequest](#_roleupsertrequest)
| + #### Responses | Code | Description | Schema | @@ -13220,6 +13254,7 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | | role_id | path | | Yes | string (uuid) | #### Responses @@ -13234,8 +13269,15 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | | role_id | path | | Yes | string (uuid) | +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [_RoleUpsertRequest](#_roleupsertrequest)
| + #### Responses | Code | Description | Schema | @@ -13248,8 +13290,15 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | | role_id | path | | Yes | string (uuid) | +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CopyRoleParam](#copyroleparam)
| + #### Responses | Code | Description | Schema | @@ -13262,6 +13311,10 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| language | query | Localized policy label language | No | string,
**Available values:** "en", "ja", "zh" | +| limit | query | | No | integer | +| page | query | | No | integer | +| reverse | query | | No | boolean | | role_id | path | | Yes | string (uuid) | #### Responses @@ -14821,7 +14874,7 @@ Model class for AI model. | deprecated | boolean | | No | | features | [ [ModelFeature](#modelfeature) ] | | No | | fetch_from | [FetchFrom](#fetchfrom) | | Yes | -| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | model | string | | Yes | | model_properties | object | | Yes | | model_type | [ModelType](#modeltype) | | Yes | @@ -14835,7 +14888,7 @@ Model class for AI model. | deprecated | boolean | | No | | features | [ [ModelFeature](#modelfeature) ] | | No | | fetch_from | [FetchFrom](#fetchfrom) | | Yes | -| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | model | string | | Yes | | model_properties | object | | Yes | | model_type | [ModelType](#modeltype) | | Yes | @@ -15119,7 +15172,7 @@ Account not link tenant. | last_login_at | integer | | No | | name | string | | Yes | | role | string | | Yes | -| roles | [ object ] | | No | +| roles | [ [MemberRoleSummaryResponse](#memberrolesummaryresponse) ] | | No | | status | string | | Yes | #### ActivateCheckQuery @@ -15213,8 +15266,9 @@ Account not link tenant. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| chat_prompt_config | object | | No | -| completion_prompt_config | object | | No | +| chat_prompt_config | [ChatPromptTemplateResponse](#chatprompttemplateresponse) | | No | +| completion_prompt_config | [CompletionPromptTemplateResponse](#completionprompttemplateresponse) | | No | +| stop | [ string ] | Stop words for completion prompts. | No | #### AgentAccessMatrix @@ -17030,10 +17084,10 @@ Inherits from ToolProviderIdentity, without any additional fields. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | author | string | The author of the tool | Yes | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The description of the tool | Yes | +| description | [I18nObject](#i18nobject) | The description of the tool | Yes | | icon | string | The icon of the tool | Yes | | icon_dark | string | The dark icon of the tool | No | -| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The label of the tool | Yes | +| label | [I18nObject](#i18nobject) | The label of the tool | Yes | | name | string | The name of the tool | Yes | | tags | [ [ToolLabelEnum](#toollabelenum) ] | The tags of the tool | No | @@ -17080,7 +17134,7 @@ Legacy Chat App model config used only for follow-up question generation. | thought | string | | No | | tool | string | | No | | tool_input | string | | No | -| tool_labels | [JSONValue](#jsonvalue) | Labels for tools used. | Yes | +| tool_labels | [AnyJSONValue](#anyjsonvalue) | Labels for tools used. | Yes | #### AgentTokenUsageStatisticResponse @@ -17105,9 +17159,9 @@ Legacy Chat App model config used only for follow-up question generation. | error | string | | No | | status | string | | Yes | | time_cost | number
integer | | Yes | -| tool_icon | | | No | +| tool_icon | string
object | Icon URL or emoji icon | No | | tool_input | object | | Yes | -| tool_label | string | | Yes | +| tool_label | string
object | Tool name or its i18n labels keyed by locale | Yes | | tool_name | string | | Yes | | tool_output | object | | Yes | | tool_parameters | object | | Yes | @@ -17284,6 +17338,12 @@ Auth Token is invalid or account already activated, please check again. | ---- | ---- | ----------- | -------- | | score_threshold | number | Score threshold | Yes | +#### AnyJSONValue + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| AnyJSONValue | | | | + #### ApiBaseUrlResponse | Name | Type | Description | Required | @@ -18150,6 +18210,14 @@ Publication state of a Dify app exposed as an MCP server; only ACTIVE servers ac | ---- | ---- | ----------- | -------- | | AppMCPServerStatus | string | Publication state of a Dify app exposed as an MCP server; only ACTIVE servers accept MCP calls. | | +#### AppMessageFeedbackPayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | Optional text feedback providing additional detail. | No | +| message_id | string | Message ID | Yes | +| rating | string,
**Available values:** "dislike", "like" | Feedback rating. Set to `null` to revoke previously submitted feedback. | No | + #### AppMetadataConditionPayload | Name | Type | Description | Required | @@ -18727,7 +18795,7 @@ Audio size exceeded. {message} | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| text | string | | Yes | +| text | string | Transcribed text from audio | Yes | #### AuthenticationFailedError @@ -19015,6 +19083,17 @@ You cannot transfer ownership to yourself. | sort_by | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | Sort field and direction
*Enum:* `"-created_at"`, `"-updated_at"`, `"created_at"`, `"updated_at"` | No | | start | string | Start date (YYYY-MM-DD HH:MM) | No | +#### ChatMessageExplorePayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| conversation_id | string | | No | +| files | [ object ] | | No | +| inputs | object | | Yes | +| parent_message_id | string | | No | +| query | string | | Yes | +| retriever_from | string,
**Default:** explore_app | | No | + #### ChatMessagePayload | Name | Type | Description | Required | @@ -19037,6 +19116,12 @@ You cannot transfer ownership to yourself. | first_id | string | First message ID for pagination | No | | limit | integer,
**Default:** 20 | Number of messages to return (1-100) | No | +#### ChatPromptTemplateResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| prompt | [ [PromptTemplateMessageResponse](#prompttemplatemessageresponse) ] | | Yes | + #### ChatRequest | Name | Type | Description | Required | @@ -19136,7 +19221,7 @@ Create child chunk index failed: {message} | id | string | | Yes | | position | integer | | Yes | | segment_id | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "automatic", "customized" | *Enum:* `"automatic"`, `"customized"` | Yes | | updated_at | integer | | Yes | | word_count | integer | | Yes | @@ -19165,6 +19250,14 @@ TEAM: Team collaboration paid plan | ---- | ---- | ----------- | -------- | | CloudPlan | string | Enum representing user plan types in the cloud platform. SANDBOX: Free/default plan with limited features PROFESSIONAL: Professional paid plan TEAM: Team collaboration paid plan | | +#### CodeBasedExtensionItemResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| form_schema | [ object ] | | No | +| label | object | | No | +| name | string | | Yes | + #### CodeBasedExtensionQuery | Name | Type | Description | Required | @@ -19175,7 +19268,7 @@ TEAM: Team collaboration paid plan | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| data | | Extension data | Yes | +| data | [ [CodeBasedExtensionItemResponse](#codebasedextensionitemresponse) ] | Non-builtin extensions of the module | Yes | | module | string | Module name | Yes | #### CompletionConversationQuery @@ -19210,6 +19303,19 @@ TEAM: Team collaboration paid plan | response_mode | string,
**Available values:** "blocking", "streaming",
**Default:** blocking | Response mode
*Enum:* `"blocking"`, `"streaming"` | No | | retriever_from | string,
**Default:** dev | Retriever source | No | +#### CompletionPromptTemplateResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| conversation_histories_role | [ConversationHistoriesRoleResponse](#conversationhistoriesroleresponse) | | No | +| prompt | [CompletionPromptTemplateTextResponse](#completionprompttemplatetextresponse) | | Yes | + +#### CompletionPromptTemplateTextResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| text | string | | Yes | + #### CompletionRequest | Name | Type | Description | Required | @@ -19443,7 +19549,7 @@ The conversation cursor is not in the current list. Refresh the list and try aga | id | string | | Yes | | introduction | string | | No | | message_count | integer | | Yes | -| model_config | [ModelConfig](#modelconfig) | | No | +| model_config | [ConversationModelConfig](#conversationmodelconfig) | | No | | status | string | | Yes | | updated_at | integer | | No | | user_feedback_stats | [FeedbackStat](#feedbackstat) | | No | @@ -19456,6 +19562,13 @@ The conversation has no message from which to generate a name. | ---- | ---- | ----------- | -------- | | ConversationFirstMessageNotFoundError | | The conversation has no message from which to generate a name. | | +#### ConversationHistoriesRoleResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| assistant_prefix | string | | Yes | +| user_prefix | string | | Yes | + #### ConversationInfiniteScrollPagination | Name | Type | Description | Required | @@ -19482,9 +19595,20 @@ The conversation has no message from which to generate a name. | from_source | string | | Yes | | id | string | | Yes | | message | [MessageDetail](#messagedetail) | | No | -| model_config | [ModelConfig](#modelconfig) | | No | +| model_config | [ConversationModelConfig](#conversationmodelconfig) | | No | | status | string | | Yes | +#### ConversationModelConfig + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| agent_mode | [AnyJSONValue](#anyjsonvalue) | | No | +| model | [AnyJSONValue](#anyjsonvalue) | | No | +| opening_statement | string | | No | +| pre_prompt | string | | No | +| suggested_questions | [AnyJSONValue](#anyjsonvalue) | | No | +| user_input_form | [AnyJSONValue](#anyjsonvalue) | | No | + #### ConversationNameRequiredError A name is required when automatic naming is disabled. @@ -19604,6 +19728,12 @@ The conversation was not found for this account and app. | icon_type | [IconType](#icontype) | Icon type | No | | name | string | Name for the copied app | No | +#### CopyRoleParam + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| copy_member | boolean,
**Default:** true | | No | + #### CrawlFailedError {message} @@ -19646,6 +19776,12 @@ Payload for creating a new snippet. | name | string | | Yes | | type | string,
**Available values:** "group", "node",
**Default:** node | *Enum:* `"group"`, `"node"` | No | +#### CreatorUserRole + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| CreatorUserRole | string | | | + #### CredentialConfiguration Model class for credential configuration. @@ -19662,10 +19798,10 @@ Model class for credential form schema. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | default | string | | No | -| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | max_length | integer | | No | | options | [ [FormOption](#formoption) ] | | No | -| placeholder | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | +| placeholder | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | | required | boolean,
**Default:** true | | No | | show_on | [ [FormShowOnObject](#formshowonobject) ],
**Default:** | | No | | type | [FormType](#formtype) | | Yes | @@ -19919,7 +20055,7 @@ Dataset access denied. | chunk_structure | string | | Yes | | created_at | integer | | Yes | | created_by | string | | Yes | -| data_source_type | string | | Yes | +| data_source_type | string,
**Available values:** "local_file", "notion_import", "online_document", "online_drive", "upload_file", "website_crawl" | | Yes | | description | string | | Yes | | doc_form | string | | Yes | | doc_metadata | [ [DatasetDocMetadataResponse](#datasetdocmetadataresponse) ] | | Yes | @@ -19932,17 +20068,18 @@ Dataset access denied. | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | | icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | Icon display configuration for the knowledge base. | No | | id | string | | Yes | -| indexing_technique | string | | Yes | +| indexing_technique | [IndexTechniqueType](#indextechniquetype) | | Yes | | is_multimodal | boolean | | Yes | | is_published | boolean | | Yes | +| keyword_number | integer | Keywords extracted per chunk by the economy index. | No | | maintainer | string | | No | | name | string | | Yes | -| permission | string | | Yes | +| permission | string,
**Available values:** "all_team_members", "only_me", "partial_members" | *Enum:* `"all_team_members"`, `"only_me"`, `"partial_members"` | Yes | | permission_keys | [ string ] | | No | | pipeline_id | string | | Yes | | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | -| runtime_mode | string | | Yes | +| runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | @@ -19961,7 +20098,7 @@ Dataset access denied. | chunk_structure | string | | Yes | | created_at | integer | | Yes | | created_by | string | | Yes | -| data_source_type | string | | Yes | +| data_source_type | string,
**Available values:** "local_file", "notion_import", "online_document", "online_drive", "upload_file", "website_crawl" | | Yes | | description | string | | Yes | | doc_form | string | | Yes | | doc_metadata | [ [DatasetDocMetadataResponse](#datasetdocmetadataresponse) ] | | Yes | @@ -19974,18 +20111,19 @@ Dataset access denied. | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | | icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | Icon display configuration for the knowledge base. | No | | id | string | | Yes | -| indexing_technique | string | | Yes | +| indexing_technique | [IndexTechniqueType](#indextechniquetype) | | Yes | | is_multimodal | boolean | | Yes | | is_published | boolean | | Yes | +| keyword_number | integer | Keywords extracted per chunk by the economy index. | No | | maintainer | string | | No | | name | string | | Yes | | partial_member_list | [ string ] | | No | -| permission | string | | Yes | +| permission | string,
**Available values:** "all_team_members", "only_me", "partial_members" | *Enum:* `"all_team_members"`, `"only_me"`, `"partial_members"` | Yes | | permission_keys | [ string ] | | No | | pipeline_id | string | | Yes | | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | -| runtime_mode | string | | Yes | +| runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | @@ -20000,7 +20138,7 @@ Dataset access denied. | ---- | ---- | ----------- | -------- | | id | string | | Yes | | name | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### DatasetExternalKnowledgeInfoResponse @@ -20044,7 +20182,7 @@ Dataset access denied. | chunk_structure | string | | Yes | | created_at | integer | | Yes | | created_by | string | | Yes | -| data_source_type | string | | Yes | +| data_source_type | string,
**Available values:** "local_file", "notion_import", "online_document", "online_drive", "upload_file", "website_crawl" | | Yes | | description | string | | Yes | | doc_form | string | | Yes | | doc_metadata | [ [DatasetDocMetadataResponse](#datasetdocmetadataresponse) ] | | Yes | @@ -20057,18 +20195,19 @@ Dataset access denied. | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | | icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | Icon display configuration for the knowledge base. | No | | id | string | | Yes | -| indexing_technique | string | | Yes | +| indexing_technique | [IndexTechniqueType](#indextechniquetype) | | Yes | | is_multimodal | boolean | | Yes | | is_published | boolean | | Yes | +| keyword_number | integer | Keywords extracted per chunk by the economy index. | No | | maintainer | string | | No | | name | string | | Yes | | partial_member_list | [ string ] | | Yes | -| permission | string | | Yes | +| permission | string,
**Available values:** "all_team_members", "only_me", "partial_members" | *Enum:* `"all_team_members"`, `"only_me"`, `"partial_members"` | Yes | | permission_keys | [ string ] | | No | | pipeline_id | string | | Yes | | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | -| runtime_mode | string | | Yes | +| runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | @@ -20092,7 +20231,7 @@ Dataset access denied. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | name | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### DatasetMetadataBuiltInFieldsResponse @@ -20107,7 +20246,7 @@ Dataset access denied. | count | integer | | No | | id | string | | Yes | | name | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### DatasetMetadataListResponse @@ -20122,7 +20261,7 @@ Dataset access denied. | ---- | ---- | ----------- | -------- | | id | string | | Yes | | name | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### DatasetNameDuplicateError @@ -20137,7 +20276,7 @@ The dataset name already exists. Please modify your dataset name. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | content | string | | Yes | -| content_type | string | | Yes | +| content_type | [QueryType](#querytype) | | Yes | | file_info | [DatasetQueryFileInfoResponse](#datasetqueryfileinforesponse) | | No | #### DatasetQueryDetailResponse @@ -20146,10 +20285,10 @@ The dataset name already exists. Please modify your dataset name. | ---- | ---- | ----------- | -------- | | created_at | integer | | Yes | | created_by | string | | Yes | -| created_by_role | string | | Yes | +| created_by_role | [CreatorUserRole](#creatoruserrole) | | Yes | | id | string | | Yes | | queries | [ [DatasetQueryContentResponse](#datasetquerycontentresponse) ] | | Yes | -| source | string | | Yes | +| source | [DatasetQuerySource](#datasetquerysource) | | Yes | | source_app_id | string | | Yes | #### DatasetQueryFileInfoResponse @@ -20173,6 +20312,14 @@ The dataset name already exists. Please modify your dataset name. | page | integer | | Yes | | total | integer | | Yes | +#### DatasetQuerySource + +Dataset query source + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| DatasetQuerySource | string | Dataset query source | | + #### DatasetRerankingModelResponse | Name | Type | Description | Required | @@ -20202,7 +20349,7 @@ The dataset name already exists. Please modify your dataset name. | reranking_model | [DatasetRerankingModelResponse](#datasetrerankingmodelresponse) | Reranking model configuration. | No | | score_threshold | number | | No | | score_threshold_enabled | boolean | | Yes | -| search_method | string | | Yes | +| search_method | [RetrievalMethod](#retrievalmethod) | | Yes | | top_k | integer | | Yes | | weights | [DatasetWeightedScoreResponse](#datasetweightedscoreresponse) | | No | @@ -20258,6 +20405,14 @@ The dataset name already exists. Please modify your dataset name. | vector_setting | [DatasetVectorSettingResponse](#datasetvectorsettingresponse) | Semantic search weight settings. | No | | weight_type | string | | No | +#### DatasourceContentPreviewPayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| credential_id | string | | No | +| datasource_type | string | | Yes | +| inputs | object | | Yes | + #### DatasourceCredentialDeletePayload | Name | Type | Description | Required | @@ -20440,9 +20595,9 @@ Datasource provider entity | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | author | string | The author of the tool | Yes | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The description of the tool | Yes | +| description | [I18nObject](#i18nobject) | The description of the tool | Yes | | icon | string | The icon of the tool | Yes | -| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The label of the tool | Yes | +| label | [I18nObject](#i18nobject) | The label of the tool | Yes | | name | string | The name of the tool | Yes | | tags | [ [ToolLabelEnum](#toollabelenum) ] | The tags of the tool | No | @@ -20638,10 +20793,10 @@ Request payload for bulk downloading documents as a zip archive. | created_at | integer | | No | | created_by | string | | No | | created_from | string | | No | -| data_source_detail_dict | | | No | -| data_source_info | | | No | +| data_source_detail_dict | [DocumentSourceDetailResponse](#documentsourcedetailresponse) | | No | +| data_source_info | object | | No | | data_source_type | string | | No | -| dataset_process_rule | | | No | +| dataset_process_rule | [DocumentProcessRuleResponse](#documentprocessruleresponse) | | No | | dataset_process_rule_id | string | | No | | disabled_at | integer | | No | | disabled_by | string | | No | @@ -20650,7 +20805,7 @@ Request payload for bulk downloading documents as a zip archive. | doc_language | string | | No | | doc_metadata | [ [DocumentMetadataResponse](#documentmetadataresponse) ] | | No | | doc_type | string | | No | -| document_process_rule | | | No | +| document_process_rule | [DocumentProcessRuleResponse](#documentprocessruleresponse) | | No | | enabled | boolean | | No | | error | string | | No | | hit_count | integer | | No | @@ -20686,8 +20841,8 @@ The document is being processed and cannot be edited. | ---- | ---- | ----------- | -------- | | id | string | | Yes | | name | string | | Yes | -| type | string | | Yes | -| value | string
integer
number
boolean | | No | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | +| value | string
integer
number | | Yes | #### DocumentMetadataUpdatePayload @@ -20705,6 +20860,29 @@ The document is being processed and cannot be edited. | datasource_type | string | | No | | input_data | [JsonValue](#jsonvalue) | | No | +#### DocumentPreProcessingRuleResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| enabled | boolean | | Yes | +| id | string | | Yes | + +#### DocumentProcessRuleResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| mode | string | | Yes | +| rules | [DocumentProcessRulesResponse](#documentprocessrulesresponse) | | No | + +#### DocumentProcessRulesResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| parent_mode | string,
**Available values:** "full-doc", "paragraph" | | No | +| pre_processing_rules | [ [DocumentPreProcessingRuleResponse](#documentpreprocessingruleresponse) ] | | No | +| segmentation | [DocumentSegmentationResponse](#documentsegmentationresponse) | | No | +| subchunk_segmentation | [DocumentSegmentationResponse](#documentsegmentationresponse) | | No | + #### DocumentRenamePayload | Name | Type | Description | Required | @@ -20719,7 +20897,7 @@ The document is being processed and cannot be edited. | created_at | integer | | No | | created_by | string | | No | | created_from | string | | No | -| data_source_detail_dict | object | | Yes | +| data_source_detail_dict | [DocumentSourceDetailResponse](#documentsourcedetailresponse) | | Yes | | data_source_info | object | | No | | data_source_type | string | | No | | dataset_process_rule_id | string | | No | @@ -20746,6 +20924,23 @@ The document is being processed and cannot be edited. | ---- | ---- | ----------- | -------- | | document_ids | [ string ] | | Yes | +#### DocumentSegmentationResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| chunk_overlap | integer | | No | +| max_tokens | integer | | Yes | +| separator | string,
**Default:** + | | No | + +#### DocumentSourceDetailResponse + +The uploaded file for file documents; Notion and website documents repeat their source info. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| upload_file | [DocumentUploadFileDetailResponse](#documentuploadfiledetailresponse) | | No | + #### DocumentStatusListResponse | Name | Type | Description | Required | @@ -20760,14 +20955,17 @@ The document is being processed and cannot be edited. | completed_at | integer | | Yes | | completed_segments | integer | | No | | error | string | | Yes | +| error_code | string | | No | +| estimated_vector_space_mb | integer | | No | | id | string | | Yes | -| indexing_status | string | | Yes | +| indexing_status | string,
**Available values:** "cleaning", "completed", "error", "indexing", "parsing", "paused", "splitting", "waiting" | *Enum:* `"cleaning"`, `"completed"`, `"error"`, `"indexing"`, `"parsing"`, `"paused"`, `"splitting"`, `"waiting"` | Yes | | parsing_completed_at | integer | | Yes | | paused_at | integer | | Yes | | processing_started_at | integer | | Yes | | splitting_completed_at | integer | | Yes | | stopped_at | integer | | Yes | | total_segments | integer | | No | +| vector_space_limit_mb | integer | | No | #### DocumentSummaryStatusResponse @@ -20777,6 +20975,18 @@ The document is being processed and cannot be edited. | summary_status | [SummaryStatusResponse](#summarystatusresponse) | | Yes | | total_segments | integer | | Yes | +#### DocumentUploadFileDetailResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| created_at | number | | Yes | +| created_by | string | | Yes | +| extension | string | | Yes | +| id | string | | Yes | +| mime_type | string | | No | +| name | string | | Yes | +| size | integer | | Yes | + #### DocumentWithSegmentsListResponse | Name | Type | Description | Required | @@ -20796,7 +21006,7 @@ The document is being processed and cannot be edited. | created_at | integer | | No | | created_by | string | | No | | created_from | string | | No | -| data_source_detail_dict | object | | Yes | +| data_source_detail_dict | [DocumentSourceDetailResponse](#documentsourcedetailresponse) | | Yes | | data_source_info | object | | No | | data_source_type | string | | No | | dataset_process_rule_id | string | | No | @@ -20813,7 +21023,7 @@ The document is being processed and cannot be edited. | name | string | | Yes | | need_summary | boolean | | No | | position | integer | | No | -| process_rule_dict | | | No | +| process_rule_dict | [DocumentProcessRuleResponse](#documentprocessruleresponse) | | No | | summary_index_status | string | | No | | tokens | integer | | No | | total_segments | integer | | No | @@ -20847,10 +21057,8 @@ Workflow graph might have been modified, please refresh and resubmit. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| datasource_info_list | [ object ] | | Yes | -| datasource_type | string | | Yes | +| files | [ object ] | | No | | inputs | object | | Yes | -| start_node_id | string | | Yes | #### DraftWorkflowSyncPayload @@ -21365,7 +21573,7 @@ The configuration of an event | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The description of the event | Yes | +| description | [I18nObject](#i18nobject) | The description of the event | Yes | | identity | [EventIdentity](#eventidentity) | | Yes | | output_schema | object | The output schema that this event produces | No | | parameters | [ [EventParameter](#eventparameter) ] | The parameters of the event | No | @@ -21555,9 +21763,16 @@ Flask blueprint initialization. | description | string | | Yes | | id | string | | Yes | | name | string | | Yes | -| settings | object | | Yes | +| settings | [ExternalKnowledgeApiSettingsResponse](#externalknowledgeapisettingsresponse) | | Yes | | tenant_id | string | | Yes | +#### ExternalKnowledgeApiSettingsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| api_key | string | | No | +| endpoint | string | | No | + #### FeatureModel Effective feature availability and limits for the current workspace. @@ -21610,8 +21825,8 @@ Enum class for fetch from. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | -| placeholder | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | +| placeholder | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | #### FileExtensionBlockedError @@ -21625,7 +21840,10 @@ The file extension is blocked for security reasons. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| file_ids | [ string ] | | Yes | +| extension | string | | Yes | +| filename | string | | Yes | +| mimetype | string | | Yes | +| size | integer | | Yes | #### FileInputConfig @@ -21781,7 +21999,7 @@ Model class for form option. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | show_on | [ [FormShowOnObject](#formshowonobject) ],
**Default:** | | No | | value | string | | Yes | @@ -21820,15 +22038,6 @@ Enum class for form type. | ---- | ---- | ----------- | -------- | | GeneratorResponse | | | | -#### Github - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| github_plugin_unique_identifier | string | | Yes | -| package | string | | Yes | -| repo | string | | Yes | -| version | string | | Yes | - #### HitTestingChildChunk | Name | Type | Description | Required | @@ -21979,6 +22188,14 @@ Enum class for form type. | node_title | string | | Yes | | resolved_default_values | object | | No | +#### HumanInputFormPreviewSubmitPayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| action | string | Selected action ID | Yes | +| form_inputs | object | Values the user provides for the form's own fields | Yes | +| inputs | object | Values used to fill missing upstream variables referenced in form_content | Yes | + #### HumanInputFormSubmissionData | Name | Type | Description | Required | @@ -21994,9 +22211,8 @@ Enum class for form type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| action | string | Selected action ID | Yes | -| form_inputs | object | Values the user provides for the form's own fields | Yes | -| inputs | object | Values used to fill missing upstream variables referenced in form_content | Yes | +| action | string | ID of the action button the recipient selected. Must match one of the `id` values from the form's `user_actions` list. | Yes | +| inputs | object | Submitted human input values keyed by output variable name. Use a string for paragraph or select input values, a file mapping for file inputs, and a list of file mappings for file-list inputs. Local file mappings use `transfer_method=local_file` with `upload_file_id`; remote file mappings use `transfer_method=remote_url` with `url` or `remote_url`. | Yes | #### HumanInputFormSubmitResponse @@ -22082,6 +22298,12 @@ How Dify forwards the end-user's identity to an MCP server. | ---- | ---- | ----------- | -------- | | include_secret | string,
**Default:** false | Whether to include secret values in the exported DSL | No | +#### IndexTechniqueType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| IndexTechniqueType | string | | | + #### IndexingEstimate | Name | Type | Description | Required | @@ -22125,7 +22347,7 @@ Knowledge indexing estimate failed: {message} | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | data_source_type | string,
**Available values:** "notion_import", "upload_file", "website_crawl" | *Enum:* `"notion_import"`, `"upload_file"`, `"website_crawl"` | Yes | -| file_info_list | [FileInfo](#fileinfo) | | No | +| file_info_list | [KnowledgeFileInfo](#knowledgefileinfo) | | No | | notion_info_list | [ [NotionInfo](#notioninfo) ] | | No | | website_info_list | [WebsiteInfo](#websiteinfo) | | No | @@ -22366,12 +22588,6 @@ This invitation was sent to another account. Please sign in with the invited acc | ---- | ---- | ----------- | -------- | | inputs | object | | No | -#### JSONValue - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| JSONValue | string
integer
number
boolean
object
[ object ] | | | - #### JSONValueType | Name | Type | Description | Required | @@ -22408,6 +22624,12 @@ This invitation was sent to another account. Please sign in with the invited acc | retrieval_model | [RetrievalModel](#retrievalmodel) | Retrieval model configuration. Controls how chunks are searched and ranked in this knowledge base. | No | | summary_index_setting | object | Summary index configuration. | No | +#### KnowledgeFileInfo + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| file_ids | [ string ] | | Yes | + #### KnowledgePipeline | Name | Type | Description | Required | @@ -22621,13 +22843,6 @@ Unable to create account because the licensed seats limit was exceeded | parameters | object | Server parameters configuration | Yes | | status | [AppMCPServerStatus](#appmcpserverstatus) | Server status | No | -#### Marketplace - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| marketplace_plugin_unique_identifier | string | | Yes | -| version | string | | No | - #### MaxKeysExceededError The API key limit has been reached. @@ -22713,6 +22928,13 @@ The member is not in the workspace. | ---- | ---- | ----------- | -------- | | MemberNotInTenantError | | The member is not in the workspace. | | +#### MemberRoleSummaryResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | string | | Yes | +| name | string | | Yes | + #### MemberRoleUpdatePayload | Name | Type | Description | Required | @@ -22759,10 +22981,10 @@ The message cursor is not in this conversation. Refresh the list and try again. | from_source | string | | Yes | | id | string | | Yes | | inputs | object | | Yes | -| message | [JSONValue](#jsonvalue) | | Yes | +| message | [AnyJSONValue](#anyjsonvalue) | | Yes | | message_files | [ [MessageFile](#messagefile) ] | | Yes | | message_tokens | integer | | Yes | -| metadata | [JSONValue](#jsonvalue) | | Yes | +| metadata | [AnyJSONValue](#anyjsonvalue) | | Yes | | parent_message_id | string | | No | | provider_response_latency | number | | Yes | | query | string | | Yes | @@ -22788,10 +23010,10 @@ The message cursor is not in this conversation. Refresh the list and try again. | from_source | string | | Yes | | id | string | | Yes | | inputs | object | | Yes | -| message | [JSONValue](#jsonvalue) | | Yes | +| message | [AnyJSONValue](#anyjsonvalue) | | Yes | | message_files | [ [MessageFile](#messagefile) ] | | Yes | | message_tokens | integer | | Yes | -| metadata | [JSONValue](#jsonvalue) | | Yes | +| metadata | [AnyJSONValue](#anyjsonvalue) | | Yes | | parent_message_id | string | | No | | provider_response_latency | number | | Yes | | query | string | | Yes | @@ -22803,7 +23025,6 @@ The message cursor is not in this conversation. Refresh the list and try again. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | content | string | Optional text feedback providing additional detail. | No | -| message_id | string | Message ID | Yes | | rating | string,
**Available values:** "dislike", "like" | Feedback rating. Set to `null` to revoke previously submitted feedback. | No | #### MessageFeedbackRatingRequiredError @@ -23067,11 +23288,11 @@ Fields required to render the collapsed model-provider list. | ---- | ---- | ----------- | -------- | | configurate_methods | [ [ConfigurateMethod](#configuratemethod) ] | | Yes | | custom_configuration | [ModelProviderCustomConfigurationSummaryResponse](#modelprovidercustomconfigurationsummaryresponse) | | Yes | -| description | [I18nObject](#i18nobject) | | No | -| icon_small | [I18nObject](#i18nobject) | | No | -| icon_small_dark | [I18nObject](#i18nobject) | | No | +| description | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| icon_small | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| icon_small_dark | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | | is_configured | boolean | | Yes | -| label | [I18nObject](#i18nobject) | | Yes | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | plugin_id | string | | Yes | | preferred_provider_type | [ProviderType](#providertype) | | Yes | | provider | string | | Yes | @@ -23084,6 +23305,15 @@ Fields required to render the collapsed model-provider list. | ---- | ---- | ----------- | -------- | | enabled | boolean | | Yes | +#### ModelRuntimeI18nObject + +Model class for i18n object. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| en_US | string | | Yes | +| zh_Hans | string | | No | + #### ModelSelectorScope | Name | Type | Description | Required | @@ -23116,7 +23346,7 @@ Model with provider entity. | features | [ [ModelFeature](#modelfeature) ] | | No | | fetch_from | [FetchFrom](#fetchfrom) | Where the model definition comes from. `predefined-model` for built-in models, `customizable-model` for user-configured models. | Yes | | has_invalid_load_balancing_configs | boolean | | No | -| label | [I18nObject](#i18nobject) | Localized display name of the model. | Yes | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | Localized display name of the model. | Yes | | load_balancing_enabled | boolean | | No | | model | string | | Yes | | model_properties | object | | Yes | @@ -23598,13 +23828,6 @@ Too many owner transfer emails have been sent. Please try again in {minutes} min | ---- | ---- | ----------- | -------- | | OwnerTransferRateLimitExceededError | | Too many owner transfer emails have been sent. Please try again in {minutes} minutes. | | -#### Package - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| plugin_unique_identifier | string | | Yes | -| version | string | | No | - #### PaginatedConversationVariableResponse | Name | Type | Description | Required | @@ -23648,8 +23871,8 @@ Model class for parameter rule. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | default | | | No | -| help | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | -| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| help | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | max | number | | No | | min | number | | No | | name | string | | Yes | @@ -23684,14 +23907,6 @@ Enum class for parameter type. | text_to_speech | { **"autoPlay"**: string, **"enabled"**: boolean, **"language"**: string, **"voice"**: string } | | Yes | | user_input_form | [ object ] | | Yes | -#### Parser - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| credential_id | string | | No | -| datasource_type | string | | Yes | -| inputs | object | | Yes | - #### ParserAsset | Name | Type | Description | Required | @@ -24051,6 +24266,36 @@ Shared permission levels for resources (datasets, credentials, etc.) | ---- | ---- | ----------- | -------- | | PermissionEnum | string | Shared permission levels for resources (datasets, credentials, etc.) | | +#### PipelineDataset + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| chunk_structure | string | | Yes | +| description | string | knowledge dataset description | No | +| id | string | | Yes | +| name | string | | Yes | + +#### PipelineDocument + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| data_source_info | object | | No | +| data_source_type | string | | Yes | +| enabled | boolean | | Yes | +| error | string | | No | +| id | string | | Yes | +| indexing_status | string | | Yes | +| name | string | | Yes | +| position | integer | | Yes | + +#### PipelineGenerateResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| batch | string | | Yes | +| dataset | [PipelineDataset](#pipelinedataset) | | Yes | +| documents | [ [PipelineDocument](#pipelinedocument) ] | | Yes | + #### PipelineNotFoundError Pipeline not found. @@ -24176,7 +24421,31 @@ Plaintext secret values are not allowed in Agent config. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | type | [PluginBundleDependencyType](#pluginbundledependencytype) | | Yes | -| value | [Github](#github)
[Marketplace](#marketplace)
[Package](#package) | | Yes | +| value | [PluginBundleDependencyGithub](#pluginbundledependencygithub)
[PluginBundleDependencyMarketplace](#pluginbundledependencymarketplace)
[PluginBundleDependencyPackage](#pluginbundledependencypackage) | | Yes | + +#### PluginBundleDependencyGithub + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| packages | string | | Yes | +| release | string | | Yes | +| repo | string | | Yes | +| repo_address | string | | Yes | + +#### PluginBundleDependencyMarketplace + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| organization | string | | Yes | +| plugin | string | | Yes | +| version | string | | Yes | + +#### PluginBundleDependencyPackage + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| manifest | [PluginDeclaration](#plugindeclaration) | | Yes | +| unique_identifier | string | | Yes | #### PluginBundleDependencyType @@ -24202,12 +24471,12 @@ Plaintext secret values are not allowed in Agent config. | ---- | ---- | ----------- | -------- | | allow_delete | boolean | | Yes | | author | string | | Yes | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| description | [I18nObject](#i18nobject) | | Yes | | icon | string
object | | Yes | | icon_dark | string
object | | Yes | | id | string | | Yes | | is_team_authorization | boolean | | Yes | -| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| label | [I18nObject](#i18nobject) | | Yes | | labels | [ string ] | | Yes | | name | string | | Yes | | plugin_id | string | | Yes | @@ -24221,8 +24490,8 @@ Plaintext secret values are not allowed in Agent config. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | author | string | | Yes | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | -| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| description | [I18nObject](#i18nobject) | | Yes | +| label | [I18nObject](#i18nobject) | | Yes | | labels | [ string ] | | Yes | | name | string | | Yes | | output_schema | object | | Yes | @@ -24284,11 +24553,11 @@ Plaintext secret values are not allowed in Agent config. | category | [PluginCategory](#plugincategory) | | Yes | | created_at | dateTime | | Yes | | datasource | [DatasourceProviderEntity](#datasourceproviderentity) | | No | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| description | [I18nObject](#i18nobject) | | Yes | | endpoint | [EndpointProviderDeclaration](#endpointproviderdeclaration) | | No | | icon | string | | Yes | | icon_dark | string | | No | -| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| label | [I18nObject](#i18nobject) | | Yes | | meta | [Meta](#meta) | | Yes | | model | [ProviderEntity](#providerentity) | | No | | name | string | | Yes | @@ -24310,11 +24579,11 @@ Plaintext secret values are not allowed in Agent config. | category | [PluginCategory](#plugincategory) | | Yes | | created_at | dateTime | | Yes | | datasource | object | | No | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| description | [I18nObject](#i18nobject) | | Yes | | endpoint | object | | No | | icon | string | | Yes | | icon_dark | string | | No | -| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| label | [I18nObject](#i18nobject) | | Yes | | meta | object | | Yes | | model | [ProviderEntityResponse](#providerentityresponse) | | No | | name | string | | Yes | @@ -24341,7 +24610,30 @@ Plaintext secret values are not allowed in Agent config. | ---- | ---- | ----------- | -------- | | current_identifier | string | | No | | type | [PluginDependencyType](#plugindependencytype) | | Yes | -| value | [Github](#github)
[Marketplace](#marketplace)
[Package](#package) | | Yes | +| value | [PluginDependencyGithub](#plugindependencygithub)
[PluginDependencyMarketplace](#plugindependencymarketplace)
[PluginDependencyPackage](#plugindependencypackage) | | Yes | + +#### PluginDependencyGithub + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_plugin_unique_identifier | string | | Yes | +| package | string | | Yes | +| repo | string | | Yes | +| version | string | | Yes | + +#### PluginDependencyMarketplace + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| marketplace_plugin_unique_identifier | string | | Yes | +| version | string | | No | + +#### PluginDependencyPackage + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| plugin_unique_identifier | string | | Yes | +| version | string | | No | #### PluginDependencyType @@ -24643,6 +24935,12 @@ Serialized pricing info with codegen-safe decimal string patterns. | mode | [ProcessRuleMode](#processrulemode) | `automatic` uses built-in rules, `custom` allows manual configuration, `hierarchical` enables parent-child chunk structure (use with `doc_form: hierarchical_model`). | Yes | | rules | [Rule](#rule) | Custom processing rules. | No | +#### ProcessRuleLimitsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| indexing_max_segmentation_tokens_length | integer | | Yes | + #### ProcessRuleMode Dataset Process Rule Mode @@ -24655,10 +24953,17 @@ Dataset Process Rule Mode | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limits | object | | Yes | +| limits | [ProcessRuleLimitsResponse](#processrulelimitsresponse) | | Yes | | mode | [ProcessRuleMode](#processrulemode) | | Yes | | rules | [Rule](#rule) | | No | +#### PromptTemplateMessageResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| role | string,
**Available values:** "assistant", "system", "user" | *Enum:* `"assistant"`, `"system"`, `"user"` | Yes | +| text | string | | Yes | + #### PromptType Prompt configuration type @@ -24723,11 +25028,11 @@ is empty when no alias exists. | ---- | ---- | ----------- | -------- | | background | string | | No | | configurate_methods | [ [ConfigurateMethod](#configuratemethod) ] | | Yes | -| description | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | +| description | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | | help | [ProviderHelpEntity](#providerhelpentity) | | No | -| icon_small | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | -| icon_small_dark | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | -| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| icon_small | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| icon_small_dark | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | model_credential_schema | [ModelCredentialSchema](#modelcredentialschema) | | No | | models | [ [AIModelEntity](#aimodelentity) ] | | No | | position | object | | No | @@ -24744,11 +25049,11 @@ Runtime provider response with codegen-safe model pricing schemas. | ---- | ---- | ----------- | -------- | | background | string | | No | | configurate_methods | [ [ConfigurateMethod](#configuratemethod) ] | | Yes | -| description | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | +| description | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | | help | [ProviderHelpEntity](#providerhelpentity) | | No | -| icon_small | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | -| icon_small_dark | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | -| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| icon_small | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| icon_small_dark | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | model_credential_schema | [ModelCredentialSchema](#modelcredentialschema) | | No | | models | [ [AIModelEntityResponse](#aimodelentityresponse) ],
**Default:** | | No | | position | object | | No | @@ -24763,8 +25068,8 @@ Model class for provider help. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| title | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | -| url | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| title | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | +| url | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | #### ProviderModelListResponse @@ -24782,7 +25087,7 @@ Model class for model response. | features | [ [ModelFeature](#modelfeature) ] | | No | | fetch_from | [FetchFrom](#fetchfrom) | Where the model definition comes from. `predefined-model` for built-in models, `customizable-model` for user-configured models. | Yes | | has_invalid_load_balancing_configs | boolean | | No | -| label | [I18nObject](#i18nobject) | Localized display name of the model. | Yes | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | Localized display name of the model. | Yes | | load_balancing_enabled | boolean | | No | | model | string | | Yes | | model_properties | object | | Yes | @@ -24842,11 +25147,11 @@ Model class for provider response. | background | string | | No | | configurate_methods | [ [ConfigurateMethod](#configuratemethod) ] | | Yes | | custom_configuration | [CustomConfigurationResponse](#customconfigurationresponse) | | Yes | -| description | [I18nObject](#i18nobject) | | No | +| description | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | | help | [ProviderHelpEntity](#providerhelpentity) | | No | -| icon_small | [I18nObject](#i18nobject) | | No | -| icon_small_dark | [I18nObject](#i18nobject) | | No | -| label | [I18nObject](#i18nobject) | | Yes | +| icon_small | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| icon_small_dark | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | model_credential_schema | [ModelCredentialSchema](#modelcredentialschema) | | No | | preferred_provider_type | [ProviderType](#providertype) | | Yes | | provider | string | | Yes | @@ -24867,9 +25172,9 @@ Model class for provider with models response. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| icon_small | [I18nObject](#i18nobject) | | No | -| icon_small_dark | [I18nObject](#i18nobject) | | No | -| label | [I18nObject](#i18nobject) | Localized display name of the provider. | Yes | +| icon_small | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| icon_small_dark | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | Localized display name of the provider. | Yes | | models | [ [ProviderModelWithStatusEntity](#providermodelwithstatusentity) ] | | Yes | | provider | string | | Yes | | status | [CustomConfigurationStatus](#customconfigurationstatus) | Provider status. `active` when credentials are configured and valid. | Yes | @@ -24877,11 +25182,8 @@ Model class for provider with models response. #### PublishWorkflowPayload -Payload for publishing snippet workflow. - | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| knowledge_base_setting | object | | No | | marked_comment | string | | No | | marked_name | string | | No | @@ -24904,6 +25206,12 @@ Payload for publishing snippet workflow. | answer | string | | Yes | | question | string | | Yes | +#### QueryType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| QueryType | string | | | + #### Quota | Name | Type | Description | Required | @@ -24984,6 +25292,15 @@ Resource types understood by access policies. | plugin_unique_identifier | string | | Yes | | provider | string | | Yes | +#### RagPipelineDraftWorkflowRunPayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| datasource_info_list | [ object ] | | Yes | +| datasource_type | string | | Yes | +| inputs | object | | Yes | +| start_node_id | string | | Yes | + #### RagPipelineImportCheckDependenciesResponse | Name | Type | Description | Required | @@ -25022,17 +25339,40 @@ Resource types understood by access policies. | ---- | ---- | ----------- | -------- | | RagPipelineOpaqueResponse | | | | +#### RagPipelinePublishedRunResponse + +Blocking run result: a preview run returns the workflow result, a processing run the queued batch. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| RagPipelinePublishedRunResponse | [PipelineGenerateResponse](#pipelinegenerateresponse)
[WorkflowAppBlockingResponse](#workflowappblockingresponse) | Blocking run result: a preview run returns the workflow result, a processing run the queued batch. | | + #### RagPipelineRecommendedPluginQuery | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | type | string,
**Default:** all | | No | +#### RagPipelineRecommendedPluginsResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| installed_recommended_plugins | [ [ToolProviderApiEntityResponse](#toolproviderapientityresponse) ] | | Yes | +| uninstalled_recommended_plugins | [ object ] | | Yes | + #### RagPipelineStepParametersResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| variables | | | Yes | +| variables | [ [PipelineVariableResponse](#pipelinevariableresponse) ] | | Yes | + +#### RagPipelineTransformResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| dataset_id | string | | Yes | +| pipeline_id | string | | Yes | +| status | string | | Yes | #### RagPipelineWorkflowPublishResponse @@ -25041,6 +25381,13 @@ Resource types understood by access policies. | created_at | integer | | Yes | | result | string | | Yes | +#### RagPipelineWorkflowRunQuery + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| last_id | string | | No | +| limit | integer,
**Default:** 20 | | No | + #### RagPipelineWorkflowSyncResponse | Name | Type | Description | Required | @@ -25148,6 +25495,12 @@ Recommended app not found. | ---- | ---- | ----------- | -------- | | language | string,
**Default:** en-US | Language code for recommended app localization | No | +#### RecursiveJSONValue + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| RecursiveJSONValue | string
integer
number
boolean
object
[ [RecursiveJSONValue](#recursivejsonvalue) ] | | | + #### RedirectResponse | Name | Type | Description | Required | @@ -25174,10 +25527,10 @@ Recommended app not found. | description | string | | Yes | | icon | string | | Yes | | icon_background | string | | Yes | -| icon_type | string | | Yes | +| icon_type | [IconType](#icontype) | | Yes | | icon_url | string | | No | | id | string | | Yes | -| mode | string | | Yes | +| mode | [AppMode](#appmode) | | Yes | | name | string | | Yes | #### RemoteFileAccessDeniedError @@ -25533,6 +25886,15 @@ The Roster Agent package exceeds the size limit. | model_config | [ModelConfig](#modelconfig) | Model configuration | Yes | | no_variable | boolean | Whether to exclude variables | No | +#### RuleGenerateResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| error | string | | Yes | +| opening_statement | string | | Yes | +| prompt | string | | Yes | +| variables | [ string ] | | Yes | + #### RuleStructuredOutputPayload | Name | Type | Description | Required | @@ -25707,7 +26069,7 @@ The Roster Agent package exceeds the size limit. | keywords | [ string ] | | Yes | | position | integer | | Yes | | sign_content | string | | Yes | -| status | string | | Yes | +| status | string,
**Available values:** "completed", "error", "indexing", "paused", "re_segment", "waiting" | *Enum:* `"completed"`, `"error"`, `"indexing"`, `"paused"`, `"re_segment"`, `"waiting"` | Yes | | stopped_at | integer | | Yes | | summary | string | | Yes | | tokens | integer | | Yes | @@ -25743,6 +26105,12 @@ The Roster Agent package exceeds the size limit. | output_variable_name | string | | Yes | | type | string | | No | +#### ShallowJSONValue + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| ShallowJSONValue | string
integer
number
boolean
object
[ object ] | | | + #### SimpleAccount | Name | Type | Description | Required | @@ -25805,7 +26173,7 @@ The Roster Agent package exceeds the size limit. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| model | [JSONValue](#jsonvalue) | | No | +| model | [AnyJSONValue](#anyjsonvalue) | | No | | pre_prompt | string | | No | #### SimpleProviderEntityResponse @@ -25814,9 +26182,9 @@ Simple provider entity response. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| icon_small | [I18nObject](#i18nobject) | | No | -| icon_small_dark | [I18nObject](#i18nobject) | | No | -| label | [I18nObject](#i18nobject) | | Yes | +| icon_small | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| icon_small_dark | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | Yes | | models | [ [AIModelEntityResponse](#aimodelentityresponse) ],
**Default:** | | No | | provider | string | | Yes | | provider_name | string | | No | @@ -26377,6 +26745,16 @@ Payload for running a loop node in snippet draft workflow. | page | integer | | Yes | | total | integer | | Yes | +#### SnippetPublishWorkflowPayload + +Payload for publishing snippet workflow. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| knowledge_base_setting | object | | No | +| marked_comment | string | | No | +| marked_name | string | | No | + #### SnippetResponse | Name | Type | Description | Required | @@ -26460,6 +26838,15 @@ Query parameters for listing snippet published workflows. | version | string | | Yes | | version_number | integer | | No | +#### SnippetWorkflowRunQuery + +Query parameters for workflow runs. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| last_id | string | | No | +| limit | integer,
**Default:** 20 | | No | + #### SpeechToTextDisabledError Speech to text is disabled. @@ -26541,6 +26928,13 @@ Default configuration for form inputs. | type | [ValueSourceType](#valuesourcetype) | | Yes | | value | string | | No | +#### StructuredOutputGenerateResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| error | string | Failure reason, empty on success | Yes | +| output | string | Generated JSON Schema, empty when generation failed | Yes | + #### SubscriptionBuilderApiEntity | Name | Type | Description | Required | @@ -27098,10 +27492,10 @@ removes TOOLS_SELECTOR from PluginParameterType | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | author | string | The author of the tool | Yes | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The description of the tool | Yes | +| description | [I18nObject](#i18nobject) | The description of the tool | Yes | | icon | string | The icon of the tool | Yes | | icon_dark | string | The dark icon of the tool | No | -| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The label of the tool | Yes | +| label | [I18nObject](#i18nobject) | The label of the tool | Yes | | name | string | The name of the tool | Yes | | tags | [ [ToolLabelEnum](#toollabelenum) ] | The tags of the tool | No | @@ -27520,10 +27914,10 @@ The identity of the trigger provider | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | author | string | The author of the trigger provider | Yes | -| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The description of the trigger provider | Yes | +| description | [I18nObject](#i18nobject) | The description of the trigger provider | Yes | | icon | string | The icon of the trigger provider | No | | icon_dark | string | The dark icon of the trigger provider | No | -| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | The label of the trigger provider | Yes | +| label | [I18nObject](#i18nobject) | The label of the trigger provider | Yes | | name | string | The name of the trigger provider | Yes | | tags | [ string ] | The tags of the trigger provider | No | @@ -27814,6 +28208,12 @@ The app access service is unavailable. Try again later. | ---- | ---- | ----------- | -------- | | protocol | [SSOProtocol](#ssoprotocol) | | Yes | +#### WebhookTriggerQuery + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| node_id | string | | Yes | + #### WebhookTriggerResponse | Name | Type | Description | Required | @@ -27922,6 +28322,33 @@ How a workflow node is bound to an Agent. | node_execution_id | string | Workflow node execution ID | Yes | | path | string | Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; `~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside Workspace are governed by backend isolation, not a Workspace-root restriction | Yes | +#### WorkflowAppBlockingResponse + +WorkflowAppBlockingResponse entity + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| data | [WorkflowAppBlockingResponseData](#workflowappblockingresponsedata) | | Yes | +| task_id | string | | Yes | +| workflow_run_id | string | | Yes | + +#### WorkflowAppBlockingResponseData + +Data entity + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| created_at | integer | | Yes | +| elapsed_time | number | | Yes | +| error | string | | No | +| finished_at | integer | | Yes | +| id | string | | Yes | +| outputs | object | | No | +| status | [WorkflowExecutionStatus](#workflowexecutionstatus) | | Yes | +| total_steps | integer | | Yes | +| total_tokens | integer | | Yes | +| workflow_id | string | | Yes | + #### WorkflowAppLogPaginationResponse | Name | Type | Description | Required | @@ -28217,7 +28644,7 @@ How a workflow node is bound to an Agent. | name | string | | Yes | | selector | [ string ] | | Yes | | type | string | | Yes | -| value | [JSONValue](#jsonvalue) | | Yes | +| value | [RecursiveJSONValue](#recursivejsonvalue) | | Yes | | value_type | string | | Yes | | visible | boolean | | Yes | @@ -28870,15 +29297,6 @@ Workflow run not found. | files | [ object
object
object
object ] | File list for workflow system file inputs. Available when file upload is enabled for the workflow. To attach a local file, first upload it via [Upload File](/api-reference/files/upload-file) and use the returned `id` as `upload_file_id` with `transfer_method: local_file`. | No | | inputs | object | Key-value pairs for workflow input variables. Values for file-type variables should be arrays of file objects with `type`, `transfer_method`, and either `url` or `upload_file_id`. Refer to the `user_input_form` field in the [Get App Parameters](/api-reference/applications/get-app-parameters) response to discover the variable names and types expected by your app. | Yes | -#### WorkflowRunQuery - -Query parameters for workflow runs. - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| last_id | string | | No | -| limit | integer,
**Default:** 20 | | No | - #### WorkflowRunRequest | Name | Type | Description | Required | @@ -29208,25 +29626,15 @@ Workflow tool configuration | page | integer | | No | | reverse | boolean | | No | -#### core__tools__entities__common_entities__I18nObject +#### _RoleUpsertRequest -Model class for i18n object. +Accepts the payload sent by the Create/Edit Role dialog. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| en_US | string | | Yes | -| ja_JP | string | | No | -| pt_BR | string | | No | -| zh_Hans | string | | No | - -#### graphon__model_runtime__entities__common_entities__I18nObject - -Model class for i18n object. - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| en_US | string | | Yes | -| zh_Hans | string | | No | +| description | string | | No | +| name | string | | Yes | +| permission_keys | [ string ],
**Default:** | | No | ## FastOpenAPI Preview (OpenAPI 3.1) diff --git a/api/openapi/markdown/openapi-openapi.md b/api/openapi/markdown/openapi-openapi.md index b05a1b31cd9b30..da78b916503a50 100644 --- a/api/openapi/markdown/openapi-openapi.md +++ b/api/openapi/markdown/openapi-openapi.md @@ -2507,15 +2507,6 @@ is exact rather than an under-annotated open object. | ---- | ---- | ----------- | -------- | | hints | [ [Hint](#hint) ] | Next steps the caller can take | No | -#### Github - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| github_plugin_unique_identifier | string | | Yes | -| package | string | | Yes | -| repo | string | | Yes | -| version | string | | Yes | - #### HealthResponse Liveness payload for `GET /openapi/v1/_health` — no auth required. @@ -2620,13 +2611,6 @@ A next step: the op id to call and its ready input. | name | string | | Yes | | provider | string | | Yes | -#### Marketplace - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| marketplace_plugin_unique_identifier | string | | Yes | -| version | string | | No | - #### MarketplacePluginListResponse | Name | Type | Description | Required | @@ -2883,13 +2867,6 @@ The console payload plus local file parts; `_files.merge_files` sets them on `in | files | object | Local file paths keyed by the form's file input name, same convention as the run ops' `files` | No | | inputs | object | Submitted human input values keyed by output variable name. Use a string for paragraph or select input values, a file mapping for file inputs, and a list of file mappings for file-list inputs. Local file mappings use `transfer_method=local_file` with `upload_file_id`; remote file mappings use `transfer_method=remote_url` with `url` or `remote_url`. | Yes | -#### Package - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| plugin_unique_identifier | string | | Yes | -| version | string | | No | - #### PermittedExternalAppsListQuery Strict (extra='forbid'). @@ -2931,7 +2908,30 @@ Strict (extra='forbid'). | ---- | ---- | ----------- | -------- | | current_identifier | string | | No | | type | [PluginDependencyType](#plugindependencytype) | | Yes | -| value | [Github](#github)
[Marketplace](#marketplace)
[Package](#package) | | Yes | +| value | [PluginDependencyGithub](#plugindependencygithub)
[PluginDependencyMarketplace](#plugindependencymarketplace)
[PluginDependencyPackage](#plugindependencypackage) | | Yes | + +#### PluginDependencyGithub + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| github_plugin_unique_identifier | string | | Yes | +| package | string | | Yes | +| repo | string | | Yes | +| version | string | | Yes | + +#### PluginDependencyMarketplace + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| marketplace_plugin_unique_identifier | string | | Yes | +| version | string | | No | + +#### PluginDependencyPackage + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| plugin_unique_identifier | string | | Yes | +| version | string | | No | #### PluginDependencyType diff --git a/api/openapi/markdown/service-openapi.md b/api/openapi/markdown/service-openapi.md index fa30f98a51c48d..17e6fcec188ea1 100644 --- a/api/openapi/markdown/service-openapi.md +++ b/api/openapi/markdown/service-openapi.md @@ -2428,7 +2428,7 @@ Agent has not been published. Please publish the Agent before using the API. | thought | string | | No | | tool | string | | No | | tool_input | string | | No | -| tool_labels | [JSONValue](#jsonvalue) | Labels for tools used. | Yes | +| tool_labels | [AnyJSONValue](#anyjsonvalue) | Labels for tools used. | Yes | #### Annotation @@ -2488,6 +2488,12 @@ Agent has not been published. Please publish the Agent before using the API. | embedding_provider_name | string | Name of the embedding model provider. | Yes | | score_threshold | float | Minimum similarity score for an annotation to be considered a match. Higher values require closer matches. | Yes | +#### AnyJSONValue + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| AnyJSONValue | | | | + #### AppFeedbackListResponse | Name | Type | Description | Required | @@ -2560,7 +2566,7 @@ Audio size exceeded. {message} | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| text | string | | Yes | +| text | string | Transcribed text from audio | Yes | #### BinaryFileResponse @@ -2763,7 +2769,7 @@ Public pause reason emitted by a blocking Chatflow execution. | id | string | | Yes | | position | integer | | Yes | | segment_id | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "automatic", "customized" | *Enum:* `"automatic"`, `"customized"` | Yes | | updated_at | integer | | Yes | | word_count | integer | | Yes | @@ -2943,7 +2949,7 @@ Enum class for custom configuration status. | chunk_structure | string | | Yes | | created_at | integer | | Yes | | created_by | string | | Yes | -| data_source_type | string | | Yes | +| data_source_type | string,
**Available values:** "local_file", "notion_import", "online_document", "online_drive", "upload_file", "website_crawl" | | Yes | | description | string | | Yes | | doc_form | string | | Yes | | doc_metadata | [ [DatasetDocMetadataResponse](#datasetdocmetadataresponse) ] | | Yes | @@ -2956,16 +2962,16 @@ Enum class for custom configuration status. | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | | icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | Icon display configuration for the knowledge base. | No | | id | string | | Yes | -| indexing_technique | string | | Yes | +| indexing_technique | [IndexTechniqueType](#indextechniquetype) | | Yes | | is_multimodal | boolean | | Yes | | is_published | boolean | | Yes | | maintainer | string | | No | | name | string | | Yes | -| permission | string | | Yes | +| permission | string,
**Available values:** "all_team_members", "only_me", "partial_members" | *Enum:* `"all_team_members"`, `"only_me"`, `"partial_members"` | Yes | | pipeline_id | string | | Yes | | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | -| runtime_mode | string | | Yes | +| runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | @@ -2984,7 +2990,7 @@ Enum class for custom configuration status. | chunk_structure | string | | Yes | | created_at | integer | | Yes | | created_by | string | | Yes | -| data_source_type | string | | Yes | +| data_source_type | string,
**Available values:** "local_file", "notion_import", "online_document", "online_drive", "upload_file", "website_crawl" | | Yes | | description | string | | Yes | | doc_form | string | | Yes | | doc_metadata | [ [DatasetDocMetadataResponse](#datasetdocmetadataresponse) ] | | Yes | @@ -2997,17 +3003,17 @@ Enum class for custom configuration status. | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | | icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | Icon display configuration for the knowledge base. | No | | id | string | | Yes | -| indexing_technique | string | | Yes | +| indexing_technique | [IndexTechniqueType](#indextechniquetype) | | Yes | | is_multimodal | boolean | | Yes | | is_published | boolean | | Yes | | maintainer | string | | No | | name | string | | Yes | | partial_member_list | [ string ] | | No | -| permission | string | | Yes | +| permission | string,
**Available values:** "all_team_members", "only_me", "partial_members" | *Enum:* `"all_team_members"`, `"only_me"`, `"partial_members"` | Yes | | pipeline_id | string | | Yes | | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes | -| runtime_mode | string | | Yes | +| runtime_mode | string,
**Available values:** "general", "rag_pipeline" | | Yes | | summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | Summary index configuration. | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | @@ -3022,7 +3028,7 @@ Enum class for custom configuration status. | ---- | ---- | ----------- | -------- | | id | string | | Yes | | name | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### DatasetExternalKnowledgeInfoResponse @@ -3087,7 +3093,7 @@ Enum class for custom configuration status. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | name | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### DatasetMetadataBuiltInFieldsResponse @@ -3102,7 +3108,7 @@ Enum class for custom configuration status. | count | integer | | No | | id | string | | Yes | | name | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### DatasetMetadataListResponse @@ -3117,7 +3123,7 @@ Enum class for custom configuration status. | ---- | ---- | ----------- | -------- | | id | string | | Yes | | name | string | | Yes | -| type | string | | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### DatasetNameDuplicateError @@ -3143,7 +3149,7 @@ The dataset name already exists. Please modify your dataset name. | reranking_model | [DatasetRerankingModelResponse](#datasetrerankingmodelresponse) | Reranking model configuration. | No | | score_threshold | number | | No | | score_threshold_enabled | boolean | | Yes | -| search_method | string | | Yes | +| search_method | [RetrievalMethod](#retrievalmethod) | | Yes | | top_k | integer | | Yes | | weights | [DatasetWeightedScoreResponse](#datasetweightedscoreresponse) | | No | @@ -3328,8 +3334,8 @@ Request payload for bulk downloading documents as a zip archive. | ---- | ---- | ----------- | -------- | | id | string | | Yes | | name | string | | Yes | -| type | string | | Yes | -| value | string
integer
number
boolean | | No | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | +| value | string
integer
number | | Yes | #### DocumentResponse @@ -3339,7 +3345,7 @@ Request payload for bulk downloading documents as a zip archive. | created_at | integer | | No | | created_by | string | | No | | created_from | string | | No | -| data_source_detail_dict | object | | Yes | +| data_source_detail_dict | [DocumentSourceDetailResponse](#documentsourcedetailresponse) | | Yes | | data_source_info | object | | No | | data_source_type | string | | No | | dataset_process_rule_id | string | | No | @@ -3360,6 +3366,14 @@ Request payload for bulk downloading documents as a zip archive. | tokens | integer | | No | | word_count | integer | | No | +#### DocumentSourceDetailResponse + +The uploaded file for file documents; Notion and website documents repeat their source info. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| upload_file | [DocumentUploadFileDetailResponse](#documentuploadfiledetailresponse) | | No | + #### DocumentStatusListResponse | Name | Type | Description | Required | @@ -3383,7 +3397,7 @@ Request payload for bulk downloading documents as a zip archive. | error_code | string | | No | | estimated_vector_space_mb | integer | | No | | id | string | | Yes | -| indexing_status | string | | Yes | +| indexing_status | string,
**Available values:** "cleaning", "completed", "error", "indexing", "parsing", "paused", "splitting", "waiting" | *Enum:* `"cleaning"`, `"completed"`, `"error"`, `"indexing"`, `"parsing"`, `"paused"`, `"splitting"`, `"waiting"` | Yes | | parsing_completed_at | integer | | Yes | | paused_at | integer | | Yes | | processing_started_at | integer | | Yes | @@ -3418,6 +3432,18 @@ Request payload for bulk downloading documents as a zip archive. | retrieval_model | [RetrievalModel](#retrievalmodel) | Controls how chunks are searched and ranked when querying this knowledge base. | No | | text | string | Document text content. | No | +#### DocumentUploadFileDetailResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| created_at | number | | Yes | +| created_by | string | | Yes | +| extension | string | | Yes | +| id | string | | Yes | +| mime_type | string | | No | +| name | string | | Yes | +| size | integer | | Yes | + #### EndUserDetail Full end-user detail returned by the Service API. @@ -3769,15 +3795,6 @@ This form has already been submitted by another user, form_id={form_id} | ---- | ---- | ----------- | -------- | | HumanInputFormSubmittedError | | This form has already been submitted by another user, form_id={form_id} | | -#### I18nObject - -Model class for i18n object. - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| en_US | string | | Yes | -| zh_Hans | string | | No | - #### IndexInfoResponse | Name | Type | Description | Required | @@ -3786,6 +3803,12 @@ Model class for i18n object. | server_version | string | | Yes | | welcome | string | | Yes | +#### IndexTechniqueType + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| IndexTechniqueType | string | | | + #### InvalidActionError Invalid action. @@ -3814,12 +3837,6 @@ The metadata content is incorrect. Please check and verify. | ---- | ---- | ----------- | -------- | | JSONObject | object | | | -#### JSONValue - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| JSONValue | | | | - #### JSONValueType | Name | Type | Description | Required | @@ -3979,6 +3996,15 @@ Enum class for model property key. | ---- | ---- | ----------- | -------- | | ModelPropertyKey | string | Enum class for model property key. | | +#### ModelRuntimeI18nObject + +Model class for i18n object. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| en_US | string | | Yes | +| zh_Hans | string | | No | + #### ModelStatus Enum class for model status. @@ -4167,7 +4193,7 @@ Model class for model response. | features | [ [ModelFeature](#modelfeature) ] | | No | | fetch_from | [FetchFrom](#fetchfrom) | Where the model definition comes from. `predefined-model` for built-in models, `customizable-model` for user-configured models. | Yes | | has_invalid_load_balancing_configs | boolean | | No | -| label | [I18nObject](#i18nobject) | Localized display name of the model. | Yes | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | Localized display name of the model. | Yes | | load_balancing_enabled | boolean | | No | | model | string | | Yes | | model_properties | object | | Yes | @@ -4210,9 +4236,9 @@ Model class for provider with models response. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| icon_small | [I18nObject](#i18nobject) | | No | -| icon_small_dark | [I18nObject](#i18nobject) | | No | -| label | [I18nObject](#i18nobject) | Localized display name of the provider. | Yes | +| icon_small | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| icon_small_dark | [ModelRuntimeI18nObject](#modelruntimei18nobject) | | No | +| label | [ModelRuntimeI18nObject](#modelruntimei18nobject) | Localized display name of the provider. | Yes | | models | [ [ProviderModelWithStatusEntity](#providermodelwithstatusentity) ] | | Yes | | provider | string | | Yes | | status | [CustomConfigurationStatus](#customconfigurationstatus) | Provider status. `active` when credentials are configured and valid. | Yes | @@ -4393,7 +4419,7 @@ Rate Limit Error | keywords | [ string ] | | Yes | | position | integer | | Yes | | sign_content | string | | Yes | -| status | string | | Yes | +| status | string,
**Available values:** "completed", "error", "indexing", "paused", "re_segment", "waiting" | *Enum:* `"completed"`, `"error"`, `"indexing"`, `"paused"`, `"re_segment"`, `"waiting"` | Yes | | stopped_at | integer | | Yes | | summary | string | | Yes | | tokens | integer | | Yes | diff --git a/api/openapi/markdown/web-openapi.md b/api/openapi/markdown/web-openapi.md index b5531af9717e11..8a7d6fa1a28452 100644 --- a/api/openapi/markdown/web-openapi.md +++ b/api/openapi/markdown/web-openapi.md @@ -1016,7 +1016,13 @@ Agent has not been published. Please publish the Agent before using the web app. | thought | string | | No | | tool | string | | No | | tool_input | string | | No | -| tool_labels | [JSONValue](#jsonvalue) | Labels for tools used. | Yes | +| tool_labels | [AnyJSONValue](#anyjsonvalue) | Labels for tools used. | Yes | + +#### AnyJSONValue + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| AnyJSONValue | | | | #### AppAccessModeQuery @@ -1490,12 +1496,6 @@ Upload token is invalid or expired. | ---- | ---- | ----------- | -------- | | InvalidUploadTokenError | | Upload token is invalid or expired. | | -#### JSONValue - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| JSONValue | string
integer
number
boolean
object
[ object ] | | | - #### JSONValueType | Name | Type | Description | Required | @@ -1873,6 +1873,12 @@ The remote file URL is not allowed. | output_variable_name | string | | Yes | | type | string | | No | +#### ShallowJSONValue + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| ShallowJSONValue | string
integer
number
boolean
object
[ object ] | | | + #### SimpleConversation | Name | Type | Description | Required | diff --git a/api/services/entities/knowledge_retrieval_inner.py b/api/services/entities/knowledge_retrieval_inner.py index aaf686da44c8f0..8d47aace3d54da 100644 --- a/api/services/entities/knowledge_retrieval_inner.py +++ b/api/services/entities/knowledge_retrieval_inner.py @@ -23,7 +23,7 @@ from fields.base import ResponseModel type JsonScalar = str | int | float | bool | None -type JsonValue = JsonScalar | list[JsonScalar] | dict[str, JsonScalar] +type CompletionParamValue = JsonScalar | list[JsonScalar] | dict[str, JsonScalar] type MetadataValue = str | list[str] | int | float | None @@ -47,7 +47,7 @@ class InnerKnowledgeRetrieveModelConfig(BaseModel): provider: str = Field(min_length=1) name: str = Field(min_length=1) mode: str = Field(min_length=1) - completion_params: dict[str, JsonValue] = Field(default_factory=dict) + completion_params: dict[str, CompletionParamValue] = Field(default_factory=dict) class InnerKnowledgeRetrieveRerankingModelConfig(BaseModel): diff --git a/api/services/knowledge/dataset_read_service.py b/api/services/knowledge/dataset_read_service.py index 4702dea90eb26e..a2d738ce7a95a2 100644 --- a/api/services/knowledge/dataset_read_service.py +++ b/api/services/knowledge/dataset_read_service.py @@ -52,6 +52,7 @@ def _dataset_detail_values(dataset: Dataset, prefetch: DatasetDetailPrefetch) -> "permission": dataset.permission, "data_source_type": dataset.data_source_type, "indexing_technique": dataset.indexing_technique, + "keyword_number": dataset.keyword_number, "app_count": prefetch.app_counts.get(dataset.id, 0), "document_count": prefetch.document_counts.get(dataset.id, 0), "word_count": prefetch.word_counts.get(dataset.id, 0), diff --git a/api/services/knowledge/documents/adapters.py b/api/services/knowledge/documents/adapters.py index e61a702bcf0ac3..6caa431621c1e4 100644 --- a/api/services/knowledge/documents/adapters.py +++ b/api/services/knowledge/documents/adapters.py @@ -259,7 +259,7 @@ def get_detail(self, ref: DocumentRef, *, metadata_only: bool) -> dict[str, Any] data_source_info=document.data_source_info_dict, data_source_detail_dict=get_document_source_detail(document, session=session), dataset_process_rule=DatasetService.get_process_rules(ref.dataset.dataset_id, session), - document_process_rule=dict(rule.to_dict()) if rule else {}, + document_process_rule=dict(rule.to_dict()) if rule else None, segment_count=segment_count, average_segment_length=(document.word_count or 0) // segment_count if segment_count else 0, hit_count=get_document_hit_count(document, session=session), diff --git a/api/services/knowledge/entities/datasets.py b/api/services/knowledge/entities/datasets.py index a769dacac840a4..c6b5c3246c953e 100644 --- a/api/services/knowledge/entities/datasets.py +++ b/api/services/knowledge/entities/datasets.py @@ -25,6 +25,7 @@ class DatasetDetailRecord(TypedDict): permission: str data_source_type: str | None indexing_technique: str | None + keyword_number: int | None app_count: int document_count: int word_count: int diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_message.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_message.py index 840619fa261c50..acc819db864f87 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_message.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_message.py @@ -10,7 +10,7 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from controllers.console.app.message import ChatMessagesQuery, MessageFeedbackPayload +from controllers.console.app.message import AppMessageFeedbackPayload, ChatMessagesQuery from controllers.console.app.message import attach_message_extra_contents as _attach_message_extra_contents from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from libs.datetime_utils import naive_utc_now @@ -99,7 +99,7 @@ def test_chat_messages_query_validators(self) -> None: def test_message_feedback_validators(self) -> None: assert ( - MessageFeedbackPayload.validate_message_id("123e4567-e89b-12d3-a456-426614174000") + AppMessageFeedbackPayload.validate_message_id("123e4567-e89b-12d3-a456-426614174000") == "123e4567-e89b-12d3-a456-426614174000" ) diff --git a/api/tests/unit_tests/controllers/common/test_schema.py b/api/tests/unit_tests/controllers/common/test_schema.py index 0dd56fabaa6d00..3af43405d7e326 100644 --- a/api/tests/unit_tests/controllers/common/test_schema.py +++ b/api/tests/unit_tests/controllers/common/test_schema.py @@ -8,7 +8,7 @@ import pytest from flask import Flask from flask_restx import Namespace, fields -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, create_model class UserModel(BaseModel): @@ -127,6 +127,103 @@ def test_register_schema_model_promotes_nested_pydantic_definitions(): assert called_schemas["ChildModel"] == parent_schema["$defs"]["ChildModel"] +def _make_same_named_model() -> type[BaseModel]: + """Build a model whose qualified name differs from UserModel but whose bare name matches.""" + + return create_model("UserModel", __module__="other_module", email=(str, ...)) + + +def test_register_schema_model_allows_reregistering_the_same_model(): + from controllers.common.schema import register_schema_model + + namespace = Namespace("test") + + register_schema_model(namespace, UserModel) + register_schema_model(namespace, UserModel) + + assert list(namespace.models) == ["UserModel"] + + +def test_register_schema_model_rejects_different_model_with_same_name(): + from controllers.common.schema import SchemaNameCollisionError, register_schema_model + + namespace = Namespace("test") + register_schema_model(namespace, UserModel) + + with pytest.raises(SchemaNameCollisionError, match="'UserModel'.*produced by both"): + register_schema_model(namespace, _make_same_named_model()) + + +def test_register_schema_model_rejects_nested_definition_with_same_name(): + from controllers.common.schema import SchemaNameCollisionError, register_schema_model + + other_child = create_model("ChildModel", __module__="other_module", other=(int, ...)) + other_parent = create_model("OtherParentModel", child=(other_child, ...)) + namespace = Namespace("test") + register_schema_model(namespace, ParentModel) + + with pytest.raises(SchemaNameCollisionError, match="'ChildModel'"): + register_schema_model(namespace, other_parent) + + +def test_register_schema_model_rejects_same_named_types_inside_one_schema(): + from controllers.common.schema import SchemaNameCollisionError, register_schema_model + + other_child = create_model("ChildModel", __module__="other_module", other=(int, ...)) + both_children = create_model("BothChildrenModel", first=(ChildModel, ...), second=(other_child, ...)) + + with pytest.raises(SchemaNameCollisionError, match="'ChildModel'"): + register_schema_model(Namespace("test"), both_children) + + +def test_register_schema_model_rejects_model_registered_in_both_modes_with_different_shapes(): + from controllers.common.schema import ( + SchemaNameCollisionError, + register_response_schema_model, + register_schema_model, + ) + + namespace = Namespace("test") + register_response_schema_model(namespace, ResponseAliasModel) + + with pytest.raises(SchemaNameCollisionError, match="two different shapes"): + register_schema_model(namespace, ResponseAliasModel) + + +def test_register_schema_model_rejects_names_that_collide_after_normalization(): + from controllers.common.schema import SchemaNameCollisionError, register_schema_model + + namespace = Namespace("test") + register_schema_model(namespace, create_model("JSONPayload", value=(str, ...))) + + with pytest.raises(SchemaNameCollisionError, match="once normalized"): + register_schema_model(namespace, create_model("JsonPayload", value=(str, ...))) + + +def test_register_schema_model_keeps_namespaces_independent(): + from controllers.common.schema import register_schema_model + + register_schema_model(Namespace("first"), UserModel) + register_schema_model(Namespace("second"), _make_same_named_model()) + + +def test_register_schema_model_applies_schema_name_overrides(monkeypatch: pytest.MonkeyPatch): + from controllers.common import schema as schema_module + + monkeypatch.setattr( + schema_module, + "SCHEMA_NAME_OVERRIDES", + {f"{ChildModel.__module__}.{ChildModel.__qualname__}": "RenamedChildModel"}, + ) + namespace = Namespace("test") + + schema_module.register_schema_model(namespace, ParentModel) + + assert set(namespace.models) == {"ParentModel", "RenamedChildModel"} + parent_schema = namespace.models["ParentModel"].__schema__ + assert parent_schema["properties"]["child"]["$ref"] == "#/components/schemas/RenamedChildModel" + + def test_register_schema_models_registers_multiple_models(): from controllers.common.schema import register_schema_models diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index cb71e5ebea7225..4f46d43a209920 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -2229,7 +2229,7 @@ def get_message_detail(**kwargs: object) -> dict[str, object]: with app.test_request_context(json={"message_id": message_id, "rating": "like"}): assert unwrap(AgentMessageFeedbackApi.post)( AgentMessageFeedbackApi(), - message_controller.MessageFeedbackPayload(message_id=message_id, rating="like"), + message_controller.AppMessageFeedbackPayload(message_id=message_id, rating="like"), unbound_session, "tenant-1", current_user, @@ -2389,7 +2389,7 @@ def test_update_message_feedback_rejects_empty_rating_without_existing_feedback( with app.test_request_context(json={"message_id": message_id, "rating": None}): with pytest.raises(ValueError, match="rating cannot be None"): message_controller._update_message_feedback( - args=message_controller.MessageFeedbackPayload(message_id=message_id, rating=None), + args=message_controller.AppMessageFeedbackPayload(message_id=message_id, rating=None), session=sqlite_session, current_user=_account(), app_model=_app_detail_obj(id=app_id), diff --git a/api/tests/unit_tests/controllers/console/app/test_advanced_prompt_template_response.py b/api/tests/unit_tests/controllers/console/app/test_advanced_prompt_template_response.py new file mode 100644 index 00000000000000..fa70d6eb6c7a96 --- /dev/null +++ b/api/tests/unit_tests/controllers/console/app/test_advanced_prompt_template_response.py @@ -0,0 +1,32 @@ +import pytest + +from controllers.console.app.advanced_prompt_template import AdvancedPromptTemplateResponse +from core.prompt.prompt_templates.advanced_prompt_templates import ( + CHAT_APP_CHAT_PROMPT_CONFIG, + CHAT_APP_COMPLETION_PROMPT_CONFIG, + COMPLETION_APP_CHAT_PROMPT_CONFIG, + COMPLETION_APP_COMPLETION_PROMPT_CONFIG, +) + + +@pytest.mark.parametrize( + "template", + [ + CHAT_APP_CHAT_PROMPT_CONFIG, + CHAT_APP_COMPLETION_PROMPT_CONFIG, + COMPLETION_APP_CHAT_PROMPT_CONFIG, + COMPLETION_APP_COMPLETION_PROMPT_CONFIG, + ], +) +def test_prompt_templates_keep_their_shape(template: dict[str, object]) -> None: + dumped = AdvancedPromptTemplateResponse.model_validate(template).model_dump(mode="json", exclude_unset=True) + + assert dumped == template + + +def test_completion_templates_keep_their_stop_words() -> None: + dumped = AdvancedPromptTemplateResponse.model_validate(CHAT_APP_COMPLETION_PROMPT_CONFIG).model_dump( + mode="json", exclude_unset=True + ) + + assert dumped["stop"] == ["Human:"] diff --git a/api/tests/unit_tests/controllers/console/app/test_app_apis.py b/api/tests/unit_tests/controllers/console/app/test_app_apis.py index bb9ed018407d24..d371be2ec66299 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_apis.py +++ b/api/tests/unit_tests/controllers/console/app/test_app_apis.py @@ -68,7 +68,7 @@ WorkflowDraftVariableUpdatePayload, ) from controllers.console.app.workflow_statistic import WorkflowStatisticQuery -from controllers.console.app.workflow_trigger import Parser, ParserEnable +from controllers.console.app.workflow_trigger import ParserEnable, WebhookTriggerQuery from machinery.context import RequestContext from models import App, Site from models.account import Account, AccountStatus @@ -784,7 +784,7 @@ def _patch_statistic_time_range(monkeypatch: pytest.MonkeyPatch) -> None: class TestWorkflowTriggerEndpoints: def test_webhook_trigger_payload(self): - payload = Parser(node_id="node-1") + payload = WebhookTriggerQuery(node_id="node-1") assert payload.node_id == "node-1" enable_payload = ParserEnable(trigger_id="trigger-1", enable_trigger=True) @@ -808,7 +808,7 @@ def test_webhook_trigger_api_get( sqlite_session.commit() with database_app.test_request_context("/?node_id=node-1"): - result = method(api, Parser(node_id="node-1"), sqlite_session, app_model=_make_app()) + result = method(api, WebhookTriggerQuery(node_id="node-1"), sqlite_session, app_model=_make_app()) assert isinstance(result, dict) assert {"id", "webhook_id", "webhook_url", "webhook_debug_url", "node_id", "created_at"} <= set(result.keys()) @@ -844,7 +844,7 @@ def test_multiple_payload_types(self): annotation_module.AnnotationReplyPayload( score_threshold=0.5, embedding_provider_name="openai", embedding_model_name="text-embedding-3-small" ), - message_module.MessageFeedbackPayload(message_id=str(uuid.uuid4()), rating="like"), + message_module.AppMessageFeedbackPayload(message_id=str(uuid.uuid4()), rating="like"), statistic_module.StatisticTimeRangeQuery(start="2024-01-01"), ] assert len(payloads) == 3 diff --git a/api/tests/unit_tests/controllers/console/app/test_generator_api.py b/api/tests/unit_tests/controllers/console/app/test_generator_api.py index 94371295601385..f268f9a38693e3 100644 --- a/api/tests/unit_tests/controllers/console/app/test_generator_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_generator_api.py @@ -67,7 +67,13 @@ def test_rule_generate_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> N api = generator_module.RuleGenerateApi() method = unwrap(api.post) - monkeypatch.setattr(generator_module.LLMGenerator, "generate_rule_config", lambda **_kwargs: {"rules": []}) + rule_config = { + "prompt": "You are a helpful assistant.", + "variables": ["topic"], + "opening_statement": "Hi", + "error": "", + } + monkeypatch.setattr(generator_module.LLMGenerator, "generate_rule_config", lambda **_kwargs: rule_config) with app.test_request_context( "/console/api/rule-generate", @@ -76,7 +82,7 @@ def test_rule_generate_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> N ): response = method(api, RuleGeneratePayload.model_validate(request.get_json()), "t1") - assert response == {"rules": []} + assert response == rule_config def test_rule_code_generate_maps_token_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/unit_tests/controllers/console/app/test_message_api.py b/api/tests/unit_tests/controllers/console/app/test_message_api.py index d21973425cad3b..e442b0c398cdaa 100644 --- a/api/tests/unit_tests/controllers/console/app/test_message_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_message_api.py @@ -115,7 +115,7 @@ def test_app_message_routes_pass_injected_session( ) == {"data": []} assert unwrap(message_module.MessageFeedbackApi.post)( message_module.MessageFeedbackApi(), - message_module.MessageFeedbackPayload(message_id=message_id, rating="like"), + message_module.AppMessageFeedbackPayload(message_id=message_id, rating="like"), session, current_user, app_model, @@ -148,7 +148,7 @@ def test_update_message_feedback_commits_injected_session(app: Flask, sqlite_ses with app.test_request_context(json={"message_id": message_id, "rating": "like", "content": "helpful"}): result = message_module._update_message_feedback( - args=message_module.MessageFeedbackPayload( + args=message_module.AppMessageFeedbackPayload( message_id=message_id, rating="like", content="helpful", @@ -273,8 +273,8 @@ def test_chat_messages_query_empty_first_id(app: Flask, monkeypatch: pytest.Monk def test_message_feedback_payload_valid_like(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - """Test MessageFeedbackPayload with like rating.""" - payload = message_module.MessageFeedbackPayload( + """Test AppMessageFeedbackPayload with like rating.""" + payload = message_module.AppMessageFeedbackPayload( message_id="550e8400-e29b-41d4-a716-446655440000", rating="like", content="Good answer", @@ -284,8 +284,8 @@ def test_message_feedback_payload_valid_like(app: Flask, monkeypatch: pytest.Mon def test_message_feedback_payload_valid_dislike(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - """Test MessageFeedbackPayload with dislike rating.""" - payload = message_module.MessageFeedbackPayload( + """Test AppMessageFeedbackPayload with dislike rating.""" + payload = message_module.AppMessageFeedbackPayload( message_id="550e8400-e29b-41d4-a716-446655440000", rating="dislike", ) @@ -293,8 +293,8 @@ def test_message_feedback_payload_valid_dislike(app: Flask, monkeypatch: pytest. def test_message_feedback_payload_no_rating(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - """Test MessageFeedbackPayload without rating.""" - payload = message_module.MessageFeedbackPayload(message_id="550e8400-e29b-41d4-a716-446655440000") + """Test AppMessageFeedbackPayload without rating.""" + payload = message_module.AppMessageFeedbackPayload(message_id="550e8400-e29b-41d4-a716-446655440000") assert payload.rating is None diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py index 1aaceaff3eb17d..1d931b0be533e7 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py @@ -48,7 +48,7 @@ def _persist_app_trigger( def test_parser_models_validate(): - parser = workflow_trigger_module.Parser(node_id="node-1") + parser = workflow_trigger_module.WebhookTriggerQuery(node_id="node-1") enable_parser = workflow_trigger_module.ParserEnable( trigger_id="550e8400-e29b-41d4-a716-446655440000", enable_trigger=True ) diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py index daae29fd75ce2e..6db24797f4aca9 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py @@ -7,7 +7,7 @@ from controllers.console import console_ns from controllers.console.datasets.rag_pipeline.datasource_content_preview import ( DataSourceContentPreviewApi, - Parser, + DatasourceContentPreviewPayload, ) from models import Account from models.dataset import Pipeline @@ -50,7 +50,7 @@ def test_post_success(self, app: Flask): return_value=service_instance, ), ): - req_data = Parser.model_validate(payload) + req_data = DatasourceContentPreviewPayload.model_validate(payload) response, status = method(api, req_data, account, pipeline, node_id) service_instance.run_datasource_node_preview.assert_called_once_with( @@ -83,7 +83,7 @@ def test_post_invalid_payload(self, app: Flask): patch.object(type(console_ns), "payload", payload), ): with pytest.raises(ValueError): - method(api, Parser.model_validate(payload), account, pipeline, "node-1") + method(api, DatasourceContentPreviewPayload.model_validate(payload), account, pipeline, "node-1") def test_post_without_credential_id(self, app: Flask): api = DataSourceContentPreviewApi() @@ -109,7 +109,7 @@ def test_post_without_credential_id(self, app: Flask): return_value=service_instance, ), ): - req_data = Parser.model_validate(payload) + req_data = DatasourceContentPreviewPayload.model_validate(payload) response, status = method(api, req_data, account, pipeline, "node-1") service_instance.run_datasource_node_preview.assert_called_once() diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py index 8794c7d7b6af4a..5d1705d3696b73 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py @@ -17,9 +17,9 @@ from controllers.console.datasets.rag_pipeline import rag_pipeline_workflow as module from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import ( - DraftWorkflowRunPayload, NodeIdQuery, PublishedWorkflowRunPayload, + RagPipelineDraftWorkflowRunPayload, RagPipelineRecommendedPluginQuery, WorkflowListQuery, WorkflowUpdatePayload, @@ -236,7 +236,27 @@ def test_draft_rag_pipeline_second_step_parameters_serializes_variables(database def test_rag_pipeline_recommended_plugins_serializes_known_envelope(database_app: Flask) -> None: recommended_plugins = { - "installed_recommended_plugins": [{"name": "Dify Extractor", "meta": {"version": "1.0.0"}}], + "installed_recommended_plugins": [ + { + "id": "langgenius/dify_extractor/dify_extractor", + "author": "langgenius", + "name": "langgenius/dify_extractor/dify_extractor", + "description": { + "en_US": "Extract text", + "zh_Hans": "Extract text", + "pt_BR": "Extract text", + "ja_JP": "Extract text", + }, + "icon": "/icon.svg", + "label": { + "en_US": "Dify Extractor", + "zh_Hans": "Dify Extractor", + "pt_BR": "Dify Extractor", + "ja_JP": "Dify Extractor", + }, + "type": "builtin", + } + ], "uninstalled_recommended_plugins": [{"plugin_id": "langgenius/notion_datasource"}], } api = module.RagPipelineRecommendedPluginApi() @@ -404,7 +424,7 @@ def test_rag_pipeline_run_uses_sqlite_session( patch.object(module.helper, "compact_generate_response", return_value={"ok": True}), ): req_data = ( - DraftWorkflowRunPayload.model_validate(payload) + RagPipelineDraftWorkflowRunPayload.model_validate(payload) if api_type is module.DraftRagPipelineRunApi else PublishedWorkflowRunPayload.model_validate(payload) ) @@ -434,7 +454,7 @@ def test_rag_pipeline_run_translates_rate_limit( handler = unwrap_all(api.post) pipeline = _pipeline() req_data = ( - DraftWorkflowRunPayload.model_validate(payload) + RagPipelineDraftWorkflowRunPayload.model_validate(payload) if api_type is module.DraftRagPipelineRunApi else PublishedWorkflowRunPayload.model_validate(payload) ) diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow_apis.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow_apis.py index 2fa671462c04aa..02c162e39afaad 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow_apis.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow_apis.py @@ -550,7 +550,27 @@ def test_recommended_plugins(self, app: Flask) -> None: service = MagicMock() recommended_plugins = { - "installed_recommended_plugins": [{"id": "p1"}], + "installed_recommended_plugins": [ + { + "id": "p1", + "author": "langgenius", + "name": "p1", + "description": { + "en_US": "Plugin one", + "zh_Hans": "Plugin one", + "pt_BR": "Plugin one", + "ja_JP": "Plugin one", + }, + "icon": "/icon.svg", + "label": { + "en_US": "Plugin one", + "zh_Hans": "Plugin one", + "pt_BR": "Plugin one", + "ja_JP": "Plugin one", + }, + "type": "builtin", + } + ], "uninstalled_recommended_plugins": [{"id": "p2"}], } service.get_recommended_plugins.return_value = recommended_plugins diff --git a/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py b/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py index ef38dec1344a5e..b4b33fe7528404 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py @@ -104,7 +104,11 @@ def test_process_rule_authorizes_document_dataset_before_read(app, documents): def test_default_process_rule_does_not_require_dataset_permission(app, documents): - documents.get_process_rule.return_value = {"mode": "automatic", "rules": {}, "limits": {}} + documents.get_process_rule.return_value = { + "mode": "automatic", + "rules": {}, + "limits": {"indexing_max_segmentation_tokens_length": 4000}, + } with app.test_request_context("/"), patch.object(controller, "enforce_rbac_checks") as enforce_checks: unwrap(GetProcessRuleApi.get)(GetProcessRuleApi(), CONTEXT) enforce_checks.assert_not_called() diff --git a/api/tests/unit_tests/controllers/console/test_extension.py b/api/tests/unit_tests/controllers/console/test_extension.py index 93cb11d5cd2579..3890be82a23f1e 100644 --- a/api/tests/unit_tests/controllers/console/test_extension.py +++ b/api/tests/unit_tests/controllers/console/test_extension.py @@ -120,7 +120,8 @@ def extensions_service() -> Iterator[MagicMock]: def test_code_based_extension_get_returns_service_data(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - service_result = [{"entrypoint": "main:agent"}] + form_schema: list[dict[str, object]] = [] + service_result = [{"name": "custom_tool", "label": {"en-US": "Custom tool"}, "form_schema": form_schema}] service_mock = MagicMock(return_value=service_result) monkeypatch.setattr( "controllers.console.extension.CodeBasedExtensionService.get_code_based_extension", diff --git a/api/tests/unit_tests/controllers/test_swagger.py b/api/tests/unit_tests/controllers/test_swagger.py index f1bf2efdfd4b3f..6cc6496dbcddb2 100644 --- a/api/tests/unit_tests/controllers/test_swagger.py +++ b/api/tests/unit_tests/controllers/test_swagger.py @@ -890,7 +890,7 @@ def test_console_datasource_catalog_exports_domain_response_schema(exported_cons ] recommended = payload["paths"]["/rag/pipelines/recommended-plugins"]["get"] assert recommended["responses"]["200"]["content"]["application/json"]["schema"] == { - "$ref": "#/components/schemas/RagPipelineOpaqueResponse" + "$ref": "#/components/schemas/RagPipelineRecommendedPluginsResponse" } diff --git a/api/tests/unit_tests/fields/test_dataset_fields.py b/api/tests/unit_tests/fields/test_dataset_fields.py index 02f0692079b547..f08875f342dbe6 100644 --- a/api/tests/unit_tests/fields/test_dataset_fields.py +++ b/api/tests/unit_tests/fields/test_dataset_fields.py @@ -425,6 +425,7 @@ def test_dataset_detail_prefetch_matches_per_dataset_lookups(sqlite_session: Ses assert plain["total_documents"] == 3 assert plain["total_available_documents"] == 1 assert plain["word_count"] == 35 + assert plain["keyword_number"] == 10 assert plain["author_name"] == "Ada" assert [tag["name"] for tag in plain["tags"]] == ["tag"] assert plain["doc_form"] == IndexStructureType.PARAGRAPH_INDEX diff --git a/api/tests/unit_tests/fields/test_document_process_rule_fields.py b/api/tests/unit_tests/fields/test_document_process_rule_fields.py new file mode 100644 index 00000000000000..39a9bef5a7744b --- /dev/null +++ b/api/tests/unit_tests/fields/test_document_process_rule_fields.py @@ -0,0 +1,29 @@ +from fields.document_fields import DocumentProcessRuleResponse, DocumentSourceDetailResponse + + +def test_process_rule_reads_the_legacy_delimiter_as_separator() -> None: + rule = DocumentProcessRuleResponse.model_validate( + { + "mode": "custom", + "rules": { + "pre_processing_rules": [{"id": "remove_extra_spaces", "enabled": True}], + "segmentation": {"delimiter": "\n\n", "max_tokens": 500}, + }, + } + ) + + assert rule.model_dump(mode="json") == { + "mode": "custom", + "rules": { + "pre_processing_rules": [{"id": "remove_extra_spaces", "enabled": True}], + "segmentation": {"separator": "\n\n", "max_tokens": 500, "chunk_overlap": 0}, + "parent_mode": None, + "subchunk_segmentation": None, + }, + } + + +def test_source_detail_keeps_notion_and_website_source_info() -> None: + detail = DocumentSourceDetailResponse.model_validate({"url": "https://example.com", "title": "Example"}) + + assert detail.model_dump(mode="json") == {"url": "https://example.com", "title": "Example"} diff --git a/e2e/features/agent-v2/support/fixtures/datasets.ts b/e2e/features/agent-v2/support/fixtures/datasets.ts index 1c7ec293eada49..64145f5e08610b 100644 --- a/e2e/features/agent-v2/support/fixtures/datasets.ts +++ b/e2e/features/agent-v2/support/fixtures/datasets.ts @@ -29,7 +29,7 @@ const getDatasetDocuments = async (client: ConsoleClient, datasetId: string) => while (hasMore) { const response = await client.datasets.byDatasetId.documents.get({ params: { dataset_id: datasetId }, - query: { limit: '100', page: String(page) }, + query: { limit: 100, page }, }) documents.push(...response.data) diff --git a/e2e/features/agent-v2/support/seed.ts b/e2e/features/agent-v2/support/seed.ts index 22d24f1babe740..25a9be453ba7e3 100644 --- a/e2e/features/agent-v2/support/seed.ts +++ b/e2e/features/agent-v2/support/seed.ts @@ -423,7 +423,7 @@ const datasetHasKnowledgeSegment = async ( ) => { const documents = await client.datasets.byDatasetId.documents.get({ params: { dataset_id: datasetId }, - query: { limit: '100', page: '1' }, + query: { limit: 100, page: 1 }, }) for (const document of documents.data) { const body = await client.datasets.byDatasetId.documents.byDocumentId.segments.get({ diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index ab179df165e607..8e9f1033d1f464 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -132,25 +132,17 @@ "count": 1 } }, - "web/app/components/app/annotation/header-opts/index.tsx": { - "typescript/no-explicit-any": { - "count": 1 - } - }, "web/app/components/app/annotation/index.tsx": { "eslint-react/set-state-in-effect": { "count": 1 }, "typescript/no-explicit-any": { - "count": 5 + "count": 1 } }, "web/app/components/app/annotation/view-annotation-modal/index.tsx": { "eslint-react/set-state-in-effect": { "count": 5 - }, - "typescript/no-explicit-any": { - "count": 1 } }, "web/app/components/app/app-publisher/__tests__/features-wrapper.spec.tsx": { @@ -313,7 +305,7 @@ }, "web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx": { "typescript/no-explicit-any": { - "count": 36 + "count": 32 } }, "web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx": { @@ -339,7 +331,7 @@ }, "web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx": { "typescript/no-explicit-any": { - "count": 2 + "count": 1 } }, "web/app/components/app/configuration/dataset-config/settings-modal/index.tsx": { @@ -449,11 +441,6 @@ "count": 5 } }, - "web/app/components/app/log/__tests__/list-utils.spec.ts": { - "typescript/no-explicit-any": { - "count": 2 - } - }, "web/app/components/app/log/__tests__/list.spec.tsx": { "typescript/no-explicit-any": { "count": 9 @@ -474,7 +461,7 @@ "count": 2 }, "typescript/no-explicit-any": { - "count": 5 + "count": 3 } }, "web/app/components/app/log/var-panel.tsx": { @@ -796,11 +783,6 @@ "count": 3 } }, - "web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts": { - "typescript/no-explicit-any": { - "count": 2 - } - }, "web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx": { "dify/require-disable-directive-description": { "count": 1 @@ -1822,21 +1804,11 @@ "count": 1 } }, - "web/app/components/header/account-setting/data-source-page-new/types.ts": { - "typescript/no-explicit-any": { - "count": 2 - } - }, "web/app/components/header/account-setting/model-provider-page/declarations.ts": { "typescript/no-explicit-any": { "count": 4 } }, - "web/app/components/header/account-setting/model-provider-page/hooks.ts": { - "@tanstack/query/prefer-query-options": { - "count": 1 - } - }, "web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 2 @@ -3989,24 +3961,14 @@ "count": 1 } }, - "web/models/datasets.ts": { - "typescript/no-explicit-any": { - "count": 5 - } - }, "web/models/debug.ts": { "typescript/no-explicit-any": { "count": 3 } }, - "web/models/log.ts": { - "typescript/no-explicit-any": { - "count": 6 - } - }, "web/models/pipeline.ts": { "typescript/no-explicit-any": { - "count": 6 + "count": 2 } }, "web/models/share.ts": { @@ -4039,11 +4001,6 @@ "count": 1 } }, - "web/service/__tests__/use-pipeline.spec.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/access-control/__tests__/use-app-access-control.spec.tsx": { "no-restricted-imports": { "count": 1 @@ -4057,36 +4014,7 @@ "count": 1 } }, - "web/service/access-control/use-member-roles.ts": { - "@tanstack/query/prefer-query-options": { - "count": 2 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/access-control/use-workspace-roles.ts": { - "@tanstack/query/prefer-query-options": { - "count": 6 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/annotation.spec.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/annotation.ts": { - "no-restricted-imports": { - "count": 1 - }, - "typescript/no-explicit-any": { - "count": 3 - } - }, - "web/service/apps.ts": { "no-restricted-imports": { "count": 1 } @@ -4109,15 +4037,7 @@ "count": 1 }, "typescript/no-explicit-any": { - "count": 4 - } - }, - "web/service/datasets.ts": { - "no-restricted-imports": { "count": 1 - }, - "typescript/no-explicit-any": { - "count": 3 } }, "web/service/debug.ts": { @@ -4125,7 +4045,7 @@ "count": 2 }, "typescript/no-explicit-any": { - "count": 6 + "count": 1 } }, "web/service/fetch.spec.ts": { @@ -4141,61 +4061,6 @@ "count": 2 } }, - "web/service/knowledge/use-create-dataset.ts": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/knowledge/use-dataset.spec.ts": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/knowledge/use-dataset.ts": { - "@tanstack/query/prefer-query-options": { - "count": 8 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/knowledge/use-document.ts": { - "@tanstack/query/prefer-query-options": { - "count": 4 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/knowledge/use-hit-testing.ts": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/knowledge/use-import.ts": { - "@tanstack/query/prefer-query-options": { - "count": 2 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/knowledge/use-metadata.ts": { - "@tanstack/query/prefer-query-options": { - "count": 8 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/knowledge/use-segment.ts": { - "@tanstack/query/prefer-query-options": { - "count": 2 - }, - "no-restricted-imports": { - "count": 1 - } - }, "web/service/log.ts": { "no-restricted-imports": { "count": 1 @@ -4224,48 +4089,11 @@ "count": 1 } }, - "web/service/tools.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/use-common.ts": { "@tanstack/query/prefer-query-options": { - "count": 9 - }, - "dify/require-disable-directive-description": { - "count": 1 - } - }, - "web/service/use-datasource.ts": { - "@tanstack/query/prefer-query-options": { - "count": 3 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/use-log.ts": { - "@tanstack/query/prefer-query-options": { - "count": 7 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/service/use-models.ts": { - "@tanstack/query/prefer-query-options": { - "count": 3 - }, - "no-restricted-imports": { "count": 1 - } - }, - "web/service/use-pipeline.ts": { - "@tanstack/query/prefer-query-options": { - "count": 8 }, - "no-restricted-imports": { + "dify/require-disable-directive-description": { "count": 1 } }, @@ -4303,14 +4131,6 @@ "count": 1 } }, - "web/service/use-tools.ts": { - "@tanstack/query/prefer-query-options": { - "count": 15 - }, - "no-restricted-imports": { - "count": 1 - } - }, "web/service/use-triggers.ts": { "@tanstack/query/prefer-query-options": { "count": 6 @@ -4325,21 +4145,10 @@ "@tanstack/query/prefer-query-options": { "count": 6 }, - "no-restricted-imports": { - "count": 1 - }, "typescript/no-explicit-any": { "count": 1 } }, - "web/service/use-workspace.ts": { - "@tanstack/query/prefer-query-options": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, "web/service/webapp-auth.ts": { "no-restricted-globals": { "count": 6 @@ -4365,7 +4174,7 @@ }, "web/types/pipeline.tsx": { "typescript/no-explicit-any": { - "count": 3 + "count": 2 } }, "web/types/workflow.ts": { diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 71720dff30e7e8..d01e6f92cb825a 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -427,7 +427,7 @@ export type AgentAppFeaturesPayload = { text_to_speech?: AgentTextToSpeechFeatureConfig | null } -export type MessageFeedbackPayload = { +export type AppMessageFeedbackPayload = { content?: string | null message_id: string rating?: 'dislike' | 'like' | null @@ -470,12 +470,12 @@ export type MessageDetailResponse = { from_source: string id: string inputs: { - [key: string]: JsonValue + [key: string]: AnyJsonValue } - message: JsonValue + message: AnyJsonValue message_files: Array message_tokens: number - metadata: JsonValue + metadata: AnyJsonValue parent_message_id?: string | null provider_response_latency: number query: string @@ -1048,7 +1048,7 @@ export type AgentThought = { thought?: string | null tool?: string | null tool_input?: string | null - tool_labels: JsonValue + tool_labels: AnyJsonValue } export type ConversationAnnotation = { @@ -1081,16 +1081,7 @@ export type Feedback = { rating: string } -export type JsonValue = - | string - | number - | number - | boolean - | { - [key: string]: unknown - } - | Array - | null +export type AnyJsonValue = unknown export type MessageFile = { belongs_to?: string | null @@ -1238,7 +1229,7 @@ export type AppDatasetConfigsResponse = { export type AppEnabledExternalDataToolResponse = { config: { - [key: string]: JsonValue2 + [key: string]: JsonValue } enabled: true icon?: string @@ -1251,7 +1242,7 @@ export type AppEnabledExternalDataToolResponse = { export type AppDisabledExternalDataToolResponse = { config?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } enabled: false icon?: string @@ -1273,7 +1264,7 @@ export type AppFileUploadResponse = { export type AppModelSelectionResponse = { completion_params?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } mode?: LlmMode | '' name?: string @@ -1289,9 +1280,9 @@ export type PromptType = 'advanced' | 'simple' export type AppSensitiveWordAvoidanceResponse = { config?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } - configs?: Array + configs?: Array enabled: boolean type?: string [key: string]: unknown @@ -1628,7 +1619,7 @@ export type HumanInputFormSubmissionData = { node_title: string rendered_content: string submitted_data?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } | null } @@ -1706,7 +1697,7 @@ export type AppProviderAgentToolResponse = { tool_label?: string tool_name: string tool_parameters: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } @@ -1718,28 +1709,28 @@ export type AppLegacyDatasetToolResponse = { export type AppLegacyGoogleSearchToolResponse = { google_search: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } export type AppLegacyWebReaderToolResponse = { web_reader: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } export type AppLegacyWikipediaToolResponse = { wikipedia: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } export type AppLegacyCurrentDatetimeToolResponse = { current_datetime: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } @@ -1795,7 +1786,7 @@ export type AppWeightsResponse = { [key: string]: unknown } -export type JsonValue2 = unknown +export type JsonValue = unknown export type FileType = 'audio' | 'custom' | 'document' | 'image' | 'video' @@ -1815,9 +1806,9 @@ export type AppUserInputFormConfigResponse = { allowed_file_types?: Array allowed_file_upload_methods?: Array config?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } - default?: JsonValue2 + default?: JsonValue description?: string enabled?: boolean hide?: boolean @@ -1826,7 +1817,7 @@ export type AppUserInputFormConfigResponse = { json_schema?: | string | { - [key: string]: JsonValue2 + [key: string]: JsonValue } | null label: string @@ -3565,7 +3556,7 @@ export type PostAgentByAgentIdFeaturesResponse = PostAgentByAgentIdFeaturesResponses[keyof PostAgentByAgentIdFeaturesResponses] export type PostAgentByAgentIdFeedbacksData = { - body: MessageFeedbackPayload + body: AppMessageFeedbackPayload path: { agent_id: string } diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 241aab2cb979ba..cd6352f8de0096 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -438,9 +438,9 @@ export const zAgentDebugConversationRefreshResponse = z.object({ }) /** - * MessageFeedbackPayload + * AppMessageFeedbackPayload */ -export const zMessageFeedbackPayload = z.object({ +export const zAppMessageFeedbackPayload = z.object({ content: z.string().nullish(), message_id: z.string(), rating: z.enum(['dislike', 'like']).nullish(), @@ -998,16 +998,7 @@ export const zAgentLogListResponse = z.object({ total: z.int(), }) -export const zJsonValue = z - .union([ - z.string(), - z.int(), - z.number(), - z.boolean(), - z.record(z.string(), z.unknown()), - z.array(z.unknown()), - ]) - .nullable() +export const zAnyJsonValue = z.unknown() /** * AgentThought @@ -1024,7 +1015,7 @@ export const zAgentThought = z.object({ thought: z.string().nullish(), tool: z.string().nullish(), tool_input: z.string().nullish(), - tool_labels: zJsonValue, + tool_labels: zAnyJsonValue, }) /** @@ -1780,13 +1771,13 @@ export const zAppRerankingModelResponse = z.object({ reranking_provider_name: z.string().optional(), }) -export const zJsonValue2 = z.unknown() +export const zJsonValue = z.unknown() /** * AppEnabledExternalDataToolResponse */ export const zAppEnabledExternalDataToolResponse = z.object({ - config: z.record(z.string(), zJsonValue2), + config: z.record(z.string(), zJsonValue), enabled: z.literal(true), icon: z.string().optional(), icon_background: z.string().optional(), @@ -1799,7 +1790,7 @@ export const zAppEnabledExternalDataToolResponse = z.object({ * AppDisabledExternalDataToolResponse */ export const zAppDisabledExternalDataToolResponse = z.object({ - config: z.record(z.string(), zJsonValue2).optional(), + config: z.record(z.string(), zJsonValue).optional(), enabled: z.literal(false), icon: z.string().optional(), icon_background: z.string().optional(), @@ -1812,8 +1803,8 @@ export const zAppDisabledExternalDataToolResponse = z.object({ * AppSensitiveWordAvoidanceResponse */ export const zAppSensitiveWordAvoidanceResponse = z.object({ - config: z.record(z.string(), zJsonValue2).optional(), - configs: z.array(zJsonValue2).optional(), + config: z.record(z.string(), zJsonValue).optional(), + configs: z.array(zJsonValue).optional(), enabled: z.boolean(), type: z.string().optional(), }) @@ -1827,35 +1818,35 @@ export const zHumanInputFormSubmissionData = z.object({ node_id: z.string(), node_title: z.string(), rendered_content: z.string(), - submitted_data: z.record(z.string(), zJsonValue2).nullish(), + submitted_data: z.record(z.string(), zJsonValue).nullish(), }) /** * AppLegacyGoogleSearchToolResponse */ export const zAppLegacyGoogleSearchToolResponse = z.object({ - google_search: z.record(z.string(), zJsonValue2), + google_search: z.record(z.string(), zJsonValue), }) /** * AppLegacyWebReaderToolResponse */ export const zAppLegacyWebReaderToolResponse = z.object({ - web_reader: z.record(z.string(), zJsonValue2), + web_reader: z.record(z.string(), zJsonValue), }) /** * AppLegacyWikipediaToolResponse */ export const zAppLegacyWikipediaToolResponse = z.object({ - wikipedia: z.record(z.string(), zJsonValue2), + wikipedia: z.record(z.string(), zJsonValue), }) /** * AppLegacyCurrentDatetimeToolResponse */ export const zAppLegacyCurrentDatetimeToolResponse = z.object({ - current_datetime: z.record(z.string(), zJsonValue2), + current_datetime: z.record(z.string(), zJsonValue), }) /** @@ -1906,7 +1897,7 @@ export const zLlmMode = z.enum(['chat', 'completion']) * AppModelSelectionResponse */ export const zAppModelSelectionResponse = z.object({ - completion_params: z.record(z.string(), zJsonValue2).optional(), + completion_params: z.record(z.string(), zJsonValue).optional(), mode: z.union([zLlmMode, z.literal('')]).optional(), name: z.string().optional(), provider: z.string().optional(), @@ -1992,14 +1983,14 @@ export const zAppUserInputFormConfigResponse = z.object({ allowed_file_extensions: z.array(z.string()).optional(), allowed_file_types: z.array(zFileType).optional(), allowed_file_upload_methods: z.array(zFileTransferMethod).optional(), - config: z.record(z.string(), zJsonValue2).optional(), - default: zJsonValue2.optional(), + config: z.record(z.string(), zJsonValue).optional(), + default: zJsonValue.optional(), description: z.string().optional(), enabled: z.boolean().optional(), hide: z.boolean().optional(), icon: z.string().nullish(), icon_background: z.string().nullish(), - json_schema: z.union([z.string(), z.record(z.string(), zJsonValue2)]).nullish(), + json_schema: z.union([z.string(), z.record(z.string(), zJsonValue)]).nullish(), label: z.string(), max_length: z.int().nullish(), options: z.array(z.string()).optional(), @@ -2478,7 +2469,7 @@ export const zAppProviderAgentToolResponse = z.object({ provider_type: zToolProviderType, tool_label: z.string().optional(), tool_name: z.string(), - tool_parameters: z.record(z.string(), zJsonValue2), + tool_parameters: z.record(z.string(), zJsonValue), }) /** @@ -3369,11 +3360,11 @@ export const zMessageDetailResponse = z.object({ from_end_user_id: z.string().nullish(), from_source: z.string(), id: z.string(), - inputs: z.record(z.string(), zJsonValue), - message: zJsonValue, + inputs: z.record(z.string(), zAnyJsonValue), + message: zAnyJsonValue, message_files: z.array(zMessageFile), message_tokens: z.int(), - metadata: zJsonValue, + metadata: zAnyJsonValue, parent_message_id: z.string().nullish(), provider_response_latency: z.number(), query: z.string(), @@ -4033,7 +4024,7 @@ export const zPostAgentByAgentIdFeaturesPath = z.object({ */ export const zPostAgentByAgentIdFeaturesResponse = zSimpleResultResponse -export const zPostAgentByAgentIdFeedbacksBody = zMessageFeedbackPayload +export const zPostAgentByAgentIdFeedbacksBody = zAppMessageFeedbackPayload export const zPostAgentByAgentIdFeedbacksPath = z.object({ agent_id: z.uuid(), diff --git a/packages/contracts/generated/api/console/app/types.gen.ts b/packages/contracts/generated/api/console/app/types.gen.ts index bc4980b7e13b3d..e3669c3e4d2700 100644 --- a/packages/contracts/generated/api/console/app/types.gen.ts +++ b/packages/contracts/generated/api/console/app/types.gen.ts @@ -5,12 +5,9 @@ export type ClientOptions = { } export type AdvancedPromptTemplateResponse = { - chat_prompt_config?: { - [key: string]: unknown - } | null - completion_prompt_config?: { - [key: string]: unknown - } | null + chat_prompt_config?: ChatPromptTemplateResponse | null + completion_prompt_config?: CompletionPromptTemplateResponse | null + stop?: Array | null } export type ErrorResponse = { @@ -23,6 +20,29 @@ export type ErrorResponse = { status: number } +export type ChatPromptTemplateResponse = { + prompt: Array +} + +export type CompletionPromptTemplateResponse = { + conversation_histories_role?: ConversationHistoriesRoleResponse | null + prompt: CompletionPromptTemplateTextResponse +} + +export type PromptTemplateMessageResponse = { + role: 'assistant' | 'system' | 'user' + text: string +} + +export type ConversationHistoriesRoleResponse = { + assistant_prefix: string + user_prefix: string +} + +export type CompletionPromptTemplateTextResponse = { + text: string +} + export type GetAppPromptTemplatesData = { body?: never path?: never diff --git a/packages/contracts/generated/api/console/app/zod.gen.ts b/packages/contracts/generated/api/console/app/zod.gen.ts index be4ed9303451b5..4141461c80f111 100644 --- a/packages/contracts/generated/api/console/app/zod.gen.ts +++ b/packages/contracts/generated/api/console/app/zod.gen.ts @@ -2,14 +2,6 @@ import * as z from 'zod' -/** - * AdvancedPromptTemplateResponse - */ -export const zAdvancedPromptTemplateResponse = z.object({ - chat_prompt_config: z.record(z.string(), z.unknown()).nullish(), - completion_prompt_config: z.record(z.string(), z.unknown()).nullish(), -}) - /** * ErrorResponse * @@ -26,6 +18,53 @@ export const zErrorResponse = z.object({ status: z.int(), }) +/** + * PromptTemplateMessageResponse + */ +export const zPromptTemplateMessageResponse = z.object({ + role: z.enum(['assistant', 'system', 'user']), + text: z.string(), +}) + +/** + * ChatPromptTemplateResponse + */ +export const zChatPromptTemplateResponse = z.object({ + prompt: z.array(zPromptTemplateMessageResponse), +}) + +/** + * ConversationHistoriesRoleResponse + */ +export const zConversationHistoriesRoleResponse = z.object({ + assistant_prefix: z.string(), + user_prefix: z.string(), +}) + +/** + * CompletionPromptTemplateTextResponse + */ +export const zCompletionPromptTemplateTextResponse = z.object({ + text: z.string(), +}) + +/** + * CompletionPromptTemplateResponse + */ +export const zCompletionPromptTemplateResponse = z.object({ + conversation_histories_role: zConversationHistoriesRoleResponse.nullish(), + prompt: zCompletionPromptTemplateTextResponse, +}) + +/** + * AdvancedPromptTemplateResponse + */ +export const zAdvancedPromptTemplateResponse = z.object({ + chat_prompt_config: zChatPromptTemplateResponse.nullish(), + completion_prompt_config: zCompletionPromptTemplateResponse.nullish(), + stop: z.array(z.string()).nullish(), +}) + export const zGetAppPromptTemplatesQuery = z.object({ app_mode: z.string(), has_context: z.string().optional().default('true'), diff --git a/packages/contracts/generated/api/console/apps/orpc.gen.ts b/packages/contracts/generated/api/console/apps/orpc.gen.ts index c42f6ce83a1237..e8dfca8fb6697f 100644 --- a/packages/contracts/generated/api/console/apps/orpc.gen.ts +++ b/packages/contracts/generated/api/console/apps/orpc.gen.ts @@ -63,6 +63,7 @@ import { zDeleteAppsByAppIdAnnotationsByAnnotationIdPath, zDeleteAppsByAppIdAnnotationsByAnnotationIdResponse, zDeleteAppsByAppIdAnnotationsPath, + zDeleteAppsByAppIdAnnotationsQuery, zDeleteAppsByAppIdAnnotationsResponse, zDeleteAppsByAppIdChatConversationsByConversationIdPath, zDeleteAppsByAppIdChatConversationsByConversationIdResponse, @@ -1542,7 +1543,12 @@ export const delete4 = oc successStatus: 204, tags: ['console'], }) - .input(z.object({ params: zDeleteAppsByAppIdAnnotationsPath })) + .input( + z.object({ + params: zDeleteAppsByAppIdAnnotationsPath, + query: zDeleteAppsByAppIdAnnotationsQuery.optional(), + }), + ) .output(zDeleteAppsByAppIdAnnotationsResponse) /** diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index c5c6f19cec7560..cdd814d87271ea 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -190,7 +190,7 @@ export type HumanInputFormPreviewResponse = { } } -export type HumanInputFormSubmitPayload = { +export type HumanInputFormPreviewSubmitPayload = { action: string form_inputs: { [key: string]: unknown @@ -210,7 +210,7 @@ export type IterationNodeRunPayload = { } | null } -export type GeneratedAppResponse = JsonValue +export type GeneratedAppResponse = ShallowJsonValue export type LoopNodeRunPayload = { inputs?: { @@ -527,7 +527,7 @@ export type ConversationDetail = { id: string introduction?: string | null message_count: number - model_config?: ModelConfig | null + model_config?: ConversationModelConfig | null status: string updated_at?: number | null user_feedback_stats?: FeedbackStat | null @@ -567,7 +567,7 @@ export type ConversationMessageDetail = { from_source: string id: string message?: MessageDetail | null - model_config?: ModelConfig | null + model_config?: ConversationModelConfig | null status: string } @@ -632,7 +632,7 @@ export type RosterAgentPackageExportFailedError = ErrorResponse & { status: 500 } -export type MessageFeedbackPayload = { +export type AppMessageFeedbackPayload = { content?: string | null message_id: string rating?: 'dislike' | 'like' | null @@ -660,12 +660,12 @@ export type MessageDetailResponse = { from_source: string id: string inputs: { - [key: string]: JsonValue + [key: string]: AnyJsonValue } - message: JsonValue + message: AnyJsonValue message_files: Array message_tokens: number - metadata: JsonValue + metadata: AnyJsonValue parent_message_id?: string | null provider_response_latency: number query: string @@ -1317,14 +1317,12 @@ export type WorkflowTriggerRunErrorResponse = { } export type DraftWorkflowRunPayload = { - datasource_info_list: Array<{ + files?: Array<{ [key: string]: unknown - }> - datasource_type: string + }> | null inputs: { [key: string]: unknown } - start_node_id: string } export type WorkflowRunSnapshotView = { @@ -1390,7 +1388,7 @@ export type WorkflowDraftVariableResponse = { name: string selector: Array type: string - value: JsonValue + value: RecursiveJsonValue value_type: string visible: boolean } @@ -1401,9 +1399,6 @@ export type WorkflowDraftVariableUpdatePayload = { } export type PublishWorkflowPayload = { - knowledge_base_setting?: { - [key: string]: unknown - } | null marked_comment?: string | null marked_name?: string | null } @@ -1601,7 +1596,7 @@ export type DslImportWarning = { export type PluginDependency = { current_identifier?: string | null type: PluginDependencyType - value: Github | Marketplace | Package + value: PluginDependencyGithub | PluginDependencyMarketplace | PluginDependencyPackage } export type RecentAppResponse = { @@ -1639,7 +1634,7 @@ export type AdvancedChatWorkflowRunForListResponse = { version?: string | null } -export type JsonValue = +export type ShallowJsonValue = | string | number | number @@ -1763,13 +1758,13 @@ export type FeedbackStat = { like: number } -export type ModelConfig = { - completion_params?: { - [key: string]: unknown - } - mode: LlmMode - name: string - provider: string +export type ConversationModelConfig = { + agent_mode?: AnyJsonValue | null + model?: AnyJsonValue | null + opening_statement?: string | null + pre_prompt?: string | null + suggested_questions?: AnyJsonValue | null + user_input_form?: AnyJsonValue | null } export type Conversation = { @@ -1805,12 +1800,12 @@ export type MessageDetail = { from_source: string id: string inputs: { - [key: string]: JsonValue + [key: string]: AnyJsonValue } - message: JsonValue + message: AnyJsonValue message_files: Array message_tokens: number - metadata: JsonValue + metadata: AnyJsonValue parent_message_id?: string | null provider_response_latency: number query: string @@ -1840,7 +1835,7 @@ export type AgentThought = { thought?: string | null tool?: string | null tool_input?: string | null - tool_labels: JsonValue + tool_labels: AnyJsonValue } export type ConversationAnnotation = { @@ -1873,6 +1868,8 @@ export type Feedback = { rating: string } +export type AnyJsonValue = unknown + export type MessageFile = { belongs_to?: string | null filename: string @@ -2201,9 +2198,7 @@ export type AccountWithRoleResponse = { last_login_at?: number | null name: string role: string - roles?: Array<{ - [key: string]: string - }> + roles?: Array status: string } @@ -2550,6 +2545,17 @@ export type WorkflowDraftVariableFullContentResponse = { value_type: string } +export type RecursiveJsonValue = + | string + | number + | number + | boolean + | { + [key: string]: RecursiveJsonValue + } + | Array + | null + export type ModelConfigPartial = { created_at?: number | null created_by?: string | null @@ -2615,7 +2621,7 @@ export type AppDatasetConfigsResponse = { export type AppEnabledExternalDataToolResponse = { config: { - [key: string]: JsonValue2 + [key: string]: JsonValue } enabled: true icon?: string @@ -2628,7 +2634,7 @@ export type AppEnabledExternalDataToolResponse = { export type AppDisabledExternalDataToolResponse = { config?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } enabled: false icon?: string @@ -2650,7 +2656,7 @@ export type AppFileUploadResponse = { export type AppModelSelectionResponse = { completion_params?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } mode?: LlmMode | '' name?: string @@ -2666,9 +2672,9 @@ export type PromptType = 'advanced' | 'simple' export type AppSensitiveWordAvoidanceResponse = { config?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } - configs?: Array + configs?: Array enabled: boolean type?: string [key: string]: unknown @@ -2727,19 +2733,19 @@ export type CustomizeTokenStrategy = 'allow' | 'must' | 'not_allow' | 'uuid' export type PluginDependencyType = 'github' | 'marketplace' | 'package' -export type Github = { +export type PluginDependencyGithub = { github_plugin_unique_identifier: string package: string repo: string version: string } -export type Marketplace = { +export type PluginDependencyMarketplace = { marketplace_plugin_unique_identifier: string version?: string | null } -export type Package = { +export type PluginDependencyPackage = { plugin_unique_identifier: string version?: string | null } @@ -2754,11 +2760,20 @@ export type AgentToolCallResponse = { error?: string | null status: string time_cost: number | number - tool_icon?: unknown + tool_icon?: + | string + | { + [key: string]: string + } + | null tool_input: { [key: string]: unknown } - tool_label: string + tool_label: + | string + | { + [key: string]: string + } tool_name: string tool_output: { [key: string]: unknown @@ -2769,7 +2784,7 @@ export type AgentToolCallResponse = { } export type SimpleModelConfig = { - model?: JsonValue | null + model?: AnyJsonValue | null pre_prompt?: string | null } @@ -2780,12 +2795,10 @@ export type StatusCount = { success: number } -export type LlmMode = 'chat' | 'completion' - export type SimpleMessageDetail = { answer: string inputs: { - [key: string]: JsonValue + [key: string]: AnyJsonValue } message: string query: string @@ -2819,7 +2832,7 @@ export type HumanInputFormSubmissionData = { node_title: string rendered_content: string submitted_data?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } | null } @@ -3062,6 +3075,11 @@ export type WorkflowRunForLogResponse = { version?: string | null } +export type MemberRoleSummaryResponse = { + id: string + name: string +} + export type WorkflowFileUploadPayload = { allowed_file_extensions?: Array | null allowed_file_types?: Array | null @@ -3356,7 +3374,7 @@ export type AppProviderAgentToolResponse = { tool_label?: string tool_name: string tool_parameters: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } @@ -3368,28 +3386,28 @@ export type AppLegacyDatasetToolResponse = { export type AppLegacyGoogleSearchToolResponse = { google_search: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } export type AppLegacyWebReaderToolResponse = { web_reader: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } export type AppLegacyWikipediaToolResponse = { wikipedia: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } export type AppLegacyCurrentDatetimeToolResponse = { current_datetime: { - [key: string]: JsonValue2 + [key: string]: JsonValue } [key: string]: unknown } @@ -3443,7 +3461,7 @@ export type AppWeightsResponse = { [key: string]: unknown } -export type JsonValue2 = unknown +export type JsonValue = unknown export type AppImageUploadResponse = { detail?: 'high' | 'low' | null @@ -3452,14 +3470,16 @@ export type AppImageUploadResponse = { transfer_methods?: Array } +export type LlmMode = 'chat' | 'completion' + export type AppUserInputFormConfigResponse = { allowed_file_extensions?: Array allowed_file_types?: Array allowed_file_upload_methods?: Array config?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } - default?: JsonValue2 + default?: JsonValue description?: string enabled?: boolean hide?: boolean @@ -3468,7 +3488,7 @@ export type AppUserInputFormConfigResponse = { json_schema?: | string | { - [key: string]: JsonValue2 + [key: string]: JsonValue } | null label: string @@ -4265,7 +4285,7 @@ export type RecentAppListResponseWritable = { data: Array } -export type GeneratedAppResponseWritable = JsonValue +export type GeneratedAppResponseWritable = ShallowJsonValue export type WorkflowCommentBasicListWritable = { data: Array @@ -4387,9 +4407,7 @@ export type AccountWithRoleResponseWritable = { last_login_at?: number | null name: string role: string - roles?: Array<{ - [key: string]: string - }> + roles?: Array status: string } @@ -4413,6 +4431,17 @@ export type WorkflowCommentReplyWritable = { id: string } +export type RecursiveJsonValueWritable = + | string + | number + | number + | boolean + | { + [key: string]: RecursiveJsonValueWritable + } + | Array + | null + export type AppConfigJsonValueWritable = | string | number @@ -4800,7 +4829,7 @@ export type PostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdForm PostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFormPreviewResponses[keyof PostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFormPreviewResponses] export type PostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFormRunData = { - body: HumanInputFormSubmitPayload + body: HumanInputFormPreviewSubmitPayload path: { app_id: string node_id: string @@ -5453,7 +5482,9 @@ export type DeleteAppsByAppIdAnnotationsData = { path: { app_id: string } - query?: never + query?: { + annotation_id?: Array + } url: '/apps/{app_id}/annotations' } @@ -6185,7 +6216,7 @@ export type GetAppsByAppIdExportResponse = GetAppsByAppIdExportResponses[keyof GetAppsByAppIdExportResponses] export type PostAppsByAppIdFeedbacksData = { - body: MessageFeedbackPayload + body: AppMessageFeedbackPayload path: { app_id: string } @@ -7977,7 +8008,7 @@ export type PostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewRespo PostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewResponses[keyof PostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewResponses] export type PostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunData = { - body: HumanInputFormSubmitPayload + body: HumanInputFormPreviewSubmitPayload path: { app_id: string node_id: string diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 180496d352ce0b..296c66e579a5f0 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -163,9 +163,9 @@ export const zHumanInputFormPreviewResponse = z.object({ }) /** - * HumanInputFormSubmitPayload + * HumanInputFormPreviewSubmitPayload */ -export const zHumanInputFormSubmitPayload = z.object({ +export const zHumanInputFormPreviewSubmitPayload = z.object({ action: z.string(), form_inputs: z.record(z.string(), z.unknown()), inputs: z.record(z.string(), z.unknown()), @@ -627,9 +627,9 @@ export const zRosterAgentPackageExportFailedError = zErrorResponse.and( ) /** - * MessageFeedbackPayload + * AppMessageFeedbackPayload */ -export const zMessageFeedbackPayload = z.object({ +export const zAppMessageFeedbackPayload = z.object({ content: z.string().nullish(), message_id: z.string(), rating: z.enum(['dislike', 'like']).nullish(), @@ -1177,10 +1177,8 @@ export const zWorkflowTriggerRunErrorResponse = z.object({ * DraftWorkflowRunPayload */ export const zDraftWorkflowRunPayload = z.object({ - datasource_info_list: z.array(z.record(z.string(), z.unknown())), - datasource_type: z.string(), + files: z.array(z.record(z.string(), z.unknown())).nullish(), inputs: z.record(z.string(), z.unknown()), - start_node_id: z.string(), }) /** @@ -1248,11 +1246,8 @@ export const zWorkflowDraftVariableUpdatePayload = z.object({ /** * PublishWorkflowPayload - * - * Payload for publishing snippet workflow. */ export const zPublishWorkflowPayload = z.object({ - knowledge_base_setting: z.record(z.string(), z.unknown()).nullish(), marked_comment: z.string().max(100).nullish(), marked_name: z.string().max(20).nullish(), }) @@ -1506,7 +1501,7 @@ export const zRecentAppListResponse = z.object({ data: z.array(zRecentAppResponse), }) -export const zJsonValue = z +export const zShallowJsonValue = z .union([ z.string(), z.int(), @@ -1520,7 +1515,7 @@ export const zJsonValue = z /** * GeneratedAppResponse */ -export const zGeneratedAppResponse = zJsonValue +export const zGeneratedAppResponse = zShallowJsonValue /** * AgentConfigVersionResponse @@ -1744,6 +1739,39 @@ export const zPaginatedConversationVariableResponse = z.object({ total: z.int(), }) +export const zAnyJsonValue = z.unknown() + +/** + * ConversationModelConfig + */ +export const zConversationModelConfig = z.object({ + agent_mode: zAnyJsonValue.nullish(), + model: zAnyJsonValue.nullish(), + opening_statement: z.string().nullish(), + pre_prompt: z.string().nullish(), + suggested_questions: zAnyJsonValue.nullish(), + user_input_form: zAnyJsonValue.nullish(), +}) + +/** + * ConversationDetail + */ +export const zConversationDetail = z.object({ + admin_feedback_stats: zFeedbackStat.nullish(), + annotated: z.boolean(), + created_at: z.int().nullish(), + from_account_id: z.string().nullish(), + from_end_user_id: z.string().nullish(), + from_source: z.string(), + id: z.string(), + introduction: z.string().nullish(), + message_count: z.int(), + model_config: zConversationModelConfig.nullish(), + status: z.string(), + updated_at: z.int().nullish(), + user_feedback_stats: zFeedbackStat.nullish(), +}) + /** * AgentThought */ @@ -1759,7 +1787,7 @@ export const zAgentThought = z.object({ thought: z.string().nullish(), tool: z.string().nullish(), tool_input: z.string().nullish(), - tool_labels: zJsonValue, + tool_labels: zAnyJsonValue, }) /** @@ -2098,30 +2126,6 @@ export const zSandboxListResponse = z.object({ truncated: z.boolean().optional().default(false), }) -/** - * AccountWithRoleResponse - */ -export const zAccountWithRoleResponse = z.object({ - avatar: z.string().nullish(), - avatar_url: z.string().nullable(), - created_at: z.int().nullish(), - email: z.string(), - id: z.string(), - last_active_at: z.int().nullish(), - last_login_at: z.int().nullish(), - name: z.string(), - role: z.string(), - roles: z.array(z.record(z.string(), z.string())).optional(), - status: z.string(), -}) - -/** - * WorkflowCommentMentionUsersPayload - */ -export const zWorkflowCommentMentionUsersPayload = z.object({ - users: z.array(zAccountWithRoleResponse), -}) - /** * WorkflowCommentAccount */ @@ -2633,6 +2637,20 @@ export const zWorkflowDraftVariableFullContentResponse = z.object({ value_type: z.string(), }) +export const zRecursiveJsonValue = z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.record( + z.string(), + z.lazy((): any => zRecursiveJsonValue), + ), + z.array(z.lazy((): any => zRecursiveJsonValue)), + ]) + .nullable() + /** * WorkflowDraftVariableResponse */ @@ -2645,7 +2663,7 @@ export const zWorkflowDraftVariableResponse = z.object({ name: z.string(), selector: z.array(z.string()), type: z.string(), - value: zJsonValue, + value: zRecursiveJsonValue, value_type: z.string(), visible: z.boolean(), }) @@ -2734,7 +2752,7 @@ export const zPluginDependencyType = z.enum(['github', 'marketplace', 'package'] /** * Github */ -export const zGithub = z.object({ +export const zPluginDependencyGithub = z.object({ github_plugin_unique_identifier: z.string(), package: z.string(), repo: z.string(), @@ -2744,7 +2762,7 @@ export const zGithub = z.object({ /** * Marketplace */ -export const zMarketplace = z.object({ +export const zPluginDependencyMarketplace = z.object({ marketplace_plugin_unique_identifier: z.string(), version: z.string().nullish(), }) @@ -2752,7 +2770,7 @@ export const zMarketplace = z.object({ /** * Package */ -export const zPackage = z.object({ +export const zPluginDependencyPackage = z.object({ plugin_unique_identifier: z.string(), version: z.string().nullish(), }) @@ -2763,7 +2781,7 @@ export const zPackage = z.object({ export const zPluginDependency = z.object({ current_identifier: z.string().nullish(), type: zPluginDependencyType, - value: z.union([zGithub, zMarketplace, zPackage]), + value: z.union([zPluginDependencyGithub, zPluginDependencyMarketplace, zPluginDependencyPackage]), }) /** @@ -2804,9 +2822,9 @@ export const zAgentToolCallResponse = z.object({ error: z.string().nullish(), status: z.string(), time_cost: z.union([z.number(), z.int()]), - tool_icon: z.unknown().optional(), + tool_icon: z.union([z.string(), z.record(z.string(), z.string())]).nullish(), tool_input: z.record(z.string(), z.unknown()), - tool_label: z.string(), + tool_label: z.union([z.string(), z.record(z.string(), z.string())]), tool_name: z.string(), tool_output: z.record(z.string(), z.unknown()), tool_parameters: z.record(z.string(), z.unknown()), @@ -2837,7 +2855,7 @@ export const zAgentLogResponse = z.object({ * SimpleModelConfig */ export const zSimpleModelConfig = z.object({ - model: zJsonValue.nullish(), + model: zAnyJsonValue.nullish(), pre_prompt: z.string().nullish(), }) @@ -2886,48 +2904,12 @@ export const zConversationWithSummaryPagination = z.object({ total: z.int(), }) -/** - * LLMMode - * - * Enum class for large language model mode. - */ -export const zLlmMode = z.enum(['chat', 'completion']) - -/** - * ModelConfig - */ -export const zModelConfig = z.object({ - completion_params: z.record(z.string(), z.unknown()).optional(), - mode: zLlmMode, - name: z.string(), - provider: z.string(), -}) - -/** - * ConversationDetail - */ -export const zConversationDetail = z.object({ - admin_feedback_stats: zFeedbackStat.nullish(), - annotated: z.boolean(), - created_at: z.int().nullish(), - from_account_id: z.string().nullish(), - from_end_user_id: z.string().nullish(), - from_source: z.string(), - id: z.string(), - introduction: z.string().nullish(), - message_count: z.int(), - model_config: zModelConfig.nullish(), - status: z.string(), - updated_at: z.int().nullish(), - user_feedback_stats: zFeedbackStat.nullish(), -}) - /** * SimpleMessageDetail */ export const zSimpleMessageDetail = z.object({ answer: z.string(), - inputs: z.record(z.string(), zJsonValue), + inputs: z.record(z.string(), zAnyJsonValue), message: z.string(), query: z.string(), }) @@ -3021,11 +3003,11 @@ export const zMessageDetail = z.object({ from_end_user_id: z.string().nullish(), from_source: z.string(), id: z.string(), - inputs: z.record(z.string(), zJsonValue), - message: zJsonValue, + inputs: z.record(z.string(), zAnyJsonValue), + message: zAnyJsonValue, message_files: z.array(zMessageFile), message_tokens: z.int(), - metadata: zJsonValue, + metadata: zAnyJsonValue, parent_message_id: z.string().nullish(), provider_response_latency: z.number(), query: z.string(), @@ -3043,7 +3025,7 @@ export const zConversationMessageDetail = z.object({ from_source: z.string(), id: z.string(), message: zMessageDetail.nullish(), - model_config: zModelConfig.nullish(), + model_config: zConversationModelConfig.nullish(), status: z.string(), }) @@ -3170,18 +3152,6 @@ export const zAppCompletionPromptPayload = z */ export const zAppEmptyDatasetCollectionPayload = z.record(z.string(), z.never()) -/** - * AppMetadataModelPayload - */ -export const zAppMetadataModelPayload = z - .object({ - completion_params: z.record(z.string(), zAppConfigJsonValue).optional(), - mode: z.union([zLlmMode, z.literal('')]).optional(), - name: z.string().optional(), - provider: z.string().optional(), - }) - .catchall(z.json()) - /** * RerankMode */ @@ -3276,29 +3246,6 @@ export const zAppFileUploadPayload = z }) .catchall(z.json()) -/** - * AppSuggestedQuestionsModelPayload - */ -export const zAppSuggestedQuestionsModelPayload = z - .object({ - completion_params: z.record(z.string(), zAppConfigJsonValue).optional(), - mode: z.union([zLlmMode, z.literal('')]).optional(), - name: z.string(), - provider: z.string(), - }) - .catchall(z.json()) - -/** - * AppSuggestedQuestionsPayload - */ -export const zAppSuggestedQuestionsPayload = z - .object({ - enabled: z.boolean().nullish(), - model: zAppSuggestedQuestionsModelPayload.optional(), - prompt: z.string().optional(), - }) - .catchall(z.json()) - /** * WorkflowRunForLogResponse */ @@ -3341,6 +3288,38 @@ export const zWorkflowAppLogPaginationResponse = z.object({ total: z.int(), }) +/** + * MemberRoleSummaryResponse + */ +export const zMemberRoleSummaryResponse = z.object({ + id: z.string(), + name: z.string(), +}) + +/** + * AccountWithRoleResponse + */ +export const zAccountWithRoleResponse = z.object({ + avatar: z.string().nullish(), + avatar_url: z.string().nullable(), + created_at: z.int().nullish(), + email: z.string(), + id: z.string(), + last_active_at: z.int().nullish(), + last_login_at: z.int().nullish(), + name: z.string(), + role: z.string(), + roles: z.array(zMemberRoleSummaryResponse).optional(), + status: z.string(), +}) + +/** + * WorkflowCommentMentionUsersPayload + */ +export const zWorkflowCommentMentionUsersPayload = z.object({ + users: z.array(zAccountWithRoleResponse), +}) + /** * WorkflowFeatureTogglePayload */ @@ -3705,13 +3684,13 @@ export const zAppRerankingModelResponse = z.object({ reranking_provider_name: z.string().optional(), }) -export const zJsonValue2 = z.unknown() +export const zJsonValue = z.unknown() /** * AppEnabledExternalDataToolResponse */ export const zAppEnabledExternalDataToolResponse = z.object({ - config: z.record(z.string(), zJsonValue2), + config: z.record(z.string(), zJsonValue), enabled: z.literal(true), icon: z.string().optional(), icon_background: z.string().optional(), @@ -3724,7 +3703,7 @@ export const zAppEnabledExternalDataToolResponse = z.object({ * AppDisabledExternalDataToolResponse */ export const zAppDisabledExternalDataToolResponse = z.object({ - config: z.record(z.string(), zJsonValue2).optional(), + config: z.record(z.string(), zJsonValue).optional(), enabled: z.literal(false), icon: z.string().optional(), icon_background: z.string().optional(), @@ -3733,11 +3712,90 @@ export const zAppDisabledExternalDataToolResponse = z.object({ variable: z.string().optional(), }) +/** + * AppSensitiveWordAvoidanceResponse + */ +export const zAppSensitiveWordAvoidanceResponse = z.object({ + config: z.record(z.string(), zJsonValue).optional(), + configs: z.array(zJsonValue).optional(), + enabled: z.boolean(), + type: z.string().optional(), +}) + +/** + * HumanInputFormSubmissionData + */ +export const zHumanInputFormSubmissionData = z.object({ + action_id: z.string(), + action_text: z.string(), + node_id: z.string(), + node_title: z.string(), + rendered_content: z.string(), + submitted_data: z.record(z.string(), zJsonValue).nullish(), +}) + +/** + * AppLegacyGoogleSearchToolResponse + */ +export const zAppLegacyGoogleSearchToolResponse = z.object({ + google_search: z.record(z.string(), zJsonValue), +}) + +/** + * AppLegacyWebReaderToolResponse + */ +export const zAppLegacyWebReaderToolResponse = z.object({ + web_reader: z.record(z.string(), zJsonValue), +}) + +/** + * AppLegacyWikipediaToolResponse + */ +export const zAppLegacyWikipediaToolResponse = z.object({ + wikipedia: z.record(z.string(), zJsonValue), +}) + +/** + * AppLegacyCurrentDatetimeToolResponse + */ +export const zAppLegacyCurrentDatetimeToolResponse = z.object({ + current_datetime: z.record(z.string(), zJsonValue), +}) + +/** + * AppImageUploadResponse + */ +export const zAppImageUploadResponse = z.object({ + detail: z.enum(['high', 'low']).nullish(), + enabled: z.boolean().optional(), + number_limits: z.int().optional(), + transfer_methods: z.array(zFileTransferMethod).optional(), +}) + +/** + * AppFileUploadResponse + */ +export const zAppFileUploadResponse = z.object({ + allowed_file_extensions: z.array(z.string()).optional(), + allowed_file_types: z.array(zFileType).optional(), + allowed_file_upload_methods: z.array(zFileTransferMethod).optional(), + enabled: z.boolean().optional(), + image: zAppImageUploadResponse.optional(), + number_limits: z.int().optional(), +}) + +/** + * LLMMode + * + * Enum class for large language model mode. + */ +export const zLlmMode = z.enum(['chat', 'completion']) + /** * AppModelSelectionResponse */ export const zAppModelSelectionResponse = z.object({ - completion_params: z.record(z.string(), zJsonValue2).optional(), + completion_params: z.record(z.string(), zJsonValue).optional(), mode: z.union([zLlmMode, z.literal('')]).optional(), name: z.string().optional(), provider: z.string().optional(), @@ -3798,16 +3856,6 @@ export const zAppPagination = z.object({ total: z.int(), }) -/** - * AppSensitiveWordAvoidanceResponse - */ -export const zAppSensitiveWordAvoidanceResponse = z.object({ - config: z.record(z.string(), zJsonValue2).optional(), - configs: z.array(zJsonValue2).optional(), - enabled: z.boolean(), - type: z.string().optional(), -}) - /** * AppSuggestedQuestionsAfterAnswerResponse */ @@ -3818,66 +3866,39 @@ export const zAppSuggestedQuestionsAfterAnswerResponse = z.object({ }) /** - * HumanInputFormSubmissionData - */ -export const zHumanInputFormSubmissionData = z.object({ - action_id: z.string(), - action_text: z.string(), - node_id: z.string(), - node_title: z.string(), - rendered_content: z.string(), - submitted_data: z.record(z.string(), zJsonValue2).nullish(), -}) - -/** - * AppLegacyGoogleSearchToolResponse - */ -export const zAppLegacyGoogleSearchToolResponse = z.object({ - google_search: z.record(z.string(), zJsonValue2), -}) - -/** - * AppLegacyWebReaderToolResponse - */ -export const zAppLegacyWebReaderToolResponse = z.object({ - web_reader: z.record(z.string(), zJsonValue2), -}) - -/** - * AppLegacyWikipediaToolResponse - */ -export const zAppLegacyWikipediaToolResponse = z.object({ - wikipedia: z.record(z.string(), zJsonValue2), -}) - -/** - * AppLegacyCurrentDatetimeToolResponse + * AppMetadataModelPayload */ -export const zAppLegacyCurrentDatetimeToolResponse = z.object({ - current_datetime: z.record(z.string(), zJsonValue2), -}) +export const zAppMetadataModelPayload = z + .object({ + completion_params: z.record(z.string(), zAppConfigJsonValue).optional(), + mode: z.union([zLlmMode, z.literal('')]).optional(), + name: z.string().optional(), + provider: z.string().optional(), + }) + .catchall(z.json()) /** - * AppImageUploadResponse + * AppSuggestedQuestionsModelPayload */ -export const zAppImageUploadResponse = z.object({ - detail: z.enum(['high', 'low']).nullish(), - enabled: z.boolean().optional(), - number_limits: z.int().optional(), - transfer_methods: z.array(zFileTransferMethod).optional(), -}) +export const zAppSuggestedQuestionsModelPayload = z + .object({ + completion_params: z.record(z.string(), zAppConfigJsonValue).optional(), + mode: z.union([zLlmMode, z.literal('')]).optional(), + name: z.string(), + provider: z.string(), + }) + .catchall(z.json()) /** - * AppFileUploadResponse + * AppSuggestedQuestionsPayload */ -export const zAppFileUploadResponse = z.object({ - allowed_file_extensions: z.array(z.string()).optional(), - allowed_file_types: z.array(zFileType).optional(), - allowed_file_upload_methods: z.array(zFileTransferMethod).optional(), - enabled: z.boolean().optional(), - image: zAppImageUploadResponse.optional(), - number_limits: z.int().optional(), -}) +export const zAppSuggestedQuestionsPayload = z + .object({ + enabled: z.boolean().nullish(), + model: zAppSuggestedQuestionsModelPayload.optional(), + prompt: z.string().optional(), + }) + .catchall(z.json()) /** * AppUserInputFormConfigResponse @@ -3886,14 +3907,14 @@ export const zAppUserInputFormConfigResponse = z.object({ allowed_file_extensions: z.array(z.string()).optional(), allowed_file_types: z.array(zFileType).optional(), allowed_file_upload_methods: z.array(zFileTransferMethod).optional(), - config: z.record(z.string(), zJsonValue2).optional(), - default: zJsonValue2.optional(), + config: z.record(z.string(), zJsonValue).optional(), + default: zJsonValue.optional(), description: z.string().optional(), enabled: z.boolean().optional(), hide: z.boolean().optional(), icon: z.string().nullish(), icon_background: z.string().nullish(), - json_schema: z.union([z.string(), z.record(z.string(), zJsonValue2)]).nullish(), + json_schema: z.union([z.string(), z.record(z.string(), zJsonValue)]).nullish(), label: z.string(), max_length: z.int().nullish(), options: z.array(z.string()).optional(), @@ -4650,7 +4671,7 @@ export const zAppProviderAgentToolResponse = z.object({ provider_type: zToolProviderType, tool_label: z.string().optional(), tool_name: z.string(), - tool_parameters: z.record(z.string(), zJsonValue2), + tool_parameters: z.record(z.string(), zJsonValue), }) /** @@ -5546,11 +5567,11 @@ export const zMessageDetailResponse = z.object({ from_end_user_id: z.string().nullish(), from_source: z.string(), id: z.string(), - inputs: z.record(z.string(), zJsonValue), - message: zJsonValue, + inputs: z.record(z.string(), zAnyJsonValue), + message: zAnyJsonValue, message_files: z.array(zMessageFile), message_tokens: z.int(), - metadata: zJsonValue, + metadata: zAnyJsonValue, parent_message_id: z.string().nullish(), provider_response_latency: z.number(), query: z.string(), @@ -5736,7 +5757,7 @@ export const zComposerSavePayload = z.object({ /** * GeneratedAppResponse */ -export const zGeneratedAppResponseWritable = zJsonValue +export const zGeneratedAppResponseWritable = zShallowJsonValue /** * AppPartial @@ -5878,7 +5899,7 @@ export const zAccountWithRoleResponseWritable = z.object({ last_login_at: z.int().nullish(), name: z.string(), role: z.string(), - roles: z.array(z.record(z.string(), z.string())).optional(), + roles: z.array(zMemberRoleSummaryResponse).optional(), status: z.string(), }) @@ -5966,6 +5987,20 @@ export const zWorkflowCommentDetailWritable = z.object({ updated_at: z.int().nullish(), }) +export const zRecursiveJsonValueWritable = z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.record( + z.string(), + z.lazy((): any => zRecursiveJsonValueWritable), + ), + z.array(z.lazy((): any => zRecursiveJsonValueWritable)), + ]) + .nullable() + export const zAppConfigJsonValueWritable = z.json().nullable() /** @@ -6173,7 +6208,7 @@ export const zPostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFo zHumanInputFormPreviewResponse export const zPostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFormRunBody = - zHumanInputFormSubmitPayload + zHumanInputFormPreviewSubmitPayload export const zPostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFormRunPath = z.object({ @@ -6518,6 +6553,10 @@ export const zDeleteAppsByAppIdAnnotationsPath = z.object({ app_id: z.uuid(), }) +export const zDeleteAppsByAppIdAnnotationsQuery = z.object({ + annotation_id: z.array(z.string()).optional(), +}) + /** * Annotations deleted successfully */ @@ -6835,7 +6874,7 @@ export const zGetAppsByAppIdExportResponse = z.union([ z.custom((value) => value instanceof Blob || value instanceof File), ]) -export const zPostAppsByAppIdFeedbacksBody = zMessageFeedbackPayload +export const zPostAppsByAppIdFeedbacksBody = zAppMessageFeedbackPayload export const zPostAppsByAppIdFeedbacksPath = z.object({ app_id: z.uuid(), @@ -7670,7 +7709,7 @@ export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewRes zHumanInputFormPreviewResponse export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunBody = - zHumanInputFormSubmitPayload + zHumanInputFormPreviewSubmitPayload export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunPath = z.object({ app_id: z.uuid(), diff --git a/packages/contracts/generated/api/console/code-based-extension/types.gen.ts b/packages/contracts/generated/api/console/code-based-extension/types.gen.ts index ac7b79a9b32a57..379aeddd89c54f 100644 --- a/packages/contracts/generated/api/console/code-based-extension/types.gen.ts +++ b/packages/contracts/generated/api/console/code-based-extension/types.gen.ts @@ -5,7 +5,7 @@ export type ClientOptions = { } export type CodeBasedExtensionResponse = { - data: unknown + data: Array module: string } @@ -19,6 +19,16 @@ export type ErrorResponse = { status: number } +export type CodeBasedExtensionItemResponse = { + form_schema?: Array<{ + [key: string]: unknown + }> | null + label?: { + [key: string]: unknown + } | null + name: string +} + export type GetCodeBasedExtensionData = { body?: never path?: never diff --git a/packages/contracts/generated/api/console/code-based-extension/zod.gen.ts b/packages/contracts/generated/api/console/code-based-extension/zod.gen.ts index 4dfe9fa793e169..c6950da4f58e93 100644 --- a/packages/contracts/generated/api/console/code-based-extension/zod.gen.ts +++ b/packages/contracts/generated/api/console/code-based-extension/zod.gen.ts @@ -2,14 +2,6 @@ import * as z from 'zod' -/** - * CodeBasedExtensionResponse - */ -export const zCodeBasedExtensionResponse = z.object({ - data: z.unknown(), - module: z.string(), -}) - /** * ErrorResponse * @@ -26,6 +18,23 @@ export const zErrorResponse = z.object({ status: z.int(), }) +/** + * CodeBasedExtensionItemResponse + */ +export const zCodeBasedExtensionItemResponse = z.object({ + form_schema: z.array(z.record(z.string(), z.unknown())).nullish(), + label: z.record(z.string(), z.unknown()).nullish(), + name: z.string(), +}) + +/** + * CodeBasedExtensionResponse + */ +export const zCodeBasedExtensionResponse = z.object({ + data: z.array(zCodeBasedExtensionItemResponse), + module: z.string(), +}) + export const zGetCodeBasedExtensionQuery = z.object({ module: z.string(), }) diff --git a/packages/contracts/generated/api/console/datasets/orpc.gen.ts b/packages/contracts/generated/api/console/datasets/orpc.gen.ts index 93b4d2ca9803a3..7470640812ce9e 100644 --- a/packages/contracts/generated/api/console/datasets/orpc.gen.ts +++ b/packages/contracts/generated/api/console/datasets/orpc.gen.ts @@ -32,6 +32,7 @@ import { zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsQuery, zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse, zDeleteDatasetsByDatasetIdDocumentsPath, + zDeleteDatasetsByDatasetIdDocumentsQuery, zDeleteDatasetsByDatasetIdDocumentsResponse, zDeleteDatasetsByDatasetIdMetadataByMetadataIdPath, zDeleteDatasetsByDatasetIdMetadataByMetadataIdResponse, @@ -89,6 +90,7 @@ import { zGetDatasetsByDatasetIdPermissionPartUsersPath, zGetDatasetsByDatasetIdPermissionPartUsersResponse, zGetDatasetsByDatasetIdQueriesPath, + zGetDatasetsByDatasetIdQueriesQuery, zGetDatasetsByDatasetIdQueriesResponse, zGetDatasetsByDatasetIdRelatedAppsPath, zGetDatasetsByDatasetIdRelatedAppsResponse, @@ -131,6 +133,7 @@ import { zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath, zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse, zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchPath, + zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchQuery, zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchResponse, zPatchDatasetsByDatasetIdMetadataByMetadataIdBody, zPatchDatasetsByDatasetIdMetadataByMetadataIdPath, @@ -737,7 +740,12 @@ export const patch2 = oc path: '/datasets/{dataset_id}/documents/status/{action}/batch', tags: ['console'], }) - .input(z.object({ params: zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchPath })) + .input( + z.object({ + params: zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchPath, + query: zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchQuery.optional(), + }), + ) .output(zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchResponse) .errors({ invalid_action: { status: 400, data: zInvalidActionError } }) @@ -1331,7 +1339,12 @@ export const delete7 = oc successStatus: 204, tags: ['console'], }) - .input(z.object({ params: zDeleteDatasetsByDatasetIdDocumentsPath })) + .input( + z.object({ + params: zDeleteDatasetsByDatasetIdDocumentsPath, + query: zDeleteDatasetsByDatasetIdDocumentsQuery.optional(), + }), + ) .output(zDeleteDatasetsByDatasetIdDocumentsResponse) /** @@ -1618,7 +1631,12 @@ export const get30 = oc path: '/datasets/{dataset_id}/queries', tags: ['console'], }) - .input(z.object({ params: zGetDatasetsByDatasetIdQueriesPath })) + .input( + z.object({ + params: zGetDatasetsByDatasetIdQueriesPath, + query: zGetDatasetsByDatasetIdQueriesQuery.optional(), + }), + ) .output(zGetDatasetsByDatasetIdQueriesResponse) export const queries = { diff --git a/packages/contracts/generated/api/console/datasets/types.gen.ts b/packages/contracts/generated/api/console/datasets/types.gen.ts index 3e315a18f90702..44da2d14ceb29e 100644 --- a/packages/contracts/generated/api/console/datasets/types.gen.ts +++ b/packages/contracts/generated/api/console/datasets/types.gen.ts @@ -39,7 +39,14 @@ export type DatasetDetailWithPartialMembersResponse = { chunk_structure: string | null created_at: number created_by: string - data_source_type: string | null + data_source_type: + | 'local_file' + | 'notion_import' + | 'online_document' + | 'online_drive' + | 'upload_file' + | 'website_crawl' + | null description: string | null doc_form: string | null doc_metadata: Array @@ -52,18 +59,19 @@ export type DatasetDetailWithPartialMembersResponse = { external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string - indexing_technique: string | null + indexing_technique: IndexTechniqueType | null is_multimodal: boolean is_published: boolean + keyword_number?: number | null maintainer?: string | null name: string partial_member_list?: Array | null - permission: string + permission: 'all_team_members' | 'only_me' | 'partial_members' permission_keys?: Array pipeline_id: string | null provider: string retrieval_model_dict: DatasetRetrievalModelResponse - runtime_mode: string | null + runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number @@ -126,7 +134,14 @@ export type DatasetDetailResponse = { chunk_structure: string | null created_at: number created_by: string - data_source_type: string | null + data_source_type: + | 'local_file' + | 'notion_import' + | 'online_document' + | 'online_drive' + | 'upload_file' + | 'website_crawl' + | null description: string | null doc_form: string | null doc_metadata: Array @@ -139,17 +154,18 @@ export type DatasetDetailResponse = { external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string - indexing_technique: string | null + indexing_technique: IndexTechniqueType | null is_multimodal: boolean is_published: boolean + keyword_number?: number | null maintainer?: string | null name: string - permission: string + permission: 'all_team_members' | 'only_me' | 'partial_members' permission_keys?: Array pipeline_id: string | null provider: string retrieval_model_dict: DatasetRetrievalModelResponse - runtime_mode: string | null + runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number @@ -181,9 +197,7 @@ export type ExternalKnowledgeApiResponse = { description: string id: string name: string - settings: { - [key: string]: unknown - } | null + settings: ExternalKnowledgeApiSettingsResponse | null tenant_id: string } @@ -286,9 +300,7 @@ export type IndexingEstimate = { } export type ProcessRuleResponse = { - limits: { - [key: string]: unknown - } + limits: ProcessRuleLimitsResponse mode: ProcessRuleMode rules?: Rule | null } @@ -374,10 +386,12 @@ export type DocumentDetailResponse = { created_at?: number | null created_by?: string | null created_from?: string | null - data_source_detail_dict?: unknown - data_source_info?: unknown + data_source_detail_dict?: DocumentSourceDetailResponse | null + data_source_info?: { + [key: string]: JsonValue + } | null data_source_type?: string | null - dataset_process_rule?: unknown + dataset_process_rule?: DocumentProcessRuleResponse | null dataset_process_rule_id?: string | null disabled_at?: number | null disabled_by?: string | null @@ -386,7 +400,7 @@ export type DocumentDetailResponse = { doc_language?: string | null doc_metadata?: Array | null doc_type?: string | null - document_process_rule?: unknown + document_process_rule?: DocumentProcessRuleResponse | null enabled?: boolean | null error?: string | null hit_count?: number | null @@ -415,14 +429,25 @@ export type DocumentStatusResponse = { completed_at: number | null completed_segments?: number | null error: string | null + error_code?: 'vector_space_estimate_exceeded' | null + estimated_vector_space_mb?: number | null id: string - indexing_status: string + indexing_status: + | 'cleaning' + | 'completed' + | 'error' + | 'indexing' + | 'parsing' + | 'paused' + | 'splitting' + | 'waiting' parsing_completed_at: number | null paused_at: number | null processing_started_at: number | null splitting_completed_at: number | null stopped_at: number | null total_segments?: number | null + vector_space_limit_mb?: number | null } export type DocumentMetadataUpdatePayload = { @@ -461,11 +486,9 @@ export type DocumentResponse = { created_at?: number | null created_by?: string | null created_from?: string | null - data_source_detail_dict: { - [key: string]: unknown - } + data_source_detail_dict: DocumentSourceDetailResponse data_source_info?: { - [key: string]: unknown + [key: string]: JsonValue } | null data_source_type?: string | null dataset_process_rule_id?: string | null @@ -622,7 +645,7 @@ export type MetadataArgs = { export type DatasetMetadataResponse = { id: string name: string - type: string + type: 'number' | 'string' | 'time' } export type MetadataUpdatePayload = { @@ -661,7 +684,14 @@ export type DatasetListItemResponse = { chunk_structure: string | null created_at: number created_by: string - data_source_type: string | null + data_source_type: + | 'local_file' + | 'notion_import' + | 'online_document' + | 'online_drive' + | 'upload_file' + | 'website_crawl' + | null description: string | null doc_form: string | null doc_metadata: Array @@ -674,18 +704,19 @@ export type DatasetListItemResponse = { external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string - indexing_technique: string | null + indexing_technique: IndexTechniqueType | null is_multimodal: boolean is_published: boolean + keyword_number?: number | null maintainer?: string | null name: string partial_member_list: Array - permission: string + permission: 'all_team_members' | 'only_me' | 'partial_members' permission_keys?: Array pipeline_id: string | null provider: string retrieval_model_dict: DatasetRetrievalModelResponse - runtime_mode: string | null + runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number @@ -700,7 +731,7 @@ export type PermissionEnum = 'all_team_members' | 'only_me' | 'partial_members' export type DatasetDocMetadataResponse = { id: string name: string - type: string + type: 'number' | 'string' | 'time' } export type DatasetExternalKnowledgeInfoResponse = { @@ -723,13 +754,15 @@ export type DatasetIconInfoResponse = { icon_url?: string | null } +export type IndexTechniqueType = 'economy' | 'high_quality' + export type DatasetRetrievalModelResponse = { reranking_enable: boolean reranking_mode?: string | null reranking_model?: DatasetRerankingModelResponse score_threshold?: number | null score_threshold_enabled: boolean - search_method: string + search_method: RetrievalMethod top_k: number weights?: DatasetWeightedScoreResponse | null } @@ -752,6 +785,11 @@ export type ExternalKnowledgeApiBindingResponse = { name: string } +export type ExternalKnowledgeApiSettingsResponse = { + api_key?: string | null + endpoint?: string | null +} + export type PreviewDetail = { child_chunks?: Array | null content: string @@ -797,7 +835,7 @@ export type DatasetResponse = { export type DatasetMetadataBuiltInFieldResponse = { name: string - type: string + type: 'number' | 'string' | 'time' } export type NotionEstimateWorkspacePayload = { @@ -806,6 +844,10 @@ export type NotionEstimateWorkspacePayload = { workspace_id: string } +export type ProcessRuleLimitsResponse = { + indexing_max_segmentation_tokens_length: number +} + export type ProcessRuleMode = 'automatic' | 'custom' | 'hierarchical' export type Rule = { @@ -821,11 +863,9 @@ export type DocumentWithSegmentsResponse = { created_at?: number | null created_by?: string | null created_from?: string | null - data_source_detail_dict: { - [key: string]: unknown - } + data_source_detail_dict: DocumentSourceDetailResponse data_source_info?: { - [key: string]: unknown + [key: string]: JsonValue } | null data_source_type?: string | null dataset_process_rule_id?: string | null @@ -842,7 +882,7 @@ export type DocumentWithSegmentsResponse = { name: string need_summary?: boolean | null position?: number | null - process_rule_dict?: unknown + process_rule_dict?: DocumentProcessRuleResponse | null summary_index_status?: string | null tokens?: number | null total_segments?: number | null @@ -855,15 +895,25 @@ export type DocumentMetadataOperation = { partial_update?: boolean } +export type DocumentSourceDetailResponse = { + upload_file?: DocumentUploadFileDetailResponse | null + [key: string]: unknown +} + +export type JsonValue = unknown + +export type DocumentProcessRuleResponse = { + mode: string + rules?: DocumentProcessRulesResponse | null +} + export type DocumentMetadataResponse = { id: string name: string - type: string - value?: string | number | number | boolean | null + type: 'number' | 'string' | 'time' + value: string | number | number | null } -export type JsonValue = unknown - export type SegmentResponse = { answer: string | null attachments: Array @@ -885,7 +935,7 @@ export type SegmentResponse = { keywords: Array | null position: number sign_content: string - status: string + status: 'completed' | 'error' | 'indexing' | 'paused' | 're_segment' | 'waiting' stopped_at: number | null summary: string | null tokens: number @@ -900,7 +950,7 @@ export type ChildChunkResponse = { id: string position: number segment_id: string - type: string + type: 'automatic' | 'customized' updated_at: number word_count: number } @@ -958,16 +1008,16 @@ export type DatasetMetadataListItemResponse = { count?: number id: string name: string - type: string + type: 'number' | 'string' | 'time' } export type DatasetQueryDetailResponse = { created_at: number created_by: string - created_by_role: string + created_by_role: CreatorUserRole id: string queries: Array - source: string + source: DatasetQuerySource source_app_id: string | null } @@ -975,10 +1025,10 @@ export type RelatedAppResponse = { description: string icon: string | null icon_background: string | null - icon_type: string | null + icon_type: IconType | null icon_url?: string | null id: string - mode: string + mode: AppMode name: string } @@ -987,6 +1037,12 @@ export type DatasetRerankingModelResponse = { reranking_provider_name?: string | null } +export type RetrievalMethod = + | 'full_text_search' + | 'hybrid_search' + | 'keyword_search' + | 'semantic_search' + export type DatasetWeightedScoreResponse = { keyword_setting?: DatasetKeywordSettingResponse vector_setting?: DatasetVectorSettingResponse @@ -995,7 +1051,7 @@ export type DatasetWeightedScoreResponse = { export type InfoList = { data_source_type: 'notion_import' | 'upload_file' | 'website_crawl' - file_info_list?: FileInfo | null + file_info_list?: KnowledgeFileInfo | null notion_info_list?: Array | null website_info_list?: WebsiteInfo | null } @@ -1010,12 +1066,6 @@ export type RerankingModel = { reranking_provider_name?: string | null } -export type RetrievalMethod = - | 'full_text_search' - | 'hybrid_search' - | 'keyword_search' - | 'semantic_search' - export type WeightModel = { keyword_setting?: WeightKeywordSetting | null vector_setting?: WeightVectorSetting | null @@ -1044,6 +1094,23 @@ export type MetadataDetail = { value?: string | number | number | null } +export type DocumentUploadFileDetailResponse = { + created_at: number + created_by: string + extension: string + id: string + mime_type?: string | null + name: string + size: number +} + +export type DocumentProcessRulesResponse = { + parent_mode?: 'full-doc' | 'paragraph' | null + pre_processing_rules?: Array | null + segmentation?: DocumentSegmentationResponse | null + subchunk_segmentation?: DocumentSegmentationResponse | null +} + export type SegmentAttachmentResponse = { extension: string id: string @@ -1095,12 +1162,28 @@ export type HitTestingSegment = { word_count: number } +export type CreatorUserRole = 'account' | 'end_user' + export type DatasetQueryContentResponse = { content: string - content_type: string + content_type: QueryType file_info?: DatasetQueryFileInfoResponse | null } +export type DatasetQuerySource = 'app' | 'hit_testing' + +export type IconType = 'emoji' | 'image' | 'link' + +export type AppMode = + | 'advanced-chat' + | 'agent' + | 'agent-chat' + | 'channel' + | 'chat' + | 'completion' + | 'rag-pipeline' + | 'workflow' + export type DatasetKeywordSettingResponse = { keyword_weight?: number | null } @@ -1111,7 +1194,7 @@ export type DatasetVectorSettingResponse = { vector_weight?: number | null } -export type FileInfo = { +export type KnowledgeFileInfo = { file_ids: Array } @@ -1166,6 +1249,17 @@ export type NotionPageType = 'database' | 'page' export type PreProcessingRuleKey = 'remove_extra_spaces' | 'remove_stopwords' | 'remove_urls_emails' +export type DocumentPreProcessingRuleResponse = { + enabled: boolean + id: string +} + +export type DocumentSegmentationResponse = { + chunk_overlap?: number + max_tokens: number + separator?: string +} + export type HitTestingDocument = { data_source_type: string doc_metadata: unknown | null @@ -1174,6 +1268,8 @@ export type HitTestingDocument = { name: string } +export type QueryType = 'image_query' | 'text_query' + export type DatasetQueryFileInfoResponse = { extension: string id: string @@ -1855,7 +1951,9 @@ export type DeleteDatasetsByDatasetIdDocumentsData = { path: { dataset_id: string } - query?: never + query?: { + document_id?: Array + } url: '/datasets/{dataset_id}/documents' } @@ -1881,8 +1979,8 @@ export type GetDatasetsByDatasetIdDocumentsData = { query?: { fetch?: string keyword?: string - limit?: string - page?: string + limit?: number + page?: number sort?: string status?: string } @@ -2012,7 +2110,9 @@ export type PatchDatasetsByDatasetIdDocumentsStatusByActionBatchData = { action: string dataset_id: string } - query?: never + query?: { + document_id?: Array + } url: '/datasets/{dataset_id}/documents/status/{action}/batch' } @@ -2991,7 +3091,10 @@ export type GetDatasetsByDatasetIdQueriesData = { path: { dataset_id: string } - query?: never + query?: { + limit?: number + page?: number + } url: '/datasets/{dataset_id}/queries' } diff --git a/packages/contracts/generated/api/console/datasets/zod.gen.ts b/packages/contracts/generated/api/console/datasets/zod.gen.ts index 1d9b1507296bbd..898d6dd8343e2f 100644 --- a/packages/contracts/generated/api/console/datasets/zod.gen.ts +++ b/packages/contracts/generated/api/console/datasets/zod.gen.ts @@ -278,14 +278,26 @@ export const zDocumentStatusResponse = z.object({ completed_at: z.int().nullable(), completed_segments: z.int().nullish(), error: z.string().nullable(), + error_code: z.literal('vector_space_estimate_exceeded').nullish(), + estimated_vector_space_mb: z.int().nullish(), id: z.string(), - indexing_status: z.string(), + indexing_status: z.enum([ + 'cleaning', + 'completed', + 'error', + 'indexing', + 'parsing', + 'paused', + 'splitting', + 'waiting', + ]), parsing_completed_at: z.int().nullable(), paused_at: z.int().nullable(), processing_started_at: z.int().nullable(), splitting_completed_at: z.int().nullable(), stopped_at: z.int().nullable(), total_segments: z.int().nullish(), + vector_space_limit_mb: z.int().nullish(), }) /** @@ -461,7 +473,7 @@ export const zMetadataArgs = z.object({ export const zDatasetMetadataResponse = z.object({ id: z.string(), name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -538,7 +550,7 @@ export const zDatasetUpdatePayload = z.object({ export const zDatasetDocMetadataResponse = z.object({ id: z.string(), name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -570,6 +582,11 @@ export const zDatasetIconInfoResponse = z.object({ icon_url: z.string().nullish(), }) +/** + * IndexTechniqueType + */ +export const zIndexTechniqueType = z.enum(['economy', 'high_quality']) + /** * DatasetSummaryIndexSettingResponse */ @@ -597,6 +614,14 @@ export const zExternalKnowledgeApiBindingResponse = z.object({ name: z.string(), }) +/** + * ExternalKnowledgeApiSettingsResponse + */ +export const zExternalKnowledgeApiSettingsResponse = z.object({ + api_key: z.string().nullish(), + endpoint: z.string().nullish(), +}) + /** * ExternalKnowledgeApiResponse */ @@ -607,7 +632,7 @@ export const zExternalKnowledgeApiResponse = z.object({ description: z.string(), id: z.string(), name: z.string(), - settings: z.record(z.string(), z.unknown()).nullable(), + settings: zExternalKnowledgeApiSettingsResponse.nullable(), tenant_id: z.string(), }) @@ -679,7 +704,7 @@ export const zDatasetResponse = z.object({ */ export const zDatasetMetadataBuiltInFieldResponse = z.object({ name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -690,141 +715,18 @@ export const zDatasetMetadataBuiltInFieldsResponse = z.object({ }) /** - * ProcessRuleMode - * - * Dataset Process Rule Mode - */ -export const zProcessRuleMode = z.enum(['automatic', 'custom', 'hierarchical']) - -/** - * DocumentMetadataResponse - */ -export const zDocumentMetadataResponse = z.object({ - id: z.string(), - name: z.string(), - type: z.string(), - value: z.union([z.string(), z.int(), z.number(), z.boolean()]).nullish(), -}) - -/** - * DocumentDetailResponse - */ -export const zDocumentDetailResponse = z.object({ - archived: z.boolean().nullish(), - average_segment_length: z.number().nullish(), - completed_at: z.int().nullish(), - created_at: z.int().nullish(), - created_by: z.string().nullish(), - created_from: z.string().nullish(), - data_source_detail_dict: z.unknown().optional(), - data_source_info: z.unknown().optional(), - data_source_type: z.string().nullish(), - dataset_process_rule: z.unknown().optional(), - dataset_process_rule_id: z.string().nullish(), - disabled_at: z.int().nullish(), - disabled_by: z.string().nullish(), - display_status: z.string().nullish(), - doc_form: z.string().nullish(), - doc_language: z.string().nullish(), - doc_metadata: z.array(zDocumentMetadataResponse).nullish(), - doc_type: z.string().nullish(), - document_process_rule: z.unknown().optional(), - enabled: z.boolean().nullish(), - error: z.string().nullish(), - hit_count: z.int().nullish(), - id: z.string(), - indexing_latency: z.number().nullish(), - indexing_status: z.string().nullish(), - name: z.string().nullish(), - need_summary: z.boolean().nullish(), - position: z.int().nullish(), - segment_count: z.int().nullish(), - tokens: z.int().nullish(), - updated_at: z.int().nullish(), -}) - -/** - * DocumentResponse - */ -export const zDocumentResponse = z.object({ - archived: z.boolean().nullish(), - created_at: z.int().nullish(), - created_by: z.string().nullish(), - created_from: z.string().nullish(), - data_source_detail_dict: z.record(z.string(), z.unknown()), - data_source_info: z.record(z.string(), z.unknown()).nullish(), - data_source_type: z.string().nullish(), - dataset_process_rule_id: z.string().nullish(), - disabled_at: z.int().nullish(), - disabled_by: z.string().nullish(), - display_status: z.string().nullish(), - doc_form: z.string().nullish(), - doc_metadata: z.array(zDocumentMetadataResponse).optional(), - enabled: z.boolean().nullish(), - error: z.string().nullish(), - hit_count: z.int().nullish(), - id: z.string(), - indexing_status: z.string().nullish(), - name: z.string(), - need_summary: z.boolean().nullish(), - position: z.int().nullish(), - summary_index_status: z.string().nullish(), - tokens: z.int().nullish(), - word_count: z.int().nullish(), -}) - -/** - * DatasetAndDocumentResponse - */ -export const zDatasetAndDocumentResponse = z.object({ - batch: z.string(), - dataset: zDatasetResponse, - documents: z.array(zDocumentResponse), -}) - -/** - * DocumentWithSegmentsResponse + * ProcessRuleLimitsResponse */ -export const zDocumentWithSegmentsResponse = z.object({ - archived: z.boolean().nullish(), - completed_segments: z.int().nullish(), - created_at: z.int().nullish(), - created_by: z.string().nullish(), - created_from: z.string().nullish(), - data_source_detail_dict: z.record(z.string(), z.unknown()), - data_source_info: z.record(z.string(), z.unknown()).nullish(), - data_source_type: z.string().nullish(), - dataset_process_rule_id: z.string().nullish(), - disabled_at: z.int().nullish(), - disabled_by: z.string().nullish(), - display_status: z.string().nullish(), - doc_form: z.string().nullish(), - doc_metadata: z.array(zDocumentMetadataResponse).optional(), - enabled: z.boolean().nullish(), - error: z.string().nullish(), - hit_count: z.int().nullish(), - id: z.string(), - indexing_status: z.string().nullish(), - name: z.string(), - need_summary: z.boolean().nullish(), - position: z.int().nullish(), - process_rule_dict: z.unknown().optional(), - summary_index_status: z.string().nullish(), - tokens: z.int().nullish(), - total_segments: z.int().nullish(), - word_count: z.int().nullish(), +export const zProcessRuleLimitsResponse = z.object({ + indexing_max_segmentation_tokens_length: z.int(), }) /** - * DocumentWithSegmentsListResponse + * ProcessRuleMode + * + * Dataset Process Rule Mode */ -export const zDocumentWithSegmentsListResponse = z.object({ - data: z.array(zDocumentWithSegmentsResponse), - has_more: z.boolean(), - limit: z.int(), - page: z.int(), - total: z.int(), -}) +export const zProcessRuleMode = z.enum(['automatic', 'custom', 'hierarchical']) export const zJsonValue = z.unknown() @@ -838,6 +740,16 @@ export const zDocumentPipelineExecutionLogResponse = z.object({ input_data: zJsonValue.nullish(), }) +/** + * DocumentMetadataResponse + */ +export const zDocumentMetadataResponse = z.object({ + id: z.string(), + name: z.string(), + type: z.enum(['number', 'string', 'time']), + value: z.union([z.string(), z.int(), z.number()]).nullable(), +}) + /** * ChildChunkResponse */ @@ -847,7 +759,7 @@ export const zChildChunkResponse = z.object({ id: z.string(), position: z.int(), segment_id: z.string(), - type: z.string(), + type: z.enum(['automatic', 'customized']), updated_at: z.int(), word_count: z.int(), }) @@ -964,7 +876,7 @@ export const zDatasetMetadataListItemResponse = z.object({ count: z.int().optional().default(0), id: z.string(), name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -975,28 +887,6 @@ export const zDatasetMetadataListResponse = z.object({ doc_metadata: z.array(zDatasetMetadataListItemResponse), }) -/** - * RelatedAppResponse - */ -export const zRelatedAppResponse = z.object({ - description: z.string(), - icon: z.string().nullable(), - icon_background: z.string().nullable(), - icon_type: z.string().nullable(), - icon_url: z.string().nullish(), - id: z.string(), - mode: z.string(), - name: z.string(), -}) - -/** - * RelatedAppListResponse - */ -export const zRelatedAppListResponse = z.object({ - data: z.array(zRelatedAppResponse), - total: z.int(), -}) - /** * DatasetRerankingModelResponse */ @@ -1005,14 +895,6 @@ export const zDatasetRerankingModelResponse = z.object({ reranking_provider_name: z.string().nullish(), }) -/** - * RerankingModel - */ -export const zRerankingModel = z.object({ - reranking_model_name: z.string().nullish(), - reranking_provider_name: z.string().nullish(), -}) - /** * RetrievalMethod */ @@ -1023,6 +905,14 @@ export const zRetrievalMethod = z.enum([ 'semantic_search', ]) +/** + * RerankingModel + */ +export const zRerankingModel = z.object({ + reranking_model_name: z.string().nullish(), + reranking_provider_name: z.string().nullish(), +}) + /** * Segmentation */ @@ -1059,6 +949,67 @@ export const zMetadataOperationData = z.object({ operation_data: z.array(zDocumentMetadataOperation), }) +/** + * DocumentUploadFileDetailResponse + */ +export const zDocumentUploadFileDetailResponse = z.object({ + created_at: z.number(), + created_by: z.string(), + extension: z.string(), + id: z.string(), + mime_type: z.string().nullish(), + name: z.string(), + size: z.int(), +}) + +/** + * DocumentSourceDetailResponse + * + * The uploaded file for file documents; Notion and website documents repeat their source info. + */ +export const zDocumentSourceDetailResponse = z.object({ + upload_file: zDocumentUploadFileDetailResponse.nullish(), +}) + +/** + * DocumentResponse + */ +export const zDocumentResponse = z.object({ + archived: z.boolean().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + created_from: z.string().nullish(), + data_source_detail_dict: zDocumentSourceDetailResponse, + data_source_info: z.record(z.string(), zJsonValue).nullish(), + data_source_type: z.string().nullish(), + dataset_process_rule_id: z.string().nullish(), + disabled_at: z.int().nullish(), + disabled_by: z.string().nullish(), + display_status: z.string().nullish(), + doc_form: z.string().nullish(), + doc_metadata: z.array(zDocumentMetadataResponse).optional(), + enabled: z.boolean().nullish(), + error: z.string().nullish(), + hit_count: z.int().nullish(), + id: z.string(), + indexing_status: z.string().nullish(), + name: z.string(), + need_summary: z.boolean().nullish(), + position: z.int().nullish(), + summary_index_status: z.string().nullish(), + tokens: z.int().nullish(), + word_count: z.int().nullish(), +}) + +/** + * DatasetAndDocumentResponse + */ +export const zDatasetAndDocumentResponse = z.object({ + batch: z.string(), + dataset: zDatasetResponse, + documents: z.array(zDocumentResponse), +}) + /** * SegmentAttachmentResponse */ @@ -1095,7 +1046,7 @@ export const zSegmentResponse = z.object({ keywords: z.array(z.string()).nullable(), position: z.int(), sign_content: z.string(), - status: z.string(), + status: z.enum(['completed', 'error', 'indexing', 'paused', 're_segment', 'waiting']), stopped_at: z.int().nullable(), summary: z.string().nullable(), tokens: z.int(), @@ -1145,6 +1096,59 @@ export const zHitTestingFile = z.object({ source_url: z.string(), }) +/** + * CreatorUserRole + */ +export const zCreatorUserRole = z.enum(['account', 'end_user']) + +/** + * DatasetQuerySource + * + * Dataset query source + */ +export const zDatasetQuerySource = z.enum(['app', 'hit_testing']) + +/** + * IconType + */ +export const zIconType = z.enum(['emoji', 'image', 'link']) + +/** + * AppMode + */ +export const zAppMode = z.enum([ + 'advanced-chat', + 'agent', + 'agent-chat', + 'channel', + 'chat', + 'completion', + 'rag-pipeline', + 'workflow', +]) + +/** + * RelatedAppResponse + */ +export const zRelatedAppResponse = z.object({ + description: z.string(), + icon: z.string().nullable(), + icon_background: z.string().nullable(), + icon_type: zIconType.nullable(), + icon_url: z.string().nullish(), + id: z.string(), + mode: zAppMode, + name: z.string(), +}) + +/** + * RelatedAppListResponse + */ +export const zRelatedAppListResponse = z.object({ + data: z.array(zRelatedAppResponse), + total: z.int(), +}) + /** * DatasetKeywordSettingResponse */ @@ -1179,7 +1183,7 @@ export const zDatasetRetrievalModelResponse = z.object({ reranking_model: zDatasetRerankingModelResponse.optional(), score_threshold: z.number().nullish(), score_threshold_enabled: z.boolean(), - search_method: z.string(), + search_method: zRetrievalMethod, top_k: z.int(), weights: zDatasetWeightedScoreResponse.nullish(), }) @@ -1194,7 +1198,16 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ chunk_structure: z.string().nullable(), created_at: z.int(), created_by: z.string(), - data_source_type: z.string().nullable(), + data_source_type: z + .enum([ + 'local_file', + 'notion_import', + 'online_document', + 'online_drive', + 'upload_file', + 'website_crawl', + ]) + .nullable(), description: z.string().nullable(), doc_form: z.string().nullable(), doc_metadata: z.array(zDatasetDocMetadataResponse), @@ -1207,18 +1220,19 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), - indexing_technique: z.string().nullable(), + indexing_technique: zIndexTechniqueType.nullable(), is_multimodal: z.boolean(), is_published: z.boolean(), + keyword_number: z.int().nullish(), maintainer: z.string().nullish(), name: z.string(), partial_member_list: z.array(z.string()).nullish(), - permission: z.string(), + permission: z.enum(['all_team_members', 'only_me', 'partial_members']), permission_keys: z.array(z.string()).optional(), pipeline_id: z.string().nullable(), provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, - runtime_mode: z.string().nullable(), + runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), @@ -1238,7 +1252,16 @@ export const zDatasetDetailResponse = z.object({ chunk_structure: z.string().nullable(), created_at: z.int(), created_by: z.string(), - data_source_type: z.string().nullable(), + data_source_type: z + .enum([ + 'local_file', + 'notion_import', + 'online_document', + 'online_drive', + 'upload_file', + 'website_crawl', + ]) + .nullable(), description: z.string().nullable(), doc_form: z.string().nullable(), doc_metadata: z.array(zDatasetDocMetadataResponse), @@ -1251,17 +1274,18 @@ export const zDatasetDetailResponse = z.object({ external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), - indexing_technique: z.string().nullable(), + indexing_technique: zIndexTechniqueType.nullable(), is_multimodal: z.boolean(), is_published: z.boolean(), + keyword_number: z.int().nullish(), maintainer: z.string().nullish(), name: z.string(), - permission: z.string(), + permission: z.enum(['all_team_members', 'only_me', 'partial_members']), permission_keys: z.array(z.string()).optional(), pipeline_id: z.string().nullable(), provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, - runtime_mode: z.string().nullable(), + runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), @@ -1281,7 +1305,16 @@ export const zDatasetListItemResponse = z.object({ chunk_structure: z.string().nullable(), created_at: z.int(), created_by: z.string(), - data_source_type: z.string().nullable(), + data_source_type: z + .enum([ + 'local_file', + 'notion_import', + 'online_document', + 'online_drive', + 'upload_file', + 'website_crawl', + ]) + .nullable(), description: z.string().nullable(), doc_form: z.string().nullable(), doc_metadata: z.array(zDatasetDocMetadataResponse), @@ -1294,18 +1327,19 @@ export const zDatasetListItemResponse = z.object({ external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), - indexing_technique: z.string().nullable(), + indexing_technique: zIndexTechniqueType.nullable(), is_multimodal: z.boolean(), is_published: z.boolean(), + keyword_number: z.int().nullish(), maintainer: z.string().nullish(), name: z.string(), partial_member_list: z.array(z.string()), - permission: z.string(), + permission: z.enum(['all_team_members', 'only_me', 'partial_members']), permission_keys: z.array(z.string()).optional(), pipeline_id: z.string().nullable(), provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, - runtime_mode: z.string().nullable(), + runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), @@ -1329,7 +1363,7 @@ export const zDatasetListResponse = z.object({ /** * FileInfo */ -export const zFileInfo = z.object({ +export const zKnowledgeFileInfo = z.object({ file_ids: z.array(z.string()), }) @@ -1499,7 +1533,7 @@ export const zRule = z.object({ * ProcessRuleResponse */ export const zProcessRuleResponse = z.object({ - limits: z.record(z.string(), z.unknown()), + limits: zProcessRuleLimitsResponse, mode: zProcessRuleMode, rules: zRule.nullish(), }) @@ -1512,6 +1546,122 @@ export const zProcessRule = z.object({ rules: zRule.nullish(), }) +/** + * DocumentPreProcessingRuleResponse + */ +export const zDocumentPreProcessingRuleResponse = z.object({ + enabled: z.boolean(), + id: z.string(), +}) + +/** + * DocumentSegmentationResponse + */ +export const zDocumentSegmentationResponse = z.object({ + chunk_overlap: z.int().optional().default(0), + max_tokens: z.int(), + separator: z.string().optional().default('\n'), +}) + +/** + * DocumentProcessRulesResponse + */ +export const zDocumentProcessRulesResponse = z.object({ + parent_mode: z.enum(['full-doc', 'paragraph']).nullish(), + pre_processing_rules: z.array(zDocumentPreProcessingRuleResponse).nullish(), + segmentation: zDocumentSegmentationResponse.nullish(), + subchunk_segmentation: zDocumentSegmentationResponse.nullish(), +}) + +/** + * DocumentProcessRuleResponse + */ +export const zDocumentProcessRuleResponse = z.object({ + mode: z.string(), + rules: zDocumentProcessRulesResponse.nullish(), +}) + +/** + * DocumentDetailResponse + */ +export const zDocumentDetailResponse = z.object({ + archived: z.boolean().nullish(), + average_segment_length: z.number().nullish(), + completed_at: z.int().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + created_from: z.string().nullish(), + data_source_detail_dict: zDocumentSourceDetailResponse.nullish(), + data_source_info: z.record(z.string(), zJsonValue).nullish(), + data_source_type: z.string().nullish(), + dataset_process_rule: zDocumentProcessRuleResponse.nullish(), + dataset_process_rule_id: z.string().nullish(), + disabled_at: z.int().nullish(), + disabled_by: z.string().nullish(), + display_status: z.string().nullish(), + doc_form: z.string().nullish(), + doc_language: z.string().nullish(), + doc_metadata: z.array(zDocumentMetadataResponse).nullish(), + doc_type: z.string().nullish(), + document_process_rule: zDocumentProcessRuleResponse.nullish(), + enabled: z.boolean().nullish(), + error: z.string().nullish(), + hit_count: z.int().nullish(), + id: z.string(), + indexing_latency: z.number().nullish(), + indexing_status: z.string().nullish(), + name: z.string().nullish(), + need_summary: z.boolean().nullish(), + position: z.int().nullish(), + segment_count: z.int().nullish(), + tokens: z.int().nullish(), + updated_at: z.int().nullish(), +}) + +/** + * DocumentWithSegmentsResponse + */ +export const zDocumentWithSegmentsResponse = z.object({ + archived: z.boolean().nullish(), + completed_segments: z.int().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + created_from: z.string().nullish(), + data_source_detail_dict: zDocumentSourceDetailResponse, + data_source_info: z.record(z.string(), zJsonValue).nullish(), + data_source_type: z.string().nullish(), + dataset_process_rule_id: z.string().nullish(), + disabled_at: z.int().nullish(), + disabled_by: z.string().nullish(), + display_status: z.string().nullish(), + doc_form: z.string().nullish(), + doc_metadata: z.array(zDocumentMetadataResponse).optional(), + enabled: z.boolean().nullish(), + error: z.string().nullish(), + hit_count: z.int().nullish(), + id: z.string(), + indexing_status: z.string().nullish(), + name: z.string(), + need_summary: z.boolean().nullish(), + position: z.int().nullish(), + process_rule_dict: zDocumentProcessRuleResponse.nullish(), + summary_index_status: z.string().nullish(), + tokens: z.int().nullish(), + total_segments: z.int().nullish(), + word_count: z.int().nullish(), +}) + +/** + * DocumentWithSegmentsListResponse + */ +export const zDocumentWithSegmentsListResponse = z.object({ + data: z.array(zDocumentWithSegmentsResponse), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + /** * HitTestingDocument */ @@ -1572,6 +1722,11 @@ export const zHitTestingResponse = z.object({ records: z.array(zHitTestingRecord), }) +/** + * QueryType + */ +export const zQueryType = z.enum(['image_query', 'text_query']) + /** * DatasetQueryFileInfoResponse */ @@ -1589,7 +1744,7 @@ export const zDatasetQueryFileInfoResponse = z.object({ */ export const zDatasetQueryContentResponse = z.object({ content: z.string(), - content_type: z.string(), + content_type: zQueryType, file_info: zDatasetQueryFileInfoResponse.nullish(), }) @@ -1599,10 +1754,10 @@ export const zDatasetQueryContentResponse = z.object({ export const zDatasetQueryDetailResponse = z.object({ created_at: z.int(), created_by: z.string(), - created_by_role: z.string(), + created_by_role: zCreatorUserRole, id: z.string(), queries: z.array(zDatasetQueryContentResponse), - source: z.string(), + source: zDatasetQuerySource, source_app_id: z.string().nullable(), }) @@ -1650,7 +1805,7 @@ export const zNotionInfo = z.object({ */ export const zInfoList = z.object({ data_source_type: z.enum(['notion_import', 'upload_file', 'website_crawl']), - file_info_list: zFileInfo.nullish(), + file_info_list: zKnowledgeFileInfo.nullish(), notion_info_list: z.array(zNotionInfo).nullish(), website_info_list: zWebsiteInfo.nullish(), }) @@ -1935,6 +2090,10 @@ export const zDeleteDatasetsByDatasetIdDocumentsPath = z.object({ dataset_id: z.uuid(), }) +export const zDeleteDatasetsByDatasetIdDocumentsQuery = z.object({ + document_id: z.array(z.string()).optional(), +}) + /** * Documents deleted successfully */ @@ -1945,11 +2104,11 @@ export const zGetDatasetsByDatasetIdDocumentsPath = z.object({ }) export const zGetDatasetsByDatasetIdDocumentsQuery = z.object({ - fetch: z.string().optional(), + fetch: z.string().optional().default('false'), keyword: z.string().optional(), - limit: z.string().optional(), - page: z.string().optional(), - sort: z.string().optional(), + limit: z.int().optional().default(20), + page: z.int().optional().default(1), + sort: z.string().optional().default('-created_at'), status: z.string().optional(), }) @@ -2010,6 +2169,10 @@ export const zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchPath = z.objec dataset_id: z.uuid(), }) +export const zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchQuery = z.object({ + document_id: z.array(z.string()).optional(), +}) + /** * Success */ @@ -2470,6 +2633,11 @@ export const zGetDatasetsByDatasetIdQueriesPath = z.object({ dataset_id: z.uuid(), }) +export const zGetDatasetsByDatasetIdQueriesQuery = z.object({ + limit: z.int().optional().default(20), + page: z.int().optional().default(1), +}) + /** * Query history retrieved successfully */ diff --git a/packages/contracts/generated/api/console/form/types.gen.ts b/packages/contracts/generated/api/console/form/types.gen.ts index f4ab4ef7046b42..761d5ee325ea68 100644 --- a/packages/contracts/generated/api/console/form/types.gen.ts +++ b/packages/contracts/generated/api/console/form/types.gen.ts @@ -25,11 +25,8 @@ export type ErrorResponse = { export type HumanInputFormSubmitPayload = { action: string - form_inputs: { - [key: string]: unknown - } inputs: { - [key: string]: unknown + [key: string]: JsonValue } } @@ -47,6 +44,8 @@ export type HumanInputFormExpiredError = ErrorResponse & { status: 412 } +export type JsonValue = unknown + export type GetFormHumanInputByFormTokenData = { body?: never path: { diff --git a/packages/contracts/generated/api/console/form/zod.gen.ts b/packages/contracts/generated/api/console/form/zod.gen.ts index fde67017e0f2a7..18724fa0f55163 100644 --- a/packages/contracts/generated/api/console/form/zod.gen.ts +++ b/packages/contracts/generated/api/console/form/zod.gen.ts @@ -35,15 +35,6 @@ export const zHumanInputFormSubmittedError = zErrorResponse.and( }), ) -/** - * HumanInputFormSubmitPayload - */ -export const zHumanInputFormSubmitPayload = z.object({ - action: z.string(), - form_inputs: z.record(z.string(), z.unknown()), - inputs: z.record(z.string(), z.unknown()), -}) - /** * ConsoleHumanInputFormSubmitResponse */ @@ -69,6 +60,16 @@ export const zHumanInputFormExpiredError = zErrorResponse.and( }), ) +export const zJsonValue = z.unknown() + +/** + * HumanInputFormSubmitPayload + */ +export const zHumanInputFormSubmitPayload = z.object({ + action: z.string(), + inputs: z.record(z.string(), zJsonValue), +}) + export const zGetFormHumanInputByFormTokenPath = z.object({ form_token: z.string(), }) diff --git a/packages/contracts/generated/api/console/installed-apps/types.gen.ts b/packages/contracts/generated/api/console/installed-apps/types.gen.ts index 22eb23077c3582..18626ea053945e 100644 --- a/packages/contracts/generated/api/console/installed-apps/types.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/types.gen.ts @@ -122,19 +122,16 @@ export type UnsupportedAudioTypeError = ErrorResponse & { status: 415 } -export type ChatMessagePayload = { +export type ChatMessageExplorePayload = { conversation_id?: string | null - draft_type?: 'debug_build' | 'draft' - files?: Array | null - inputs: { + files?: Array<{ [key: string]: unknown - } - model_config?: { + }> | null + inputs: { [key: string]: unknown } parent_message_id?: string | null query: string - response_mode?: 'blocking' | 'streaming' retriever_from?: string } @@ -203,7 +200,7 @@ export type SimpleConversation = { created_at?: number | null id: string inputs: { - [key: string]: JsonValue + [key: string]: AnyJsonValue } introduction?: string | null name: string @@ -238,7 +235,6 @@ export type MessageCursorNotFoundError = ErrorResponse & { export type MessageFeedbackPayload = { content?: string | null - message_id: string rating?: 'dislike' | 'like' | null } @@ -677,16 +673,7 @@ export type InstalledAppInfoResponse = { use_icon_as_answer_icon: boolean } -export type JsonValue = - | string - | number - | number - | boolean - | { - [key: string]: unknown - } - | Array - | null +export type AnyJsonValue = unknown export type ExploreMessageListItem = { agent_thoughts: Array @@ -758,7 +745,7 @@ export type AgentThought = { thought?: string | null tool?: string | null tool_input?: string | null - tool_labels: JsonValue + tool_labels: AnyJsonValue } export type HumanInputContent = { @@ -838,7 +825,7 @@ export type HumanInputFormSubmissionData = { node_title: string rendered_content: string submitted_data?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } | null } @@ -864,7 +851,7 @@ export type FormInputConfig = type: 'file-list' } & FileListInputConfig) -export type JsonValue2 = unknown +export type JsonValue = unknown export type ButtonStyle = 'accent' | 'default' | 'ghost' | 'primary' @@ -1089,7 +1076,7 @@ export type PostInstalledAppsByInstalledAppIdAudioToTextResponse = PostInstalledAppsByInstalledAppIdAudioToTextResponses[keyof PostInstalledAppsByInstalledAppIdAudioToTextResponses] export type PostInstalledAppsByInstalledAppIdChatMessagesData = { - body: ChatMessagePayload + body: ChatMessageExplorePayload path: { installed_app_id: string } diff --git a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts index e129bf478b0a42..cb87804d153735 100644 --- a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts @@ -233,18 +233,15 @@ export const zUnsupportedAudioTypeError = zErrorResponse.and( ) /** - * ChatMessagePayload + * ChatMessageExplorePayload */ -export const zChatMessagePayload = z.object({ +export const zChatMessageExplorePayload = z.object({ conversation_id: z.string().nullish(), - draft_type: z.enum(['debug_build', 'draft']).optional().default('draft'), - files: z.array(z.unknown()).nullish(), + files: z.array(z.record(z.string(), z.unknown())).nullish(), inputs: z.record(z.string(), z.unknown()), - model_config: z.record(z.string(), z.unknown()).optional(), parent_message_id: z.string().nullish(), query: z.string(), - response_mode: z.enum(['blocking', 'streaming']).optional().default('blocking'), - retriever_from: z.string().optional().default('dev'), + retriever_from: z.string().optional().default('explore_app'), }) /** @@ -390,7 +387,6 @@ export const zMessageCursorNotFoundError = zErrorResponse.and( */ export const zMessageFeedbackPayload = z.object({ content: z.string().nullish(), - message_id: z.string(), rating: z.enum(['dislike', 'like']).nullish(), }) @@ -551,16 +547,7 @@ export const zNotWorkflowAppError = zErrorResponse.and( }), ) -export const zJsonValue = z - .union([ - z.string(), - z.int(), - z.number(), - z.boolean(), - z.record(z.string(), z.unknown()), - z.array(z.unknown()), - ]) - .nullable() +export const zAnyJsonValue = z.unknown() /** * SimpleConversation @@ -568,7 +555,7 @@ export const zJsonValue = z export const zSimpleConversation = z.object({ created_at: z.int().nullish(), id: z.uuid(), - inputs: z.record(z.string(), zJsonValue), + inputs: z.record(z.string(), zAnyJsonValue), introduction: z.string().nullish(), name: z.string(), status: z.string(), @@ -1007,7 +994,7 @@ export const zAgentThought = z.object({ thought: z.string().nullish(), tool: z.string().nullish(), tool_input: z.string().nullish(), - tool_labels: zJsonValue, + tool_labels: zAnyJsonValue, }) /** @@ -1089,7 +1076,7 @@ export const zRetrieverResource = z.object({ */ export const zExecutionContentType = z.enum(['human_input']) -export const zJsonValue2 = z.unknown() +export const zJsonValue = z.unknown() /** * HumanInputFormSubmissionData @@ -1100,7 +1087,7 @@ export const zHumanInputFormSubmissionData = z.object({ node_id: z.string(), node_title: z.string(), rendered_content: z.string(), - submitted_data: z.record(z.string(), zJsonValue2).nullish(), + submitted_data: z.record(z.string(), zJsonValue).nullish(), }) /** @@ -1397,7 +1384,7 @@ export const zPostInstalledAppsByInstalledAppIdAudioToTextPath = z.object({ */ export const zPostInstalledAppsByInstalledAppIdAudioToTextResponse = zAudioTranscriptResponse -export const zPostInstalledAppsByInstalledAppIdChatMessagesBody = zChatMessagePayload +export const zPostInstalledAppsByInstalledAppIdChatMessagesBody = zChatMessageExplorePayload export const zPostInstalledAppsByInstalledAppIdChatMessagesPath = z.object({ installed_app_id: z.uuid(), diff --git a/packages/contracts/generated/api/console/rag/types.gen.ts b/packages/contracts/generated/api/console/rag/types.gen.ts index 212b3db7d7d198..d8639a3ef1ece1 100644 --- a/packages/contracts/generated/api/console/rag/types.gen.ts +++ b/packages/contracts/generated/api/console/rag/types.gen.ts @@ -52,7 +52,14 @@ export type DatasetDetailResponse = { chunk_structure: string | null created_at: number created_by: string - data_source_type: string | null + data_source_type: + | 'local_file' + | 'notion_import' + | 'online_document' + | 'online_drive' + | 'upload_file' + | 'website_crawl' + | null description: string | null doc_form: string | null doc_metadata: Array @@ -65,17 +72,18 @@ export type DatasetDetailResponse = { external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string - indexing_technique: string | null + indexing_technique: IndexTechniqueType | null is_multimodal: boolean is_published: boolean + keyword_number?: number | null maintainer?: string | null name: string - permission: string + permission: 'all_team_members' | 'only_me' | 'partial_members' permission_keys?: Array pipeline_id: string | null provider: string retrieval_model_dict: DatasetRetrievalModelResponse - runtime_mode: string | null + runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number @@ -127,7 +135,18 @@ export type PipelineNotFoundError = ErrorResponse & { status: 404 } -export type RagPipelineOpaqueResponse = unknown +export type RagPipelineRecommendedPluginsResponse = { + installed_recommended_plugins: Array + uninstalled_recommended_plugins: Array<{ + [key: string]: unknown + }> +} + +export type RagPipelineTransformResponse = { + dataset_id: string + pipeline_id: string + status: string +} export type WorkflowRunPaginationResponse = { data: Array @@ -243,6 +262,8 @@ export type DatasourceNodeRunPayload = { } } +export type RagPipelineOpaqueResponse = unknown + export type DatasourceVariablesPayload = { datasource_info: { [key: string]: unknown @@ -299,10 +320,10 @@ export type WorkflowDraftVariableListResponse = { } export type RagPipelineStepParametersResponse = { - variables: unknown + variables: Array } -export type DraftWorkflowRunPayload = { +export type RagPipelineDraftWorkflowRunPayload = { datasource_info_list: Array<{ [key: string]: unknown }> @@ -332,7 +353,7 @@ export type WorkflowDraftVariableResponse = { name: string selector: Array type: string - value: JsonValue + value: RecursiveJsonValue value_type: string visible: boolean } @@ -347,7 +368,7 @@ export type RagPipelineWorkflowPublishResponse = { result: string } -export type Parser = { +export type DatasourceContentPreviewPayload = { credential_id?: string | null datasource_type: string inputs: { @@ -369,6 +390,8 @@ export type PublishedWorkflowRunPayload = { start_node_id: string } +export type RagPipelinePublishedRunResponse = PipelineGenerateResponse | WorkflowAppBlockingResponse + export type WorkflowUpdatePayload = { marked_comment?: string | null marked_name?: string | null @@ -379,7 +402,7 @@ export type ImportStatus = 'completed' | 'completed-with-warnings' | 'failed' | export type DatasetDocMetadataResponse = { id: string name: string - type: string + type: 'number' | 'string' | 'time' } export type DatasetExternalKnowledgeInfoResponse = { @@ -402,13 +425,15 @@ export type DatasetIconInfoResponse = { icon_url?: string | null } +export type IndexTechniqueType = 'economy' | 'high_quality' + export type DatasetRetrievalModelResponse = { reranking_enable: boolean reranking_mode?: string | null reranking_model?: DatasetRerankingModelResponse score_threshold?: number | null score_threshold_enabled: boolean - search_method: string + search_method: RetrievalMethod top_k: number weights?: DatasetWeightedScoreResponse | null } @@ -450,7 +475,49 @@ export type RagPipelineDatasourceProviderResponse = { export type PluginDependency = { current_identifier?: string | null type: PluginDependencyType - value: Github | Marketplace | Package + value: PluginDependencyGithub | PluginDependencyMarketplace | PluginDependencyPackage +} + +export type ToolProviderApiEntityResponse = { + allow_delete?: boolean + authentication?: McpAuthentication | null + author: string + configuration?: McpConfiguration | null + description: I18nObject + icon: + | string + | { + [key: string]: string + } + icon_dark?: + | string + | { + [key: string]: string + } + id: string + identity_mode?: string + is_dynamic_registration?: boolean + is_team_authorization?: boolean + label: I18nObject + labels?: Array + masked_headers?: { + [key: string]: string + } | null + name: string + original_headers?: { + [key: string]: string + } | null + plugin_id?: string | null + plugin_unique_identifier?: string | null + server_identifier?: string | null + server_url?: string | null + team_credentials?: { + [key: string]: unknown + } + tools?: Array + type: ToolProviderType + updated_at?: number + workflow_app_id?: string | null } export type WorkflowRunForListResponse = { @@ -545,22 +612,40 @@ export type WorkflowDraftVariableFullContentResponse = { value_type: string } -export type JsonValue = +export type RecursiveJsonValue = | string | number | number | boolean | { - [key: string]: unknown + [key: string]: RecursiveJsonValue } - | Array + | Array | null +export type PipelineGenerateResponse = { + batch: string + dataset: PipelineDataset + documents: Array +} + +export type WorkflowAppBlockingResponse = { + data: WorkflowAppBlockingResponseData + task_id: string + workflow_run_id: string +} + export type DatasetRerankingModelResponse = { reranking_model_name?: string | null reranking_provider_name?: string | null } +export type RetrievalMethod = + | 'full_text_search' + | 'hybrid_search' + | 'keyword_search' + | 'semantic_search' + export type DatasetWeightedScoreResponse = { keyword_setting?: DatasetKeywordSettingResponse vector_setting?: DatasetVectorSettingResponse @@ -577,23 +662,96 @@ export type DatasourceProviderEntityWithPlugin = { export type PluginDependencyType = 'github' | 'marketplace' | 'package' -export type Github = { +export type PluginDependencyGithub = { github_plugin_unique_identifier: string package: string repo: string version: string } -export type Marketplace = { +export type PluginDependencyMarketplace = { marketplace_plugin_unique_identifier: string version?: string | null } -export type Package = { +export type PluginDependencyPackage = { plugin_unique_identifier: string version?: string | null } +export type McpAuthentication = { + client_id: string + client_secret?: string | null +} + +export type McpConfiguration = { + sse_read_timeout?: number + timeout?: number +} + +export type I18nObject = { + en_US: string + ja_JP?: string | null + pt_BR?: string | null + zh_Hans?: string | null +} + +export type ToolApiEntity = { + author: string + description: I18nObject + label: I18nObject + labels?: Array + name: string + output_schema?: { + [key: string]: unknown + } + parameters?: Array | null +} + +export type ToolProviderType = + | 'api' + | 'app' + | 'builtin' + | 'dataset-retrieval' + | 'mcp' + | 'plugin' + | 'workflow' + +export type PipelineDataset = { + chunk_structure: string + description?: string + id: string + name: string +} + +export type PipelineDocument = { + data_source_info?: { + [key: string]: unknown + } | null + data_source_type: string + enabled: boolean + error?: string | null + id: string + indexing_status: string + name: string + position: number +} + +export type WorkflowAppBlockingResponseData = { + created_at: number + elapsed_time: number + error?: string | null + finished_at: number | null + id: string + outputs?: { + [key: string]: unknown + } | null + status: WorkflowExecutionStatus + total_steps: number + total_tokens: number + workflow_id: string +} + export type DatasetKeywordSettingResponse = { keyword_weight?: number | null } @@ -629,9 +787,9 @@ export type DatasourceEntity = { export type DatasourceProviderIdentity = { author: string - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject icon: string - label: CoreToolsEntitiesCommonEntitiesI18nObject + label: I18nObject name: string tags?: Array | null } @@ -647,13 +805,47 @@ export type DatasourceProviderType = | 'online_drive' | 'website_crawl' -export type I18nObject = { - en_US: string - ja_JP?: string | null - pt_BR?: string | null - zh_Hans?: string | null +export type ToolParameter = { + auto_generate?: PluginParameterAutoGenerate | null + default?: + | number + | number + | string + | boolean + | Array + | { + [key: string]: unknown + } + | null + form: ToolParameterForm + human_description?: I18nObject | null + input_schema?: { + [key: string]: unknown + } | null + label: I18nObject + llm_description?: string | null + max?: number | number | null + min?: number | number | null + multiple?: boolean + name: string + options?: Array + placeholder?: I18nObject | null + precision?: number | null + required?: boolean + scope?: string | null + template?: PluginParameterTemplate | null + type: ToolParameterType } +export type WorkflowExecutionStatus = + | 'failed' + | 'partial-succeeded' + | 'paused' + | 'running' + | 'scheduled' + | 'stopped' + | 'succeeded' + export type Option = { label: I18nObject value: string @@ -715,13 +907,6 @@ export type DatasourceParameter = { type: DatasourceParameterType } -export type CoreToolsEntitiesCommonEntitiesI18nObject = { - en_US: string - ja_JP?: string | null - pt_BR?: string | null - zh_Hans?: string | null -} - export type ToolLabelEnum = | 'business' | 'design' @@ -745,6 +930,8 @@ export type PluginParameterAutoGenerate = { type: PluginParameterAutoGenerateType } +export type ToolParameterForm = 'form' | 'llm' | 'schema' + export type PluginParameterOption = { icon?: string | null label: I18nObject @@ -755,6 +942,25 @@ export type PluginParameterTemplate = { enabled?: boolean } +export type ToolParameterType = + | 'any' + | 'app-selector' + | 'array' + | 'boolean' + | 'checkbox' + | 'date' + | 'date-range' + | 'dynamic-select' + | 'file' + | 'files' + | 'model-selector' + | 'number' + | 'object' + | 'secret-input' + | 'select' + | 'string' + | 'system-files' + export type DatasourceParameterType = | 'boolean' | 'file' @@ -1039,7 +1245,7 @@ export type GetRagPipelinesRecommendedPluginsError = GetRagPipelinesRecommendedPluginsErrors[keyof GetRagPipelinesRecommendedPluginsErrors] export type GetRagPipelinesRecommendedPluginsResponses = { - 200: RagPipelineOpaqueResponse + 200: RagPipelineRecommendedPluginsResponse } export type GetRagPipelinesRecommendedPluginsResponse = @@ -1063,7 +1269,7 @@ export type PostRagPipelinesTransformDatasetsByDatasetIdError = PostRagPipelinesTransformDatasetsByDatasetIdErrors[keyof PostRagPipelinesTransformDatasetsByDatasetIdErrors] export type PostRagPipelinesTransformDatasetsByDatasetIdResponses = { - 200: RagPipelineOpaqueResponse + 200: RagPipelineTransformResponse } export type PostRagPipelinesTransformDatasetsByDatasetIdResponse = @@ -1627,7 +1833,7 @@ export type GetRagPipelinesByPipelineIdWorkflowsDraftProcessingParametersRespons GetRagPipelinesByPipelineIdWorkflowsDraftProcessingParametersResponses[keyof GetRagPipelinesByPipelineIdWorkflowsDraftProcessingParametersResponses] export type PostRagPipelinesByPipelineIdWorkflowsDraftRunData = { - body: DraftWorkflowRunPayload + body: RagPipelineDraftWorkflowRunPayload path: { pipeline_id: string } @@ -1876,7 +2082,7 @@ export type PostRagPipelinesByPipelineIdWorkflowsPublishResponse = PostRagPipelinesByPipelineIdWorkflowsPublishResponses[keyof PostRagPipelinesByPipelineIdWorkflowsPublishResponses] export type PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewData = { - body: Parser + body: DatasourceContentPreviewPayload path: { node_id: string pipeline_id: string @@ -1999,7 +2205,7 @@ export type PostRagPipelinesByPipelineIdWorkflowsPublishedRunError = PostRagPipelinesByPipelineIdWorkflowsPublishedRunErrors[keyof PostRagPipelinesByPipelineIdWorkflowsPublishedRunErrors] export type PostRagPipelinesByPipelineIdWorkflowsPublishedRunResponses = { - 200: RagPipelineOpaqueResponse + 200: RagPipelinePublishedRunResponse } export type PostRagPipelinesByPipelineIdWorkflowsPublishedRunResponse = diff --git a/packages/contracts/generated/api/console/rag/zod.gen.ts b/packages/contracts/generated/api/console/rag/zod.gen.ts index 0471efcfa57780..b0ea3725a061dc 100644 --- a/packages/contracts/generated/api/console/rag/zod.gen.ts +++ b/packages/contracts/generated/api/console/rag/zod.gen.ts @@ -95,9 +95,13 @@ export const zPipelineNotFoundError = zErrorResponse.and( ) /** - * RagPipelineOpaqueResponse + * RagPipelineTransformResponse */ -export const zRagPipelineOpaqueResponse = z.unknown() +export const zRagPipelineTransformResponse = z.object({ + dataset_id: z.string(), + pipeline_id: z.string(), + status: z.string(), +}) /** * SimpleResultResponse @@ -170,6 +174,11 @@ export const zDatasourceNodeRunPayload = z.object({ inputs: z.record(z.string(), z.unknown()), }) +/** + * RagPipelineOpaqueResponse + */ +export const zRagPipelineOpaqueResponse = z.unknown() + /** * DatasourceVariablesPayload */ @@ -195,16 +204,9 @@ export const zNodeRunRequiredPayload = z.object({ }) /** - * RagPipelineStepParametersResponse + * RagPipelineDraftWorkflowRunPayload */ -export const zRagPipelineStepParametersResponse = z.object({ - variables: z.unknown(), -}) - -/** - * DraftWorkflowRunPayload - */ -export const zDraftWorkflowRunPayload = z.object({ +export const zRagPipelineDraftWorkflowRunPayload = z.object({ datasource_info_list: z.array(z.record(z.string(), z.unknown())), datasource_type: z.string(), inputs: z.record(z.string(), z.unknown()), @@ -240,9 +242,9 @@ export const zRagPipelineWorkflowPublishResponse = z.object({ }) /** - * Parser + * DatasourceContentPreviewPayload */ -export const zParser = z.object({ +export const zDatasourceContentPreviewPayload = z.object({ credential_id: z.string().nullish(), datasource_type: z.string(), inputs: z.record(z.string(), z.unknown()), @@ -293,7 +295,7 @@ export const zRagPipelineImportResponse = z.object({ export const zDatasetDocMetadataResponse = z.object({ id: z.string(), name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -325,6 +327,11 @@ export const zDatasetIconInfoResponse = z.object({ icon_url: z.string().nullish(), }) +/** + * IndexTechniqueType + */ +export const zIndexTechniqueType = z.enum(['economy', 'high_quality']) + /** * DatasetSummaryIndexSettingResponse */ @@ -542,6 +549,13 @@ export const zWorkflowPaginationResponse = z.object({ page: z.int(), }) +/** + * RagPipelineStepParametersResponse + */ +export const zRagPipelineStepParametersResponse = z.object({ + variables: z.array(zPipelineVariableResponse), +}) + /** * EnvironmentVariableItemResponse */ @@ -598,14 +612,17 @@ export const zWorkflowDraftVariableFullContentResponse = z.object({ value_type: z.string(), }) -export const zJsonValue = z +export const zRecursiveJsonValue = z .union([ z.string(), z.int(), z.number(), z.boolean(), - z.record(z.string(), z.unknown()), - z.array(z.unknown()), + z.record( + z.string(), + z.lazy((): any => zRecursiveJsonValue), + ), + z.array(z.lazy((): any => zRecursiveJsonValue)), ]) .nullable() @@ -621,7 +638,7 @@ export const zWorkflowDraftVariableResponse = z.object({ name: z.string(), selector: z.array(z.string()), type: z.string(), - value: zJsonValue, + value: zRecursiveJsonValue, value_type: z.string(), visible: z.boolean(), }) @@ -641,6 +658,16 @@ export const zDatasetRerankingModelResponse = z.object({ reranking_provider_name: z.string().nullish(), }) +/** + * RetrievalMethod + */ +export const zRetrievalMethod = z.enum([ + 'full_text_search', + 'hybrid_search', + 'keyword_search', + 'semantic_search', +]) + /** * PluginDependencyType */ @@ -649,7 +676,7 @@ export const zPluginDependencyType = z.enum(['github', 'marketplace', 'package'] /** * Github */ -export const zGithub = z.object({ +export const zPluginDependencyGithub = z.object({ github_plugin_unique_identifier: z.string(), package: z.string(), repo: z.string(), @@ -659,7 +686,7 @@ export const zGithub = z.object({ /** * Marketplace */ -export const zMarketplace = z.object({ +export const zPluginDependencyMarketplace = z.object({ marketplace_plugin_unique_identifier: z.string(), version: z.string().nullish(), }) @@ -667,7 +694,7 @@ export const zMarketplace = z.object({ /** * Package */ -export const zPackage = z.object({ +export const zPluginDependencyPackage = z.object({ plugin_unique_identifier: z.string(), version: z.string().nullish(), }) @@ -678,7 +705,7 @@ export const zPackage = z.object({ export const zPluginDependency = z.object({ current_identifier: z.string().nullish(), type: zPluginDependencyType, - value: z.union([zGithub, zMarketplace, zPackage]), + value: z.union([zPluginDependencyGithub, zPluginDependencyMarketplace, zPluginDependencyPackage]), }) /** @@ -688,6 +715,82 @@ export const zRagPipelineImportCheckDependenciesResponse = z.object({ leaked_dependencies: z.array(zPluginDependency).optional(), }) +/** + * MCPAuthentication + */ +export const zMcpAuthentication = z.object({ + client_id: z.string(), + client_secret: z.string().nullish(), +}) + +/** + * MCPConfiguration + */ +export const zMcpConfiguration = z.object({ + sse_read_timeout: z.number().optional().default(300), + timeout: z.number().optional().default(30), +}) + +/** + * I18nObject + * + * Model class for i18n object. + */ +export const zI18nObject = z.object({ + en_US: z.string(), + ja_JP: z.string().nullish(), + pt_BR: z.string().nullish(), + zh_Hans: z.string().nullish(), +}) + +/** + * ToolProviderType + * + * Enum class for tool provider + */ +export const zToolProviderType = z.enum([ + 'api', + 'app', + 'builtin', + 'dataset-retrieval', + 'mcp', + 'plugin', + 'workflow', +]) + +/** + * PipelineDataset + */ +export const zPipelineDataset = z.object({ + chunk_structure: z.string(), + description: z.string().optional().default(''), + id: z.string(), + name: z.string(), +}) + +/** + * PipelineDocument + */ +export const zPipelineDocument = z.object({ + data_source_info: z.record(z.string(), z.unknown()).nullish(), + data_source_type: z.string(), + enabled: z.boolean(), + error: z.string().nullish(), + id: z.string(), + indexing_status: z.string(), + name: z.string(), + position: z.int(), +}) + +/** + * PipelineGenerateResponse + */ +export const zPipelineGenerateResponse = z.object({ + batch: z.string(), + dataset: zPipelineDataset, + documents: z.array(zPipelineDocument), +}) + /** * DatasetKeywordSettingResponse */ @@ -722,7 +825,7 @@ export const zDatasetRetrievalModelResponse = z.object({ reranking_model: zDatasetRerankingModelResponse.optional(), score_threshold: z.number().nullish(), score_threshold_enabled: z.boolean(), - search_method: z.string(), + search_method: zRetrievalMethod, top_k: z.int(), weights: zDatasetWeightedScoreResponse.nullish(), }) @@ -737,7 +840,16 @@ export const zDatasetDetailResponse = z.object({ chunk_structure: z.string().nullable(), created_at: z.int(), created_by: z.string(), - data_source_type: z.string().nullable(), + data_source_type: z + .enum([ + 'local_file', + 'notion_import', + 'online_document', + 'online_drive', + 'upload_file', + 'website_crawl', + ]) + .nullable(), description: z.string().nullable(), doc_form: z.string().nullable(), doc_metadata: z.array(zDatasetDocMetadataResponse), @@ -750,17 +862,18 @@ export const zDatasetDetailResponse = z.object({ external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), - indexing_technique: z.string().nullable(), + indexing_technique: zIndexTechniqueType.nullable(), is_multimodal: z.boolean(), is_published: z.boolean(), + keyword_number: z.int().nullish(), maintainer: z.string().nullish(), name: z.string(), - permission: z.string(), + permission: z.enum(['all_team_members', 'only_me', 'partial_members']), permission_keys: z.array(z.string()).optional(), pipeline_id: z.string().nullable(), provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, - runtime_mode: z.string().nullable(), + runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), @@ -783,17 +896,57 @@ export const zDatasourceProviderType = z.enum([ ]) /** - * I18nObject + * WorkflowExecutionStatus + */ +export const zWorkflowExecutionStatus = z.enum([ + 'failed', + 'partial-succeeded', + 'paused', + 'running', + 'scheduled', + 'stopped', + 'succeeded', +]) + +/** + * Data * - * Model class for i18n object. + * Data entity */ -export const zI18nObject = z.object({ - en_US: z.string(), - ja_JP: z.string().nullish(), - pt_BR: z.string().nullish(), - zh_Hans: z.string().nullish(), +export const zWorkflowAppBlockingResponseData = z.object({ + created_at: z.int(), + elapsed_time: z.number(), + error: z.string().nullish(), + finished_at: z.int().nullable(), + id: z.string(), + outputs: z.record(z.string(), z.unknown()).nullish(), + status: zWorkflowExecutionStatus, + total_steps: z.int(), + total_tokens: z.int(), + workflow_id: z.string(), +}) + +/** + * WorkflowAppBlockingResponse + * + * WorkflowAppBlockingResponse entity + */ +export const zWorkflowAppBlockingResponse = z.object({ + data: zWorkflowAppBlockingResponseData, + task_id: z.string(), + workflow_run_id: z.string(), }) +/** + * RagPipelinePublishedRunResponse + * + * Blocking run result: a preview run returns the workflow result, a processing run the queued batch. + */ +export const zRagPipelinePublishedRunResponse = z.union([ + zPipelineGenerateResponse, + zWorkflowAppBlockingResponse, +]) + /** * Option */ @@ -878,18 +1031,6 @@ export const zDatasourceIdentity = z.object({ provider: z.string(), }) -/** - * I18nObject - * - * Model class for i18n object. - */ -export const zCoreToolsEntitiesCommonEntitiesI18nObject = z.object({ - en_US: z.string(), - ja_JP: z.string().nullish(), - pt_BR: z.string().nullish(), - zh_Hans: z.string().nullish(), -}) - /** * ToolLabelEnum */ @@ -918,13 +1059,18 @@ export const zToolLabelEnum = z.enum([ */ export const zDatasourceProviderIdentity = z.object({ author: z.string(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, icon: z.string(), - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zI18nObject, name: z.string(), tags: z.array(zToolLabelEnum).nullish().default([]), }) +/** + * ToolParameterForm + */ +export const zToolParameterForm = z.enum(['form', 'llm', 'schema']) + /** * PluginParameterOption */ @@ -941,6 +1087,31 @@ export const zPluginParameterTemplate = z.object({ enabled: z.boolean().optional().default(false), }) +/** + * ToolParameterType + * + * removes TOOLS_SELECTOR from PluginParameterType + */ +export const zToolParameterType = z.enum([ + 'any', + 'app-selector', + 'array', + 'boolean', + 'checkbox', + 'date', + 'date-range', + 'dynamic-select', + 'file', + 'files', + 'model-selector', + 'number', + 'object', + 'secret-input', + 'select', + 'string', + 'system-files', +]) + /** * DatasourceParameterType * @@ -969,6 +1140,96 @@ export const zPluginParameterAutoGenerate = z.object({ type: zPluginParameterAutoGenerateType, }) +/** + * ToolParameter + * + * Tool-specific parameter declaration and invocation-value normalization. + */ +export const zToolParameter = z.object({ + auto_generate: zPluginParameterAutoGenerate.nullish(), + default: z + .union([ + z.number(), + z.int(), + z.string(), + z.boolean(), + z.array(z.unknown()), + z.record(z.string(), z.unknown()), + ]) + .nullish(), + form: zToolParameterForm, + human_description: zI18nObject.nullish(), + input_schema: z.record(z.string(), z.unknown()).nullish(), + label: zI18nObject, + llm_description: z.string().nullish(), + max: z.union([z.number(), z.int()]).nullish(), + min: z.union([z.number(), z.int()]).nullish(), + multiple: z.boolean().optional().default(false), + name: z.string(), + options: z.array(zPluginParameterOption).optional(), + placeholder: zI18nObject.nullish(), + precision: z.int().nullish(), + required: z.boolean().optional().default(false), + scope: z.string().nullish(), + template: zPluginParameterTemplate.nullish(), + type: zToolParameterType, +}) + +/** + * ToolApiEntity + */ +export const zToolApiEntity = z.object({ + author: z.string(), + description: zI18nObject, + label: zI18nObject, + labels: z.array(z.string()).optional(), + name: z.string(), + output_schema: z.record(z.string(), z.unknown()).optional(), + parameters: z.array(zToolParameter).nullish(), +}) + +/** + * ToolProviderApiEntityResponse + */ +export const zToolProviderApiEntityResponse = z.object({ + allow_delete: z.boolean().optional().default(true), + authentication: zMcpAuthentication.nullish(), + author: z.string(), + configuration: zMcpConfiguration.nullish(), + description: zI18nObject, + icon: z.union([z.string(), z.record(z.string(), z.string())]), + icon_dark: z + .union([z.string(), z.record(z.string(), z.string())]) + .optional() + .default(''), + id: z.string(), + identity_mode: z.string().optional().default('off'), + is_dynamic_registration: z.boolean().optional().default(true), + is_team_authorization: z.boolean().optional().default(false), + label: zI18nObject, + labels: z.array(z.string()).optional(), + masked_headers: z.record(z.string(), z.string()).nullish(), + name: z.string(), + original_headers: z.record(z.string(), z.string()).nullish(), + plugin_id: z.string().nullish().default(''), + plugin_unique_identifier: z.string().nullish().default(''), + server_identifier: z.string().nullish().default(''), + server_url: z.string().nullish().default(''), + team_credentials: z.record(z.string(), z.unknown()).optional(), + tools: z.array(zToolApiEntity).optional(), + type: zToolProviderType, + updated_at: z.int().optional(), + workflow_app_id: z.string().nullish(), +}) + +/** + * RagPipelineRecommendedPluginsResponse + */ +export const zRagPipelineRecommendedPluginsResponse = z.object({ + installed_recommended_plugins: z.array(zToolProviderApiEntityResponse), + uninstalled_recommended_plugins: z.array(z.record(z.string(), z.unknown())), +}) + /** * DatasourceParameter * @@ -1140,7 +1401,7 @@ export const zGetRagPipelinesRecommendedPluginsQuery = z.object({ /** * Success */ -export const zGetRagPipelinesRecommendedPluginsResponse = zRagPipelineOpaqueResponse +export const zGetRagPipelinesRecommendedPluginsResponse = zRagPipelineRecommendedPluginsResponse export const zPostRagPipelinesTransformDatasetsByDatasetIdPath = z.object({ dataset_id: z.uuid(), @@ -1149,7 +1410,7 @@ export const zPostRagPipelinesTransformDatasetsByDatasetIdPath = z.object({ /** * Success */ -export const zPostRagPipelinesTransformDatasetsByDatasetIdResponse = zRagPipelineOpaqueResponse +export const zPostRagPipelinesTransformDatasetsByDatasetIdResponse = zRagPipelineTransformResponse export const zPostRagPipelinesByPipelineIdCustomizedPublishBody = zCustomizedPipelineTemplatePayload @@ -1422,7 +1683,8 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftProcessingParametersQuery export const zGetRagPipelinesByPipelineIdWorkflowsDraftProcessingParametersResponse = zRagPipelineStepParametersResponse -export const zPostRagPipelinesByPipelineIdWorkflowsDraftRunBody = zDraftWorkflowRunPayload +export const zPostRagPipelinesByPipelineIdWorkflowsDraftRunBody = + zRagPipelineDraftWorkflowRunPayload export const zPostRagPipelinesByPipelineIdWorkflowsDraftRunPath = z.object({ pipeline_id: z.uuid(), @@ -1531,7 +1793,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsPublishResponse = zRagPipelineWorkflowPublishResponse export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewBody = - zParser + zDatasourceContentPreviewPayload export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewPath = z.object({ @@ -1597,7 +1859,8 @@ export const zPostRagPipelinesByPipelineIdWorkflowsPublishedRunPath = z.object({ /** * Success */ -export const zPostRagPipelinesByPipelineIdWorkflowsPublishedRunResponse = zRagPipelineOpaqueResponse +export const zPostRagPipelinesByPipelineIdWorkflowsPublishedRunResponse = + zRagPipelinePublishedRunResponse export const zDeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath = z.object({ pipeline_id: z.uuid(), diff --git a/packages/contracts/generated/api/console/rule-generate/types.gen.ts b/packages/contracts/generated/api/console/rule-generate/types.gen.ts index 0102cc6299562f..c620715302ecf6 100644 --- a/packages/contracts/generated/api/console/rule-generate/types.gen.ts +++ b/packages/contracts/generated/api/console/rule-generate/types.gen.ts @@ -10,7 +10,12 @@ export type RuleGeneratePayload = { no_variable?: boolean } -export type GeneratorResponse = unknown +export type RuleGenerateResponse = { + error: string + opening_statement: string + prompt: string + variables: Array +} export type CompletionRequestError = ErrorResponse & { code: 'completion_request_error' @@ -73,7 +78,7 @@ export type PostRuleGenerateErrors = { export type PostRuleGenerateError = PostRuleGenerateErrors[keyof PostRuleGenerateErrors] export type PostRuleGenerateResponses = { - 200: GeneratorResponse + 200: RuleGenerateResponse } export type PostRuleGenerateResponse = PostRuleGenerateResponses[keyof PostRuleGenerateResponses] diff --git a/packages/contracts/generated/api/console/rule-generate/zod.gen.ts b/packages/contracts/generated/api/console/rule-generate/zod.gen.ts index ccdb81084d4511..e480b37ca96ace 100644 --- a/packages/contracts/generated/api/console/rule-generate/zod.gen.ts +++ b/packages/contracts/generated/api/console/rule-generate/zod.gen.ts @@ -3,9 +3,14 @@ import * as z from 'zod' /** - * GeneratorResponse + * RuleGenerateResponse */ -export const zGeneratorResponse = z.unknown() +export const zRuleGenerateResponse = z.object({ + error: z.string(), + opening_statement: z.string(), + prompt: z.string(), + variables: z.array(z.string()), +}) /** * ErrorResponse @@ -102,4 +107,4 @@ export const zPostRuleGenerateBody = zRuleGeneratePayload /** * Rule configuration generated successfully */ -export const zPostRuleGenerateResponse = zGeneratorResponse +export const zPostRuleGenerateResponse = zRuleGenerateResponse diff --git a/packages/contracts/generated/api/console/rule-structured-output-generate/types.gen.ts b/packages/contracts/generated/api/console/rule-structured-output-generate/types.gen.ts index e2c3b371874b64..7b69be91cd00b1 100644 --- a/packages/contracts/generated/api/console/rule-structured-output-generate/types.gen.ts +++ b/packages/contracts/generated/api/console/rule-structured-output-generate/types.gen.ts @@ -9,7 +9,10 @@ export type RuleStructuredOutputPayload = { model_config: ModelConfig } -export type GeneratorResponse = unknown +export type StructuredOutputGenerateResponse = { + error: string + output: string +} export type CompletionRequestError = ErrorResponse & { code: 'completion_request_error' @@ -73,7 +76,7 @@ export type PostRuleStructuredOutputGenerateError = PostRuleStructuredOutputGenerateErrors[keyof PostRuleStructuredOutputGenerateErrors] export type PostRuleStructuredOutputGenerateResponses = { - 200: GeneratorResponse + 200: StructuredOutputGenerateResponse } export type PostRuleStructuredOutputGenerateResponse = diff --git a/packages/contracts/generated/api/console/rule-structured-output-generate/zod.gen.ts b/packages/contracts/generated/api/console/rule-structured-output-generate/zod.gen.ts index b6014692261115..c0a0ad54867f95 100644 --- a/packages/contracts/generated/api/console/rule-structured-output-generate/zod.gen.ts +++ b/packages/contracts/generated/api/console/rule-structured-output-generate/zod.gen.ts @@ -3,9 +3,12 @@ import * as z from 'zod' /** - * GeneratorResponse + * StructuredOutputGenerateResponse */ -export const zGeneratorResponse = z.unknown() +export const zStructuredOutputGenerateResponse = z.object({ + error: z.string(), + output: z.string(), +}) /** * ErrorResponse @@ -101,4 +104,4 @@ export const zPostRuleStructuredOutputGenerateBody = zRuleStructuredOutputPayloa /** * Structured output generated successfully */ -export const zPostRuleStructuredOutputGenerateResponse = zGeneratorResponse +export const zPostRuleStructuredOutputGenerateResponse = zStructuredOutputGenerateResponse diff --git a/packages/contracts/generated/api/console/snippets/types.gen.ts b/packages/contracts/generated/api/console/snippets/types.gen.ts index b60a71d2991af6..fa03cb5a41a19a 100644 --- a/packages/contracts/generated/api/console/snippets/types.gen.ts +++ b/packages/contracts/generated/api/console/snippets/types.gen.ts @@ -131,7 +131,7 @@ export type SnippetIterationNodeRunPayload = { } | null } -export type GeneratedAppResponse = JsonValue +export type GeneratedAppResponse = ShallowJsonValue export type SnippetLoopNodeRunPayload = { inputs?: { @@ -301,7 +301,7 @@ export type WorkflowDraftVariableResponse = { name: string selector: Array type: string - value: JsonValue + value: RecursiveJsonValue value_type: string visible: boolean } @@ -311,7 +311,7 @@ export type WorkflowDraftVariableUpdatePayload = { value?: unknown | null } -export type PublishWorkflowPayload = { +export type SnippetPublishWorkflowPayload = { knowledge_base_setting?: { [key: string]: unknown } | null @@ -403,7 +403,7 @@ export type EnvironmentVariableItemResponse = { visible: boolean } -export type JsonValue = +export type ShallowJsonValue = | string | number | number @@ -601,6 +601,17 @@ export type WorkflowDraftVariableFullContentResponse = { value_type: string } +export type RecursiveJsonValue = + | string + | number + | number + | boolean + | { + [key: string]: RecursiveJsonValue + } + | Array + | null + export type AgentScope = 'roster' | 'workflow_only' export type AgentSource = 'agent_app' | 'imported' | 'roster' | 'system' | 'workflow' @@ -2014,7 +2025,7 @@ export type GetSnippetsBySnippetIdWorkflowsPublishResponse = GetSnippetsBySnippetIdWorkflowsPublishResponses[keyof GetSnippetsBySnippetIdWorkflowsPublishResponses] export type PostSnippetsBySnippetIdWorkflowsPublishData = { - body: PublishWorkflowPayload + body: SnippetPublishWorkflowPayload path: { snippet_id: string } diff --git a/packages/contracts/generated/api/console/snippets/zod.gen.ts b/packages/contracts/generated/api/console/snippets/zod.gen.ts index e7fb74729fd0e5..7981ab88b37c2d 100644 --- a/packages/contracts/generated/api/console/snippets/zod.gen.ts +++ b/packages/contracts/generated/api/console/snippets/zod.gen.ts @@ -223,11 +223,11 @@ export const zWorkflowDraftVariableUpdatePayload = z.object({ }) /** - * PublishWorkflowPayload + * SnippetPublishWorkflowPayload * * Payload for publishing snippet workflow. */ -export const zPublishWorkflowPayload = z.object({ +export const zSnippetPublishWorkflowPayload = z.object({ knowledge_base_setting: z.record(z.string(), z.unknown()).nullish(), marked_comment: z.string().max(100).nullish(), marked_name: z.string().max(20).nullish(), @@ -451,7 +451,7 @@ export const zEnvironmentVariableListResponse = z.object({ items: z.array(zEnvironmentVariableItemResponse), }) -export const zJsonValue = z +export const zShallowJsonValue = z .union([ z.string(), z.int(), @@ -465,7 +465,7 @@ export const zJsonValue = z /** * GeneratedAppResponse */ -export const zGeneratedAppResponse = zJsonValue +export const zGeneratedAppResponse = zShallowJsonValue /** * AgentConfigSnapshotSummaryResponse @@ -626,6 +626,20 @@ export const zWorkflowDraftVariableFullContentResponse = z.object({ value_type: z.string(), }) +export const zRecursiveJsonValue = z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.record( + z.string(), + z.lazy((): any => zRecursiveJsonValue), + ), + z.array(z.lazy((): any => zRecursiveJsonValue)), + ]) + .nullable() + /** * WorkflowDraftVariableResponse */ @@ -638,7 +652,7 @@ export const zWorkflowDraftVariableResponse = z.object({ name: z.string(), selector: z.array(z.string()), type: z.string(), - value: zJsonValue, + value: zRecursiveJsonValue, value_type: z.string(), visible: z.boolean(), }) @@ -2137,7 +2151,7 @@ export const zGetSnippetsBySnippetIdWorkflowsPublishPath = z.object({ */ export const zGetSnippetsBySnippetIdWorkflowsPublishResponse = zSnippetWorkflowResponse -export const zPostSnippetsBySnippetIdWorkflowsPublishBody = zPublishWorkflowPayload +export const zPostSnippetsBySnippetIdWorkflowsPublishBody = zSnippetPublishWorkflowPayload export const zPostSnippetsBySnippetIdWorkflowsPublishPath = z.object({ snippet_id: z.uuid(), diff --git a/packages/contracts/generated/api/console/workspaces/orpc.gen.ts b/packages/contracts/generated/api/console/workspaces/orpc.gen.ts index 02e30c33be121d..17306247ac6248 100644 --- a/packages/contracts/generated/api/console/workspaces/orpc.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/orpc.gen.ts @@ -93,6 +93,7 @@ import { zGetWorkspacesCurrentEndpointsListPluginResponse, zGetWorkspacesCurrentEndpointsListQuery, zGetWorkspacesCurrentEndpointsListResponse, + zGetWorkspacesCurrentMembersQuery, zGetWorkspacesCurrentMembersResponse, zGetWorkspacesCurrentModelProvidersByProviderCheckoutUrlPath, zGetWorkspacesCurrentModelProvidersByProviderCheckoutUrlResponse, @@ -187,6 +188,7 @@ import { zGetWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistPath, zGetWorkspacesCurrentRbacDatasetsByDatasetIdWhitelistResponse, zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesPath, + zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesQuery, zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesResponse, zGetWorkspacesCurrentRbacMyPermissionsResponse, zGetWorkspacesCurrentRbacRolePermissionsCatalogAgentResponse, @@ -194,9 +196,12 @@ import { zGetWorkspacesCurrentRbacRolePermissionsCatalogDatasetResponse, zGetWorkspacesCurrentRbacRolePermissionsCatalogResponse, zGetWorkspacesCurrentRbacRolesByRoleIdMembersPath, + zGetWorkspacesCurrentRbacRolesByRoleIdMembersQuery, zGetWorkspacesCurrentRbacRolesByRoleIdMembersResponse, zGetWorkspacesCurrentRbacRolesByRoleIdPath, + zGetWorkspacesCurrentRbacRolesByRoleIdQuery, zGetWorkspacesCurrentRbacRolesByRoleIdResponse, + zGetWorkspacesCurrentRbacRolesQuery, zGetWorkspacesCurrentRbacRolesResponse, zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsPath, zGetWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdMemberBindingsResponse, @@ -409,8 +414,12 @@ import { zPostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyPath, zPostWorkspacesCurrentRbacAccessPoliciesByPolicyIdCopyResponse, zPostWorkspacesCurrentRbacAccessPoliciesResponse, + zPostWorkspacesCurrentRbacRolesBody, + zPostWorkspacesCurrentRbacRolesByRoleIdCopyBody, zPostWorkspacesCurrentRbacRolesByRoleIdCopyPath, + zPostWorkspacesCurrentRbacRolesByRoleIdCopyQuery, zPostWorkspacesCurrentRbacRolesByRoleIdCopyResponse, + zPostWorkspacesCurrentRbacRolesQuery, zPostWorkspacesCurrentRbacRolesResponse, zPostWorkspacesCurrentSkillsBody, zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesBody, @@ -537,7 +546,9 @@ import { zPutWorkspacesCurrentRbacMembersByMemberIdRbacRolesBody, zPutWorkspacesCurrentRbacMembersByMemberIdRbacRolesPath, zPutWorkspacesCurrentRbacMembersByMemberIdRbacRolesResponse, + zPutWorkspacesCurrentRbacRolesByRoleIdBody, zPutWorkspacesCurrentRbacRolesByRoleIdPath, + zPutWorkspacesCurrentRbacRolesByRoleIdQuery, zPutWorkspacesCurrentRbacRolesByRoleIdResponse, zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsBody, zPutWorkspacesCurrentRbacWorkspaceAgentsAccessPoliciesByPolicyIdBindingsPath, @@ -1262,6 +1273,7 @@ export const get12 = oc path: '/workspaces/current/members', tags: ['console'], }) + .input(z.object({ query: zGetWorkspacesCurrentMembersQuery.optional() })) .output(zGetWorkspacesCurrentMembersResponse) export const members = { @@ -3106,7 +3118,12 @@ export const get57 = oc path: '/workspaces/current/rbac/members/{member_id}/rbac-roles', tags: ['console'], }) - .input(z.object({ params: zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesPath })) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesPath, + query: zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesQuery.optional(), + }), + ) .output(zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesResponse) export const put14 = oc @@ -3224,7 +3241,13 @@ export const post46 = oc successStatus: 201, tags: ['console'], }) - .input(z.object({ params: zPostWorkspacesCurrentRbacRolesByRoleIdCopyPath })) + .input( + z.object({ + body: zPostWorkspacesCurrentRbacRolesByRoleIdCopyBody, + params: zPostWorkspacesCurrentRbacRolesByRoleIdCopyPath, + query: zPostWorkspacesCurrentRbacRolesByRoleIdCopyQuery.optional(), + }), + ) .output(zPostWorkspacesCurrentRbacRolesByRoleIdCopyResponse) export const copy2 = { @@ -3239,7 +3262,12 @@ export const get63 = oc path: '/workspaces/current/rbac/roles/{role_id}/members', tags: ['console'], }) - .input(z.object({ params: zGetWorkspacesCurrentRbacRolesByRoleIdMembersPath })) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacRolesByRoleIdMembersPath, + query: zGetWorkspacesCurrentRbacRolesByRoleIdMembersQuery.optional(), + }), + ) .output(zGetWorkspacesCurrentRbacRolesByRoleIdMembersResponse) export const members3 = { @@ -3265,7 +3293,12 @@ export const get64 = oc path: '/workspaces/current/rbac/roles/{role_id}', tags: ['console'], }) - .input(z.object({ params: zGetWorkspacesCurrentRbacRolesByRoleIdPath })) + .input( + z.object({ + params: zGetWorkspacesCurrentRbacRolesByRoleIdPath, + query: zGetWorkspacesCurrentRbacRolesByRoleIdQuery.optional(), + }), + ) .output(zGetWorkspacesCurrentRbacRolesByRoleIdResponse) export const put15 = oc @@ -3276,7 +3309,13 @@ export const put15 = oc path: '/workspaces/current/rbac/roles/{role_id}', tags: ['console'], }) - .input(z.object({ params: zPutWorkspacesCurrentRbacRolesByRoleIdPath })) + .input( + z.object({ + body: zPutWorkspacesCurrentRbacRolesByRoleIdBody, + params: zPutWorkspacesCurrentRbacRolesByRoleIdPath, + query: zPutWorkspacesCurrentRbacRolesByRoleIdQuery.optional(), + }), + ) .output(zPutWorkspacesCurrentRbacRolesByRoleIdResponse) export const byRoleId = { @@ -3295,6 +3334,7 @@ export const get65 = oc path: '/workspaces/current/rbac/roles', tags: ['console'], }) + .input(z.object({ query: zGetWorkspacesCurrentRbacRolesQuery.optional() })) .output(zGetWorkspacesCurrentRbacRolesResponse) export const post47 = oc @@ -3306,6 +3346,12 @@ export const post47 = oc successStatus: 201, tags: ['console'], }) + .input( + z.object({ + body: zPostWorkspacesCurrentRbacRolesBody, + query: zPostWorkspacesCurrentRbacRolesQuery.optional(), + }), + ) .output(zPostWorkspacesCurrentRbacRolesResponse) export const roles = { diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 7a93e7704c48b5..2a62eee92448ca 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -737,6 +737,12 @@ export type RbacRoleList = { pagination?: Pagination | null } +export type RoleUpsertRequest = { + description?: string + name: string + permission_keys?: Array +} + export type RbacRole = { category?: string description?: string @@ -749,6 +755,10 @@ export type RbacRole = { type: string } +export type CopyRoleParam = { + copy_member?: boolean +} + export type MembersInRoleList = { data?: Array pagination?: Pagination | null @@ -1516,7 +1526,7 @@ export type DslImportWarning = { export type PluginDependency = { current_identifier?: string | null type: PluginDependencyType - value: Github | Marketplace | Package + value: PluginDependencyGithub | PluginDependencyMarketplace | PluginDependencyPackage } export type AccountWithRoleResponse = { @@ -1529,9 +1539,7 @@ export type AccountWithRoleResponse = { last_login_at?: number | null name: string role: string - roles?: Array<{ - [key: string]: string - }> + roles?: Array status: string } @@ -1586,11 +1594,11 @@ export type ProviderResponse = { background?: string | null configurate_methods: Array custom_configuration: CustomConfigurationResponse - description?: I18nObject | null + description?: ModelRuntimeI18nObject | null help?: ProviderHelpEntity | null - icon_small?: I18nObject | null - icon_small_dark?: I18nObject | null - label: I18nObject + icon_small?: ModelRuntimeI18nObject | null + icon_small_dark?: ModelRuntimeI18nObject | null + label: ModelRuntimeI18nObject model_credential_schema?: ModelCredentialSchema | null preferred_provider_type: ProviderType provider: string @@ -1603,11 +1611,11 @@ export type ProviderResponse = { export type ModelProviderSummaryResponse = { configurate_methods: Array custom_configuration: ModelProviderCustomConfigurationSummaryResponse - description?: I18nObject | null - icon_small?: I18nObject | null - icon_small_dark?: I18nObject | null + description?: ModelRuntimeI18nObject | null + icon_small?: ModelRuntimeI18nObject | null + icon_small_dark?: ModelRuntimeI18nObject | null is_configured: boolean - label: I18nObject + label: ModelRuntimeI18nObject plugin_id: string preferred_provider_type: ProviderType provider: string @@ -1629,7 +1637,7 @@ export type ModelWithProviderEntityResponse = { features?: Array | null fetch_from: FetchFrom has_invalid_load_balancing_configs?: boolean - label: I18nObject + label: ModelRuntimeI18nObject load_balancing_enabled?: boolean model: string model_properties: { @@ -1661,8 +1669,8 @@ export type ModelLoadBalancingResponse = { export type ParameterRule = { default?: unknown | null - help?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null - label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + help?: ModelRuntimeI18nObject | null + label: ModelRuntimeI18nObject max?: number | null min?: number | null name: string @@ -1674,9 +1682,9 @@ export type ParameterRule = { } export type ProviderWithModelsResponse = { - icon_small?: I18nObject | null - icon_small_dark?: I18nObject | null - label: I18nObject + icon_small?: ModelRuntimeI18nObject | null + icon_small_dark?: ModelRuntimeI18nObject | null + label: ModelRuntimeI18nObject models: Array provider: string status: CustomConfigurationStatus @@ -1713,11 +1721,11 @@ export type PluginDeclaration = { category: PluginCategory created_at: string datasource?: DatasourceProviderEntity | null - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject endpoint?: EndpointProviderDeclaration | null icon: string icon_dark?: string | null - label: CoreToolsEntitiesCommonEntitiesI18nObject + label: I18nObject meta: Meta model?: ProviderEntity | null name: string @@ -1802,7 +1810,10 @@ export type TenantPluginInstallPermission = 'admins' | 'everyone' | 'noone' export type PluginBundleDependency = { type: PluginBundleDependencyType - value: Github | Marketplace | Package + value: + | PluginBundleDependencyGithub + | PluginBundleDependencyMarketplace + | PluginBundleDependencyPackage } export type PluginVerification = { @@ -1812,7 +1823,7 @@ export type PluginVerification = { export type PluginCategoryBuiltinToolProviderResponse = { allow_delete: boolean author: string - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject icon: | string | { @@ -1826,7 +1837,7 @@ export type PluginCategoryBuiltinToolProviderResponse = { | null id: string is_team_authorization: boolean - label: CoreToolsEntitiesCommonEntitiesI18nObject + label: I18nObject labels: Array name: string plugin_id: string | null @@ -2209,10 +2220,10 @@ export type TenantInfoResponse = { export type AgentStrategyProviderIdentity = { author: string - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject icon: string icon_dark?: string | null - label: CoreToolsEntitiesCommonEntitiesI18nObject + label: I18nObject name: string tags?: Array | null } @@ -2230,27 +2241,32 @@ export type AgentStrategyEntity = { export type PluginDependencyType = 'github' | 'marketplace' | 'package' -export type Github = { +export type PluginDependencyGithub = { github_plugin_unique_identifier: string package: string repo: string version: string } -export type Marketplace = { +export type PluginDependencyMarketplace = { marketplace_plugin_unique_identifier: string version?: string | null } -export type Package = { +export type PluginDependencyPackage = { plugin_unique_identifier: string version?: string | null } +export type MemberRoleSummaryResponse = { + id: string + name: string +} + export type SimpleProviderEntityResponse = { - icon_small?: I18nObject | null - icon_small_dark?: I18nObject | null - label: I18nObject + icon_small?: ModelRuntimeI18nObject | null + icon_small_dark?: ModelRuntimeI18nObject | null + label: ModelRuntimeI18nObject models?: Array provider: string provider_name?: string @@ -2274,9 +2290,14 @@ export type CustomConfigurationResponse = { status: CustomConfigurationStatus } +export type ModelRuntimeI18nObject = { + en_US: string + zh_Hans?: string | null +} + export type ProviderHelpEntity = { - title: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject - url: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + title: ModelRuntimeI18nObject + url: ModelRuntimeI18nObject } export type ModelCredentialSchema = { @@ -2358,11 +2379,6 @@ export type ModelLoadBalancingConfigResponse = { ttl: number } -export type GraphonModelRuntimeEntitiesCommonEntitiesI18nObject = { - en_US: string - zh_Hans?: string | null -} - export type ParameterType = 'boolean' | 'float' | 'int' | 'string' | 'text' export type ProviderModelWithStatusEntity = { @@ -2370,7 +2386,7 @@ export type ProviderModelWithStatusEntity = { features?: Array | null fetch_from: FetchFrom has_invalid_load_balancing_configs?: boolean - label: I18nObject + label: ModelRuntimeI18nObject load_balancing_enabled?: boolean model: string model_properties: { @@ -2406,13 +2422,6 @@ export type DatasourceProviderEntity = { provider_type: DatasourceProviderType } -export type CoreToolsEntitiesCommonEntitiesI18nObject = { - en_US: string - ja_JP?: string | null - pt_BR?: string | null - zh_Hans?: string | null -} - export type EndpointProviderDeclaration = { endpoints?: Array | null settings?: Array @@ -2421,11 +2430,11 @@ export type EndpointProviderDeclaration = { export type ProviderEntity = { background?: string | null configurate_methods: Array - description?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null + description?: ModelRuntimeI18nObject | null help?: ProviderHelpEntity | null - icon_small?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null - icon_small_dark?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null - label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + icon_small?: ModelRuntimeI18nObject | null + icon_small_dark?: ModelRuntimeI18nObject | null + label: ModelRuntimeI18nObject model_credential_schema?: ModelCredentialSchema | null models?: Array position?: { @@ -2486,13 +2495,13 @@ export type PluginDeclarationResponse = { datasource?: { [key: string]: unknown } | null - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject endpoint?: { [key: string]: unknown } | null icon: string icon_dark?: string | null - label: CoreToolsEntitiesCommonEntitiesI18nObject + label: I18nObject meta: { [key: string]: unknown } @@ -2518,12 +2527,30 @@ export type PluginDeclarationResponse = { export type PluginBundleDependencyType = 'github' | 'marketplace' | 'package' +export type PluginBundleDependencyGithub = { + packages: string + release: string + repo: string + repo_address: string +} + +export type PluginBundleDependencyMarketplace = { + organization: string + plugin: string + version: string +} + +export type PluginBundleDependencyPackage = { + manifest: PluginDeclaration + unique_identifier: string +} + export type AuthorizedCategory = 'community' | 'langgenius' | 'partner' export type PluginCategoryBuiltinToolResponse = { author: string - description: CoreToolsEntitiesCommonEntitiesI18nObject - label: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject + label: I18nObject labels: Array name: string output_schema: { @@ -2711,7 +2738,7 @@ export type AiModelEntityResponse = { deprecated?: boolean features?: Array | null fetch_from: FetchFrom - label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + label: ModelRuntimeI18nObject model: string model_properties: { [key in ModelPropertyKey]?: unknown @@ -2760,10 +2787,10 @@ export type CustomModelConfiguration = { export type CredentialFormSchema = { default?: string | null - label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + label: ModelRuntimeI18nObject max_length?: number options?: Array | null - placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null + placeholder?: ModelRuntimeI18nObject | null required?: boolean show_on?: Array type: FormType @@ -2771,8 +2798,8 @@ export type CredentialFormSchema = { } export type FieldModelSchema = { - label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject - placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null + label: ModelRuntimeI18nObject + placeholder?: ModelRuntimeI18nObject | null } export type ProviderQuotaType = 'free' | 'paid' | 'trial' @@ -2788,9 +2815,9 @@ export type QuotaConfiguration = { export type DatasourceProviderIdentity = { author: string - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject icon: string - label: CoreToolsEntitiesCommonEntitiesI18nObject + label: I18nObject name: string tags?: Array | null } @@ -2811,7 +2838,7 @@ export type AiModelEntity = { deprecated?: boolean features?: Array | null fetch_from: FetchFrom - label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + label: ModelRuntimeI18nObject model: string model_properties: { [key in ModelPropertyKey]?: unknown @@ -2831,16 +2858,16 @@ export type Permission = { export type ToolProviderIdentity = { author: string - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject icon: string icon_dark?: string | null - label: CoreToolsEntitiesCommonEntitiesI18nObject + label: I18nObject name: string tags?: Array | null } export type EventEntity = { - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject identity: EventIdentity output_schema?: { [key: string]: unknown @@ -2850,10 +2877,10 @@ export type EventEntity = { export type TriggerProviderIdentity = { author: string - description: CoreToolsEntitiesCommonEntitiesI18nObject + description: I18nObject icon?: string | null icon_dark?: string | null - label: CoreToolsEntitiesCommonEntitiesI18nObject + label: I18nObject name: string tags?: Array } @@ -2861,11 +2888,11 @@ export type TriggerProviderIdentity = { export type ProviderEntityResponse = { background?: string | null configurate_methods: Array - description?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null + description?: ModelRuntimeI18nObject | null help?: ProviderHelpEntity | null - icon_small?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null - icon_small_dark?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null - label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + icon_small?: ModelRuntimeI18nObject | null + icon_small_dark?: ModelRuntimeI18nObject | null + label: ModelRuntimeI18nObject model_credential_schema?: ModelCredentialSchema | null models?: Array position?: { @@ -2967,7 +2994,7 @@ export type EndpointProviderConfigScope = | 'workflow' export type FormOption = { - label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + label: ModelRuntimeI18nObject show_on?: Array value: string } @@ -3036,9 +3063,7 @@ export type AccountWithRoleResponseWritable = { last_login_at?: number | null name: string role: string - roles?: Array<{ - [key: string]: string - }> + roles?: Array status: string } @@ -3694,7 +3719,9 @@ export type PatchWorkspacesCurrentEndpointsByIdResponse = export type GetWorkspacesCurrentMembersData = { body?: never path?: never - query?: never + query?: { + language?: 'en' | 'ja' | 'zh' + } url: '/workspaces/current/members' } @@ -6102,7 +6129,9 @@ export type GetWorkspacesCurrentRbacMembersByMemberIdRbacRolesData = { path: { member_id: string } - query?: never + query?: { + language?: 'en' | 'ja' | 'zh' + } url: '/workspaces/current/rbac/members/{member_id}/rbac-roles' } @@ -6251,7 +6280,13 @@ export type GetWorkspacesCurrentRbacRolePermissionsCatalogDatasetResponse = export type GetWorkspacesCurrentRbacRolesData = { body?: never path?: never - query?: never + query?: { + include_owner?: number + language?: 'en' | 'ja' | 'zh' + limit?: number + page?: number + reverse?: boolean + } url: '/workspaces/current/rbac/roles' } @@ -6270,9 +6305,11 @@ export type GetWorkspacesCurrentRbacRolesResponse = GetWorkspacesCurrentRbacRolesResponses[keyof GetWorkspacesCurrentRbacRolesResponses] export type PostWorkspacesCurrentRbacRolesData = { - body?: never + body: RoleUpsertRequest path?: never - query?: never + query?: { + language?: 'en' | 'ja' | 'zh' + } url: '/workspaces/current/rbac/roles' } @@ -6318,7 +6355,9 @@ export type GetWorkspacesCurrentRbacRolesByRoleIdData = { path: { role_id: string } - query?: never + query?: { + language?: 'en' | 'ja' | 'zh' + } url: '/workspaces/current/rbac/roles/{role_id}' } @@ -6337,11 +6376,13 @@ export type GetWorkspacesCurrentRbacRolesByRoleIdResponse = GetWorkspacesCurrentRbacRolesByRoleIdResponses[keyof GetWorkspacesCurrentRbacRolesByRoleIdResponses] export type PutWorkspacesCurrentRbacRolesByRoleIdData = { - body?: never + body: RoleUpsertRequest path: { role_id: string } - query?: never + query?: { + language?: 'en' | 'ja' | 'zh' + } url: '/workspaces/current/rbac/roles/{role_id}' } @@ -6360,11 +6401,13 @@ export type PutWorkspacesCurrentRbacRolesByRoleIdResponse = PutWorkspacesCurrentRbacRolesByRoleIdResponses[keyof PutWorkspacesCurrentRbacRolesByRoleIdResponses] export type PostWorkspacesCurrentRbacRolesByRoleIdCopyData = { - body?: never + body: CopyRoleParam path: { role_id: string } - query?: never + query?: { + language?: 'en' | 'ja' | 'zh' + } url: '/workspaces/current/rbac/roles/{role_id}/copy' } @@ -6387,7 +6430,12 @@ export type GetWorkspacesCurrentRbacRolesByRoleIdMembersData = { path: { role_id: string } - query?: never + query?: { + language?: 'en' | 'ja' | 'zh' + limit?: number + page?: number + reverse?: boolean + } url: '/workspaces/current/rbac/roles/{role_id}/members' } diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index 7ff7c43cd1673b..90cf9467deb154 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -674,6 +674,17 @@ export const zReplaceMemberRolesRequest = z.object({ role_ids: z.array(z.string()).optional().default([]), }) +/** + * _RoleUpsertRequest + * + * Accepts the payload sent by the Create/Edit Role dialog. + */ +export const zRoleUpsertRequest = z.object({ + description: z.string().optional().default(''), + name: z.string(), + permission_keys: z.array(z.string()).optional().default([]), +}) + /** * RBACRole */ @@ -697,6 +708,13 @@ export const zMemberRolesResponse = z.object({ roles: z.array(zRbacRole).optional(), }) +/** + * CopyRoleParam + */ +export const zCopyRoleParam = z.object({ + copy_member: z.boolean().optional().default(true), +}) + /** * _ReplaceBindingsRequest */ @@ -1331,30 +1349,6 @@ export const zSnippetImportResponse = z.object({ warnings: z.array(zDslImportWarning), }) -/** - * AccountWithRoleResponse - */ -export const zAccountWithRoleResponse = z.object({ - avatar: z.string().nullish(), - avatar_url: z.string().nullable(), - created_at: z.int().nullish(), - email: z.string(), - id: z.string(), - last_active_at: z.int().nullish(), - last_login_at: z.int().nullish(), - name: z.string(), - role: z.string(), - roles: z.array(z.record(z.string(), z.string())).optional(), - status: z.string(), -}) - -/** - * AccountWithRoleListResponse - */ -export const zAccountWithRoleListResponse = z.object({ - accounts: z.array(zAccountWithRoleResponse), -}) - /** * MemberInviteSuccessResponse */ @@ -2348,7 +2342,7 @@ export const zPluginDependencyType = z.enum(['github', 'marketplace', 'package'] /** * Github */ -export const zGithub = z.object({ +export const zPluginDependencyGithub = z.object({ github_plugin_unique_identifier: z.string(), package: z.string(), repo: z.string(), @@ -2358,7 +2352,7 @@ export const zGithub = z.object({ /** * Marketplace */ -export const zMarketplace = z.object({ +export const zPluginDependencyMarketplace = z.object({ marketplace_plugin_unique_identifier: z.string(), version: z.string().nullish(), }) @@ -2366,7 +2360,7 @@ export const zMarketplace = z.object({ /** * Package */ -export const zPackage = z.object({ +export const zPluginDependencyPackage = z.object({ plugin_unique_identifier: z.string(), version: z.string().nullish(), }) @@ -2377,7 +2371,7 @@ export const zPackage = z.object({ export const zPluginDependency = z.object({ current_identifier: z.string().nullish(), type: zPluginDependencyType, - value: z.union([zGithub, zMarketplace, zPackage]), + value: z.union([zPluginDependencyGithub, zPluginDependencyMarketplace, zPluginDependencyPackage]), }) /** @@ -2387,6 +2381,38 @@ export const zSnippetDependencyCheckResponse = z.object({ leaked_dependencies: z.array(zPluginDependency), }) +/** + * MemberRoleSummaryResponse + */ +export const zMemberRoleSummaryResponse = z.object({ + id: z.string(), + name: z.string(), +}) + +/** + * AccountWithRoleResponse + */ +export const zAccountWithRoleResponse = z.object({ + avatar: z.string().nullish(), + avatar_url: z.string().nullable(), + created_at: z.int().nullish(), + email: z.string(), + id: z.string(), + last_active_at: z.int().nullish(), + last_login_at: z.int().nullish(), + name: z.string(), + role: z.string(), + roles: z.array(zMemberRoleSummaryResponse).optional(), + status: z.string(), +}) + +/** + * AccountWithRoleListResponse + */ +export const zAccountWithRoleListResponse = z.object({ + accounts: z.array(zAccountWithRoleResponse), +}) + /** * ConfigurateMethod * @@ -2394,6 +2420,26 @@ export const zSnippetDependencyCheckResponse = z.object({ */ export const zConfigurateMethod = z.enum(['customizable-model', 'predefined-model']) +/** + * I18nObject + * + * Model class for i18n object. + */ +export const zModelRuntimeI18nObject = z.object({ + en_US: z.string(), + zh_Hans: z.string().nullish(), +}) + +/** + * ProviderHelpEntity + * + * Model class for provider help. + */ +export const zProviderHelpEntity = z.object({ + title: zModelRuntimeI18nObject, + url: zModelRuntimeI18nObject, +}) + /** * ProviderType */ @@ -2513,26 +2559,6 @@ export const zModelCredentialResponse = z.object({ load_balancing: zModelLoadBalancingResponse, }) -/** - * I18nObject - * - * Model class for i18n object. - */ -export const zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject = z.object({ - en_US: z.string(), - zh_Hans: z.string().nullish(), -}) - -/** - * ProviderHelpEntity - * - * Model class for provider help. - */ -export const zProviderHelpEntity = z.object({ - title: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, - url: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, -}) - /** * ParameterType * @@ -2547,8 +2573,8 @@ export const zParameterType = z.enum(['boolean', 'float', 'int', 'string', 'text */ export const zParameterRule = z.object({ default: z.unknown().nullish(), - help: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), - label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + help: zModelRuntimeI18nObject.nullish(), + label: zModelRuntimeI18nObject, max: z.number().nullish(), min: z.number().nullish(), name: z.string(), @@ -2576,7 +2602,7 @@ export const zProviderModelWithStatusEntity = z.object({ features: z.array(zModelFeature).nullish(), fetch_from: zFetchFrom, has_invalid_load_balancing_configs: z.boolean().optional().default(false), - label: zI18nObject, + label: zModelRuntimeI18nObject, load_balancing_enabled: z.boolean().optional().default(false), model: z.string(), model_properties: z.record(z.string(), z.unknown()), @@ -2597,9 +2623,9 @@ export const zCustomConfigurationStatus = z.enum(['active', 'no-configure']) * Model class for provider with models response. */ export const zProviderWithModelsResponse = z.object({ - icon_small: zI18nObject.nullish(), - icon_small_dark: zI18nObject.nullish(), - label: zI18nObject, + icon_small: zModelRuntimeI18nObject.nullish(), + icon_small_dark: zModelRuntimeI18nObject.nullish(), + label: zModelRuntimeI18nObject, models: z.array(zProviderModelWithStatusEntity), provider: z.string(), status: zCustomConfigurationStatus, @@ -2633,11 +2659,11 @@ export const zModelProviderCustomConfigurationSummaryResponse = z.object({ export const zModelProviderSummaryResponse = z.object({ configurate_methods: z.array(zConfigurateMethod), custom_configuration: zModelProviderCustomConfigurationSummaryResponse, - description: zI18nObject.nullish(), - icon_small: zI18nObject.nullish(), - icon_small_dark: zI18nObject.nullish(), + description: zModelRuntimeI18nObject.nullish(), + icon_small: zModelRuntimeI18nObject.nullish(), + icon_small_dark: zModelRuntimeI18nObject.nullish(), is_configured: z.boolean(), - label: zI18nObject, + label: zModelRuntimeI18nObject, plugin_id: z.string(), preferred_provider_type: zProviderType, provider: z.string(), @@ -2713,18 +2739,6 @@ export const zPluginCategory = z.enum([ 'trigger', ]) -/** - * I18nObject - * - * Model class for i18n object. - */ -export const zCoreToolsEntitiesCommonEntitiesI18nObject = z.object({ - en_US: z.string(), - ja_JP: z.string().nullish(), - pt_BR: z.string().nullish(), - zh_Hans: z.string().nullish(), -}) - /** * Plugins */ @@ -2796,17 +2810,23 @@ export const zPluginTaskResponse = z.object({ export const zPluginBundleDependencyType = z.enum(['github', 'marketplace', 'package']) /** - * PluginBundleDependency + * Github */ -export const zPluginBundleDependency = z.object({ - type: zPluginBundleDependencyType, - value: z.union([zGithub, zMarketplace, zPackage]), +export const zPluginBundleDependencyGithub = z.object({ + packages: z.string(), + release: z.string(), + repo: z.string(), + repo_address: z.string(), }) /** - * PluginBundleUploadResponse + * Marketplace */ -export const zPluginBundleUploadResponse = z.array(zPluginBundleDependency) +export const zPluginBundleDependencyMarketplace = z.object({ + organization: z.string(), + plugin: z.string(), + version: z.string(), +}) /** * AuthorizedCategory @@ -2827,8 +2847,8 @@ export const zPluginVerification = z.object({ */ export const zPluginCategoryBuiltinToolResponse = z.object({ author: z.string(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, + label: zI18nObject, labels: z.array(z.string()), name: z.string(), output_schema: z.record(z.string(), z.unknown()), @@ -2841,12 +2861,12 @@ export const zPluginCategoryBuiltinToolResponse = z.object({ export const zPluginCategoryBuiltinToolProviderResponse = z.object({ allow_delete: z.boolean(), author: z.string(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, icon: z.union([z.string(), z.record(z.string(), z.string())]), icon_dark: z.union([z.string(), z.record(z.string(), z.string())]).nullable(), id: z.string(), is_team_authorization: z.boolean(), - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zI18nObject, labels: z.array(z.string()), name: z.string(), plugin_id: z.string().nullable(), @@ -3186,10 +3206,10 @@ export const zToolLabelEnum = z.enum([ */ export const zAgentStrategyProviderIdentity = z.object({ author: z.string(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, icon: z.string(), icon_dark: z.string().nullish(), - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zI18nObject, name: z.string(), tags: z.array(zToolLabelEnum).nullish().default([]), }) @@ -3274,8 +3294,8 @@ export const zCustomConfigurationResponse = z.object({ * FieldModelSchema */ export const zFieldModelSchema = z.object({ - label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, - placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), + label: zModelRuntimeI18nObject, + placeholder: zModelRuntimeI18nObject.nullish(), }) /** @@ -3288,9 +3308,9 @@ export const zProviderQuotaType = z.enum(['free', 'paid', 'trial']) */ export const zDatasourceProviderIdentity = z.object({ author: z.string(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, icon: z.string(), - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zI18nObject, name: z.string(), tags: z.array(zToolLabelEnum).nullish().default([]), }) @@ -3345,10 +3365,10 @@ export const zEndpointProviderDeclaration = z.object({ */ export const zToolProviderIdentity = z.object({ author: z.string(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, icon: z.string(), icon_dark: z.string().nullish(), - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zI18nObject, name: z.string(), tags: z.array(zToolLabelEnum).nullish().default([]), }) @@ -3370,10 +3390,10 @@ export const zToolProviderEntity = z.object({ */ export const zTriggerProviderIdentity = z.object({ author: z.string(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, icon: z.string().nullish(), icon_dark: z.string().nullish(), - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zI18nObject, name: z.string(), tags: z.array(z.string()).optional(), }) @@ -3472,7 +3492,7 @@ export const zAiModelEntityResponse = z.object({ deprecated: z.boolean().optional().default(false), features: z.array(zModelFeature).nullish(), fetch_from: zFetchFrom, - label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + label: zModelRuntimeI18nObject, model: z.string(), model_properties: z.record(z.string(), z.unknown()), model_type: zModelType, @@ -3486,9 +3506,9 @@ export const zAiModelEntityResponse = z.object({ * Simple provider entity response. */ export const zSimpleProviderEntityResponse = z.object({ - icon_small: zI18nObject.nullish(), - icon_small_dark: zI18nObject.nullish(), - label: zI18nObject, + icon_small: zModelRuntimeI18nObject.nullish(), + icon_small_dark: zModelRuntimeI18nObject.nullish(), + label: zModelRuntimeI18nObject, models: z.array(zAiModelEntityResponse).optional().default([]), provider: z.string(), provider_name: z.string().optional().default(''), @@ -3524,7 +3544,7 @@ export const zModelWithProviderEntityResponse = z.object({ features: z.array(zModelFeature).nullish(), fetch_from: zFetchFrom, has_invalid_load_balancing_configs: z.boolean().optional().default(false), - label: zI18nObject, + label: zModelRuntimeI18nObject, load_balancing_enabled: z.boolean().optional().default(false), model: z.string(), model_properties: z.record(z.string(), z.unknown()), @@ -3643,7 +3663,7 @@ export const zFormShowOnObject = z.object({ * Model class for form option. */ export const zFormOption = z.object({ - label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + label: zModelRuntimeI18nObject, show_on: z.array(zFormShowOnObject).optional().default([]), value: z.string(), }) @@ -3662,10 +3682,10 @@ export const zFormType = z.enum(['radio', 'secret-input', 'select', 'switch', 't */ export const zCredentialFormSchema = z.object({ default: z.string().nullish(), - label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + label: zModelRuntimeI18nObject, max_length: z.int().optional().default(0), options: z.array(zFormOption).nullish(), - placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), + placeholder: zModelRuntimeI18nObject.nullish(), required: z.boolean().optional().default(true), show_on: z.array(zFormShowOnObject).optional().default([]), type: zFormType, @@ -3699,11 +3719,11 @@ export const zProviderCredentialSchema = z.object({ export const zProviderEntityResponse = z.object({ background: z.string().nullish(), configurate_methods: z.array(zConfigurateMethod), - description: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), + description: zModelRuntimeI18nObject.nullish(), help: zProviderHelpEntity.nullish(), - icon_small: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), - icon_small_dark: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), - label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + icon_small: zModelRuntimeI18nObject.nullish(), + icon_small_dark: zModelRuntimeI18nObject.nullish(), + label: zModelRuntimeI18nObject, model_credential_schema: zModelCredentialSchema.nullish(), models: z.array(zAiModelEntityResponse).optional().default([]), position: z.record(z.string(), z.array(z.string())).nullish().default({}), @@ -3722,11 +3742,11 @@ export const zPluginDeclarationResponse = z.object({ category: zPluginCategory, created_at: z.iso.datetime(), datasource: z.record(z.string(), z.unknown()).nullish(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, endpoint: z.record(z.string(), z.unknown()).nullish(), icon: z.string(), icon_dark: z.string().nullish(), - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zI18nObject, meta: z.record(z.string(), z.unknown()), model: zProviderEntityResponse.nullish(), name: z.string(), @@ -3846,11 +3866,11 @@ export const zProviderResponse = z.object({ background: z.string().nullish(), configurate_methods: z.array(zConfigurateMethod), custom_configuration: zCustomConfigurationResponse, - description: zI18nObject.nullish(), + description: zModelRuntimeI18nObject.nullish(), help: zProviderHelpEntity.nullish(), - icon_small: zI18nObject.nullish(), - icon_small_dark: zI18nObject.nullish(), - label: zI18nObject, + icon_small: zModelRuntimeI18nObject.nullish(), + icon_small_dark: zModelRuntimeI18nObject.nullish(), + label: zModelRuntimeI18nObject, model_credential_schema: zModelCredentialSchema.nullish(), preferred_provider_type: zProviderType, provider: z.string(), @@ -3891,7 +3911,7 @@ export const zAiModelEntity = z.object({ deprecated: z.boolean().optional().default(false), features: z.array(zModelFeature).nullish(), fetch_from: zFetchFrom, - label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + label: zModelRuntimeI18nObject, model: z.string(), model_properties: z.record(z.string(), z.unknown()), model_type: zModelType, @@ -3911,11 +3931,11 @@ export const zAiModelEntity = z.object({ export const zProviderEntity = z.object({ background: z.string().nullish(), configurate_methods: z.array(zConfigurateMethod), - description: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), + description: zModelRuntimeI18nObject.nullish(), help: zProviderHelpEntity.nullish(), - icon_small: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), - icon_small_dark: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), - label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + icon_small: zModelRuntimeI18nObject.nullish(), + icon_small_dark: zModelRuntimeI18nObject.nullish(), + label: zModelRuntimeI18nObject, model_credential_schema: zModelCredentialSchema.nullish(), models: z.array(zAiModelEntity).optional(), position: z.record(z.string(), z.array(z.string())).nullish().default({}), @@ -4290,7 +4310,7 @@ export const zAgentProviderListResponse = z.array(zAgentProviderResponse) * The configuration of an event */ export const zEventEntity = z.object({ - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, identity: zEventIdentity, output_schema: z.record(z.string(), z.unknown()).nullish(), parameters: z.array(zEventParameter).optional(), @@ -4320,11 +4340,11 @@ export const zPluginDeclaration = z.object({ category: zPluginCategory, created_at: z.iso.datetime(), datasource: zDatasourceProviderEntity.nullish(), - description: zCoreToolsEntitiesCommonEntitiesI18nObject, + description: zI18nObject, endpoint: zEndpointProviderDeclaration.nullish(), icon: z.string(), icon_dark: z.string().nullish(), - label: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zI18nObject, meta: zMeta, model: zProviderEntity.nullish(), name: z.string().regex(/^[a-z0-9_-]{1,128}$/), @@ -4384,6 +4404,31 @@ export const zPluginListResponse = z.object({ total: z.int(), }) +/** + * Package + */ +export const zPluginBundleDependencyPackage = z.object({ + manifest: zPluginDeclaration, + unique_identifier: z.string(), +}) + +/** + * PluginBundleDependency + */ +export const zPluginBundleDependency = z.object({ + type: zPluginBundleDependencyType, + value: z.union([ + zPluginBundleDependencyGithub, + zPluginBundleDependencyMarketplace, + zPluginBundleDependencyPackage, + ]), +}) + +/** + * PluginBundleUploadResponse + */ +export const zPluginBundleUploadResponse = z.array(zPluginBundleDependency) + /** * AccountWithRoleResponse */ @@ -4396,7 +4441,7 @@ export const zAccountWithRoleResponseWritable = z.object({ last_login_at: z.int().nullish(), name: z.string(), role: z.string(), - roles: z.array(z.record(z.string(), z.string())).optional(), + roles: z.array(zMemberRoleSummaryResponse).optional(), status: z.string(), }) @@ -4651,6 +4696,10 @@ export const zPatchWorkspacesCurrentEndpointsByIdPath = z.object({ */ export const zPatchWorkspacesCurrentEndpointsByIdResponse = zEndpointMutationResponse +export const zGetWorkspacesCurrentMembersQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), +}) + /** * Success */ @@ -5718,6 +5767,10 @@ export const zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesPath = z.object( member_id: z.uuid(), }) +export const zGetWorkspacesCurrentRbacMembersByMemberIdRbacRolesQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), +}) + /** * Success */ @@ -5761,11 +5814,25 @@ export const zGetWorkspacesCurrentRbacRolePermissionsCatalogAppResponse = zPermi export const zGetWorkspacesCurrentRbacRolePermissionsCatalogDatasetResponse = zPermissionCatalogResponse +export const zGetWorkspacesCurrentRbacRolesQuery = z.object({ + include_owner: z.int().gte(0).lte(1).optional().default(0), + language: z.enum(['en', 'ja', 'zh']).optional(), + limit: z.int().gte(1).lte(99999).optional(), + page: z.int().gte(1).optional(), + reverse: z.boolean().optional(), +}) + /** * Success */ export const zGetWorkspacesCurrentRbacRolesResponse = zRbacRoleList +export const zPostWorkspacesCurrentRbacRolesBody = zRoleUpsertRequest + +export const zPostWorkspacesCurrentRbacRolesQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), +}) + /** * Role created */ @@ -5784,24 +5851,40 @@ export const zGetWorkspacesCurrentRbacRolesByRoleIdPath = z.object({ role_id: z.uuid(), }) +export const zGetWorkspacesCurrentRbacRolesByRoleIdQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), +}) + /** * Success */ export const zGetWorkspacesCurrentRbacRolesByRoleIdResponse = zRbacRole +export const zPutWorkspacesCurrentRbacRolesByRoleIdBody = zRoleUpsertRequest + export const zPutWorkspacesCurrentRbacRolesByRoleIdPath = z.object({ role_id: z.uuid(), }) +export const zPutWorkspacesCurrentRbacRolesByRoleIdQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), +}) + /** * Success */ export const zPutWorkspacesCurrentRbacRolesByRoleIdResponse = zRbacRole +export const zPostWorkspacesCurrentRbacRolesByRoleIdCopyBody = zCopyRoleParam + export const zPostWorkspacesCurrentRbacRolesByRoleIdCopyPath = z.object({ role_id: z.uuid(), }) +export const zPostWorkspacesCurrentRbacRolesByRoleIdCopyQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), +}) + /** * Role copied */ @@ -5811,6 +5894,13 @@ export const zGetWorkspacesCurrentRbacRolesByRoleIdMembersPath = z.object({ role_id: z.uuid(), }) +export const zGetWorkspacesCurrentRbacRolesByRoleIdMembersQuery = z.object({ + language: z.enum(['en', 'ja', 'zh']).optional(), + limit: z.int().gte(1).lte(99999).optional(), + page: z.int().gte(1).optional(), + reverse: z.boolean().optional(), +}) + /** * Success */ diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index 6499d259e50f21..bf2233f83c83ea 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -560,13 +560,6 @@ export type FormSubmitResponse = { hints?: Array } -export type Github = { - github_plugin_unique_identifier: string - package: string - repo: string - version: string -} - export type HealthResponse = { ok: boolean } @@ -653,11 +646,6 @@ export type LlmModelBlock = { provider: string } -export type Marketplace = { - marketplace_plugin_unique_identifier: string - version?: string | null -} - export type MarketplacePluginListResponse = { data: Array has_more: boolean @@ -926,11 +914,6 @@ export type OpenApiFormSubmitPayload = { } } -export type Package = { - plugin_unique_identifier: string - version?: string | null -} - export type PermittedExternalAppsListQuery = { limit?: number mode?: SupportedAppType | null @@ -963,7 +946,24 @@ export type PluginDeleteResponse = { export type PluginDependency = { current_identifier?: string | null type: PluginDependencyType - value: Github | Marketplace | Package + value: PluginDependencyGithub | PluginDependencyMarketplace | PluginDependencyPackage +} + +export type PluginDependencyGithub = { + github_plugin_unique_identifier: string + package: string + repo: string + version: string +} + +export type PluginDependencyMarketplace = { + marketplace_plugin_unique_identifier: string + version?: string | null +} + +export type PluginDependencyPackage = { + plugin_unique_identifier: string + version?: string | null } export type PluginDependencyType = 'github' | 'marketplace' | 'package' diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index b3738f7103bbce..557ce82b7c7ffc 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -668,16 +668,6 @@ export const zFileUploadPayload = z.object({ file: z.custom((value) => value instanceof Blob || value instanceof File), }) -/** - * Github - */ -export const zGithub = z.object({ - github_plugin_unique_identifier: z.string(), - package: z.string(), - repo: z.string(), - version: z.string(), -}) - /** * HealthResponse * @@ -837,14 +827,6 @@ export const zLlmModelBlock = z.object({ provider: z.string(), }) -/** - * Marketplace - */ -export const zMarketplace = z.object({ - marketplace_plugin_unique_identifier: z.string(), - version: z.string().nullish(), -}) - /** * MarketplacePluginRow */ @@ -1226,14 +1208,6 @@ export const zOpenApiFormSubmitPayload = z.object({ inputs: z.record(z.string(), zJsonValue), }) -/** - * Package - */ -export const zPackage = z.object({ - plugin_unique_identifier: z.string(), - version: z.string().nullish(), -}) - /** * PermittedExternalAppsListResponse */ @@ -1276,6 +1250,32 @@ export const zPluginDeleteResponse = z.object({ plugin_id: z.string(), }) +/** + * Github + */ +export const zPluginDependencyGithub = z.object({ + github_plugin_unique_identifier: z.string(), + package: z.string(), + repo: z.string(), + version: z.string(), +}) + +/** + * Marketplace + */ +export const zPluginDependencyMarketplace = z.object({ + marketplace_plugin_unique_identifier: z.string(), + version: z.string().nullish(), +}) + +/** + * Package + */ +export const zPluginDependencyPackage = z.object({ + plugin_unique_identifier: z.string(), + version: z.string().nullish(), +}) + /** * PluginDependencyType */ @@ -1287,7 +1287,7 @@ export const zPluginDependencyType = z.enum(['github', 'marketplace', 'package'] export const zPluginDependency = z.object({ current_identifier: z.string().nullish(), type: zPluginDependencyType, - value: z.union([zGithub, zMarketplace, zPackage]), + value: z.union([zPluginDependencyGithub, zPluginDependencyMarketplace, zPluginDependencyPackage]), }) /** diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index b535b50d7aebda..2f479e7204fca3 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -21,7 +21,7 @@ export type AgentThought = { thought?: string | null tool?: string | null tool_input?: string | null - tool_labels: JsonValue + tool_labels: AnyJsonValue } export type Annotation = { @@ -68,6 +68,8 @@ export type AnnotationReplyActionPayload = { score_threshold: number } +export type AnyJsonValue = unknown + export type AppFeedbackListResponse = { data: Array } @@ -352,7 +354,7 @@ export type ChildChunkResponse = { id: string position: number segment_id: string - type: string + type: 'automatic' | 'customized' updated_at: number word_count: number } @@ -591,7 +593,14 @@ export type DatasetDetailResponse = { chunk_structure: string | null created_at: number created_by: string - data_source_type: string | null + data_source_type: + | 'local_file' + | 'notion_import' + | 'online_document' + | 'online_drive' + | 'upload_file' + | 'website_crawl' + | null description: string | null doc_form: string | null doc_metadata: Array @@ -604,16 +613,16 @@ export type DatasetDetailResponse = { external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string - indexing_technique: string | null + indexing_technique: IndexTechniqueType | null is_multimodal: boolean is_published: boolean maintainer?: string | null name: string - permission: string + permission: 'all_team_members' | 'only_me' | 'partial_members' pipeline_id: string | null provider: string retrieval_model_dict: DatasetRetrievalModelResponse - runtime_mode: string | null + runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number @@ -630,7 +639,14 @@ export type DatasetDetailWithPartialMembersResponse = { chunk_structure: string | null created_at: number created_by: string - data_source_type: string | null + data_source_type: + | 'local_file' + | 'notion_import' + | 'online_document' + | 'online_drive' + | 'upload_file' + | 'website_crawl' + | null description: string | null doc_form: string | null doc_metadata: Array @@ -643,17 +659,17 @@ export type DatasetDetailWithPartialMembersResponse = { external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string - indexing_technique: string | null + indexing_technique: IndexTechniqueType | null is_multimodal: boolean is_published: boolean maintainer?: string | null name: string partial_member_list?: Array | null - permission: string + permission: 'all_team_members' | 'only_me' | 'partial_members' pipeline_id: string | null provider: string retrieval_model_dict: DatasetRetrievalModelResponse - runtime_mode: string | null + runtime_mode: 'general' | 'rag_pipeline' | null summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number @@ -666,7 +682,7 @@ export type DatasetDetailWithPartialMembersResponse = { export type DatasetDocMetadataResponse = { id: string name: string - type: string + type: 'number' | 'string' | 'time' } export type DatasetExternalKnowledgeInfoResponse = { @@ -715,7 +731,7 @@ export type DatasetMetadataActionResponse = { export type DatasetMetadataBuiltInFieldResponse = { name: string - type: string + type: 'number' | 'string' | 'time' } export type DatasetMetadataBuiltInFieldsResponse = { @@ -726,7 +742,7 @@ export type DatasetMetadataListItemResponse = { count?: number id: string name: string - type: string + type: 'number' | 'string' | 'time' } export type DatasetMetadataListResponse = { @@ -737,7 +753,7 @@ export type DatasetMetadataListResponse = { export type DatasetMetadataResponse = { id: string name: string - type: string + type: 'number' | 'string' | 'time' } export type DatasetNameDuplicateError = ErrorResponse & { @@ -756,7 +772,7 @@ export type DatasetRetrievalModelResponse = { reranking_model?: DatasetRerankingModelResponse score_threshold?: number | null score_threshold_enabled: boolean - search_method: string + search_method: RetrievalMethod top_k: number weights?: DatasetWeightedScoreResponse | null } @@ -930,8 +946,8 @@ export type DocumentMetadataOperation = { export type DocumentMetadataResponse = { id: string name: string - type: string - value?: string | number | number | boolean | null + type: 'number' | 'string' | 'time' + value: string | number | number | null } export type DocumentResponse = { @@ -939,11 +955,9 @@ export type DocumentResponse = { created_at?: number | null created_by?: string | null created_from?: string | null - data_source_detail_dict: { - [key: string]: unknown - } + data_source_detail_dict: DocumentSourceDetailResponse data_source_info?: { - [key: string]: unknown + [key: string]: JsonValue } | null data_source_type?: string | null dataset_process_rule_id?: string | null @@ -965,6 +979,11 @@ export type DocumentResponse = { word_count?: number | null } +export type DocumentSourceDetailResponse = { + upload_file?: DocumentUploadFileDetailResponse | null + [key: string]: unknown +} + export type DocumentStatusListResponse = { data: Array } @@ -978,10 +997,18 @@ export type DocumentStatusResponse = { completed_at: number | null completed_segments?: number | null error: string | null - error_code?: string | null + error_code?: 'vector_space_estimate_exceeded' | null estimated_vector_space_mb?: number | null id: string - indexing_status: string + indexing_status: + | 'cleaning' + | 'completed' + | 'error' + | 'indexing' + | 'parsing' + | 'paused' + | 'splitting' + | 'waiting' parsing_completed_at: number | null paused_at: number | null processing_started_at: number | null @@ -1030,6 +1057,16 @@ export type DocumentTextUpdate = ( text?: string | null } +export type DocumentUploadFileDetailResponse = { + created_at: number + created_by: string + extension: string + id: string + mime_type?: string | null + name: string + size: number +} + export type EndUserDetail = { app_id?: string | null created_at: string @@ -1279,21 +1316,21 @@ export type HumanInputFormSubmissionData = { node_title: string rendered_content: string submitted_data?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } | null } export type HumanInputFormSubmitPayload = { action: string inputs: { - [key: string]: JsonValue2 + [key: string]: JsonValue } } export type HumanInputFormSubmitPayloadWithUser = { action: string inputs: { - [key: string]: JsonValue2 + [key: string]: JsonValue } user: string } @@ -1307,17 +1344,14 @@ export type HumanInputFormSubmittedError = ErrorResponse & { status: 412 } -export type I18nObject = { - en_US: string - zh_Hans?: string | null -} - export type IndexInfoResponse = { api_version: string server_version: string welcome: string } +export type IndexTechniqueType = 'economy' | 'high_quality' + export type InvalidActionError = ErrorResponse & { code: 'invalid_action' status: 400 @@ -1337,11 +1371,9 @@ export type JsonObject = { [key: string]: unknown } -export type JsonValue = unknown - export type JsonValueType = unknown -export type JsonValue2 = unknown +export type JsonValue = unknown export type KnowledgeTagListResponse = Array @@ -1466,6 +1498,11 @@ export type ModelPropertyKey = | 'voices' | 'word_limit' +export type ModelRuntimeI18nObject = { + en_US: string + zh_Hans?: string | null +} + export type ModelStatus = | 'active' | 'credential-removed' @@ -1923,7 +1960,7 @@ export type ProviderModelWithStatusEntity = { features?: Array | null fetch_from: FetchFrom has_invalid_load_balancing_configs?: boolean - label: I18nObject + label: ModelRuntimeI18nObject load_balancing_enabled?: boolean model: string model_properties: { @@ -1953,9 +1990,9 @@ export type ProviderWithModelsListResponse = { } export type ProviderWithModelsResponse = { - icon_small?: I18nObject | null - icon_small_dark?: I18nObject | null - label: I18nObject + icon_small?: ModelRuntimeI18nObject | null + icon_small_dark?: ModelRuntimeI18nObject | null + label: ModelRuntimeI18nObject models: Array provider: string status: CustomConfigurationStatus @@ -2107,7 +2144,7 @@ export type SegmentResponse = { keywords: Array | null position: number sign_content: string - status: string + status: 'completed' | 'error' | 'indexing' | 'paused' | 're_segment' | 'waiting' stopped_at: number | null summary: string | null tokens: number @@ -2152,7 +2189,7 @@ export type SimpleConversation = { created_at?: number | null id: string inputs: { - [key: string]: JsonValue + [key: string]: AnyJsonValue } introduction?: string | null name: string diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index 0f9503a04c32ea..dc091de05887b2 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -67,6 +67,26 @@ export const zAnnotationReplyActionPayload = z.object({ score_threshold: z.number(), }) +export const zAnyJsonValue = z.unknown() + +/** + * AgentThought + */ +export const zAgentThought = z.object({ + answer: z.string().nullish(), + chain_id: z.string().nullish(), + created_at: z.int().nullish(), + files: z.array(z.string()), + id: z.string(), + message_id: z.string(), + observation: z.string().nullish(), + position: z.int(), + thought: z.string().nullish(), + tool: z.string().nullish(), + tool_input: z.string().nullish(), + tool_labels: zAnyJsonValue, +}) + /** * AppFeedbackResponse */ @@ -312,7 +332,7 @@ export const zChildChunkResponse = z.object({ id: z.string(), position: z.int(), segment_id: z.string(), - type: z.string(), + type: z.enum(['automatic', 'customized']), updated_at: z.int(), word_count: z.int(), }) @@ -597,7 +617,7 @@ export const zDatasetBoundTagListResponse = z.object({ export const zDatasetDocMetadataResponse = z.object({ id: z.string(), name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -659,7 +679,7 @@ export const zDatasetMetadataActionResponse = z.object({ */ export const zDatasetMetadataBuiltInFieldResponse = z.object({ name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -676,7 +696,7 @@ export const zDatasetMetadataListItemResponse = z.object({ count: z.int().optional().default(0), id: z.string(), name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -693,7 +713,7 @@ export const zDatasetMetadataListResponse = z.object({ export const zDatasetMetadataResponse = z.object({ id: z.string(), name: z.string(), - type: z.string(), + type: z.enum(['number', 'string', 'time']), }) /** @@ -741,116 +761,6 @@ export const zDatasetWeightedScoreResponse = z.object({ weight_type: z.string().nullish(), }) -/** - * DatasetRetrievalModelResponse - */ -export const zDatasetRetrievalModelResponse = z.object({ - reranking_enable: z.boolean(), - reranking_mode: z.string().nullish(), - reranking_model: zDatasetRerankingModelResponse.optional(), - score_threshold: z.number().nullish(), - score_threshold_enabled: z.boolean(), - search_method: z.string(), - top_k: z.int(), - weights: zDatasetWeightedScoreResponse.nullish(), -}) - -/** - * DatasetDetailResponse - */ -export const zDatasetDetailResponse = z.object({ - app_count: z.int(), - author_name: z.string().nullable(), - built_in_field_enabled: z.boolean(), - chunk_structure: z.string().nullable(), - created_at: z.int(), - created_by: z.string(), - data_source_type: z.string().nullable(), - description: z.string().nullable(), - doc_form: z.string().nullable(), - doc_metadata: z.array(zDatasetDocMetadataResponse), - document_count: z.int(), - embedding_available: z.boolean().nullish(), - embedding_model: z.string().nullable(), - embedding_model_provider: z.string().nullable(), - enable_api: z.boolean(), - external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), - external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), - icon_info: zDatasetIconInfoResponse.optional(), - id: z.string(), - indexing_technique: z.string().nullable(), - is_multimodal: z.boolean(), - is_published: z.boolean(), - maintainer: z.string().nullish(), - name: z.string(), - permission: z.string(), - pipeline_id: z.string().nullable(), - provider: z.string(), - retrieval_model_dict: zDatasetRetrievalModelResponse, - runtime_mode: z.string().nullable(), - summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), - tags: z.array(zDatasetTagResponse), - total_available_documents: z.int(), - total_documents: z.int(), - updated_at: z.int(), - updated_by: z.string().nullable(), - word_count: z.int(), -}) - -/** - * DatasetDetailWithPartialMembersResponse - */ -export const zDatasetDetailWithPartialMembersResponse = z.object({ - app_count: z.int(), - author_name: z.string().nullable(), - built_in_field_enabled: z.boolean(), - chunk_structure: z.string().nullable(), - created_at: z.int(), - created_by: z.string(), - data_source_type: z.string().nullable(), - description: z.string().nullable(), - doc_form: z.string().nullable(), - doc_metadata: z.array(zDatasetDocMetadataResponse), - document_count: z.int(), - embedding_available: z.boolean().nullish(), - embedding_model: z.string().nullable(), - embedding_model_provider: z.string().nullable(), - enable_api: z.boolean(), - external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), - external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), - icon_info: zDatasetIconInfoResponse.optional(), - id: z.string(), - indexing_technique: z.string().nullable(), - is_multimodal: z.boolean(), - is_published: z.boolean(), - maintainer: z.string().nullish(), - name: z.string(), - partial_member_list: z.array(z.string()).nullish(), - permission: z.string(), - pipeline_id: z.string().nullable(), - provider: z.string(), - retrieval_model_dict: zDatasetRetrievalModelResponse, - runtime_mode: z.string().nullable(), - summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), - tags: z.array(zDatasetTagResponse), - total_available_documents: z.int(), - total_documents: z.int(), - updated_at: z.int(), - updated_by: z.string().nullable(), - word_count: z.int(), -}) - -/** - * DatasetListResponse - */ -export const zDatasetListResponse = z.object({ - data: z.array(zDatasetDetailResponse), - has_more: z.boolean(), - limit: z.int(), - page: z.int(), - total: z.int(), -}) - /** * DatasourceCredentialInfoResponse */ @@ -930,8 +840,8 @@ export const zDocumentListQuery = z.object({ export const zDocumentMetadataResponse = z.object({ id: z.string(), name: z.string(), - type: z.string(), - value: z.union([z.string(), z.int(), z.number(), z.boolean()]).nullish(), + type: z.enum(['number', 'string', 'time']), + value: z.union([z.string(), z.int(), z.number()]).nullable(), }) /** @@ -973,55 +883,6 @@ export const zDocumentDetailResponse = z.object({ updated_at: z.int().nullish(), }) -/** - * DocumentResponse - */ -export const zDocumentResponse = z.object({ - archived: z.boolean().nullish(), - created_at: z.int().nullish(), - created_by: z.string().nullish(), - created_from: z.string().nullish(), - data_source_detail_dict: z.record(z.string(), z.unknown()), - data_source_info: z.record(z.string(), z.unknown()).nullish(), - data_source_type: z.string().nullish(), - dataset_process_rule_id: z.string().nullish(), - disabled_at: z.int().nullish(), - disabled_by: z.string().nullish(), - display_status: z.string().nullish(), - doc_form: z.string().nullish(), - doc_metadata: z.array(zDocumentMetadataResponse).optional(), - enabled: z.boolean().nullish(), - error: z.string().nullish(), - hit_count: z.int().nullish(), - id: z.string(), - indexing_status: z.string().nullish(), - name: z.string(), - need_summary: z.boolean().nullish(), - position: z.int().nullish(), - summary_index_status: z.string().nullish(), - tokens: z.int().nullish(), - word_count: z.int().nullish(), -}) - -/** - * DocumentAndBatchResponse - */ -export const zDocumentAndBatchResponse = z.object({ - batch: z.string(), - document: zDocumentResponse, -}) - -/** - * DocumentListResponse - */ -export const zDocumentListResponse = z.object({ - data: z.array(zDocumentResponse), - has_more: z.boolean(), - limit: z.int(), - page: z.int(), - total: z.int(), -}) - /** * DocumentStatusPayload */ @@ -1037,10 +898,19 @@ export const zDocumentStatusResponse = z.object({ completed_at: z.int().nullable(), completed_segments: z.int().nullish(), error: z.string().nullable(), - error_code: z.string().nullish(), + error_code: z.literal('vector_space_estimate_exceeded').nullish(), estimated_vector_space_mb: z.int().nullish(), id: z.string(), - indexing_status: z.string(), + indexing_status: z.enum([ + 'cleaning', + 'completed', + 'error', + 'indexing', + 'parsing', + 'paused', + 'splitting', + 'waiting', + ]), parsing_completed_at: z.int().nullable(), paused_at: z.int().nullable(), processing_started_at: z.int().nullable(), @@ -1057,6 +927,28 @@ export const zDocumentStatusListResponse = z.object({ data: z.array(zDocumentStatusResponse), }) +/** + * DocumentUploadFileDetailResponse + */ +export const zDocumentUploadFileDetailResponse = z.object({ + created_at: z.number(), + created_by: z.string(), + extension: z.string(), + id: z.string(), + mime_type: z.string().nullish(), + name: z.string(), + size: z.int(), +}) + +/** + * DocumentSourceDetailResponse + * + * The uploaded file for file documents; Notion and website documents repeat their source info. + */ +export const zDocumentSourceDetailResponse = z.object({ + upload_file: zDocumentUploadFileDetailResponse.nullish(), +}) + /** * EndUserDetail * @@ -1451,16 +1343,6 @@ export const zHumanInputFormSubmittedError = zErrorResponse.and( }), ) -/** - * I18nObject - * - * Model class for i18n object. - */ -export const zI18nObject = z.object({ - en_US: z.string(), - zh_Hans: z.string().nullish(), -}) - /** * IndexInfoResponse */ @@ -1470,6 +1352,11 @@ export const zIndexInfoResponse = z.object({ welcome: z.string(), }) +/** + * IndexTechniqueType + */ +export const zIndexTechniqueType = z.enum(['economy', 'high_quality']) + /** * InvalidActionError * @@ -1574,29 +1461,58 @@ export const zChatBlockingResponse = z.discriminatedUnion('event', [ zChatPausedBlockingResponse.extend({ event: z.literal('workflow_paused') }), ]) +export const zJsonValueType = z.unknown() + export const zJsonValue = z.unknown() /** - * AgentThought + * DocumentResponse */ -export const zAgentThought = z.object({ - answer: z.string().nullish(), - chain_id: z.string().nullish(), +export const zDocumentResponse = z.object({ + archived: z.boolean().nullish(), created_at: z.int().nullish(), - files: z.array(z.string()), + created_by: z.string().nullish(), + created_from: z.string().nullish(), + data_source_detail_dict: zDocumentSourceDetailResponse, + data_source_info: z.record(z.string(), zJsonValue).nullish(), + data_source_type: z.string().nullish(), + dataset_process_rule_id: z.string().nullish(), + disabled_at: z.int().nullish(), + disabled_by: z.string().nullish(), + display_status: z.string().nullish(), + doc_form: z.string().nullish(), + doc_metadata: z.array(zDocumentMetadataResponse).optional(), + enabled: z.boolean().nullish(), + error: z.string().nullish(), + hit_count: z.int().nullish(), id: z.string(), - message_id: z.string(), - observation: z.string().nullish(), - position: z.int(), - thought: z.string().nullish(), - tool: z.string().nullish(), - tool_input: z.string().nullish(), - tool_labels: zJsonValue, + indexing_status: z.string().nullish(), + name: z.string(), + need_summary: z.boolean().nullish(), + position: z.int().nullish(), + summary_index_status: z.string().nullish(), + tokens: z.int().nullish(), + word_count: z.int().nullish(), }) -export const zJsonValueType = z.unknown() +/** + * DocumentAndBatchResponse + */ +export const zDocumentAndBatchResponse = z.object({ + batch: z.string(), + document: zDocumentResponse, +}) -export const zJsonValue2 = z.unknown() +/** + * DocumentListResponse + */ +export const zDocumentListResponse = z.object({ + data: z.array(zDocumentResponse), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) /** * HumanInputFormSubmissionData @@ -1607,7 +1523,7 @@ export const zHumanInputFormSubmissionData = z.object({ node_id: z.string(), node_title: z.string(), rendered_content: z.string(), - submitted_data: z.record(z.string(), zJsonValue2).nullish(), + submitted_data: z.record(z.string(), zJsonValue).nullish(), }) /** @@ -1615,7 +1531,7 @@ export const zHumanInputFormSubmissionData = z.object({ */ export const zHumanInputFormSubmitPayload = z.object({ action: z.string(), - inputs: z.record(z.string(), zJsonValue2), + inputs: z.record(z.string(), zJsonValue), }) /** @@ -1623,7 +1539,7 @@ export const zHumanInputFormSubmitPayload = z.object({ */ export const zHumanInputFormSubmitPayloadWithUser = z.object({ action: z.string(), - inputs: z.record(z.string(), zJsonValue2), + inputs: z.record(z.string(), zJsonValue), user: z.string(), }) @@ -1784,6 +1700,16 @@ export const zModelPropertyKey = z.enum([ 'word_limit', ]) +/** + * I18nObject + * + * Model class for i18n object. + */ +export const zModelRuntimeI18nObject = z.object({ + en_US: z.string(), + zh_Hans: z.string().nullish(), +}) + /** * ModelStatus * @@ -1992,7 +1918,7 @@ export const zProviderModelWithStatusEntity = z.object({ features: z.array(zModelFeature).nullish(), fetch_from: zFetchFrom, has_invalid_load_balancing_configs: z.boolean().optional().default(false), - label: zI18nObject, + label: zModelRuntimeI18nObject, load_balancing_enabled: z.boolean().optional().default(false), model: z.string(), model_properties: z.record(z.string(), z.unknown()), @@ -2042,9 +1968,9 @@ export const zProviderQuotaExceededError = zErrorResponse.and( * Model class for provider with models response. */ export const zProviderWithModelsResponse = z.object({ - icon_small: zI18nObject.nullish(), - icon_small_dark: zI18nObject.nullish(), - label: zI18nObject, + icon_small: zModelRuntimeI18nObject.nullish(), + icon_small_dark: zModelRuntimeI18nObject.nullish(), + label: zModelRuntimeI18nObject, models: z.array(zProviderModelWithStatusEntity), provider: z.string(), status: zCustomConfigurationStatus, @@ -2104,6 +2030,134 @@ export const zRetrievalMethod = z.enum([ 'semantic_search', ]) +/** + * DatasetRetrievalModelResponse + */ +export const zDatasetRetrievalModelResponse = z.object({ + reranking_enable: z.boolean(), + reranking_mode: z.string().nullish(), + reranking_model: zDatasetRerankingModelResponse.optional(), + score_threshold: z.number().nullish(), + score_threshold_enabled: z.boolean(), + search_method: zRetrievalMethod, + top_k: z.int(), + weights: zDatasetWeightedScoreResponse.nullish(), +}) + +/** + * DatasetDetailResponse + */ +export const zDatasetDetailResponse = z.object({ + app_count: z.int(), + author_name: z.string().nullable(), + built_in_field_enabled: z.boolean(), + chunk_structure: z.string().nullable(), + created_at: z.int(), + created_by: z.string(), + data_source_type: z + .enum([ + 'local_file', + 'notion_import', + 'online_document', + 'online_drive', + 'upload_file', + 'website_crawl', + ]) + .nullable(), + description: z.string().nullable(), + doc_form: z.string().nullable(), + doc_metadata: z.array(zDatasetDocMetadataResponse), + document_count: z.int(), + embedding_available: z.boolean().nullish(), + embedding_model: z.string().nullable(), + embedding_model_provider: z.string().nullable(), + enable_api: z.boolean(), + external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), + external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), + icon_info: zDatasetIconInfoResponse.optional(), + id: z.string(), + indexing_technique: zIndexTechniqueType.nullable(), + is_multimodal: z.boolean(), + is_published: z.boolean(), + maintainer: z.string().nullish(), + name: z.string(), + permission: z.enum(['all_team_members', 'only_me', 'partial_members']), + pipeline_id: z.string().nullable(), + provider: z.string(), + retrieval_model_dict: zDatasetRetrievalModelResponse, + runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), + summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), + tags: z.array(zDatasetTagResponse), + total_available_documents: z.int(), + total_documents: z.int(), + updated_at: z.int(), + updated_by: z.string().nullable(), + word_count: z.int(), +}) + +/** + * DatasetDetailWithPartialMembersResponse + */ +export const zDatasetDetailWithPartialMembersResponse = z.object({ + app_count: z.int(), + author_name: z.string().nullable(), + built_in_field_enabled: z.boolean(), + chunk_structure: z.string().nullable(), + created_at: z.int(), + created_by: z.string(), + data_source_type: z + .enum([ + 'local_file', + 'notion_import', + 'online_document', + 'online_drive', + 'upload_file', + 'website_crawl', + ]) + .nullable(), + description: z.string().nullable(), + doc_form: z.string().nullable(), + doc_metadata: z.array(zDatasetDocMetadataResponse), + document_count: z.int(), + embedding_available: z.boolean().nullish(), + embedding_model: z.string().nullable(), + embedding_model_provider: z.string().nullable(), + enable_api: z.boolean(), + external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), + external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), + icon_info: zDatasetIconInfoResponse.optional(), + id: z.string(), + indexing_technique: zIndexTechniqueType.nullable(), + is_multimodal: z.boolean(), + is_published: z.boolean(), + maintainer: z.string().nullish(), + name: z.string(), + partial_member_list: z.array(z.string()).nullish(), + permission: z.enum(['all_team_members', 'only_me', 'partial_members']), + pipeline_id: z.string().nullable(), + provider: z.string(), + retrieval_model_dict: zDatasetRetrievalModelResponse, + runtime_mode: z.enum(['general', 'rag_pipeline']).nullable(), + summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), + tags: z.array(zDatasetTagResponse), + total_available_documents: z.int(), + total_documents: z.int(), + updated_at: z.int(), + updated_by: z.string().nullable(), + word_count: z.int(), +}) + +/** + * DatasetListResponse + */ +export const zDatasetListResponse = z.object({ + data: z.array(zDatasetDetailResponse), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + /** * RetrieverResource */ @@ -2202,7 +2256,7 @@ export const zSegmentResponse = z.object({ keywords: z.array(z.string()).nullable(), position: z.int(), sign_content: z.string(), - status: z.string(), + status: z.enum(['completed', 'error', 'indexing', 'paused', 're_segment', 'waiting']), stopped_at: z.int().nullable(), summary: z.string().nullable(), tokens: z.int(), @@ -2301,7 +2355,7 @@ export const zSimpleAccountResponse = z.object({ export const zSimpleConversation = z.object({ created_at: z.int().nullish(), id: z.uuid(), - inputs: z.record(z.string(), zJsonValue), + inputs: z.record(z.string(), zAnyJsonValue), introduction: z.string().nullish(), name: z.string(), status: z.string(), diff --git a/packages/contracts/generated/api/web/types.gen.ts b/packages/contracts/generated/api/web/types.gen.ts index e0f67332c0d02a..8cfa2e0e530c8f 100644 --- a/packages/contracts/generated/api/web/types.gen.ts +++ b/packages/contracts/generated/api/web/types.gen.ts @@ -39,9 +39,11 @@ export type AgentThought = { thought?: string | null tool?: string | null tool_input?: string | null - tool_labels: JsonValue + tool_labels: AnyJsonValue } +export type AnyJsonValue = unknown + export type AppAccessModeQuery = { appCode?: string | null appId?: string | null @@ -306,7 +308,7 @@ export type FormInputConfig = type: 'file-list' } & FileListInputConfig) -export type GeneratedAppResponse = JsonValue +export type GeneratedAppResponse = ShallowJsonValue export type HumanInputContent = { form_definition?: HumanInputFormDefinition | null @@ -358,14 +360,14 @@ export type HumanInputFormSubmissionData = { node_title: string rendered_content: string submitted_data?: { - [key: string]: JsonValue2 + [key: string]: JsonValue } | null } export type HumanInputFormSubmitPayload = { action: string inputs: { - [key: string]: JsonValue2 + [key: string]: JsonValue } } @@ -408,20 +410,9 @@ export type InvalidUploadTokenError = ErrorResponse & { status: 403 } -export type JsonValue = - | string - | number - | number - | boolean - | { - [key: string]: unknown - } - | Array - | null - export type JsonValueType = unknown -export type JsonValue2 = unknown +export type JsonValue = unknown export type LicenseStatus = 'active' | 'expired' | 'expiring' | 'inactive' | 'lost' | 'none' @@ -992,11 +983,22 @@ export type SelectInputConfig = { type?: 'select' } +export type ShallowJsonValue = + | string + | number + | number + | boolean + | { + [key: string]: unknown + } + | Array + | null + export type SimpleConversation = { created_at?: number | null id: string inputs: { - [key: string]: JsonValue + [key: string]: AnyJsonValue } introduction?: string | null name: string @@ -1248,7 +1250,7 @@ export type WorkflowRunPayload = { } } -export type GeneratedAppResponseWritable = JsonValue +export type GeneratedAppResponseWritable = ShallowJsonValue export type HumanInputFormSubmitResponseWritable = { [key: string]: unknown diff --git a/packages/contracts/generated/api/web/zod.gen.ts b/packages/contracts/generated/api/web/zod.gen.ts index 24a2a4a27a368c..a886560a00e0fe 100644 --- a/packages/contracts/generated/api/web/zod.gen.ts +++ b/packages/contracts/generated/api/web/zod.gen.ts @@ -24,6 +24,26 @@ export const zAccessTokenResultResponse = z.object({ result: z.string(), }) +export const zAnyJsonValue = z.unknown() + +/** + * AgentThought + */ +export const zAgentThought = z.object({ + answer: z.string().nullish(), + chain_id: z.string().nullish(), + created_at: z.int().nullish(), + files: z.array(z.string()), + id: z.string(), + message_id: z.string(), + observation: z.string().nullish(), + position: z.int(), + thought: z.string().nullish(), + tool: z.string().nullish(), + tool_input: z.string().nullish(), + tool_labels: zAnyJsonValue, +}) + /** * AppAccessModeQuery */ @@ -563,43 +583,9 @@ export const zInvalidUploadTokenError = zErrorResponse.and( }), ) -export const zJsonValue = z - .union([ - z.string(), - z.int(), - z.number(), - z.boolean(), - z.record(z.string(), z.unknown()), - z.array(z.unknown()), - ]) - .nullable() - -/** - * AgentThought - */ -export const zAgentThought = z.object({ - answer: z.string().nullish(), - chain_id: z.string().nullish(), - created_at: z.int().nullish(), - files: z.array(z.string()), - id: z.string(), - message_id: z.string(), - observation: z.string().nullish(), - position: z.int(), - thought: z.string().nullish(), - tool: z.string().nullish(), - tool_input: z.string().nullish(), - tool_labels: zJsonValue, -}) - -/** - * GeneratedAppResponse - */ -export const zGeneratedAppResponse = zJsonValue - export const zJsonValueType = z.unknown() -export const zJsonValue2 = z.unknown() +export const zJsonValue = z.unknown() /** * HumanInputFormSubmissionData @@ -610,7 +596,7 @@ export const zHumanInputFormSubmissionData = z.object({ node_id: z.string(), node_title: z.string(), rendered_content: z.string(), - submitted_data: z.record(z.string(), zJsonValue2).nullish(), + submitted_data: z.record(z.string(), zJsonValue).nullish(), }) /** @@ -618,7 +604,7 @@ export const zHumanInputFormSubmissionData = z.object({ */ export const zHumanInputFormSubmitPayload = z.object({ action: z.string(), - inputs: z.record(z.string(), zJsonValue2), + inputs: z.record(z.string(), zJsonValue), }) /** @@ -1026,13 +1012,29 @@ export const zSavedMessageListQuery = z.object({ limit: z.int().gte(1).lte(100).optional().default(20), }) +export const zShallowJsonValue = z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.record(z.string(), z.unknown()), + z.array(z.unknown()), + ]) + .nullable() + +/** + * GeneratedAppResponse + */ +export const zGeneratedAppResponse = zShallowJsonValue + /** * SimpleConversation */ export const zSimpleConversation = z.object({ created_at: z.int().nullish(), id: z.uuid(), - inputs: z.record(z.string(), zJsonValue), + inputs: z.record(z.string(), zAnyJsonValue), introduction: z.string().nullish(), name: z.string(), status: z.string(), @@ -1861,7 +1863,7 @@ export const zWorkflowRunPayload = z.object({ /** * GeneratedAppResponse */ -export const zGeneratedAppResponseWritable = zJsonValue +export const zGeneratedAppResponseWritable = zShallowJsonValue /** * HumanInputFormSubmitResponse diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2da0ca8f170f56..04dc2cf3ca4ea0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -374,9 +374,6 @@ catalogs: '@types/picomatch': specifier: 4.0.3 version: 4.0.3 - '@types/qs': - specifier: 6.15.1 - version: 6.15.1 '@types/react': specifier: 19.3.0 version: 19.3.0 @@ -647,9 +644,6 @@ catalogs: qrcode.react: specifier: 4.2.0 version: 4.2.0 - qs: - specifier: 6.16.0 - version: 6.16.0 react: specifier: 19.3.0 version: 19.3.0 @@ -1600,9 +1594,6 @@ importers: qrcode.react: specifier: 'catalog:' version: 4.2.0(react@19.3.0) - qs: - specifier: 'catalog:' - version: 6.16.0 react: specifier: 'catalog:' version: 19.3.0 @@ -1763,9 +1754,6 @@ importers: '@types/node': specifier: 'catalog:' version: 25.9.9 - '@types/qs': - specifier: 'catalog:' - version: 6.15.1 '@types/react': specifier: 'catalog:' version: 19.3.0 @@ -3171,10 +3159,6 @@ packages: resolution: {integrity: sha512-Ouw1fMwjAy1V4MpnDASfu1DCPgkP0nNFteiiWbFoEGSqa7Vnmkb6if2c522N2WcMk+RuaaabQbC1F1D4/kTXcg==} engines: {node: '>=12.4.0'} - '@nolyfill/side-channel@1.0.44': - resolution: {integrity: sha512-y3SvzjuY1ygnzWA4Krwx/WaJAsTMP11DN+e21A8Fa8PW1oDtVB5NSRW7LWurAiS2oKRkuCgcjTYMkBuBkcPCRg==} - engines: {node: '>=12.4.0'} - '@orpc/client@1.15.5': resolution: {integrity: sha512-0usheWO9w5HRHG20+nGB5AJlOQfea/fEmOX1KKHneBTkLpFTBSTkyJSqAN6ugJbhWm8BKVs9caimFQr8vcI0Ig==} @@ -4939,9 +4923,6 @@ packages: '@types/picomatch@4.0.3': resolution: {integrity: sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==} - '@types/qs@6.15.1': - resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} - '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} peerDependencies: @@ -6441,10 +6422,6 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -8322,10 +8299,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - qs@6.16.0: - resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} - engines: {node: '>=0.6'} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -10973,8 +10946,6 @@ snapshots: '@nolyfill/safer-buffer@1.0.44': {} - '@nolyfill/side-channel@1.0.44': {} - '@orpc/client@1.15.5': dependencies: '@orpc/shared': 1.15.5 @@ -12342,8 +12313,6 @@ snapshots: '@types/picomatch@4.0.3': {} - '@types/qs@6.15.1': {} - '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: '@types/react': 19.3.0 @@ -13783,8 +13752,6 @@ snapshots: dependencies: stackframe: 1.3.4 - es-define-property@1.0.1: {} - es-errors@1.3.0: {} es-module-lexer@1.7.0: {} @@ -16072,11 +16039,6 @@ snapshots: dependencies: react: 19.3.0 - qs@6.16.0: - dependencies: - es-define-property: 1.0.1 - side-channel: '@nolyfill/side-channel@1.0.44' - quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -17577,7 +17539,6 @@ time: '@types/negotiator@0.6.5': '2026-08-11T07:26:20.690Z' '@types/node@25.9.9': '2026-10-01T22:39:33.611Z' '@types/picomatch@4.0.3': '2026-04-03T00:13:45.392Z' - '@types/qs@6.15.1': '2026-05-06T23:46:01.024Z' '@types/react-dom@19.3.0': '2026-09-09T18:07:51.886Z' '@types/react@19.3.0': '2026-09-09T18:08:49.750Z' '@types/sortablejs@1.15.9': '2025-10-24T04:31:45.132Z' @@ -17671,7 +17632,6 @@ time: playwright@1.64.0: '2026-10-07T20:42:55.791Z' postcss@8.5.29: '2026-10-05T09:28:55.780Z' qrcode.react@4.2.0: '2024-12-11T17:22:40.569Z' - qs@6.16.0: '2026-08-29T23:50:15.803Z' react-dom@19.3.0: '2026-09-09T17:17:39.744Z' react-easy-crop@6.2.4: '2026-10-06T06:35:05.433Z' react-i18next@17.0.16: '2026-10-06T12:19:42.259Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 43b1ee8d5ac933..a1ab8dd54049d8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -138,7 +138,6 @@ catalog: '@types/negotiator': 0.6.5 '@types/node': 25.9.9 '@types/picomatch': 4.0.3 - '@types/qs': 6.15.1 '@types/react': 19.3.0 '@types/react-dom': 19.3.0 '@types/sortablejs': 1.15.9 @@ -230,7 +229,6 @@ catalog: playwright: 1.64.0 postcss: 8.5.29 qrcode.react: 4.2.0 - qs: 6.16.0 react: 19.3.0 react-dom: 19.3.0 react-easy-crop: 6.2.4 diff --git a/web/__tests__/api-key-management.spec.tsx b/web/__tests__/api-key-management.spec.tsx index 140d82ff6f14ec..0c34726d3be8fa 100644 --- a/web/__tests__/api-key-management.spec.tsx +++ b/web/__tests__/api-key-management.spec.tsx @@ -8,6 +8,7 @@ import { ApiSecretKeyButton } from '@/app/components/app/access-point/built-in-a import { EnvironmentApiKeyButton } from '@/app/components/app/access-point/deployed-environment-access-points/environment-api-key-button' import { ServiceApi } from '@/app/components/datasets/extra-info/service-api' import { render } from '@/test/console/render' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { createTestQueryClient } from '@/test/query-client' vi.mock('@/context/permission-state', async () => { @@ -46,6 +47,14 @@ vi.mock('@/service/console/browser', () => ({ return apiMocks.createApp(input) case 'apps.byResourceId.apiKeys.byApiKeyId.delete': return apiMocks.deleteApp(input) + case 'datasets.get': { + const keyword = (input as { query?: { keyword?: string } }).query?.keyword ?? '' + const data = [ + createDatasetFixture({ id: 'engineering', name: 'Engineering' }), + createDatasetFixture({ id: 'support', name: 'Support' }), + ].filter((dataset) => dataset.name.toLowerCase().includes(keyword.toLowerCase())) + return { data, has_more: false, limit: 20, page: 1, total: data.length } + } case 'datasets.apiKeys.get': await apiMocks.listDataset() return { data: apiMocks.datasetKeys } @@ -67,21 +76,6 @@ vi.mock('@/service/console/browser', () => ({ }, })) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInfiniteDatasets: ({ keyword = '' }: { keyword?: string }) => ({ - data: { - pages: [ - { - data: [ - { id: 'engineering', name: 'Engineering' }, - { id: 'support', name: 'Support' }, - ].filter((dataset) => dataset.name.toLowerCase().includes(keyword.toLowerCase())), - }, - ], - }, - }), -})) - const mockCurrentWorkspace = vi.fn().mockReturnValue({ id: 'workspace-1', name: 'Test Workspace', diff --git a/web/__tests__/base/notion-page-selector-flow.test.tsx b/web/__tests__/base/notion-page-selector-flow.test.tsx index 02f0fdbdd0e43c..5fdf9ed68bf790 100644 --- a/web/__tests__/base/notion-page-selector-flow.test.tsx +++ b/web/__tests__/base/notion-page-selector-flow.test.tsx @@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import NotionPageSelector from '@/app/components/base/notion-page-selector/base' import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types' +import { consoleQuery } from '@/service/console' const mockInvalidPreImportNotionPages = vi.fn() const mockSetSettingsDestination = vi.fn() @@ -22,11 +23,25 @@ vi.mock('@tanstack/react-virtual', () => ({ }), })) -vi.mock('@/service/knowledge/use-import', () => ({ - usePreImportNotionPages: (params: { datasetId: string; credentialId: string }) => - mockUsePreImportNotionPages(params), - useInvalidPreImportNotionPages: () => mockInvalidPreImportNotionPages, -})) +// The selector reads normalized workspaces from the pre-import pages query and +// invalidates that query when the credential changes. +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useQuery: (options: { + queryKey: [unknown, { input: { query: { dataset_id: string; credential_id: string } } }] + }) => { + const { query } = options.queryKey[1].input + const result = mockUsePreImportNotionPages({ + datasetId: query.dataset_id, + credentialId: query.credential_id, + }) + return { ...result, data: result.data?.notion_info } + }, + useQueryClient: () => ({ invalidateQueries: mockInvalidPreImportNotionPages }), + } +}) vi.mock('nuqs', async (importOriginal) => { const actual = await importOriginal() @@ -180,8 +195,9 @@ describe('Base Notion Page Selector Flow', () => { await user.click(screen.getByRole('option', { name: /Workspace 2/ })) expect(mockInvalidPreImportNotionPages).toHaveBeenCalledWith({ - datasetId: 'dataset-1', - credentialId: 'c2', + queryKey: consoleQuery.notion.preImport.pages.get.queryKey({ + input: { query: { dataset_id: 'dataset-1', credential_id: 'c2' } }, + }), }) expect(onSelect).toHaveBeenCalledWith([]) diff --git a/web/__tests__/file-supported-formats.spec.tsx b/web/__tests__/file-supported-formats.spec.tsx index 997e5c5a28eb8d..2fe8d812d92314 100644 --- a/web/__tests__/file-supported-formats.spec.tsx +++ b/web/__tests__/file-supported-formats.spec.tsx @@ -9,6 +9,15 @@ import { LanguagesSupported } from '@/i18n/language' import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ + data: { file_size_limit: 15, batch_count_limit: 5, file_upload_limit: 10 }, + }), + }) +}) + let mockLocale = 'en-US' const request = vi.hoisted(() => vi.fn<(url: string) => Promise>()) @@ -17,12 +26,6 @@ vi.mock('@/service/base', async (importOriginal) => ({ request, })) -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ - data: { file_size_limit: 15, batch_count_limit: 5, file_upload_limit: 10 }, - }), -})) - vi.mock('#i18n', async (importOriginal) => ({ ...(await importOriginal()), useLocale: () => mockLocale, diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx index 14f417ca6ab8d2..ece92fc09d44e5 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx @@ -1,6 +1,5 @@ import { screen, waitFor } from '@testing-library/react' import { createHttpError } from '@/service/http-error' -import { useDatasetDetail } from '@/service/knowledge/use-dataset' import { renderWithConsoleQuery } from '@/test/console/query-data' import { DatasetACLPermission } from '@/utils/permission' import CreateDocumentsPage from '../documents/create/page' @@ -22,9 +21,7 @@ const render = (ui: Parameters[0]) => vi.mock('@/next/navigation', () => mockNavigation) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useDatasetDetail: vi.fn(), -})) +const { mockDatasetDetailQuery } = vi.hoisted(() => ({ mockDatasetDetailQuery: vi.fn() })) vi.mock('nuqs', async (importOriginal) => ({ ...(await importOriginal()), @@ -35,13 +32,17 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () useDefaultModel: () => ({ data: undefined }), })) -vi.mock('@/service/use-datasource', () => ({ - useGetDefaultDataSourceListAuth: () => ({ - data: { result: [] }, - isLoading: false, - isError: false, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.get': mockDatasetDetailQuery, + 'auth.plugin.datasource.defaultList.get': () => ({ + data: { result: [] }, + isLoading: false, + isError: false, + }), + }) +}) vi.mock('@/app/components/datasets/create/step-one', () => ({ default: () =>
Create knowledge content
, @@ -79,7 +80,6 @@ vi.mock('@/context/event-emitter', () => ({ const mockUseRouter = mockNavigation.useRouter const mockUsePathname = mockNavigation.usePathname -const mockUseDatasetDetail = vi.mocked(useDatasetDetail) describe('DatasetDetailLayout', () => { beforeEach(() => { @@ -104,7 +104,7 @@ describe('DatasetDetailLayout', () => { ['/datasets/dataset-1/api', 'common.appMenus.apiAccess'], ])('identifies the current detail page for %s', async (pathname, pageTitle) => { mockUsePathname.mockReturnValue(pathname) - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -115,7 +115,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) render( @@ -133,7 +133,7 @@ describe('DatasetDetailLayout', () => { '/datasets/dataset-1/documents/document-1/settings', ])('delegates the document title for %s to the document page', (pathname) => { mockUsePathname.mockReturnValue(pathname) - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -144,7 +144,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) render( @@ -157,7 +157,7 @@ describe('DatasetDetailLayout', () => { it('keeps the dataset title when the document creation route is composed', async () => { mockUsePathname.mockReturnValue('/datasets/dataset-1/documents/create') - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -168,7 +168,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) const page = await CreateDocumentsPage({ params: Promise.resolve({ datasetId: 'dataset-1' }), }) @@ -187,11 +187,11 @@ describe('DatasetDetailLayout', () => { 'should redirect to datasets page when dataset detail returns %s', async (status) => { // Arrange - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: undefined, error: createHttpError(new Response(null, { status })), refetch: vi.fn(), - } as unknown as ReturnType) + }) // Act render( @@ -212,7 +212,7 @@ describe('DatasetDetailLayout', () => { describe('Rendering', () => { it('should render children when dataset detail is available', () => { // Arrange - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -222,7 +222,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) // Act render( @@ -239,7 +239,7 @@ describe('DatasetDetailLayout', () => { it('should render document creation route content without owning the main skip target', () => { // Arrange mockUsePathname.mockReturnValue('/datasets/dataset-1/documents/create') - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -249,7 +249,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) // Act render( @@ -268,7 +268,7 @@ describe('DatasetDetailLayout', () => { it('should redirect from hit testing when retrieval recall permission is missing', async () => { // Arrange mockUsePathname.mockReturnValue('/datasets/dataset-1/hitTesting') - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -279,7 +279,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) // Act render( @@ -298,7 +298,7 @@ describe('DatasetDetailLayout', () => { it('should redirect from access config when access config permission is missing', async () => { // Arrange mockUsePathname.mockReturnValue('/datasets/dataset-1/access-config') - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -309,7 +309,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) // Act render( @@ -328,7 +328,7 @@ describe('DatasetDetailLayout', () => { it('should render access config when access config permission is granted', () => { // Arrange mockUsePathname.mockReturnValue('/datasets/dataset-1/access-config') - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -339,7 +339,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) // Act render( @@ -357,7 +357,7 @@ describe('DatasetDetailLayout', () => { // Arrange mockIsRbacEnabled = false mockUsePathname.mockReturnValue('/datasets/dataset-1/access-config') - mockUseDatasetDetail.mockReturnValue({ + mockDatasetDetailQuery.mockReturnValue({ data: { id: 'dataset-1', name: 'Dataset 1', @@ -368,7 +368,7 @@ describe('DatasetDetailLayout', () => { }, error: null, refetch: vi.fn(), - } as unknown as ReturnType) + }) // Act render( diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index aa434d00f23d08..d0ec9948e1ee94 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import type { DataSet } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useEffect } from 'react' @@ -19,8 +19,8 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useDocumentTitle from '@/hooks/use-document-title' import { usePathname, useRouter } from '@/next/navigation' +import { consoleQuery } from '@/service/console' import { getHttpErrorStatus } from '@/service/http-error' -import { useDatasetDetail } from '@/service/knowledge/use-dataset' import { getDatasetACLCapabilities } from '@/utils/permission' type IAppDetailLayoutProps = { @@ -96,7 +96,16 @@ const DatasetDetailLayout: FC = (props) => { }) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) - const { data: datasetRes, error, refetch: mutateDatasetRes } = useDatasetDetail(datasetId) + const { + data: datasetRes, + error, + refetch: mutateDatasetRes, + } = useQuery( + consoleQuery.datasets.byDatasetId.get.queryOptions({ + input: { params: { dataset_id: datasetId } }, + enabled: !!datasetId, + }), + ) const shouldRedirect = shouldRedirectToDatasetList(error) const datasetACLCapabilities = React.useMemo( () => @@ -158,7 +167,7 @@ const DatasetDetailLayout: FC = (props) => { > $['error.registrationNotAllowed'], { ns: 'login' })) - } else { - toast.error(res.data) } } catch (error) { console.error(error) diff --git a/web/app/account/(commonLayout)/account-page/__tests__/email-change-modal.spec.tsx b/web/app/account/(commonLayout)/account-page/__tests__/email-change-modal.spec.tsx index 7e6c2ceaa03985..c8c6c0489f7e89 100644 --- a/web/app/account/(commonLayout)/account-page/__tests__/email-change-modal.spec.tsx +++ b/web/app/account/(commonLayout)/account-page/__tests__/email-change-modal.spec.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { toast } from '@/app/notifications' -import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common' +import { consoleClient } from '@/service/console' import { createHttpError } from '@/service/http-error' import EmailChangeModal from '../email-change-modal' @@ -11,11 +11,24 @@ vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn() }, })) -vi.mock('@/service/common', () => ({ - checkEmailExisted: vi.fn(), - resetEmail: vi.fn(), - sendVerifyCode: vi.fn(), - verifyEmail: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + account: { + changeEmail: { + checkEmailUnique: { + post: vi.fn(), + }, + reset: { + post: vi.fn(), + }, + post: vi.fn(), + validity: { + post: vi.fn(), + }, + }, + }, + }, })) vi.mock('@/service/use-common', () => ({ @@ -25,14 +38,18 @@ vi.mock('@/service/use-common', () => ({ describe('EmailChangeModal', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(sendVerifyCode).mockResolvedValue({ result: 'success', data: 'email-token' }) - vi.mocked(verifyEmail).mockResolvedValue({ + vi.mocked(consoleClient.account.changeEmail.post).mockResolvedValue({ result: 'success', + data: 'email-token', + }) + vi.mocked(consoleClient.account.changeEmail.validity.post).mockResolvedValue({ email: 'old@example.com', is_valid: true, token: 'verified-token', }) - vi.mocked(checkEmailExisted).mockResolvedValue({ result: 'success' }) + vi.mocked(consoleClient.account.changeEmail.checkEmailUnique.post).mockResolvedValue({ + result: 'success', + }) }) it('submits the labeled verification fields with Enter through both email phases', async () => { @@ -46,10 +63,12 @@ describe('EmailChangeModal', () => { name: 'accountSettings.account.changeEmail.codeLabel', }) await user.type(originCode, '123456{Enter}') - expect(verifyEmail).toHaveBeenCalledWith({ - email: 'old@example.com', - code: '123456', - token: 'email-token', + expect(consoleClient.account.changeEmail.validity.post).toHaveBeenCalledWith({ + body: { + email: 'old@example.com', + code: '123456', + token: 'email-token', + }, }) const email = await screen.findByRole('textbox', { @@ -62,10 +81,12 @@ describe('EmailChangeModal', () => { ).toBeEnabled(), ) await user.type(email, '{Enter}') - expect(sendVerifyCode).toHaveBeenLastCalledWith({ - email: 'new@example.com', - phase: 'new_email', - token: 'verified-token', + expect(consoleClient.account.changeEmail.post).toHaveBeenLastCalledWith({ + body: { + email: 'new@example.com', + phase: 'new_email', + token: 'verified-token', + }, }) const newCode = await screen.findByRole('textbox', { @@ -74,16 +95,18 @@ describe('EmailChangeModal', () => { expect(newCode).toHaveValue('') await user.type(newCode, '654321{Enter}') await waitFor(() => - expect(resetEmail).toHaveBeenCalledWith({ - new_email: 'new@example.com', - token: 'verified-token', + expect(consoleClient.account.changeEmail.reset.post).toHaveBeenCalledWith({ + body: { + new_email: 'new@example.com', + token: 'verified-token', + }, }), ) expect(mockLogout).toHaveBeenCalledOnce() }) it('associates an unavailable-email error and clears it when the user edits the address', async () => { - vi.mocked(checkEmailExisted).mockRejectedValue( + vi.mocked(consoleClient.account.changeEmail.checkEmailUnique.post).mockRejectedValue( createHttpError(new Response(null, { status: 400 }), { code: 'email_already_in_use' }), ) const user = userEvent.setup() @@ -112,7 +135,7 @@ describe('EmailChangeModal', () => { }) it('shows the server message when sending a verification code fails', async () => { - vi.mocked(sendVerifyCode).mockRejectedValueOnce( + vi.mocked(consoleClient.account.changeEmail.post).mockRejectedValueOnce( createHttpError(new Response(null, { status: 429 }), { code: 'rate_limit_exceeded', message: 'Try again in one minute', @@ -130,12 +153,12 @@ describe('EmailChangeModal', () => { 'Error sending verification code: Try again in one minute', ), ) - expect(verifyEmail).not.toHaveBeenCalled() + expect(consoleClient.account.changeEmail.validity.post).not.toHaveBeenCalled() expect(mockLogout).not.toHaveBeenCalled() }) it('preserves the verification step after an HTTP error and allows another attempt', async () => { - vi.mocked(verifyEmail).mockRejectedValueOnce( + vi.mocked(consoleClient.account.changeEmail.validity.post).mockRejectedValueOnce( createHttpError(new Response(null, { status: 400 }), { code: 'invalid_code', message: 'Verification code has expired', @@ -161,7 +184,7 @@ describe('EmailChangeModal', () => { expect( screen.queryByRole('textbox', { name: 'accountSettings.account.changeEmail.emailLabel' }), ).not.toBeInTheDocument() - expect(resetEmail).not.toHaveBeenCalled() + expect(consoleClient.account.changeEmail.reset.post).not.toHaveBeenCalled() await user.type(originCode, '654321{Enter}') @@ -170,10 +193,12 @@ describe('EmailChangeModal', () => { name: 'accountSettings.account.changeEmail.emailLabel', }), ).toBeVisible() - expect(verifyEmail).toHaveBeenLastCalledWith({ - email: 'old@example.com', - code: '654321', - token: 'email-token', + expect(consoleClient.account.changeEmail.validity.post).toHaveBeenLastCalledWith({ + body: { + email: 'old@example.com', + code: '654321', + token: 'email-token', + }, }) }) }) diff --git a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx index 5eef71289d9503..e9d486952e5934 100644 --- a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx +++ b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx @@ -9,7 +9,7 @@ import { useDebounceFn } from 'ahooks' import { useCallback, useEffect, useRef, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' -import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail } from '@/service/common' +import { consoleClient } from '@/service/console' import { useLogout } from '@/service/use-common' import { getErrorMessage } from '@/utils/error-message' @@ -67,10 +67,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => { const sendEmail = async (email: string, isOrigin: boolean, token?: string) => { try { - const res = await sendVerifyCode({ - email, - phase: isOrigin ? 'old_email' : 'new_email', - token, + const res = await consoleClient.account.changeEmail.post({ + body: { email, phase: isOrigin ? 'old_email' : 'new_email', token }, }) startCount() if (res.data) setStepToken(res.data) @@ -86,10 +84,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => { callback?: (token: string) => void, ) => { try { - const res = await verifyEmail({ - email, - code, - token, + const res = await consoleClient.account.changeEmail.validity.post({ + body: { email, code, token }, }) if (res.is_valid) { setStepToken(res.token) @@ -119,9 +115,10 @@ const EmailChangeModal = ({ onClose, email }: Props) => { const checkNewEmailExisted = async (email: string) => { setIsCheckingEmail(true) try { - await checkEmailExisted({ - email, - }) + await consoleClient.account.changeEmail.checkEmailUnique.post( + { body: { email } }, + { context: { silent: true } }, + ) if (latestEmailRef.current !== email) return setNewEmailExited(false) setUnAvailableEmail(false) @@ -172,9 +169,8 @@ const EmailChangeModal = ({ onClose, email }: Props) => { const { mutateAsync: logout } = useLogout() const updateEmail = async (lastToken: string) => { try { - await resetEmail({ - new_email: mail.trim(), - token: lastToken, + await consoleClient.account.changeEmail.reset.post({ + body: { new_email: mail.trim(), token: lastToken }, }) await logout() } catch (error) { diff --git a/web/app/account/(commonLayout)/delete-account/state.tsx b/web/app/account/(commonLayout)/delete-account/state.tsx index 20f78ca38df1b9..c29a285467bc3d 100644 --- a/web/app/account/(commonLayout)/delete-account/state.tsx +++ b/web/app/account/(commonLayout)/delete-account/state.tsx @@ -1,10 +1,6 @@ import { useMutation } from '@tanstack/react-query' import { create } from 'zustand' -import { - sendDeleteAccountCode, - submitDeleteAccountFeedback, - verifyDeleteAccountCode, -} from '@/service/common' +import { consoleClient } from '@/service/console' type State = { sendEmailToken: string @@ -20,7 +16,7 @@ export function useSendDeleteAccountEmail() { const updateEmailToken = useAccountDeleteStore((state) => state.setSendEmailToken) return useMutation({ mutationKey: ['delete-account'], - mutationFn: sendDeleteAccountCode, + mutationFn: () => consoleClient.account.delete.verify.get(), onSuccess: (ret) => { if (ret.result === 'success') updateEmailToken(ret.data) }, @@ -30,13 +26,15 @@ export function useSendDeleteAccountEmail() { export function useConfirmDeleteAccount() { return useMutation({ mutationKey: ['confirm-delete-account'], - mutationFn: verifyDeleteAccountCode, + mutationFn: (body: { code: string; token: string }) => + consoleClient.account.delete.post({ body }), }) } export function useDeleteAccountFeedback() { return useMutation({ mutationKey: ['delete-account-feedback'], - mutationFn: submitDeleteAccountFeedback, + mutationFn: (body: { feedback: string; email: string }) => + consoleClient.account.delete.feedback.post({ body }), }) } diff --git a/web/app/activate/activateForm.tsx b/web/app/activate/activateForm.tsx index 32e2c586a5cfff..c725cf2dd61583 100644 --- a/web/app/activate/activateForm.tsx +++ b/web/app/activate/activateForm.tsx @@ -1,12 +1,13 @@ 'use client' import { buttonVariants } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { skipToken, useQuery } from '@tanstack/react-query' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' -import { useInvitationCheck } from '@/service/use-common' +import { consoleQuery } from '@/service/console' const ActivateForm = () => { const router = useRouter() @@ -24,12 +25,11 @@ const ActivateForm = () => { token, }, } - const { data: checkRes } = useInvitationCheck( - { - ...checkParams.params, - token: token || undefined, - }, - true, + const { data: checkRes } = useQuery( + consoleQuery.activate.check.get.queryOptions({ + input: token ? { query: { ...checkParams.params, token } } : skipToken, + retry: false, + }), ) useDocumentTitle( checkRes?.is_valid === false @@ -40,9 +40,8 @@ const ActivateForm = () => { useEffect(() => { if (checkRes?.is_valid) { const params = new URLSearchParams(searchParams) - const { email, workspace_id } = checkRes.data - params.set('email', encodeURIComponent(email)) - params.set('workspace_id', encodeURIComponent(workspace_id)) + params.set('email', encodeURIComponent(checkRes.data?.email ?? '')) + params.set('workspace_id', encodeURIComponent(checkRes.data?.workspace_id ?? '')) params.set('invite_token', encodeURIComponent(token as string)) router.replace(`/signin?${params.toString()}`) } diff --git a/web/app/components/access-rules-editor/add-access-subject-menu.tsx b/web/app/components/access-rules-editor/add-access-subject-menu.tsx index 597acfb2233f41..4b0facdd7e0733 100644 --- a/web/app/components/access-rules-editor/add-access-subject-menu.tsx +++ b/web/app/components/access-rules-editor/add-access-subject-menu.tsx @@ -47,7 +47,7 @@ function AddAccessSubjectMenuContent({ data: membersData, isPending: isLoading, error, - } = useQuery(consoleQuery.workspaces.current.members.get.queryOptions()) + } = useQuery(consoleQuery.workspaces.current.members.get.queryOptions({ input: {} })) const existingAccountIdSet = new Set(existingAccountIds) const members = membersData?.accounts ?? [] diff --git a/web/app/components/api-key/dataset-scope-dialog.tsx b/web/app/components/api-key/dataset-scope-dialog.tsx index ad359f4eb705a9..1f90661e9c76d8 100644 --- a/web/app/components/api-key/dataset-scope-dialog.tsx +++ b/web/app/components/api-key/dataset-scope-dialog.tsx @@ -25,9 +25,10 @@ import { } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Radio, RadioGroup } from '@langgenius/dify-ui/radio-group' +import { useInfiniteQuery } from '@tanstack/react-query' import { useId, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useInfiniteDatasets } from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' type Scope = 'all' | 'specific' type SelectedKb = { id: string; name: string } @@ -77,9 +78,15 @@ function DatasetScopeContent({ const [selected, setSelected] = useState([]) const [keyword, setKeyword] = useState('') - const { data: datasetsPages, isLoading } = useInfiniteDatasets( - { keyword }, - { enabled: scope === 'specific' }, + const { data: datasetsPages, isLoading } = useInfiniteQuery( + consoleQuery.datasets.get.infiniteOptions({ + input: (page: number) => ({ query: { page, ...(keyword ? { keyword } : {}) } }), + getNextPageParam: (lastPage) => (lastPage.has_more ? lastPage.page + 1 : undefined), + initialPageParam: 1, + enabled: scope === 'specific', + staleTime: 0, + refetchOnMount: 'always', + }), ) const datasets = useMemo( () => diff --git a/web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx b/web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx index 889a176b9082c6..52d5bde5d8a652 100644 --- a/web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx +++ b/web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx @@ -31,10 +31,13 @@ vi.mock('@/context/permission-state', async () => { return createPermissionStateModuleMock(() => mockConsoleState.current) }) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useDatasetDetail: () => ({ data: mockDataset, refetch: vi.fn() }), - useDatasetRelatedApps: () => ({ data: mockRelatedApps }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.get': () => ({ data: mockDataset, refetch: vi.fn() }), + 'datasets.byDatasetId.relatedApps.get': () => ({ data: mockRelatedApps }), + }) +}) vi.mock('../dataset-info', () => ({ default: () =>
dataset info
, diff --git a/web/app/components/app-sidebar/dataset-detail-section.tsx b/web/app/components/app-sidebar/dataset-detail-section.tsx index fe3dbcb670e0fd..b27c7addd8941f 100644 --- a/web/app/components/app-sidebar/dataset-detail-section.tsx +++ b/web/app/components/app-sidebar/dataset-detail-section.tsx @@ -2,7 +2,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { Separator } from '@langgenius/dify-ui/separator' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -12,7 +12,7 @@ import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { usePathname } from '@/next/navigation' -import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' import { getDatasetACLCapabilities } from '@/utils/permission' import DatasetInfo from './dataset-info' import NavLink from './nav-link' @@ -37,10 +37,18 @@ const DatasetDetailSection = ({ expand = true }: DatasetDetailSectionProps) => { }) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const isRbacEnabled = systemFeatures.rbac_enabled - const { data: datasetRes, refetch: mutateDatasetRes } = useDatasetDetail(datasetId ?? '') - const { data: relatedApps } = useDatasetRelatedApps(datasetId ?? '', { - enabled: !!datasetId && !!datasetRes, - }) + const { data: datasetRes, refetch: mutateDatasetRes } = useQuery( + consoleQuery.datasets.byDatasetId.get.queryOptions({ + input: { params: { dataset_id: datasetId ?? '' } }, + enabled: !!datasetId, + }), + ) + const { data: relatedApps } = useQuery( + consoleQuery.datasets.byDatasetId.relatedApps.get.queryOptions({ + input: { params: { dataset_id: datasetId ?? '' } }, + enabled: !!datasetId && !!datasetRes, + }), + ) const datasetACLCapabilities = useMemo( () => getDatasetACLCapabilities(datasetRes?.permission_keys, { @@ -127,7 +135,7 @@ const DatasetDetailSection = ({ expand = true }: DatasetDetailSectionProps) => { return ( [0]) => }) const createDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Dataset Name', - indexing_status: 'completed', icon_info: { icon: '📙', icon_background: '#FFF4ED', @@ -52,7 +58,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 1, - total_document_count: 1, word_count: 1000, provider: 'internal', embedding_model: 'text-embedding-3', @@ -66,14 +71,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { reranking_provider_name: '', reranking_model_name: '' }, - top_k: 5, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', @@ -112,18 +109,12 @@ vi.mock('@/context/permission-state', async () => { return createPermissionStateModuleMock(() => mockConsoleState.current) }) -vi.mock('@/service/knowledge/use-dataset', () => ({ - datasetDetailQueryKeyPrefix: ['dataset', 'detail'], - useInvalidDatasetList: () => mockInvalidDatasetList, -})) - -vi.mock('@/service/use-base', () => ({ - useInvalid: () => mockInvalidDatasetDetail, -})) - -vi.mock('@/service/use-pipeline', () => ({ - useExportPipelineDSL: () => ({ mutateAsync: mockExportPipeline }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.exports.get': () => ({ mutateAsync: mockExportPipeline }), + }) +}) vi.mock('@/service/datasets', () => ({ checkIsUsedInApp: (...args: unknown[]) => mockCheckIsUsedInApp(...args), @@ -181,8 +172,8 @@ describe('Dropdown callback coverage', () => { await user.click(screen.getByText('Success')) await waitFor(() => { - expect(mockInvalidDatasetList).toHaveBeenCalled() - expect(mockInvalidDatasetDetail).toHaveBeenCalled() + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: datasetListKey }) + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: datasetDetailKey }) }) }) diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx index e02eece79f7f27..0ac8030d0d6b11 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx @@ -1,9 +1,13 @@ import type { DataSet } from '@/models/datasets' +import { QueryClient } from '@tanstack/react-query' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { isEqual } from 'es-toolkit/compat' import * as React from 'react' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { consoleQuery } from '@/service/console' import { renderWithConsoleQuery } from '@/test/console/query-data' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import { DatasetACLPermission } from '@/utils/permission' import DatasetInfo from '..' @@ -12,8 +16,8 @@ import Dropdown from '../dropdown' let mockDataset: DataSet const mockPush = vi.fn() const mockReplace = vi.fn() -const mockInvalidDatasetList = vi.fn() -const mockInvalidDatasetDetail = vi.fn() +const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries') +const datasetListKey = consoleQuery.datasets.get.key() const mockExportPipeline = vi.fn() const mockCheckIsUsedInApp = vi.fn() const mockDeleteDataset = vi.fn() @@ -33,9 +37,9 @@ const render = (ui: Parameters[0]) => }) const createDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Dataset Name', - indexing_status: 'completed', icon_info: { icon: '📙', icon_background: '#FFF4ED', @@ -52,7 +56,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 1, - total_document_count: 1, word_count: 1000, provider: 'internal', embedding_model: 'text-embedding-3', @@ -69,17 +72,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 5, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', @@ -121,20 +113,14 @@ vi.mock('@/context/permission-state', async () => { return createPermissionStateModuleMock(() => mockConsoleState.current) }) -vi.mock('@/service/knowledge/use-dataset', () => ({ - datasetDetailQueryKeyPrefix: ['dataset', 'detail'], - useInvalidDatasetList: () => mockInvalidDatasetList, -})) - -vi.mock('@/service/use-base', () => ({ - useInvalid: () => mockInvalidDatasetDetail, -})) - -vi.mock('@/service/use-pipeline', () => ({ - useExportPipelineDSL: () => ({ - mutateAsync: mockExportPipeline, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.exports.get': () => ({ + mutateAsync: mockExportPipeline, + }), + }) +}) vi.mock('@/service/datasets', () => ({ checkIsUsedInApp: (...args: unknown[]) => mockCheckIsUsedInApp(...args), @@ -326,8 +312,8 @@ describe('Dropdown', () => { // Assert await waitFor(() => { expect(mockExportPipeline).toHaveBeenCalledWith({ - pipelineId: 'pipeline-1', - include: false, + params: { pipeline_id: 'pipeline-1' }, + query: { include_secret: 'false' }, }) }) expect(createObjectURLSpy).toHaveBeenCalledTimes(1) @@ -363,7 +349,11 @@ describe('Dropdown', () => { await waitFor(() => { expect(mockDeleteDataset).toHaveBeenCalledWith('dataset-1') }) - expect(mockInvalidDatasetList).toHaveBeenCalledTimes(1) + expect( + invalidateQueries.mock.calls.filter(([filters]) => + isEqual(filters, { queryKey: datasetListKey }), + ), + ).toHaveLength(1) expect(mockReplace).toHaveBeenCalledWith('/datasets') }) diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index 2511a490422c7d..d8b73f3f3eaf32 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -17,7 +17,7 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useCallback, useState } from 'react' @@ -28,10 +28,8 @@ import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter } from '@/next/navigation' +import { consoleQuery } from '@/service/console' import { checkIsUsedInApp, deleteDataset } from '@/service/datasets' -import { datasetDetailQueryKeyPrefix, useInvalidDatasetList } from '@/service/knowledge/use-dataset' -import { useInvalid } from '@/service/use-base' -import { useExportPipelineDSL } from '@/service/use-pipeline' import { downloadBlob } from '@/utils/download' import { getErrorMessage } from '@/utils/error-message' import { getDatasetACLCapabilities } from '@/utils/permission' @@ -79,8 +77,17 @@ const DropDown = ({ expand, triggerClassName }: DropDownProps) => { datasetACLCapabilities.canAccessConfig || datasetACLCapabilities.canDelete - const invalidDatasetList = useInvalidDatasetList() - const invalidDatasetDetail = useInvalid([...datasetDetailQueryKeyPrefix, dataset.id]) + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) + const invalidDatasetDetail = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.get.queryKey({ + input: { params: { dataset_id: dataset.id } }, + }), + }) + }, [queryClient, dataset.id]) const refreshDataset = useCallback(() => { invalidDatasetList() @@ -93,7 +100,9 @@ const DropDown = ({ expand, triggerClassName }: DropDownProps) => { }) }, []) - const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL() + const { mutateAsync: exportPipelineConfig } = useMutation( + consoleQuery.rag.pipelines.byPipelineId.exports.get.mutationOptions(), + ) const handleExportPipeline = useCallback( async (include = false) => { @@ -101,8 +110,8 @@ const DropDown = ({ expand, triggerClassName }: DropDownProps) => { if (!pipeline_id) return try { const { data } = await exportPipelineConfig({ - pipelineId: pipeline_id, - include, + params: { pipeline_id }, + query: { include_secret: String(include) }, }) const file = new Blob([data], { type: 'application/yaml' }) downloadBlob({ data: file, fileName: `${name}.pipeline` }) diff --git a/web/app/components/app-sidebar/dataset-info/index.tsx b/web/app/components/app-sidebar/dataset-info/index.tsx index 32e70c8a39af61..f8c9241975cd30 100644 --- a/web/app/components/app-sidebar/dataset-info/index.tsx +++ b/web/app/components/app-sidebar/dataset-info/index.tsx @@ -3,6 +3,7 @@ import type { DataSet } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useTranslation } from 'react-i18next' +import { toChunkingMode } from '@/app/components/datasets/dataset-fields' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useKnowledge } from '@/hooks/use-knowledge' import { DOC_FORM_TEXT } from '@/models/datasets' @@ -23,6 +24,7 @@ const DatasetInfo = ({ expand }: DatasetInfoProps) => { icon_url: '', } const isExternalProvider = dataset.provider === 'external' + const chunkingMode = toChunkingMode(dataset.doc_form) const { formatIndexingTechniqueAndMethod } = useKnowledge() return ( @@ -56,10 +58,10 @@ const DatasetInfo = ({ expand }: DatasetInfoProps) => { {isExternalProvider && ( {t(($) => $.externalTag, { ns: 'dataset' })} )} - {!!(!isExternalProvider && dataset.doc_form && dataset.indexing_technique) && ( + {!!(!isExternalProvider && chunkingMode && dataset.indexing_technique) && ( <> - {t(($) => $[`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`], { + {t(($) => $[`chunkingMode.${DOC_FORM_TEXT[chunkingMode]}`], { ns: 'dataset', })} diff --git a/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx b/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx index c5582800c6943f..c14292260b3968 100644 --- a/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx @@ -11,7 +11,6 @@ import { createTestQueryClient } from '@/test/query-client' import { MCPAccessPointCard } from '../built-in-access-points/mcp-card' const mocks = vi.hoisted(() => ({ - invalidateServerDetail: vi.fn(), serverDetail: { data: undefined as undefined | { id: string; server_code: string; status: string }, isPending: false, @@ -28,31 +27,38 @@ vi.mock('@/app/notifications', () => ({ }, })) -vi.mock('@/service/console', () => ({ - consoleQuery: { - apps: { - byAppId: { - server: { - put: { - mutationOptions: (options = {}) => ({ - mutationFn: mocks.updateServer, - ...options, - }), +vi.mock('@/service/console', async () => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleQuery: { + apps: { + byAppId: { + server: { + get: consoleOperationStub('apps.byAppId.server.get'), + refresh: { post: consoleOperationStub('apps.byAppId.server.refresh.post') }, + put: { + mutationOptions: (options = {}) => ({ + mutationFn: mocks.updateServer, + ...options, + }), + }, }, }, }, }, - }, -})) + } +}) -vi.mock('@/service/use-tools', () => ({ - useInvalidateMCPServerDetail: () => mocks.invalidateServerDetail, - useMCPServerDetail: () => mocks.serverDetail, - useRefreshMCPServerCode: () => ({ - isPending: false, - mutateAsync: mocks.refreshServerCode, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.server.get': () => mocks.serverDetail, + 'apps.byAppId.server.refresh.post': () => ({ + isPending: false, + mutateAsync: mocks.refreshServerCode, + }), + }) +}) vi.mock('@/app/components/tools/mcp/mcp-server-modal', () => ({ default: (props: Record) => { @@ -111,8 +117,9 @@ function createDeferredPromise() { function renderCard(cardAppInfo: AppDetailWithSite = appInfo, workflow?: PublishedWorkflow) { const queryClient = createTestQueryClient() + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') - return render( + const view = render( , ) + return { ...view, invalidateQueries } } describe('MCPAccessPointCard', () => { @@ -227,7 +235,7 @@ describe('MCPAccessPointCard', () => { mocks.updateServer .mockReturnValueOnce(firstToggle.promise) .mockReturnValueOnce(secondToggle.promise) - renderCard() + const { invalidateQueries } = renderCard() const accessSwitch = screen.getByRole('switch') await user.click(accessSwitch) @@ -249,7 +257,13 @@ describe('MCPAccessPointCard', () => { secondToggle.resolve() await waitFor(() => { - expect(mocks.invalidateServerDetail).toHaveBeenCalledTimes(2) + expect(invalidateQueries).toHaveBeenCalledTimes(2) + expect(invalidateQueries).toHaveBeenLastCalledWith({ + queryKey: [ + ['console', 'apps', 'byAppId', 'server', 'get'], + { type: 'query', input: { params: { app_id: 'app-1' } } }, + ], + }) }) expect(accessSwitch).toHaveAttribute('aria-checked', 'true') }) diff --git a/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx b/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx index d478942b42e598..fc0965dc2e5d9b 100644 --- a/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx @@ -1,4 +1,4 @@ -import type { AppTrigger } from '@/service/use-tools' +import type { AppTrigger } from '@/app/components/tools/types' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -9,7 +9,6 @@ import { createTestQueryClient } from '@/test/query-client' import { TriggerAccessPointCard } from '../built-in-access-points/trigger-card' const mocks = vi.hoisted(() => ({ - invalidateTriggers: vi.fn(), setTriggerStatuses: vi.fn(), triggerQuery: { data: undefined as { data: AppTrigger[] } | undefined, @@ -38,10 +37,15 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `https://docs.example.test/en${path}`, })) -vi.mock('@/service/console', () => ({ +vi.mock('@/service/console', async () => ({ consoleQuery: { apps: { byAppId: { + triggers: { + get: (await import('@/test/console/operation-hooks')).consoleOperationStub( + 'apps.byAppId.triggers.get', + ), + }, triggerEnable: { post: { mutationOptions: (options = {}) => ({ @@ -55,10 +59,12 @@ vi.mock('@/service/console', () => ({ }, })) -vi.mock('@/service/use-tools', () => ({ - useAppTriggers: () => mocks.triggerQuery, - useInvalidateAppTriggers: () => mocks.invalidateTriggers, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.triggers.get': () => mocks.triggerQuery, + }) +}) vi.mock('@/service/use-triggers', () => ({ useAllTriggerPlugins: () => ({ data: [] }), @@ -87,14 +93,21 @@ function createTrigger(id: string, status: AppTrigger['status']): AppTrigger { } } +const triggersQueryKey = [ + ['console', 'apps', 'byAppId', 'triggers', 'get'], + { type: 'query', input: { params: { app_id: 'app-1' } } }, +] + function renderCard(availability: 'available' | 'loading' | 'unavailable') { const queryClient = createTestQueryClient() + queryClient.setQueryData(triggersQueryKey, { data: [] }) render( , ) + return queryClient } function createDeferredPromise() { @@ -186,7 +199,7 @@ describe('TriggerAccessPointCard', () => { mocks.triggerQuery.data = { data: [createTrigger('disabled', 'disabled')], } - renderCard('available') + const queryClient = renderCard('available') await user.click(screen.getByRole('switch', { name: 'Trigger disabled' })) @@ -203,7 +216,7 @@ describe('TriggerAccessPointCard', () => { }, expect.any(Object), ) - expect(mocks.invalidateTriggers).toHaveBeenCalledWith('app-1') + expect(queryClient.getQueryState(triggersQueryKey)?.isInvalidated).toBe(true) }) expect(toast.success).not.toHaveBeenCalled() }) @@ -234,7 +247,8 @@ describe('TriggerAccessPointCard', () => { mocks.updateTriggerStatus .mockReturnValueOnce(firstToggle.promise) .mockReturnValueOnce(secondToggle.promise) - renderCard('available') + const queryClient = renderCard('available') + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') const triggerSwitch = screen.getByRole('switch', { name: 'Trigger enabled' }) await user.click(triggerSwitch) @@ -256,7 +270,7 @@ describe('TriggerAccessPointCard', () => { secondToggle.resolve() await waitFor(() => { - expect(mocks.invalidateTriggers).toHaveBeenCalledTimes(2) + expect(invalidateQueries).toHaveBeenCalledTimes(2) }) expect(triggerSwitch).toHaveAttribute('aria-checked', 'true') }) diff --git a/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx b/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx index c88200249e4b75..a870a805add0d7 100644 --- a/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx +++ b/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx @@ -12,20 +12,16 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { AccessPointCard } from '@/app/components/base/access-point/card' import { AccessPointUrl } from '@/app/components/base/access-point/url' import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal' +import { selectMCPServerDetail } from '@/app/components/tools/utils/query-select' import { BlockEnum } from '@/app/components/workflow/types' import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' -import { - useInvalidateMCPServerDetail, - useMCPServerDetail, - useRefreshMCPServerCode, -} from '@/service/use-tools' import { useAccessPointStatusLabel } from '../shared/use-access-point-status-label' import { getPublishedWorkflowNodes, isAdvancedApp } from '../shared/utils' @@ -54,23 +50,29 @@ export function MCPAccessPointCard({ const [showRegenerate, setShowRegenerate] = useState(false) const basicConfig = appInfo.model_config const basicAppInputForm = basicConfig?.user_input_form - const { data: detail, isPending: serverDetailLoading } = useMCPServerDetail( - appInfo.id, - Boolean(appInfo.id), - ) - const invalidateServerDetail = useInvalidateMCPServerDetail() + const queryClient = useQueryClient() + const serverDetailQueryOptions = consoleQuery.apps.byAppId.server.get.queryOptions({ + input: { params: { app_id: appInfo.id } }, + enabled: Boolean(appInfo.id), + select: selectMCPServerDetail, + }) + const { data: detail, isPending: serverDetailLoading } = useQuery(serverDetailQueryOptions) + const invalidateServerDetail = () => + queryClient.invalidateQueries({ queryKey: serverDetailQueryOptions.queryKey }) const updateServerMutation = useMutation( consoleQuery.apps.byAppId.server.put.mutationOptions({ scope: { id: `app-mcp-toggle:${appInfo.id}`, }, - onSuccess: () => invalidateServerDetail(appInfo.id), + onSuccess: () => invalidateServerDetail(), onError: () => { toast.error(t(($) => $['actionMsg.modifiedUnsuccessfully'], { ns: 'common' })) }, }), ) - const { mutateAsync: refreshServerCode, isPending: regenerating } = useRefreshMCPServerCode() + const { mutateAsync: refreshServerCode, isPending: regenerating } = useMutation( + consoleQuery.apps.byAppId.server.refresh.post.mutationOptions(), + ) const serverPublished = Boolean(detail?.id) const serverActivated = detail?.status === 'active' @@ -136,8 +138,8 @@ export function MCPAccessPointCard({ const handleRegenerate = async () => { if (!canManageAccessPoint || !detail?.id) return - await refreshServerCode(appInfo.id) - invalidateServerDetail(appInfo.id) + await refreshServerCode({ params: { app_id: appInfo.id } }) + invalidateServerDetail() setShowRegenerate(false) } @@ -206,7 +208,7 @@ export function MCPAccessPointCard({ latestParams={latestParams} onHide={() => { setShowServerModal(false) - invalidateServerDetail(appInfo.id) + invalidateServerDetail() }} appInfo={appInfo} /> diff --git a/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx b/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx index abf8c9bf080a2e..6398864f3a1ac7 100644 --- a/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx +++ b/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx @@ -1,11 +1,11 @@ 'use client' import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { AppTrigger } from '@/app/components/tools/types' import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types' -import type { AppTrigger } from '@/service/use-tools' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { AccessPointCard, AccessPointEmptyContent } from '@/app/components/base/access-point/card' @@ -16,7 +16,6 @@ import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import Link from '@/next/link' import { consoleQuery } from '@/service/console' -import { useAppTriggers, useInvalidateAppTriggers } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { canFindTool } from '@/utils' import { useAccessPointStatusLabel } from '../shared/use-access-point-status-label' @@ -64,13 +63,18 @@ function TriggerAccessPointItem({ triggerPlugins: TriggerWithProvider[] }) { const { t } = useTranslation(['agentV2', 'appOverview', 'common']) - const invalidateTriggers = useInvalidateAppTriggers() + const queryClient = useQueryClient() const updateTriggerMutation = useMutation( consoleQuery.apps.byAppId.triggerEnable.post.mutationOptions({ scope: { id: `app-trigger-toggle:${appId}:${trigger.id}`, }, - onSuccess: () => invalidateTriggers(appId), + onSuccess: () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.apps.byAppId.triggers.get.queryKey({ + input: { params: { app_id: appId } }, + }), + }), onError: () => { toast.error(t(($) => $['actionMsg.modifiedUnsuccessfully'], { ns: 'common' })) }, @@ -145,7 +149,12 @@ export function TriggerAccessPointCard({ }: TriggerAccessPointCardProps) { const { t } = useTranslation(['appOverview', 'deployments', 'navigation']) const docLink = useDocLink() - const { data: response, isLoading } = useAppTriggers(appInfo.id) + const { data: response, isLoading } = useQuery( + consoleQuery.apps.byAppId.triggers.get.queryOptions({ + input: { params: { app_id: appInfo.id } }, + enabled: !!appInfo.id, + }), + ) const { data: triggerPlugins = [] } = useAllTriggerPlugins() const setTriggerStatuses = useTriggerStatusStore((state) => state.setTriggerStatuses) const triggers = useMemo(() => response?.data ?? [], [response?.data]) diff --git a/web/app/components/app/annotation/__tests__/filter.spec.tsx b/web/app/components/app/annotation/__tests__/filter.spec.tsx index efc9bb1eb88e82..9733c5ddd4f662 100644 --- a/web/app/components/app/annotation/__tests__/filter.spec.tsx +++ b/web/app/components/app/annotation/__tests__/filter.spec.tsx @@ -1,16 +1,33 @@ import type { AnnotationCountResponse } from '@dify/contracts/api/console/apps/types.gen' import type { UseQueryResult } from '@tanstack/react-query' -import type { Mock } from 'vite-plus/test' import type { QueryParam } from '../filter' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import * as useLogModule from '@/service/use-log' import Filter from '../filter' -vi.mock('@/service/use-log') - -const mockUseAnnotationsCount = useLogModule.useAnnotationsCount as Mock +const mockUseQuery = vi.fn() + +vi.mock('@tanstack/react-query', async (importOriginal) => ({ + ...(await importOriginal()), + useQuery: (options: unknown) => mockUseQuery(options), +})) + +vi.mock('@/service/console', () => ({ + consoleQuery: { + apps: { + byAppId: { + annotations: { + count: { + get: { + queryOptions: (options: unknown) => options, + }, + }, + }, + }, + }, + }, +})) // ============================================================================ // Test Utilities @@ -65,7 +82,7 @@ describe('Filter', () => { describe('Rendering', () => { it('should render nothing when data is loading', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ isLoading: true }), ) @@ -82,7 +99,7 @@ describe('Filter', () => { it('should render nothing when data is undefined', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: undefined, isLoading: false }), ) @@ -99,7 +116,7 @@ describe('Filter', () => { it('should render filter and children when data is available', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: { count: 20 }, isLoading: false, @@ -123,9 +140,9 @@ describe('Filter', () => { // Props Tests (REQUIRED) // -------------------------------------------------------------------------- describe('Props', () => { - it('should call useAnnotationsCount with appId', () => { + it('should query the annotations count for appId', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: { count: 10 }, isLoading: false, @@ -140,12 +157,12 @@ describe('Filter', () => { ) // Assert - expect(mockUseAnnotationsCount).toHaveBeenCalledWith(appId) + expect(mockUseQuery).toHaveBeenCalledWith({ input: { params: { app_id: appId } } }) }) it('should display keyword value in input', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: { count: 10 }, isLoading: false, @@ -173,7 +190,7 @@ describe('Filter', () => { describe('User Interactions', () => { it('should call setQueryParams when typing in search input', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: { count: 20 }, isLoading: false, @@ -198,7 +215,7 @@ describe('Filter', () => { it('should call setQueryParams with empty keyword when clearing input', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: { count: 20 }, isLoading: false, @@ -227,7 +244,7 @@ describe('Filter', () => { describe('Edge Cases', () => { it('should handle empty keyword in queryParams', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: { count: 5 }, isLoading: false, @@ -247,7 +264,7 @@ describe('Filter', () => { it('should handle undefined keyword in queryParams', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: { count: 5 }, isLoading: false, @@ -267,7 +284,7 @@ describe('Filter', () => { it('should handle zero count', () => { // Arrange - mockUseAnnotationsCount.mockReturnValue( + mockUseQuery.mockReturnValue( createMockQueryResult({ data: { count: 0 }, isLoading: false, diff --git a/web/app/components/app/annotation/__tests__/index.spec.tsx b/web/app/components/app/annotation/__tests__/index.spec.tsx index 6a10430d40c593..d8fb422ecfd22a 100644 --- a/web/app/components/app/annotation/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/__tests__/index.spec.tsx @@ -1,20 +1,8 @@ import type { ReactElement } from 'react' -import type { Mock } from 'vite-plus/test' import type { AnnotationItem } from '../type' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { toast } from '@/app/notifications' -import { - addAnnotation, - delAnnotation, - delAnnotations, - editAnnotation, - fetchAnnotationConfig, - fetchAnnotationList, - queryAnnotationJobStatus, - updateAnnotationScore, - updateAnnotationStatus, -} from '@/service/annotation' import { renderWithConsoleQuery } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' import Annotation from '../index' @@ -30,18 +18,43 @@ vi.mock('ahooks', () => ({ useDebounce: (value: any) => value, })) -vi.mock('@/service/annotation', () => ({ +const mocks = vi.hoisted(() => ({ addAnnotation: vi.fn(), delAnnotation: vi.fn(), delAnnotations: vi.fn(), - fetchAnnotationConfig: vi.fn(), editAnnotation: vi.fn(), + fetchAnnotationConfig: vi.fn(), fetchAnnotationList: vi.fn(), queryAnnotationJobStatus: vi.fn(), updateAnnotationScore: vi.fn(), updateAnnotationStatus: vi.fn(), })) +vi.mock('@/service/annotation', () => ({ + updateAnnotationStatus: mocks.updateAnnotationStatus, +})) + +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + apps: { + byAppId: { + annotationSetting: { get: mocks.fetchAnnotationConfig }, + annotationReply: { + byAction: { status: { byJobId: { get: mocks.queryAnnotationJobStatus } } }, + }, + annotationSettings: { byAnnotationSettingId: { post: mocks.updateAnnotationScore } }, + annotations: { + get: mocks.fetchAnnotationList, + post: mocks.addAnnotation, + delete: mocks.delAnnotations, + byAnnotationId: { delete: mocks.delAnnotation, post: mocks.editAnnotation }, + }, + }, + }, + }, +})) + vi.mock('../filter', () => ({ default: ({ children }: { children: React.ReactNode }) => (
{children}
@@ -168,15 +181,15 @@ vi.spyOn(toast, 'info').mockImplementation((message, options) => { mockNotify({ type: 'info', message, ...options }) return 'toast-info-id' }) -const addAnnotationMock = addAnnotation as Mock -const delAnnotationMock = delAnnotation as Mock -const delAnnotationsMock = delAnnotations as Mock -const editAnnotationMock = editAnnotation as Mock -const fetchAnnotationConfigMock = fetchAnnotationConfig as Mock -const fetchAnnotationListMock = fetchAnnotationList as Mock -const queryAnnotationJobStatusMock = queryAnnotationJobStatus as Mock -const updateAnnotationScoreMock = updateAnnotationScore as Mock -const updateAnnotationStatusMock = updateAnnotationStatus as Mock +const addAnnotationMock = mocks.addAnnotation +const delAnnotationMock = mocks.delAnnotation +const delAnnotationsMock = mocks.delAnnotations +const editAnnotationMock = mocks.editAnnotation +const fetchAnnotationConfigMock = mocks.fetchAnnotationConfig +const fetchAnnotationListMock = mocks.fetchAnnotationList +const queryAnnotationJobStatusMock = mocks.queryAnnotationJobStatus +const updateAnnotationScoreMock = mocks.updateAnnotationScore +const updateAnnotationStatusMock = mocks.updateAnnotationStatus const appDetail = createAppDetailFixture({ id: 'app-id', @@ -231,13 +244,10 @@ describe('Annotation', () => { 'https://docs.example.com/use-dify/monitor/annotation-reply', ) expect(await screen.findByTestId('empty-element')).toBeInTheDocument() - expect(fetchAnnotationListMock).toHaveBeenCalledWith( - appDetail.id, - expect.objectContaining({ - page: 1, - keyword: '', - }), - ) + expect(fetchAnnotationListMock).toHaveBeenCalledWith({ + params: { app_id: appDetail.id }, + query: expect.objectContaining({ page: 1, keyword: '' }), + }) }) it('should handle annotation creation and refresh list data', async () => { @@ -251,9 +261,9 @@ describe('Annotation', () => { fireEvent.click(screen.getByTestId('trigger-add')) await waitFor(() => { - expect(addAnnotationMock).toHaveBeenCalledWith(appDetail.id, { - question: 'new question', - answer: 'new answer', + expect(addAnnotationMock).toHaveBeenCalledWith({ + params: { app_id: appDetail.id }, + body: { question: 'new question', answer: 'new answer' }, }) expect(mockNotify).toHaveBeenCalledWith( expect.objectContaining({ @@ -299,7 +309,10 @@ describe('Annotation', () => { await latestListProps.onBatchDelete() }) await waitFor(() => { - expect(delAnnotationsMock).toHaveBeenCalledWith(appDetail.id, [annotation.id]) + expect(delAnnotationsMock).toHaveBeenCalledWith({ + params: { app_id: appDetail.id }, + query: { annotation_id: [annotation.id] }, + }) expect(mockNotify).toHaveBeenCalledWith( expect.objectContaining({ type: 'success', @@ -315,7 +328,9 @@ describe('Annotation', () => { fireEvent.click(screen.getByTestId('view-modal-remove')) }) await waitFor(() => { - expect(delAnnotationMock).toHaveBeenCalledWith(appDetail.id, annotation.id) + expect(delAnnotationMock).toHaveBeenCalledWith({ + params: { app_id: appDetail.id, annotation_id: annotation.id }, + }) }) }) @@ -401,11 +416,9 @@ describe('Annotation', () => { }), 0.5, ) - expect(queryAnnotationJobStatusMock).toHaveBeenCalledWith( - appDetail.id, - AnnotationEnableStatus.disable, - 'job-1', - ) + expect(queryAnnotationJobStatusMock).toHaveBeenCalledWith({ + params: { app_id: appDetail.id, action: AnnotationEnableStatus.disable, job_id: 'job-1' }, + }) expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false') }) }) @@ -439,7 +452,10 @@ describe('Annotation', () => { }, 0.7, ) - expect(updateAnnotationScoreMock).toHaveBeenCalledWith(appDetail.id, 'config-id', 0.7) + expect(updateAnnotationScoreMock).toHaveBeenCalledWith({ + params: { app_id: appDetail.id, annotation_setting_id: 'config-id' }, + body: { score_threshold: 0.7 }, + }) expect(mockNotify).toHaveBeenCalledWith( expect.objectContaining({ message: 'common.api.actionSuccess', @@ -461,9 +477,9 @@ describe('Annotation', () => { fireEvent.click(screen.getByTestId('view-modal-save')) await waitFor(() => { - expect(editAnnotationMock).toHaveBeenCalledWith(appDetail.id, annotation.id, { - question: 'Edited question', - answer: 'Edited answer', + expect(editAnnotationMock).toHaveBeenCalledWith({ + params: { app_id: appDetail.id, annotation_id: annotation.id }, + body: { question: 'Edited question', answer: 'Edited answer' }, }) }) diff --git a/web/app/components/app/annotation/__tests__/list.spec.tsx b/web/app/components/app/annotation/__tests__/list.spec.tsx index 764651a96339fd..14b40c3776711d 100644 --- a/web/app/components/app/annotation/__tests__/list.spec.tsx +++ b/web/app/components/app/annotation/__tests__/list.spec.tsx @@ -52,7 +52,7 @@ describe('List', () => { />, ) - fireEvent.click(screen.getByText(item.question)) + fireEvent.click(screen.getByText(item.question!)) expect(onView).toHaveBeenCalledWith(item) expect(mockFormatTime).toHaveBeenCalledWith(item.created_at, 'appLog.dateTimeFormat') @@ -113,7 +113,7 @@ describe('List', () => { />, ) - const row = screen.getByText(item.question).closest('tr') as HTMLTableRowElement + const row = screen.getByText(item.question!).closest('tr') as HTMLTableRowElement const actionButtons = within(row).getAllByRole('button') fireEvent.click(actionButtons[1]!) diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx index 22744957f9c714..fc8a631e18ee47 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx @@ -1,19 +1,34 @@ import type { ReactElement } from 'react' -import type { Mock } from 'vite-plus/test' import type { IBatchModalProps } from '../index' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' import { renderWithConsoleQuery } from '@/test/console/query-data' import BatchModal, { ProcessStatus } from '../index' let annotationQuota = { size: 0, limit: 10 } -vi.mock('@/service/annotation', () => ({ +const mocks = vi.hoisted(() => ({ annotationBatchImport: vi.fn(), checkAnnotationBatchImportProgress: vi.fn(), })) +vi.mock('@/service/annotation', () => ({ + annotationBatchImport: mocks.annotationBatchImport, +})) + +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + apps: { + byAppId: { + annotations: { + batchImportStatus: { byJobId: { get: mocks.checkAnnotationBatchImportProgress } }, + }, + }, + }, + }, +})) + vi.mock('../csv-downloader', () => ({ default: () =>
, })) @@ -53,8 +68,8 @@ vi.mock('@/app/notifications', () => ({ info: (message: string) => mockNotify({ type: 'info', message }), }, })) -const annotationBatchImportMock = annotationBatchImport as Mock -const checkAnnotationBatchImportProgressMock = checkAnnotationBatchImportProgress as Mock +const annotationBatchImportMock = mocks.annotationBatchImport +const checkAnnotationBatchImportProgressMock = mocks.checkAnnotationBatchImportProgress const renderComponent = (props: Partial = {}) => { const mergedProps: IBatchModalProps = { @@ -141,7 +156,7 @@ describe('BatchModal', () => { expect(annotationBatchImportMock).toHaveBeenCalledTimes(1) }) - const formData = annotationBatchImportMock.mock.calls[0]![0].body as FormData + const formData = annotationBatchImportMock.mock.calls[0]![1] as FormData expect(formData.get('file')).toBe(lastUploadedFile) await waitFor(() => { diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx index dc6b732ffb8fb7..dc3872a5d05e88 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx @@ -11,8 +11,8 @@ import { useTranslation } from 'react-i18next' import AnnotationFull from '@/app/components/billing/annotation-full' import { toast } from '@/app/notifications' import { deploymentEditionAtom } from '@/features/system-features/state' -import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' -import { consoleQuery } from '@/service/console' +import { annotationBatchImport } from '@/service/annotation' +import { consoleClient, consoleQuery } from '@/service/console' import CSVDownloader from './csv-downloader' import CSVUploader from './csv-uploader' @@ -59,7 +59,9 @@ const BatchModal: FC = ({ appId, isShow, onCancel, onAdded }) const [importStatus, setImportStatus] = useState() const checkProcess = async (jobID: string) => { try { - const res = await checkAnnotationBatchImportProgress({ jobID, appId }) + const res = await consoleClient.apps.byAppId.annotations.batchImportStatus.byJobId.get({ + params: { app_id: appId, job_id: jobID }, + }) setImportStatus(res.job_status) if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING) setTimeout(() => checkProcess(res.job_id), 2500) @@ -81,10 +83,7 @@ const BatchModal: FC = ({ appId, isShow, onCancel, onAdded }) const formData = new FormData() formData.append('file', csv) try { - const res = await annotationBatchImport({ - url: `/apps/${appId}/annotations/batch-import`, - body: formData, - }) + const res = await annotationBatchImport(appId, formData) setImportStatus(res.job_status) checkProcess(res.job_id) } catch (e: any) { diff --git a/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx index 4030169a51717e..edabfefcaef920 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx @@ -8,8 +8,10 @@ const { mockEditAnnotation } = vi.hoisted(() => ({ })) // Mock only external dependencies -vi.mock('@/service/annotation', () => ({ - editAnnotation: mockEditAnnotation, +vi.mock('@/service/console', () => ({ + consoleClient: { + apps: { byAppId: { annotations: { byAnnotationId: { post: mockEditAnnotation } } } }, + }, })) vi.mock('@/hooks/use-timestamp', () => ({ @@ -197,7 +199,6 @@ describe('EditAnnotationModal', () => { const props = { ...defaultProps, annotationId: 'test-annotation-id', - messageId: 'test-message-id', onEdited: mockOnEdited, } const user = userEvent.setup() @@ -217,10 +218,9 @@ describe('EditAnnotationModal', () => { await user.click(saveButton) // Assert - expect(mockEditAnnotation).toHaveBeenCalledWith('test-app-id', 'test-annotation-id', { - message_id: 'test-message-id', - question: 'Modified query', - answer: 'Test answer', + expect(mockEditAnnotation).toHaveBeenCalledWith({ + params: { app_id: 'test-app-id', annotation_id: 'test-annotation-id' }, + body: { question: 'Modified query', answer: 'Test answer' }, }) }) }) @@ -413,7 +413,6 @@ describe('EditAnnotationModal', () => { const props = { ...defaultProps, annotationId: 'test-annotation-id', - messageId: 'test-message-id', onEdited: mockOnEdited, } const user = userEvent.setup() @@ -453,7 +452,6 @@ describe('EditAnnotationModal', () => { const props = { ...defaultProps, annotationId: 'test-annotation-id', - messageId: 'test-message-id', onEdited: mockOnEdited, } const user = userEvent.setup() diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index 00f3d94bde5902..2adee68ad5ac24 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -26,14 +26,13 @@ import { useRef } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import useTimestamp from '@/hooks/use-timestamp' -import { editAnnotation } from '@/service/annotation' +import { consoleClient } from '@/service/console' import EditItem, { EditItemType } from './edit-item' type Props = Readonly<{ isShow: boolean onHide: () => void appId: string - messageId?: string annotationId: string query: string answer: string @@ -50,7 +49,6 @@ const EditAnnotationModal: FC = ({ answer, onEdited, appId, - messageId, annotationId, createdAt, onRemove, @@ -64,10 +62,9 @@ const EditAnnotationModal: FC = ({ if (type === EditItemType.Query) postQuery = editedContent else postAnswer = editedContent try { - await editAnnotation(appId, annotationId, { - message_id: messageId, - question: postQuery, - answer: postAnswer, + await consoleClient.apps.byAppId.annotations.byAnnotationId.post({ + params: { app_id: appId, annotation_id: annotationId }, + body: { question: postQuery, answer: postAnswer }, }) onEdited(postQuery, postAnswer) diff --git a/web/app/components/app/annotation/filter.tsx b/web/app/components/app/annotation/filter.tsx index 7fd53304c528cd..f7b0c541dc0f31 100644 --- a/web/app/components/app/annotation/filter.tsx +++ b/web/app/components/app/annotation/filter.tsx @@ -1,9 +1,10 @@ 'use client' import type { FC } from 'react' +import { skipToken, useQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' -import { useAnnotationsCount } from '@/service/use-log' +import { consoleQuery } from '@/service/console' export type QueryParam = { keyword?: string @@ -17,7 +18,11 @@ type IFilterProps = { } const Filter: FC = ({ appId, queryParams, setQueryParams, children }) => { - const { data, isLoading } = useAnnotationsCount(appId) + const { data, isLoading } = useQuery( + consoleQuery.apps.byAppId.annotations.count.get.queryOptions({ + input: appId ? { params: { app_id: appId } } : skipToken, + }), + ) const { t } = useTranslation(['common']) if (isLoading || !data) return null return ( diff --git a/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx b/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx index 840c71a1b53f26..de3aa10e516cce 100644 --- a/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx @@ -7,7 +7,6 @@ import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useLocale } from '#i18n' import { LanguagesSupported } from '@/i18n/language' -import { fetchExportAnnotationList } from '@/service/annotation' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import HeaderOptions from '../index' @@ -22,8 +21,15 @@ vi.mock('react-papaparse', () => ({ jsonToCSV: (data: unknown) => mockJsonToCSV(data), })) -vi.mock('@/service/annotation', () => ({ - fetchExportAnnotationList: vi.fn(), +const { mockedFetchAnnotations } = vi.hoisted(() => ({ + mockedFetchAnnotations: vi.fn(), +})) + +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + apps: { byAppId: { annotations: { export: { get: mockedFetchAnnotations } } } }, + }, })) const { mockedRequest } = vi.hoisted(() => ({ @@ -104,8 +110,6 @@ const mockAnnotations: AnnotationItemBasic[] = [ }, ] -const mockedFetchAnnotations = vi.mocked(fetchExportAnnotationList) - describe('HeaderOptions', () => { const originalUrl = window.location.href @@ -128,7 +132,7 @@ describe('HeaderOptions', () => { renderComponent() await waitFor(() => { - expect(mockedFetchAnnotations).toHaveBeenCalledWith('test-app-id') + expect(mockedFetchAnnotations).toHaveBeenCalledWith({ params: { app_id: 'test-app-id' } }) }) await expandExportMenu(user) diff --git a/web/app/components/app/annotation/header-opts/index.tsx b/web/app/components/app/annotation/header-opts/index.tsx index 882c03b91b0e40..01b3cc6c645a6d 100644 --- a/web/app/components/app/annotation/header-opts/index.tsx +++ b/web/app/components/app/annotation/header-opts/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import type { AnnotationItemBasic } from '../type' +import type { AnnotationItem, AnnotationItemBasic } from '../type' import { AlertDialog } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' import { @@ -19,8 +19,7 @@ import { useTranslation } from 'react-i18next' import { jsonToCSV } from 'react-papaparse' import { useLocale } from '#i18n' import { LanguagesSupported } from '@/i18n/language' -import { fetchExportAnnotationList } from '@/service/annotation' -import { consoleQuery } from '@/service/console' +import { consoleClient, consoleQuery } from '@/service/console' import { downloadBlob } from '@/utils/download' import AddAnnotationModal from '../add-annotation-modal' import BatchAddModal from '../batch-add-annotation-modal' @@ -37,27 +36,27 @@ type Props = Readonly<{ }> type OperationsMenuProps = { - list: AnnotationItemBasic[] + list: AnnotationItem[] onBulkImport: () => void onClearAll: () => void onExportJsonl: () => void } -const buildAnnotationJsonlRecords = (list: AnnotationItemBasic[]) => - list.map((item: AnnotationItemBasic) => { - return `{"messages": [{"role": "system", "content": ""}, {"role": "user", "content": ${JSON.stringify(item.question)}}, {"role": "assistant", "content": ${JSON.stringify(item.answer)}}]}` +const buildAnnotationJsonlRecords = (list: AnnotationItem[]) => + list.map((item) => { + return `{"messages": [{"role": "system", "content": ""}, {"role": "user", "content": ${JSON.stringify(item.question ?? '')}}, {"role": "assistant", "content": ${JSON.stringify(item.answer ?? '')}}]}` }) -const downloadAnnotationJsonl = (list: AnnotationItemBasic[], locale: string) => { +const downloadAnnotationJsonl = (list: AnnotationItem[], locale: string) => { const content = buildAnnotationJsonlRecords(list).join('\n') const file = new Blob([content], { type: 'application/jsonl' }) downloadBlob({ data: file, fileName: `annotations-${locale}.jsonl` }) } -const downloadAnnotationCsv = (list: AnnotationItemBasic[], locale: string) => { +const downloadAnnotationCsv = (list: AnnotationItem[], locale: string) => { const content = jsonToCSV([ locale !== LanguagesSupported[1] ? CSV_HEADER_QA_EN : CSV_HEADER_QA_CN, - ...list.map((item) => [item.question, item.answer]), + ...list.map((item) => [item.question ?? '', item.answer ?? '']), ]) const file = new Blob([`\uFEFF${content}`], { type: 'text/csv;charset=utf-8;' }) downloadBlob({ data: file, fileName: `annotations-${locale}.csv` }) @@ -131,11 +130,13 @@ const OperationsMenu: FC = ({ const HeaderOptions: FC = ({ appId, onAdd, onAdded, controlUpdateList }) => { const { t } = useTranslation(['appAnnotation', 'common']) const locale = useLocale() - const [list, setList] = useState([]) + const [list, setList] = useState([]) const fetchList = React.useCallback(async () => { - const { data }: any = await fetchExportAnnotationList(appId) - setList(data as AnnotationItemBasic[]) + const { data } = await consoleClient.apps.byAppId.annotations.export.get({ + params: { app_id: appId }, + }) + setList(data) }, [appId]) useEffect(() => { diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index 8f7ad84a322afb..7ba059288cba93 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -23,18 +23,8 @@ import { toast } from '@/app/notifications' import { APP_PAGE_LIMIT } from '@/config' import { useDocLink } from '@/context/i18n' import { deploymentEditionAtom } from '@/features/system-features/state' -import { - addAnnotation, - delAnnotation, - delAnnotations, - fetchAnnotationConfig as doFetchAnnotationConfig, - editAnnotation, - fetchAnnotationList, - queryAnnotationJobStatus, - updateAnnotationScore, - updateAnnotationStatus, -} from '@/service/annotation' -import { consoleQuery } from '@/service/console' +import { updateAnnotationStatus } from '@/service/annotation' +import { consoleClient, consoleQuery } from '@/service/console' import { sleep } from '@/utils' import PageTitle from '../log-annotation/page-title' import EmptyElement from './empty-element' @@ -86,7 +76,9 @@ const Annotation: FC = (props) => { const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) const fetchAnnotationConfig = async () => { - const res = await doFetchAnnotationConfig(appDetail.id) + const res = await consoleClient.apps.byAppId.annotationSetting.get({ + params: { app_id: appDetail.id }, + }) setAnnotationConfig(res as AnnotationReplyConfig) return (res as AnnotationReplyConfig).id } @@ -97,7 +89,9 @@ const Annotation: FC = (props) => { const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => { while (true) { - const res: any = await queryAnnotationJobStatus(appDetail.id, status, jobId) + const res = await consoleClient.apps.byAppId.annotationReply.byAction.status.byJobId.get({ + params: { app_id: appDetail.id, action: status, job_id: jobId }, + }) if (res.job_status === JobStatus.completed) break await sleep(2000) } @@ -106,12 +100,11 @@ const Annotation: FC = (props) => { const fetchList = async (page = 1) => { setIsLoading(true) try { - const { data, total }: any = await fetchAnnotationList(appDetail.id, { - page, - limit, - keyword: debouncedQueryParams.keyword || '', + const { data, total } = await consoleClient.apps.byAppId.annotations.get({ + params: { app_id: appDetail.id }, + query: { page, limit, keyword: debouncedQueryParams.keyword || '' }, }) - setList(data as AnnotationItem[]) + setList(data) setTotal(total) } finally { setIsLoading(false) @@ -123,14 +116,19 @@ const Annotation: FC = (props) => { }, [currPage, limit, debouncedQueryParams]) const handleAdd = async (payload: AnnotationItemBasic) => { - await addAnnotation(appDetail.id, payload) + await consoleClient.apps.byAppId.annotations.post({ + params: { app_id: appDetail.id }, + body: payload, + }) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) } const handleRemove = async (id: string) => { - await delAnnotation(appDetail.id, id) + await consoleClient.apps.byAppId.annotations.byAnnotationId.delete({ + params: { app_id: appDetail.id, annotation_id: id }, + }) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) @@ -138,7 +136,10 @@ const Annotation: FC = (props) => { const handleBatchDelete = async () => { try { - await delAnnotations(appDetail.id, selectedIds) + await consoleClient.apps.byAppId.annotations.delete({ + params: { app_id: appDetail.id }, + query: { annotation_id: selectedIds }, + }) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) @@ -156,7 +157,10 @@ const Annotation: FC = (props) => { const handleSave = async (question: string, answer: string) => { if (!currItem) return - await editAnnotation(appDetail.id, currItem.id, { question, answer }) + await consoleClient.apps.byAppId.annotations.byAnnotationId.post({ + params: { app_id: appDetail.id, annotation_id: currItem.id }, + body: { question, answer }, + }) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) @@ -207,7 +211,7 @@ const Annotation: FC = (props) => { } setIsShowEdit(true) } else { - const { job_id: jobId }: any = await updateAnnotationStatus( + const { job_id: jobId } = await updateAnnotationStatus( appDetail.id, AnnotationEnableStatus.disable, annotationConfig?.embedding_model, @@ -319,7 +323,7 @@ const Annotation: FC = (props) => { embeddingModel.embedding_provider_name !== annotationConfig?.embedding_model?.embedding_provider_name ) { - const { job_id: jobId }: any = await updateAnnotationStatus( + const { job_id: jobId } = await updateAnnotationStatus( appDetail.id, AnnotationEnableStatus.enable, embeddingModel, @@ -329,7 +333,10 @@ const Annotation: FC = (props) => { } const annotationId = await fetchAnnotationConfig() if (score !== annotationConfig?.score_threshold) - await updateAnnotationScore(appDetail.id, annotationId, score) + await consoleClient.apps.byAppId.annotationSettings.byAnnotationSettingId.post({ + params: { app_id: appDetail.id, annotation_setting_id: annotationId! }, + body: { score_threshold: score }, + }) await fetchAnnotationConfig() toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) diff --git a/web/app/components/app/annotation/list.tsx b/web/app/components/app/annotation/list.tsx index e3310f22e1f511..491f5e497b733c 100644 --- a/web/app/components/app/annotation/list.tsx +++ b/web/app/components/app/annotation/list.tsx @@ -60,10 +60,10 @@ function AnnotationTableRow({
- + {item.question} - + {item.answer} {formattedCreatedAt} @@ -160,10 +160,14 @@ export function List({ $.dateTimeFormat, { ns: 'appLog' }) as string, - )} + formattedCreatedAt={ + item.created_at + ? formatTime( + item.created_at, + t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, + ) + : '-' + } appId={appId} onView={onView} onRemoved={onRemoved} diff --git a/web/app/components/app/annotation/type.ts b/web/app/components/app/annotation/type.ts index 160b971cbff051..bf0f1a6603bb59 100644 --- a/web/app/components/app/annotation/type.ts +++ b/web/app/components/app/annotation/type.ts @@ -1,32 +1,14 @@ +import type { Annotation, AnnotationHitHistory } from '@dify/contracts/api/console/apps/types.gen' + export type AnnotationItemBasic = { message_id?: string question: string answer: string } -export type AnnotationItem = { - id: string - question: string - answer: string - created_at: number - hit_count: number -} - -export type AnnotationCreateResponse = AnnotationItem & { - account?: { - name?: string - } -} +export type AnnotationItem = Annotation -export type HitHistoryItem = { - id: string - question: string - match: string - response: string - source: string - score: number - created_at: number -} +export type HitHistoryItem = AnnotationHitHistory export type EmbeddingModelConfig = { embedding_provider_name: string diff --git a/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx index 7c1cbb63f1b6f8..7b23991c5e4124 100644 --- a/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx @@ -1,9 +1,7 @@ -import type { Mock } from 'vite-plus/test' import type { AnnotationItem, HitHistoryItem } from '../../type' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' -import { fetchHitHistoryList } from '@/service/annotation' import ViewAnnotationModal from '../index' const mockFormatTime = vi.fn(() => 'formatted-time') @@ -14,8 +12,18 @@ vi.mock('@/hooks/use-timestamp', () => ({ }), })) -vi.mock('@/service/annotation', () => ({ - fetchHitHistoryList: vi.fn(), +const { fetchHitHistoryListMock } = vi.hoisted(() => ({ + fetchHitHistoryListMock: vi.fn(), +})) + +vi.mock('@/service/console', () => ({ + consoleClient: { + apps: { + byAppId: { + annotations: { byAnnotationId: { hitHistories: { get: fetchHitHistoryListMock } } }, + }, + }, + }, })) vi.mock('../../edit-annotation-modal/edit-item', () => { @@ -45,8 +53,6 @@ vi.mock('../../edit-annotation-modal/edit-item', () => { } }) -const fetchHitHistoryListMock = fetchHitHistoryList as Mock - const createAnnotationItem = (overrides: Partial = {}): AnnotationItem => ({ id: overrides.id ?? 'annotation-id', question: overrides.question ?? 'question', diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx index 4127d11e2987a0..3a65b2a98836c3 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx @@ -30,7 +30,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { APP_PAGE_LIMIT } from '@/config' import useTimestamp from '@/hooks/use-timestamp' -import { fetchHitHistoryList } from '@/service/annotation' +import { consoleClient } from '@/service/console' import EditItem, { EditItemType } from '../edit-annotation-modal/edit-item' import HitHistoryNoData from './hit-history-no-data' @@ -51,7 +51,9 @@ const TabType = { type TabType = (typeof TabType)[keyof typeof TabType] const ViewAnnotationModal: FC = ({ appId, isShow, onHide, item, onSave, onRemove }) => { - const { id, question, answer, created_at: createdAt } = item + const { id, created_at: createdAt } = item + const question = item.question ?? '' + const answer = item.answer ?? '' const [newQuestion, setNewQuery] = useState(question) const [newAnswer, setNewAnswer] = useState(answer) const { t } = useTranslation(['appAnnotation', 'appDebug', 'appLog', 'common']) @@ -72,11 +74,12 @@ const ViewAnnotationModal: FC = ({ appId, isShow, onHide, item, onSave, o const fetchHitHistory = async (page = 1) => { try { - const { data, total }: any = await fetchHitHistoryList(appId, id, { - page, - limit: 10, - }) - setHitHistoryList(data as HitHistoryItem[]) + const { data, total } = + await consoleClient.apps.byAppId.annotations.byAnnotationId.hitHistories.get({ + params: { app_id: appId, annotation_id: id }, + query: { page, limit: 10 }, + }) + setHitHistoryList(data) setTotal(total) } catch {} } @@ -158,29 +161,31 @@ const ViewAnnotationModal: FC = ({ appId, isShow, onHide, item, onSave, o > {item.question} {item.match} {item.response} {item.source} {item.score ? item.score.toFixed(2) : '-'} - {formatTime( - item.created_at, - t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, - )} + {item.created_at + ? formatTime( + item.created_at, + t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, + ) + : '-'} ))} @@ -305,10 +310,12 @@ const ViewAnnotationModal: FC = ({ appId, isShow, onHide, item, onSave, o
{t(($) => $['editModal.createdAt'], { ns: 'appAnnotation' })}   - {formatTime( - createdAt, - t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, - )} + {createdAt + ? formatTime( + createdAt, + t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, + ) + : '-'}
)} diff --git a/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx b/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx index 4529251fdd6676..c983f3fa2a811f 100644 --- a/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx @@ -67,12 +67,15 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + }) +}) function publishedWorkflowVersion({ id, diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index b2c76e2b488c18..9db72f88485e14 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -96,10 +96,6 @@ vi.mock('@/service/access-control/use-app-access-control', () => ({ const mockPublishToCreatorsPlatform = vi.fn() const mockCreateWorkflowToolProvider = vi.fn() -vi.mock('@/service/apps', () => ({ - publishToCreatorsPlatform: (...args: unknown[]) => mockPublishToCreatorsPlatform(...args), -})) - vi.mock('@/service/tools', () => ({ createWorkflowToolProvider: (...args: unknown[]) => mockCreateWorkflowToolProvider(...args), saveWorkflowToolProvider: vi.fn(), @@ -135,14 +131,15 @@ vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () }, })) -vi.mock('@/service/use-tools', () => ({ - useWorkflowToolDetailByAppID: () => ({ - data: undefined, - isLoading: false, - }), - useInvalidateAllWorkflowTools: () => vi.fn(), - useInvalidateWorkflowToolDetailByAppID: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProvider.workflow.get.get': () => ({ + data: undefined, + isLoading: false, + }), + }) +}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') @@ -971,8 +968,13 @@ describe('AppPublisher', () => { }) it('should show marketplace button and open redirect URL on success', async () => { - mockPublishToCreatorsPlatform.mockResolvedValue({ - redirect_url: 'https://marketplace.example.com/publish?code=abc', + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { + const request = new Request(input, init) + if (new URL(request.url).pathname.endsWith('/apps/app-1/publish-to-creators-platform')) { + mockPublishToCreatorsPlatform(request.method) + return Response.json({ redirect_url: 'https://marketplace.example.com/publish?code=abc' }) + } + throw new Error(`Unexpected request: ${request.method} ${request.url}`) }) const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null) @@ -993,7 +995,7 @@ describe('AppPublisher', () => { fireEvent.click(screen.getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/)) await waitFor(() => { - expect(mockPublishToCreatorsPlatform).toHaveBeenCalledWith({ appID: 'app-1' }) + expect(mockPublishToCreatorsPlatform).toHaveBeenCalledWith('POST') expect(windowOpenSpy).toHaveBeenCalledWith( 'https://marketplace.example.com/publish?code=abc', '_blank', @@ -1004,7 +1006,7 @@ describe('AppPublisher', () => { }) it('should show toast error when publish to marketplace fails', async () => { - mockPublishToCreatorsPlatform.mockRejectedValue(new Error('network error')) + vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network error')) renderWithConsoleQuery( , diff --git a/web/app/components/app/app-publisher/publisher-content/use-marketplace-publish.ts b/web/app/components/app/app-publisher/publisher-content/use-marketplace-publish.ts index 0d09f969ae1c4a..f6a32a9005718a 100644 --- a/web/app/components/app/app-publisher/publisher-content/use-marketplace-publish.ts +++ b/web/app/components/app/app-publisher/publisher-content/use-marketplace-publish.ts @@ -1,7 +1,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' -import { publishToCreatorsPlatform } from '@/service/apps' +import { consoleClient } from '@/service/console' export function useMarketplacePublish(appId?: string) { const { t } = useTranslation(['workflow']) @@ -12,7 +12,9 @@ export function useMarketplacePublish(appId?: string) { setIsPublishing(true) try { - const response = await publishToCreatorsPlatform({ appID: appId }) + const response = await consoleClient.apps.byAppId.publishToCreatorsPlatform.post({ + params: { app_id: appId }, + }) if (response.redirect_url) window.open(response.redirect_url, '_blank') } catch { toast.error(t(($) => $['common.publishToMarketplaceFailed'], { ns: 'workflow' })) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx index 815959a94b589d..05cdf23b2b2102 100644 --- a/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx @@ -4,13 +4,14 @@ import userEvent from '@testing-library/user-event' import * as React from 'react' import { toast } from '@/app/components/app/configuration/toast' import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' -import { commonQueryKeys } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, renderWithConsoleQuery, seedAppDetail, } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' +import { createFileUploadConfigFixture } from '@/test/fixtures/file-upload-config' import { TransferMethod } from '@/types/app' import ConfigModal from '../index' @@ -19,11 +20,6 @@ vi.mock('@/next/navigation', async (importOriginal) => ({ useParams: () => ({ appId: 'app-1' }), })) -vi.mock('@/service/use-common', async () => ({ - ...(await vi.importActual('@/service/use-common')), - useFileUploadConfig: () => ({ data: undefined }), -})) - vi.mock('@monaco-editor/react', async () => ({ ...(await vi.importActual('@monaco-editor/react')), default: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => ( @@ -38,6 +34,11 @@ vi.mock('@monaco-editor/react', async () => ({ const render = (ui: React.ReactElement) => { const queryClient = createConsoleQueryClient() seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode: 'chat' as const })) + // Upload limits stay pending unless a case seeds them. + void queryClient.query({ + queryKey: consoleQuery.files.upload.get.queryKey(), + queryFn: () => new Promise(() => {}), + }) return renderWithConsoleQuery(ui, { queryClient }) } @@ -393,15 +394,18 @@ describe('ConfigModal', () => { onConfirm={onConfirm} />, ) - queryClient.setQueryData(commonQueryKeys.fileUploadConfig, { - batch_count_limit: 5, - image_file_batch_limit: 10, - single_chunk_attachment_limit: 10, - attachment_image_file_size_limit: 2, - file_size_limit: 15, - file_upload_limit: 5, - workflow_file_upload_limit: 3, - }) + queryClient.setQueryData( + consoleQuery.files.upload.get.queryKey(), + createFileUploadConfigFixture({ + batch_count_limit: 5, + image_file_batch_limit: 10, + single_chunk_attachment_limit: 10, + attachment_image_file_size_limit: 2, + file_size_limit: 15, + file_upload_limit: 5, + workflow_file_upload_limit: 3, + }), + ) await user.click(screen.getByRole('button', { name: 'common.operation.save' })) diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index e70cb9de404ccf..8950a222dd769e 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -4,7 +4,6 @@ import type { ChangeEvent, FC } from 'react' import type { Item as SelectItem } from './type-select' import type { ConfigModalValidationError } from './utils' import type { InputVar, InputVarType, MoreInfo } from '@/app/components/workflow/types' -import type { FileUploadConfigResponse } from '@/models/common' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { skipToken, useQuery, useQueryClient } from '@tanstack/react-query' import * as React from 'react' @@ -15,7 +14,6 @@ import { toast } from '@/app/components/app/configuration/toast' import ConfigContext from '@/context/debug-configuration' import { useParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' import { checkKeys, getNewVarInWorkflow, @@ -177,9 +175,7 @@ const ConfigModal: FC = ({ ) const handleConfirm = () => { - const fileUploadConfig = queryClient.getQueryData( - commonQueryKeys.fileUploadConfig, - ) + const fileUploadConfig = queryClient.getQueryData(consoleQuery.files.upload.get.queryKey()) const { errorMessage, errorField, moreInfo, payloadToSave } = validateConfigModalPayload({ tempPayload, payload, diff --git a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx index af5bcc2af18f8e..c9a7cb328589e3 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx @@ -31,12 +31,15 @@ let builtInTools: ToolWithProvider[] = [] let customTools: ToolWithProvider[] = [] let workflowTools: ToolWithProvider[] = [] let mcpTools: ToolWithProvider[] = [] -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: builtInTools }), - useAllCustomTools: () => ({ data: customTools }), - useAllWorkflowTools: () => ({ data: workflowTools }), - useAllMCPTools: () => ({ data: mcpTools }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: builtInTools }), + 'workspaces.current.tools.api.get': () => ({ data: customTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: workflowTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: mcpTools }), + }) +}) type ToolPickerProps = React.ComponentProps let singleToolSelection: ToolDefaultValue | null = null diff --git a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx index 86b86cfb3989eb..f94aebe52152fd 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx @@ -5,12 +5,10 @@ import * as React from 'react' import { CollectionType } from '@/app/components/tools/types' import SettingBuiltInTool from '../setting-built-in-tool' -const fetchModelToolList = vi.fn() const fetchBuiltInToolList = vi.fn() const fetchCustomToolList = vi.fn() const fetchWorkflowToolList = vi.fn() vi.mock('@/service/tools', () => ({ - fetchModelToolList: (collectionName: string) => fetchModelToolList(collectionName), fetchBuiltInToolList: (collectionName: string) => fetchBuiltInToolList(collectionName), fetchCustomToolList: (collectionName: string) => fetchCustomToolList(collectionName), fetchWorkflowToolList: (appId: string) => fetchWorkflowToolList(appId), @@ -133,7 +131,6 @@ const renderComponent = (props?: Partial { }) it('should fetch tool list when collection has no tools', async () => { - fetchModelToolList.mockResolvedValueOnce([createTool()]) + fetchBuiltInToolList.mockResolvedValueOnce([createTool()]) renderComponent({ collection: { ...baseCollection, @@ -166,8 +163,8 @@ describe('SettingBuiltInTool', () => { }) await waitFor(() => { - expect(fetchModelToolList).toHaveBeenCalledTimes(1) - expect(fetchModelToolList).toHaveBeenCalledWith('vendor/provider-1') + expect(fetchBuiltInToolList).toHaveBeenCalledTimes(1) + expect(fetchBuiltInToolList).toHaveBeenCalledWith('vendor/provider-1') }) expect(await screen.findByText('Search Tool')).toBeInTheDocument() }) @@ -287,7 +284,6 @@ describe('SettingBuiltInTool', () => { id: 'workflow-1', } as any, isBuiltIn: false, - isModel: false, }) await waitFor(() => { diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index 95703f7be31dc1..2dc637af297b76 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -12,6 +12,7 @@ import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiDeleteBinLine, RiEqualizer2Line } from '@remixicon/react' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' @@ -22,6 +23,7 @@ import { OperationButton } from '@/app/components/app/configuration/base/operati import AppIcon from '@/app/components/base/app-icon' import { parseToolProviderType } from '@/app/components/tools/provider-type' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { addDefaultValue, toolParametersToFormSchemas, @@ -29,12 +31,7 @@ import { import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' import { MAX_TOOLS_NUM } from '@/config' import ConfigContext from '@/context/debug-configuration' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { writeTextToClipboard } from '@/utils/clipboard' import { matchesProviderReference } from '@/utils/provider-reference' import { useFormattingChangedDispatcher } from '../../../debug/hooks' @@ -48,10 +45,22 @@ const AgentTools: FC = () => { const { t } = useTranslation(['appDebug', 'common', 'tools']) const [isShowChooseTool, setIsShowChooseTool] = useState(false) const { readonly, modelConfig, setModelConfig } = useContext(ConfigContext) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const collectionList = useMemo(() => { const allTools = [ ...(buildInTools || []), @@ -402,7 +411,6 @@ const AgentTools: FC = () => { toolName={currentTool?.tool_name as string} setting={currentTool?.tool_parameters} collection={currentTool?.collection as ToolWithProvider} - isModel={currentTool?.collection?.type === CollectionType.model} onSave={handleToolSettingChange} onHide={() => setIsShowSettingTool(false)} credentialId={currentTool?.credential_id} diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index 8334ca4dbe0d1c..0b0dd5d56eaeaa 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -30,18 +30,12 @@ import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' import { CollectionType } from '@/app/components/tools/types' import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { getPluginLanguage } from '@/i18n/metadata' -import { - fetchBuiltInToolList, - fetchCustomToolList, - fetchModelToolList, - fetchWorkflowToolList, -} from '@/service/tools' +import { fetchBuiltInToolList, fetchCustomToolList, fetchWorkflowToolList } from '@/service/tools' type Props = Readonly<{ showBackButton?: boolean collection: Collection | ToolWithProvider isBuiltIn?: boolean - isModel?: boolean toolName: string setting?: Record readonly?: boolean @@ -56,7 +50,6 @@ const SettingBuiltInTool: FC = ({ showBackButton = false, collection, isBuiltIn = true, - isModel = true, toolName, setting = {}, readonly, @@ -90,8 +83,7 @@ const SettingBuiltInTool: FC = ({ try { const list = await new Promise((resolve) => { ;(async function () { - if (isModel) resolve(await fetchModelToolList(collection.name)) - else if (isBuiltIn) resolve(await fetchBuiltInToolList(collection.name)) + if (isBuiltIn) resolve(await fetchBuiltInToolList(collection.name)) else if (collection.type === CollectionType.workflow) resolve(await fetchWorkflowToolList(collection.id)) else resolve(await fetchCustomToolList(collection.name)) diff --git a/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx index 75f3ac0ae2a8c6..adbc1cde66e208 100644 --- a/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx @@ -11,6 +11,7 @@ import { getSelectedDatasetsMode } from '@/app/components/workflow/nodes/knowled import { DatasetPermission, DataSourceType } from '@/models/datasets' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { ModelModeType, RETRIEVE_TYPE } from '@/types/app' import { DatasetACLPermission, getDatasetACLCapabilities } from '@/utils/permission' import DatasetConfig from '../index' @@ -243,9 +244,9 @@ vi.mock('use-context-selector', () => ({ const createMockDataset = (overrides: Partial = {}): DataSet => { const defaultDataset: DataSet = { + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', - indexing_status: 'completed' as any, icon_info: { icon: '📘', icon_type: 'emoji', @@ -264,7 +265,6 @@ const createMockDataset = (overrides: Partial = {}): DataSet => { app_count: 0, doc_form: 'text' as any, document_count: 10, - total_document_count: 10, total_available_documents: 10, word_count: 1000, provider: 'dify', @@ -282,17 +282,6 @@ const createMockDataset = (overrides: Partial = {}): DataSet => { score_threshold_enabled: false, score_threshold: 0.7, }, - retrieval_model: { - search_method: 'semantic_search' as any, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 4, - score_threshold_enabled: false, - score_threshold: 0.7, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', @@ -1057,31 +1046,9 @@ describe('DatasetConfig', () => { const datasets = [ createMockDataset({ id: 'ds1', - retrieval_model: { - search_method: 'semantic_search' as any, - reranking_enable: true, - reranking_model: { - reranking_provider_name: 'cohere', - reranking_model_name: 'rerank-v3.5', - }, - top_k: 5, - score_threshold_enabled: true, - score_threshold: 0.8, - }, }), createMockDataset({ id: 'ds2', - retrieval_model: { - search_method: 'full_text_search' as any, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 3, - score_threshold_enabled: false, - score_threshold: 0.5, - }, }), ] diff --git a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx index 105d77835d7567..67724ec317d5aa 100644 --- a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx @@ -7,6 +7,7 @@ import { fireEvent, render, screen, waitFor, within } from '@testing-library/rea import userEvent from '@testing-library/user-event' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import Item from '../index' @@ -55,16 +56,12 @@ const baseRetrievalConfig: RetrievalConfig = { const defaultIndexingTechnique: IndexingType = 'high_quality' as IndexingType const createDataset = (overrides: Partial = {}): DataSet => { - const { retrieval_model, retrieval_model_dict, icon_info, ...restOverrides } = overrides + const { retrieval_model_dict, icon_info, ...restOverrides } = overrides const resolvedRetrievalModelDict = { ...baseRetrievalConfig, ...retrieval_model_dict, } - const resolvedRetrievalModel = { - ...baseRetrievalConfig, - ...(retrieval_model ?? retrieval_model_dict), - } const defaultIconInfo = { icon: '📘', @@ -76,9 +73,9 @@ const createDataset = (overrides: Partial = {}): DataSet => { const resolvedIconInfo = 'icon_info' in overrides ? icon_info : defaultIconInfo return { + ...createDatasetFixture(), id: 'dataset-id', name: 'Dataset Name', - indexing_status: 'completed', icon_info: resolvedIconInfo as DataSet['icon_info'], description: 'A test dataset', permission: DatasetPermission.onlyMe, @@ -91,7 +88,6 @@ const createDataset = (overrides: Partial = {}): DataSet => { app_count: 0, doc_form: ChunkingMode.text, document_count: 0, - total_document_count: 0, total_available_documents: 0, word_count: 0, provider: 'dify', @@ -99,7 +95,6 @@ const createDataset = (overrides: Partial = {}): DataSet => { embedding_model_provider: 'openai', embedding_available: true, retrieval_model_dict: resolvedRetrievalModelDict, - retrieval_model: resolvedRetrievalModel, tags: [], external_knowledge_info: { external_knowledge_id: 'external-id', diff --git a/web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx index 4098f23d302cda..0af86900dedc7f 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx @@ -17,6 +17,7 @@ import { RerankingModeEnum, WeightedScoreEnum, } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' import ConfigContent from '../config-content' @@ -77,16 +78,12 @@ const baseRetrievalConfig: RetrievalConfig = { const defaultIndexingTechnique: IndexingType = 'high_quality' as IndexingType const createDataset = (overrides: Partial = {}): DataSet => { - const { retrieval_model, retrieval_model_dict, icon_info, ...restOverrides } = overrides + const { retrieval_model_dict, icon_info, ...restOverrides } = overrides const resolvedRetrievalModelDict = { ...baseRetrievalConfig, ...retrieval_model_dict, } - const resolvedRetrievalModel = { - ...baseRetrievalConfig, - ...(retrieval_model ?? retrieval_model_dict), - } const defaultIconInfo = { icon: '📘', @@ -98,9 +95,9 @@ const createDataset = (overrides: Partial = {}): DataSet => { const resolvedIconInfo = 'icon_info' in overrides ? icon_info : defaultIconInfo return { + ...createDatasetFixture(), id: 'dataset-id', name: 'Dataset Name', - indexing_status: 'completed', icon_info: resolvedIconInfo as DataSet['icon_info'], description: 'A test dataset', permission: DatasetPermission.onlyMe, @@ -113,7 +110,6 @@ const createDataset = (overrides: Partial = {}): DataSet => { app_count: 0, doc_form: ChunkingMode.text, document_count: 0, - total_document_count: 0, total_available_documents: 0, word_count: 0, provider: 'dify', @@ -121,7 +117,6 @@ const createDataset = (overrides: Partial = {}): DataSet => { embedding_model_provider: 'openai', embedding_available: true, retrieval_model_dict: resolvedRetrievalModelDict, - retrieval_model: resolvedRetrievalModel, tags: [], external_knowledge_info: { external_knowledge_id: 'external-id', diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx index 2fcc3bf14fe0cd..f8fac8852141ef 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/__tests__/index.spec.tsx @@ -17,10 +17,13 @@ vi.mock('ahooks', async (importOriginal) => { } }) -const mockUseInfiniteDatasets = vi.fn() -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInfiniteDatasets: (...args: any[]) => mockUseInfiniteDatasets(...args), -})) +const { mockDatasetsInfiniteQuery } = vi.hoisted(() => ({ mockDatasetsInfiniteQuery: vi.fn() })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.get': mockDatasetsInfiniteQuery, + }) +}) let mockWorkspacePermissionKeys = ['dataset.create_and_management'] @@ -93,7 +96,7 @@ describe('SelectDataSet', () => { embedding_available: false, provider: 'external', }) - mockUseInfiniteDatasets.mockReturnValue({ + mockDatasetsInfiniteQuery.mockReturnValue({ data: { pages: [{ data: [datasetOne, datasetTwo] }] }, isLoading: false, isFetchingNextPage: false, @@ -122,7 +125,7 @@ describe('SelectDataSet', () => { }) it('shows empty state when no datasets are available and disables add', async () => { - mockUseInfiniteDatasets.mockReturnValue({ + mockDatasetsInfiniteQuery.mockReturnValue({ data: { pages: [{ data: [] }] }, isLoading: false, isFetchingNextPage: false, @@ -144,7 +147,7 @@ describe('SelectDataSet', () => { it('should hide the create dataset link when dataset.create_and_management is unavailable', async () => { mockWorkspacePermissionKeys = [] - mockUseInfiniteDatasets.mockReturnValue({ + mockDatasetsInfiniteQuery.mockReturnValue({ data: { pages: [{ data: [] }] }, isLoading: false, isFetchingNextPage: false, @@ -168,7 +171,7 @@ describe('SelectDataSet', () => { id: 'set-1', name: 'Dataset One', }) - mockUseInfiniteDatasets.mockReturnValue({ + mockDatasetsInfiniteQuery.mockReturnValue({ data: { pages: [{ data: [datasetOne] }] }, isLoading: false, isFetchingNextPage: false, @@ -201,7 +204,7 @@ describe('SelectDataSet', () => { embedding_available: false, }) const onSelect = vi.fn() - mockUseInfiniteDatasets.mockReturnValue({ + mockDatasetsInfiniteQuery.mockReturnValue({ data: { pages: [{ data: [datasetOne, datasetTwo] }] }, isLoading: false, isFetchingNextPage: false, @@ -233,7 +236,7 @@ describe('SelectDataSet', () => { it('should fetch the next page when infinite scroll can continue', async () => { const fetchNextPage = vi.fn() - mockUseInfiniteDatasets.mockReturnValue({ + mockDatasetsInfiniteQuery.mockReturnValue({ data: { pages: [{ data: [makeDataset({ id: 'set-1' })] }] }, isLoading: false, isFetchingNextPage: false, @@ -257,7 +260,7 @@ describe('SelectDataSet', () => { it('should skip infinite scroll fetches when a next page is unavailable or already loading', async () => { const fetchNextPage = vi.fn() - mockUseInfiniteDatasets.mockReturnValue({ + mockDatasetsInfiniteQuery.mockReturnValue({ data: { pages: [{ data: [makeDataset({ id: 'set-1' })] }] }, isLoading: false, isFetchingNextPage: true, diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx index 28c65f75d8eba8..4f66898bffee49 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx @@ -5,6 +5,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' +import { useInfiniteQuery } from '@tanstack/react-query' import { useInfiniteScroll } from 'ahooks' import { useAtomValue } from 'jotai' import * as React from 'react' @@ -18,7 +19,7 @@ import FeatureIcon from '@/app/components/header/account-setting/model-provider- import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useKnowledge } from '@/hooks/use-knowledge' import Link from '@/next/link' -import { useInfiniteDatasets } from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' type ISelectDataSetProps = { @@ -42,9 +43,15 @@ const SelectDataSet: FC = ({ const { formatIndexingTechniqueAndMethod } = useKnowledge() const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const canCreateDataset = hasPermission(workspacePermissionKeys, 'dataset.create_and_management') - const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteDatasets( - { page: 1 }, - { enabled: isShow, staleTime: 0, refetchOnMount: 'always' }, + const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery( + consoleQuery.datasets.get.infiniteOptions({ + input: (page: number) => ({ query: { page } }), + getNextPageParam: (lastPage) => (lastPage.has_more ? lastPage.page + 1 : undefined), + initialPageParam: 1, + enabled: isShow, + staleTime: 0, + refetchOnMount: 'always', + }), ) const datasets = useMemo(() => { const pages = data?.pages || [] diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/index.spec.tsx index cff81dec09681c..1c28cb2af25b0e 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/index.spec.tsx @@ -17,14 +17,18 @@ import { } from '@/models/datasets' import { consoleQuery } from '@/service/console' import { updateDatasetSetting } from '@/service/datasets' -import { useMembers } from '@/service/use-common' import { seedAccountProfileQuery } from '@/test/console/account-profile' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { createSystemFeaturesFixture } from '@/test/console/system-features' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import { DatasetACLPermission } from '@/utils/permission' import SettingsModal from '../index' +const { mockMembersQuery } = vi.hoisted(() => ({ + mockMembersQuery: vi.fn(), +})) + const toastMocks = vi.hoisted(() => ({ call: vi.fn(), dismiss: vi.fn(), @@ -81,7 +85,7 @@ vi.mock('@/service/datasets', () => ({ vi.mock('@/service/use-common', async () => ({ ...(await vi.importActual('@/service/use-common')), - useMembers: vi.fn(), + mockMembersQuery: vi.fn(), })) vi.mock('nuqs', async (importOriginal) => { @@ -113,7 +117,7 @@ vi.mock('@/app/components/datasets/settings/utils', () => ({ })) const mockUpdateDatasetSetting = updateDatasetSetting as MockedFunction -const mockUseMembers = useMembers as MockedFunction +const mockUseMembers = mockMembersQuery as MockedFunction const createRetrievalConfig = (overrides: Partial = {}): RetrievalConfig => ({ search_method: RETRIEVE_METHOD.semantic, @@ -135,9 +139,9 @@ const createDataset = ( ): DataSet => { const retrievalConfig = createRetrievalConfig(retrievalOverrides) return { + ...createDatasetFixture(), id: 'dataset-id', name: 'Test Dataset', - indexing_status: 'completed', icon_info: { icon: 'icon', icon_type: 'emoji', @@ -153,7 +157,6 @@ const createDataset = ( app_count: 0, doc_form: ChunkingMode.text, document_count: 0, - total_document_count: 0, total_available_documents: 0, word_count: 0, provider: 'internal', @@ -187,10 +190,6 @@ const createDataset = ( ...retrievalConfig, ...overrides.retrieval_model_dict, }, - retrieval_model: { - ...retrievalConfig, - ...overrides.retrieval_model, - }, } } @@ -244,7 +243,7 @@ describe('SettingsModal', () => { }, ], }, - } as ReturnType) + } as ReturnType) mockModelListQuery.mockImplementation((type: ModelTypeEnum) => { if (type === ModelTypeEnum.rerank) { return { @@ -470,7 +469,6 @@ describe('SettingsModal', () => { }, }) const dataset = createDataset({ - retrieval_model: rerankRetrieval, retrieval_model_dict: rerankRetrieval, }) @@ -522,7 +520,6 @@ describe('SettingsModal', () => { }, }) const dataset = createDataset({ - retrieval_model: hybridRetrieval, retrieval_model_dict: hybridRetrieval, }) @@ -568,7 +565,6 @@ describe('SettingsModal', () => { }, } as Partial) const dataset = createDataset({ - retrieval_model: hybridRetrieval, retrieval_model_dict: hybridRetrieval, }) @@ -694,6 +690,8 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { { params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] } > }) => { + if (options.queryKey[0].join('.') === 'console.workspaces.current.members.get') + return mockMembersQuery() if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options) const args = options.queryKey[1].input?.params?.model_type diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/retrieval-section.spec.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/retrieval-section.spec.tsx index d83522ff973399..d32da33f2c2155 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/retrieval-section.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/retrieval-section.spec.tsx @@ -17,6 +17,7 @@ import { } from '@/models/datasets' import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { withSelectorKey } from '@/test/i18n-mock' import { RETRIEVE_METHOD } from '@/types/app' import { RetrievalChangeTip, RetrievalSection } from '../retrieval-section' @@ -84,9 +85,9 @@ const createDataset = ( ): DataSet => { const retrievalConfig = createRetrievalConfig(retrievalOverrides) return { + ...createDatasetFixture(), id: 'dataset-id', name: 'Test Dataset', - indexing_status: 'completed', icon_info: { icon: 'icon', icon_type: 'emoji', @@ -102,7 +103,6 @@ const createDataset = ( app_count: 0, doc_form: ChunkingMode.text, document_count: 0, - total_document_count: 0, total_available_documents: 0, word_count: 0, provider: 'internal', @@ -135,10 +135,6 @@ const createDataset = ( ...retrievalConfig, ...overrides.retrieval_model_dict, }, - retrieval_model: { - ...retrievalConfig, - ...overrides.retrieval_model, - }, } } diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index bee49a88883723..3d33c1abfbd596 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -18,6 +18,7 @@ import { normalizeRetrievalConfigForSave, } from '@/app/components/datasets/common/check-rerank-model' import { IndexingType } from '@/app/components/datasets/create/step-two' +import { toRetrievalConfig } from '@/app/components/datasets/dataset-fields' import IndexMethod from '@/app/components/datasets/settings/index-method' import PermissionSelector from '@/app/components/datasets/settings/permission-selector' import { checkShowMultiModalTip } from '@/app/components/datasets/settings/utils' @@ -31,7 +32,6 @@ import { useDocLink } from '@/context/i18n' import { DatasetPermission } from '@/models/datasets' import { consoleQuery } from '@/service/console' import { updateDatasetSetting } from '@/service/datasets' -import { useMembers } from '@/service/use-common' import { RetrievalChangeTip, RetrievalSection } from './retrieval-section' type SettingsModalProps = { @@ -75,22 +75,24 @@ const SettingsModal: FC = ({ const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser) const [loading, setLoading] = useState(false) const [localeCurrentDataset, setLocaleCurrentDataset] = useState({ ...currentDataset }) - const [topK, setTopK] = useState(localeCurrentDataset?.external_retrieval_model.top_k ?? 2) + const [topK, setTopK] = useState(localeCurrentDataset.external_retrieval_model?.top_k ?? 2) const [scoreThreshold, setScoreThreshold] = useState( - localeCurrentDataset?.external_retrieval_model.score_threshold ?? 0.5, + localeCurrentDataset.external_retrieval_model?.score_threshold ?? 0.5, ) const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState( - localeCurrentDataset?.external_retrieval_model.score_threshold_enabled ?? false, + localeCurrentDataset.external_retrieval_model?.score_threshold_enabled ?? false, ) const [selectedMemberIDs, setSelectedMemberIDs] = useState( currentDataset.partial_member_list || [], ) const [memberList, setMemberList] = useState([]) - const { data: membersData } = useMembers() + const { data: membersData } = useQuery( + consoleQuery.workspaces.current.members.get.queryOptions({ input: {} }), + ) - const [indexMethod, setIndexMethod] = useState(currentDataset.indexing_technique) - const [retrievalConfig, setRetrievalConfig] = useState( - localeCurrentDataset?.retrieval_model_dict as RetrievalConfig, + const [indexMethod, setIndexMethod] = useState(currentDataset.indexing_technique ?? undefined) + const [retrievalConfig, setRetrievalConfig] = useState(() => + toRetrievalConfig(currentDataset.retrieval_model_dict), ) const [keywordNumber, setKeywordNumber] = useState(currentDataset.keyword_number ?? 10) @@ -99,8 +101,8 @@ const SettingsModal: FC = ({ } const [isHideChangedTip, setIsHideChangedTip] = useState(false) const isRetrievalChanged = - !isEqual(retrievalConfig, localeCurrentDataset?.retrieval_model_dict) || - indexMethod !== localeCurrentDataset?.indexing_technique + !isEqual(retrievalConfig, toRetrievalConfig(localeCurrentDataset.retrieval_model_dict)) || + indexMethod !== (localeCurrentDataset.indexing_technique ?? undefined) const handleSettingsChange = (data: { top_k?: number @@ -115,8 +117,9 @@ const SettingsModal: FC = ({ setLocaleCurrentDataset({ ...localeCurrentDataset, external_retrieval_model: { - ...localeCurrentDataset?.external_retrieval_model, - ...data, + top_k: data.top_k ?? topK, + score_threshold: data.score_threshold ?? scoreThreshold, + score_threshold_enabled: data.score_threshold_enabled ?? scoreThresholdEnabled, }, }) } @@ -160,9 +163,9 @@ const SettingsModal: FC = ({ embedding_model: localeCurrentDataset.embedding_model, embedding_model_provider: localeCurrentDataset.embedding_model_provider, ...(isExternal && { - external_knowledge_id: currentDataset!.external_knowledge_info.external_knowledge_id, + external_knowledge_id: currentDataset.external_knowledge_info?.external_knowledge_id, external_knowledge_api_id: - currentDataset!.external_knowledge_info.external_knowledge_api_id, + currentDataset.external_knowledge_info?.external_knowledge_api_id, external_retrieval_model: { top_k: topK, score_threshold: scoreThreshold, @@ -183,7 +186,7 @@ const SettingsModal: FC = ({ toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) onSave({ ...localeCurrentDataset, - indexing_technique: indexMethod, + indexing_technique: indexMethod ?? null, retrieval_model_dict: retrievalConfig, }) } catch { @@ -201,8 +204,8 @@ const SettingsModal: FC = ({ const showMultiModalTip = useMemo(() => { return checkShowMultiModalTip({ embeddingModel: { - provider: localeCurrentDataset.embedding_model_provider, - model: localeCurrentDataset.embedding_model, + provider: localeCurrentDataset.embedding_model_provider ?? '', + model: localeCurrentDataset.embedding_model ?? '', }, rerankingEnable: retrievalConfig.reranking_enable, rerankModel: { @@ -305,7 +308,7 @@ const SettingsModal: FC = ({ disabled={!localeCurrentDataset?.embedding_available} value={indexMethod} onChange={setIndexMethod} - currentValue={currentDataset.indexing_technique} + currentValue={currentDataset.indexing_technique ?? undefined} keywordNumber={keywordNumber} onKeywordNumberChange={setKeywordNumber} /> @@ -324,8 +327,8 @@ const SettingsModal: FC = ({ diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx index 9667f2f21af944..fef660c7fdc95e 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx @@ -75,11 +75,11 @@ const ExternalRetrievalSection: FC = ({ className="i-custom-vender-solid-development-api-connection-mod size-4 text-text-secondary" />
- {currentDataset?.external_knowledge_info.external_knowledge_api_name} + {currentDataset?.external_knowledge_info?.external_knowledge_api_name}
·
- {currentDataset?.external_knowledge_info.external_knowledge_api_endpoint} + {currentDataset?.external_knowledge_info?.external_knowledge_api_endpoint}
@@ -93,7 +93,7 @@ const ExternalRetrievalSection: FC = ({
- {currentDataset?.external_knowledge_info.external_knowledge_id} + {currentDataset?.external_knowledge_info?.external_knowledge_id}
@@ -105,7 +105,8 @@ const ExternalRetrievalSection: FC = ({ ) type InternalRetrievalSectionProps = CommonSectionProps & { - indexMethod: IndexingType + // Unset until the knowledge base indexes its first document. + indexMethod?: IndexingType retrievalConfig: RetrievalConfig showMultiModalTip: boolean onRetrievalConfigChange: (value: RetrievalConfig) => void diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx index d6049b27283f99..256a40e77d970d 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx @@ -99,7 +99,6 @@ vi.mock('@/service/base', () => ({ get: vi.fn(() => Promise.resolve({ data: {} })), del: vi.fn(() => Promise.resolve({ data: {} })), patch: vi.fn(() => Promise.resolve({ data: {} })), - put: vi.fn(() => Promise.resolve({ data: {} })), })) vi.mock('@/service/fetch', () => ({ diff --git a/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts b/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts index 7beac4051248a9..2bc68191674bec 100644 --- a/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts +++ b/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts @@ -557,7 +557,6 @@ describe('useConfiguration utils', () => { ids: ['dataset-1'], page: 1, }, - url: '/datasets', }) expect(state.collectionList[0]!.icon).toBe('/console/tool.svg') expect(state.promptMode).toBe('advanced') @@ -622,7 +621,6 @@ describe('useConfiguration utils', () => { const state = await loadConfigurationState({ appId: 'app-2' }) expect(mockFetchDatasets).toHaveBeenCalledWith({ - url: '/datasets', params: { page: 1, ids: ['dataset-from-tool'], diff --git a/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx b/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx index 11ebc94ba1c2c5..ea77656b68198b 100644 --- a/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx +++ b/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx @@ -16,6 +16,15 @@ import { ModelModeType } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import { useConfiguration } from '../use-configuration' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ + data: undefined, + }), + }) +}) + let mockAppPermissionKeys: string[] = [AppACLPermission.Edit, AppACLPermission.ReleaseAndVersion] const createRouteAppDetail = (overrides: Partial = {}) => @@ -110,12 +119,6 @@ vi.mock('nuqs', async (importOriginal) => { return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] } }) -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ - data: undefined, - }), -})) - vi.mock('@/hooks/use-breakpoints', () => ({ __esModule: true, default: () => 'desktop', @@ -191,6 +194,7 @@ vi.mock('@/service/console', async () => { ...actual, consoleQuery: { ...actual.consoleQuery, + files: actual.consoleQuery.files, account: actual.consoleQuery.account, apps: { byAppId: { diff --git a/web/app/components/app/configuration/hooks/configuration-lifecycle/load.ts b/web/app/components/app/configuration/hooks/configuration-lifecycle/load.ts index 4633cd27dc8ee3..6d23c62ff87fc2 100644 --- a/web/app/components/app/configuration/hooks/configuration-lifecycle/load.ts +++ b/web/app/components/app/configuration/hooks/configuration-lifecycle/load.ts @@ -41,7 +41,6 @@ export async function loadConfigurationState({ if (datasetIds.length) { const { data } = await fetchDatasets({ - url: '/datasets', params: { page: 1, ids: datasetIds, diff --git a/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts b/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts index 7f820369fa286d..3bbdaf733ef0ab 100644 --- a/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts +++ b/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts @@ -129,6 +129,7 @@ const useAdvancedPromptConfig = ({ hasSetDataSet, }) if (modelModeType === ModelModeType.chat) { + if (!chat_prompt_config) return const newPromptConfig = produce(chat_prompt_config, (draft) => { draft.prompt = draft.prompt.map((p) => { return { @@ -139,6 +140,7 @@ const useAdvancedPromptConfig = ({ }) setChatPromptConfig(newPromptConfig) } else { + if (!completion_prompt_config) return const newPromptConfig = produce(completion_prompt_config, (draft) => { draft.prompt.text = draft.prompt.text.replace( PRE_PROMPT_PLACEHOLDER_TEXT, @@ -163,6 +165,7 @@ const useAdvancedPromptConfig = ({ }) if (toModelModeType === ModelModeType.completion) { + if (!completion_prompt_config) return const newPromptConfig = produce(completion_prompt_config, (draft) => { if (!completionPromptConfig.prompt?.text) draft.prompt.text = draft.prompt.text.replace( @@ -191,6 +194,7 @@ const useAdvancedPromptConfig = ({ } setStop(stop) // switch mode's params is async. It may override the stop value. } else { + if (!chat_prompt_config) return const newPromptConfig = produce(chat_prompt_config, (draft) => { draft.prompt = draft.prompt.map((p) => { return { diff --git a/web/app/components/app/configuration/hooks/use-configuration.ts b/web/app/components/app/configuration/hooks/use-configuration.ts index 840993dedf4e40..514e96a908a3c5 100644 --- a/web/app/components/app/configuration/hooks/use-configuration.ts +++ b/web/app/components/app/configuration/hooks/use-configuration.ts @@ -6,6 +6,7 @@ import type { ConfigurationAppMode } from '@/app/components/app/configuration/ho import type { OnFeaturesChange } from '@/app/components/base/features/types' import type { Collection } from '@/app/components/tools/types' import type { Inputs, ModelConfig, PromptConfig, PromptVariable } from '@/models/debug' +import { useQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { produce } from 'immer' import { useQueryState } from 'nuqs' @@ -26,7 +27,7 @@ import { } from '@/app/components/header/account-setting/query-params' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { PromptMode } from '@/models/debug' -import { useFileUploadConfig } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { ModelModeType } from '@/types/app' import { supportFunctionCall } from '@/utils/tool-call' import { buildConfigurationFeaturesData, getConfigurationPublishingState } from '../utils' @@ -57,7 +58,7 @@ export const useConfiguration = (): ConfigurationViewModel => { serverLatestPublishedAt, updateModelConfig, } = useConfigurationAppContext() - const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: fileUploadConfigResponse } = useQuery(consoleQuery.files.upload.get.queryOptions()) const [showAppConfigureFeaturesModal, setShowAppConfigureFeaturesModal] = useState(false) const [formattingChanged, setFormattingChanged] = useState(false) const [hasFetchedDetail, setHasFetchedDetail] = useState(false) diff --git a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx index 319744604744c4..e3e4a60fa09b41 100644 --- a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx +++ b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx @@ -2,6 +2,39 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { mockEmojiData, renderWithEmoji as render } from '@/test/emoji-picker' import ExternalDataToolModal from '../external-data-tool-modal' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'codeBasedExtension.get': () => ({ + data: { + data: [ + { + name: 'code-tool', + label: { + 'en-US': 'Code Provider', + 'zh-Hans': '代码提供方', + }, + form_schema: [ + { + variable: 'api_key', + default: 'default-key', + required: true, + type: 'text', + placeholder: '', + options: [], + label: { + 'en-US': 'API Key', + 'zh-Hans': '接口密钥', + }, + }, + ], + }, + ], + }, + }), + }) +}) + const mockToastError = vi.fn() let mockLocale = 'en-US' @@ -20,36 +53,6 @@ vi.mock('#i18n', async (importOriginal) => ({ useLocale: () => mockLocale, })) -vi.mock('@/service/use-common', () => ({ - useCodeBasedExtensions: () => ({ - data: { - data: [ - { - name: 'code-tool', - label: { - 'en-US': 'Code Provider', - 'zh-Hans': '代码提供方', - }, - form_schema: [ - { - variable: 'api_key', - default: 'default-key', - required: true, - type: 'text', - placeholder: '', - options: [], - label: { - 'en-US': 'API Key', - 'zh-Hans': '接口密钥', - }, - }, - ], - }, - ], - }, - }), -})) - vi.mock('@/app/components/base/app-icon', () => ({ default: ({ onClick }: { onClick: () => void }) => ( diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index cfa2c4bff0ac77..2bfd2667422f93 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -11,6 +11,7 @@ import { SelectTrigger, SelectValue, } from '@langgenius/dify-ui/select' +import { useQuery } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -25,7 +26,7 @@ import { } from '@/app/components/base/icon-picker' import { ApiBasedExtensionSelector } from '@/app/components/header/account-setting/api-based-extension-page/selector' import { useDocLink } from '@/context/i18n' -import { useCodeBasedExtensions } from '@/service/use-common' +import { codeBasedExtensionsQueryOptions } from '@/features/code-based-extension/query' import { buildProviders, formatExternalDataTool, @@ -49,7 +50,9 @@ const ExternalDataToolModal: FC = ({ const docLink = useDocLink() const locale = useLocale() const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' }) - const { data: codeBasedExtensionList } = useCodeBasedExtensions('external_data_tool') + const { data: codeBasedExtensionList } = useQuery( + codeBasedExtensionsQueryOptions('external_data_tool'), + ) const providers = buildProviders({ codeBasedExtensionList, diff --git a/web/app/components/app/deploy/__tests__/index.spec.tsx b/web/app/components/app/deploy/__tests__/index.spec.tsx index df8515bc0cd36b..de6df06a254a59 100644 --- a/web/app/components/app/deploy/__tests__/index.spec.tsx +++ b/web/app/components/app/deploy/__tests__/index.spec.tsx @@ -822,24 +822,27 @@ vi.mock('@/hooks/use-timestamp', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllMCPTools: () => ({ - data: [ - { - icon: '/notion-mcp.svg', - id: 'notion', - name: 'Notion', - plugin_id: 'langgenius/notion', - }, - ], - }), - useAllWorkflowTools: () => ({ data: [] }), - useMCPServerDetail: () => ({ - data: mockBuiltInEnvironment.mcpServerDetail, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ + data: [ + { + icon: '/notion-mcp.svg', + id: 'notion', + name: 'Notion', + plugin_id: 'langgenius/notion', + }, + ], + }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'apps.byAppId.server.get': () => ({ + data: mockBuiltInEnvironment.mcpServerDetail, + }), + }) +}) vi.mock('@/next/navigation', async (importOriginal) => ({ ...(await importOriginal()), diff --git a/web/app/components/app/deploy/built-in-environment-card/index.tsx b/web/app/components/app/deploy/built-in-environment-card/index.tsx index c576f94b04315b..ca83bbecce13b2 100644 --- a/web/app/components/app/deploy/built-in-environment-card/index.tsx +++ b/web/app/components/app/deploy/built-in-environment-card/index.tsx @@ -10,7 +10,6 @@ import { BlockEnum, isTriggerNode } from '@/app/components/workflow/types' import useTimestamp from '@/hooks/use-timestamp' import { useParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' -import { useMCPServerDetail } from '@/service/use-tools' import { appWorkflowQueryOptions } from '@/service/workflow-queries' import { AccessPointIcon } from '../shared/access-point-icon' import { RuntimeStateIndicator } from '../shared/runtime-state' @@ -30,7 +29,11 @@ export const BuiltInEnvironmentCard = memo( consoleQuery.apps.byAppId.get.queryOptions({ input: { params: { app_id: appId } } }), ) const { data: publishedWorkflow } = useQuery(appWorkflowQueryOptions(appId)) - const { data: mcpServerDetail } = useMCPServerDetail(appId) + const { data: mcpServerDetail } = useQuery( + consoleQuery.apps.byAppId.server.get.queryOptions({ + input: { params: { app_id: appId } }, + }), + ) const publishedNodes = Array.isArray(publishedWorkflow?.graph.nodes) ? (publishedWorkflow.graph.nodes as Node[]) : [] diff --git a/web/app/components/app/deploy/shared/deployment-configuration/__tests__/use-provider-icon.spec.tsx b/web/app/components/app/deploy/shared/deployment-configuration/__tests__/use-provider-icon.spec.tsx index d698f9f945d97d..b3975a4347910c 100644 --- a/web/app/components/app/deploy/shared/deployment-configuration/__tests__/use-provider-icon.spec.tsx +++ b/web/app/components/app/deploy/shared/deployment-configuration/__tests__/use-provider-icon.spec.tsx @@ -10,12 +10,15 @@ import { useGetProviderIcon } from '../use-provider-icon' const request = vi.hoisted(() => vi.fn<(url: string) => Promise>()) vi.mock('@/service/base', () => ({ request })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({}), - useAllCustomTools: () => ({}), - useAllWorkflowTools: () => ({}), - useAllMCPTools: () => ({}), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({}), + 'workspaces.current.tools.api.get': () => ({}), + 'workspaces.current.tools.workflow.get': () => ({}), + 'workspaces.current.tools.mcp.get': () => ({}), + }) +}) vi.mock('@/service/use-triggers', () => ({ useAllTriggerPlugins: () => ({}) })) vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }) })) vi.mock('@/utils/var', async (importOriginal) => ({ diff --git a/web/app/components/app/deploy/shared/deployment-configuration/use-provider-icon.ts b/web/app/components/app/deploy/shared/deployment-configuration/use-provider-icon.ts index efc796759d24a0..3ec0579b15aae4 100644 --- a/web/app/components/app/deploy/shared/deployment-configuration/use-provider-icon.ts +++ b/web/app/components/app/deploy/shared/deployment-configuration/use-provider-icon.ts @@ -2,16 +2,11 @@ import type { UnsupportedNode } from '@dify/contracts/enterprise-app-deploy/type import type { Node } from '@/app/components/workflow/types' import { useQuery } from '@tanstack/react-query' import { useCallback } from 'react' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { findNodeIcon } from '@/app/components/workflow/hooks/use-tool-icon' import { BlockEnum } from '@/app/components/workflow/types' import useTheme from '@/hooks/use-theme' import { consoleQuery } from '@/service/console' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' const PROVIDER_ICON_BLOCK_TYPES = new Set([ @@ -35,10 +30,30 @@ export const useGetProviderIcon = (nodes: UnsupportedNode[]) => { const hasToolNode = nodes.some((node) => node.type === BlockEnum.Tool) const hasDataSourceNode = nodes.some((node) => node.type === BlockEnum.DataSource) const hasTriggerPluginNode = nodes.some((node) => node.type === BlockEnum.TriggerPlugin) - const { data: buildInTools } = useAllBuiltInTools(hasToolNode) - const { data: customTools } = useAllCustomTools(hasToolNode) - const { data: workflowTools } = useAllWorkflowTools(hasToolNode) - const { data: mcpTools } = useAllMCPTools(hasToolNode) + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + enabled: hasToolNode, + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: hasToolNode, + select: selectToolsWithProvider, + }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: hasToolNode, + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: hasToolNode, + select: selectToolsWithProvider, + }), + ) const { data: dataSourceList } = useQuery( consoleQuery.rag.pipelines.datasourcePlugins.get.queryOptions({ enabled: hasDataSourceNode, diff --git a/web/app/components/app/log/__tests__/filter.spec.tsx b/web/app/components/app/log/__tests__/filter.spec.tsx index fda389b312f779..f47249346dcb09 100644 --- a/web/app/components/app/log/__tests__/filter.spec.tsx +++ b/web/app/components/app/log/__tests__/filter.spec.tsx @@ -27,19 +27,30 @@ const render = (ui: ReactElement) => { queryFn: () => new Promise(() => {}), }) } else seedFeatures(queryClient, { billing: { subscription: { plan: scenario.planType } } }) + const annotationsCountQueryKey = consoleQuery.apps.byAppId.annotations.count.get.queryKey({ + input: { params: { app_id: 'test-app-id' } }, + }) + if (mockAnnotationsCountLoading) { + void queryClient.query({ + queryKey: annotationsCountQueryKey, + queryFn: () => new Promise(() => {}), + }) + } else if (mockAnnotationsCountData) { + queryClient.setQueryData(annotationsCountQueryKey, mockAnnotationsCountData) + } else { + void queryClient + .query({ + queryKey: annotationsCountQueryKey, + queryFn: () => Promise.reject(new Error('annotations count unavailable')), + }) + .catch(() => {}) + } return renderWithConsoleQuery(ui, { queryClient, systemFeatures: { deployment_edition: scenario.deploymentEdition }, }) } -vi.mock('@/service/use-log', () => ({ - useAnnotationsCount: () => ({ - data: mockAnnotationsCountData, - isLoading: mockAnnotationsCountLoading, - }), -})) - vi.mock('@/app/components/base/chip', async () => { const { useState } = await import('react') @@ -350,7 +361,7 @@ describe('Filter', () => { it('should display annotated status with count when annotation_status is annotated', () => { const propsWithAnnotation = { ...defaultProps, - queryParams: { ...defaultQueryParams, annotation_status: 'annotated' }, + queryParams: { ...defaultQueryParams, annotation_status: 'annotated' as const }, } render() @@ -363,7 +374,7 @@ describe('Filter', () => { it('should display not_annotated status when annotation_status is not_annotated', () => { const propsWithNotAnnotated = { ...defaultProps, - queryParams: { ...defaultQueryParams, annotation_status: 'not_annotated' }, + queryParams: { ...defaultQueryParams, annotation_status: 'not_annotated' as const }, } render() @@ -394,7 +405,7 @@ describe('Filter', () => { const propsWithSort = { ...defaultProps, isChatMode: true, - queryParams: { ...defaultQueryParams, sort_by: 'created_at' }, + queryParams: { ...defaultQueryParams, sort_by: 'created_at' as const }, } render() @@ -408,7 +419,7 @@ describe('Filter', () => { const propsWithDescSort = { ...defaultProps, isChatMode: true, - queryParams: { ...defaultQueryParams, sort_by: '-created_at' }, + queryParams: { ...defaultQueryParams, sort_by: '-created_at' as const }, } render() diff --git a/web/app/components/app/log/__tests__/index.spec.tsx b/web/app/components/app/log/__tests__/index.spec.tsx index 035b692d01fe7d..0ea362539cf448 100644 --- a/web/app/components/app/log/__tests__/index.spec.tsx +++ b/web/app/components/app/log/__tests__/index.spec.tsx @@ -34,13 +34,31 @@ vi.mock('@/next/navigation', () => ({ }), })) -vi.mock('@/service/use-log', () => ({ - useChatConversations: (...args: unknown[]) => mockUseChatConversations(...args), - useCompletionConversations: (...args: unknown[]) => mockUseCompletionConversations(...args), - useAnnotationsCount: () => ({ - data: { count: 0 }, - isLoading: false, - }), +vi.mock('@/service/console', () => { + const tagged = (operation: string) => ({ + queryOptions: (options: Record) => ({ ...options, operation }), + }) + return { + consoleQuery: { + apps: { + byAppId: { + chatConversations: { get: tagged('chatConversations') }, + completionConversations: { get: tagged('completionConversations') }, + annotations: { count: { get: tagged('annotationsCount') } }, + }, + }, + }, + } +}) + +vi.mock('@tanstack/react-query', async (importOriginal) => ({ + ...(await importOriginal()), + useQuery: ({ operation, ...options }: { operation?: string }) => { + if (operation === 'chatConversations') return mockUseChatConversations(options) + if (operation === 'completionConversations') return mockUseCompletionConversations(options) + if (operation === 'annotationsCount') return { data: { count: 0 }, isLoading: false } + return { data: undefined } + }, })) vi.mock('../cloud-sandbox-retention', async (importOriginal) => { @@ -101,7 +119,7 @@ describe('Logs', () => { expect(mockUseChatConversations).toHaveBeenCalledWith( expect.objectContaining({ - appId: 'app-1', + input: expect.objectContaining({ params: { app_id: 'app-1' } }), }), ) expect(screen.getByRole('heading', { name: /(?:^|\.)title(?=$|:)/ })).toBeInTheDocument() @@ -132,7 +150,7 @@ describe('Logs', () => { expect(mockUseCompletionConversations).toHaveBeenCalledWith( expect.objectContaining({ - appId: 'app-2', + input: expect.objectContaining({ params: { app_id: 'app-2' } }), }), ) expect(screen.getByText('empty-logs')).toBeInTheDocument() @@ -187,9 +205,11 @@ describe('Logs', () => { ).toBeInTheDocument() expect(mockUseChatConversations.mock.calls.at(-1)?.[0]).toEqual( expect.objectContaining({ - params: expect.objectContaining({ - start: dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), - end: dayjs().endOf('day').format('YYYY-MM-DD HH:mm'), + input: expect.objectContaining({ + query: expect.objectContaining({ + start: dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), + end: dayjs().endOf('day').format('YYYY-MM-DD HH:mm'), + }), }), }), ) @@ -212,9 +232,11 @@ describe('Logs', () => { await user.click(await screen.findByText(/appLog\.filter\.period\.allTime/)) expect(mockUseChatConversations.mock.calls.at(-1)?.[0]).toEqual( expect.objectContaining({ - params: expect.not.objectContaining({ - start: expect.anything(), - end: expect.anything(), + input: expect.objectContaining({ + query: expect.not.objectContaining({ + start: expect.anything(), + end: expect.anything(), + }), }), }), ) @@ -233,9 +255,11 @@ describe('Logs', () => { ).toBeInTheDocument() expect(mockUseChatConversations.mock.calls.at(-1)?.[0]).toEqual( expect.objectContaining({ - params: expect.objectContaining({ - start: dayjs().startOf('day').format('YYYY-MM-DD HH:mm'), - end: expect.any(String), + input: expect.objectContaining({ + query: expect.objectContaining({ + start: dayjs().startOf('day').format('YYYY-MM-DD HH:mm'), + end: expect.any(String), + }), }), }), ) diff --git a/web/app/components/app/log/__tests__/list-utils.spec.ts b/web/app/components/app/log/__tests__/list-utils.spec.ts index 837b05db0015fa..d28c818a881f3d 100644 --- a/web/app/components/app/log/__tests__/list-utils.spec.ts +++ b/web/app/components/app/log/__tests__/list-utils.spec.ts @@ -1,4 +1,6 @@ +import type { MessageDetailResponse } from '@dify/contracts/api/console/apps/types.gen' import type { IChatItem } from '@/app/components/base/chat/chat/type' +import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { applyAnnotationAdded, applyAnnotationEdited, @@ -22,11 +24,30 @@ const createChatItems = (): IChatItem[] => { id: 'answer-2', content: 'reply', isAnswer: true, parentMessageId: 'question-2' }, ] as IChatItem[] +const createMessage = (overrides: Partial): MessageDetailResponse => ({ + id: 'message', + conversation_id: 'conversation', + inputs: {}, + query: '', + answer: '', + answer_tokens: 0, + message_tokens: 0, + message: [], + message_files: [], + agent_thoughts: [], + feedbacks: [], + metadata: {}, + from_source: 'console', + provider_response_latency: 0, + status: 'normal', + ...overrides, +}) + describe('log list utils', () => { it('should format chat messages into paired question and answer items', () => { const items = getFormattedChatList( [ - { + createMessage({ id: 'message-1', inputs: { query: 'hello' }, query: 'hello', @@ -36,11 +57,23 @@ describe('log list utils', () => { message_tokens: 2, message: [{ role: 'user', text: 'hello' }], message_files: [ - { belongs_to: 'assistant', id: 'file-1' }, - { belongs_to: 'user', id: 'file-2' }, + { + belongs_to: 'assistant', + id: 'file-1', + filename: '', + transfer_method: 'local_file', + type: 'image', + }, + { + belongs_to: 'user', + id: 'file-2', + filename: '', + transfer_method: 'local_file', + type: 'image', + }, ], - }, - ] as any, + }), + ], 'conversation-1', 'UTC', 'YYYY-MM-DD', @@ -67,7 +100,7 @@ describe('log list utils', () => { it('should preserve feedback and annotation hit history when formatting chat items', () => { const items = getFormattedChatList( [ - { + createMessage({ id: 'message-2', inputs: { default_input: 'fallback prompt' }, query: '', @@ -82,11 +115,11 @@ describe('log list utils', () => { ], annotation_hit_history: { annotation_id: 'annotation-1', - annotation_create_account: {}, + annotation_create_account: null, created_at: 123, }, - }, - ] as any, + }), + ], 'conversation-2', 'UTC', 'YYYY-MM-DD', @@ -259,6 +292,55 @@ describe('log list utils', () => { ).toEqual(['https://example.com/file-1']) }) + it('keeps agent thought files raw so the agent renderer converts them once', () => { + const [, answer] = getFormattedChatList( + [ + createMessage({ + id: 'message-1', + agent_thoughts: [ + { + id: 'thought-1', + message_id: 'message-1', + position: 1, + files: ['file-1'], + tool: 'image_generator', + tool_labels: {}, + }, + ], + message_files: [ + { + belongs_to: 'assistant', + id: 'file-1', + filename: 'image.png', + transfer_method: 'tool_file', + type: 'image', + url: 'https://example.com/image.png', + }, + ], + }), + ], + 'conversation-1', + 'UTC', + 'YYYY-MM-DD', + ) + + const thoughtFiles = answer!.agent_thoughts![0]!.message_files! + expect(thoughtFiles).toEqual([ + expect.objectContaining({ + id: 'file-1', + type: 'image', + url: 'https://example.com/image.png', + }), + ]) + // Mirrors AgentContent, which converts thought files for FileList. + const [rendered] = getProcessedFilesFromResponse( + thoughtFiles.map((file) => ({ ...file, related_id: file.id }) as never), + ) + expect(rendered).toEqual( + expect.objectContaining({ supportFileType: 'image', url: 'https://example.com/image.png' }), + ) + }) + it('should handle default inputs', () => { expect( getConversationRowValues({ diff --git a/web/app/components/app/log/__tests__/list.spec.tsx b/web/app/components/app/log/__tests__/list.spec.tsx index 25d55db8ed094a..30a1dd32f1016e 100644 --- a/web/app/components/app/log/__tests__/list.spec.tsx +++ b/web/app/components/app/log/__tests__/list.spec.tsx @@ -1,3 +1,4 @@ +import type { MessageDetailResponse } from '@dify/contracts/api/console/apps/types.gen' import type { ReactNode } from 'react' import type { IChatItem } from '@/app/components/base/chat/chat/type' import { act, fireEvent, screen, waitFor, within } from '@testing-library/react' @@ -34,24 +35,35 @@ vi.mock('@/hooks/use-breakpoints', () => ({ }, })) -vi.mock('@/service/use-log', () => ({ - useChatConversationDetail: () => ({ - data: mockChatConversationDetail, - }), - useCompletionConversationDetail: () => ({ - data: mockCompletionConversationDetail, - refetch: mockCompletionRefetch, - }), -})) - -vi.mock('@/service/log', () => ({ - fetchChatMessages: (...args: unknown[]) => mockFetchChatMessages(...args), - updateLogMessageFeedbacks: (...args: unknown[]) => mockUpdateLogMessageFeedbacks(...args), - updateLogMessageAnnotations: (...args: unknown[]) => mockUpdateLogMessageAnnotations(...args), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useQuery: (options: Parameters[0]) => { + const [path] = options.queryKey as [string[]] + if (Array.isArray(path) && path.includes('byConversationId')) { + if (path.includes('chatConversations')) return { data: mockChatConversationDetail } + return { data: mockCompletionConversationDetail, refetch: mockCompletionRefetch } + } + return actual.useQuery(options) + }, + } +}) -vi.mock('@/service/annotation', () => ({ - delAnnotation: (...args: unknown[]) => mockDelAnnotation(...args), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + apps: { + byAppId: { + chatMessages: { get: (...args: unknown[]) => mockFetchChatMessages(...args) }, + feedbacks: { post: (...args: unknown[]) => mockUpdateLogMessageFeedbacks(...args) }, + annotations: { + post: (...args: unknown[]) => mockUpdateLogMessageAnnotations(...args), + byAnnotationId: { delete: (...args: unknown[]) => mockDelAnnotation(...args) }, + }, + }, + }, + }, })) vi.mock('@/app/components/base/loading-placeholder', () => ({ @@ -59,7 +71,9 @@ vi.mock('@/app/components/base/loading-placeholder', () => ({ })) vi.mock('@/app/components/app/log/model-info', () => ({ - default: ({ model }: { model: string }) =>
{model}
, + default: ({ model }: { model: { name: string } }) => ( +
{model.name}
+ ), })) vi.mock('@/app/components/app/log/var-panel', () => ({ @@ -285,8 +299,17 @@ const createCompletionLogs = () => ({ ], }) -const createChatMessage = (id: string, overrides: Record = {}) => ({ +const createChatMessage = ( + id: string, + overrides: Partial = {}, +): MessageDetailResponse => ({ id, + conversation_id: 'conversation-1', + agent_thoughts: [], + metadata: {}, + from_source: 'console', + provider_response_latency: 0, + status: 'normal', answer: `Assistant reply ${id}`, query: `Question ${id}`, created_at: 1710000000, @@ -298,7 +321,7 @@ const createChatMessage = (id: string, overrides: Record = {}) message_files: [], answer_tokens: 10, message_tokens: 5, - parent_message_id: undefined, + parent_message_id: null, ...overrides, }) @@ -400,11 +423,9 @@ describe('ConversationList', () => { mockChatConversationDetail = { id: 'conversation-1', created_at: 1710000000, + introduction: 'Hello there', model_config: { - model: 'gpt-4o', - configs: { - introduction: 'Hello there', - }, + model: { provider: 'openai', name: 'gpt-4o', mode: 'chat', completion_params: {} }, user_input_form: [], }, message: { @@ -442,11 +463,9 @@ describe('ConversationList', () => { mockChatConversationDetail = { id: 'conversation-1', created_at: 1710000000, + introduction: 'Hello there', model_config: { - model: 'gpt-4o', - configs: { - introduction: 'Hello there', - }, + model: { provider: 'openai', name: 'gpt-4o', mode: 'chat', completion_params: {} }, user_input_form: [ { query: { @@ -485,11 +504,8 @@ describe('ConversationList', () => { await waitFor(() => { expect(mockFetchChatMessages).toHaveBeenCalledWith({ - url: '/apps/app-1/chat-messages', - params: { - conversation_id: 'conversation-1', - limit: 10, - }, + params: { app_id: 'app-1' }, + query: { conversation_id: 'conversation-1', limit: 10 }, }) }) @@ -504,7 +520,7 @@ describe('ConversationList', () => { await waitFor(() => { expect(mockUpdateLogMessageFeedbacks).toHaveBeenCalledWith({ - url: '/apps/app-1/feedbacks', + params: { app_id: 'app-1' }, body: { message_id: 'message-1', rating: 'like', @@ -526,11 +542,9 @@ describe('ConversationList', () => { mockChatConversationDetail = { id: 'conversation-1', created_at: 1710000000, + introduction: 'Hello there', model_config: { - model: 'gpt-4o', - configs: { - introduction: 'Hello there', - }, + model: { provider: 'openai', name: 'gpt-4o', mode: 'chat', completion_params: {} }, user_input_form: [], }, message: { @@ -556,7 +570,7 @@ describe('ConversationList', () => { id: 'conversation-1', created_at: 1710000000, model_config: { - model: 'gpt-4o-mini', + model: { provider: 'openai', name: 'gpt-4o-mini', mode: 'chat', completion_params: {} }, user_input_form: [ { query: { @@ -594,7 +608,7 @@ describe('ConversationList', () => { await waitFor(() => { expect(mockUpdateLogMessageFeedbacks).toHaveBeenCalledWith({ - url: '/apps/app-1/feedbacks', + params: { app_id: 'app-1' }, body: { message_id: 'message-1', rating: 'like', @@ -674,11 +688,9 @@ describe('ConversationList', () => { mockChatConversationDetail = { id: 'conversation-1', created_at: 1710000000, + introduction: 'Hello there', model_config: { - model: 'gpt-4o', - configs: { - introduction: 'Hello there', - }, + model: { provider: 'openai', name: 'gpt-4o', mode: 'chat', completion_params: {} }, user_input_form: [ { query: { @@ -700,7 +712,7 @@ describe('ConversationList', () => { annotation: { id: 'annotation-1', content: 'Annotated answer', - account: { name: 'Admin' }, + account: { id: 'account-1', name: 'Admin', email: 'admin@example.com' }, }, }), createChatMessage('message-2', { parent_message_id: 'message-1' }), @@ -729,7 +741,9 @@ describe('ConversationList', () => { fireEvent.click(screen.getByText('chat-remove-annotation')) await waitFor(() => { - expect(mockDelAnnotation).toHaveBeenCalledWith('app-1', 'annotation-2') + expect(mockDelAnnotation).toHaveBeenCalledWith({ + params: { app_id: 'app-1', annotation_id: 'annotation-2' }, + }) }) const scrollableDiv = document.getElementById('scrollableDiv') as HTMLDivElement diff --git a/web/app/components/app/log/filter.tsx b/web/app/components/app/log/filter.tsx index d75e54078c512e..49b6d6c9ef631d 100644 --- a/web/app/components/app/log/filter.tsx +++ b/web/app/components/app/log/filter.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import type { QueryParam } from './index' import type { I18nKeysByPrefix } from '@/types/i18n' import { RiCalendarLine } from '@remixicon/react' +import { skipToken, useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import quarterOfYear from 'dayjs/plugin/quarterOfYear' import * as React from 'react' @@ -10,7 +11,7 @@ import { useTranslation } from 'react-i18next' import Chip from '@/app/components/base/chip' import { SearchInput } from '@/app/components/base/search-input' import Sort from '@/app/components/base/sort' -import { useAnnotationsCount } from '@/service/use-log' +import { consoleQuery } from '@/service/console' import { CLOUD_SANDBOX_CLEARED_TIME_PERIOD, CLOUD_SANDBOX_TIME_PERIOD_KEYS, @@ -50,7 +51,11 @@ const Filter: FC = ({ queryParams, setQueryParams, }: IFilterProps) => { - const { data, isLoading } = useAnnotationsCount(appId) + const { data, isLoading } = useQuery( + consoleQuery.apps.byAppId.annotations.count.get.queryOptions({ + input: appId ? { params: { app_id: appId } } : skipToken, + }), + ) const { t } = useTranslation(['appLog', 'common']) const planState = useCloudSandboxPlanStatus() const isTimePeriodRestricted = isLogTimePeriodRestricted(planState) @@ -86,7 +91,10 @@ const Filter: FC = ({ showLeftIcon={false} value={queryParams.annotation_status || 'all'} onSelect={(item) => { - setQueryParams({ ...queryParams, annotation_status: item.value as string }) + setQueryParams({ + ...queryParams, + annotation_status: item.value as QueryParam['annotation_status'], + }) }} onClear={() => setQueryParams({ ...queryParams, annotation_status: 'all' })} items={[ @@ -123,7 +131,7 @@ const Filter: FC = ({ }, ]} onSelect={(value) => { - setQueryParams({ ...queryParams, sort_by: value as string }) + setQueryParams({ ...queryParams, sort_by: value as QueryParam['sort_by'] }) }} /> diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index 714253fe4985a2..4a8c59cf38c90d 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -1,8 +1,12 @@ 'use client' -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { + AppDetailWithSite, + GetAppsByAppIdChatConversationsData, +} from '@dify/contracts/api/console/apps/types.gen' import type { FC } from 'react' import { Pagination } from '@langgenius/dify-ui/pagination' +import { skipToken, useQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' import dayjs from 'dayjs' import { omit } from 'es-toolkit/object' @@ -13,7 +17,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { APP_PAGE_LIMIT } from '@/config' import { useDocLink } from '@/context/i18n' import { usePathname, useRouter, useSearchParams } from '@/next/navigation' -import { useChatConversations, useCompletionConversations } from '@/service/use-log' +import { consoleQuery } from '@/service/console' import PageTitle from '../log-annotation/page-title' import { resolveLogTimePeriod, @@ -29,11 +33,13 @@ type ILogsProps = { appDetail: AppDetailWithSite } +type ConversationsQuery = NonNullable + export type QueryParam = { period: string - annotation_status?: string + annotation_status?: ConversationsQuery['annotation_status'] keyword?: string - sort_by?: string + sort_by?: ConversationsQuery['sort_by'] } const defaultQueryParams: QueryParam = { @@ -114,16 +120,17 @@ const Logs: FC = ({ appDetail }) => { } // When the details are obtained, proceed to the next request - const { data: chatConversations, refetch: mutateChatList } = useChatConversations({ - appId: isChatMode ? appDetail.id : '', - params: query, - }) - - const { data: completionConversations, refetch: mutateCompletionList } = - useCompletionConversations({ - appId: !isChatMode ? appDetail.id : '', - params: query, - }) + const { data: chatConversations, refetch: mutateChatList } = useQuery( + consoleQuery.apps.byAppId.chatConversations.get.queryOptions({ + input: isChatMode ? { params: { app_id: appDetail.id }, query } : skipToken, + }), + ) + + const { data: completionConversations, refetch: mutateCompletionList } = useQuery( + consoleQuery.apps.byAppId.completionConversations.get.queryOptions({ + input: !isChatMode ? { params: { app_id: appDetail.id }, query } : skipToken, + }), + ) const total = isChatMode ? chatConversations?.total : completionConversations?.total const totalPages = total ? Math.max(Math.ceil(total / limit), 1) : 1 diff --git a/web/app/components/app/log/list-utils.ts b/web/app/components/app/log/list-utils.ts index c776ca9345ce03..02d5a5a929a552 100644 --- a/web/app/components/app/log/list-utils.ts +++ b/web/app/components/app/log/list-utils.ts @@ -1,132 +1,210 @@ +import type { MessageDetailResponse } from '@dify/contracts/api/console/apps/types.gen' import type { ChatItemInTree } from '../../base/chat/types' -import type { IChatItem } from '@/app/components/base/chat/chat/type' -import type { Annotation, ChatMessage } from '@/models/log' +import type { + CitationItem, + FeedbackType, + IChatItem, + ThoughtItem, +} from '@/app/components/base/chat/chat/type' +import type { Annotation } from '@/models/log' +import type { VisionFile } from '@/types/app' import type { FileResponse } from '@/types/workflow' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' import { get } from 'es-toolkit/compat' +import { isPlainObject } from 'es-toolkit/predicate' import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' +import { TransferMethod } from '@/types/app' dayjs.extend(utc) dayjs.extend(timezone) type LogDetailMessage = { answer?: string | number | null - inputs?: Record - message_files?: Array<{ url?: string }> + inputs?: Record + message_files?: Array<{ url?: string | null }> query?: string } -type UserInputFormItem = Record - type ConversationLogDetail = { - from_account_name?: string - from_end_user_session_id?: string - message?: LogDetailMessage + from_account_name?: string | null + from_end_user_session_id?: string | null + message?: LogDetailMessage | null message_count?: number model_config?: { - user_input_form?: UserInputFormItem[] - } + // Untyped JSON in the console API response. + user_input_form?: unknown + } | null name?: string } -const getUserInputVariable = (item: UserInputFormItem) => { - const variable = Object.values(item)[0]?.variable +const getUserInputVariable = (item: unknown) => { + if (!isPlainObject(item)) return undefined + const field = Object.values(item)[0] + const variable = isPlainObject(field) ? field.variable : undefined return typeof variable === 'string' ? variable : undefined } -const toFileResponse = (file: NonNullable[number]): FileResponse => ({ +type LogMessage = MessageDetailResponse + +const isTransferMethod = (value: string): value is TransferMethod => + (Object.values(TransferMethod) as string[]).includes(value) + +// Agent thoughts keep raw message files; the agent content renderer converts them once. +const toThoughtFiles = (item: LogMessage): VisionFile[] => + item.message_files.map((file) => ({ + id: file.id, + type: file.type, + transfer_method: isTransferMethod(file.transfer_method) + ? file.transfer_method + : TransferMethod.local_file, + url: file.url ?? '', + upload_file_id: file.upload_file_id ?? '', + belongs_to: file.belongs_to ?? undefined, + })) + +const toFileResponse = (file: LogMessage['message_files'][number]): FileResponse => ({ related_id: file.id ?? file.upload_file_id, extension: '', filename: '', size: 0, mime_type: '', - transfer_method: file.transfer_method, + transfer_method: isTransferMethod(file.transfer_method) + ? file.transfer_method + : TransferMethod.local_file, type: file.type, - url: file.url, - upload_file_id: file.upload_file_id, - remote_url: file.url, + url: file.url ?? '', + upload_file_id: file.upload_file_id ?? '', + remote_url: file.url ?? '', }) +const toText = (value: unknown) => (typeof value === 'string' && value ? value : undefined) + +// `inputs` values are untyped JSON in the console API response. +export const getStringInputs = (inputs: LogMessage['inputs']) => + Object.fromEntries( + Object.entries(inputs).filter( + (entry): entry is [string, string] => typeof entry[1] === 'string', + ), + ) + +const toFeedback = (feedback?: LogMessage['feedbacks'][number]): FeedbackType | undefined => + feedback?.rating === 'like' || feedback?.rating === 'dislike' + ? { rating: feedback.rating, content: feedback.content } + : undefined + +const toThoughtItems = (message: LogMessage): ThoughtItem[] => + message.agent_thoughts.map((thought) => ({ + id: thought.id, + tool: thought.tool ?? '', + thought: thought.thought ?? '', + answer: thought.answer ?? undefined, + tool_input: thought.tool_input ?? '', + // `tool_labels` is untyped JSON in the console API response. + tool_labels: isPlainObject(thought.tool_labels) + ? (thought.tool_labels as ThoughtItem['tool_labels']) + : undefined, + message_id: thought.message_id, + conversation_id: message.conversation_id, + observation: thought.observation ?? '', + position: thought.position, + files: thought.files, + })) + +// `message` and `metadata` are untyped JSON in the console API response. +const getPromptLog = (message: LogMessage) => + (Array.isArray(message.message) ? message.message : []) as NonNullable + +const getCitations = (message: LogMessage) => + isPlainObject(message.metadata) && Array.isArray(message.metadata.retriever_resources) + ? (message.metadata.retriever_resources as CitationItem[]) + : undefined + +const getAnnotation = (message: LogMessage): Annotation | undefined => { + if (message.annotation_hit_history) { + return { + id: message.annotation_hit_history.annotation_id, + authorName: message.annotation_hit_history.annotation_create_account?.name || 'N/A', + created_at: message.annotation_hit_history.created_at ?? undefined, + } + } + + if (message.annotation) { + const { account } = message.annotation + return { + id: message.annotation.id, + authorName: account?.name || 'N/A', + logAnnotation: { + id: message.annotation.id, + content: message.annotation.content, + account: { id: account?.id ?? '', name: account?.name ?? '', email: account?.email ?? '' }, + created_at: message.annotation.created_at ?? 0, + }, + created_at: 0, + } + } + + return undefined +} + export const getFormattedChatList = ( - messages: ChatMessage[], + messages: LogMessage[], conversationId: string, timezone: string, format: string, ) => { const newChatList: IChatItem[] = [] - messages.forEach((item: ChatMessage) => { - const questionFiles = item.message_files?.filter((file) => file.belongs_to === 'user') || [] + messages.forEach((item) => { + const questionFiles = getProcessedFilesFromResponse( + item.message_files.filter((file) => file.belongs_to === 'user').map(toFileResponse), + ) newChatList.push({ id: `question-${item.id}`, - content: item.inputs.query || item.inputs.default_input || item.query, + content: toText(item.inputs.query) || toText(item.inputs.default_input) || item.query, isAnswer: false, - message_files: getProcessedFilesFromResponse(questionFiles.map(toFileResponse)), + message_files: questionFiles, parentMessageId: item.parent_message_id || undefined, }) - const answerFiles = item.message_files?.filter((file) => file.belongs_to === 'assistant') || [] + const answerFiles = getProcessedFilesFromResponse( + item.message_files.filter((file) => file.belongs_to === 'assistant').map(toFileResponse), + ) + const promptLog = getPromptLog(item) newChatList.push({ id: item.id, content: item.answer, - agent_thoughts: addFileInfos( - item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, - item.message_files, + agent_thoughts: addFileInfos(sortAgentSorts(toThoughtItems(item)), toThoughtFiles(item)), + feedback: toFeedback(item.feedbacks.find((feedback) => feedback.from_source === 'user')), + adminFeedback: toFeedback( + item.feedbacks.find((feedback) => feedback.from_source === 'admin'), ), - feedback: item.feedbacks?.find((feedback) => feedback.from_source === 'user'), - adminFeedback: item.feedbacks?.find((feedback) => feedback.from_source === 'admin'), feedbackDisabled: false, isAnswer: true, - message_files: getProcessedFilesFromResponse(answerFiles.map(toFileResponse)), + message_files: answerFiles, log: [ - ...(item.message ?? []), - ...(item.message?.[item.message.length - 1]?.role !== 'assistant' - ? [ - { - role: 'assistant', - text: item.answer, - files: item.message_files?.filter((file) => file.belongs_to === 'assistant') || [], - }, - ] + ...promptLog, + ...(promptLog.at(-1)?.role !== 'assistant' + ? [{ role: 'assistant', text: item.answer, files: answerFiles }] : []), - ] as IChatItem['log'], - workflow_run_id: item.workflow_run_id, + ], + workflow_run_id: item.workflow_run_id ?? undefined, conversationId, input: { inputs: item.inputs, query: item.query, }, more: { - time: dayjs.unix(item.created_at).tz(timezone).format(format), + time: item.created_at ? dayjs.unix(item.created_at).tz(timezone).format(format) : '', tokens: item.answer_tokens + item.message_tokens, latency: (item.provider_response_latency ?? 0).toFixed(2), }, - citation: item.metadata?.retriever_resources, - annotation: (() => { - if (item.annotation_hit_history) { - return { - id: item.annotation_hit_history.annotation_id, - authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A', - created_at: item.annotation_hit_history.created_at, - } - } - - if (item.annotation) { - return { - id: item.annotation.id, - authorName: item.annotation.account.name, - logAnnotation: item.annotation, - created_at: 0, - } - } - - return undefined - })(), + citation: getCitations(item), + annotation: getAnnotation(item), parentMessageId: `question-${item.id}`, }) }) @@ -305,10 +383,12 @@ export const getConversationRowValues = ({ noOutputLabel: string }) => { const endUser = log.from_end_user_session_id || log.from_account_name - const leftValue = + const rawLeftValue: unknown = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? get(log, 'message.query') || get(log, 'message.inputs.default_input') : '') || '' + const leftValue = + typeof rawLeftValue === 'string' || typeof rawLeftValue === 'number' ? rawLeftValue : '' const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer') return { @@ -323,8 +403,11 @@ export const getConversationRowValues = ({ export const getDetailVarList = ( detail: ConversationLogDetail, varValues: Record, -) => - detail.model_config?.user_input_form?.flatMap((item) => { +) => { + const userInputForm = detail.model_config?.user_input_form + if (!Array.isArray(userInputForm)) return [] + + return userInputForm.flatMap((item) => { const variable = getUserInputVariable(item) if (!variable) return [] @@ -339,7 +422,8 @@ export const getDetailVarList = ( value, }, ] - }) || [] + }) +} export const getCompletionMessageFiles = (detail: ConversationLogDetail, isChatMode: boolean) => { const messageFiles = detail.message?.message_files @@ -348,3 +432,25 @@ export const getCompletionMessageFiles = (detail: ConversationLogDetail, isChatM return messageFiles.flatMap((item) => (item.url ? [item.url] : [])) } + +// `model_config.model` is untyped JSON in the console API response. +export const getLogModelInfo = (model: unknown) => { + if (!isPlainObject(model)) return undefined + const { name, provider, completion_params } = model + if (typeof name !== 'string' || typeof provider !== 'string') return undefined + + return { + name, + provider, + completion_params: isPlainObject(completion_params) ? completion_params : undefined, + } +} + +export const getAdminFeedback = ( + feedbacks: Array<{ content?: string | null; from_source: string; rating: string }>, +): FeedbackType | undefined => { + const feedback = feedbacks.find((item) => item.from_source === 'admin') + if (feedback?.rating !== 'like' && feedback?.rating !== 'dislike') return undefined + + return { rating: feedback.rating, content: feedback.content } +} diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 9d27fa5b04366c..2d69c8266ce42b 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -1,6 +1,15 @@ 'use client' -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { + AppDetailWithSite, + Conversation, + ConversationDetail, + ConversationMessageDetail, + ConversationPagination, + ConversationWithSummary, + ConversationWithSummaryPagination, + GetAppsByAppIdChatMessagesData, +} from '@dify/contracts/api/console/apps/types.gen' import type { FC } from 'react' import type { ChatItemInTree } from '../../base/chat/types' import type { @@ -9,14 +18,7 @@ import type { IChatItem, SubmitAnnotationFunc, } from '@/app/components/base/chat/chat/type' -import type { - ChatConversationGeneralDetail, - ChatConversationsResponse, - ChatMessagesRequest, - CompletionConversationGeneralDetail, - CompletionConversationsResponse, - LogAnnotation, -} from '@/models/log' +import type { LogAnnotation } from '@/models/log' import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline' import { cn } from '@langgenius/dify-ui/cn' import { @@ -33,7 +35,7 @@ import { IconButton } from '@langgenius/dify-ui/icon-button' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiEditFill } from '@remixicon/react' -import { useQuery } from '@tanstack/react-query' +import { skipToken, useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' @@ -54,23 +56,21 @@ import { toast } from '@/app/notifications' import { userProfileQueryOptions } from '@/features/account-profile/client' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' -import { - fetchChatMessages, - updateLogMessageAnnotations, - updateLogMessageFeedbacks, -} from '@/service/log' +import { consoleClient, consoleQuery } from '@/service/console' import { AppSourceType } from '@/service/share' -import { useChatConversationDetail, useCompletionConversationDetail } from '@/service/use-log' import PromptLogModal from '../../base/prompt-log-modal' import { applyAnnotationAdded, applyAnnotationEdited, applyAnnotationRemoved, buildChatThreadState, + getAdminFeedback, getCompletionMessageFiles, getConversationRowValues, getDetailVarList, getFormattedChatList, + getLogModelInfo, + getStringInputs, getThreadChatItems, isNearTopLoadMore, mergePaginatedChatItems, @@ -78,14 +78,14 @@ import { } from './list-utils' import VarPanel from './var-panel' -type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail +type ConversationListItem = ConversationWithSummary | Conversation type ConversationSelection = ConversationListItem | { id: string; isPlaceholder?: true } dayjs.extend(utc) dayjs.extend(timezone) type IConversationList = { - logs?: ChatConversationsResponse | CompletionConversationsResponse + logs?: ConversationWithSummaryPagination | ConversationPagination appDetail: AppDetailWithSite onRefresh: () => void } @@ -156,7 +156,7 @@ const statusTdRender = (statusCount: StatusCount) => { type IDetailPanel = { appDetail: AppDetailWithSite - detail: any + detail: ConversationDetail | ConversationMessageDetail onClose: () => void onFeedback: FeedbackFunc onSubmitAnnotation: SubmitAnnotationFunc @@ -201,22 +201,22 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) { try { isLoadingRef.current = true - const params: ChatMessagesRequest = { + const params: GetAppsByAppIdChatMessagesData['query'] = { conversation_id: detail.id, limit: 10, } // Use ref for pagination anchor to avoid stale closure issues if (oldestAnswerIdRef.current) params.first_id = oldestAnswerIdRef.current - const messageRes = await fetchChatMessages({ - url: `/apps/${appDetail.id}/chat-messages`, - params, + const messageRes = await consoleClient.apps.byAppId.chatMessages.get({ + params: { app_id: appDetail.id }, + query: params, }) // Ignore stale responses if (currentRequestId !== requestIdRef.current || controller.signal.aborted) return if (messageRes.data.length > 0) { - const varValues = messageRes.data.at(-1)!.inputs + const varValues = getStringInputs(messageRes.data.at(-1)!.inputs) setVarValues(varValues) } setHasMore(messageRes.has_more) @@ -246,12 +246,12 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) { const nextThreadState = buildChatThreadState({ allChatItems, hasMore, - introduction: detail?.model_config?.configs?.introduction, + introduction: 'introduction' in detail ? (detail.introduction ?? undefined) : undefined, }) setChatItemTree(nextThreadState.chatItemTree) setThreadChatItems(nextThreadState.threadChatItems) if (nextThreadState.oldestAnswerId) oldestAnswerIdRef.current = nextThreadState.oldestAnswerId - }, [allChatItems, hasMore, detail?.model_config?.configs?.introduction]) + }, [allChatItems, hasMore, detail]) const switchSibling = useCallback( (siblingMessageId: string) => { @@ -280,8 +280,9 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) { try { if (annotation?.id) { - const { delAnnotation } = await import('@/service/annotation') - await delAnnotation(appDetail.id, annotation.id) + await consoleClient.apps.byAppId.annotations.byAnnotationId.delete({ + params: { app_id: appDetail.id, annotation_id: annotation.id }, + }) } setAllChatItems(applyAnnotationRemoved(allChatItems, index)) @@ -321,7 +322,7 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) { setIsLoading(true) try { - const params: ChatMessagesRequest = { + const params: GetAppsByAppIdChatMessagesData['query'] = { conversation_id: detail.id, limit: 10, } @@ -331,9 +332,9 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) { params.first_id = oldestAnswerIdRef.current } - const messageRes = await fetchChatMessages({ - url: `/apps/${appDetail.id}/chat-messages`, - params, + const messageRes = await consoleClient.apps.byAppId.chatMessages.get({ + params: { app_id: appDetail.id }, + query: params, }) if (!messageRes.data || messageRes.data.length === 0) { @@ -343,7 +344,7 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) { } if (messageRes.data.length > 0) { - const varValues = messageRes.data.at(-1)!.inputs + const varValues = getStringInputs(messageRes.data.at(-1)!.inputs) setVarValues(varValues) } @@ -406,6 +407,8 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) { const varList = getDetailVarList(detail, varValues) const message_files = getCompletionMessageFiles(detail, isChatMode) + const completionMessage = 'message' in detail ? detail.message : undefined + const modelInfo = getLogModelInfo(detail.model_config?.model) const [width, setWidth] = useState(0) const ref = useRef(null) @@ -443,15 +446,17 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) { )} {!isChatMode && (
- {formatTime( - detail.created_at, - t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, - )} + {detail.created_at + ? formatTime( + detail.created_at, + t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, + ) + : defaultValue}
)}
- {!isAdvanced && } + {!isAdvanced && modelInfo && }
{!isChatMode ? ( -
-
-
- {t(($) => $['table.header.output'], { ns: 'appLog' })} + completionMessage && ( +
+
+
+ {t(($) => $['table.header.output'], { ns: 'appLog' })} +
+
-
{ + onFeedback(completionMessage.id, feedback).catch(noop) }} - >
+ isShowTextToSpeech + siteInfo={null} + />
- item.from_source === 'admin')} - onFeedback={(feedback) => { - onFeedback(detail.message.id, feedback).catch(noop) - }} - isShowTextToSpeech - siteInfo={null} - /> -
+ ) ) : threadChatItems.length < MIN_ITEMS_FOR_SCROLL_LOADING ? (
= ({ onClose, }) => { // Text Generator App Session Details Including Message List - const { data: conversationDetail, refetch: conversationDetailMutate } = - useCompletionConversationDetail(appDetail.id, conversationId) + const { data: conversationDetail, refetch: conversationDetailMutate } = useQuery( + consoleQuery.apps.byAppId.completionConversations.byConversationId.get.queryOptions({ + input: conversationId + ? { params: { app_id: appDetail.id, conversation_id: conversationId } } + : skipToken, + }), + ) const { t } = useTranslation(['appLog', 'common']) const handleFeedback = async (mid: string, { rating, content }: FeedbackType) => { - await updateLogMessageFeedbacks({ - url: `/apps/${appDetail.id}/feedbacks`, + await consoleClient.apps.byAppId.feedbacks.post({ + params: { app_id: appDetail.id }, body: { message_id: mid, rating, content: content ?? undefined }, }) conversationDetailMutate() @@ -640,8 +652,8 @@ const CompletionConversationDetailComp: FC = ({ const handleAnnotation = async (mid: string, value: string): Promise => { try { - await updateLogMessageAnnotations({ - url: `/apps/${appDetail.id}/annotations`, + await consoleClient.apps.byAppId.annotations.post({ + params: { app_id: appDetail.id }, body: { message_id: mid, content: value }, }) conversationDetailMutate() @@ -681,20 +693,26 @@ const ChatConversationDetailComp: FC = ({ conversationId, onClose, }) => { - const { data: conversationDetail } = useChatConversationDetail(appDetail.id, conversationId) + const { data: conversationDetail } = useQuery( + consoleQuery.apps.byAppId.chatConversations.byConversationId.get.queryOptions({ + input: conversationId + ? { params: { app_id: appDetail.id, conversation_id: conversationId } } + : skipToken, + }), + ) const { t } = useTranslation(['appLog', 'common']) const handleFeedback = async (mid: string, { rating, content }: FeedbackType) => { - await updateLogMessageFeedbacks({ - url: `/apps/${appDetail.id}/feedbacks`, + await consoleClient.apps.byAppId.feedbacks.post({ + params: { app_id: appDetail.id }, body: { message_id: mid, rating, content: content ?? undefined }, }) } const handleAnnotation = async (mid: string, value: string): Promise => { try { - await updateLogMessageAnnotations({ - url: `/apps/${appDetail.id}/annotations`, + await consoleClient.apps.byAppId.annotations.post({ + params: { app_id: appDetail.id }, body: { message_id: mid, content: value }, }) toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) diff --git a/web/app/components/app/log/model-info.tsx b/web/app/components/app/log/model-info.tsx index fff5f044670b88..d49ab71baa55e1 100644 --- a/web/app/components/app/log/model-info.tsx +++ b/web/app/components/app/log/model-info.tsx @@ -1,5 +1,4 @@ 'use client' -import type { ModelConfig } from '@dify/contracts/api/console/apps/types.gen' import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' @@ -19,8 +18,14 @@ const PARAM_MAP = { frequency_penalty: 'Frequency Penalty', } +type LogModelConfig = { + name: string + provider: string + completion_params?: Record +} + type Props = Readonly<{ - model: Pick + model: LogModelConfig }> const ModelInfo: FC = ({ model }) => { diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index 10255b9956535f..6664d0371ddd9d 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -170,7 +170,14 @@ const GenerationItem: FC = ({ appId: params.appId as string, messageId: messageId!, }) - onOpenLog(buildPromptLogItem(data)) + const promptLog = buildPromptLogItem(data) + onOpenLog({ + id: promptLog.id, + // The prompt log modal only renders `log`; message file entries are passed through as-is. + log: promptLog.log as IChatItem['log'], + content: data.answer, + isAnswer: true, + }) } const [currentTab, setCurrentTab] = useState('DETAIL') diff --git a/web/app/components/app/workflow-log/__tests__/filter.spec.tsx b/web/app/components/app/workflow-log/__tests__/filter.spec.tsx index 0585e1bdb6ea81..130cf5a563d92f 100644 --- a/web/app/components/app/workflow-log/__tests__/filter.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/filter.spec.tsx @@ -214,7 +214,7 @@ describe('Filter', () => { ])('should display correct label for %s status', (statusValue, expectedLabel) => { render( , ) diff --git a/web/app/components/app/workflow-log/__tests__/index.spec.tsx b/web/app/components/app/workflow-log/__tests__/index.spec.tsx index 6be089cf741ace..babe4abd572634 100644 --- a/web/app/components/app/workflow-log/__tests__/index.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/index.spec.tsx @@ -1,4 +1,11 @@ -import type { AppDetailWithSite, AppMode } from '@dify/contracts/api/console/apps/types.gen' +import type { + AppDetailWithSite, + AppMode, + GetAppsByAppIdWorkflowAppLogsData, + WorkflowAppLogPaginationResponse, + WorkflowAppLogPartialResponse, + WorkflowRunForLogResponse, +} from '@dify/contracts/api/console/apps/types.gen' import type { UseQueryResult } from '@tanstack/react-query' /** * Logs Container Component Tests @@ -15,16 +22,13 @@ import type { UseQueryResult } from '@tanstack/react-query' * - detail.spec.tsx * - trigger-by-display.spec.tsx */ -import type { MockedFunction } from 'vite-plus/test' import type { CloudSandboxPlanState } from '../../log/cloud-sandbox-retention' import type { ILogsProps } from '../index' -import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } from '@/models/log' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import dayjs from 'dayjs' import { APP_PAGE_LIMIT } from '@/config' import { WorkflowRunTriggeredFrom } from '@/models/log' -import * as useLogModule from '@/service/use-log' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app' @@ -40,7 +44,26 @@ const mockPlanState = vi.hoisted(() => ({ value: 'unrestricted' as CloudSandboxPlanState, })) -vi.mock('@/service/use-log') +type WorkflowLogsInput = { + params: { app_id: string } + query?: GetAppsByAppIdWorkflowAppLogsData['query'] +} + +const mockedUseWorkflowLogs = + vi.fn<(input: WorkflowLogsInput) => UseQueryResult>() + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useQuery: (options: Parameters[0]) => { + const [path, meta] = options.queryKey as [string[], { input?: WorkflowLogsInput }?] + if (Array.isArray(path) && path.includes('workflowAppLogs') && meta?.input) + return mockedUseWorkflowLogs(meta.input) + return actual.useQuery(options) + }, + } +}) vi.mock('../../log/cloud-sandbox-retention', async (importOriginal) => { const actual = await importOriginal() @@ -105,10 +128,6 @@ vi.mock('@/app/components/workflow/context', () => ({ WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })) -const mockedUseWorkflowLogs = useLogModule.useWorkflowLogs as MockedFunction< - typeof useLogModule.useWorkflowLogs -> - // ============================================================================ // Test Utilities // ============================================================================ @@ -179,14 +198,14 @@ const createMockApp = (overrides: Partial = {}): AppDetailWit ...overrides, }) -const createMockWorkflowRun = (overrides: Partial = {}): WorkflowRunDetail => ({ +const createMockWorkflowRun = ( + overrides: Partial = {}, +): WorkflowRunForLogResponse => ({ id: 'run-1', version: '1.0.0', status: 'succeeded', elapsed_time: 1.234, total_tokens: 100, - total_price: 0.001, - currency: 'USD', total_steps: 5, finished_at: Date.now(), triggered_from: WorkflowRunTriggeredFrom.APP_RUN, @@ -194,8 +213,8 @@ const createMockWorkflowRun = (overrides: Partial = {}): Work }) const createMockWorkflowLog = ( - overrides: Partial = {}, -): WorkflowAppLogDetail => ({ + overrides: Partial = {}, +): WorkflowAppLogPartialResponse => ({ id: 'log-1', workflow_run: createMockWorkflowRun(), created_from: 'web-app', @@ -210,9 +229,9 @@ const createMockWorkflowLog = ( }) const createMockLogsResponse = ( - data: WorkflowAppLogDetail[] = [], + data: WorkflowAppLogPartialResponse[] = [], total = data.length, -): WorkflowLogsResponse => ({ +): WorkflowAppLogPaginationResponse => ({ data, has_more: data.length < total, limit: APP_PAGE_LIMIT, @@ -224,14 +243,9 @@ const createMockLogsResponse = ( // Type-safe Mock Helper // ============================================================================ -type WorkflowLogsParams = { - appId: string - params?: Record -} - -const getMockCallParams = (): WorkflowLogsParams | undefined => { - const lastCall = mockedUseWorkflowLogs.mock.calls.at(-1) - return lastCall?.[0] +const getMockCallParams = () => { + const input = mockedUseWorkflowLogs.mock.calls.at(-1)?.[0] + return input && { appId: input.params.app_id, params: input.query } } // ============================================================================ @@ -255,7 +269,7 @@ describe('Logs Container', () => { it('should render title and subtitle', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -271,7 +285,7 @@ describe('Logs Container', () => { it('should render Filter component', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -292,7 +306,7 @@ describe('Logs Container', () => { it('should show loading spinner when data is undefined', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: undefined, isLoading: true, }), @@ -308,7 +322,7 @@ describe('Logs Container', () => { it('should not show loading spinner when data is available', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([createMockWorkflowLog()], 1), }), ) @@ -328,7 +342,7 @@ describe('Logs Container', () => { it('should render empty element when total is 0', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -349,7 +363,7 @@ describe('Logs Container', () => { it('should call useWorkflowLogs with correct appId and default params', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -372,7 +386,7 @@ describe('Logs Container', () => { it('should include date filters for non-allTime periods', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -389,7 +403,7 @@ describe('Logs Container', () => { it('should not include status param when status is all', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -411,7 +425,7 @@ describe('Logs Container', () => { // Arrange const user = userEvent.setup() mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -435,7 +449,7 @@ describe('Logs Container', () => { // Arrange const user = userEvent.setup() mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -458,7 +472,7 @@ describe('Logs Container', () => { const user = userEvent.setup() mockPlanState.value = 'sandbox' mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -481,7 +495,7 @@ describe('Logs Container', () => { // Arrange const user = userEvent.setup() mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) @@ -509,7 +523,7 @@ describe('Logs Container', () => { it('should not render pagination when total is less than limit', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([createMockWorkflowLog()], 1), }), ) @@ -528,7 +542,7 @@ describe('Logs Container', () => { ) mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse(logs, APP_PAGE_LIMIT + 10), }), ) @@ -548,7 +562,7 @@ describe('Logs Container', () => { it('should render List component when data is available', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([createMockWorkflowLog()], 1), }), ) @@ -563,7 +577,7 @@ describe('Logs Container', () => { it('should display log data in table', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse( [ createMockWorkflowLog({ @@ -606,7 +620,7 @@ describe('Logs Container', () => { it('should handle different app modes', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([createMockWorkflowLog()], 1), }), ) @@ -623,7 +637,7 @@ describe('Logs Container', () => { it('should handle error state from useWorkflowLogs', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: undefined, error: new Error('Failed to fetch'), }), @@ -639,7 +653,7 @@ describe('Logs Container', () => { it('should handle app with different ID', () => { // Arrange mockedUseWorkflowLogs.mockReturnValue( - createMockQueryResult({ + createMockQueryResult({ data: createMockLogsResponse([], 0), }), ) diff --git a/web/app/components/app/workflow-log/__tests__/list.spec.tsx b/web/app/components/app/workflow-log/__tests__/list.spec.tsx index 5f872d76da4c6c..de02357a991822 100644 --- a/web/app/components/app/workflow-log/__tests__/list.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/list.spec.tsx @@ -1,5 +1,10 @@ -import type { AppDetailWithSite, AppMode } from '@dify/contracts/api/console/apps/types.gen' -import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } from '@/models/log' +import type { + AppDetailWithSite, + AppMode, + WorkflowAppLogPaginationResponse, + WorkflowAppLogPartialResponse, + WorkflowRunForLogResponse, +} from '@dify/contracts/api/console/apps/types.gen' /** * WorkflowAppLogList Component Tests * @@ -98,14 +103,14 @@ const createMockApp = (overrides: Partial = {}): AppDetailWit ...overrides, }) -const createMockWorkflowRun = (overrides: Partial = {}): WorkflowRunDetail => ({ +const createMockWorkflowRun = ( + overrides: Partial = {}, +): WorkflowRunForLogResponse => ({ id: 'run-1', version: '1.0.0', status: 'succeeded', elapsed_time: 1.234, total_tokens: 100, - total_price: 0.001, - currency: 'USD', total_steps: 5, finished_at: Date.now(), triggered_from: WorkflowRunTriggeredFrom.APP_RUN, @@ -113,8 +118,9 @@ const createMockWorkflowRun = (overrides: Partial = {}): Work }) const createMockWorkflowLog = ( - overrides: Partial = {}, -): WorkflowAppLogDetail => ({ + // read_at is not declared by the console response yet; the list still honours it. + overrides: Partial & { read_at?: number } = {}, +): WorkflowAppLogPartialResponse => ({ id: 'log-1', workflow_run: createMockWorkflowRun(), created_from: 'web-app', @@ -129,9 +135,9 @@ const createMockWorkflowLog = ( }) const createMockLogsResponse = ( - data: WorkflowAppLogDetail[] = [], + data: WorkflowAppLogPartialResponse[] = [], total = data.length, -): WorkflowLogsResponse => ({ +): WorkflowAppLogPaginationResponse => ({ data, has_more: data.length < total, limit: APP_PAGE_LIMIT, diff --git a/web/app/components/app/workflow-log/filter.tsx b/web/app/components/app/workflow-log/filter.tsx index 99d6ee7ab9ca33..28a626b99b042c 100644 --- a/web/app/components/app/workflow-log/filter.tsx +++ b/web/app/components/app/workflow-log/filter.tsx @@ -54,7 +54,7 @@ const Filter: FC = ({ queryParams, setQueryParams }: IFilterProps) { - setQueryParams({ ...queryParams, status: item.value as string }) + setQueryParams({ ...queryParams, status: item.value as QueryParam['status'] }) trackEvent('workflow_log_filter_status_selected', { workflow_log_filter_status: item.value as string, }) diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx index 8b28ddf80737ff..168a3adc73c93f 100644 --- a/web/app/components/app/workflow-log/index.tsx +++ b/web/app/components/app/workflow-log/index.tsx @@ -1,6 +1,9 @@ 'use client' -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { + AppDetailWithSite, + GetAppsByAppIdWorkflowAppLogsData, +} from '@dify/contracts/api/console/apps/types.gen' import type { FC } from 'react' import { Pagination } from '@langgenius/dify-ui/pagination' import { useQuery } from '@tanstack/react-query' @@ -16,7 +19,7 @@ import EmptyElement from '@/app/components/app/log/empty-element' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { APP_PAGE_LIMIT } from '@/config' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { useWorkflowLogs } from '@/service/use-log' +import { consoleQuery } from '@/service/console' import PageTitle from '../log-annotation/page-title' import { ArchivedLogsNotice } from '../log/archived-logs-notice' import { shouldShowArchivedLogsNotice } from '../log/archived-logs-notice-utils' @@ -36,9 +39,11 @@ export type ILogsProps = { appDetail: AppDetailWithSite } +type WorkflowLogStatus = NonNullable['status'] + export type QueryParam = { period: string - status?: string + status?: WorkflowLogStatus | 'all' keyword?: string } @@ -81,10 +86,11 @@ const Logs: FC = ({ appDetail }) => { ...omit(requestQueryParams, ['period', 'status']), } - const { data: workflowLogs, refetch: mutate } = useWorkflowLogs({ - appId: appDetail.id, - params: query, - }) + const { data: workflowLogs, refetch: mutate } = useQuery( + consoleQuery.apps.byAppId.workflowAppLogs.get.queryOptions({ + input: { params: { app_id: appDetail.id }, query }, + }), + ) const total = workflowLogs?.total const totalPages = total ? Math.max(Math.ceil(total / limit), 1) : 1 const showArchivedLogsNotice = shouldShowArchivedLogsNotice( diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx index 8ca5eb29d15f51..bddb1bca30e6c4 100644 --- a/web/app/components/app/workflow-log/list.tsx +++ b/web/app/components/app/workflow-log/list.tsx @@ -1,12 +1,12 @@ 'use client' -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' -import type { FC } from 'react' import type { - WorkflowAppLogDetail, - WorkflowLogsResponse, - WorkflowRunTriggeredFrom, -} from '@/models/log' + AppDetailWithSite, + WorkflowAppLogPaginationResponse, + WorkflowAppLogPartialResponse, +} from '@dify/contracts/api/console/apps/types.gen' +import type { FC } from 'react' +import type { WorkflowLogDetails, WorkflowRunTriggeredFrom } from '@/models/log' import { ArrowDownIcon } from '@heroicons/react/24/outline' import { cn } from '@langgenius/dify-ui/cn' import { @@ -29,7 +29,7 @@ import DetailPanel from './detail' import TriggerByDisplay from './trigger-by-display' type ILogs = { - logs?: WorkflowLogsResponse + logs?: WorkflowAppLogPaginationResponse appDetail?: AppDetailWithSite onRefresh: () => void } @@ -44,9 +44,9 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { const isMobile = media === MediaType.mobile const [showDrawer, setShowDrawer] = useState(false) - const [currentLog, setCurrentLog] = useState() + const [currentLog, setCurrentLog] = useState() const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') - const [localLogs, setLocalLogs] = useState(logs?.data || []) + const [localLogs, setLocalLogs] = useState(logs?.data || []) useEffect(() => { if (!logs?.data) { @@ -55,7 +55,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { } const sortedLogs = [...logs.data].sort((a, b) => { - const result = a.created_at - b.created_at + const result = (a.created_at ?? 0) - (b.created_at ?? 0) return sortOrder === 'asc' ? result : -result }) @@ -68,7 +68,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { const isWorkflow = appDetail?.mode === 'workflow' - const statusTdRender = (status: string) => { + const statusTdRender = (status?: string | null) => { if (status === 'succeeded') { return (
@@ -208,7 +208,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { - {localLogs.map((log: WorkflowAppLogDetail) => { + {localLogs.map((log: WorkflowAppLogPartialResponse) => { const endUser = log.created_by_end_user ? log.created_by_end_user.session_id : log.created_by_account @@ -229,7 +229,8 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { }} > - {!log.read_at && ( + {/* The console workflow-log response does not declare read_at yet. */} + {!('read_at' in log && log.read_at) && (
@@ -245,21 +246,21 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { setShowDrawer(true) }} > - {formatTime( - log.created_at, - t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, - )} + {log.created_at + ? formatTime( + log.created_at, + t(($) => $.dateTimeFormat, { ns: 'appLog' }) as string, + ) + : defaultValue} - {statusTdRender(log.workflow_run.status)} + {statusTdRender(log.workflow_run?.status)} -
- {`${log.workflow_run.elapsed_time.toFixed(3)}s`} +
+ {`${(log.workflow_run?.elapsed_time ?? 0).toFixed(3)}s`}
- {log.workflow_run.total_tokens} + {log.workflow_run?.total_tokens}
= ({ logs, appDetail, onRefresh }) => { {isWorkflow && ( )} @@ -291,10 +294,10 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { diff --git a/web/app/components/apps/__tests__/creators-filter.spec.tsx b/web/app/components/apps/__tests__/creators-filter.spec.tsx index 0cecea6cff92f2..44259dfe2e6c3c 100644 --- a/web/app/components/apps/__tests__/creators-filter.spec.tsx +++ b/web/app/components/apps/__tests__/creators-filter.spec.tsx @@ -5,6 +5,24 @@ import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' import CreatorsFilter from '../creators-filter' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => ({ + data: { + accounts: [ + { id: 'member-1', name: 'Zoe', avatar_url: null, status: 'active' }, + { id: 'member-2', name: 'Alice', avatar_url: null, status: 'active' }, + { id: 'member-3', name: 'Bob', avatar_url: null, status: 'active' }, + { id: 'member-4', name: 'Pending User', avatar_url: null, status: 'pending' }, + { id: 'member-5', name: 'Charlie', avatar_url: null, status: 'active' }, + { id: 'member-6', name: 'Robin', avatar_url: null, status: 'active' }, + ], + }, + }), + }) +}) + const mockOnChange = vi.hoisted(() => vi.fn()) const render = (ui: Parameters[0]) => @@ -17,21 +35,6 @@ const StatefulCreatorsFilter = ({ initialValue }: { initialValue: string[] }) => return } -vi.mock('@/service/use-common', () => ({ - useMembers: () => ({ - data: { - accounts: [ - { id: 'member-1', name: 'Zoe', avatar_url: null, status: 'active' }, - { id: 'member-2', name: 'Alice', avatar_url: null, status: 'active' }, - { id: 'member-3', name: 'Bob', avatar_url: null, status: 'active' }, - { id: 'member-4', name: 'Pending User', avatar_url: null, status: 'pending' }, - { id: 'member-5', name: 'Charlie', avatar_url: null, status: 'active' }, - { id: 'member-6', name: 'Robin', avatar_url: null, status: 'active' }, - ], - }, - }), -})) - describe('CreatorsFilter', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index aeaeec51f07a2a..61d565add521e4 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -78,6 +78,18 @@ vi.mock('@/service/console', () => ({ }, }, consoleQuery: { + workspaces: { + current: { + members: { + get: { + queryOptions: (options: { input?: unknown }) => ({ + ...options, + queryKey: [['console', 'workspaces', 'current', 'members', 'get'], options], + }), + }, + }, + }, + }, account: { profile: { get: { @@ -124,17 +136,6 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/use-common', () => ({ - useMembers: () => ({ - data: { - accounts: [ - { id: 'creator-1', name: 'Alice', avatar_url: null, status: 'active' }, - { id: 'creator-2', name: 'Bob', avatar_url: null, status: 'active' }, - ], - }, - }), -})) - vi.mock('@/features/tag-management/components/tag-filter', () => ({ TagFilter: ({ value, @@ -233,16 +234,26 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal() return { ...actual, - useQuery: (options: { input?: unknown }) => - options.input - ? { - data: mockStarredIsLoading ? undefined : mockStarredAppData, - error: mockStarredError, - } - : { - data: mockSystemFeatures, - error: null, - }, + useQuery: (options: { input?: unknown; queryKey?: unknown[] }) => + Array.isArray(options.queryKey?.[0]) && + options.queryKey[0].join('.') === 'console.workspaces.current.members.get' + ? (() => ({ + data: { + accounts: [ + { id: 'creator-1', name: 'Alice', avatar_url: null, status: 'active' }, + { id: 'creator-2', name: 'Bob', avatar_url: null, status: 'active' }, + ], + }, + }))() + : options.input + ? { + data: mockStarredIsLoading ? undefined : mockStarredAppData, + error: mockStarredError, + } + : { + data: mockSystemFeatures, + error: null, + }, useSuspenseQuery: () => ({ data: mockSystemFeatures }), useInfiniteQuery: () => ({ data: mockServiceState.isLoading ? undefined : mockAppData, diff --git a/web/app/components/apps/creators-filter.tsx b/web/app/components/apps/creators-filter.tsx index 3cb382a60601da..d94d78b6ed81f7 100644 --- a/web/app/components/apps/creators-filter.tsx +++ b/web/app/components/apps/creators-filter.tsx @@ -25,11 +25,11 @@ import { createComboboxItems, } from '@langgenius/dify-ui/combobox' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { useMembers } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { getAvatarInitial } from '@/utils/avatar' type CreatorsFilterProps = { @@ -53,7 +53,9 @@ const CreatorsFilter = ({ value, onChange }: CreatorsFilterProps) => { ...userProfileQueryOptions(), select: (data) => data.profile.id, }) - const { data: membersData } = useMembers() + const { data: membersData } = useQuery( + consoleQuery.workspaces.current.members.get.queryOptions({ input: {} }), + ) const triggerRef = useRef(null) const creatorOptions = useMemo(() => { diff --git a/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx index eb677b3a45895b..ce8b091e4ee0ee 100644 --- a/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx @@ -3,7 +3,6 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentLogDetailResponse } from '@/models/log' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { createHttpError } from '@/service/http-error' -import { fetchAgentLogDetail } from '@/service/log' import AgentLogDetail from '../detail' const { mockToast } = vi.hoisted(() => { @@ -19,10 +18,14 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@/service/log', () => ({ +const { fetchAgentLogDetail } = vi.hoisted(() => ({ fetchAgentLogDetail: vi.fn(), })) +vi.mock('@/service/console', () => ({ + consoleClient: { apps: { byAppId: { agent: { logs: { get: fetchAgentLogDetail } } } } }, +})) + vi.mock('@/app/notifications', () => ({ toast: mockToast, })) @@ -100,6 +103,10 @@ const createMockResponse = ( tool_name: 'tool1', status: 'success', tool_icon: null, + time_cost: 0, + tool_input: {}, + tool_output: {}, + tool_parameters: {}, tool_label: { 'en-US': 'Tool 1' }, }, ], @@ -136,7 +143,7 @@ describe('AgentLogDetail', () => { describe('Rendering', () => { it('should show loading indicator while fetching data', async () => { - vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) + fetchAgentLogDetail.mockReturnValue(new Promise(() => {})) renderComponent() @@ -144,7 +151,7 @@ describe('AgentLogDetail', () => { }) it('should display result panel after data loads', async () => { - vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) + fetchAgentLogDetail.mockResolvedValue(createMockResponse()) await renderAndWaitForData() @@ -153,23 +160,20 @@ describe('AgentLogDetail', () => { }) it('should call fetchAgentLogDetail with correct params', async () => { - vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) + fetchAgentLogDetail.mockResolvedValue(createMockResponse()) await renderAndWaitForData() expect(fetchAgentLogDetail).toHaveBeenCalledWith({ - appID: 'app-id', - params: { - conversation_id: 'conv-id', - message_id: 'msg-id', - }, + params: { app_id: 'app-id' }, + query: { conversation_id: 'conv-id', message_id: 'msg-id' }, }) }) }) describe('Props', () => { it('should default to DETAIL tab when activeTab is not provided', async () => { - vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) + fetchAgentLogDetail.mockResolvedValue(createMockResponse()) await renderAndWaitForData() @@ -178,7 +182,7 @@ describe('AgentLogDetail', () => { }) it('should show TRACING tab when activeTab is TRACING', async () => { - vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) + fetchAgentLogDetail.mockResolvedValue(createMockResponse()) await renderAndWaitForData({ activeTab: 'TRACING' }) @@ -189,7 +193,7 @@ describe('AgentLogDetail', () => { describe('User Interactions', () => { it('should switch to TRACING tab when clicked', async () => { - vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) + fetchAgentLogDetail.mockResolvedValue(createMockResponse()) await renderAndWaitForData() @@ -205,7 +209,7 @@ describe('AgentLogDetail', () => { }) it('should switch back to DETAIL tab after switching to TRACING', async () => { - vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) + fetchAgentLogDetail.mockResolvedValue(createMockResponse()) await renderAndWaitForData() @@ -226,7 +230,7 @@ describe('AgentLogDetail', () => { describe('Edge Cases', () => { it('should notify on API error', async () => { - vi.mocked(fetchAgentLogDetail).mockRejectedValue( + fetchAgentLogDetail.mockRejectedValue( createHttpError(new Response(null, { status: 500 }), { message: 'API Error' }), ) @@ -238,7 +242,7 @@ describe('AgentLogDetail', () => { }) it('should stop loading after API error', async () => { - vi.mocked(fetchAgentLogDetail).mockRejectedValue(new Error('Network failure')) + fetchAgentLogDetail.mockRejectedValue(new Error('Network failure')) renderComponent() @@ -248,7 +252,7 @@ describe('AgentLogDetail', () => { }) it('should handle response with empty iterations', async () => { - vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse({ iterations: [] })) + fetchAgentLogDetail.mockResolvedValue(createMockResponse({ iterations: [] })) await renderAndWaitForData() }) @@ -267,12 +271,20 @@ describe('AgentLogDetail', () => { tool_name: 'tool1', status: 'success', tool_icon: null, + time_cost: 0, + tool_input: {}, + tool_output: {}, + tool_parameters: {}, tool_label: { 'en-US': 'Tool 1' }, }, { tool_name: 'tool2', status: 'success', tool_icon: null, + time_cost: 0, + tool_input: {}, + tool_output: {}, + tool_parameters: {}, tool_label: { 'en-US': 'Tool 2' }, }, ], @@ -288,13 +300,17 @@ describe('AgentLogDetail', () => { tool_name: 'tool1', status: 'success', tool_icon: null, + time_cost: 0, + tool_input: {}, + tool_output: {}, + tool_parameters: {}, tool_label: { 'en-US': 'Tool 1' }, }, ], }, ], }) - vi.mocked(fetchAgentLogDetail).mockResolvedValue(response) + fetchAgentLogDetail.mockResolvedValue(response) await renderAndWaitForData() diff --git a/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx index 28d814de454276..c0816744d31e13 100644 --- a/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx @@ -1,7 +1,6 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { useClickAway } from 'ahooks' -import { fetchAgentLogDetail } from '@/service/log' import AgentLogModal from '../index' const { mockToast } = vi.hoisted(() => { @@ -17,10 +16,14 @@ const { mockToast } = vi.hoisted(() => { return { mockToast } }) -vi.mock('@/service/log', () => ({ +const { fetchAgentLogDetail } = vi.hoisted(() => ({ fetchAgentLogDetail: vi.fn(), })) +vi.mock('@/service/console', () => ({ + consoleClient: { apps: { byAppId: { agent: { logs: { get: fetchAgentLogDetail } } } } }, +})) + vi.mock('@/app/notifications', () => ({ toast: mockToast, })) @@ -86,7 +89,7 @@ const mockProps = { describe('AgentLogModal', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(fetchAgentLogDetail).mockResolvedValue({ + fetchAgentLogDetail.mockResolvedValue({ meta: { status: 'succeeded', executor: 'User', @@ -146,7 +149,7 @@ describe('AgentLogModal', () => { }) it('should render the floating modal through a dialog portal', () => { - vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) + fetchAgentLogDetail.mockReturnValue(new Promise(() => {})) const { container } = render() @@ -157,7 +160,7 @@ describe('AgentLogModal', () => { }) it('should call onCancel when close button is clicked', () => { - vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) + fetchAgentLogDetail.mockReturnValue(new Promise(() => {})) render() @@ -168,7 +171,7 @@ describe('AgentLogModal', () => { }) it('should call onCancel when clicking away', () => { - vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) + fetchAgentLogDetail.mockReturnValue(new Promise(() => {})) let clickAwayHandler!: (event: Event) => void vi.mocked(useClickAway).mockImplementation((callback) => { @@ -182,7 +185,7 @@ describe('AgentLogModal', () => { }) it('should ignore click-away before mounted state is set', () => { - vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) + fetchAgentLogDetail.mockReturnValue(new Promise(() => {})) let invoked = false vi.mocked(useClickAway).mockImplementation((callback) => { if (!invoked) { @@ -197,7 +200,7 @@ describe('AgentLogModal', () => { }) it('should not use click-away to close the floating dialog', () => { - vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) + fetchAgentLogDetail.mockReturnValue(new Promise(() => {})) let clickAwayHandler!: (event: Event) => void vi.mocked(useClickAway).mockImplementation((callback) => { @@ -228,15 +231,13 @@ describe('AgentLogModal', () => { const pending = new Promise>>((resolve) => { resolveOldResponse = resolve }) - vi.mocked(fetchAgentLogDetail) - .mockResolvedValue(response) - .mockImplementationOnce(() => pending) + fetchAgentLogDetail.mockResolvedValue(response).mockImplementationOnce(() => pending) const { rerender } = render() rerender() await screen.findByText('New executor') expect(fetchAgentLogDetail).toHaveBeenLastCalledWith({ - appID: 'new-app', - params: { conversation_id: mockLog.conversationId, message_id: mockLog.id }, + params: { app_id: 'new-app' }, + query: { conversation_id: mockLog.conversationId, message_id: mockLog.id }, }) await act(async () => { resolveOldResponse({ ...response, meta: { ...response.meta, executor: 'Old executor' } }) @@ -263,8 +264,8 @@ describe('AgentLogModal', () => { expect(screen.getByRole('progressbar')).toBeInTheDocument() await waitFor(() => expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()) expect(fetchAgentLogDetail).toHaveBeenLastCalledWith({ - appID: 'app-id', - params: { conversation_id: mockLog.conversationId, message_id: 'next-message' }, + params: { app_id: 'app-id' }, + query: { conversation_id: mockLog.conversationId, message_id: 'next-message' }, }) }) @@ -273,7 +274,7 @@ describe('AgentLogModal', () => { const pending = new Promise>>((_, reject) => { rejectOldResponse = reject }) - vi.mocked(fetchAgentLogDetail).mockReturnValueOnce(pending) + fetchAgentLogDetail.mockReturnValueOnce(pending) const { rerender } = render() rerender() await screen.findByText('User') diff --git a/web/app/components/base/agent-log-modal/__tests__/iteration.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/iteration.spec.tsx index f124d4856a6513..3e841d76363c5b 100644 --- a/web/app/components/base/agent-log-modal/__tests__/iteration.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/iteration.spec.tsx @@ -26,6 +26,10 @@ const mockIterationInfo: AgentIteration = { tool_name: 'test_tool', tool_label: { en: 'Test Tool', 'en-US': 'Test Tool' }, tool_icon: null, + time_cost: 0, + tool_input: {}, + tool_output: {}, + tool_parameters: {}, }, ], tool_raw: { diff --git a/web/app/components/base/agent-log-modal/__tests__/tracing.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/tracing.spec.tsx index c38caef38614ee..4d744ebf256d3c 100644 --- a/web/app/components/base/agent-log-modal/__tests__/tracing.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/tracing.spec.tsx @@ -22,7 +22,16 @@ const createIteration = (thought: string, tokens: number): AgentIteration => ({ thought, tokens, tool_calls: [ - { tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } }, + { + tool_name: 'tool1', + status: 'success', + tool_icon: null, + tool_label: { 'en-US': 'Tool 1' }, + time_cost: 0, + tool_input: {}, + tool_output: {}, + tool_parameters: {}, + }, ], tool_raw: { inputs: '', outputs: '' }, }) diff --git a/web/app/components/base/agent-log-modal/detail.tsx b/web/app/components/base/agent-log-modal/detail.tsx index 2db2606eddb2d9..8c078660d6e95f 100644 --- a/web/app/components/base/agent-log-modal/detail.tsx +++ b/web/app/components/base/agent-log-modal/detail.tsx @@ -9,7 +9,7 @@ import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { toast } from '@/app/notifications' -import { fetchAgentLogDetail } from '@/service/log' +import { consoleClient } from '@/service/console' import ResultPanel from './result' import TracingPanel from './tracing' @@ -94,21 +94,23 @@ function AgentLogContent({ useEffect(() => { let active = true - fetchAgentLogDetail({ - appID: appId, - params: { conversation_id: conversationID, message_id: messageID }, - }).then( - (data) => { - if (!active) return - setRunDetail(data) - setLoading(false) - }, - (error) => { - if (!active) return - toast.error(error instanceof Error ? error.message : String(error)) - setLoading(false) - }, - ) + consoleClient.apps.byAppId.agent.logs + .get({ + params: { app_id: appId }, + query: { conversation_id: conversationID, message_id: messageID }, + }) + .then( + (data) => { + if (!active) return + setRunDetail(data) + setLoading(false) + }, + (error) => { + if (!active) return + toast.error(error instanceof Error ? error.message : String(error)) + setLoading(false) + }, + ) return () => { active = false } @@ -131,8 +133,7 @@ function AgentLogContent({ inputs={log.input} outputs={log.content} status={runDetail.meta.status} - error={runDetail.meta.error} - elapsed_time={runDetail.meta.elapsed_time} + elapsed_time={runDetail.meta.elapsed_time ?? undefined} total_tokens={runDetail.meta.total_tokens} created_at={runDetail.meta.start_time} created_by={runDetail.meta.executor} diff --git a/web/app/components/base/agent-log-modal/index.stories.tsx b/web/app/components/base/agent-log-modal/index.stories.tsx index fbf8874471cf24..21c2779a2616a4 100644 --- a/web/app/components/base/agent-log-modal/index.stories.tsx +++ b/web/app/components/base/agent-log-modal/index.stories.tsx @@ -14,7 +14,6 @@ const MOCK_RESPONSE: AgentLogDetailResponse = { total_tokens: 2589, agent_mode: 'ReACT', iterations: 2, - error: undefined, }, iterations: [ { @@ -33,6 +32,7 @@ const MOCK_RESPONSE: AgentLogDetailResponse = { 'en-US': 'Revenue Search', }, time_cost: 1.8, + tool_parameters: {}, }, ], tool_raw: { diff --git a/web/app/components/base/agent-log-modal/tool-call.tsx b/web/app/components/base/agent-log-modal/tool-call.tsx index 04d354289a13c7..1aab26d4e91db5 100644 --- a/web/app/components/base/agent-log-modal/tool-call.tsx +++ b/web/app/components/base/agent-log-modal/tool-call.tsx @@ -10,8 +10,15 @@ import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/ import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { BlockEnum } from '@/app/components/workflow/types' +const getToolIcon = (icon: ToolCall['tool_icon']) => { + if (typeof icon === 'string') return icon + if (icon?.content !== undefined && icon.background !== undefined) + return { content: icon.content, background: icon.background } + return undefined +} + type Props = Readonly<{ - toolCall: ToolCall + toolCall: Partial & Pick isLLM: boolean isFinal?: boolean tokens?: number @@ -31,7 +38,9 @@ const ToolCallItem: FC = ({ const locale = useLocale() const toolName = isLLM ? 'LLM' - : toolCall.tool_label[locale] || toolCall.tool_label[locale.replaceAll('-', '_')] + : typeof toolCall.tool_label === 'string' + ? toolCall.tool_label + : toolCall.tool_label?.[locale] || toolCall.tool_label?.[locale.replaceAll('-', '_')] const getTime = (time: number) => { if (time < 1) return `${(time * 1000).toFixed(3)} ms` @@ -73,7 +82,7 @@ const ToolCallItem: FC = ({
{ it('should handle human input form submission for installed app', async () => { const { submitHumanInputForm: submitWorkflowForm } = await import('@/service/workflow') - vi.mocked(submitWorkflowForm).mockResolvedValue({} as unknown as void) + vi.mocked(submitWorkflowForm).mockResolvedValue({}) let resolveWorkflowEventsReady: (isReady: boolean) => void = () => {} const prepareHumanInputSubmission = vi.fn( () => diff --git a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx index 6139fbf46146bf..f3b6c3cf856470 100644 --- a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx +++ b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx @@ -32,8 +32,9 @@ vi.mock('@/context/modal-context', () => ({ }), })) -vi.mock('@/service/annotation', () => ({ - addAnnotation: mockAddAnnotation, +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { apps: { byAppId: { annotations: { post: mockAddAnnotation } } } }, })) vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({ @@ -242,7 +243,7 @@ describe('Operation', () => { mockContextValue.showRegenerate = false mockContextValue.onOpenLog = undefined - mockAddAnnotation.mockResolvedValue({ id: 'ann-new', account: { name: 'Test User' } }) + mockAddAnnotation.mockResolvedValue({ id: 'ann-new' }) }) describe('feedback submission sessions', () => { diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx index a7dbdec3947aa6..40503fcc970bde 100644 --- a/web/app/components/base/chat/chat/answer/operation.tsx +++ b/web/app/components/base/chat/chat/answer/operation.tsx @@ -526,7 +526,6 @@ function Operation({ onAnnotationEdited?.(editedQuery, editedAnswer, index) } appId={config?.appId || ''} - messageId={id} annotationId={annotation.id} createdAt={annotation?.created_at} onRemove={() => onAnnotationRemoved?.(index)} diff --git a/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx b/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx index 821e87a95f0946..ae656b60105b19 100644 --- a/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx @@ -6,6 +6,7 @@ import userEvent from '@testing-library/user-event' import * as React from 'react' import { transcribeAudio } from '@/app/components/base/voice-input/api' import { startVoiceRecorder } from '@/app/components/base/voice-input/recorder' +import { createFileUploadConfigFixture } from '@/test/fixtures/file-upload-config' import { TransferMethod } from '@/types/app' import ChatInputArea from '../index' @@ -174,7 +175,7 @@ vi.mock('@/next/navigation', () => ({ // --------------------------------------------------------------------------- const mockVisionConfig: FileUpload = { enabled: true, - fileUploadConfig: { + fileUploadConfig: createFileUploadConfigFixture({ image_file_size_limit: 10, file_size_limit: 10, audio_file_size_limit: 10, @@ -185,7 +186,7 @@ const mockVisionConfig: FileUpload = { single_chunk_attachment_limit: 0, attachment_image_file_size_limit: 0, file_upload_limit: 0, - }, + }), allowed_file_types: [], allowed_file_extensions: [], number_limits: 3, diff --git a/web/app/components/base/chat/chat/citation/__tests__/popup.spec.tsx b/web/app/components/base/chat/chat/citation/__tests__/popup.spec.tsx index 16b18b00ba38e5..351442be5ed282 100644 --- a/web/app/components/base/chat/chat/citation/__tests__/popup.spec.tsx +++ b/web/app/components/base/chat/chat/citation/__tests__/popup.spec.tsx @@ -1,13 +1,16 @@ import type { Resources } from '../index' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { useDocumentDownload } from '@/service/knowledge/use-document' import { downloadUrl } from '@/utils/download' import Popup from '../popup' -vi.mock('@/service/knowledge/use-document', () => ({ - useDocumentDownload: vi.fn(), -})) +const { mockUseDocumentDownload } = vi.hoisted(() => ({ mockUseDocumentDownload: vi.fn() })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.download.get': () => mockUseDocumentDownload(), + }) +}) vi.mock('@/utils/download', () => ({ downloadUrl: vi.fn(), @@ -30,7 +33,6 @@ vi.mock('../tooltip', () => ({ })) const mockDownloadDocument = vi.fn() -const mockUseDocumentDownload = vi.mocked(useDocumentDownload) const mockDownloadUrl = vi.mocked(downloadUrl) const makeSource = ( @@ -76,7 +78,7 @@ describe('Popup', () => { mockUseDocumentDownload.mockReturnValue({ mutateAsync: mockDownloadDocument, isPending: false, - } as unknown as ReturnType) + }) }) describe('Rendering – Trigger', () => { @@ -210,7 +212,7 @@ describe('Popup', () => { mockUseDocumentDownload.mockReturnValue({ mutateAsync: mockDownloadDocument, isPending: true, - } as unknown as ReturnType) + }) const user = userEvent.setup() render() @@ -504,8 +506,7 @@ describe('Popup', () => { await waitFor(() => { expect(mockDownloadDocument).toHaveBeenCalledWith({ - datasetId: 'ds-1', - documentId: 'doc-1', + params: { dataset_id: 'ds-1', document_id: 'doc-1' }, }) expect(mockDownloadUrl).toHaveBeenCalledWith({ url: 'https://example.com/file.pdf', @@ -547,7 +548,7 @@ describe('Popup', () => { mockUseDocumentDownload.mockReturnValue({ mutateAsync: mockDownloadDocument, isPending: true, - } as unknown as ReturnType) + }) const user = userEvent.setup() render() @@ -575,8 +576,7 @@ describe('Popup', () => { await waitFor(() => { expect(mockDownloadDocument).toHaveBeenCalledWith({ - datasetId: 'ds-1', - documentId: 'primary-doc-id', + params: { dataset_id: 'ds-1', document_id: 'primary-doc-id' }, }) }) }) diff --git a/web/app/components/base/chat/chat/citation/popup.tsx b/web/app/components/base/chat/chat/citation/popup.tsx index 1afd685ca498a6..ab76b7777a31c5 100644 --- a/web/app/components/base/chat/chat/citation/popup.tsx +++ b/web/app/components/base/chat/chat/citation/popup.tsx @@ -1,11 +1,12 @@ import type { FC, MouseEvent } from 'react' import type { Resources } from './index' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { useMutation } from '@tanstack/react-query' import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import FileIcon from '@/app/components/base/file-icon' import Link from '@/next/link' -import { useDocumentDownload } from '@/service/knowledge/use-document' +import { consoleQuery } from '@/service/console' import { downloadUrl } from '@/utils/download' import ProgressTooltip from './progress-tooltip' import Tooltip from './tooltip' @@ -20,7 +21,9 @@ const Popup: FC = ({ data, showHitInfo = false }) => { const fileType = data.dataSourceType !== 'notion' ? /\.([^.]*)$/.exec(data.documentName)?.[1] || '' : 'notion' - const { mutateAsync: downloadDocument, isPending: isDownloading } = useDocumentDownload() + const { mutateAsync: downloadDocument, isPending: isDownloading } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.download.get.mutationOptions(), + ) const handleDownloadUploadFile = async (e: MouseEvent) => { e.preventDefault() @@ -31,7 +34,9 @@ const Popup: FC = ({ data, showHitInfo = false }) => { const documentId = data.documentId || data.sources?.[0]?.document_id if (!isUploadFile || !datasetId || !documentId || isDownloading) return - const res = await downloadDocument({ datasetId, documentId }) + const res = await downloadDocument({ + params: { dataset_id: datasetId, document_id: documentId }, + }) if (res?.url) downloadUrl({ url: res.url, fileName: data.documentName }) } diff --git a/web/app/components/base/chat/chat/type.ts b/web/app/components/base/chat/chat/type.ts index 04f32f79f4f7dc..22299ba1ae4ac6 100644 --- a/web/app/components/base/chat/chat/type.ts +++ b/web/app/components/base/chat/chat/type.ts @@ -134,7 +134,7 @@ export type IChatItem = { extra_contents?: ExtraContent[] } -export type Metadata = { +type Metadata = { retriever_resources?: CitationItem[] annotation_reply: { id: string diff --git a/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx index 159787b56a64d8..a66373148b05af 100644 --- a/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx @@ -5,6 +5,13 @@ import { renderWithConsoleQuery } from '@/test/console/query-data' import { FeaturesProvider } from '../../context' import NewFeaturePanel from '../index' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'codeBasedExtension.get': () => ({ data: undefined }), + }) +}) + vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }), usePathname: () => '/app/test-app-id/configuration', @@ -62,10 +69,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec ModelSelector: () =>
Model Selector
, })) -vi.mock('@/service/use-common', () => ({ - useCodeBasedExtensions: () => ({ data: undefined }), -})) - const defaultFeatures: Features = { moreLikeThis: { enabled: false }, opening: { enabled: false }, diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/annotation-ctrl-button.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/annotation-ctrl-button.spec.tsx index b4387ed2087961..3fc0384d590b84 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/annotation-ctrl-button.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/annotation-ctrl-button.spec.tsx @@ -12,13 +12,15 @@ vi.mock('@/context/modal-context', () => ({ let mockAnnotatedResponseUsage = 5 -const mockAddAnnotation = vi.fn().mockResolvedValue({ - id: 'annotation-1', - account: { name: 'Test User' }, -}) - -vi.mock('@/service/annotation', () => ({ - addAnnotation: (...args: unknown[]) => mockAddAnnotation(...args), +const mockAddAnnotation = vi.fn().mockResolvedValue({ id: 'annotation-1' }) + +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + apps: { + byAppId: { annotations: { post: (...args: unknown[]) => mockAddAnnotation(...args) } }, + }, + }, })) vi.mock('@/app/notifications', () => ({ @@ -128,12 +130,11 @@ describe('AnnotationCtrlButton', () => { fireEvent.click(screen.getByRole('button', { name: 'appDebug.feature.annotation.add' })) await waitFor(() => { - expect(mockAddAnnotation).toHaveBeenCalledWith('test-app', { - message_id: 'msg-1', - question: 'test query', - answer: 'test answer', + expect(mockAddAnnotation).toHaveBeenCalledWith({ + params: { app_id: 'test-app' }, + body: { message_id: 'msg-1', question: 'test query', answer: 'test answer' }, }) - expect(onAdded).toHaveBeenCalledWith('annotation-1', 'Test User') + expect(onAdded).toHaveBeenCalledWith('annotation-1', '') }) }) @@ -156,30 +157,4 @@ describe('AnnotationCtrlButton', () => { expect(mockSetShowAnnotationFullModal).toHaveBeenCalled() expect(mockAddAnnotation).not.toHaveBeenCalled() }) - - it('should fallback author name to empty string when account name is missing', async () => { - const onAdded = vi.fn() - mockAddAnnotation.mockResolvedValueOnce({ - id: 'annotation-2', - account: undefined, - }) - - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: 'appDebug.feature.annotation.add' })) - - await waitFor(() => { - expect(onAdded).toHaveBeenCalledWith('annotation-2', '') - }) - }) }) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts index ec76acd16facb9..a8d0a18222a962 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts @@ -1,15 +1,29 @@ import type { AnnotationReplyConfig } from '@/models/debug' import { act } from '@testing-library/react' -import { queryAnnotationJobStatus, updateAnnotationStatus } from '@/service/annotation' +import { updateAnnotationStatus } from '@/service/annotation' import { renderHookWithConsoleQuery } from '@/test/console/query-data' import { sleep } from '@/utils' import useAnnotationConfig from '../use-annotation-config' let mockIsAnnotationFull = false +const { queryAnnotationJobStatus } = vi.hoisted(() => ({ + queryAnnotationJobStatus: vi.fn().mockResolvedValue({ job_status: 'completed' }), +})) + vi.mock('@/service/annotation', () => ({ updateAnnotationStatus: vi.fn().mockResolvedValue({ job_id: 'test-job-id' }), - queryAnnotationJobStatus: vi.fn().mockResolvedValue({ job_status: 'completed' }), +})) + +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + apps: { + byAppId: { + annotationReply: { byAction: { status: { byJobId: { get: queryAnnotationJobStatus } } } }, + }, + }, + }, })) vi.mock('@/utils', () => ({ @@ -328,16 +342,12 @@ describe('useAnnotationConfig', () => { it('should poll job status until completed when enabling annotation', async () => { const setAnnotationConfig = vi.fn() - const queryJobStatusMock = vi.mocked(queryAnnotationJobStatus) + const queryJobStatusMock = queryAnnotationJobStatus const sleepMock = vi.mocked(sleep) queryJobStatusMock - .mockResolvedValueOnce({ job_status: 'pending' } as unknown as Awaited< - ReturnType - >) - .mockResolvedValueOnce({ job_status: 'completed' } as unknown as Awaited< - ReturnType - >) + .mockResolvedValueOnce({ job_status: 'pending' }) + .mockResolvedValueOnce({ job_status: 'completed' }) const { result } = renderHook(() => useAnnotationConfig({ diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx index af1203900ef5ce..05f7e7c035d73a 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx @@ -10,8 +10,7 @@ import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { useModalContext } from '@/context/modal-context' import { deploymentEditionAtom } from '@/features/system-features/state' -import { addAnnotation } from '@/service/annotation' -import { consoleQuery } from '@/service/console' +import { consoleClient, consoleQuery } from '@/service/console' type Props = Readonly<{ appId: string @@ -54,13 +53,13 @@ const AnnotationCtrlButton: FC = ({ setShowAnnotationFullModal() return } - const res = await addAnnotation(appId, { - message_id: messageId, - question: query, - answer, + const res = await consoleClient.apps.byAppId.annotations.post({ + params: { app_id: appId }, + body: { message_id: messageId, question: query, answer }, }) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' }) as string) - onAdded(res.id, res.account?.name ?? '') + // The create response does not include the author. + onAdded(res.id, '') } return ( <> diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts b/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts index 58e4b238979936..9b8a7af5f0a25a 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts @@ -8,8 +8,8 @@ import { useState } from 'react' import { AnnotationEnableStatus, JobStatus } from '@/app/components/app/annotation/type' import { ANNOTATION_DEFAULT } from '@/config' import { deploymentEditionAtom } from '@/features/system-features/state' -import { queryAnnotationJobStatus, updateAnnotationStatus } from '@/service/annotation' -import { consoleQuery } from '@/service/console' +import { updateAnnotationStatus } from '@/service/annotation' +import { consoleClient, consoleQuery } from '@/service/console' import { sleep } from '@/utils' type Params = { @@ -48,7 +48,9 @@ const useAnnotationConfig = ({ appId, annotationConfig, setAnnotationConfig }: P const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => { let isCompleted = false while (!isCompleted) { - const res: any = await queryAnnotationJobStatus(appId, status, jobId) + const res = await consoleClient.apps.byAppId.annotationReply.byAction.status.byJobId.get({ + params: { app_id: appId, action: status, job_id: jobId }, + }) isCompleted = res.job_status === JobStatus.completed if (isCompleted) break @@ -59,7 +61,7 @@ const useAnnotationConfig = ({ appId, annotationConfig, setAnnotationConfig }: P const handleEnableAnnotation = async (embeddingModel: EmbeddingModelConfig, score?: number) => { if (!annotationConfig.enabled && (isAnnotationQuotaUnavailable || isAnnotationFull)) return - const { job_id: jobId }: any = await updateAnnotationStatus( + const { job_id: jobId } = await updateAnnotationStatus( appId, AnnotationEnableStatus.enable, embeddingModel, diff --git a/web/app/components/base/features/new-feature-panel/file-upload/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/file-upload/__tests__/index.spec.tsx index c1a1a88e7171ea..198acd3b06d080 100644 --- a/web/app/components/base/features/new-feature-panel/file-upload/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/file-upload/__tests__/index.spec.tsx @@ -3,9 +3,12 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { FeaturesProvider } from '../../../context' import FileUpload from '../index' -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ data: undefined }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ data: undefined }), + }) +}) const defaultFeatures: Features = { moreLikeThis: { enabled: false }, diff --git a/web/app/components/base/features/new-feature-panel/file-upload/__tests__/setting-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/file-upload/__tests__/setting-modal.spec.tsx index 68b18fe209a08b..0cdbab950cc6a5 100644 --- a/web/app/components/base/features/new-feature-panel/file-upload/__tests__/setting-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/file-upload/__tests__/setting-modal.spec.tsx @@ -4,9 +4,12 @@ import { TransferMethod } from '@/types/app' import { FeaturesProvider } from '../../../context' import FileUploadSettings from '../setting-modal' -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ data: undefined }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ data: undefined }), + }) +}) const defaultFeatures: Features = { moreLikeThis: { enabled: false }, diff --git a/web/app/components/base/features/new-feature-panel/image-upload/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/image-upload/__tests__/index.spec.tsx index b86e1172484d84..810581eeff163f 100644 --- a/web/app/components/base/features/new-feature-panel/image-upload/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/image-upload/__tests__/index.spec.tsx @@ -3,9 +3,12 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { FeaturesProvider } from '../../../context' import ImageUpload from '../index' -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ data: undefined }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ data: undefined }), + }) +}) const defaultFeatures: Features = { moreLikeThis: { enabled: false }, diff --git a/web/app/components/base/features/new-feature-panel/moderation/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/moderation/__tests__/index.spec.tsx index 90f2cda16ee205..043080402b4824 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/__tests__/index.spec.tsx @@ -13,9 +13,6 @@ vi.mock('@/context/modal-context', () => ({ setShowModerationSettingModal: mockSetShowModerationSettingModal, }), })) -vi.mock('@/service/use-common', () => ({ - useCodeBasedExtensions: () => ({ data: { data: mockCodeBasedExtensionData } }), -})) const defaultFeatures: Features = { moreLikeThis: { enabled: false }, @@ -41,6 +38,13 @@ const renderWithProvider = ( ) } +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'codeBasedExtension.get': () => ({ data: { data: mockCodeBasedExtensionData } }), + }) +}) + describe('Moderation', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal-submission.spec.tsx b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal-submission.spec.tsx index d539f41148fa40..c9d4d4dbebedad 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal-submission.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal-submission.spec.tsx @@ -6,6 +6,14 @@ import userEvent from '@testing-library/user-event' import { renderWithConsoleQuery } from '@/test/console/query-data' import ModerationSettingModal from '../moderation-setting-modal' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'codeBasedExtension.get': () => ({ data: { data: [] } }), + 'workspaces.current.modelProviders.get': () => ({ data: { data: [] }, isPending: false }), + }) +}) + const { getExtensions, createExtension } = vi.hoisted(() => ({ getExtensions: vi.fn(), createExtension: vi.fn(), @@ -29,11 +37,6 @@ vi.mock('@/service/console', async (importOriginal) => { return { ...actual, consoleClient, consoleQuery: createConsoleQuery(consoleClient) } }) -vi.mock('@/service/use-common', () => ({ - useCodeBasedExtensions: () => ({ data: { data: [] } }), - useModelProviderDetails: () => ({ data: { data: [] }, isPending: false }), -})) - vi.mock('nuqs', async (importOriginal) => ({ ...(await importOriginal()), useQueryState: () => [null, vi.fn()], diff --git a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx index 1d5d5101bedb1c..09147855a76bb1 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx @@ -50,14 +50,15 @@ let mockModelProvidersData: { refetch: vi.fn(), } -vi.mock('@/service/use-common', () => ({ - useCodeBasedExtensions: () => mockCodeBasedExtensions, - useModelProviderDetails: () => mockModelProvidersData, -})) - -vi.mock('@/app/components/header/account-setting/model-provider-page/declarations', () => ({ - CustomConfigurationStatusEnum: { active: 'active' }, -})) +vi.mock( + '@/app/components/header/account-setting/model-provider-page/declarations', + async (importOriginal) => ({ + ...(await importOriginal< + typeof import('@/app/components/header/account-setting/model-provider-page/declarations') + >()), + CustomConfigurationStatusEnum: { active: 'active' }, + }), +) vi.mock('@/app/components/header/account-setting/api-based-extension-page/selector', () => ({ ApiBasedExtensionSelector: ({ onChange }: { value: string; onChange: (v: string) => void }) => ( @@ -79,6 +80,14 @@ const defaultData: ModerationConfig = { }, } +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'codeBasedExtension.get': () => mockCodeBasedExtensions, + 'workspaces.current.modelProviders.get': () => mockModelProvidersData, + }) +}) + describe('ModerationSettingModal', () => { const onSave = vi.fn() const renderModal = async (ui: React.ReactElement) => { diff --git a/web/app/components/base/features/new-feature-panel/moderation/index.tsx b/web/app/components/base/features/new-feature-panel/moderation/index.tsx index d979de50ca2b59..634e87649f7bf9 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/index.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/index.tsx @@ -1,6 +1,7 @@ import type { OnFeaturesChange } from '@/app/components/base/features/types' import { Button } from '@langgenius/dify-ui/button' import { RiEqualizer2Line } from '@remixicon/react' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -9,7 +10,7 @@ import { useFeatures, useFeaturesStore } from '@/app/components/base/features/ho import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card' import { FeatureEnum } from '@/app/components/base/features/types' import { useModalContext } from '@/context/modal-context' -import { useCodeBasedExtensions } from '@/service/use-common' +import { codeBasedExtensionsQueryOptions } from '@/features/code-based-extension/query' type Props = Readonly<{ disabled?: boolean @@ -22,7 +23,7 @@ const Moderation = ({ disabled, onChange }: Props) => { const locale = useLocale() const featuresStore = useFeaturesStore() const moderation = useFeatures((s) => s.features.moderation) - const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation') + const { data: codeBasedExtensionList } = useQuery(codeBasedExtensionsQueryOptions('moderation')) const [isHovering, setIsHovering] = useState(false) const handleOpenModerationSettingModal = () => { diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index cac0f95ebb57d4..6ba79b9ef6474d 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -6,20 +6,22 @@ import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Separator } from '@langgenius/dify-ui/separator' import { Textarea } from '@langgenius/dify-ui/textarea' +import { useQuery } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { ApiBasedExtensionSelector } from '@/app/components/header/account-setting/api-based-extension-page/selector' import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { modelProviderDetailsQueryOptions } from '@/app/components/header/account-setting/model-provider-page/utils' import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' +import { codeBasedExtensionsQueryOptions } from '@/features/code-based-extension/query' import { LanguagesSupported } from '@/i18n/language' -import { useCodeBasedExtensions, useModelProviderDetails } from '@/service/use-common' import FormGeneration from './form-generation' import ModerationContent from './moderation-content' @@ -60,7 +62,9 @@ const ModerationSettingModal: FC = ({ data, onCance const { t } = useTranslation(['appDebug', 'common', 'navigation']) const docLink = useDocLink() const locale = useLocale() - const { data: modelProviders, isPending: isLoading } = useModelProviderDetails() + const { data: modelProviders, isPending: isLoading } = useQuery( + modelProviderDetailsQueryOptions(), + ) const localeDataRef = useRef(data) const [localeData, setLocaleData] = useState(data) const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser) @@ -80,7 +84,7 @@ const ModerationSettingModal: FC = ({ data, onCance const handleOpenSettingsModal = () => { setSettingsDestination('provider') } - const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation') + const { data: codeBasedExtensionList } = useQuery(codeBasedExtensionsQueryOptions('moderation')) const openaiProvider = modelProviders?.data.find( (item) => item.provider === 'langgenius/openai/openai', ) diff --git a/web/app/components/base/linked-apps-panel/__tests__/index.spec.tsx b/web/app/components/base/linked-apps-panel/__tests__/index.spec.tsx index 518a468a010a98..198da5ac081d14 100644 --- a/web/app/components/base/linked-apps-panel/__tests__/index.spec.tsx +++ b/web/app/components/base/linked-apps-panel/__tests__/index.spec.tsx @@ -24,6 +24,7 @@ describe('LinkedAppsPanel Component', () => { { id: 'app-1', name: 'Chatbot App', + description: '', mode: 'chat' as const, icon_type: 'emoji' as const, icon: '🤖', @@ -33,6 +34,7 @@ describe('LinkedAppsPanel Component', () => { { id: 'app-2', name: 'Workflow App', + description: '', mode: 'workflow' as const, icon_type: 'image' as const, icon: 'file-id', @@ -42,6 +44,7 @@ describe('LinkedAppsPanel Component', () => { { id: 'app-3', name: '', + description: '', mode: 'agent-chat' as const, icon_type: 'emoji' as const, icon: '🕵️', diff --git a/web/app/components/base/linked-apps-panel/index.stories.tsx b/web/app/components/base/linked-apps-panel/index.stories.tsx index 4b2517ec227225..80a92d5d2ac3b9 100644 --- a/web/app/components/base/linked-apps-panel/index.stories.tsx +++ b/web/app/components/base/linked-apps-panel/index.stories.tsx @@ -6,6 +6,7 @@ const mockRelatedApps: RelatedApp[] = [ { id: 'app-cx', name: 'Customer Support Assistant', + description: '', mode: 'chat' as const, icon_type: 'emoji', icon: '\u{1F4AC}', @@ -15,6 +16,7 @@ const mockRelatedApps: RelatedApp[] = [ { id: 'app-ops', name: 'Ops Workflow Orchestrator', + description: '', mode: 'workflow' as const, icon_type: 'emoji', icon: '\u{1F6E0}\u{FE0F}', @@ -24,6 +26,7 @@ const mockRelatedApps: RelatedApp[] = [ { id: 'app-research', name: 'Research Synthesizer', + description: '', mode: 'advanced-chat' as const, icon_type: 'emoji', icon: '\u{1F9E0}', diff --git a/web/app/components/base/notion-page-selector/__tests__/base.spec.tsx b/web/app/components/base/notion-page-selector/__tests__/base.spec.tsx index 8975198bbc84c1..f5ce1df6354b7d 100644 --- a/web/app/components/base/notion-page-selector/__tests__/base.spec.tsx +++ b/web/app/components/base/notion-page-selector/__tests__/base.spec.tsx @@ -4,17 +4,20 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types' -import { - useInvalidPreImportNotionPages, - usePreImportNotionPages, -} from '@/service/knowledge/use-import' +import { consoleQuery } from '@/service/console' import NotionPageSelector from '../base' vi.mock('@tanstack/react-virtual') -vi.mock('@/service/knowledge/use-import', () => ({ +const { usePreImportNotionPages, mockInvalidateQueries } = vi.hoisted(() => ({ usePreImportNotionPages: vi.fn(), - useInvalidPreImportNotionPages: vi.fn(), + mockInvalidateQueries: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => ({ + ...(await importOriginal()), + useQuery: () => usePreImportNotionPages(), + useQueryClient: () => ({ invalidateQueries: mockInvalidateQueries }), })) const mockSetSettingsDestination = vi.fn() @@ -101,23 +104,20 @@ const createPreImportResult = ({ notionInfo?: DataSourceNotionWorkspace[] isFetching?: boolean isError?: boolean -} = {}) => - ({ - data: { notion_info: notionInfo }, - isFetching, - isError, - }) as ReturnType +} = {}) => ({ + // The query selects the normalized workspaces. + data: notionInfo, + isFetching, + isError, +}) describe('NotionPageSelector Base', () => { - const mockInvalidPreImportNotionPages = vi.fn() - beforeEach(() => { vi.clearAllMocks() - vi.mocked(useInvalidPreImportNotionPages).mockReturnValue(mockInvalidPreImportNotionPages) }) it('should render loading state when pages are being fetched', () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult({ isFetching: true })) + usePreImportNotionPages.mockReturnValue(createPreImportResult({ isFetching: true })) render() @@ -125,7 +125,7 @@ describe('NotionPageSelector Base', () => { }) it('should render connector and open settings when fetch fails', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult({ isError: true })) + usePreImportNotionPages.mockReturnValue(createPreImportResult({ isError: true })) const user = userEvent.setup() render() @@ -137,7 +137,7 @@ describe('NotionPageSelector Base', () => { }) it('should render page selector and allow selecting a page tree', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const handleSelect = vi.fn() const user = userEvent.setup() @@ -160,7 +160,7 @@ describe('NotionPageSelector Base', () => { }) it('should keep bound pages disabled and selected by default', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const handleSelect = vi.fn() const user = userEvent.setup() render() @@ -172,7 +172,7 @@ describe('NotionPageSelector Base', () => { }) it('should filter and clear search results from search input actions', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const user = userEvent.setup() render() @@ -185,7 +185,7 @@ describe('NotionPageSelector Base', () => { }) it('should switch credential and reset selection when choosing a different workspace', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const handleSelect = vi.fn() const onSelectCredential = vi.fn() const user = userEvent.setup() @@ -203,16 +203,17 @@ describe('NotionPageSelector Base', () => { const item2 = screen.getByRole('option', { name: /Workspace 2/ }) await user.click(item2) - expect(mockInvalidPreImportNotionPages).toHaveBeenCalledWith({ - datasetId: 'dataset-1', - credentialId: 'c2', + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.notion.preImport.pages.get.queryKey({ + input: { query: { dataset_id: 'dataset-1', credential_id: 'c2' } }, + }), }) expect(handleSelect).toHaveBeenCalledWith([]) expect(onSelectCredential).toHaveBeenLastCalledWith('c2') }) it('should open settings when configuration action in header is clicked', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const user = userEvent.setup() render() @@ -223,7 +224,7 @@ describe('NotionPageSelector Base', () => { }) it('should preview a page and call onPreview when callback is provided', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const onPreview = vi.fn() const user = userEvent.setup() render( @@ -243,7 +244,7 @@ describe('NotionPageSelector Base', () => { }) it('should handle preview click without onPreview callback', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const user = userEvent.setup() render() await user.click(screen.getByTestId('notion-page-preview-root-1')) @@ -251,7 +252,7 @@ describe('NotionPageSelector Base', () => { }) it('should call onSelectCredential with current credential on initial render', () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const onSelectCredential = vi.fn() render( { }) it('should fallback to first credential when current credential is removed in error mode', async () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult({ isError: true })) + usePreImportNotionPages.mockReturnValue(createPreImportResult({ isError: true })) const onSelect = vi.fn() const onSelectCredential = vi.fn() const { rerender } = render( @@ -287,9 +288,10 @@ describe('NotionPageSelector Base', () => { ) await waitFor(() => { - expect(mockInvalidPreImportNotionPages).toHaveBeenCalledWith({ - datasetId: 'dataset-fallback', - credentialId: 'c3', + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.notion.preImport.pages.get.queryKey({ + input: { query: { dataset_id: 'dataset-fallback', credential_id: 'c3' } }, + }), }) expect(onSelect).toHaveBeenCalledWith([]) expect(onSelectCredential).toHaveBeenLastCalledWith('c3') @@ -297,7 +299,7 @@ describe('NotionPageSelector Base', () => { }) it('should update selected page state when controlled value changes', () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const { rerender } = render( { }) it('should hide preview actions when canPreview is false', () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) render( { }) it('should handle undefined data gracefully during loading', () => { - vi.mocked(usePreImportNotionPages).mockReturnValue({ + usePreImportNotionPages.mockReturnValue({ data: undefined, isFetching: true, isError: false, @@ -339,7 +341,7 @@ describe('NotionPageSelector Base', () => { }) it('should handle credential with empty id', () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const onSelectCredential = vi.fn() render( { expect(onSelectCredential).toHaveBeenCalledWith('') }) - it('should render empty page selector when notion_info is empty', () => { - vi.mocked(usePreImportNotionPages).mockReturnValue({ - data: { notion_info: undefined }, + it('should render empty page selector when there are no workspaces', () => { + usePreImportNotionPages.mockReturnValue({ + data: [], isFetching: false, isError: false, } as unknown as ReturnType) @@ -362,7 +364,7 @@ describe('NotionPageSelector Base', () => { }) it('should run credential effect fallback when onSelectCredential is not provided', () => { - vi.mocked(usePreImportNotionPages).mockReturnValue(createPreImportResult()) + usePreImportNotionPages.mockReturnValue(createPreImportResult()) const { rerender } = render( , ) diff --git a/web/app/components/base/notion-page-selector/base.tsx b/web/app/components/base/notion-page-selector/base.tsx index c0762bb0cef5c5..89f89806586ec1 100644 --- a/web/app/components/base/notion-page-selector/base.tsx +++ b/web/app/components/base/notion-page-selector/base.tsx @@ -1,3 +1,4 @@ +import type { NotionIntegrateInfoListResponse } from '@dify/contracts/api/console/notion/types.gen' import type { DataSourceCredential } from '../../header/account-setting/data-source-page-new/types' import type { NotionCredential } from './credential-selector' import type { @@ -5,6 +6,7 @@ import type { DataSourceNotionWorkspace, NotionPage, } from '@/models/common' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -13,16 +15,35 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' -import { - useInvalidPreImportNotionPages, - usePreImportNotionPages, -} from '@/service/knowledge/use-import' +import { consoleQuery } from '@/service/console' import Header from '../../datasets/create/website/base/header' import NotionConnector from '../notion-connector' import WorkspaceSelector from './credential-selector' import PageSelector from './page-selector' import SearchInput from './search-input' +// Top-level pages have no parent in the API response; the page tree calls them `root`. +const toNotionWorkspaces = ({ notion_info }: NotionIntegrateInfoListResponse) => + notion_info.map((workspace): DataSourceNotionWorkspace => ({ + ...workspace, + workspace_id: workspace.workspace_id ?? '', + workspace_name: workspace.workspace_name ?? '', + pages: workspace.pages.map((page) => ({ + ...page, + parent_id: page.parent_id ?? 'root', + page_icon: page.page_icon && { + type: page.page_icon.type ?? null, + url: page.page_icon.url ?? null, + emoji: page.page_icon.emoji ?? null, + }, + })), + })) + +const getStringField = (record: Record, key: string) => { + const value = record[key] + return typeof value === 'string' ? value : undefined +} + type NotionPageSelectorProps = { value?: string[] onSelect: (selectedPages: NotionPage[]) => void @@ -48,15 +69,24 @@ const NotionPageSelector = ({ const [searchValue, setSearchValue] = useState('') const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser) - const invalidPreImportNotionPages = useInvalidPreImportNotionPages() + const queryClient = useQueryClient() + const invalidPreImportNotionPages = useCallback( + ({ datasetId, credentialId }: { datasetId: string; credentialId: string }) => + queryClient.invalidateQueries({ + queryKey: consoleQuery.notion.preImport.pages.get.queryKey({ + input: { query: { dataset_id: datasetId, credential_id: credentialId } }, + }), + }), + [queryClient], + ) const notionCredentials = useMemo((): NotionCredential[] => { return credentialList.map((item) => { return { credentialId: item.id, credentialName: item.name, - workspaceIcon: item.credential.workspace_icon, - workspaceName: item.credential.workspace_name, + workspaceIcon: getStringField(item.credential, 'workspace_icon'), + workspaceName: getStringField(item.credential, 'workspace_name'), } }) }, [credentialList]) @@ -99,14 +129,20 @@ const NotionPageSelector = ({ data: notionsPages, isFetching: isFetchingNotionPages, isError: isFetchingNotionPagesError, - } = usePreImportNotionPages({ datasetId, credentialId: currentCredentialId }) - const notionPages = notionsPages?.notion_info?.flatMap((workspace) => workspace.pages) || [] + } = useQuery( + consoleQuery.notion.preImport.pages.get.queryOptions({ + input: { query: { dataset_id: datasetId, credential_id: currentCredentialId } }, + retry: 0, + select: toNotionWorkspaces, + }), + ) + const notionPages = notionsPages?.flatMap((workspace) => workspace.pages) || [] const pagesMapAndSelectedPagesId: [DataSourceNotionPageMap, Set, Set] = useMemo(() => { const selectedPagesId = new Set() const boundPagesId = new Set() - const notionWorkspaces = notionsPages?.notion_info || [] + const notionWorkspaces = notionsPages || [] const pagesMap = notionWorkspaces.reduce( (prev: DataSourceNotionPageMap, cur: DataSourceNotionWorkspace) => { cur.pages.forEach((page) => { @@ -125,7 +161,7 @@ const NotionPageSelector = ({ {}, ) return [pagesMap, selectedPagesId, boundPagesId] - }, [notionsPages?.notion_info]) + }, [notionsPages]) const defaultSelectedPagesId = useMemo(() => { return [...Array.from(pagesMapAndSelectedPagesId[1]), ...(value || [])] diff --git a/web/app/components/base/prompt-editor/plugins/context-block/__tests__/context-block-replacement-block.spec.tsx b/web/app/components/base/prompt-editor/plugins/context-block/__tests__/context-block-replacement-block.spec.tsx index c86c37be3d22c1..b3547cea169ba1 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block/__tests__/context-block-replacement-block.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/__tests__/context-block-replacement-block.spec.tsx @@ -53,7 +53,7 @@ function EditorCapture({ type ReadResult = { count: number - datasets: Array<{ id: string; name: string; type: string }> + datasets: Array<{ id: string; name: string; type: string | null }> canNotAddContext: boolean } diff --git a/web/app/components/base/prompt-editor/plugins/context-block/index.tsx b/web/app/components/base/prompt-editor/plugins/context-block/index.tsx index 972fc5314a69b5..e601716ea85b95 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/index.tsx @@ -12,7 +12,7 @@ export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_ export type Dataset = { id: string name: string - type: string + type: string | null } const ContextBlock = memo( diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field-keyboard.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field-keyboard.spec.tsx index 300e8554f93728..2c3dad2fb71fa2 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field-keyboard.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field-keyboard.spec.tsx @@ -6,9 +6,12 @@ import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/ import { TransferMethod } from '@/types/app' import InputField from '../input-field' -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ data: undefined }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ data: undefined }), + }) +}) it('lets the extension input commit its tag before the field shortcut saves', async () => { const user = userEvent.setup() diff --git a/web/app/components/datasets/__tests__/dataset-fields.spec.ts b/web/app/components/datasets/__tests__/dataset-fields.spec.ts new file mode 100644 index 00000000000000..51980385012b0c --- /dev/null +++ b/web/app/components/datasets/__tests__/dataset-fields.spec.ts @@ -0,0 +1,87 @@ +import { + toChunkingMode, + toCreationDataSourceType, + toIconInfo, + toRetrievalConfig, + toSummaryIndexSetting, +} from '../dataset-fields' + +describe('dataset fields', () => { + it('keeps chunk structures the console renders', () => { + expect(toChunkingMode('hierarchical_model')).toBe('hierarchical_model') + expect(toChunkingMode('graph_model')).toBeUndefined() + expect(toChunkingMode(null)).toBeUndefined() + }) + + it('keeps only the classic creation data sources', () => { + expect(toCreationDataSourceType('notion_import')).toBe('notion_import') + expect(toCreationDataSourceType('online_drive')).toBeUndefined() + }) + + it('reads an icon only when one was chosen', () => { + expect( + toIconInfo({ icon: '📙', icon_type: 'emoji', icon_background: '#FFF', icon_url: null }), + ).toEqual({ icon: '📙', icon_type: 'emoji', icon_background: '#FFF', icon_url: undefined }) + expect(toIconInfo({ icon: null, icon_type: 'emoji' })).toBeUndefined() + expect(toIconInfo({ icon: 'file-1', icon_type: 'svg' })).toBeUndefined() + expect(toIconInfo(undefined)).toBeUndefined() + }) + + it('leaves unset summary settings unset', () => { + expect(toSummaryIndexSetting({ enable: true, model_name: null })).toEqual({ + enable: true, + model_name: undefined, + model_provider_name: undefined, + summary_prompt: undefined, + }) + }) + + describe('toRetrievalConfig', () => { + const retrieval = { + search_method: 'hybrid_search', + reranking_enable: true, + top_k: 4, + score_threshold_enabled: false, + } as const + + it('reads unset reranking models as no model selected', () => { + expect(toRetrievalConfig(retrieval)).toEqual({ + ...retrieval, + reranking_model: { reranking_provider_name: '', reranking_model_name: '' }, + score_threshold: undefined, + reranking_mode: undefined, + weights: undefined, + }) + }) + + it('keeps weighted scores only when both weights were stored', () => { + const weights = { + weight_type: 'customized', + vector_setting: { vector_weight: 0.7, embedding_model_name: 'embed' }, + keyword_setting: { keyword_weight: 0.3 }, + } + + expect( + toRetrievalConfig({ ...retrieval, reranking_mode: 'weighted_score', weights }), + ).toMatchObject({ + reranking_mode: 'weighted_score', + weights: { + weight_type: 'customized', + vector_setting: { + vector_weight: 0.7, + embedding_provider_name: '', + embedding_model_name: 'embed', + }, + keyword_setting: { keyword_weight: 0.3 }, + }, + }) + expect( + toRetrievalConfig({ ...retrieval, weights: { ...weights, keyword_setting: {} } }).weights, + ).toBeUndefined() + expect( + toRetrievalConfig({ ...retrieval, weights: { ...weights, weight_type: null } }).weights + ?.weight_type, + ).toBe('customized') + }) + }) +}) diff --git a/web/app/components/datasets/common/document-picker/__tests__/document-list.spec.tsx b/web/app/components/datasets/common/document-picker/__tests__/document-list.spec.tsx index 55d161c9506a08..318ce3457c3d99 100644 --- a/web/app/components/datasets/common/document-picker/__tests__/document-list.spec.tsx +++ b/web/app/components/datasets/common/document-picker/__tests__/document-list.spec.tsx @@ -15,9 +15,7 @@ vi.mock('../../document-file-icon', () => ({ const createDocument = (overrides: Partial = {}): SimpleDocumentDetail => ({ id: 'doc-1', - batch: 'batch-1', position: 1, - dataset_id: 'dataset-1', data_source_type: DataSourceType.FILE, data_source_info: { upload_file: { @@ -40,16 +38,19 @@ const createDocument = (overrides: Partial = {}): SimpleDo indexing_status: 'completed', display_status: 'enabled', doc_form: ChunkingMode.text, - doc_language: 'en', enabled: true, word_count: 1000, archived: false, - updated_at: Date.now(), hit_count: 0, data_source_detail_dict: { upload_file: { + id: 'file-1', name: 'report.pdf', + size: 1024, extension: 'pdf', + mime_type: 'application/pdf', + created_by: 'user-1', + created_at: 1700000000, }, }, ...overrides, diff --git a/web/app/components/datasets/common/document-picker/__tests__/index.spec.tsx b/web/app/components/datasets/common/document-picker/__tests__/index.spec.tsx index ed34b8c9e93b96..eb735992cfd246 100644 --- a/web/app/components/datasets/common/document-picker/__tests__/index.spec.tsx +++ b/web/app/components/datasets/common/document-picker/__tests__/index.spec.tsx @@ -11,15 +11,22 @@ const { mockUseDocumentList } = vi.hoisted(() => ({ mockUseDocumentList: vi.fn(), })) -vi.mock('@/service/knowledge/use-document', () => ({ - useDocumentList: mockUseDocumentList, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.get': (options: { + input: { params: { dataset_id: string }; query: Record } + }) => + mockUseDocumentList({ + datasetId: options.input.params.dataset_id, + query: options.input.query, + }), + }) +}) const createDocument = (overrides: Partial = {}): SimpleDocumentDetail => ({ id: 'doc-1', - batch: 'batch-1', position: 1, - dataset_id: 'dataset-1', data_source_type: DataSourceType.FILE, data_source_info: { upload_file: { @@ -42,16 +49,19 @@ const createDocument = (overrides: Partial = {}): SimpleDo indexing_status: 'completed', display_status: 'enabled', doc_form: ChunkingMode.text, - doc_language: 'en', enabled: true, word_count: 1000, archived: false, - updated_at: Date.now(), hit_count: 0, data_source_detail_dict: { upload_file: { + id: 'file-1', name: 'document.pdf', + size: 1024, extension: 'pdf', + mime_type: 'application/pdf', + created_by: 'user-1', + created_at: 1700000000, }, }, ...overrides, diff --git a/web/app/components/datasets/common/document-picker/document-list.tsx b/web/app/components/datasets/common/document-picker/document-list.tsx index 0dca1538f50f77..fbb0a573a6dd58 100644 --- a/web/app/components/datasets/common/document-picker/document-list.tsx +++ b/web/app/components/datasets/common/document-picker/document-list.tsx @@ -2,6 +2,7 @@ import type { SimpleDocumentDetail } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' import { ComboboxItem, ComboboxItemText, ComboboxList } from '@langgenius/dify-ui/combobox' +import { getUploadFileExtension } from '@/app/components/datasets/documents/document-source' import FileIcon from '../document-file-icon' type Props = Readonly<{ @@ -12,10 +13,7 @@ function getDocumentExtension(document: SimpleDocumentDetail) { const detailExtension = document.data_source_detail_dict?.upload_file?.extension if (detailExtension) return detailExtension - const dataSourceInfo = document.data_source_info - if (dataSourceInfo && 'upload_file' in dataSourceInfo) return dataSourceInfo.upload_file.extension - - return '' + return getUploadFileExtension(document.data_source_info) ?? '' } export default function DocumentList({ className }: Props) { diff --git a/web/app/components/datasets/common/document-picker/index.tsx b/web/app/components/datasets/common/document-picker/index.tsx index 46531e7a5bb879..0302eab536e63a 100644 --- a/web/app/components/datasets/common/document-picker/index.tsx +++ b/web/app/components/datasets/common/document-picker/index.tsx @@ -15,50 +15,55 @@ import { ComboboxValue, } from '@langgenius/dify-ui/combobox' import { SpinnerIcon } from '@langgenius/dify-ui/spinner' +import { keepPreviousData, useQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { getUploadFileExtension } from '@/app/components/datasets/documents/document-source' import { ChunkingMode } from '@/models/datasets' -import { useDocumentList } from '@/service/knowledge/use-document' +import { consoleQuery } from '@/service/console' import FileIcon from '../document-file-icon' import DocumentList from './document-list' +// The list query and the document detail query both describe the picked document. +export type PickerDocument = Pick & { + name?: string | null + data_source_detail_dict?: SimpleDocumentDetail['data_source_detail_dict'] | null +} + type Props = Readonly<{ datasetId: string - value?: SimpleDocumentDetail | null + value?: PickerDocument | null parentMode?: ParentMode - onChange: (value: SimpleDocumentDetail) => void + onChange: (value: PickerDocument) => void }> -function getDocumentLabel(document: SimpleDocumentDetail) { - return document.name +function getDocumentLabel(document: PickerDocument) { + return document.name ?? '' } -function getDocumentValue(document: SimpleDocumentDetail) { +function getDocumentValue(document: PickerDocument) { return document.id } -function isSameDocument(item: SimpleDocumentDetail, value: SimpleDocumentDetail) { +function isSameDocument(item: PickerDocument, value: PickerDocument) { return item.id === value.id } -function getDocumentExtension(document?: SimpleDocumentDetail | null) { +function getDocumentExtension(document?: PickerDocument | null) { if (!document) return '' const detailExtension = document.data_source_detail_dict?.upload_file?.extension if (detailExtension) return detailExtension - const dataSourceInfo = document.data_source_info - if (dataSourceInfo && 'upload_file' in dataSourceInfo) return dataSourceInfo.upload_file.extension - - return '' + return getUploadFileExtension(document.data_source_info) ?? '' } function DocumentPickerTriggerValue({ document, parentMode, }: { - document?: SimpleDocumentDetail | null + document?: PickerDocument | null parentMode?: ParentMode }) { const { t } = useTranslation(['dataset']) @@ -74,7 +79,11 @@ function DocumentPickerTriggerValue({ return ( - + @@ -112,14 +121,15 @@ export function DocumentPicker({ datasetId, value, parentMode, onChange }: Props const [searchValue, setSearchValue] = useState('') const debouncedSearchValue = useDebounce(searchValue, { wait: 500 }) - const { data } = useDocumentList({ - datasetId, - query: { - keyword: debouncedSearchValue, - page: 1, - limit: 20, - }, - }) + const { data } = useQuery( + consoleQuery.datasets.byDatasetId.documents.get.queryOptions({ + input: { + params: { dataset_id: datasetId }, + query: { keyword: debouncedSearchValue, page: 1, limit: 20 }, + }, + placeholderData: keepPreviousData, + }), + ) const documentsList = data?.data ?? [] const handleInputValueChange = (inputValue: string, details: ComboboxChangeEventDetails) => { @@ -130,7 +140,7 @@ export function DocumentPicker({ datasetId, value, parentMode, onChange }: Props if (!nextOpen) setSearchValue('') } - const handleDocumentChange = (document: SimpleDocumentDetail | null) => { + const handleDocumentChange = (document: PickerDocument | null) => { if (!document) return onChange(document) @@ -138,7 +148,7 @@ export function DocumentPicker({ datasetId, value, parentMode, onChange }: Props } return ( - + items={documentsList} value={value ?? null} inputValue={searchValue} @@ -154,7 +164,7 @@ export function DocumentPicker({ datasetId, value, parentMode, onChange }: Props aria-label={value?.name || t(($) => $['operation.search'], { ns: 'common' })} className="ml-1 flex min-w-0 items-center gap-0.5 rounded-lg px-2 py-1 text-start transition-colors hover:bg-state-base-hover focus-visible:bg-state-base-hover data-popup-open:bg-state-base-hover motion-reduce:transition-none" > - > + > {(document) => } diff --git a/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx b/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx index 23d795c26fad5d..a4a0058fcd3546 100644 --- a/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx +++ b/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx @@ -1,37 +1,24 @@ -import { queryOptions, useQuery } from '@tanstack/react-query' -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { toast } from '@/app/notifications' -import { useAutoDisabledDocuments } from '@/service/knowledge/use-document' -import { createConsoleQueryWrapper } from '@/test/console/query-data' +import { consoleQuery } from '@/service/console' +import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import AutoDisabledDocument from '../auto-disabled-document' -const { mockToastSuccess } = vi.hoisted(() => ({ +const { mockToastSuccess, mockEnableDocuments } = vi.hoisted(() => ({ mockToastSuccess: vi.fn(), + mockEnableDocuments: vi.fn(), })) -type AutoDisabledDocumentsResponse = { document_ids: string[] } - -const createMockQueryResult = ( - data: AutoDisabledDocumentsResponse | undefined, - isLoading: boolean, -) => - ({ - data, - isLoading, - }) as ReturnType - -const mockMutateAsync = vi.fn() -const mockInvalidDisabledDocument = vi.fn() - -vi.mock('@/service/knowledge/use-document', () => ({ - useAutoDisabledDocuments: vi.fn(), - useDocumentEnable: vi.fn(() => ({ - mutateAsync: mockMutateAsync, - })), - useInvalidDisabledDocument: vi.fn(() => mockInvalidDisabledDocument), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.status.byAction.batch.patch': () => ({ + mutateAsync: mockEnableDocuments, + }), + }) +}) vi.mock('@/app/notifications', () => ({ toast: { @@ -39,149 +26,75 @@ vi.mock('@/app/notifications', () => ({ }, })) -const mockUseAutoDisabledDocuments = vi.mocked(useAutoDisabledDocuments) +const autoDisableLogsKey = (datasetId: string) => + consoleQuery.datasets.byDatasetId.autoDisableLogs.get.queryKey({ + input: { params: { dataset_id: datasetId } }, + }) + +const renderWithDisabledDocuments = (datasetId: string, documentIds?: string[]) => { + const queryClient = createConsoleQueryClient() + if (documentIds) { + queryClient.setQueryData(autoDisableLogsKey(datasetId), { + document_ids: documentIds, + count: documentIds.length, + }) + } + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries').mockResolvedValue() + return { + ...renderWithConsoleQuery(, { queryClient }), + invalidateQueries, + } +} describe('AutoDisabledDocument', () => { beforeEach(() => { vi.clearAllMocks() - mockMutateAsync.mockResolvedValue({}) + mockEnableDocuments.mockResolvedValue({ result: 'success' }) }) describe('Rendering', () => { - it('should render nothing when loading', () => { - mockUseAutoDisabledDocuments.mockReturnValue(createMockQueryResult(undefined, true)) - - const { container } = render() - expect(container.firstChild).toBeNull() - }) - - it('should render nothing when no disabled documents', () => { - mockUseAutoDisabledDocuments.mockReturnValue( - createMockQueryResult({ document_ids: [] }, false), - ) - - const { container } = render() + it('should render nothing while the disabled documents are loading', () => { + vi.mocked(globalThis.fetch).mockImplementationOnce(() => new Promise(() => {})) + const { container } = renderWithDisabledDocuments('test-dataset') expect(container.firstChild).toBeNull() }) - it('should render nothing when document_ids is undefined', () => { - mockUseAutoDisabledDocuments.mockReturnValue(createMockQueryResult(undefined, false)) - - const { container } = render() + it('should render nothing when no documents were disabled', () => { + const { container } = renderWithDisabledDocuments('test-dataset', []) expect(container.firstChild).toBeNull() }) - it('should render StatusWithAction when disabled documents exist', () => { - mockUseAutoDisabledDocuments.mockReturnValue( - createMockQueryResult({ document_ids: ['doc1', 'doc2'] }, false), - ) - - render() - expect(screen.getByText(/enable/i)).toBeInTheDocument() - }) - }) - - describe('Props', () => { - it('should pass datasetId to useAutoDisabledDocuments', () => { - mockUseAutoDisabledDocuments.mockReturnValue( - createMockQueryResult({ document_ids: [] }, false), - ) - - render() - expect(mockUseAutoDisabledDocuments).toHaveBeenCalledWith('my-dataset-id') - }) - }) - - it('enables the current document IDs after the initial query completes', async () => { - const user = userEvent.setup() - mockUseAutoDisabledDocuments.mockImplementation(() => - useQuery( - queryOptions({ - queryKey: ['disabled-documents'], - queryFn: () => new Promise(() => {}), - }), - ), - ) - const { wrapper, queryClient } = createConsoleQueryWrapper() - render(, { wrapper }) - expect(screen.queryByRole('button', { name: /enable/i })).not.toBeInTheDocument() - await act(async () => { - queryClient.setQueryData(['disabled-documents'], { document_ids: ['returned-document'] }) - }) - await user.click(await screen.findByRole('button', { name: /enable/i })) - expect(mockMutateAsync).toHaveBeenCalledWith({ - datasetId: 'dataset', - documentIds: ['returned-document'], + it('should offer to enable the disabled documents', async () => { + renderWithDisabledDocuments('test-dataset', ['doc1', 'doc2']) + expect(await screen.findByRole('button', { name: /enable/i })).toBeInTheDocument() }) }) describe('User Interactions', () => { - it('should call enableDocument when action button is clicked', async () => { - mockUseAutoDisabledDocuments.mockReturnValue( - createMockQueryResult({ document_ids: ['doc1', 'doc2'] }, false), - ) - - render() + it('should enable every disabled document of the dataset', async () => { + const user = userEvent.setup() + renderWithDisabledDocuments('test-dataset', ['doc1', 'doc2']) - const actionButton = screen.getByText(/enable/i) - fireEvent.click(actionButton) + await user.click(await screen.findByRole('button', { name: /enable/i })) - await waitFor(() => { - expect(mockMutateAsync).toHaveBeenCalledWith({ - datasetId: 'test-dataset', - documentIds: ['doc1', 'doc2'], - }) + expect(mockEnableDocuments).toHaveBeenCalledWith({ + params: { dataset_id: 'test-dataset', action: 'enable' }, + query: { document_id: ['doc1', 'doc2'] }, }) }) - it('should invalidate cache after enabling documents', async () => { - mockUseAutoDisabledDocuments.mockReturnValue( - createMockQueryResult({ document_ids: ['doc1'] }, false), - ) - - render() + it('should refresh the disabled documents and confirm after enabling', async () => { + const user = userEvent.setup() + const { invalidateQueries } = renderWithDisabledDocuments('test-dataset', ['doc1']) - const actionButton = screen.getByText(/enable/i) - fireEvent.click(actionButton) + await user.click(await screen.findByRole('button', { name: /enable/i })) await waitFor(() => { - expect(mockInvalidDisabledDocument).toHaveBeenCalled() - }) - }) - - it('should show success toast after enabling documents', async () => { - mockUseAutoDisabledDocuments.mockReturnValue( - createMockQueryResult({ document_ids: ['doc1'] }, false), - ) - - render() - - const actionButton = screen.getByText(/enable/i) - fireEvent.click(actionButton) - - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith(expect.any(String)) + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: autoDisableLogsKey('test-dataset'), + }) }) - }) - }) - - describe('Edge Cases', () => { - it('should handle single disabled document', () => { - mockUseAutoDisabledDocuments.mockReturnValue( - createMockQueryResult({ document_ids: ['doc1'] }, false), - ) - - render() - expect(screen.getByText(/enable/i)).toBeInTheDocument() - }) - - it('should handle multiple disabled documents', () => { - mockUseAutoDisabledDocuments.mockReturnValue( - createMockQueryResult({ document_ids: ['doc1', 'doc2', 'doc3', 'doc4', 'doc5'] }, false), - ) - - render() - expect(screen.getByText(/enable/i)).toBeInTheDocument() + expect(toast.success).toHaveBeenCalledWith(expect.any(String)) }) }) }) diff --git a/web/app/components/datasets/common/document-status-with-action/__tests__/index-failed.spec.tsx b/web/app/components/datasets/common/document-status-with-action/__tests__/index-failed.spec.tsx index db25917776311d..5c9a3abb4bf34b 100644 --- a/web/app/components/datasets/common/document-status-with-action/__tests__/index-failed.spec.tsx +++ b/web/app/components/datasets/common/document-status-with-action/__tests__/index-failed.spec.tsx @@ -1,21 +1,24 @@ -import type { ErrorDocsResponse } from '@/models/datasets' +import type { ErrorDocsResponse } from '@dify/contracts/api/console/datasets/types.gen' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { retryErrorDocs } from '@/service/datasets' -import { useDatasetErrorDocs } from '@/service/knowledge/use-dataset' import RetryButton from '../index-failed' const mockRefetch = vi.fn() -vi.mock('@/service/knowledge/use-dataset', () => ({ - useDatasetErrorDocs: vi.fn(), -})) +const { mockErrorDocsQuery } = vi.hoisted(() => ({ mockErrorDocsQuery: vi.fn() })) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.errorDocs.get': mockErrorDocsQuery, + }) +}) vi.mock('@/service/datasets', () => ({ retryErrorDocs: vi.fn(), })) -const mockUseDatasetErrorDocs = vi.mocked(useDatasetErrorDocs) const mockRetryErrorDocs = vi.mocked(retryErrorDocs) afterEach(() => { @@ -24,35 +27,34 @@ afterEach(() => { }) // Helper to create mock query result -const createMockQueryResult = (data: ErrorDocsResponse | undefined, isLoading: boolean) => - ({ - data, - isLoading, - refetch: mockRefetch, - // Required query result properties - error: null, - isError: false, - isFetched: true, - isFetching: false, - isSuccess: !isLoading && !!data, - status: isLoading ? 'pending' : 'success', - dataUpdatedAt: Date.now(), - errorUpdatedAt: 0, - failureCount: 0, - failureReason: null, - errorUpdateCount: 0, - isLoadingError: false, - isPaused: false, - isPlaceholderData: false, - isPending: isLoading, - isRefetchError: false, - isRefetching: false, - isStale: false, - fetchStatus: 'idle', - promise: Promise.resolve(data as ErrorDocsResponse), - isFetchedAfterMount: true, - isInitialLoading: false, - }) as unknown as ReturnType +const createMockQueryResult = (data: ErrorDocsResponse | undefined, isLoading: boolean) => ({ + data, + isLoading, + refetch: mockRefetch, + // Required query result properties + error: null, + isError: false, + isFetched: true, + isFetching: false, + isSuccess: !isLoading && !!data, + status: isLoading ? 'pending' : 'success', + dataUpdatedAt: Date.now(), + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isLoadingError: false, + isPaused: false, + isPlaceholderData: false, + isPending: isLoading, + isRefetchError: false, + isRefetching: false, + isStale: false, + fetchStatus: 'idle', + promise: Promise.resolve(data as ErrorDocsResponse), + isFetchedAfterMount: true, + isInitialLoading: false, +}) describe('RetryButton (IndexFailed)', () => { beforeEach(() => { @@ -62,21 +64,21 @@ describe('RetryButton (IndexFailed)', () => { describe('Rendering', () => { it('should render nothing when loading', () => { - mockUseDatasetErrorDocs.mockReturnValue(createMockQueryResult(undefined, true)) + mockErrorDocsQuery.mockReturnValue(createMockQueryResult(undefined, true)) const { container } = render() expect(container.firstChild).toBeNull() }) it('should render nothing when no error documents', () => { - mockUseDatasetErrorDocs.mockReturnValue(createMockQueryResult({ total: 0, data: [] }, false)) + mockErrorDocsQuery.mockReturnValue(createMockQueryResult({ total: 0, data: [] }, false)) const { container } = render() expect(container.firstChild).toBeNull() }) it('should render StatusWithAction when error documents exist', () => { - mockUseDatasetErrorDocs.mockReturnValue( + mockErrorDocsQuery.mockReturnValue( createMockQueryResult( { total: 3, @@ -91,7 +93,7 @@ describe('RetryButton (IndexFailed)', () => { }) it('should display error count in description', () => { - mockUseDatasetErrorDocs.mockReturnValue( + mockErrorDocsQuery.mockReturnValue( createMockQueryResult( { total: 5, @@ -107,17 +109,22 @@ describe('RetryButton (IndexFailed)', () => { }) describe('Props', () => { - it('should pass datasetId to useDatasetErrorDocs', () => { - mockUseDatasetErrorDocs.mockReturnValue(createMockQueryResult({ total: 0, data: [] }, false)) + it('should load the error documents of the given dataset', () => { + mockErrorDocsQuery.mockReturnValue(createMockQueryResult({ total: 0, data: [] }, false)) render() - expect(mockUseDatasetErrorDocs).toHaveBeenCalledWith('my-dataset-id') + expect(mockErrorDocsQuery).toHaveBeenCalledWith( + expect.objectContaining({ + input: { params: { dataset_id: 'my-dataset-id' } }, + enabled: true, + }), + ) }) }) describe('User Interactions', () => { it('should call retryErrorDocs when retry button is clicked', async () => { - mockUseDatasetErrorDocs.mockReturnValue( + mockErrorDocsQuery.mockReturnValue( createMockQueryResult( { total: 2, @@ -148,7 +155,7 @@ describe('RetryButton (IndexFailed)', () => { }) it('should refetch error docs after successful retry', async () => { - mockUseDatasetErrorDocs.mockReturnValue( + mockErrorDocsQuery.mockReturnValue( createMockQueryResult( { total: 1, @@ -171,7 +178,7 @@ describe('RetryButton (IndexFailed)', () => { }) it('should disable button while retrying', async () => { - mockUseDatasetErrorDocs.mockReturnValue( + mockErrorDocsQuery.mockReturnValue( createMockQueryResult( { total: 1, @@ -210,7 +217,7 @@ describe('RetryButton (IndexFailed)', () => { describe('State Management', () => { it('should transition to error state when retry fails', async () => { - mockUseDatasetErrorDocs.mockReturnValue( + mockErrorDocsQuery.mockReturnValue( createMockQueryResult( { total: 1, @@ -220,7 +227,7 @@ describe('RetryButton (IndexFailed)', () => { ), ) - mockRetryErrorDocs.mockResolvedValue({ result: 'fail' }) + mockRetryErrorDocs.mockRejectedValue(new Error('retry failed')) render() @@ -242,7 +249,7 @@ describe('RetryButton (IndexFailed)', () => { const { rerender } = render() // Initially has errors - mockUseDatasetErrorDocs.mockReturnValue( + mockErrorDocsQuery.mockReturnValue( createMockQueryResult( { total: 1, @@ -256,7 +263,7 @@ describe('RetryButton (IndexFailed)', () => { expect(screen.getByText(/retry/i)).toBeInTheDocument() // Now no errors - mockUseDatasetErrorDocs.mockReturnValue(createMockQueryResult({ total: 0, data: [] }, false)) + mockErrorDocsQuery.mockReturnValue(createMockQueryResult({ total: 0, data: [] }, false)) rerender() @@ -268,7 +275,7 @@ describe('RetryButton (IndexFailed)', () => { describe('Edge Cases', () => { it('should handle empty data array', () => { - mockUseDatasetErrorDocs.mockReturnValue(createMockQueryResult({ total: 0, data: [] }, false)) + mockErrorDocsQuery.mockReturnValue(createMockQueryResult({ total: 0, data: [] }, false)) const { container } = render() expect(container.firstChild).toBeNull() @@ -277,7 +284,7 @@ describe('RetryButton (IndexFailed)', () => { it('should handle undefined data by showing error state', () => { // When data is undefined but not loading, the component shows error state // because errorDocs?.total is not strictly equal to 0 - mockUseDatasetErrorDocs.mockReturnValue(createMockQueryResult(undefined, false)) + mockErrorDocsQuery.mockReturnValue(createMockQueryResult(undefined, false)) render() // Component renders with undefined count @@ -285,7 +292,7 @@ describe('RetryButton (IndexFailed)', () => { }) it('should handle retry with empty document list', async () => { - mockUseDatasetErrorDocs.mockReturnValue(createMockQueryResult({ total: 1, data: [] }, false)) + mockErrorDocsQuery.mockReturnValue(createMockQueryResult({ total: 1, data: [] }, false)) mockRetryErrorDocs.mockResolvedValue({ result: 'success' }) diff --git a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx index cba6d20fd1217d..e5574167473662 100644 --- a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx +++ b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx @@ -1,13 +1,10 @@ 'use client' import type { FC } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' -import { - useAutoDisabledDocuments, - useDocumentEnable, - useInvalidDisabledDocument, -} from '@/service/knowledge/use-document' +import { consoleQuery } from '@/service/console' import StatusWithAction from './status-with-action' type Props = Readonly<{ @@ -16,15 +13,23 @@ type Props = Readonly<{ const AutoDisabledDocument: FC = ({ datasetId }) => { const { t } = useTranslation(['common', 'dataset']) - const { data, isLoading } = useAutoDisabledDocuments(datasetId) - const invalidDisabledDocument = useInvalidDisabledDocument() + const queryClient = useQueryClient() + const autoDisableLogsQuery = consoleQuery.datasets.byDatasetId.autoDisableLogs.get.queryOptions({ + input: { params: { dataset_id: datasetId } }, + }) + const { data, isLoading } = useQuery(autoDisableLogsQuery) const documentIds = data?.document_ids - const { mutateAsync: enableDocument } = useDocumentEnable() + const { mutateAsync: changeDocumentStatus } = useMutation( + consoleQuery.datasets.byDatasetId.documents.status.byAction.batch.patch.mutationOptions(), + ) if (!documentIds?.length || isLoading) return null const handleEnableDocuments = async () => { - await enableDocument({ datasetId, documentIds }) - invalidDisabledDocument() + await changeDocumentStatus({ + params: { dataset_id: datasetId, action: 'enable' }, + query: { document_id: documentIds }, + }) + queryClient.invalidateQueries({ queryKey: autoDisableLogsQuery.queryKey }) toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) } diff --git a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx index 4102874d96001b..dc88e92834e459 100644 --- a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx +++ b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx @@ -1,12 +1,12 @@ 'use client' import type { FC } from 'react' -import type { IndexingStatusResponse } from '@/models/datasets' +import { useQuery } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import * as React from 'react' import { useEffect, useReducer } from 'react' import { useTranslation } from 'react-i18next' +import { consoleQuery } from '@/service/console' import { retryErrorDocs } from '@/service/datasets' -import { useDatasetErrorDocs } from '@/service/knowledge/use-dataset' import StatusWithAction from './status-with-action' type Props = Readonly<{ @@ -36,16 +36,25 @@ const indexStateReducer = (state: IIndexState, action: IAction) => { const RetryButton: FC = ({ datasetId }) => { const { t } = useTranslation(['dataset']) const [indexState, dispatch] = useReducer(indexStateReducer, { value: 'success' }) - const { data: errorDocs, isLoading, refetch: refetchErrorDocs } = useDatasetErrorDocs(datasetId) + const { + data: errorDocs, + isLoading, + refetch: refetchErrorDocs, + } = useQuery( + consoleQuery.datasets.byDatasetId.errorDocs.get.queryOptions({ + input: { params: { dataset_id: datasetId } }, + enabled: !!datasetId, + }), + ) const onRetryErrorDocs = async () => { dispatch({ type: 'retry' }) - const document_ids = errorDocs?.data.map((doc: IndexingStatusResponse) => doc.id) || [] - const res = await retryErrorDocs({ datasetId, document_ids }) - if (res.result === 'success') { + const document_ids = errorDocs?.data.map((doc) => doc.id) || [] + try { + await retryErrorDocs({ datasetId, document_ids }) refetchErrorDocs() dispatch({ type: 'success' }) - } else { + } catch { dispatch({ type: 'error' }) } } diff --git a/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx b/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx index 2abf79ace6ba84..f25b0a7d9e164c 100644 --- a/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx +++ b/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx @@ -7,20 +7,23 @@ import { toast } from '@/app/notifications' import { FileContextProvider } from '../../store' import { useUpload } from '../use-upload' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': vi.fn(() => ({ + data: { + image_file_batch_limit: 10, + single_chunk_attachment_limit: 20, + attachment_image_file_size_limit: 15, + }, + })), + }) +}) + const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), })) -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: vi.fn(() => ({ - data: { - image_file_batch_limit: 10, - single_chunk_attachment_limit: 20, - attachment_image_file_size_limit: 15, - }, - })), -})) - vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError, diff --git a/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts b/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts index 99986b8b93f8bd..65b17dabc2e4c6 100644 --- a/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts +++ b/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts @@ -1,11 +1,12 @@ import type { FileEntity, FileUploadConfig } from '../types' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { v4 as uuid4 } from 'uuid' import { fileUpload, getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils' import { toast } from '@/app/notifications' -import { useFileUploadConfig } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { ACCEPT_TYPES } from '../constants' import { useFileStore } from '../store' import { getFileType, getFileUploadConfig, traverseFileEntry } from '../utils' @@ -19,7 +20,7 @@ export const useUpload = () => { const dragRef = useRef(null) const dropRef = useRef(null) - const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: fileUploadConfigResponse } = useQuery(consoleQuery.files.upload.get.queryOptions()) const fileUploadConfig: FileUploadConfig = useMemo(() => { return getFileUploadConfig(fileUploadConfigResponse) diff --git a/web/app/components/datasets/common/image-uploader/image-uploader-in-chunk/__tests__/index.spec.tsx b/web/app/components/datasets/common/image-uploader/image-uploader-in-chunk/__tests__/index.spec.tsx index 96619e874ef37f..29e7c2dbfdacd8 100644 --- a/web/app/components/datasets/common/image-uploader/image-uploader-in-chunk/__tests__/index.spec.tsx +++ b/web/app/components/datasets/common/image-uploader/image-uploader-in-chunk/__tests__/index.spec.tsx @@ -3,15 +3,18 @@ import { fireEvent, render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vite-plus/test' import ImageUploaderInChunkWrapper from '../index' -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: vi.fn(() => ({ - data: { - image_file_batch_limit: 10, - single_chunk_attachment_limit: 20, - attachment_image_file_size_limit: 15, - }, - })), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': vi.fn(() => ({ + data: { + image_file_batch_limit: 10, + single_chunk_attachment_limit: 20, + attachment_image_file_size_limit: 15, + }, + })), + }) +}) vi.mock('@/app/components/datasets/common/image-previewer', () => ({ default: ({ onClose }: { onClose: () => void }) => ( diff --git a/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/__tests__/index.spec.tsx b/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/__tests__/index.spec.tsx index 0daf4e9086e3e9..6f8b4b7633bbf1 100644 --- a/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/__tests__/index.spec.tsx +++ b/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/__tests__/index.spec.tsx @@ -3,15 +3,18 @@ import { fireEvent, render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vite-plus/test' import ImageUploaderInRetrievalTestingWrapper from '../index' -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: vi.fn(() => ({ - data: { - image_file_batch_limit: 10, - single_chunk_attachment_limit: 20, - attachment_image_file_size_limit: 15, - }, - })), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': vi.fn(() => ({ + data: { + image_file_batch_limit: 10, + single_chunk_attachment_limit: 20, + attachment_image_file_size_limit: 15, + }, + })), + }) +}) vi.mock('@/app/components/datasets/common/image-previewer', () => ({ default: ({ onClose }: { onClose: () => void }) => ( diff --git a/web/app/components/datasets/common/retrieval-method-info/index.tsx b/web/app/components/datasets/common/retrieval-method-info/index.tsx index 0b342ca52913ac..9be100b20baaa7 100644 --- a/web/app/components/datasets/common/retrieval-method-info/index.tsx +++ b/web/app/components/datasets/common/retrieval-method-info/index.tsx @@ -8,7 +8,6 @@ export const getIcon = (type: RETRIEVE_METHOD) => { [RETRIEVE_METHOD.semantic]: retrievalIcon.vector, [RETRIEVE_METHOD.fullText]: retrievalIcon.fullText, [RETRIEVE_METHOD.hybrid]: retrievalIcon.hybrid, - [RETRIEVE_METHOD.invertedIndex]: retrievalIcon.vector, [RETRIEVE_METHOD.keywordSearch]: retrievalIcon.vector, }[type] || retrievalIcon.vector ) diff --git a/web/app/components/datasets/create-from-pipeline/__tests__/footer.spec.tsx b/web/app/components/datasets/create-from-pipeline/__tests__/footer.spec.tsx index 66422db74a9f74..a2d8157cf48cb9 100644 --- a/web/app/components/datasets/create-from-pipeline/__tests__/footer.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/__tests__/footer.spec.tsx @@ -1,7 +1,13 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { QueryClient } from '@tanstack/react-query' +import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQuery } from '@/service/console' +import { renderWithConsoleQuery } from '@/test/console/query-data' import Footer from '../footer' +const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries') +const datasetListKey = consoleQuery.datasets.get.key() + // Configurable mock for search params let mockSearchParams = new URLSearchParams() const mockReplace = vi.fn() @@ -11,11 +17,6 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: () => mockSearchParams, })) -const mockInvalidDatasetList = vi.fn() -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => mockInvalidDatasetList, -})) - // Mock CreateFromDSLModal to capture props let capturedActiveTab: string | undefined let capturedDslUrl: string | undefined @@ -65,19 +66,19 @@ describe('Footer', () => { describe('Rendering', () => { it('should render import button with icon', () => { - const { container } = render(
) + const { container } = renderWithConsoleQuery(
) const button = screen.getByRole('button') expect(button).toBeInTheDocument() expect(container.querySelector('svg')).toBeInTheDocument() }) it('should not show modal initially', () => { - render(
) + renderWithConsoleQuery(
) expect(screen.queryByTestId('dsl-modal')).not.toBeInTheDocument() }) it('should render divider', () => { - const { container } = render(
) + const { container } = renderWithConsoleQuery(
) const divider = container.querySelector('[class*="w-8"]') expect(divider).toBeInTheDocument() }) @@ -85,7 +86,7 @@ describe('Footer', () => { describe('User Interactions', () => { it('should open modal when import button is clicked', () => { - render(
) + renderWithConsoleQuery(
) const importButton = screen.getByText(/importDSL/i) fireEvent.click(importButton) @@ -94,7 +95,7 @@ describe('Footer', () => { }) it('should close modal when onClose is called', () => { - render(
) + renderWithConsoleQuery(
) const importButton = screen.getByText(/importDSL/i) fireEvent.click(importButton) @@ -106,7 +107,7 @@ describe('Footer', () => { }) it('should call invalidDatasetList on success', () => { - render(
) + renderWithConsoleQuery(
) const importButton = screen.getByText(/importDSL/i) fireEvent.click(importButton) @@ -115,13 +116,13 @@ describe('Footer', () => { const successButton = screen.getByTestId('success-modal') fireEvent.click(successButton) - expect(mockInvalidDatasetList).toHaveBeenCalled() + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: datasetListKey }) }) }) describe('Layout', () => { it('should have backdrop blur-sm effect', () => { - const { container } = render(
) + const { container } = renderWithConsoleQuery(
) const footerDiv = container.firstChild as HTMLElement expect(footerDiv).toHaveClass('backdrop-blur-[6px]') }) @@ -132,7 +133,7 @@ describe('Footer', () => { it('should set activeTab to FROM_URL when dslUrl is present', () => { mockSearchParams = new URLSearchParams('remoteInstallUrl=https://example.com/dsl') - render(
) + renderWithConsoleQuery(
) // Open modal to trigger prop capture const importButton = screen.getByText(/importDSL/i) @@ -145,7 +146,7 @@ describe('Footer', () => { it('should set activeTab to undefined when dslUrl is not present', () => { mockSearchParams = new URLSearchParams() - render(
) + renderWithConsoleQuery(
) // Open modal to trigger prop capture const importButton = screen.getByText(/importDSL/i) @@ -158,7 +159,7 @@ describe('Footer', () => { it('should call replace when closing modal with dslUrl present', () => { mockSearchParams = new URLSearchParams('remoteInstallUrl=https://example.com/dsl') - render(
) + renderWithConsoleQuery(
) const importButton = screen.getByText(/importDSL/i) fireEvent.click(importButton) @@ -173,7 +174,7 @@ describe('Footer', () => { it('should not call replace when closing modal without dslUrl', () => { mockSearchParams = new URLSearchParams() - render(
) + renderWithConsoleQuery(
) const importButton = screen.getByText(/importDSL/i) fireEvent.click(importButton) diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx index 83574c22c920b8..03f13662611be9 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx @@ -15,14 +15,17 @@ vi.mock('@/next/navigation', () => ({ const mockImportDSL = vi.fn() const mockImportDSLConfirm = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - useImportPipelineDSL: () => ({ - mutateAsync: mockImportDSL, - }), - useImportPipelineDSLConfirm: () => ({ - mutateAsync: mockImportDSLConfirm, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.imports.post': () => ({ + mutateAsync: mockImportDSL, + }), + 'rag.pipelines.imports.byImportId.confirm.post': () => ({ + mutateAsync: mockImportDSLConfirm, + }), + }) +}) // Mock plugin dependencies hook const mockHandleCheckPluginDependencies = vi.fn() @@ -311,8 +314,10 @@ describe('CreateFromDSLModal', () => { await waitFor(() => { expect(mockImportDSL).toHaveBeenCalledWith({ - mode: 'yaml-url', - yaml_url: 'https://example.com/test.pipeline', + body: { + mode: 'yaml-url', + yaml_url: 'https://example.com/test.pipeline', + }, }) }) }) @@ -538,8 +543,10 @@ describe('CreateFromDSLModal', () => { await waitFor(() => { expect(mockImportDSL).toHaveBeenCalledWith({ - mode: 'yaml-url', - yaml_url: 'https://example.com/test.pipeline', + body: { + mode: 'yaml-url', + yaml_url: 'https://example.com/test.pipeline', + }, }) }) }) @@ -865,8 +872,10 @@ describe('CreateFromDSLModal', () => { // Verify API was called with file content await waitFor(() => { expect(mockImportDSL).toHaveBeenCalledWith({ - mode: 'yaml-content', - yaml_content: fileContent, + body: { + mode: 'yaml-content', + yaml_content: fileContent, + }, }) }) }) @@ -967,7 +976,7 @@ describe('CreateFromDSLModal', () => { // Verify confirm was called await waitFor(() => { - expect(mockImportDSLConfirm).toHaveBeenCalledWith('import-123') + expect(mockImportDSLConfirm).toHaveBeenCalledWith({ params: { import_id: 'import-123' } }) }) // Verify success handling diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/__tests__/use-dsl-import.spec.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/__tests__/use-dsl-import.spec.tsx index 7c56bb112fc343..828ad7ec931331 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/__tests__/use-dsl-import.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/__tests__/use-dsl-import.spec.tsx @@ -15,14 +15,17 @@ vi.mock('@/next/navigation', () => ({ const mockImportDSL = vi.fn() const mockImportDSLConfirm = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - useImportPipelineDSL: () => ({ - mutateAsync: mockImportDSL, - }), - useImportPipelineDSLConfirm: () => ({ - mutateAsync: mockImportDSLConfirm, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.imports.post': () => ({ + mutateAsync: mockImportDSL, + }), + 'rag.pipelines.imports.byImportId.confirm.post': () => ({ + mutateAsync: mockImportDSLConfirm, + }), + }) +}) // Mock plugin dependencies hook const mockHandleCheckPluginDependencies = vi.fn() @@ -277,8 +280,10 @@ describe('useDSLImport', () => { await waitFor(() => { expect(mockImportDSL).toHaveBeenCalledWith({ - mode: 'yaml-url', - yaml_url: 'https://example.com/test.pipeline', + body: { + mode: 'yaml-url', + yaml_url: 'https://example.com/test.pipeline', + }, }) }) @@ -625,8 +630,10 @@ describe('useDSLImport', () => { await waitFor(() => { expect(mockImportDSL).toHaveBeenCalledWith({ - mode: 'yaml-content', - yaml_content: fileContent, + body: { + mode: 'yaml-content', + yaml_content: fileContent, + }, }) }) @@ -706,7 +713,7 @@ describe('useDSLImport', () => { }) await waitFor(() => { - expect(mockImportDSLConfirm).toHaveBeenCalledWith('import-123') + expect(mockImportDSLConfirm).toHaveBeenCalledWith({ params: { import_id: 'import-123' } }) expect(onSuccess).toHaveBeenCalled() expect(result.current.showConfirmModal).toBe(false) }) diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts index 7afbf30863042b..619b5951cb22e3 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts @@ -1,4 +1,5 @@ 'use client' +import { useMutation } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -6,7 +7,7 @@ import { usePluginDependencies } from '@/app/components/workflow/plugin-dependen import { toast } from '@/app/notifications' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { useRouter } from '@/next/navigation' -import { useImportPipelineDSL, useImportPipelineDSLConfirm } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import { getErrorMessage } from '@/utils/error-message' export const CreateFromDSLModalTab = { @@ -44,8 +45,12 @@ export const useDSLImport = ({ const [isConfirming, setIsConfirming] = useState(false) const { handleCheckPluginDependencies } = usePluginDependencies() const isCreatingRef = useRef(false) - const { mutateAsync: importDSL } = useImportPipelineDSL() - const { mutateAsync: importDSLConfirm } = useImportPipelineDSLConfirm() + const { mutateAsync: importDSL } = useMutation( + consoleQuery.rag.pipelines.imports.post.mutationOptions(), + ) + const { mutateAsync: importDSLConfirm } = useMutation( + consoleQuery.rag.pipelines.imports.byImportId.confirm.post.mutationOptions(), + ) const notifyError = useCallback( (message?: string) => { toast.error(message || t(($) => $['creation.errorTip'], { ns: 'datasetPipeline' })) @@ -77,14 +82,12 @@ export const useDSLImport = ({ let response if (currentTab === CreateFromDSLModalTab.FROM_FILE) { response = await importDSL({ - mode: DSLImportMode.YAML_CONTENT, - yaml_content: fileContent || '', + body: { mode: DSLImportMode.YAML_CONTENT, yaml_content: fileContent || '' }, }) } if (currentTab === CreateFromDSLModalTab.FROM_URL) { response = await importDSL({ - mode: DSLImportMode.YAML_URL, - yaml_url: dslUrlValue || '', + body: { mode: DSLImportMode.YAML_URL, yaml_url: dslUrlValue || '' }, }) } if (!response) { @@ -150,7 +153,7 @@ export const useDSLImport = ({ if (!importId) return setIsConfirming(true) try { - const response = await importDSLConfirm(importId) + const response = await importDSLConfirm({ params: { import_id: importId } }) if (!response) { notifyError() return diff --git a/web/app/components/datasets/create-from-pipeline/footer.tsx b/web/app/components/datasets/create-from-pipeline/footer.tsx index 60a56059f90462..753d708271551f 100644 --- a/web/app/components/datasets/create-from-pipeline/footer.tsx +++ b/web/app/components/datasets/create-from-pipeline/footer.tsx @@ -1,10 +1,11 @@ import { Separator } from '@langgenius/dify-ui/separator' import { RiFileUploadLine } from '@remixicon/react' +import { useQueryClient } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRouter, useSearchParams } from '@/next/navigation' -import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-options/create-from-dsl-modal' const Footer = () => { @@ -15,7 +16,10 @@ const Footer = () => { const searchParams = useSearchParams() const { replace } = useRouter() const dslUrl = searchParams.get('remoteInstallUrl') || undefined - const invalidDatasetList = useInvalidDatasetList() + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) const activeTab = useMemo(() => { if (dslUrl) return CreateFromDSLModalTab.FROM_URL diff --git a/web/app/components/datasets/create-from-pipeline/list/__tests__/built-in-pipeline-list.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/__tests__/built-in-pipeline-list.spec.tsx index 442088e11df1fa..5e47e45fee23ee 100644 --- a/web/app/components/datasets/create-from-pipeline/list/__tests__/built-in-pipeline-list.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/__tests__/built-in-pipeline-list.spec.tsx @@ -38,9 +38,15 @@ vi.mock('#i18n', async (importOriginal) => ({ })) const mockUsePipelineTemplateList = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - usePipelineTemplateList: (...args: unknown[]) => mockUsePipelineTemplateList(...args), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipeline.templates.get': (o: { + input: { query: { type: string; language?: string } } + enabled?: boolean + }) => mockUsePipelineTemplateList(o.input.query, o.enabled), + }) +}) // BuiltInPipelineList Component Tests diff --git a/web/app/components/datasets/create-from-pipeline/list/__tests__/create-card.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/__tests__/create-card.spec.tsx index e773173da73b10..2f18005ce3e0c4 100644 --- a/web/app/components/datasets/create-from-pipeline/list/__tests__/create-card.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/__tests__/create-card.spec.tsx @@ -1,5 +1,6 @@ -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderWithConsoleQuery } from '@/test/console/query-data' import CreateCard from '../create-card' const mockPush = vi.fn() @@ -30,17 +31,13 @@ vi.mock('@/app/notifications', async (importOriginal) => { }) const mockCreateEmptyDataset = vi.fn() -const mockInvalidDatasetList = vi.fn() -vi.mock('@/service/knowledge/use-create-dataset', () => ({ - useCreatePipelineDataset: () => ({ - mutateAsync: mockCreateEmptyDataset, - }), -})) - -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => mockInvalidDatasetList, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipeline.emptyDataset.post': () => ({ mutateAsync: mockCreateEmptyDataset }), + }) +}) // CreateCard Component Tests @@ -53,13 +50,13 @@ describe('CreateCard', () => { describe('Rendering', () => { it('should render title and description', () => { - render() + renderWithConsoleQuery() expect(screen.getByText(/createFromScratch\.title/i)).toBeInTheDocument() expect(screen.getByText(/createFromScratch\.description/i)).toBeInTheDocument() }) it('should render add icon', () => { - const { container } = render() + const { container } = renderWithConsoleQuery() const icon = container.querySelector('svg') expect(icon).toBeInTheDocument() }) @@ -67,13 +64,13 @@ describe('CreateCard', () => { describe('Layout', () => { it('should have fixed height', () => { - const { container } = render() + const { container } = renderWithConsoleQuery() const card = container.firstChild as HTMLElement expect(card).toHaveClass('h-33') }) it('should have shadow and border', () => { - const { container } = render() + const { container } = renderWithConsoleQuery() const card = container.firstChild as HTMLElement expect(card).toHaveClass('border-[0.5px]', 'shadow-xs') }) diff --git a/web/app/components/datasets/create-from-pipeline/list/__tests__/customized-list.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/__tests__/customized-list.spec.tsx index 620b690a94c622..43f987d00749e9 100644 --- a/web/app/components/datasets/create-from-pipeline/list/__tests__/customized-list.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/__tests__/customized-list.spec.tsx @@ -13,9 +13,15 @@ vi.mock('../template-card', () => ({ // Mock usePipelineTemplateList hook const mockUsePipelineTemplateList = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - usePipelineTemplateList: (...args: unknown[]) => mockUsePipelineTemplateList(...args), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipeline.templates.get': (o: { + input: { query: { type: string; language?: string } } + enabled?: boolean + }) => mockUsePipelineTemplateList(o.input.query), + }) +}) // CustomizedList Component Tests diff --git a/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx b/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx index 1badbede64aa7b..f1cc9614ccb764 100644 --- a/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx @@ -1,9 +1,10 @@ -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useLocale } from '#i18n' +import { selectPipelineTemplateList } from '@/app/components/rag-pipeline/utils/query-select' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { LanguagesSupported } from '@/i18n/language' -import { usePipelineTemplateList } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import CreateCard from './create-card' import TemplateCard from './template-card' @@ -17,9 +18,12 @@ const BuiltInPipelineList = () => { ...systemFeaturesQueryOptions(), select: (s) => s.enable_marketplace, }) - const { data: pipelineList, isLoading } = usePipelineTemplateList( - { type: 'built-in', language }, - enableMarketplace, + const { data: pipelineList, isLoading } = useQuery( + consoleQuery.rag.pipeline.templates.get.queryOptions({ + input: { query: { type: 'built-in', language } }, + enabled: enableMarketplace, + select: selectPipelineTemplateList, + }), ) const list = pipelineList?.pipeline_templates || [] diff --git a/web/app/components/datasets/create-from-pipeline/list/create-card.tsx b/web/app/components/datasets/create-from-pipeline/list/create-card.tsx index 3cb501b67cb9d6..425a22b962ccc5 100644 --- a/web/app/components/datasets/create-from-pipeline/list/create-card.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/create-card.tsx @@ -1,19 +1,24 @@ import { RiAddCircleLine } from '@remixicon/react' +import { useMutation, useQueryClient } from '@tanstack/react-query' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' -import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset' -import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' const CreateCard = () => { const { t } = useTranslation(['datasetPipeline']) const { push } = useRouter() - const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset() - const invalidDatasetList = useInvalidDatasetList() + const { mutateAsync: createEmptyDataset } = useMutation( + consoleQuery.rag.pipeline.emptyDataset.post.mutationOptions(), + ) + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) const handleCreate = useCallback(async () => { await createEmptyDataset(undefined, { diff --git a/web/app/components/datasets/create-from-pipeline/list/customized-list.tsx b/web/app/components/datasets/create-from-pipeline/list/customized-list.tsx index cbbe5e6de2e8a8..b5771754fada45 100644 --- a/web/app/components/datasets/create-from-pipeline/list/customized-list.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/customized-list.tsx @@ -1,10 +1,17 @@ +import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' -import { usePipelineTemplateList } from '@/service/use-pipeline' +import { selectPipelineTemplateList } from '@/app/components/rag-pipeline/utils/query-select' +import { consoleQuery } from '@/service/console' import TemplateCard from './template-card' const CustomizedList = () => { const { t } = useTranslation(['datasetPipeline']) - const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'customized' }) + const { data: pipelineList, isLoading } = useQuery( + consoleQuery.rag.pipeline.templates.get.queryOptions({ + input: { query: { type: 'customized' } }, + select: selectPipelineTemplateList, + }), + ) const list = pipelineList?.pipeline_templates || [] if (isLoading || list.length === 0) return null diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx index af7a31ac8b5115..019af27353a7a2 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx @@ -1,6 +1,7 @@ import type { ReactElement } from 'react' import type { PipelineTemplate } from '@/models/pipeline' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' +import { QueryClient } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' @@ -16,14 +17,15 @@ const render = (ui: ReactElement) => ) const mockUpdatePipeline = vi.fn() -const mockInvalidCustomizedTemplateList = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - useUpdateTemplateInfo: () => ({ - mutateAsync: mockUpdatePipeline, - }), - useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipeline.customized.templates.byTemplateId.patch': () => ({ + mutateAsync: mockUpdatePipeline, + }), + }) +}) const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn(), @@ -82,6 +84,7 @@ describe('EditPipelineInfo', () => { }) beforeEach(() => { + vi.spyOn(QueryClient.prototype, 'invalidateQueries') vi.clearAllMocks() mockToastError.mockReset() }) @@ -192,7 +195,9 @@ describe('EditPipelineInfo', () => { fireEvent.click(saveButton) await waitFor(() => { - expect(mockInvalidCustomizedTemplateList).toHaveBeenCalled() + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: expect.arrayContaining([['console', 'rag', 'pipeline', 'templates', 'get']]), + }) }) }) @@ -304,9 +309,11 @@ describe('EditPipelineInfo', () => { await waitFor(() => { expect(mockUpdatePipeline).toHaveBeenCalledWith( expect.objectContaining({ - icon_info: expect.objectContaining({ - icon_type: 'image', - icon: 'file-id-123', + body: expect.objectContaining({ + icon_info: expect.objectContaining({ + icon_type: 'image', + icon: 'file-id-123', + }), }), }), expect.any(Object), @@ -328,9 +335,11 @@ describe('EditPipelineInfo', () => { await waitFor(() => { expect(mockUpdatePipeline).toHaveBeenCalledWith( expect.objectContaining({ - icon_info: expect.objectContaining({ - icon_type: 'emoji', - icon: '📊', + body: expect.objectContaining({ + icon_info: expect.objectContaining({ + icon_type: 'emoji', + icon: '📊', + }), }), }), expect.any(Object), @@ -384,10 +393,12 @@ describe('EditPipelineInfo', () => { await waitFor(() => { expect(mockUpdatePipeline).toHaveBeenCalledWith( expect.objectContaining({ - icon_info: expect.objectContaining({ - icon_type: 'emoji', - icon: expect.any(String), - icon_background: '#F3FEE7', + body: expect.objectContaining({ + icon_info: expect.objectContaining({ + icon_type: 'emoji', + icon: expect.any(String), + icon_background: '#F3FEE7', + }), }), }), expect.any(Object), @@ -478,10 +489,12 @@ describe('EditPipelineInfo', () => { await waitFor(() => { expect(mockUpdatePipeline).toHaveBeenCalledWith( expect.objectContaining({ - icon_info: expect.objectContaining({ - icon_type: 'emoji', - icon: '📊', - icon_background: '#F3FEE7', + body: expect.objectContaining({ + icon_info: expect.objectContaining({ + icon_type: 'emoji', + icon: '📊', + icon_background: '#F3FEE7', + }), }), }), expect.any(Object), @@ -511,10 +524,12 @@ describe('EditPipelineInfo', () => { await waitFor(() => { expect(mockUpdatePipeline).toHaveBeenCalledWith( expect.objectContaining({ - icon_info: expect.objectContaining({ - icon_type: 'emoji', - icon: expect.any(String), - icon_background: '#F3FEE7', + body: expect.objectContaining({ + icon_info: expect.objectContaining({ + icon_type: 'emoji', + icon: expect.any(String), + icon_background: '#F3FEE7', + }), }), }), expect.any(Object), @@ -539,11 +554,13 @@ describe('EditPipelineInfo', () => { await waitFor(() => { expect(mockUpdatePipeline).toHaveBeenCalledWith( expect.objectContaining({ - template_id: 'pipeline-1', - name: 'Test Pipeline', - description: 'Test pipeline description', - icon_info: expect.objectContaining({ - icon_type: 'emoji', + params: { template_id: 'pipeline-1' }, + body: expect.objectContaining({ + name: 'Test Pipeline', + description: 'Test pipeline description', + icon_info: expect.objectContaining({ + icon_type: 'emoji', + }), }), }), expect.any(Object), @@ -569,9 +586,11 @@ describe('EditPipelineInfo', () => { await waitFor(() => { expect(mockUpdatePipeline).toHaveBeenCalledWith( expect.objectContaining({ - template_id: 'pipeline-2', - icon_info: expect.objectContaining({ - icon_type: 'image', + params: { template_id: 'pipeline-2' }, + body: expect.objectContaining({ + icon_info: expect.objectContaining({ + icon_type: 'image', + }), }), }), expect.any(Object), diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/focus.browser.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/focus.browser.spec.tsx index e82a8b59b6fbee..af9a947e01ca5b 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/focus.browser.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/focus.browser.spec.tsx @@ -22,16 +22,20 @@ vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn() })) vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({ usePluginDependencies: () => ({ handleCheckPluginDependencies: vi.fn() }), })) -vi.mock('@/service/knowledge/use-create-dataset', () => ({ - useCreatePipelineDatasetFromCustomized: () => ({ mutateAsync: vi.fn() }), -})) -vi.mock('@/service/knowledge/use-dataset', () => ({ useInvalidDatasetList: () => vi.fn() })) -vi.mock('@/service/use-pipeline', () => ({ - usePipelineTemplateById: () => useQuery(templateQueryOptions), - useDeleteTemplate: () => ({ mutateAsync: vi.fn() }), - useExportTemplateDSL: () => ({ mutateAsync: vi.fn(), isPending: false }), - useInvalidCustomizedTemplateList: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipeline.templates.byTemplateId.get': function useTemplateDetailQuery() { + return useQuery(templateQueryOptions) + }, + 'rag.pipeline.dataset.post': () => ({ mutateAsync: vi.fn() }), + 'rag.pipeline.customized.templates.byTemplateId.delete': () => ({ mutateAsync: vi.fn() }), + 'rag.pipeline.customized.templates.byTemplateId.post': () => ({ + mutateAsync: vi.fn(), + isPending: false, + }), + }) +}) vi.mock('@/app/components/workflow/workflow-preview', () => ({ default: () =>
Workflow preview
, diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx index 7a15e480d013e4..55ea2e0882da0a 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx @@ -1,10 +1,16 @@ import type { PipelineTemplate } from '@/models/pipeline' import { DialogTrigger } from '@langgenius/dify-ui/dialog' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { QueryClient } from '@tanstack/react-query' +import { fireEvent, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { ChunkingMode } from '@/models/datasets' +import { consoleQuery } from '@/service/console' +import { renderWithConsoleQuery as render } from '@/test/console/query-data' import TemplateCard from '../index' +const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries') +const datasetListKey = consoleQuery.datasets.get.key() + const mockPush = vi.fn() vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: mockPush }), @@ -100,48 +106,39 @@ vi.mock('@/app/components/workflow/workflow-preview', () => ({ })) const mockCreateDataset = vi.fn() -const mockInvalidDatasetList = vi.fn() const mockGetPipelineTemplateInfo = vi.fn() const mockDeletePipeline = vi.fn() const mockExportPipelineDSL = vi.fn() -const mockInvalidCustomizedTemplateList = vi.fn() const mockHandleCheckPluginDependencies = vi.fn() // Configurable isPending for export let mockIsExporting = false -vi.mock('@/service/knowledge/use-create-dataset', () => ({ - useCreatePipelineDatasetFromCustomized: () => ({ - mutateAsync: mockCreateDataset, - }), -})) - -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => mockInvalidDatasetList, -})) - -vi.mock('@/service/use-pipeline', () => ({ - usePipelineTemplateById: () => ({ - data: { - name: 'Test Pipeline', - description: 'Test pipeline description', - icon_info: { icon_type: 'emoji', icon: '📊', icon_background: '#FFF4ED', icon_url: '' }, - chunk_structure: 'text', - graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } }, - }, - refetch: mockGetPipelineTemplateInfo, - }), - useDeleteTemplate: () => ({ - mutateAsync: mockDeletePipeline, - }), - useExportTemplateDSL: () => ({ - mutateAsync: mockExportPipelineDSL, - get isPending() { - return mockIsExporting - }, - }), - useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipeline.dataset.post': () => ({ mutateAsync: mockCreateDataset }), + 'rag.pipeline.templates.byTemplateId.get': () => ({ + data: { + name: 'Test Pipeline', + description: 'Test pipeline description', + icon_info: { icon_type: 'emoji', icon: '📊', icon_background: '#FFF4ED', icon_url: '' }, + chunk_structure: 'text', + graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } }, + }, + refetch: mockGetPipelineTemplateInfo, + }), + 'rag.pipeline.customized.templates.byTemplateId.delete': () => ({ + mutateAsync: mockDeletePipeline, + }), + 'rag.pipeline.customized.templates.byTemplateId.post': () => ({ + mutateAsync: mockExportPipelineDSL, + get isPending() { + return mockIsExporting + }, + }), + }) +}) // Mock plugin dependencies hook vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({ @@ -180,6 +177,7 @@ describe('TemplateCard', () => { screen.getByRole('button', { name: 'common.operation.cancel' }) beforeEach(() => { + vi.spyOn(QueryClient.prototype, 'invalidateQueries') vi.clearAllMocks() mockToastSuccess.mockReset() mockToastError.mockReset() @@ -275,7 +273,7 @@ describe('TemplateCard', () => { fireEvent.click(chooseButton) await waitFor(() => { - expect(mockInvalidDatasetList).toHaveBeenCalled() + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: datasetListKey }) }) }) @@ -453,7 +451,10 @@ describe('TemplateCard', () => { fireEvent.click(exportButton) await waitFor(() => { - expect(mockExportPipelineDSL).toHaveBeenCalledWith('pipeline-1', expect.any(Object)) + expect(mockExportPipelineDSL).toHaveBeenCalledWith( + { params: { template_id: 'pipeline-1' } }, + expect.any(Object), + ) }) }) @@ -553,7 +554,10 @@ describe('TemplateCard', () => { fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { - expect(mockDeletePipeline).toHaveBeenCalledWith('pipeline-1', expect.any(Object)) + expect(mockDeletePipeline).toHaveBeenCalledWith( + { params: { template_id: 'pipeline-1' } }, + expect.any(Object), + ) }) }) @@ -574,7 +578,9 @@ describe('TemplateCard', () => { fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { - expect(mockInvalidCustomizedTemplateList).toHaveBeenCalled() + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: expect.arrayContaining([['console', 'rag', 'pipeline', 'templates', 'get']]), + }) }) }) diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/details/__tests__/index.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/details/__tests__/index.spec.tsx index 11f75abdd2e8f9..cbe95cd6fd9104 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/details/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/details/__tests__/index.spec.tsx @@ -15,9 +15,19 @@ vi.mock('@/app/components/workflow/workflow-preview', () => ({ })) const mockUsePipelineTemplateById = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - usePipelineTemplateById: (...args: unknown[]) => mockUsePipelineTemplateById(...args), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipeline.templates.byTemplateId.get': (o: { + input: { params: { template_id: string }; query: { type: string } } + enabled?: boolean + }) => + mockUsePipelineTemplateById( + { template_id: o.input.params.template_id, type: o.input.query.type }, + o.enabled ?? true, + ), + }) +}) const createPipelineTemplateInfo = (overrides = {}) => ({ name: 'Test Pipeline', diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/details/index.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/details/index.tsx index 3bfdafb86297dc..41bd48cbee9107 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/details/index.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/details/index.tsx @@ -3,12 +3,14 @@ import { DialogClose, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip' import { RiAddLine } from '@remixicon/react' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { selectPipelineTemplate } from '@/app/components/rag-pipeline/utils/query-select' import WorkflowPreview from '@/app/components/workflow/workflow-preview' -import { usePipelineTemplateById } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import ChunkStructureCard from './chunk-structure-card' import { useChunkStructureConfig } from './hooks' @@ -23,12 +25,11 @@ const Details = ({ id, type, name, onApplyTemplate }: DetailsProps) => { const structureLabelId = React.useId() const { t } = useTranslation(['datasetPipeline', 'common']) - const { data: pipelineTemplateInfo } = usePipelineTemplateById( - { - template_id: id, - type, - }, - true, + const { data: pipelineTemplateInfo } = useQuery( + consoleQuery.rag.pipeline.templates.byTemplateId.get.queryOptions({ + input: { params: { template_id: id }, query: { type } }, + select: selectPipelineTemplate, + }), ) const chunkStructureConfig = useChunkStructureConfig() diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx index 36d237c0318e67..fe07d7f09a0e20 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx @@ -6,6 +6,7 @@ import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Input } from '@langgenius/dify-ui/input' import { Textarea } from '@langgenius/dify-ui/textarea' +import { useMutation, useQueryClient } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -16,7 +17,7 @@ import { IconPickerTrigger, } from '@/app/components/base/icon-picker' import { toast } from '@/app/notifications' -import { useInvalidCustomizedTemplateList, useUpdateTemplateInfo } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' type EditPipelineInfoProps = { onClose: () => void @@ -51,16 +52,17 @@ const EditPipelineInfo = ({ onClose, pipeline }: EditPipelineInfoProps) => { setDescription(value) }, []) - const { mutateAsync: updatePipeline } = useUpdateTemplateInfo() - const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList() + const queryClient = useQueryClient() + const { mutateAsync: updatePipeline } = useMutation( + consoleQuery.rag.pipeline.customized.templates.byTemplateId.patch.mutationOptions(), + ) const handleSave = useCallback(async () => { if (!name) { toast.error(t(($) => $.editPipelineInfoNameRequired, { ns: 'datasetPipeline' })) return } - const request = { - template_id: pipeline.id, + const body = { name, icon_info: { icon_type: appIcon.type, @@ -70,22 +72,20 @@ const EditPipelineInfo = ({ onClose, pipeline }: EditPipelineInfoProps) => { }, description, } - await updatePipeline(request, { - onSuccess: () => { - invalidCustomizedTemplateList() - onClose() + await updatePipeline( + { params: { template_id: pipeline.id }, body }, + { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: consoleQuery.rag.pipeline.templates.get.queryKey({ + input: { query: { type: 'customized' } }, + }), + }) + onClose() + }, }, - }) - }, [ - name, - appIcon, - description, - pipeline.id, - updatePipeline, - invalidCustomizedTemplateList, - onClose, - t, - ]) + ) + }, [name, appIcon, description, pipeline.id, updatePipeline, queryClient, onClose, t]) return (
{ + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) const handleUseTemplate = useCallback(async () => { const { data: pipelineTemplateInfo } = await getPipelineTemplateInfo() @@ -62,23 +62,26 @@ const TemplateCard = ({ pipeline, showMoreOperations = true, type }: TemplateCar const request = { yaml_content: pipelineTemplateInfo.export_data, } - await createDataset(request, { - onSuccess: async (newDataset) => { - toast.success(t(($) => $['creation.successTip'], { ns: 'datasetPipeline' })) - invalidDatasetList() - if (newDataset.pipeline_id) - await handleCheckPluginDependencies(newDataset.pipeline_id, true) - trackEvent('create_datasets_with_pipeline', { - template_name: pipeline.name, - template_id: pipeline.id, - template_type: type, - }) - push(`/datasets/${newDataset.dataset_id}/pipeline`) - }, - onError: () => { - toast.error(t(($) => $['creation.errorTip'], { ns: 'datasetPipeline' })) + await createDataset( + { body: request }, + { + onSuccess: async (newDataset) => { + toast.success(t(($) => $['creation.successTip'], { ns: 'datasetPipeline' })) + invalidDatasetList() + if (newDataset.pipeline_id) + await handleCheckPluginDependencies(newDataset.pipeline_id, true) + trackEvent('create_datasets_with_pipeline', { + template_name: pipeline.name, + template_id: pipeline.id, + template_type: type, + }) + push(`/datasets/${newDataset.dataset_id}/pipeline`) + }, + onError: () => { + toast.error(t(($) => $['creation.errorTip'], { ns: 'datasetPipeline' })) + }, }, - }) + ) }, [ getPipelineTemplateInfo, createDataset, @@ -99,20 +102,25 @@ const TemplateCard = ({ pipeline, showMoreOperations = true, type }: TemplateCar setShowEditModal(false) }, []) - const { mutateAsync: exportPipelineDSL, isPending: isExporting } = useExportTemplateDSL() + const { mutateAsync: exportPipelineDSL, isPending: isExporting } = useMutation( + consoleQuery.rag.pipeline.customized.templates.byTemplateId.post.mutationOptions(), + ) const handleExportDSL = useCallback(async () => { if (isExporting) return - await exportPipelineDSL(pipeline.id, { - onSuccess: (res) => { - const blob = new Blob([res.data], { type: 'application/yaml' }) - downloadBlob({ data: blob, fileName: `${pipeline.name}.pipeline` }) - toast.success(t(($) => $['exportDSL.successTip'], { ns: 'datasetPipeline' })) - }, - onError: () => { - toast.error(t(($) => $['exportDSL.errorTip'], { ns: 'datasetPipeline' })) + await exportPipelineDSL( + { params: { template_id: pipeline.id } }, + { + onSuccess: (res) => { + const blob = new Blob([res.data], { type: 'application/yaml' }) + downloadBlob({ data: blob, fileName: `${pipeline.name}.pipeline` }) + toast.success(t(($) => $['exportDSL.successTip'], { ns: 'datasetPipeline' })) + }, + onError: () => { + toast.error(t(($) => $['exportDSL.errorTip'], { ns: 'datasetPipeline' })) + }, }, - }) + ) }, [t, isExporting, pipeline.id, pipeline.name, exportPipelineDSL]) const handleDelete = useCallback(() => { @@ -123,17 +131,25 @@ const TemplateCard = ({ pipeline, showMoreOperations = true, type }: TemplateCar setShowConfirmDelete(false) }, []) - const { mutateAsync: deletePipeline } = useDeleteTemplate() - const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList() + const { mutateAsync: deletePipeline } = useMutation( + consoleQuery.rag.pipeline.customized.templates.byTemplateId.delete.mutationOptions(), + ) const onConfirmDelete = useCallback(async () => { - await deletePipeline(pipeline.id, { - onSuccess: () => { - invalidCustomizedTemplateList() - setShowConfirmDelete(false) + await deletePipeline( + { params: { template_id: pipeline.id } }, + { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: consoleQuery.rag.pipeline.templates.get.queryKey({ + input: { query: { type: 'customized' } }, + }), + }) + setShowConfirmDelete(false) + }, }, - }) - }, [pipeline.id, deletePipeline, invalidCustomizedTemplateList]) + ) + }, [pipeline.id, deletePipeline, queryClient]) return (
diff --git a/web/app/components/datasets/create/__tests__/index.spec.tsx b/web/app/components/datasets/create/__tests__/index.spec.tsx index 6a80a5eb1f66a3..cc6f4464683b74 100644 --- a/web/app/components/datasets/create/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/__tests__/index.spec.tsx @@ -13,6 +13,7 @@ import { DataSourceProvider } from '@/models/common' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { render as renderWithConsoleState } from '@/test/console/render' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import DatasetUpdateForm from '../index' @@ -114,13 +115,16 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () let mockDataSourceList: { result: DataSourceAuth[] } | undefined let mockIsLoadingDataSourceList = false let mockFetchingError = false -vi.mock('@/service/use-datasource', () => ({ - useGetDefaultDataSourceListAuth: () => ({ - data: mockDataSourceList, - isLoading: mockIsLoadingDataSourceList, - isError: mockFetchingError, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'auth.plugin.datasource.defaultList.get': () => ({ + data: mockDataSourceList, + isLoading: mockIsLoadingDataSourceList, + isError: mockFetchingError, + }), + }) +}) // Track props passed to child components let stepOneProps = {} as StepOneProps @@ -254,7 +258,13 @@ vi.mock('../step-two', () => ({ @@ -290,9 +300,9 @@ vi.mock('../top-bar', () => ({ })) const createMockDataset = (overrides?: Partial): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-123', name: 'Test Dataset', - indexing_status: 'completed', icon_info: { icon: '', icon_background: '', icon_type: 'emoji' as const }, description: 'Test description', permission: DatasetPermission.onlyMe, @@ -304,7 +314,6 @@ const createMockDataset = (overrides?: Partial): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 0, - total_document_count: 0, word_count: 0, provider: 'openai', embedding_model: 'text-embedding-ada-002', @@ -320,16 +329,6 @@ const createMockDataset = (overrides?: Partial): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_mode: undefined, - reranking_model: { reranking_provider_name: '', reranking_model_name: '' }, - weights: undefined, - top_k: 3, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', diff --git a/web/app/components/datasets/create/embedding-process/__tests__/index.spec.tsx b/web/app/components/datasets/create/embedding-process/__tests__/index.spec.tsx index 1d1d14c008cfb9..e1a5e0fe6e88c2 100644 --- a/web/app/components/datasets/create/embedding-process/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/embedding-process/__tests__/index.spec.tsx @@ -1,11 +1,12 @@ import type { ReactElement } from 'react' import type { IndexingStatusResponse } from '@/models/datasets' +import { QueryClient } from '@tanstack/react-query' import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { consoleQuery } from '@/service/console' import { renderWithConsoleQuery } from '@/test/console/query-data' import EmbeddingProcess from '../index' -const mockInvalidDocumentList = vi.fn() let deploymentEdition: 'CLOUD' | 'COMMUNITY' = 'COMMUNITY' let mockPlanType: 'sandbox' | 'professional' | 'team' = 'sandbox' let mockPollingState: { @@ -35,13 +36,12 @@ vi.mock('@/next/link', () => ({ ), })) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useProcessRule: () => ({ data: undefined }), -})) - -vi.mock('@/service/knowledge/use-document', () => ({ - useInvalidDocumentList: () => mockInvalidDocumentList, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.processRule.get': () => ({ data: undefined }), + }) +}) vi.mock('@/hooks/use-api-access-url', () => ({ useDatasetApiAccessUrl: () => 'https://api.example.com/docs', @@ -88,6 +88,7 @@ function render(ui: ReactElement) { describe('EmbeddingProcess', () => { beforeEach(() => { + vi.spyOn(QueryClient.prototype, 'invalidateQueries') vi.clearAllMocks() deploymentEdition = 'COMMUNITY' mockPlanType = 'sandbox' @@ -189,7 +190,9 @@ describe('EmbeddingProcess', () => { expect(link).toHaveAttribute('href', '/datasets/dataset-1/documents') await user.click(link) - expect(mockInvalidDocumentList).toHaveBeenCalledOnce() + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key(), + }) }) it('links to the dataset API reference', () => { diff --git a/web/app/components/datasets/create/embedding-process/__tests__/utils.spec.ts b/web/app/components/datasets/create/embedding-process/__tests__/utils.spec.ts index 5fc81ccc70a71c..a100a89fc09e4c 100644 --- a/web/app/components/datasets/create/embedding-process/__tests__/utils.spec.ts +++ b/web/app/components/datasets/create/embedding-process/__tests__/utils.spec.ts @@ -1,33 +1,6 @@ -import type { DataSourceInfo, FullDocumentDetail, IndexingStatusResponse } from '@/models/datasets' +import type { IndexingStatusResponse, InitialDocumentDetail } from '@/models/datasets' import { describe, expect, it } from 'vite-plus/test' -import { - createDocumentLookup, - getFileType, - getSourcePercent, - isLegacyDataSourceInfo, - isSourceEmbedding, -} from '../utils' - -describe('isLegacyDataSourceInfo', () => { - it('should return true when upload_file object exists', () => { - const info = { upload_file: { id: '1', name: 'test.pdf' } } as unknown as DataSourceInfo - expect(isLegacyDataSourceInfo(info)).toBe(true) - }) - - it('should return false when upload_file is absent', () => { - const info = { notion_page_icon: 'icon' } as unknown as DataSourceInfo - expect(isLegacyDataSourceInfo(info)).toBe(false) - }) - - it('should return false for null', () => { - expect(isLegacyDataSourceInfo(null as unknown as DataSourceInfo)).toBe(false) - }) - - it('should return false when upload_file is a string', () => { - const info = { upload_file: 'not-an-object' } as unknown as DataSourceInfo - expect(isLegacyDataSourceInfo(info)).toBe(false) - }) -}) +import { createDocumentLookup, getFileType, getSourcePercent, isSourceEmbedding } from '../utils' describe('isSourceEmbedding', () => { const embeddingStatuses = ['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'] @@ -110,7 +83,7 @@ describe('createDocumentLookup', () => { notion_page_icon: 'https://icon.url', }, }, - ] as unknown as FullDocumentDetail[] + ] as unknown as InitialDocumentDetail[] it('should get document by id', () => { const lookup = createDocumentLookup(documents) @@ -143,7 +116,7 @@ describe('createDocumentLookup', () => { id: 'doc-3', data_source_info: { some_other: 'field' }, }, - ] as unknown as FullDocumentDetail[] + ] as unknown as InitialDocumentDetail[] const lookup = createDocumentLookup(docs) expect(lookup.getNotionIcon('doc-3')).toBeUndefined() }) diff --git a/web/app/components/datasets/create/embedding-process/index.tsx b/web/app/components/datasets/create/embedding-process/index.tsx index 9f1a93266bda52..382adf60671ffe 100644 --- a/web/app/components/datasets/create/embedding-process/index.tsx +++ b/web/app/components/datasets/create/embedding-process/index.tsx @@ -1,11 +1,11 @@ import type { FC } from 'react' -import type { FullDocumentDetail } from '@/models/datasets' +import type { InitialDocumentDetail } from '@/models/datasets' import type { RETRIEVE_METHOD } from '@/types/app' import { buttonVariants } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Separator } from '@langgenius/dify-ui/separator' import { RiArrowRightLine, RiLoader2Fill, RiTerminalBoxLine } from '@remixicon/react' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -14,8 +14,6 @@ import { deploymentEditionAtom } from '@/features/system-features/state' import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' import Link from '@/next/link' import { consoleQuery } from '@/service/console' -import { useProcessRule } from '@/service/knowledge/use-dataset' -import { useInvalidDocumentList } from '@/service/knowledge/use-document' import IndexingProgressItem from './indexing-progress-item' import RuleDetail from './rule-detail' import UpgradeBanner from './upgrade-banner' @@ -25,7 +23,7 @@ import { createDocumentLookup } from './utils' type EmbeddingProcessProps = { datasetId: string batchId: string - documents?: FullDocumentDetail[] + documents?: InitialDocumentDetail[] indexingType?: string retrievalMethod?: RETRIEVE_METHOD } @@ -99,7 +97,11 @@ const EmbeddingProcess: FC = ({ select: (data) => data.billing.subscription.plan, }), ) - const invalidDocumentList = useInvalidDocumentList() + const queryClient = useQueryClient() + const invalidDocumentList = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key(), + }) const apiReferenceUrl = useDatasetApiAccessUrl() // Polling hook for indexing status @@ -110,7 +112,12 @@ const EmbeddingProcess: FC = ({ // Get process rule for the first document const firstDocumentId = documents[0]?.id - const { data: ruleDetail } = useProcessRule(firstDocumentId) + const { data: ruleDetail } = useQuery( + consoleQuery.datasets.processRule.get.queryOptions({ + input: { query: { document_id: firstDocumentId } }, + enabled: !!firstDocumentId, + }), + ) // Document lookup utilities - memoized for performance const documentLookup = useMemo(() => createDocumentLookup(documents), [documents]) diff --git a/web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx b/web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx index 039cd69baacd6f..f62412e493570f 100644 --- a/web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx +++ b/web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx @@ -1,4 +1,4 @@ -import type { FC } from 'react' +import type { ComponentProps, FC } from 'react' import type { IndexingStatusResponse } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip' @@ -15,7 +15,7 @@ type IndexingProgressItemProps = { detail: IndexingStatusResponse name?: string sourceType?: DataSourceType - notionIcon?: string + notionIcon?: ComponentProps['src'] } // Status icon component for completed/error states @@ -54,7 +54,7 @@ const StatusIcon: FC<{ status: string; error?: string }> = ({ status, error }) = const SourceTypeIcon: FC<{ sourceType?: DataSourceType name?: string - notionIcon?: string + notionIcon?: ComponentProps['src'] }> = ({ sourceType, name, notionIcon }) => { if (sourceType === DataSourceType.FILE) { return ( @@ -119,7 +119,7 @@ const IndexingProgressItem: FC = ({
{isEmbedding &&
{`${percent}%`}
} - +
) diff --git a/web/app/components/datasets/create/embedding-process/rule-detail.tsx b/web/app/components/datasets/create/embedding-process/rule-detail.tsx index b0d3d722946688..cee66e0bfb3509 100644 --- a/web/app/components/datasets/create/embedding-process/rule-detail.tsx +++ b/web/app/components/datasets/create/embedding-process/rule-detail.tsx @@ -26,7 +26,6 @@ const RETRIEVAL_ICON_MAP: Partial> = { [RETRIEVE_METHOD.fullText]: retrievalIcon.fullText, [RETRIEVE_METHOD.hybrid]: retrievalIcon.hybrid, [RETRIEVE_METHOD.semantic]: retrievalIcon.vector, - [RETRIEVE_METHOD.invertedIndex]: retrievalIcon.fullText, [RETRIEVE_METHOD.keywordSearch]: retrievalIcon.fullText, } diff --git a/web/app/components/datasets/create/embedding-process/utils.ts b/web/app/components/datasets/create/embedding-process/utils.ts index 0f0fbfd84d7cd7..3b7e22491740e8 100644 --- a/web/app/components/datasets/create/embedding-process/utils.ts +++ b/web/app/components/datasets/create/embedding-process/utils.ts @@ -1,20 +1,9 @@ -import type { - DataSourceInfo, - DataSourceType, - FullDocumentDetail, - IndexingStatusResponse, - LegacyDataSourceInfo, -} from '@/models/datasets' +import type { IndexingStatusResponse, InitialDocumentDetail } from '@/models/datasets' +import { toCreationDataSourceType } from '@/app/components/datasets/dataset-fields' +import { getNotionPageIcon } from '@/app/components/datasets/documents/document-source' const EMBEDDING_STATUSES = ['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'] as const -/** - * Type guard for legacy data source info with upload_file property - */ -export const isLegacyDataSourceInfo = (info: DataSourceInfo): info is LegacyDataSourceInfo => { - return info != null && typeof (info as LegacyDataSourceInfo).upload_file === 'object' -} - /** * Check if a status indicates the source is being embedded */ @@ -42,7 +31,7 @@ export const getFileType = (name?: string): string => name?.split('.').pop() || /** * Document lookup utilities - provides document info by ID from a list */ -export const createDocumentLookup = (documents: FullDocumentDetail[]) => { +export const createDocumentLookup = (documents: InitialDocumentDetail[]) => { const documentMap = new Map(documents.map((doc) => [doc.id, doc])) return { @@ -50,13 +39,8 @@ export const createDocumentLookup = (documents: FullDocumentDetail[]) => { getName: (id: string) => documentMap.get(id)?.name, - getSourceType: (id: string) => - documentMap.get(id)?.data_source_type as DataSourceType | undefined, + getSourceType: (id: string) => toCreationDataSourceType(documentMap.get(id)?.data_source_type), - getNotionIcon: (id: string) => { - const info = documentMap.get(id)?.data_source_info - if (info && isLegacyDataSourceInfo(info)) return info.notion_page_icon - return undefined - }, + getNotionIcon: (id: string) => getNotionPageIcon(documentMap.get(id)?.data_source_info, false), } } diff --git a/web/app/components/datasets/create/empty-dataset-creation-modal/__tests__/index.spec.tsx b/web/app/components/datasets/create/empty-dataset-creation-modal/__tests__/index.spec.tsx index ffc7cbf5dfc292..375cdaeda14439 100644 --- a/web/app/components/datasets/create/empty-dataset-creation-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/empty-dataset-creation-modal/__tests__/index.spec.tsx @@ -1,10 +1,15 @@ import type { MockedFunction } from 'vite-plus/test' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { QueryClient } from '@tanstack/react-query' +import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { consoleQuery } from '@/service/console' import { createEmptyDataset } from '@/service/datasets' -import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' +import { renderWithConsoleQuery } from '@/test/console/query-data' import EmptyDatasetCreationModal from '../index' +const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries') +const datasetListKey = consoleQuery.datasets.get.key() + const { mockNotify, mockToast } = vi.hoisted(() => { const mockNotify = vi.fn() const mockToast = Object.assign(mockNotify, { @@ -36,17 +41,8 @@ vi.mock('@/service/datasets', () => ({ createEmptyDataset: vi.fn(), })) -// Mock useInvalidDatasetList hook -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: vi.fn(), -})) - // Type cast mocked functions const mockCreateEmptyDataset = createEmptyDataset as MockedFunction -const mockInvalidDatasetList = vi.fn() -const mockUseInvalidDatasetList = useInvalidDatasetList as MockedFunction< - typeof useInvalidDatasetList -> // Test data builder for props const createDefaultProps = (overrides?: Partial<{ show: boolean; onHide: () => void }>) => ({ @@ -58,7 +54,6 @@ const createDefaultProps = (overrides?: Partial<{ show: boolean; onHide: () => v describe('EmptyDatasetCreationModal', () => { beforeEach(() => { vi.clearAllMocks() - mockUseInvalidDatasetList.mockReturnValue(mockInvalidDatasetList) mockCreateEmptyDataset.mockResolvedValue({ id: 'dataset-123', name: 'Test Dataset', @@ -70,7 +65,7 @@ describe('EmptyDatasetCreationModal', () => { it('should render modal with correct elements', () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument() expect(screen.getByText('datasetCreation.stepOne.modal.tip')).toBeInTheDocument() @@ -84,7 +79,7 @@ describe('EmptyDatasetCreationModal', () => { it('should render input with empty value initially', () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText( 'datasetCreation.stepOne.modal.placeholder', @@ -95,7 +90,7 @@ describe('EmptyDatasetCreationModal', () => { it('should not render modal content when show is false', () => { const props = createDefaultProps({ show: false }) - render() + renderWithConsoleQuery() // Assert - Modal should not be visible (check for absence of title) expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument() @@ -106,20 +101,22 @@ describe('EmptyDatasetCreationModal', () => { describe('Props', () => { describe('show prop', () => { it('should show modal when show is true', () => { - render() + renderWithConsoleQuery() expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument() }) it('should hide modal when show is false', () => { - render() + renderWithConsoleQuery() expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument() }) it('should toggle visibility when show prop changes', () => { const onHide = vi.fn() - const { rerender } = render() + const { rerender } = renderWithConsoleQuery( + , + ) // Act & Assert - Initially hidden expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument() @@ -133,7 +130,7 @@ describe('EmptyDatasetCreationModal', () => { describe('onHide prop', () => { it('should call onHide when cancel button is clicked', () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const cancelButton = screen.getByText('datasetCreation.stepOne.modal.cancelButton') fireEvent.click(cancelButton) @@ -143,7 +140,7 @@ describe('EmptyDatasetCreationModal', () => { it('should call onHide when close icon is clicked', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const closeButton = await screen.findByRole('button', { name: /operation\.close$/ }) fireEvent.click(closeButton) @@ -157,7 +154,7 @@ describe('EmptyDatasetCreationModal', () => { describe('State Management', () => { it('should update input value when user types', () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText( 'datasetCreation.stepOne.modal.placeholder', ) as HTMLInputElement @@ -169,7 +166,9 @@ describe('EmptyDatasetCreationModal', () => { it('should persist input value when modal is hidden and shown again via rerender', () => { const onHide = vi.fn() - const { rerender } = render() + const { rerender } = renderWithConsoleQuery( + , + ) const input = screen.getByPlaceholderText( 'datasetCreation.stepOne.modal.placeholder', ) as HTMLInputElement @@ -191,7 +190,7 @@ describe('EmptyDatasetCreationModal', () => { it('should handle consecutive input changes', () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText( 'datasetCreation.stepOne.modal.placeholder', ) as HTMLInputElement @@ -211,7 +210,7 @@ describe('EmptyDatasetCreationModal', () => { describe('User Interactions', () => { it('should submit form when confirm button is clicked with valid input', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -225,7 +224,7 @@ describe('EmptyDatasetCreationModal', () => { it('should show error notification when input is empty', async () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') // Act - Click confirm without entering a name @@ -242,7 +241,7 @@ describe('EmptyDatasetCreationModal', () => { it('should show error notification when input exceeds 40 characters', async () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -262,7 +261,7 @@ describe('EmptyDatasetCreationModal', () => { it('should allow exactly 40 characters', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -278,7 +277,7 @@ describe('EmptyDatasetCreationModal', () => { it('should close modal on cancel button click', () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const cancelButton = screen.getByText('datasetCreation.stepOne.modal.cancelButton') fireEvent.click(cancelButton) @@ -291,7 +290,7 @@ describe('EmptyDatasetCreationModal', () => { describe('API Calls', () => { it('should submit from the dataset name input with Enter', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const user = userEvent.setup() const input = screen.getByRole('textbox', { name: 'datasetCreation.stepOne.modal.input' }) @@ -312,7 +311,7 @@ describe('EmptyDatasetCreationModal', () => { }), ) const onHide = vi.fn() - render() + renderWithConsoleQuery() const user = userEvent.setup() const input = screen.getByRole('textbox', { name: 'datasetCreation.stepOne.modal.input' }) @@ -334,7 +333,7 @@ describe('EmptyDatasetCreationModal', () => { it('should call invalidDatasetList after successful creation', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -342,13 +341,13 @@ describe('EmptyDatasetCreationModal', () => { fireEvent.click(confirmButton) await waitFor(() => { - expect(mockInvalidDatasetList).toHaveBeenCalled() + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: datasetListKey }) }) }) it('should call onHide after successful creation', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -363,7 +362,7 @@ describe('EmptyDatasetCreationModal', () => { it('should show error notification on API failure', async () => { mockCreateEmptyDataset.mockRejectedValue(new Error('API Error')) const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -381,7 +380,7 @@ describe('EmptyDatasetCreationModal', () => { it('should not call onHide on API failure', async () => { mockCreateEmptyDataset.mockRejectedValue(new Error('API Error')) const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -399,7 +398,7 @@ describe('EmptyDatasetCreationModal', () => { it('should not invalidate dataset list on API failure', async () => { mockCreateEmptyDataset.mockRejectedValue(new Error('API Error')) const props = createDefaultProps() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -409,7 +408,7 @@ describe('EmptyDatasetCreationModal', () => { await waitFor(() => { expect(mockNotify).toHaveBeenCalled() }) - expect(mockInvalidDatasetList).not.toHaveBeenCalled() + expect(invalidateQueries).not.toHaveBeenCalledWith({ queryKey: datasetListKey }) }) }) @@ -421,7 +420,7 @@ describe('EmptyDatasetCreationModal', () => { name: 'Test', } as ReturnType extends Promise ? T : never) const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -435,7 +434,7 @@ describe('EmptyDatasetCreationModal', () => { it('should not navigate on validation error', async () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') // Act - Click confirm with empty input @@ -450,7 +449,7 @@ describe('EmptyDatasetCreationModal', () => { it('should not navigate on API error', async () => { mockCreateEmptyDataset.mockRejectedValue(new Error('API Error')) const props = createDefaultProps() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -468,7 +467,7 @@ describe('EmptyDatasetCreationModal', () => { describe('Edge Cases', () => { it('should handle whitespace-only input as valid (component behavior)', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -484,7 +483,7 @@ describe('EmptyDatasetCreationModal', () => { it('should handle special characters in input', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -498,7 +497,7 @@ describe('EmptyDatasetCreationModal', () => { it('should handle Unicode characters in input', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -512,7 +511,7 @@ describe('EmptyDatasetCreationModal', () => { it('should handle input at exactly 40 character boundary', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -528,7 +527,7 @@ describe('EmptyDatasetCreationModal', () => { it('should reject input at 41 character boundary', async () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -548,7 +547,7 @@ describe('EmptyDatasetCreationModal', () => { it('should handle rapid consecutive submits', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -566,7 +565,7 @@ describe('EmptyDatasetCreationModal', () => { it('should handle input with leading/trailing spaces', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -581,7 +580,7 @@ describe('EmptyDatasetCreationModal', () => { it('should handle newline characters in input (browser strips newlines)', async () => { const mockOnHide = vi.fn() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -599,7 +598,7 @@ describe('EmptyDatasetCreationModal', () => { describe('Validation', () => { it('should not submit when input is empty string', async () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') fireEvent.click(confirmButton) @@ -614,7 +613,7 @@ describe('EmptyDatasetCreationModal', () => { it('should validate length before calling API', async () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -633,7 +632,7 @@ describe('EmptyDatasetCreationModal', () => { it('should validate empty string before length check', async () => { const props = createDefaultProps() - render() + renderWithConsoleQuery() const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') // Act - Don't enter anything @@ -657,7 +656,7 @@ describe('EmptyDatasetCreationModal', () => { id: 'new-id-789', name: 'Complete Flow Test', } as ReturnType extends Promise ? T : never) - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -669,7 +668,7 @@ describe('EmptyDatasetCreationModal', () => { // 1. API called expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Complete Flow Test' }) // 2. Dataset list invalidated - expect(mockInvalidDatasetList).toHaveBeenCalled() + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: datasetListKey }) // 3. Modal closed expect(mockOnHide).toHaveBeenCalled() // 4. Navigation happened @@ -680,7 +679,7 @@ describe('EmptyDatasetCreationModal', () => { it('should handle error flow correctly', async () => { const mockOnHide = vi.fn() mockCreateEmptyDataset.mockRejectedValue(new Error('Server Error')) - render() + renderWithConsoleQuery() const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton') @@ -699,7 +698,7 @@ describe('EmptyDatasetCreationModal', () => { }) // 3. These should NOT happen on error - expect(mockInvalidDatasetList).not.toHaveBeenCalled() + expect(invalidateQueries).not.toHaveBeenCalledWith({ queryKey: datasetListKey }) expect(mockOnHide).not.toHaveBeenCalled() expect(mockPush).not.toHaveBeenCalled() }) diff --git a/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx b/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx index be333a7070a2ca..501853ce987fad 100644 --- a/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx +++ b/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx @@ -5,14 +5,15 @@ import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Field, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' +import { useQueryClient } from '@tanstack/react-query' import * as React from 'react' -import { useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' +import { consoleQuery } from '@/service/console' import { createEmptyDataset } from '@/service/datasets' -import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' import s from './index.module.css' type IProps = { @@ -24,7 +25,10 @@ const EmptyDatasetCreationModal = ({ show = false, onHide }: IProps) => { const [isSubmitting, setIsSubmitting] = useState(false) const { t } = useTranslation(['common', 'datasetCreation']) const router = useRouter() - const invalidDatasetList = useInvalidDatasetList() + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) const submit = async () => { if (isSubmitting) return diff --git a/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx b/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx index 570969458b003b..c05053fb3da6b9 100644 --- a/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx +++ b/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx @@ -41,9 +41,6 @@ const supportedFormats = { allowed_extensions: ['pdf', 'docx', 'txt', 'md'], } -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ data: mockFileUploadConfig }), -})) vi.mock('@/i18n/language', () => ({ LanguagesSupported: ['en-US', 'zh-Hans'], })) @@ -72,6 +69,13 @@ const render = (ui: ReactElement) => systemFeatures: { deployment_edition: 'CLOUD' }, }) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ data: mockFileUploadConfig }), + }) +}) + describe('useFileUpload', () => { const defaultOptions = { fileList: [] as FileItem[], diff --git a/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts b/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts index dbdff6af6da468..c3c3b79862afd5 100644 --- a/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts +++ b/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts @@ -11,7 +11,6 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { LanguagesSupported } from '@/i18n/language' import { upload } from '@/service/base' import { consoleQuery } from '@/service/console' -import { useFileUploadConfig } from '@/service/use-common' import { getFileExtension } from '@/utils/format' import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../constants' @@ -85,7 +84,7 @@ export const useFileUpload = ({ const hideUpload = !supportBatchUpload && fileList.length > 0 - const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: fileUploadConfigResponse } = useQuery(consoleQuery.files.upload.get.queryOptions()) const { data: supportFileTypesResponse } = useQuery( consoleQuery.files.supportType.get.queryOptions(), ) diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx index f3605fa11da78d..660acaffe182b2 100644 --- a/web/app/components/datasets/create/index.tsx +++ b/web/app/components/datasets/create/index.tsx @@ -8,7 +8,7 @@ import type { FileItem, } from '@/models/datasets' import type { RETRIEVE_METHOD } from '@/types/app' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useAtomValue } from 'jotai' import { useQueryState } from 'nuqs' @@ -30,7 +30,7 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { DataSourceProvider } from '@/models/common' import { DataSourceType } from '@/models/datasets' import { useRouter } from '@/next/navigation' -import { useGetDefaultDataSourceListAuth } from '@/service/use-datasource' +import { consoleQuery } from '@/service/console' import { getDatasetACLCapabilities } from '@/utils/permission' import AppUnavailable from '../../base/app-unavailable' import { ModelTypeEnum } from '../../header/account-setting/model-provider-page/declarations' @@ -94,7 +94,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { data: dataSourceList, isLoading: isLoadingAuthedDataSourceList, isError: fetchingAuthedDataSourceListError, - } = useGetDefaultDataSourceListAuth() + } = useQuery(consoleQuery.auth.plugin.datasource.defaultList.get.queryOptions()) const updateNotionPages = useCallback((value: NotionPage[]) => { setNotionPages(value) @@ -192,7 +192,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { setSettingsDestination('provider')} - indexingType={datasetDetail?.indexing_technique} + indexingType={datasetDetail?.indexing_technique ?? undefined} datasetId={datasetId} dataSourceType={dataSourceType} files={fileList.map((file) => file.file)} diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx index 760bdb083345a6..910445322d99d7 100644 --- a/web/app/components/datasets/create/step-one/index.tsx +++ b/web/app/components/datasets/create/step-one/index.tsx @@ -14,6 +14,7 @@ import NotionConnector from '@/app/components/base/notion-connector' import { NotionPageSelector } from '@/app/components/base/notion-page-selector' import VectorSpaceFull from '@/app/components/billing/vector-space-full' import VectorSpaceUnavailable from '@/app/components/billing/vector-space-unavailable' +import { toCreationDataSourceType } from '@/app/components/datasets/dataset-fields' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { deploymentEditionAtom } from '@/features/system-features/state' import { DataSourceType } from '@/models/datasets' @@ -138,7 +139,7 @@ const StepOne = ({ // Default to FILE type when no type is provided from either source const dataSourceType = isInCreatePage ? (inCreatePageDataSourceType ?? DataSourceType.FILE) - : (dataset?.data_source_type ?? DataSourceType.FILE) + : (toCreationDataSourceType(dataset?.data_source_type) ?? DataSourceType.FILE) const allFileLoaded = files.length > 0 && files.every((file) => file.file.id) const hasNotion = notionPages.length > 0 diff --git a/web/app/components/datasets/create/step-three/index.tsx b/web/app/components/datasets/create/step-three/index.tsx index 6c15653d95b340..158ab953738f20 100644 --- a/web/app/components/datasets/create/step-three/index.tsx +++ b/web/app/components/datasets/create/step-three/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { createDocumentResponse, FullDocumentDetail } from '@/models/datasets' +import type { createDocumentResponse } from '@/models/datasets' import type { RETRIEVE_METHOD } from '@/types/app' import { Separator } from '@langgenius/dify-ui/separator' import { RiBookOpenLine } from '@remixicon/react' @@ -27,7 +27,8 @@ const StepThree = ({ const { t } = useTranslation(['datasetCreation', 'datasetPipeline']) const docLink = useDocLink() - const iconInfo = creationCache?.dataset?.icon_info || { + // The creation response carries no icon; new knowledge bases start with the default one. + const iconInfo = { icon: '📙', icon_type: 'emoji', icon_background: '#FFF4ED', @@ -73,11 +74,9 @@ const StepThree = ({
diff --git a/web/app/components/datasets/create/step-two/__tests__/index.spec.tsx b/web/app/components/datasets/create/step-two/__tests__/index.spec.tsx index c81a4e41a60f77..4001b92de1d67c 100644 --- a/web/app/components/datasets/create/step-two/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/step-two/__tests__/index.spec.tsx @@ -1,8 +1,4 @@ -import type { - GetWorkspacesCurrentModelsModelTypesByModelTypeData, - ProviderWithModelsResponse, -} from '@dify/contracts/api/console/workspaces/types.gen' -import type { OperationKey } from '@orpc/tanstack-query' +import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { DataSourceProvider, NotionPage } from '@/models/common' import type { CrawlOptions, @@ -15,15 +11,7 @@ import type { Rules, } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' -import { - act, - cleanup, - fireEvent, - renderHook, - screen, - waitFor, - within, -} from '@testing-library/react' +import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { ConfigurationMethodEnum, @@ -33,7 +21,10 @@ import { import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { ChunkingMode, DataSourceType, ProcessMode } from '@/models/datasets' import { expectLoadingButton } from '@/test/button' -import { renderWithConsoleQuery as render } from '@/test/console/query-data' +import { + renderWithConsoleQuery as render, + renderHookWithConsoleQuery, +} from '@/test/console/query-data' import { RETRIEVE_METHOD } from '@/types/app' import { PreviewPanel } from '../components/preview-panel' import { StepTwoFooter } from '../components/step-two-footer' @@ -139,89 +130,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () const mockFetchDefaultProcessRuleMutate = vi.fn() const mockResetFileEstimate = vi.fn() -vi.mock('@/service/knowledge/use-create-dataset', () => ({ - useFetchDefaultProcessRule: ({ - onSuccess, - }: { - onSuccess: (data: { - rules: Rules - limits: { indexing_max_segmentation_tokens_length: number } - }) => void - }) => ({ - mutate: (url: string) => { - mockFetchDefaultProcessRuleMutate(url) - onSuccess({ - rules: { - segmentation: { separator: '\\n', max_tokens: 500, chunk_overlap: 50 }, - pre_processing_rules: [ - { id: 'remove_extra_spaces', enabled: true }, - { id: 'remove_urls_emails', enabled: false }, - ], - parent_mode: 'paragraph', - subchunk_segmentation: { separator: '\\n', max_tokens: 256 }, - }, - limits: { indexing_max_segmentation_tokens_length: 4000 }, - }) - }, - isPending: false, - }), - useFetchFileIndexingEstimateForFile: () => ({ - mutate: vi.fn(), - data: undefined, - isIdle: true, - isPending: false, - reset: mockResetFileEstimate, - }), - useFetchFileIndexingEstimateForNotion: () => ({ - mutate: vi.fn(), - data: undefined, - isIdle: true, - isPending: false, - reset: vi.fn(), - }), - useFetchFileIndexingEstimateForWeb: () => ({ - mutate: vi.fn(), - data: undefined, - isIdle: true, - isPending: false, - reset: vi.fn(), - }), - useCreateFirstDocument: () => ({ - mutateAsync: vi - .fn() - .mockImplementation( - async (params: unknown, options?: { onSuccess?: (data: unknown) => void }) => { - const data = { dataset: { id: 'new-dataset-id' } } - options?.onSuccess?.(data) - return data - }, - ), - isPending: false, - }), - useCreateDocument: () => ({ - mutateAsync: vi - .fn() - .mockImplementation( - async (params: unknown, options?: { onSuccess?: (data: unknown) => void }) => { - const data = { document: { id: 'new-doc-id' } } - options?.onSuccess?.(data) - return data - }, - ), - isPending: false, - }), - getNotionInfo: vi - .fn() - .mockReturnValue([{ workspace_id: 'ws-1', pages: [{ page_id: 'page-1' }] }]), - getWebsiteInfo: vi - .fn() - .mockReturnValue({ provider: 'jinaReader', job_id: 'job-123', urls: ['https://test.com'] }), -})) - -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => vi.fn(), -})) - // Mock amplitude tracking (external service) vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn(), @@ -324,9 +232,6 @@ const createMockDocumentDetail = (overrides?: Partial): Full id: 'doc-1', doc_form: ChunkingMode.text, doc_language: 'English', - file: { id: 'file-1', name: 'test.pdf', extension: 'pdf' }, - notion_page: createMockNotionPage(), - website_page: createMockWebsitePage(), dataset_process_rule: { mode: ProcessMode.general, rules: { @@ -352,7 +257,6 @@ const createMockEstimate = ( overrides?: Partial, ): FileIndexingEstimateResponse => ({ total_segments: 10, - total_nodes: 10, tokens: 5000, total_price: 0.01, currency: 'USD', @@ -490,7 +394,7 @@ describe('useSegmentationState', () => { // Tests for initial state describe('Initial State', () => { it('should initialize with default values', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) expect(result.current.segmentationType).toBe(ProcessMode.general) expect(result.current.segmentIdentifier).toBe(DEFAULT_SEGMENT_IDENTIFIER) @@ -501,7 +405,7 @@ describe('useSegmentationState', () => { }) it('should initialize with custom segmentation type', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useSegmentationState({ initialSegmentationType: ProcessMode.parentChild }), ) @@ -512,7 +416,7 @@ describe('useSegmentationState', () => { // Tests for state setters describe('State Management', () => { it('should update segmentation type', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setSegmentationType(ProcessMode.parentChild) @@ -522,7 +426,7 @@ describe('useSegmentationState', () => { }) it('should update max chunk length', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setMaxChunkLength(2048) @@ -532,7 +436,7 @@ describe('useSegmentationState', () => { }) it('should update overlap', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setOverlap(100) @@ -542,8 +446,8 @@ describe('useSegmentationState', () => { }) it('should update rules', () => { - const { result } = renderHook(() => useSegmentationState()) - const newRules: PreProcessingRule[] = [{ id: 'test', enabled: true }] + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) + const newRules: PreProcessingRule[] = [{ id: 'remove_stopwords', enabled: true }] act(() => { result.current.setRules(newRules) @@ -556,7 +460,7 @@ describe('useSegmentationState', () => { // Tests for setSegmentIdentifier with escape describe('setSegmentIdentifier', () => { it('should escape special characters', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setSegmentIdentifier('\n\n') @@ -566,7 +470,7 @@ describe('useSegmentationState', () => { }) it('should use default when empty and canEmpty is false', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setSegmentIdentifier('') @@ -576,7 +480,7 @@ describe('useSegmentationState', () => { }) it('should allow empty when canEmpty is true', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setSegmentIdentifier('', true) @@ -589,46 +493,46 @@ describe('useSegmentationState', () => { // Tests for toggleRule describe('toggleRule', () => { it('should toggle rule enabled state', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setRules([ - { id: 'rule1', enabled: true }, - { id: 'rule2', enabled: false }, + { id: 'remove_extra_spaces', enabled: true }, + { id: 'remove_urls_emails', enabled: false }, ]) }) act(() => { - result.current.toggleRule('rule1') + result.current.toggleRule('remove_extra_spaces') }) - expect(result.current.rules.find((r) => r.id === 'rule1')?.enabled).toBe(false) - expect(result.current.rules.find((r) => r.id === 'rule2')?.enabled).toBe(false) + expect(result.current.rules.find((r) => r.id === 'remove_extra_spaces')?.enabled).toBe(false) + expect(result.current.rules.find((r) => r.id === 'remove_urls_emails')?.enabled).toBe(false) }) it('should not affect other rules', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setRules([ - { id: 'rule1', enabled: true }, - { id: 'rule2', enabled: false }, + { id: 'remove_extra_spaces', enabled: true }, + { id: 'remove_urls_emails', enabled: false }, ]) }) act(() => { - result.current.toggleRule('rule2') + result.current.toggleRule('remove_urls_emails') }) - expect(result.current.rules.find((r) => r.id === 'rule1')?.enabled).toBe(true) - expect(result.current.rules.find((r) => r.id === 'rule2')?.enabled).toBe(true) + expect(result.current.rules.find((r) => r.id === 'remove_extra_spaces')?.enabled).toBe(true) + expect(result.current.rules.find((r) => r.id === 'remove_urls_emails')?.enabled).toBe(true) }) }) // Tests for parent-child config describe('Parent-Child Configuration', () => { it('should update parent config delimiter with truthy value', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.updateParentConfig('delimiter', '\n\n\n') @@ -638,7 +542,7 @@ describe('useSegmentationState', () => { }) it('should update parent config delimiter with empty value', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.updateParentConfig('delimiter', '') @@ -648,7 +552,7 @@ describe('useSegmentationState', () => { }) it('should update parent config maxLength', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.updateParentConfig('maxLength', 2048) @@ -658,7 +562,7 @@ describe('useSegmentationState', () => { }) it('should update child config delimiter with truthy value', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.updateChildConfig('delimiter', '\n') @@ -668,7 +572,7 @@ describe('useSegmentationState', () => { }) it('should update child config delimiter with empty value', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.updateChildConfig('delimiter', '') @@ -678,7 +582,7 @@ describe('useSegmentationState', () => { }) it('should update child config maxLength', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.updateChildConfig('maxLength', 256) @@ -688,7 +592,7 @@ describe('useSegmentationState', () => { }) it('should set chunk for context mode', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setChunkForContext('full-doc') @@ -701,7 +605,7 @@ describe('useSegmentationState', () => { // Tests for resetToDefaults describe('resetToDefaults', () => { it('should reset to default config when available', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) // Set non-default values and default config act(() => { @@ -721,7 +625,7 @@ describe('useSegmentationState', () => { }) it('should only reset parentChildConfig when no default config', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) // Set non-default values without setting defaultConfig act(() => { @@ -746,7 +650,7 @@ describe('useSegmentationState', () => { // Tests for applyConfigFromRules describe('applyConfigFromRules', () => { it('should apply general config from rules', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) const rules = createMockRules({ segmentation: { separator: '---', max_tokens: 512, chunk_overlap: 25 }, }) @@ -760,7 +664,7 @@ describe('useSegmentationState', () => { }) it('should apply hierarchical config from rules', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) const rules = createMockRules({ parent_mode: 'paragraph', subchunk_segmentation: { separator: '\n', max_tokens: 256 }, @@ -775,7 +679,7 @@ describe('useSegmentationState', () => { }) it('should apply full hierarchical parent-child config from rules', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) const rules = createMockRules({ segmentation: { separator: '\n\n', max_tokens: 1024, chunk_overlap: 50 }, parent_mode: 'full-doc', @@ -800,7 +704,7 @@ describe('useSegmentationState', () => { // Tests for getProcessRule describe('getProcessRule', () => { it('should return general process rule', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) const processRule = result.current.getProcessRule(ChunkingMode.text) @@ -809,7 +713,7 @@ describe('useSegmentationState', () => { }) it('should return hierarchical process rule for parent-child', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) const processRule = result.current.getProcessRule(ChunkingMode.parentChild) @@ -832,7 +736,7 @@ describe('useIndexingConfig', () => { // Note: Hook has useEffect that syncs state, so we test the state after effects settle describe('Initial State', () => { it('should initialize with QUALIFIED when API key is set', async () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: true, hasSetIndexType: false }), ) @@ -843,7 +747,7 @@ describe('useIndexingConfig', () => { }) it('should initialize with ECONOMICAL when API key is not set', async () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: false, hasSetIndexType: false }), ) @@ -853,7 +757,7 @@ describe('useIndexingConfig', () => { }) it('should use initial index type when provided', async () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: false, hasSetIndexType: true, @@ -870,7 +774,7 @@ describe('useIndexingConfig', () => { // Tests for state setters describe('State Management', () => { it('should update index type', async () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: true, hasSetIndexType: false }), ) @@ -887,7 +791,7 @@ describe('useIndexingConfig', () => { }) it('should update embedding model', async () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: true, hasSetIndexType: false }), ) @@ -903,7 +807,7 @@ describe('useIndexingConfig', () => { }) it('should update retrieval config', async () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: true, hasSetIndexType: false }), ) @@ -931,7 +835,7 @@ describe('useIndexingConfig', () => { // Tests for getIndexingTechnique describe('getIndexingTechnique', () => { it('should return initial type when set', async () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: true, hasSetIndexType: true, @@ -945,7 +849,7 @@ describe('useIndexingConfig', () => { }) it('should return current type when no initial type', async () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: true, hasSetIndexType: false }), ) @@ -976,7 +880,7 @@ describe('useIndexingConfig', () => { score_threshold: 0.8, } - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingConfig({ isAPIKeySet: true, hasSetIndexType: false, @@ -1012,22 +916,22 @@ describe('usePreviewState', () => { // Tests for initial state describe('Initial State', () => { it('should initialize with first file for FILE data source', () => { - const { result } = renderHook(() => usePreviewState(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => usePreviewState(defaultOptions)) expect(result.current.previewFile).toEqual(defaultOptions.files[0]) }) it('should initialize with first notion page for NOTION data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.NOTION }), ) expect(result.current.previewNotionPage).toEqual(defaultOptions.notionPages[0]) }) - it('should initialize with document detail when provided', () => { + it('should start without a preview file when editing an existing document', () => { const documentDetail = createMockDocumentDetail() - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, documentDetail, @@ -1035,21 +939,23 @@ describe('usePreviewState', () => { }), ) - expect(result.current.previewFile).toEqual(documentDetail.file) + expect(result.current.previewFile).toBeUndefined() }) }) // Tests for getPreviewPickerItems describe('getPreviewPickerItems', () => { it('should return files for FILE data source', () => { - const { result } = renderHook(() => usePreviewState(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => usePreviewState(defaultOptions)) const items = result.current.getPreviewPickerItems() - expect(items).toEqual(defaultOptions.files) + expect(items).toEqual( + defaultOptions.files.map(({ id, name, extension }) => ({ id, name, extension })), + ) }) it('should return mapped notion pages for NOTION data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.NOTION }), ) @@ -1062,7 +968,7 @@ describe('usePreviewState', () => { }) it('should return mapped website pages for WEB data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.WEB }), ) @@ -1075,7 +981,7 @@ describe('usePreviewState', () => { }) it('should return empty array for unknown data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: 'unknown' as DataSourceType }), ) @@ -1087,15 +993,16 @@ describe('usePreviewState', () => { // Tests for getPreviewPickerValue describe('getPreviewPickerValue', () => { it('should return file value for FILE data source', () => { - const { result } = renderHook(() => usePreviewState(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => usePreviewState(defaultOptions)) const value = result.current.getPreviewPickerValue() - expect(value).toEqual(defaultOptions.files[0]) + const [file] = defaultOptions.files + expect(value).toEqual({ id: file!.id, name: file!.name, extension: file!.extension }) }) it('should return mapped notion page value for NOTION data source', () => { const notionPage = createMockNotionPage({ page_id: 'page-123', page_name: 'My Page' }) - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.NOTION, @@ -1116,7 +1023,7 @@ describe('usePreviewState', () => { source_url: 'https://test.com', title: 'Test Title', }) - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.WEB, @@ -1133,7 +1040,7 @@ describe('usePreviewState', () => { }) it('should return empty value for unknown data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: 'unknown' as DataSourceType }), ) @@ -1142,7 +1049,7 @@ describe('usePreviewState', () => { }) it('should handle undefined notion page gracefully', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.NOTION, @@ -1159,7 +1066,7 @@ describe('usePreviewState', () => { }) it('should handle undefined website page gracefully', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.WEB, @@ -1180,7 +1087,9 @@ describe('usePreviewState', () => { describe('handlePreviewChange', () => { it('should update preview file for FILE data source', () => { const files = [createMockFile(), createMockFile({ id: 'file-2', name: 'second.pdf' })] - const { result } = renderHook(() => usePreviewState({ ...defaultOptions, files })) + const { result } = renderHookWithConsoleQuery(() => + usePreviewState({ ...defaultOptions, files }), + ) act(() => { result.current.handlePreviewChange({ id: 'file-2', name: 'second.pdf' }) @@ -1194,7 +1103,7 @@ describe('usePreviewState', () => { createMockNotionPage(), createMockNotionPage({ page_id: 'notion-page-2', page_name: 'Second Page' }), ] - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.NOTION, notionPages }), ) @@ -1210,7 +1119,7 @@ describe('usePreviewState', () => { createMockWebsitePage(), createMockWebsitePage({ source_url: 'https://example.com/page2', title: 'Second Page' }), ] - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ ...defaultOptions, dataSourceType: DataSourceType.WEB, websitePages }), ) @@ -1241,7 +1150,7 @@ describe('useDocumentCreation', () => { // Tests for validateParams describe('validateParams', () => { it('should return false when overlap exceeds max chunk length', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const isValid = result.current.validateParams({ segmentationType: 'general', @@ -1265,7 +1174,7 @@ describe('useDocumentCreation', () => { }) it('should return false when max chunk length exceeds limit', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const isValid = result.current.validateParams({ segmentationType: 'general', @@ -1289,7 +1198,7 @@ describe('useDocumentCreation', () => { }) it('should return true for valid params', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const isValid = result.current.validateParams({ segmentationType: 'general', @@ -1316,7 +1225,7 @@ describe('useDocumentCreation', () => { // Tests for buildCreationParams describe('buildCreationParams', () => { it('should build params for file upload', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const params = result.current.buildCreationParams( ChunkingMode.text, @@ -1342,7 +1251,7 @@ describe('useDocumentCreation', () => { it('should build params for setting mode', () => { const documentDetail = createMockDocumentDetail() - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, isSetting: true, @@ -1370,7 +1279,7 @@ describe('useDocumentCreation', () => { }) it('should build params for notion_import data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, dataSourceType: DataSourceType.NOTION, @@ -1401,7 +1310,7 @@ describe('useDocumentCreation', () => { }) it('should build params for website_crawl data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, dataSourceType: DataSourceType.WEB, @@ -1437,7 +1346,7 @@ describe('useDocumentCreation', () => { // Tests for validateParams edge cases describe('validateParams - additional cases', () => { it('should return false when embedding model is missing for QUALIFIED index type', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const isValid = result.current.validateParams({ segmentationType: 'general', @@ -1461,7 +1370,7 @@ describe('useDocumentCreation', () => { }) it('should return false when rerank model is required but not selected', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) // isReRankModelSelected returns false when: // - indexMethod === 'high_quality' (IndexingType.QUALIFIED) @@ -1501,7 +1410,7 @@ describe('useDocumentCreation', () => { const mockUpdateRetrievalMethodCache = vi.fn() const mockOnSave = vi.fn() - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, datasetId: undefined, @@ -1545,7 +1454,7 @@ describe('useDocumentCreation', () => { it('should call createDocumentMutation when datasetId is provided', async () => { const mockOnStepChange = vi.fn() - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, datasetId: 'existing-dataset-id', @@ -1586,7 +1495,7 @@ describe('useDocumentCreation', () => { it('should call onSave when in setting mode', async () => { const mockOnSave = vi.fn() const documentDetail = createMockDocumentDetail() - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, datasetId: 'existing-dataset-id', @@ -1630,14 +1539,14 @@ describe('useDocumentCreation', () => { // Tests for validatePreviewParams describe('validatePreviewParams', () => { it('should return true for valid max chunk length', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const isValid = result.current.validatePreviewParams(1000) expect(isValid).toBe(true) }) it('should return false when max chunk length exceeds maximum', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const isValid = result.current.validatePreviewParams(10000) expect(isValid).toBe(false) @@ -1667,7 +1576,7 @@ describe('useIndexingEstimate', () => { // Tests for initial state describe('Initial State', () => { it('should initialize with idle state', () => { - const { result } = renderHook(() => useIndexingEstimate(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useIndexingEstimate(defaultOptions)) expect(result.current.isIdle).toBe(true) expect(result.current.isPending).toBe(false) @@ -1678,7 +1587,7 @@ describe('useIndexingEstimate', () => { // Tests for fetchEstimate describe('fetchEstimate', () => { it('should call fetchEstimate for FILE data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingEstimate({ ...defaultOptions, dataSourceType: DataSourceType.FILE, @@ -1695,7 +1604,7 @@ describe('useIndexingEstimate', () => { }) it('should call fetchEstimate for NOTION data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingEstimate({ ...defaultOptions, dataSourceType: DataSourceType.NOTION, @@ -1712,7 +1621,7 @@ describe('useIndexingEstimate', () => { }) it('should call fetchEstimate for WEB data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingEstimate({ ...defaultOptions, dataSourceType: DataSourceType.WEB, @@ -1734,7 +1643,7 @@ describe('useIndexingEstimate', () => { // Tests for getCurrentMutation based on data source type describe('Data Source Selection', () => { it('should use file query for FILE data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingEstimate({ ...defaultOptions, dataSourceType: DataSourceType.FILE, @@ -1746,7 +1655,7 @@ describe('useIndexingEstimate', () => { }) it('should use notion query for NOTION data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingEstimate({ ...defaultOptions, dataSourceType: DataSourceType.NOTION, @@ -1758,7 +1667,7 @@ describe('useIndexingEstimate', () => { }) it('should use website query for WEB data source', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useIndexingEstimate({ ...defaultOptions, dataSourceType: DataSourceType.WEB, @@ -2100,7 +2009,7 @@ describe('Edge Cases', () => { describe('Empty/Null Values', () => { it('should handle empty files array in usePreviewState', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ dataSourceType: DataSourceType.FILE, files: [], @@ -2113,7 +2022,7 @@ describe('Edge Cases', () => { }) it('should handle empty notion pages array', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ dataSourceType: DataSourceType.NOTION, files: [], @@ -2126,7 +2035,7 @@ describe('Edge Cases', () => { }) it('should handle empty website pages array', () => { - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ dataSourceType: DataSourceType.WEB, files: [], @@ -2141,7 +2050,7 @@ describe('Edge Cases', () => { describe('Boundary Conditions', () => { it('should handle very large chunk length', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setMaxChunkLength(999999) @@ -2151,7 +2060,7 @@ describe('Edge Cases', () => { }) it('should handle zero overlap', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setOverlap(0) @@ -2161,7 +2070,7 @@ describe('Edge Cases', () => { }) it('should handle special characters in segment identifier', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setSegmentIdentifier('<<>>') @@ -2173,7 +2082,7 @@ describe('Edge Cases', () => { describe('Callback Stability', () => { it('should maintain stable setSegmentIdentifier reference', () => { - const { result, rerender } = renderHook(() => useSegmentationState()) + const { result, rerender } = renderHookWithConsoleQuery(() => useSegmentationState()) const initialSetter = result.current.setSegmentIdentifier rerender() @@ -2182,7 +2091,7 @@ describe('Edge Cases', () => { }) it('should maintain stable toggleRule reference', () => { - const { result, rerender } = renderHook(() => useSegmentationState()) + const { result, rerender } = renderHookWithConsoleQuery(() => useSegmentationState()) const initialToggle = result.current.toggleRule rerender() @@ -2191,7 +2100,7 @@ describe('Edge Cases', () => { }) it('should maintain stable getProcessRule reference', () => { - const { result, rerender } = renderHook(() => useSegmentationState()) + const { result, rerender } = renderHookWithConsoleQuery(() => useSegmentationState()) // Update some state to trigger re-render act(() => { @@ -2218,8 +2127,8 @@ describe('Integration Scenarios', () => { it('should build and validate params for file upload workflow', () => { const files = [createMockFile()] - const { result: segResult } = renderHook(() => useSegmentationState()) - const { result: creationResult } = renderHook(() => + const { result: segResult } = renderHookWithConsoleQuery(() => useSegmentationState()) + const { result: creationResult } = renderHookWithConsoleQuery(() => useDocumentCreation({ dataSourceType: DataSourceType.FILE, files, @@ -2251,7 +2160,7 @@ describe('Integration Scenarios', () => { }) it('should handle parent-child document form', () => { - const { result } = renderHook(() => useSegmentationState()) + const { result } = renderHookWithConsoleQuery(() => useSegmentationState()) act(() => { result.current.setSegmentationType(ProcessMode.parentChild) @@ -2276,7 +2185,7 @@ describe('Integration Scenarios', () => { createMockFile({ id: 'file-2', name: 'second.pdf' }), ] - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => usePreviewState({ dataSourceType: DataSourceType.FILE, files, @@ -2286,7 +2195,7 @@ describe('Integration Scenarios', () => { ) // Initial state - expect(result.current.getPreviewPickerValue().name).toBe('first.pdf') + expect(result.current.getPreviewPickerValue()?.name).toBe('first.pdf') // Change preview act(() => { @@ -2414,7 +2323,7 @@ describe('StepTwo Component', () => { describe('Initialization', () => { it('should fetch default process rule when not in setting mode', () => { render() - expect(mockFetchDefaultProcessRuleMutate).toHaveBeenCalledWith('/datasets/process-rule') + expect(mockFetchDefaultProcessRuleMutate).toHaveBeenCalled() }) it('should apply config from rules when in setting mode with document detail', () => { @@ -2722,17 +2631,49 @@ describe('StepTwo Component', () => { }) vi.mock('@tanstack/react-query', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useQuery: (options: { - queryKey: OperationKey< - 'query', - { params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] } - > - }) => { - if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options) - return { data: mockEmbeddingModelList } - }, - } + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + const createdDocuments = (data: unknown) => + vi.fn(async (_input: unknown, options?: { onSuccess?: (data: unknown) => void }) => { + options?.onSuccess?.(data) + return data + }) + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.models.modelTypes.byModelType.get': () => ({ + data: mockEmbeddingModelList, + }), + 'datasets.processRule.get': (options: { onSuccess?: (data: unknown) => void }) => ({ + mutate: (input: unknown) => { + mockFetchDefaultProcessRuleMutate(input) + options.onSuccess?.({ + mode: 'custom', + rules: { + segmentation: { separator: '\\n', max_tokens: 500, chunk_overlap: 50 }, + pre_processing_rules: [ + { id: 'remove_extra_spaces', enabled: true }, + { id: 'remove_urls_emails', enabled: false }, + ], + parent_mode: 'paragraph', + subchunk_segmentation: { separator: '\\n', max_tokens: 256 }, + }, + limits: { indexing_max_segmentation_tokens_length: 4000 }, + }) + }, + isPending: false, + }), + 'datasets.indexingEstimate.post': () => ({ + mutate: vi.fn(), + data: undefined, + isIdle: true, + isPending: false, + reset: mockResetFileEstimate, + }), + 'datasets.init.post': () => ({ + mutateAsync: createdDocuments({ dataset: { id: 'new-dataset-id' } }), + isPending: false, + }), + 'datasets.byDatasetId.documents.post': () => ({ + mutateAsync: createdDocuments({ document: { id: 'new-doc-id' } }), + isPending: false, + }), + }) }) diff --git a/web/app/components/datasets/create/step-two/__tests__/process-rule.spec.ts b/web/app/components/datasets/create/step-two/__tests__/process-rule.spec.ts new file mode 100644 index 00000000000000..5eb9a4a63ae546 --- /dev/null +++ b/web/app/components/datasets/create/step-two/__tests__/process-rule.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vite-plus/test' +import { isProcessMode, toSegmentationRules } from '../process-rule' + +describe('toSegmentationRules', () => { + it('should restore stored segmentation with the backend defaults', () => { + expect( + toSegmentationRules({ + pre_processing_rules: [ + { id: 'remove_extra_spaces', enabled: true }, + { id: 'legacy_rule', enabled: true }, + ], + segmentation: { max_tokens: 500 }, + parent_mode: 'paragraph', + subchunk_segmentation: { separator: '\n', max_tokens: 200, chunk_overlap: 10 }, + }), + ).toEqual({ + pre_processing_rules: [{ id: 'remove_extra_spaces', enabled: true }], + segmentation: { separator: '\n', max_tokens: 500, chunk_overlap: 0 }, + parent_mode: 'paragraph', + subchunk_segmentation: { separator: '\n', max_tokens: 200, chunk_overlap: 10 }, + }) + }) + + it('should restore nothing for automatic rules without segmentation', () => { + expect(toSegmentationRules({ pre_processing_rules: null })).toBeUndefined() + }) +}) + +describe('isProcessMode', () => { + it('should accept only the editable process modes', () => { + expect(isProcessMode('custom')).toBe(true) + expect(isProcessMode('hierarchical')).toBe(true) + expect(isProcessMode('automatic')).toBe(false) + }) +}) diff --git a/web/app/components/datasets/create/step-two/components/preview-panel.tsx b/web/app/components/datasets/create/step-two/components/preview-panel.tsx index 36a95eee36c58d..1c68ce6d3d0dc3 100644 --- a/web/app/components/datasets/create/step-two/components/preview-panel.tsx +++ b/web/app/components/datasets/create/step-two/components/preview-panel.tsx @@ -36,7 +36,7 @@ type PreviewPanelProps = { isSetting?: boolean // Picker pickerFiles: Array<{ id: string; name: string; extension: string }> - pickerValue: { id: string; name: string; extension: string } + pickerValue?: { id: string; name: string; extension: string } // Mutation state isIdle: boolean isPending: boolean @@ -148,8 +148,8 @@ export const PreviewPanel: FC = ({ const indexForLabel = index + 1 const childChunks = parentChildConfig.chunkForContext === 'full-doc' - ? item.child_chunks.slice(0, FULL_DOC_PREVIEW_LENGTH) - : item.child_chunks + ? (item.child_chunks ?? []).slice(0, FULL_DOC_PREVIEW_LENGTH) + : (item.child_chunks ?? []) return ( { + const workspacesMap = groupBy(notionPages, 'workspace_id') + const workspaces = Object.keys(workspacesMap).map((workspaceId) => { + return { + workspaceId, + pages: workspacesMap[workspaceId], + } + }) + return workspaces.map((workspace) => { + return { + credential_id: credentialId, + workspace_id: workspace.workspaceId, + pages: workspace.pages!.map((page) => { + const { page_id, page_name, page_icon, type } = page + return { + page_id, + page_name, + // The backend requires an icon type; an icon without one carries nothing to render. + page_icon: page_icon?.type ? { ...page_icon, type: page_icon.type } : null, + type, + } + }), + } + }) +} + +export const getWebsiteInfo = (opts: { + websiteCrawlProvider: DataSourceProvider + websiteCrawlJobId: string + websitePages: CrawlResultItem[] + crawlOptions?: CrawlOptions +}) => { + const { websiteCrawlProvider, websiteCrawlJobId, websitePages, crawlOptions } = opts + return { + provider: websiteCrawlProvider, + job_id: websiteCrawlJobId, + urls: websitePages.map((page) => page.source_url), + only_main_content: crawlOptions?.only_main_content, + } +} + +type GetFileIndexingEstimateParamsOptionBase = { + docForm: ChunkingMode + docLanguage: string + indexingTechnique: IndexingType + processRule: ProcessRule + dataset_id: string +} + +type GetFileIndexingEstimateParamsOptionFile = GetFileIndexingEstimateParamsOptionBase & { + dataSourceType: typeof DataSourceType.FILE + files: StepTwoFile[] +} + +export const getFileIndexingEstimateParamsForFile = ({ + docForm, + docLanguage, + dataSourceType, + files, + indexingTechnique, + processRule, + dataset_id, +}: GetFileIndexingEstimateParamsOptionFile): IndexingEstimatePayload => { + const fileIds = files.map((file) => file.id).filter((id): id is string => Boolean(id)) + + return { + info_list: { + data_source_type: dataSourceType, + file_info_list: { + file_ids: fileIds, + }, + }, + indexing_technique: indexingTechnique, + process_rule: processRule, + doc_form: docForm, + doc_language: docLanguage, + dataset_id, + } +} + +type GetFileIndexingEstimateParamsOptionNotion = GetFileIndexingEstimateParamsOptionBase & { + dataSourceType: typeof DataSourceType.NOTION + notionPages: StepTwoNotionPage[] + credential_id: string +} + +export const getFileIndexingEstimateParamsForNotion = ({ + docForm, + docLanguage, + dataSourceType, + notionPages, + indexingTechnique, + processRule, + dataset_id, + credential_id, +}: GetFileIndexingEstimateParamsOptionNotion): IndexingEstimatePayload => { + return { + info_list: { + data_source_type: dataSourceType, + notion_info_list: getNotionInfo(notionPages, credential_id), + }, + indexing_technique: indexingTechnique, + process_rule: processRule, + doc_form: docForm, + doc_language: docLanguage, + dataset_id, + } +} + +type GetFileIndexingEstimateParamsOptionWeb = GetFileIndexingEstimateParamsOptionBase & { + dataSourceType: typeof DataSourceType.WEB + websitePages: CrawlResultItem[] + crawlOptions?: CrawlOptions + websiteCrawlProvider: DataSourceProvider + websiteCrawlJobId: string +} + +export const getFileIndexingEstimateParamsForWeb = ({ + docForm, + docLanguage, + dataSourceType, + websitePages, + crawlOptions, + websiteCrawlProvider, + websiteCrawlJobId, + indexingTechnique, + processRule, + dataset_id, +}: GetFileIndexingEstimateParamsOptionWeb): IndexingEstimatePayload => { + return { + info_list: { + data_source_type: dataSourceType, + website_info_list: getWebsiteInfo({ + websiteCrawlProvider, + websiteCrawlJobId, + websitePages, + crawlOptions, + }), + }, + indexing_technique: indexingTechnique, + process_rule: processRule, + doc_form: docForm, + doc_language: docLanguage, + dataset_id, + } +} diff --git a/web/app/components/datasets/create/step-two/hooks/__tests__/use-document-creation.spec.ts b/web/app/components/datasets/create/step-two/hooks/__tests__/use-document-creation.spec.ts index f2065bc673ac64..8875b1950acb30 100644 --- a/web/app/components/datasets/create/step-two/hooks/__tests__/use-document-creation.spec.ts +++ b/web/app/components/datasets/create/step-two/hooks/__tests__/use-document-creation.spec.ts @@ -5,11 +5,16 @@ import type { ProcessRule, } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' -import { renderHook } from '@testing-library/react' +import { QueryClient } from '@tanstack/react-query' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { ChunkingMode, DataSourceType } from '@/models/datasets' +import { consoleQuery } from '@/service/console' +import { renderHookWithConsoleQuery } from '@/test/console/query-data' import { RETRIEVE_METHOD } from '@/types/app' +const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries') +const datasetListKey = consoleQuery.datasets.get.key() + // Hoisted mocks const mocks = vi.hoisted(() => ({ toastNotify: vi.fn(), @@ -37,16 +42,16 @@ vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({ isReRankModelSelected: mocks.isReRankModelSelected, })) -vi.mock('@/service/knowledge/use-create-dataset', () => ({ - useCreateFirstDocument: () => ({ mutateAsync: mocks.mutateAsync, isPending: false }), - useCreateDocument: () => ({ mutateAsync: mocks.mutateAsync, isPending: false }), - getNotionInfo: vi.fn(() => []), - getWebsiteInfo: vi.fn(() => ({})), -})) - -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => mocks.invalidDatasetList, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.init.post': () => ({ mutateAsync: mocks.mutateAsync, isPending: false }), + 'datasets.byDatasetId.documents.post': () => ({ + mutateAsync: mocks.mutateAsync, + isPending: false, + }), + }) +}) const { useDocumentCreation } = await import('../use-document-creation') const { IndexingType } = await import('../use-indexing-config') @@ -85,19 +90,19 @@ describe('useDocumentCreation', () => { describe('validateParams', () => { it('should return true for valid params', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) expect(result.current.validateParams(defaultValidationParams)).toBe(true) }) it('should return false when overlap > maxChunkLength', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const invalid = { ...defaultValidationParams, overlap: 2000, maxChunkLength: 1000 } expect(result.current.validateParams(invalid)).toBe(false) expect(mocks.toastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })) }) it('should return false when maxChunkLength > limitMaxChunkLength', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const invalid = { ...defaultValidationParams, maxChunkLength: 5000, @@ -107,7 +112,7 @@ describe('useDocumentCreation', () => { }) it('should return false when qualified but no embedding model', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const invalid = { ...defaultValidationParams, indexType: IndexingType.QUALIFIED, @@ -118,13 +123,13 @@ describe('useDocumentCreation', () => { it('should return false when rerank model not selected', () => { mocks.isReRankModelSelected.mockReturnValue(false) - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) expect(result.current.validateParams(defaultValidationParams)).toBe(false) }) it('should skip embedding/rerank checks when isSetting is true', () => { mocks.isReRankModelSelected.mockReturnValue(false) - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, isSetting: true }), ) const params = { @@ -137,7 +142,7 @@ describe('useDocumentCreation', () => { describe('buildCreationParams', () => { it('should build params for FILE data source', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) const processRule = { mode: 'custom', rules: {} } as unknown as ProcessRule const retrievalConfig = defaultValidationParams.retrievalConfig const embeddingModel = { provider: 'openai', model: 'text-embedding-3-small' } @@ -160,7 +165,7 @@ describe('useDocumentCreation', () => { it('should build params for isSetting mode', () => { const detail = { id: 'doc-1' } as FullDocumentDetail - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, isSetting: true, documentDetail: detail }), ) const params = result.current.buildCreationParams( @@ -179,12 +184,12 @@ describe('useDocumentCreation', () => { describe('validatePreviewParams', () => { it('should return true when maxChunkLength is within limit', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) expect(result.current.validatePreviewParams(1024)).toBe(true) }) it('should return false when maxChunkLength exceeds limit', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) expect(result.current.validatePreviewParams(999999)).toBe(false) expect(mocks.toastNotify).toHaveBeenCalled() }) @@ -193,7 +198,7 @@ describe('useDocumentCreation', () => { describe('executeCreation', () => { it('should not create document when existing dataset cannot add documents', async () => { const onStepChange = vi.fn() - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, datasetId: 'dataset-1', @@ -209,13 +214,13 @@ describe('useDocumentCreation', () => { ) expect(mocks.mutateAsync).not.toHaveBeenCalled() - expect(mocks.invalidDatasetList).not.toHaveBeenCalled() + expect(invalidateQueries).not.toHaveBeenCalledWith({ queryKey: datasetListKey }) expect(onStepChange).not.toHaveBeenCalled() }) it('should allow setting saves even when document creation permission is false', async () => { const onSave = vi.fn() - const { result } = renderHook(() => + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation({ ...defaultOptions, datasetId: 'dataset-1', @@ -238,7 +243,7 @@ describe('useDocumentCreation', () => { describe('isCreating', () => { it('should reflect mutation pending state', () => { - const { result } = renderHook(() => useDocumentCreation(defaultOptions)) + const { result } = renderHookWithConsoleQuery(() => useDocumentCreation(defaultOptions)) expect(result.current.isCreating).toBe(false) }) }) diff --git a/web/app/components/datasets/create/step-two/hooks/__tests__/use-indexing-estimate.spec.ts b/web/app/components/datasets/create/step-two/hooks/__tests__/use-indexing-estimate.spec.ts index a59d9297c19458..b32f0ec4e891b5 100644 --- a/web/app/components/datasets/create/step-two/hooks/__tests__/use-indexing-estimate.spec.ts +++ b/web/app/components/datasets/create/step-two/hooks/__tests__/use-indexing-estimate.spec.ts @@ -3,115 +3,110 @@ import type { NotionPage } from '@/models/common' import type { ChunkingMode, CrawlResultItem, CustomFile, ProcessRule } from '@/models/datasets' import { renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { DataSourceProvider } from '@/models/common' import { DataSourceType } from '@/models/datasets' -// Hoisted mocks const mocks = vi.hoisted(() => ({ - fileMutate: vi.fn(), - fileReset: vi.fn(), - notionMutate: vi.fn(), - notionReset: vi.fn(), - webMutate: vi.fn(), - webReset: vi.fn(), + mutate: vi.fn(), + reset: vi.fn(), })) -vi.mock('@/service/knowledge/use-create-dataset', () => ({ - useFetchFileIndexingEstimateForFile: () => ({ - mutate: mocks.fileMutate, - reset: mocks.fileReset, - data: { tokens: 100, total_segments: 5 }, - isIdle: true, - isPending: false, - }), - useFetchFileIndexingEstimateForNotion: () => ({ - mutate: mocks.notionMutate, - reset: mocks.notionReset, - data: null, - isIdle: true, - isPending: false, - }), - useFetchFileIndexingEstimateForWeb: () => ({ - mutate: mocks.webMutate, - reset: mocks.webReset, - data: null, - isIdle: true, - isPending: false, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.indexingEstimate.post': () => ({ + mutate: mocks.mutate, + reset: mocks.reset, + data: { tokens: 100, total_segments: 5 }, + isIdle: true, + isPending: false, + }), + }) +}) const { useIndexingEstimate } = await import('../use-indexing-estimate') describe('useIndexingEstimate', () => { const defaultOptions = { dataSourceType: DataSourceType.FILE, + datasetId: 'dataset-1', currentDocForm: 'text_model' as ChunkingMode, docLanguage: 'English', files: [{ id: 'f-1', name: 'test.txt' }] as unknown as CustomFile[], - previewNotionPage: {} as unknown as NotionPage, - notionCredentialId: '', - previewWebsitePage: {} as unknown as CrawlResultItem, + previewNotionPage: { + page_id: 'page-1', + page_name: 'Page', + page_icon: null, + type: 'page', + workspace_id: 'workspace-1', + } as unknown as NotionPage, + notionCredentialId: 'credential-1', + previewWebsitePage: { source_url: 'https://example.com' } as unknown as CrawlResultItem, + websiteCrawlProvider: DataSourceProvider.jinaReader, + websiteCrawlJobId: 'job-1', indexingTechnique: 'high_quality' as unknown as IndexingType, processRule: { mode: 'custom', rules: {} } as unknown as ProcessRule, } + const sentBody = () => mocks.mutate.mock.calls.at(-1)?.[0].body + beforeEach(() => { vi.clearAllMocks() }) - describe('currentMutation selection', () => { - it('should select file mutation for FILE type', () => { - const { result } = renderHook(() => useIndexingEstimate(defaultOptions)) - expect(result.current.estimate).toEqual({ tokens: 100, total_segments: 5 }) - }) - - it('should select notion mutation for NOTION type', () => { - const { result } = renderHook(() => - useIndexingEstimate({ - ...defaultOptions, - dataSourceType: DataSourceType.NOTION, - }), - ) - expect(result.current.estimate).toBeNull() - }) - - it('should select web mutation for WEB type', () => { - const { result } = renderHook(() => - useIndexingEstimate({ - ...defaultOptions, - dataSourceType: DataSourceType.WEB, - }), - ) - expect(result.current.estimate).toBeNull() - }) + it('exposes the estimate', () => { + const { result } = renderHook(() => useIndexingEstimate(defaultOptions)) + expect(result.current.estimate).toEqual({ tokens: 100, total_segments: 5 }) }) describe('fetchEstimate', () => { - it('should call file mutate for FILE type', () => { + it('estimates the uploaded files', () => { const { result } = renderHook(() => useIndexingEstimate(defaultOptions)) result.current.fetchEstimate() - expect(mocks.fileMutate).toHaveBeenCalledOnce() + + expect(sentBody()).toMatchObject({ + info_list: { data_source_type: 'upload_file', file_info_list: { file_ids: ['f-1'] } }, + doc_form: 'text_model', + dataset_id: 'dataset-1', + }) }) - it('should call notion mutate for NOTION type', () => { + it('estimates the previewed Notion page', () => { const { result } = renderHook(() => - useIndexingEstimate({ - ...defaultOptions, - dataSourceType: DataSourceType.NOTION, - }), + useIndexingEstimate({ ...defaultOptions, dataSourceType: DataSourceType.NOTION }), ) result.current.fetchEstimate() - expect(mocks.notionMutate).toHaveBeenCalledOnce() + + expect(sentBody()).toMatchObject({ + info_list: { + data_source_type: 'notion_import', + notion_info_list: [ + { + credential_id: 'credential-1', + workspace_id: 'workspace-1', + pages: [{ page_id: 'page-1' }], + }, + ], + }, + }) }) - it('should call web mutate for WEB type', () => { + it('estimates the previewed website page', () => { const { result } = renderHook(() => - useIndexingEstimate({ - ...defaultOptions, - dataSourceType: DataSourceType.WEB, - }), + useIndexingEstimate({ ...defaultOptions, dataSourceType: DataSourceType.WEB }), ) result.current.fetchEstimate() - expect(mocks.webMutate).toHaveBeenCalledOnce() + + expect(sentBody()).toMatchObject({ + info_list: { + data_source_type: 'website_crawl', + website_info_list: { + provider: DataSourceProvider.jinaReader, + job_id: 'job-1', + urls: ['https://example.com'], + }, + }, + }) }) }) @@ -129,7 +124,7 @@ describe('useIndexingEstimate', () => { it('should expose reset function', () => { const { result } = renderHook(() => useIndexingEstimate(defaultOptions)) result.current.reset() - expect(mocks.fileReset).toHaveBeenCalledOnce() + expect(mocks.reset).toHaveBeenCalledOnce() }) }) }) diff --git a/web/app/components/datasets/create/step-two/hooks/__tests__/use-preview-state.spec.ts b/web/app/components/datasets/create/step-two/hooks/__tests__/use-preview-state.spec.ts index ba322945ac29a0..a05b167a271353 100644 --- a/web/app/components/datasets/create/step-two/hooks/__tests__/use-preview-state.spec.ts +++ b/web/app/components/datasets/create/step-two/hooks/__tests__/use-preview-state.spec.ts @@ -143,7 +143,7 @@ describe('usePreviewState', () => { }), ) const value = result.current.getPreviewPickerValue() - expect(value).toBe(files[0]) + expect(value).toEqual({ id: 'f-1', name: 'file1.txt', extension: 'txt' }) }) it('should return mapped notion page value for NOTION type', () => { diff --git a/web/app/components/datasets/create/step-two/hooks/use-document-creation.ts b/web/app/components/datasets/create/step-two/hooks/use-document-creation.ts index 841ce841dd2b2f..4fc150fbaefd42 100644 --- a/web/app/components/datasets/create/step-two/hooks/use-document-creation.ts +++ b/web/app/components/datasets/create/step-two/hooks/use-document-creation.ts @@ -1,18 +1,18 @@ import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen' +import type { StepTwoFile, StepTwoNotionPage } from '../types' import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations' -import type { NotionPage } from '@/models/common' import type { ChunkingMode, CrawlOptions, CrawlResultItem, CreateDocumentReq, createDocumentResponse, - CustomFile, FullDocumentDetail, ProcessRule, SummaryIndexSetting as SummaryIndexSettingType, } from '@/models/datasets' import type { RetrievalConfig, RETRIEVE_METHOD } from '@/types/app' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' @@ -20,13 +20,8 @@ import { isReRankModelSelected } from '@/app/components/datasets/common/check-re import { toast } from '@/app/notifications' import { DataSourceProvider } from '@/models/common' import { DataSourceType } from '@/models/datasets' -import { - getNotionInfo, - getWebsiteInfo, - useCreateDocument, - useCreateFirstDocument, -} from '@/service/knowledge/use-create-dataset' -import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' +import { getNotionInfo, getWebsiteInfo } from '../creation-params' import { IndexingType } from './use-indexing-config' import { MAXIMUM_CHUNK_TOKEN_LENGTH } from './use-segmentation-state' @@ -35,8 +30,8 @@ type UseDocumentCreationOptions = { isSetting?: boolean documentDetail?: FullDocumentDetail dataSourceType: DataSourceType - files: CustomFile[] - notionPages: NotionPage[] + files: StepTwoFile[] + notionPages: StepTwoNotionPage[] notionCredentialId: string websitePages: CrawlResultItem[] crawlOptions?: CrawlOptions @@ -83,10 +78,17 @@ export const useDocumentCreation = (options: UseDocumentCreationOptions) => { onSave, mutateDatasetRes, } = options - const createFirstDocumentMutation = useCreateFirstDocument() - const createDocumentMutation = useCreateDocument(datasetId!) - const invalidDatasetList = useInvalidDatasetList() - const isCreating = createFirstDocumentMutation.isPending || createDocumentMutation.isPending + const { mutateAsync: createFirstDocument, isPending: isCreatingFirstDocument } = useMutation( + consoleQuery.datasets.init.post.mutationOptions(), + ) + const { mutateAsync: createDocument, isPending: isCreatingDocument } = useMutation( + consoleQuery.datasets.byDatasetId.documents.post.mutationOptions(), + ) + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) + const isCreating = isCreatingFirstDocument || isCreatingDocument // Validate creation params const validateParams = useCallback( (params: ValidationParams): boolean => { @@ -220,21 +222,27 @@ export const useDocumentCreation = (options: UseDocumentCreationOptions) => { if (datasetId && !isSetting && !canCreateDocument) return if (!datasetId) { - await createFirstDocumentMutation.mutateAsync(params, { - onSuccess(data) { - updateIndexingTypeCache?.(indexType) - updateResultCache?.(data) - updateRetrievalMethodCache?.(retrievalConfig.search_method as RETRIEVE_METHOD) + await createFirstDocument( + { body: params }, + { + onSuccess(data) { + updateIndexingTypeCache?.(indexType) + updateResultCache?.(data) + updateRetrievalMethodCache?.(retrievalConfig.search_method as RETRIEVE_METHOD) + }, }, - }) + ) } else { - await createDocumentMutation.mutateAsync(params, { - onSuccess(data) { - updateIndexingTypeCache?.(indexType) - updateResultCache?.(data) - updateRetrievalMethodCache?.(retrievalConfig.search_method as RETRIEVE_METHOD) + await createDocument( + { params: { dataset_id: datasetId }, body: params }, + { + onSuccess(data) { + updateIndexingTypeCache?.(indexType) + updateResultCache?.(data) + updateRetrievalMethodCache?.(retrievalConfig.search_method as RETRIEVE_METHOD) + }, }, - }) + ) } mutateDatasetRes?.() invalidDatasetList() @@ -247,8 +255,8 @@ export const useDocumentCreation = (options: UseDocumentCreationOptions) => { }, [ datasetId, - createFirstDocumentMutation, - createDocumentMutation, + createFirstDocument, + createDocument, updateIndexingTypeCache, updateResultCache, updateRetrievalMethodCache, diff --git a/web/app/components/datasets/create/step-two/hooks/use-indexing-estimate.ts b/web/app/components/datasets/create/step-two/hooks/use-indexing-estimate.ts index f2f9ea6089e11d..013079e24c8458 100644 --- a/web/app/components/datasets/create/step-two/hooks/use-indexing-estimate.ts +++ b/web/app/components/datasets/create/step-two/hooks/use-indexing-estimate.ts @@ -1,20 +1,16 @@ +import type { StepTwoFile, StepTwoNotionPage } from '../types' import type { IndexingType } from './use-indexing-config' -import type { NotionPage } from '@/models/common' -import type { - ChunkingMode, - CrawlOptions, - CrawlResultItem, - CustomFile, - ProcessRule, -} from '@/models/datasets' +import type { ChunkingMode, CrawlOptions, CrawlResultItem, ProcessRule } from '@/models/datasets' +import { useMutation } from '@tanstack/react-query' import { useCallback } from 'react' import { DataSourceProvider } from '@/models/common' import { DataSourceType } from '@/models/datasets' +import { consoleQuery } from '@/service/console' import { - useFetchFileIndexingEstimateForFile, - useFetchFileIndexingEstimateForNotion, - useFetchFileIndexingEstimateForWeb, -} from '@/service/knowledge/use-create-dataset' + getFileIndexingEstimateParamsForFile, + getFileIndexingEstimateParamsForNotion, + getFileIndexingEstimateParamsForWeb, +} from '../creation-params' type UseIndexingEstimateOptions = { dataSourceType: DataSourceType @@ -23,13 +19,13 @@ type UseIndexingEstimateOptions = { currentDocForm: ChunkingMode docLanguage: string // File data source - files: CustomFile[] + files: StepTwoFile[] previewFileName?: string // Notion data source - previewNotionPage: NotionPage + previewNotionPage?: StepTwoNotionPage notionCredentialId: string // Website data source - previewWebsitePage: CrawlResultItem + previewWebsitePage?: CrawlResultItem crawlOptions?: CrawlOptions websiteCrawlProvider?: DataSourceProvider websiteCrawlJobId?: string @@ -56,65 +52,75 @@ export const useIndexingEstimate = (options: UseIndexingEstimateOptions) => { processRule, } = options - // File indexing estimate - const fileQuery = useFetchFileIndexingEstimateForFile({ - docForm: currentDocForm, - docLanguage, - dataSourceType: DataSourceType.FILE, - files: previewFileName ? [files.find((file) => file.name === previewFileName)!] : files, - indexingTechnique, - processRule, - dataset_id: datasetId!, - }) - - // Notion indexing estimate - const notionQuery = useFetchFileIndexingEstimateForNotion({ - docForm: currentDocForm, - docLanguage, - dataSourceType: DataSourceType.NOTION, - notionPages: [previewNotionPage], - indexingTechnique, - processRule, - dataset_id: datasetId || '', - credential_id: notionCredentialId, - }) + // Step two keeps one data source, so one estimate request serves it. + const estimateMutation = useMutation( + consoleQuery.datasets.indexingEstimate.post.mutationOptions(), + ) - // Website indexing estimate - const websiteQuery = useFetchFileIndexingEstimateForWeb({ - docForm: currentDocForm, + const getEstimateParams = useCallback(() => { + if (dataSourceType === DataSourceType.FILE) { + return getFileIndexingEstimateParamsForFile({ + docForm: currentDocForm, + docLanguage, + dataSourceType: DataSourceType.FILE, + files: previewFileName ? [files.find((file) => file.name === previewFileName)!] : files, + indexingTechnique, + processRule, + dataset_id: datasetId!, + }) + } + if (dataSourceType === DataSourceType.NOTION) { + return getFileIndexingEstimateParamsForNotion({ + docForm: currentDocForm, + docLanguage, + dataSourceType: DataSourceType.NOTION, + notionPages: previewNotionPage ? [previewNotionPage] : [], + indexingTechnique, + processRule, + dataset_id: datasetId || '', + credential_id: notionCredentialId, + }) + } + return getFileIndexingEstimateParamsForWeb({ + docForm: currentDocForm, + docLanguage, + dataSourceType: DataSourceType.WEB, + websitePages: previewWebsitePage ? [previewWebsitePage] : [], + crawlOptions, + websiteCrawlProvider: websiteCrawlProvider ?? DataSourceProvider.jinaReader, + websiteCrawlJobId: websiteCrawlJobId ?? '', + indexingTechnique, + processRule, + dataset_id: datasetId || '', + }) + }, [ + dataSourceType, + currentDocForm, docLanguage, - dataSourceType: DataSourceType.WEB, - websitePages: [previewWebsitePage], + files, + previewFileName, + previewNotionPage, + notionCredentialId, + previewWebsitePage, crawlOptions, - websiteCrawlProvider: websiteCrawlProvider ?? DataSourceProvider.jinaReader, - websiteCrawlJobId: websiteCrawlJobId ?? '', + websiteCrawlProvider, + websiteCrawlJobId, indexingTechnique, processRule, - dataset_id: datasetId || '', - }) - - // Get current mutation based on data source type - const getCurrentMutation = useCallback(() => { - if (dataSourceType === DataSourceType.FILE) return fileQuery - if (dataSourceType === DataSourceType.NOTION) return notionQuery - return websiteQuery - }, [dataSourceType, fileQuery, notionQuery, websiteQuery]) - - const currentMutation = getCurrentMutation() + datasetId, + ]) - // Trigger estimate fetch + const { mutate } = estimateMutation const fetchEstimate = useCallback(() => { - if (dataSourceType === DataSourceType.FILE) fileQuery.mutate() - else if (dataSourceType === DataSourceType.NOTION) notionQuery.mutate() - else websiteQuery.mutate() - }, [dataSourceType, fileQuery, notionQuery, websiteQuery]) + mutate({ body: getEstimateParams() }) + }, [mutate, getEstimateParams]) return { - currentMutation, - estimate: currentMutation.data, - isIdle: currentMutation.isIdle, - isPending: currentMutation.isPending, + currentMutation: estimateMutation, + estimate: estimateMutation.data, + isIdle: estimateMutation.isIdle, + isPending: estimateMutation.isPending, fetchEstimate, - reset: currentMutation.reset, + reset: estimateMutation.reset, } } diff --git a/web/app/components/datasets/create/step-two/hooks/use-preview-state.ts b/web/app/components/datasets/create/step-two/hooks/use-preview-state.ts index f7eda67405a20d..41cefaf60c7420 100644 --- a/web/app/components/datasets/create/step-two/hooks/use-preview-state.ts +++ b/web/app/components/datasets/create/step-two/hooks/use-preview-state.ts @@ -1,17 +1,12 @@ -import type { NotionPage } from '@/models/common' -import type { - CrawlResultItem, - CustomFile, - DocumentItem, - FullDocumentDetail, -} from '@/models/datasets' +import type { StepTwoFile, StepTwoNotionPage } from '../types' +import type { CrawlResultItem, DocumentItem, FullDocumentDetail } from '@/models/datasets' import { useCallback, useState } from 'react' import { DataSourceType } from '@/models/datasets' type UsePreviewStateOptions = { dataSourceType: DataSourceType - files: CustomFile[] - notionPages: NotionPage[] + files: StepTwoFile[] + notionPages: StepTwoNotionPage[] websitePages: CrawlResultItem[] documentDetail?: FullDocumentDetail datasetId?: string @@ -21,24 +16,30 @@ export const usePreviewState = (options: UsePreviewStateOptions) => { const { dataSourceType, files, notionPages, websitePages, documentDetail, datasetId } = options // File preview state - const [previewFile, setPreviewFile] = useState( - datasetId && documentDetail ? documentDetail.file : files[0], + // Editing an existing document has no preview item until one is picked. + const isEditing = !!datasetId && !!documentDetail + const [previewFile, setPreviewFile] = useState( + isEditing ? undefined : files[0], ) // Notion page preview state - const [previewNotionPage, setPreviewNotionPage] = useState( - datasetId && documentDetail ? documentDetail.notion_page : notionPages[0], + const [previewNotionPage, setPreviewNotionPage] = useState( + isEditing ? undefined : notionPages[0], ) // Website page preview state - const [previewWebsitePage, setPreviewWebsitePage] = useState( - datasetId && documentDetail ? documentDetail.website_page : websitePages[0], + const [previewWebsitePage, setPreviewWebsitePage] = useState( + isEditing ? undefined : websitePages[0], ) // Get preview items for document picker based on data source type const getPreviewPickerItems = useCallback(() => { if (dataSourceType === DataSourceType.FILE) { - return files as Array> + return files.map((file) => ({ + id: file.id ?? '', + name: file.name, + extension: file.extension ?? '', + })) } if (dataSourceType === DataSourceType.NOTION) { return notionPages.map((page) => ({ @@ -60,7 +61,13 @@ export const usePreviewState = (options: UsePreviewStateOptions) => { // Get current preview value for picker const getPreviewPickerValue = useCallback(() => { if (dataSourceType === DataSourceType.FILE) { - return previewFile as Required + return ( + previewFile && { + id: previewFile.id ?? '', + name: previewFile.name, + extension: previewFile.extension ?? '', + } + ) } if (dataSourceType === DataSourceType.NOTION) { return { diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index caf9111a3fc18f..8419f7e84227aa 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -5,16 +5,23 @@ import type { StepTwoProps } from './types' import { cn } from '@langgenius/dify-ui/cn' import { RadioGroup } from '@langgenius/dify-ui/radio-group' import { Separator } from '@langgenius/dify-ui/separator' +import { useMutation } from '@tanstack/react-query' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' +import { + toChunkingMode, + toCreationDataSourceType, + toRetrievalConfig, + toSummaryIndexSetting, +} from '@/app/components/datasets/dataset-fields' import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { LanguagesSupported } from '@/i18n/language' import { DataSourceProvider } from '@/models/common' import { ChunkingMode, ProcessMode } from '@/models/datasets' -import { useFetchDefaultProcessRule } from '@/service/knowledge/use-create-dataset' +import { consoleQuery } from '@/service/console' import { GeneralChunkingOptions, IndexingModeSection, @@ -31,6 +38,7 @@ import { usePreviewState, useSegmentationState, } from './hooks' +import { isProcessMode, toSegmentationRules } from './process-rule' // oxlint-disable-next-line no-barrel-files/no-barrel-files export { IndexingType } @@ -75,39 +83,46 @@ const StepTwo: FC = ({ const isInCreatePage = !datasetId || (datasetId && !currentDataset?.data_source_type) const dataSourceType = isInCreatePage ? inCreatePageDataSourceType - : (currentDataset?.data_source_type ?? inCreatePageDataSourceType) + : (toCreationDataSourceType(currentDataset?.data_source_type) ?? inCreatePageDataSourceType) const hasSetIndexType = !!propsIndexingType const isModelAndRetrievalConfigDisabled = !!datasetId && !!currentDataset?.data_source_type // Document form state const [docForm, setDocForm] = useState( - datasetId && documentDetail ? (documentDetail.doc_form as ChunkingMode) : ChunkingMode.text, + (datasetId && toChunkingMode(documentDetail?.doc_form)) || ChunkingMode.text, ) const [docLanguage, setDocLanguage] = useState(() => - datasetId && documentDetail + datasetId && documentDetail?.doc_language ? documentDetail.doc_language : locale !== LanguagesSupported[1] ? 'English' : 'Chinese Simplified', ) const [isQAConfirmDialogOpen, setIsQAConfirmDialogOpen] = useState(false) - const currentDocForm = currentDataset?.doc_form || docForm + const datasetChunkingMode = toChunkingMode(currentDataset?.doc_form) + const currentDocForm = datasetChunkingMode || docForm // Custom hooks const segmentation = useSegmentationState({ initialSegmentationType: - currentDataset?.doc_form === ChunkingMode.parentChild + datasetChunkingMode === ChunkingMode.parentChild ? ProcessMode.parentChild : ProcessMode.general, - initialSummaryIndexSetting: currentDataset?.summary_index_setting, + initialSummaryIndexSetting: + currentDataset?.summary_index_setting && + toSummaryIndexSetting(currentDataset.summary_index_setting), }) const showSummaryIndexSetting = !currentDataset const indexing = useIndexingConfig({ initialIndexType: propsIndexingType, initialEmbeddingModel: currentDataset?.embedding_model - ? { provider: currentDataset.embedding_model_provider, model: currentDataset.embedding_model } + ? { + provider: currentDataset.embedding_model_provider ?? '', + model: currentDataset.embedding_model, + } : undefined, - initialRetrievalConfig: currentDataset?.retrieval_model_dict, + initialRetrievalConfig: + currentDataset && toRetrievalConfig(currentDataset.retrieval_model_dict), isAPIKeySet, hasSetIndexType, }) @@ -157,13 +172,17 @@ const StepTwo: FC = ({ }) // Fetch default process rule - const fetchDefaultProcessRuleMutation = useFetchDefaultProcessRule({ + const fetchDefaultProcessRuleMutation = useMutation({ + ...consoleQuery.datasets.processRule.get.mutationOptions(), onSuccess(data) { - segmentation.setSegmentIdentifier(data.rules.segmentation.separator) - segmentation.setMaxChunkLength(data.rules.segmentation.max_tokens) - segmentation.setOverlap(data.rules.segmentation.chunk_overlap!) - segmentation.setRules(data.rules.pre_processing_rules) - segmentation.setDefaultConfig(data.rules) + const defaultRules = data.rules ? toSegmentationRules(data.rules) : undefined + if (defaultRules) { + segmentation.setSegmentIdentifier(defaultRules.segmentation.separator) + segmentation.setMaxChunkLength(defaultRules.segmentation.max_tokens) + segmentation.setOverlap(defaultRules.segmentation.chunk_overlap!) + segmentation.setRules(defaultRules.pre_processing_rules) + segmentation.setDefaultConfig(defaultRules) + } segmentation.setLimitMaxChunkLength(data.limits.indexing_max_segmentation_tokens_length) }, }) @@ -252,25 +271,28 @@ const StepTwo: FC = ({ // Initialize rules useEffect(() => { if (!isSetting) { - fetchDefaultProcessRuleMutation.mutate('/datasets/process-rule') - } else if (documentDetail) { - const rules = documentDetail.dataset_process_rule.rules - const isHierarchical = - documentDetail.doc_form === ChunkingMode.parentChild || - Boolean(rules.parent_mode && rules.subchunk_segmentation) - segmentation.applyConfigFromRules(rules, isHierarchical) - segmentation.setSegmentationType(documentDetail.dataset_process_rule.mode) + fetchDefaultProcessRuleMutation.mutate({}) + } else if (documentDetail?.dataset_process_rule) { + const { mode, rules } = documentDetail.dataset_process_rule + const segmentationRules = rules ? toSegmentationRules(rules) : undefined + if (segmentationRules) { + const isHierarchical = + documentDetail.doc_form === ChunkingMode.parentChild || + Boolean(segmentationRules.parent_mode && segmentationRules.subchunk_segmentation) + segmentation.applyConfigFromRules(segmentationRules, isHierarchical) + } + if (isProcessMode(mode)) segmentation.setSegmentationType(mode) } // oxlint-disable-next-line react/exhaustive-deps }, []) // Show options conditions const showGeneralOption = - (isInUpload && generalChunkingModes.includes(currentDataset!.doc_form)) || + (isInUpload && !!datasetChunkingMode && generalChunkingModes.includes(datasetChunkingMode)) || isUploadInEmptyDataset || isInInit const showParentChildOption = - (isInUpload && currentDataset!.doc_form === ChunkingMode.parentChild) || + (isInUpload && datasetChunkingMode === ChunkingMode.parentChild) || isUploadInEmptyDataset || isInInit @@ -375,9 +397,7 @@ const StepTwo: FC = ({ estimate={estimateHook.estimate} parentChildConfig={segmentation.parentChildConfig} isSetting={isSetting} - pickerFiles={ - preview.getPreviewPickerItems() as Array<{ id: string; name: string; extension: string }> - } + pickerFiles={preview.getPreviewPickerItems()} pickerValue={preview.getPreviewPickerValue()} isIdle={estimateHook.isIdle} isPending={estimateHook.isPending} diff --git a/web/app/components/datasets/create/step-two/process-rule.ts b/web/app/components/datasets/create/step-two/process-rule.ts new file mode 100644 index 00000000000000..7c23e97302bb12 --- /dev/null +++ b/web/app/components/datasets/create/step-two/process-rule.ts @@ -0,0 +1,43 @@ +import type { + DocumentProcessRulesResponse, + Rule, +} from '@dify/contracts/api/console/datasets/types.gen' +import type { PreProcessingRule, Rules } from '@/models/datasets' +import { ProcessMode } from '@/models/datasets' + +const PRE_PROCESSING_RULE_IDS = new Set([ + 'remove_extra_spaces', + 'remove_stopwords', + 'remove_urls_emails', +] satisfies PreProcessingRule['id'][]) + +const isPreProcessingRule = (rule: { id: string; enabled: boolean }): rule is PreProcessingRule => + PRE_PROCESSING_RULE_IDS.has(rule.id) + +export const isProcessMode = (mode: string): mode is ProcessMode => + Object.values(ProcessMode).includes(mode) + +type StoredRules = DocumentProcessRulesResponse | Rule + +const toSegmentation = (segmentation: NonNullable) => ({ + // The backend defaults an unspecified separator to a newline and overlap to zero. + separator: segmentation.separator ?? '\n', + max_tokens: segmentation.max_tokens, + chunk_overlap: segmentation.chunk_overlap ?? 0, +}) + +/** + * Segmentation settings from a stored process rule. Automatic-mode rules store no segmentation, + * so there is nothing to restore for them. + */ +export const toSegmentationRules = (rules: StoredRules): Rules | undefined => { + if (!rules.segmentation) return undefined + return { + pre_processing_rules: (rules.pre_processing_rules ?? []).filter(isPreProcessingRule), + segmentation: toSegmentation(rules.segmentation), + parent_mode: rules.parent_mode ?? undefined, + subchunk_segmentation: rules.subchunk_segmentation + ? toSegmentation(rules.subchunk_segmentation) + : undefined, + } +} diff --git a/web/app/components/datasets/create/step-two/types.ts b/web/app/components/datasets/create/step-two/types.ts index ab5542901d900c..591b114ac4306f 100644 --- a/web/app/components/datasets/create/step-two/types.ts +++ b/web/app/components/datasets/create/step-two/types.ts @@ -10,6 +10,15 @@ import type { } from '@/models/datasets' import type { RETRIEVE_METHOD } from '@/types/app' +/** The fields step two reads from a file to index; uploads and stored documents both provide them. */ +export type StepTwoFile = Pick + +/** The fields step two reads from a Notion page to index. */ +export type StepTwoNotionPage = Pick< + NotionPage, + 'workspace_id' | 'page_id' | 'page_name' | 'page_icon' | 'type' +> + export type StepTwoProps = { isSetting?: boolean documentDetail?: FullDocumentDetail @@ -19,8 +28,8 @@ export type StepTwoProps = { indexingType?: IndexingType retrievalMethod?: string dataSourceType: DataSourceType - files: CustomFile[] - notionPages?: NotionPage[] + files: StepTwoFile[] + notionPages?: StepTwoNotionPage[] notionCredentialId: string websitePages?: CrawlResultItem[] crawlOptions?: CrawlOptions diff --git a/web/app/components/datasets/create/website/__tests__/index.spec.tsx b/web/app/components/datasets/create/website/__tests__/index.spec.tsx index fd0a861941a8d9..d6d0696f6c1468 100644 --- a/web/app/components/datasets/create/website/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/website/__tests__/index.spec.tsx @@ -92,6 +92,8 @@ const createMockDataSourceAuth = (provider: string, credentialsCount = 1): DataS name: provider, label: { en_US: provider, zh_Hans: provider }, description: { en_US: `${provider} description`, zh_Hans: `${provider} description` }, + credential_schema: [], + oauth_schema: null, credentials_list: Array.from({ length: credentialsCount }, (_, i) => ({ credential: {}, type: CredentialTypeEnum.API_KEY, diff --git a/web/app/components/datasets/dataset-fields.ts b/web/app/components/datasets/dataset-fields.ts new file mode 100644 index 00000000000000..b70b54d1c197b4 --- /dev/null +++ b/web/app/components/datasets/dataset-fields.ts @@ -0,0 +1,93 @@ +import type { + DatasetIconInfoResponse, + DatasetRetrievalModelResponse, + DatasetSummaryIndexSettingResponse, +} from '@dify/contracts/api/console/datasets/types.gen' +import type { IconInfo, SummaryIndexSetting } from '@/models/datasets' +import type { AppIconType, RetrievalConfig } from '@/types/app' +import { + ChunkingMode, + DataSourceType, + RerankingModeEnum, + WeightedScoreEnum, +} from '@/models/datasets' + +const isOneOf = (values: Record, value: unknown): value is T => + typeof value === 'string' && Object.values(values).includes(value) + +/** A dataset's chunk structure, when it is one the console can render. */ +export const toChunkingMode = (docForm: string | null | undefined) => + isOneOf(ChunkingMode, docForm) ? docForm : undefined + +/** The data sources of the classic document creation flow. */ +export const toCreationDataSourceType = (dataSourceType: string | null | undefined) => + isOneOf(DataSourceType, dataSourceType) ? dataSourceType : undefined + +const APP_ICON_TYPES = { image: 'image', emoji: 'emoji', link: 'link' } satisfies Record< + AppIconType, + AppIconType +> + +/** The dataset icon, when one was chosen. */ +export const toIconInfo = (iconInfo: DatasetIconInfoResponse | undefined): IconInfo | undefined => { + if (!iconInfo?.icon || !isOneOf(APP_ICON_TYPES, iconInfo.icon_type)) return undefined + return { + icon: iconInfo.icon, + icon_type: iconInfo.icon_type, + icon_background: iconInfo.icon_background ?? undefined, + icon_url: iconInfo.icon_url ?? undefined, + } +} + +/** The summary index settings form state; unset fields stay unset. */ +export const toSummaryIndexSetting = ({ + enable, + model_name, + model_provider_name, + summary_prompt, +}: DatasetSummaryIndexSettingResponse): SummaryIndexSetting => ({ + enable: enable ?? undefined, + model_name: model_name ?? undefined, + model_provider_name: model_provider_name ?? undefined, + summary_prompt: summary_prompt ?? undefined, +}) + +/** + * The retrieval settings form state for a stored retrieval model. Unset model names stay empty, + * as in the backend's default retrieval model, which the form reads as no model selected. + */ +export const toRetrievalConfig = (retrieval: DatasetRetrievalModelResponse): RetrievalConfig => { + const weights = retrieval.weights + const vectorWeight = weights?.vector_setting?.vector_weight + const keywordWeight = weights?.keyword_setting?.keyword_weight + return { + search_method: retrieval.search_method, + reranking_enable: retrieval.reranking_enable, + reranking_model: { + reranking_provider_name: retrieval.reranking_model?.reranking_provider_name ?? '', + reranking_model_name: retrieval.reranking_model?.reranking_model_name ?? '', + }, + top_k: retrieval.top_k, + score_threshold_enabled: retrieval.score_threshold_enabled, + score_threshold: retrieval.score_threshold ?? undefined, + reranking_mode: isOneOf(RerankingModeEnum, retrieval.reranking_mode) + ? retrieval.reranking_mode + : undefined, + // Weighted scores are kept only when both weights were stored. The backend ignores the weight + // type, and the console writes customized weights. + weights: + weights && vectorWeight != null && keywordWeight != null + ? { + weight_type: isOneOf(WeightedScoreEnum, weights.weight_type) + ? weights.weight_type + : WeightedScoreEnum.Customized, + vector_setting: { + vector_weight: vectorWeight, + embedding_provider_name: weights.vector_setting?.embedding_provider_name ?? '', + embedding_model_name: weights.vector_setting?.embedding_model_name ?? '', + }, + keyword_setting: { keyword_weight: keywordWeight }, + } + : undefined, + } +} diff --git a/web/app/components/datasets/documents/__tests__/document-source.spec.ts b/web/app/components/datasets/documents/__tests__/document-source.spec.ts new file mode 100644 index 00000000000000..e01f78eb846248 --- /dev/null +++ b/web/app/components/datasets/documents/__tests__/document-source.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vite-plus/test' +import { + getLocalFileExtension, + getNotionPageIcon, + getOnlineDriveFileName, + getUploadFileExtension, +} from '../document-source' + +describe('document source info', () => { + it('should read the uploaded file extension of legacy file documents', () => { + expect(getUploadFileExtension({ upload_file: { extension: 'pdf' } })).toBe('pdf') + expect(getUploadFileExtension({ upload_file: 'broken' })).toBeUndefined() + expect(getUploadFileExtension(null)).toBeUndefined() + }) + + it('should read pipeline file details', () => { + expect(getLocalFileExtension({ extension: 'md' })).toBe('md') + expect(getOnlineDriveFileName({ name: 'report.docx' })).toBe('report.docx') + expect(getOnlineDriveFileName({ name: 3 })).toBeUndefined() + }) + + it('should read the Notion page icon for imports and pipeline pages', () => { + const icon = { type: 'emoji', emoji: '📄', url: null } + + expect(getNotionPageIcon({ notion_page_icon: 'https://icon.url' }, false)).toBe( + 'https://icon.url', + ) + expect(getNotionPageIcon({ page: { page_icon: icon } }, true)).toEqual(icon) + expect(getNotionPageIcon({ page: { page_icon: 42 } }, true)).toBeUndefined() + expect(getNotionPageIcon({ notion_page_icon: icon }, true)).toBeUndefined() + }) +}) diff --git a/web/app/components/datasets/documents/__tests__/index.spec.tsx b/web/app/components/datasets/documents/__tests__/index.spec.tsx index ba98e6ef6c7fa2..925b6da12274e5 100644 --- a/web/app/components/datasets/documents/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/__tests__/index.spec.tsx @@ -1,15 +1,33 @@ +import type { DocumentWithSegmentsListResponse as DocumentListResponse } from '@dify/contracts/api/console/datasets/types.gen' import type { ReactNode } from 'react' -import type { DocumentListResponse } from '@/models/datasets' -import { QueryClientProvider } from '@tanstack/react-query' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, fireEvent, screen } from '@testing-library/react' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { DataSourceType } from '@/models/datasets' -import { useDocumentList } from '@/service/knowledge/use-document' +import { consoleQuery } from '@/service/console' import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { render as renderWithConsoleState } from '@/test/console/render' import { useDocumentsPageState } from '../hooks/use-documents-page-state' import Documents from '../index' +type DocumentListQueryOptions = { + input: { query: { status?: string } } + refetchInterval?: (query: { + state: { data?: { data: Array<{ indexing_status?: string | null }> } } + }) => number | false +} + +const expectDocumentsInvalidated = () => { + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params: { dataset_id: 'test-dataset-id' } }, + }), + }) + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key(), + }) +} + const render = (ui: Parameters[0]) => { const queryClient = createAccountProfileQueryClient({ id: 'test-user' }) return renderWithConsoleState(ui, { @@ -78,54 +96,50 @@ vi.mock('@/features/system-features/state', async () => { })) }) -// Mock document service hooks -const mockInvalidDocumentList = vi.fn() -const mockInvalidDocumentDetail = vi.fn() - -vi.mock('@/service/knowledge/use-document', () => ({ - useDocumentList: vi.fn(() => ({ - data: { - data: [ - { - id: 'doc-1', - name: 'Document 1', - indexing_status: 'completed', - data_source_type: 'upload_file', - position: 1, - enabled: true, - }, - { - id: 'doc-2', - name: 'Document 2', - indexing_status: 'indexing', - data_source_type: 'upload_file', - position: 2, - enabled: true, - }, - ], - total: 2, - page: 1, - limit: 10, - has_more: false, - } as DocumentListResponse, - isLoading: false, - refetch: vi.fn(), - })), - useInvalidDocumentList: vi.fn(() => mockInvalidDocumentList), - useInvalidDocumentDetail: vi.fn(() => mockInvalidDocumentDetail), -})) - -// Mock segment service hooks -vi.mock('@/service/knowledge/use-segment', () => ({ - useSegmentListKey: 'segment-list-key', - useChildSegmentListKey: 'child-segment-list-key', +// Mock the generated document list query +const { mockDocumentListQuery } = vi.hoisted(() => ({ + mockDocumentListQuery: vi.fn( + ( + _options?: unknown, + ): { data?: DocumentListResponse; isLoading: boolean; refetch: () => void } => ({ + data: { + data: [ + { + id: 'doc-1', + name: 'Document 1', + indexing_status: 'completed', + data_source_type: 'upload_file', + position: 1, + enabled: true, + }, + { + id: 'doc-2', + name: 'Document 2', + indexing_status: 'indexing', + data_source_type: 'upload_file', + position: 2, + enabled: true, + }, + ], + total: 2, + page: 1, + limit: 10, + has_more: false, + } as DocumentListResponse, + isLoading: false, + refetch: vi.fn(), + }), + ), })) -// Mock base service hooks -vi.mock('@/service/use-base', () => ({ - useInvalid: vi.fn(() => vi.fn()), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.get': (options: unknown) => mockDocumentListQuery(options), + }) +}) +// Mock segment service hooks // Mock metadata hook vi.mock('../../metadata/hooks/use-edit-dataset-metadata', () => ({ default: vi.fn(() => ({ @@ -303,6 +317,7 @@ describe('Documents', () => { } beforeEach(() => { + vi.spyOn(QueryClient.prototype, 'invalidateQueries') vi.clearAllMocks() mockPush.mockClear() // Reset context mocks to default @@ -336,18 +351,18 @@ describe('Documents', () => { }) it('should render loading state when isLoading is true', () => { - vi.mocked(useDocumentList).mockReturnValueOnce({ + mockDocumentListQuery.mockReturnValueOnce({ data: undefined, isLoading: true, refetch: vi.fn(), - } as unknown as ReturnType) + }) render() expect(screen.queryByTestId('documents-list')).not.toBeInTheDocument() }) it('should keep rendering list when loading with existing data', () => { - vi.mocked(useDocumentList).mockReturnValueOnce({ + mockDocumentListQuery.mockReturnValueOnce({ data: { data: [ { @@ -366,7 +381,7 @@ describe('Documents', () => { } as DocumentListResponse, isLoading: true, refetch: vi.fn(), - } as unknown as ReturnType) + }) render() expect(screen.getByTestId('documents-list')).toBeInTheDocument() @@ -374,11 +389,11 @@ describe('Documents', () => { }) it('should render empty element when no documents exist', () => { - vi.mocked(useDocumentList).mockReturnValueOnce({ + mockDocumentListQuery.mockReturnValueOnce({ data: { data: [], total: 0, page: 1, limit: 10, has_more: false }, isLoading: false, refetch: vi.fn(), - } as unknown as ReturnType) + }) render() expect(screen.getByTestId('empty-element')).toBeInTheDocument() @@ -403,11 +418,11 @@ describe('Documents', () => { return selector(mockState as MockState) }, ) - vi.mocked(useDocumentList).mockReturnValueOnce({ + mockDocumentListQuery.mockReturnValueOnce({ data: { data: [], total: 0, page: 1, limit: 10, has_more: false }, isLoading: false, refetch: vi.fn(), - } as unknown as ReturnType) + }) render() expect(screen.getByTestId('empty-type')).toHaveTextContent('sync') @@ -538,11 +553,11 @@ describe('Documents', () => { return selector(mockState as MockState) }, ) - vi.mocked(useDocumentList).mockReturnValueOnce({ + mockDocumentListQuery.mockReturnValueOnce({ data: { data: [], total: 0, page: 1, limit: 10, has_more: false }, isLoading: false, refetch: vi.fn(), - } as unknown as ReturnType) + }) render() @@ -553,10 +568,12 @@ describe('Documents', () => { }) describe('Query Options', () => { - it('should pass function refetchInterval to useDocumentList', () => { + it('should pass function refetchInterval to the document list query', () => { render() - const payload = vi.mocked(useDocumentList).mock.calls.at(-1)?.[0] + const payload = mockDocumentListQuery.mock.calls.at(-1)?.[0] as + | DocumentListQueryOptions + | undefined expect(payload).toBeDefined() expect(typeof payload?.refetchInterval).toBe('function') }) @@ -564,7 +581,9 @@ describe('Documents', () => { it('should stop polling when all documents are in terminal statuses', () => { render() - const payload = vi.mocked(useDocumentList).mock.calls.at(-1)?.[0] + const payload = mockDocumentListQuery.mock.calls.at(-1)?.[0] as + | DocumentListQueryOptions + | undefined const refetchInterval = payload?.refetchInterval expect(typeof refetchInterval).toBe('function') if (typeof refetchInterval !== 'function') @@ -580,7 +599,7 @@ describe('Documents', () => { ], }, }, - } as unknown as Parameters[0]) + }) expect(interval).toBe(false) }) @@ -606,9 +625,9 @@ describe('Documents', () => { render() - const payload = vi - .mocked(useDocumentList) - .mock.calls.find(([options]) => options.query.status === 'indexing')?.[0] + const payload = mockDocumentListQuery.mock.calls.find( + ([options]) => (options as DocumentListQueryOptions).input.query.status === 'indexing', + )?.[0] as DocumentListQueryOptions | undefined const refetchInterval = payload?.refetchInterval expect(typeof refetchInterval).toBe('function') if (typeof refetchInterval !== 'function') @@ -620,7 +639,7 @@ describe('Documents', () => { data: [{ indexing_status: 'completed' }], }, }, - } as unknown as Parameters[0]) + }) expect(interval).toBe(2500) }) @@ -632,8 +651,7 @@ describe('Documents', () => { screen.getByTestId('update-btn').click() - expect(mockInvalidDocumentList).toHaveBeenCalled() - expect(mockInvalidDocumentDetail).toHaveBeenCalled() + expectDocumentsInvalidated() }) it('should handle update with delayed chunk invalidation', async () => { @@ -642,8 +660,7 @@ describe('Documents', () => { render() screen.getByTestId('update-btn').click() - expect(mockInvalidDocumentList).toHaveBeenCalled() - expect(mockInvalidDocumentDetail).toHaveBeenCalled() + expectDocumentsInvalidated() await act(async () => { vi.advanceTimersByTime(5000) @@ -668,11 +685,11 @@ describe('Documents', () => { }) it('should handle empty documents array', () => { - vi.mocked(useDocumentList).mockReturnValueOnce({ + mockDocumentListQuery.mockReturnValueOnce({ data: { data: [], total: 0, page: 1, limit: 10, has_more: false }, isLoading: false, refetch: vi.fn(), - } as unknown as ReturnType) + }) render() @@ -680,11 +697,11 @@ describe('Documents', () => { }) it('should handle undefined documentsRes', () => { - vi.mocked(useDocumentList).mockReturnValueOnce({ + mockDocumentListQuery.mockReturnValueOnce({ data: undefined, isLoading: false, refetch: vi.fn(), - } as unknown as ReturnType) + }) render() diff --git a/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx b/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx index da2b79c295f373..231b12d79e0a86 100644 --- a/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx +++ b/web/app/components/datasets/documents/components/__tests__/operations.spec.tsx @@ -42,19 +42,41 @@ const mockResume = vi.fn() let isDownloadPending = false const mockGenerateSummary = vi.fn() -vi.mock('@/service/knowledge/use-document', () => ({ - useDocumentArchive: () => ({ mutateAsync: mockArchive }), - useDocumentUnArchive: () => ({ mutateAsync: mockUnArchive }), - useDocumentEnable: () => ({ mutateAsync: mockEnable }), - useDocumentDisable: () => ({ mutateAsync: mockDisable }), - useDocumentDelete: () => ({ mutateAsync: mockDelete }), - useDocumentDownload: () => ({ mutateAsync: mockDownload, isPending: isDownloadPending }), - useSyncDocument: () => ({ mutateAsync: mockSync }), - useSyncWebsite: () => ({ mutateAsync: mockSyncWebsite }), - useDocumentPause: () => ({ mutateAsync: mockPause }), - useDocumentResume: () => ({ mutateAsync: mockResume }), - useDocumentSummary: () => ({ mutateAsync: mockGenerateSummary }), -})) +const statusMocks: Record unknown> = { + archive: (input) => mockArchive(input), + un_archive: (input) => mockUnArchive(input), + enable: (input) => mockEnable(input), + disable: (input) => mockDisable(input), +} +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.status.byAction.batch.patch': () => ({ + mutateAsync: (input: { params: { action: string } }) => + statusMocks[input.params.action]!(input), + }), + 'datasets.byDatasetId.documents.delete': () => ({ mutateAsync: mockDelete }), + 'datasets.byDatasetId.documents.byDocumentId.download.get': () => ({ + mutateAsync: mockDownload, + isPending: isDownloadPending, + }), + 'datasets.byDatasetId.documents.byDocumentId.notion.sync.get': () => ({ + mutateAsync: mockSync, + }), + 'datasets.byDatasetId.documents.byDocumentId.websiteSync.get': () => ({ + mutateAsync: mockSyncWebsite, + }), + 'datasets.byDatasetId.documents.byDocumentId.processing.pause.patch': () => ({ + mutateAsync: mockPause, + }), + 'datasets.byDatasetId.documents.byDocumentId.processing.resume.patch': () => ({ + mutateAsync: mockResume, + }), + 'datasets.byDatasetId.documents.generateSummary.post': () => ({ + mutateAsync: mockGenerateSummary, + }), + }) +}) // Mock downloadUrl utility const mockDownloadUrl = vi.fn() @@ -229,7 +251,10 @@ describe('Operations', () => { await act(async () => { vi.advanceTimersByTime(600) }) - expect(mockEnable).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockEnable).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', action: 'enable' }, + query: { document_id: ['doc-1'] }, + }) vi.useRealTimers() }) @@ -246,7 +271,10 @@ describe('Operations', () => { await act(async () => { vi.advanceTimersByTime(600) }) - expect(mockDisable).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockDisable).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', action: 'disable' }, + query: { document_id: ['doc-1'] }, + }) vi.useRealTimers() }) @@ -351,7 +379,10 @@ describe('Operations', () => { fireEvent.click(archiveButton) }) await waitFor(() => { - expect(mockArchive).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockArchive).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', action: 'archive' }, + query: { document_id: ['doc-1'] }, + }) }) }) @@ -363,7 +394,10 @@ describe('Operations', () => { fireEvent.click(unarchiveButton) }) await waitFor(() => { - expect(mockUnArchive).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockUnArchive).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', action: 'un_archive' }, + query: { document_id: ['doc-1'] }, + }) }) }) @@ -391,7 +425,10 @@ describe('Operations', () => { fireEvent.click(confirmButton) }) await waitFor(() => { - expect(mockDelete).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockDelete).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1' }, + query: { document_id: ['doc-1'] }, + }) }) }) @@ -463,7 +500,9 @@ describe('Operations', () => { fireEvent.click(syncButton) }) await waitFor(() => { - expect(mockSync).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockSync).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', document_id: 'doc-1' }, + }) }) }) @@ -481,8 +520,7 @@ describe('Operations', () => { }) await waitFor(() => { expect(mockSyncWebsite).toHaveBeenCalledWith({ - datasetId: 'dataset-1', - documentId: 'doc-1', + params: { dataset_id: 'dataset-1', document_id: 'doc-1' }, }) }) }) @@ -497,7 +535,9 @@ describe('Operations', () => { fireEvent.click(pauseButton) }) await waitFor(() => { - expect(mockPause).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockPause).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', document_id: 'doc-1' }, + }) }) }) @@ -511,7 +551,9 @@ describe('Operations', () => { fireEvent.click(resumeButton) }) await waitFor(() => { - expect(mockResume).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockResume).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', document_id: 'doc-1' }, + }) }) }) @@ -523,7 +565,9 @@ describe('Operations', () => { fireEvent.click(downloadButton) }) await waitFor(() => { - expect(mockDownload).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockDownload).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', document_id: 'doc-1' }, + }) expect(mockDownloadUrl).toHaveBeenCalledWith({ url: 'https://example.com/download', fileName: 'Test Document', @@ -578,7 +622,9 @@ describe('Operations', () => { fireEvent.click(downloadButton) }) await waitFor(() => { - expect(mockDownload).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockDownload).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', document_id: 'doc-1' }, + }) }) }) }) diff --git a/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx b/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx index 105857911a4ec7..2953c7172eccd2 100644 --- a/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx +++ b/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx @@ -25,6 +25,9 @@ vi.mock('@/app/notifications', () => ({ const mockRenameDocumentName = vi.mocked(renameDocumentName) +// The rename endpoint answers with the renamed document. +const renamedDocument = { id: 'doc-123', name: 'New Name', data_source_detail_dict: {} } + describe('RenameModal', () => { const defaultProps = { datasetId: 'dataset-123', @@ -95,7 +98,7 @@ describe('RenameModal', () => { }) it('should call renameDocumentName with correct params when save is clicked', async () => { - mockRenameDocumentName.mockResolvedValueOnce({ result: 'success' }) + mockRenameDocumentName.mockResolvedValueOnce(renamedDocument) render() const input = screen.getByRole('textbox') @@ -114,7 +117,7 @@ describe('RenameModal', () => { }) it('should call onSaved and onClose on successful save', async () => { - mockRenameDocumentName.mockResolvedValueOnce({ result: 'success' }) + mockRenameDocumentName.mockResolvedValueOnce(renamedDocument) const handleSaved = vi.fn() const handleClose = vi.fn() @@ -133,8 +136,8 @@ describe('RenameModal', () => { describe('Loading State', () => { it('should not submit again while saving', async () => { // Create a promise that we can resolve manually - let resolvePromise: (value: { result: 'success' | 'fail' }) => void - const pendingPromise = new Promise<{ result: 'success' | 'fail' }>((resolve) => { + let resolvePromise: (value: typeof renamedDocument) => void + const pendingPromise = new Promise((resolve) => { resolvePromise = resolve }) mockRenameDocumentName.mockReturnValueOnce(pendingPromise) @@ -151,7 +154,7 @@ describe('RenameModal', () => { await user.keyboard('{Enter}') expect(mockRenameDocumentName).toHaveBeenCalledTimes(1) - resolvePromise!({ result: 'success' }) + resolvePromise!(renamedDocument) await waitFor(() => { expect(defaultProps.onClose).toHaveBeenCalledTimes(1) }) diff --git a/web/app/components/datasets/documents/components/document-list/components/document-source-icon.tsx b/web/app/components/datasets/documents/components/document-list/components/document-source-icon.tsx index 6a43e2edfe03b3..57d4a131fd77c7 100644 --- a/web/app/components/datasets/documents/components/document-list/components/document-source-icon.tsx +++ b/web/app/components/datasets/documents/components/document-list/components/document-source-icon.tsx @@ -1,11 +1,5 @@ import type { FC } from 'react' -import type { - LegacyDataSourceInfo, - LocalFileInfo, - OnlineDocumentInfo, - OnlineDriveInfo, - SimpleDocumentDetail, -} from '@/models/datasets' +import type { SimpleDocumentDetail } from '@/models/datasets' import { RiGlobalLine } from '@remixicon/react' import * as React from 'react' import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon' @@ -13,31 +7,37 @@ import NotionIcon from '@/app/components/base/notion-icon' import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type' import { DataSourceType } from '@/models/datasets' import { DatasourceType } from '@/models/pipeline' +import { + getLocalFileExtension, + getNotionPageIcon, + getOnlineDriveFileName, + getUploadFileExtension, +} from '../../../document-source' type DocumentSourceIconProps = { doc: SimpleDocumentDetail fileType?: string } -const isLocalFile = (dataSourceType: DataSourceType | DatasourceType) => { +const isLocalFile = (dataSourceType?: string | null) => { return dataSourceType === DatasourceType.localFile || dataSourceType === DataSourceType.FILE } -const isOnlineDocument = (dataSourceType: DataSourceType | DatasourceType) => { +const isOnlineDocument = (dataSourceType?: string | null) => { return ( dataSourceType === DatasourceType.onlineDocument || dataSourceType === DataSourceType.NOTION ) } -const isWebsiteCrawl = (dataSourceType: DataSourceType | DatasourceType) => { +const isWebsiteCrawl = (dataSourceType?: string | null) => { return dataSourceType === DatasourceType.websiteCrawl } -const isOnlineDrive = (dataSourceType: DataSourceType | DatasourceType) => { +const isOnlineDrive = (dataSourceType?: string | null) => { return dataSourceType === DatasourceType.onlineDrive } -const isCreateFromRAGPipeline = (createdFrom: string) => { +const isCreateFromRAGPipeline = (createdFrom?: string | null) => { return createdFrom === 'rag-pipeline' } @@ -54,11 +54,7 @@ const DocumentSourceIcon: FC = React.memo(({ doc, fileT ) } @@ -68,8 +64,8 @@ const DocumentSourceIcon: FC = React.memo(({ doc, fileT @@ -80,7 +76,7 @@ const DocumentSourceIcon: FC = React.memo(({ doc, fileT return ( diff --git a/web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx b/web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx index 2537af270c2f7d..ffd42ac1a751c4 100644 --- a/web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx +++ b/web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx @@ -16,7 +16,7 @@ import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import useTimestamp from '@/hooks/use-timestamp' -import { DataSourceType } from '@/models/datasets' +import { DataSourceType, isDocumentDisplayStatus } from '@/models/datasets' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' import { formatNumber } from '@/utils/format' @@ -39,7 +39,7 @@ type DocumentTableRowProps = { onUpdate: () => void } -const renderCount = (count: number | undefined) => { +const renderCount = (count?: number | null) => { if (!count) return renderTdValue(0, true) if (count < 1000) return count @@ -169,13 +169,17 @@ const DocumentTableRow = React.memo( {renderCount(doc.word_count)} {renderCount(doc.hit_count)} - {formatTime( - doc.created_at, - t(($) => $.dateTimeFormat, { ns: 'datasetHitTesting' }) as string, - )} + {doc.created_at + ? formatTime( + doc.created_at, + t(($) => $.dateTimeFormat, { ns: 'datasetHitTesting' }) as string, + ) + : '-'} - + {isDocumentDisplayStatus(doc.display_status) && ( + + )} ({ mockMutateAsync: vi.fn() })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + const mutation = () => ({ mutateAsync: mockMutateAsync, isPending: false }) + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.status.byAction.batch.patch': mutation, + 'datasets.byDatasetId.documents.generateSummary.post': mutation, + 'datasets.byDatasetId.documents.delete': mutation, + 'datasets.byDatasetId.retry.post': mutation, + }) +}) + const mockUseDocumentDownloadZip = vi.mocked(useDocument.useDocumentDownloadZip) const createConsoleQueryClient = () => @@ -32,8 +38,6 @@ const createWrapper = () => { } describe('useDocumentActions', () => { - const mockMutateAsync = vi.fn() - beforeEach(() => { vi.clearAllMocks() @@ -55,24 +59,6 @@ describe('useDocumentActions', () => { submittedAt: 0, }) - mockUseDocumentArchive.mockReturnValue( - createMockMutation() as unknown as ReturnType, - ) - mockUseDocumentSummary.mockReturnValue( - createMockMutation() as unknown as ReturnType, - ) - mockUseDocumentEnable.mockReturnValue( - createMockMutation() as unknown as ReturnType, - ) - mockUseDocumentDisable.mockReturnValue( - createMockMutation() as unknown as ReturnType, - ) - mockUseDocumentDelete.mockReturnValue( - createMockMutation() as unknown as ReturnType, - ) - mockUseDocumentBatchRetryIndex.mockReturnValue( - createMockMutation() as unknown as ReturnType, - ) mockUseDocumentDownloadZip.mockReturnValue({ ...createMockMutation(), isPending: false, @@ -102,8 +88,8 @@ describe('useDocumentActions', () => { }) expect(mockMutateAsync).toHaveBeenCalledWith({ - datasetId: 'ds1', - documentIds: ['doc1'], + params: { dataset_id: 'ds1', action: 'archive' }, + query: { document_id: ['doc1'] }, }) }) @@ -183,8 +169,8 @@ describe('useDocumentActions', () => { }) expect(mockMutateAsync).toHaveBeenCalledWith({ - datasetId: 'ds1', - documentIds: ['doc1', 'doc2'], + params: { dataset_id: 'ds1' }, + body: { document_ids: ['doc1', 'doc2'] }, }) }) diff --git a/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts b/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts index 97052acdf353b9..72d0f330740276 100644 --- a/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts +++ b/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts @@ -17,13 +17,24 @@ const mockRetryIndex = vi.fn() const mockDownloadZip = vi.fn() let mockIsDownloadingZip = false +const statusMocks: Record unknown> = { + archive: (input) => mockArchive(input), + enable: (input) => mockEnable(input), + disable: (input) => mockDisable(input), +} +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.status.byAction.batch.patch': () => ({ + mutateAsync: (input: { params: { action: string } }) => + statusMocks[input.params.action]!(input), + }), + 'datasets.byDatasetId.documents.generateSummary.post': () => ({ mutateAsync: mockSummary }), + 'datasets.byDatasetId.documents.delete': () => ({ mutateAsync: mockDelete }), + 'datasets.byDatasetId.retry.post': () => ({ mutateAsync: mockRetryIndex }), + }) +}) vi.mock('@/service/knowledge/use-document', () => ({ - useDocumentArchive: () => ({ mutateAsync: mockArchive }), - useDocumentSummary: () => ({ mutateAsync: mockSummary }), - useDocumentEnable: () => ({ mutateAsync: mockEnable }), - useDocumentDisable: () => ({ mutateAsync: mockDisable }), - useDocumentDelete: () => ({ mutateAsync: mockDelete }), - useDocumentBatchRetryIndex: () => ({ mutateAsync: mockRetryIndex }), useDocumentDownloadZip: () => ({ mutateAsync: mockDownloadZip, isPending: mockIsDownloadingZip }), })) @@ -71,8 +82,8 @@ describe('useDocumentActions', () => { }) expect(mockArchive).toHaveBeenCalledWith({ - datasetId: 'ds-1', - documentIds: ['doc-1', 'doc-2'], + params: { dataset_id: 'ds-1', action: 'archive' }, + query: { document_id: ['doc-1', 'doc-2'] }, }) expect(mockToastSuccess).toHaveBeenCalledWith(expect.any(String)) expect(defaultOptions.onUpdate).toHaveBeenCalled() @@ -87,8 +98,8 @@ describe('useDocumentActions', () => { }) expect(mockEnable).toHaveBeenCalledWith({ - datasetId: 'ds-1', - documentIds: ['doc-1', 'doc-2'], + params: { dataset_id: 'ds-1', action: 'enable' }, + query: { document_id: ['doc-1', 'doc-2'] }, }) expect(defaultOptions.onUpdate).toHaveBeenCalled() }) @@ -162,8 +173,8 @@ describe('useDocumentActions', () => { }) expect(mockRetryIndex).toHaveBeenCalledWith({ - datasetId: 'ds-1', - documentIds: ['doc-1', 'doc-2'], + params: { dataset_id: 'ds-1' }, + body: { document_ids: ['doc-1', 'doc-2'] }, }) expect(defaultOptions.onClearSelection).toHaveBeenCalled() expect(defaultOptions.onUpdate).toHaveBeenCalled() diff --git a/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts b/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts index d29b03d2bfa38b..f4beceaf0706e0 100644 --- a/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts +++ b/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts @@ -1,17 +1,10 @@ -import type { CommonResponse } from '@/models/common' -import { useCallback, useMemo } from 'react' +import { useMutation } from '@tanstack/react-query' +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { DocumentActionType } from '@/models/datasets' -import { - useDocumentArchive, - useDocumentBatchRetryIndex, - useDocumentDelete, - useDocumentDisable, - useDocumentDownloadZip, - useDocumentEnable, - useDocumentSummary, -} from '@/service/knowledge/use-document' +import { consoleQuery } from '@/service/console' +import { useDocumentDownloadZip } from '@/service/knowledge/use-document' import { asyncRunSafe } from '@/utils' import { downloadBlob } from '@/utils/download' @@ -44,12 +37,17 @@ export const useDocumentActions = ({ }: UseDocumentActionsOptions) => { const { t } = useTranslation(['common']) - const { mutateAsync: archiveDocument } = useDocumentArchive() - const { mutateAsync: generateSummary } = useDocumentSummary() - const { mutateAsync: enableDocument } = useDocumentEnable() - const { mutateAsync: disableDocument } = useDocumentDisable() - const { mutateAsync: deleteDocument } = useDocumentDelete() - const { mutateAsync: retryIndexDocument } = useDocumentBatchRetryIndex() + const documents = consoleQuery.datasets.byDatasetId.documents + const { mutateAsync: changeDocumentStatus } = useMutation( + documents.status.byAction.batch.patch.mutationOptions(), + ) + const { mutateAsync: generateSummary } = useMutation( + documents.generateSummary.post.mutationOptions(), + ) + const { mutateAsync: deleteDocuments } = useMutation(documents.delete.mutationOptions()) + const { mutateAsync: retryIndexDocuments } = useMutation( + consoleQuery.datasets.byDatasetId.retry.post.mutationOptions(), + ) const { mutateAsync: requestDocumentsZip, isPending: isDownloadingZip } = useDocumentDownloadZip() type SupportedActionType = @@ -59,27 +57,33 @@ export const useDocumentActions = ({ | typeof DocumentActionType.disable | typeof DocumentActionType.delete - const actionMutationMap = useMemo( - () => - ({ - [DocumentActionType.archive]: archiveDocument, - [DocumentActionType.summary]: generateSummary, - [DocumentActionType.enable]: enableDocument, - [DocumentActionType.disable]: disableDocument, - [DocumentActionType.delete]: deleteDocument, - }) as const, - [archiveDocument, generateSummary, enableDocument, disableDocument, deleteDocument], + const runAction = useCallback( + (actionName: SupportedActionType): Promise => { + switch (actionName) { + case DocumentActionType.summary: + return generateSummary({ + params: { dataset_id: datasetId }, + body: { document_list: selectedIds }, + }) + case DocumentActionType.delete: + return deleteDocuments({ + params: { dataset_id: datasetId }, + query: { document_id: selectedIds }, + }) + default: + return changeDocumentStatus({ + params: { dataset_id: datasetId, action: actionName }, + query: { document_id: selectedIds }, + }) + } + }, + [changeDocumentStatus, datasetId, deleteDocuments, generateSummary, selectedIds], ) const handleAction = useCallback( (actionName: SupportedActionType) => { return async () => { - const opApi = actionMutationMap[actionName] - if (!opApi) return - - const [e] = await asyncRunSafe( - opApi({ datasetId, documentIds: selectedIds }), - ) + const [e] = await asyncRunSafe(runAction(actionName)) if (!e) { if (actionName === DocumentActionType.delete) onClearSelection() @@ -90,12 +94,15 @@ export const useDocumentActions = ({ } } }, - [actionMutationMap, datasetId, selectedIds, onClearSelection, onUpdate, t], + [runAction, onClearSelection, onUpdate, t], ) const handleBatchReIndex = useCallback(async () => { - const [e] = await asyncRunSafe( - retryIndexDocument({ datasetId, documentIds: selectedIds }), + const [e] = await asyncRunSafe( + retryIndexDocuments({ + params: { dataset_id: datasetId }, + body: { document_ids: selectedIds }, + }), ) if (!e) { onClearSelection() @@ -104,7 +111,7 @@ export const useDocumentActions = ({ } else { toast.error(t(($) => $['actionMsg.modifiedUnsuccessfully'], { ns: 'common' })) } - }, [retryIndexDocument, datasetId, selectedIds, onClearSelection, onUpdate, t]) + }, [retryIndexDocuments, datasetId, selectedIds, onClearSelection, onUpdate, t]) const handleBatchDownload = useCallback(async () => { if (isDownloadingZip) return diff --git a/web/app/components/datasets/documents/components/operations.tsx b/web/app/components/datasets/documents/components/operations.tsx index 177985f78136ac..28eb214d2d540b 100644 --- a/web/app/components/datasets/documents/components/operations.tsx +++ b/web/app/components/datasets/documents/components/operations.tsx @@ -1,6 +1,4 @@ import type { OperationName } from '../types' -import type { CommonResponse } from '@/models/common' -import type { DocumentDownloadResponse } from '@/service/datasets' import { AlertDialog, AlertDialogCancelButton, @@ -21,7 +19,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { Separator } from '@langgenius/dify-ui/separator' import { Switch } from '@langgenius/dify-ui/switch' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useBoolean, useDebounceFn } from 'ahooks' import { noop } from 'es-toolkit/function' import * as React from 'react' @@ -31,19 +29,7 @@ import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { DataSourceType, DocumentActionType } from '@/models/datasets' import { useRouter } from '@/next/navigation' -import { - useDocumentArchive, - useDocumentDelete, - useDocumentDisable, - useDocumentDownload, - useDocumentEnable, - useDocumentPause, - useDocumentResume, - useDocumentSummary, - useDocumentUnArchive, - useSyncDocument, - useSyncWebsite, -} from '@/service/knowledge/use-document' +import { consoleQuery } from '@/service/console' import { asyncRunSafe } from '@/utils' import { downloadUrl } from '@/utils/download' import s from '../style.module.css' @@ -52,13 +38,13 @@ import RenameModal from './rename-modal' type OperationsProps = { embeddingAvailable: boolean detail: { - name: string - enabled: boolean - archived: boolean id: string - data_source_type: string - doc_form: string - display_status?: string + name?: string | null + enabled?: boolean | null + archived?: boolean | null + data_source_type?: string | null + doc_form?: string | null + display_status?: string | null } selectedIds?: string[] onSelectedIdChange?: (ids: string[]) => void @@ -90,36 +76,45 @@ const Operations = ({ select: ({ deployment_edition }) => deployment_edition, }) const isNonCloudEdition = deploymentEdition === 'COMMUNITY' || deploymentEdition === 'ENTERPRISE' - const { - id, - name, - enabled = false, - archived = false, - data_source_type, - display_status, - } = detail || {} + const { id, name, data_source_type, display_status } = detail + const enabled = detail.enabled ?? false + const archived = detail.archived ?? false const [showModal, setShowModal] = useState(false) const [deleting, setDeleting] = useState(false) const { t } = useTranslation(['common', 'datasetDocuments']) const router = useRouter() - const { mutateAsync: archiveDocument } = useDocumentArchive() - const { mutateAsync: unArchiveDocument } = useDocumentUnArchive() - const { mutateAsync: enableDocument } = useDocumentEnable() - const { mutateAsync: disableDocument } = useDocumentDisable() - const { mutateAsync: deleteDocument } = useDocumentDelete() - const { mutateAsync: downloadDocument, isPending: isDownloading } = useDocumentDownload() - const { mutateAsync: syncDocument } = useSyncDocument() - const { mutateAsync: syncWebsite } = useSyncWebsite() - const { mutateAsync: generateSummary } = useDocumentSummary() - const { mutateAsync: pauseDocument } = useDocumentPause() - const { mutateAsync: resumeDocument } = useDocumentResume() + const documents = consoleQuery.datasets.byDatasetId.documents + const { mutateAsync: changeDocumentStatus } = useMutation( + documents.status.byAction.batch.patch.mutationOptions(), + ) + const { mutateAsync: deleteDocuments } = useMutation(documents.delete.mutationOptions()) + const { mutateAsync: downloadDocument, isPending: isDownloading } = useMutation( + documents.byDocumentId.download.get.mutationOptions(), + ) + const { mutateAsync: syncNotionDocument } = useMutation( + documents.byDocumentId.notion.sync.get.mutationOptions(), + ) + const { mutateAsync: syncWebsiteDocument } = useMutation( + documents.byDocumentId.websiteSync.get.mutationOptions(), + ) + const { mutateAsync: generateSummary } = useMutation( + documents.generateSummary.post.mutationOptions(), + ) + const { mutateAsync: pauseDocument } = useMutation( + documents.byDocumentId.processing.pause.patch.mutationOptions(), + ) + const { mutateAsync: resumeDocument } = useMutation( + documents.byDocumentId.processing.resume.patch.mutationOptions(), + ) const isListScene = scene === 'list' const canShowRenameAction = canEdit && !archived const canShowSummaryAction = canEdit && !archived && isNonCloudEdition const canShowSettingsAction = canViewSettings const canShowDownloadAction = canDownload && data_source_type === DataSourceType.FILE const canShowSyncAction = - canEdit && !archived && ['notion_import', DataSourceType.WEB].includes(data_source_type) + canEdit && + !archived && + (data_source_type === DataSourceType.NOTION || data_source_type === DataSourceType.WEB) const canShowPauseAction = canEdit && !archived && display_status?.toLowerCase() === 'indexing' const canShowResumeAction = canEdit && !archived && display_status?.toLowerCase() === 'paused' const canShowArchiveAction = canEdit && !archived @@ -147,41 +142,39 @@ const Operations = ({ async (operationName: OperationName) => { if (!canOperate(operationName)) return - let opApi - switch (operationName) { - case 'archive': - opApi = archiveDocument - break - case 'un_archive': - opApi = unArchiveDocument - break - case 'enable': - opApi = enableDocument - break - case 'disable': - opApi = disableDocument - break - case 'sync': - if (data_source_type === 'notion_import') opApi = syncDocument - else opApi = syncWebsite - break - case 'summary': - opApi = generateSummary - break - case 'pause': - opApi = pauseDocument - break - case 'resume': - opApi = resumeDocument - break - default: - opApi = deleteDocument - setDeleting(true) - break + const documentParams = { dataset_id: datasetId, document_id: id } + const runOperation = (): Promise => { + switch (operationName) { + case 'archive': + case 'un_archive': + case 'enable': + case 'disable': + return changeDocumentStatus({ + params: { dataset_id: datasetId, action: operationName }, + query: { document_id: [id] }, + }) + case 'sync': + return data_source_type === 'notion_import' + ? syncNotionDocument({ params: documentParams }) + : syncWebsiteDocument({ params: documentParams }) + case 'summary': + return generateSummary({ + params: { dataset_id: datasetId }, + body: { document_list: [id] }, + }) + case 'pause': + return pauseDocument({ params: documentParams }) + case 'resume': + return resumeDocument({ params: documentParams }) + default: + setDeleting(true) + return deleteDocuments({ + params: { dataset_id: datasetId }, + query: { document_id: [id] }, + }) + } } - const [e] = await asyncRunSafe( - opApi({ datasetId, documentId: id }) as Promise, - ) + const [e] = await asyncRunSafe(runOperation()) if (!e) { // The switch already shows the result of enable and disable. if (operationName !== 'enable' && operationName !== 'disable') @@ -196,13 +189,11 @@ const Operations = ({ if (operationName === DocumentActionType.delete) setDeleting(false) }, [ - archiveDocument, canOperate, + changeDocumentStatus, data_source_type, datasetId, - deleteDocument, - disableDocument, - enableDocument, + deleteDocuments, generateSummary, id, onSelectedIdChange, @@ -210,10 +201,9 @@ const Operations = ({ pauseDocument, resumeDocument, selectedIds, - syncDocument, - syncWebsite, + syncNotionDocument, + syncWebsiteDocument, t, - unArchiveDocument, ], ) const { run: handleSwitch } = useDebounceFn( @@ -253,20 +243,20 @@ const Operations = ({ // Avoid repeated clicks while the signed URL request is in-flight. if (isDownloading) return // Request a signed URL first (it points to `/files//file-preview?...&as_attachment=true`). - const [e, res] = await asyncRunSafe( - downloadDocument({ datasetId, documentId: id }) as Promise, + const [e, res] = await asyncRunSafe( + downloadDocument({ params: { dataset_id: datasetId, document_id: id } }), ) if (e || !res?.url) { toast.error(t(($) => $['actionMsg.downloadUnsuccessfully'], { ns: 'common' })) return } // Trigger download without navigating away (helps avoid duplicate downloads in some browsers). - downloadUrl({ url: res.url, fileName: name }) + downloadUrl({ url: res.url, fileName: name ?? undefined }) }, [canDownload, datasetId, downloadDocument, id, isDownloading, name, t]) const handleShowRename = useCallback(() => { handleShowRenameModal({ id: detail.id, - name: detail.name, + name: detail.name ?? '', }) }, [detail.id, detail.name, handleShowRenameModal]) const handleMenuOperation = useCallback( diff --git a/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx index 92c28d97a32e6f..c7c2022b1f60dd 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx @@ -3,6 +3,19 @@ import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import CreateFromPipeline from '../index' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.publish.get': () => ({ + data: { graph: { nodes: [] } }, + isFetching: false, + }), + 'files.upload.get': () => ({ + data: { file_size_limit: 15, batch_count_limit: 5 }, + }), + }) +}) + const mockPlan = { usage: { vectorSpace: 50 }, total: { vectorSpace: 100 }, @@ -89,19 +102,6 @@ vi.mock('@/next/navigation', () => ({ }), })) -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ - data: { file_size_limit: 15, batch_count_limit: 5 }, - }), -})) - -vi.mock('@/service/use-pipeline', () => ({ - usePublishedPipelineInfo: () => ({ - data: { graph: { nodes: [] } }, - isFetching: false, - }), -})) - vi.mock('../data-source/store', () => ({ useDataSourceStore: () => ({ getState: () => ({}) }), })) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx index 4b5eae85517f6f..13e8424f326a2a 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx @@ -22,7 +22,7 @@ const Item = ({ credential, isSelected, onCredentialChange }: ItemProps) => { className="flex cursor-pointer items-center gap-x-2 rounded-lg p-2 hover:bg-state-base-hover" onClick={handleCredentialChange} > - + {name} {isSelected && }
diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx index c65d742cd319d8..2bb9c18132b7bd 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx @@ -19,7 +19,7 @@ const Trigger = ({ currentCredential, isOpen }: TriggerProps) => { isOpen ? 'bg-state-base-hover' : 'hover:bg-state-base-hover', )} > - +
{name} diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx index c6a61851df3ab3..b0a1574eb50121 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/hooks/__tests__/use-local-file-upload.spec.tsx @@ -10,6 +10,19 @@ import { } from '@/test/console/query-data' import { PROGRESS_ERROR } from '../../constants' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': vi.fn(() => ({ + data: { + file_size_limit: 15, + batch_count_limit: 5, + file_upload_limit: 10, + }, + })), + }) +}) + const { mockNotify, mockToast } = vi.hoisted(() => { const mockNotify = vi.fn() const mockToast = Object.assign(mockNotify, { @@ -63,15 +76,6 @@ vi.mock('../../../store', () => ({ })) // Mock file upload config -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: vi.fn(() => ({ - data: { - file_size_limit: 15, - batch_count_limit: 5, - file_upload_limit: 10, - }, - })), -})) // Mock upload service const mockUpload = vi.fn() diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/__tests__/index.spec.tsx index f0636b4b83889c..18f7d47f853a0a 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/__tests__/index.spec.tsx @@ -54,9 +54,21 @@ const { mockUseGetDataSourceAuth } = vi.hoisted(() => ({ mockUseGetDataSourceAuth: vi.fn(), })) -vi.mock('@/service/use-datasource', () => ({ - useGetDataSourceAuth: mockUseGetDataSourceAuth, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + // The credential query is keyed by `${pluginId}/${provider}`; report it as the two ids. + 'auth.plugin.datasource.byProviderId.get': (options: { queryKey: unknown[] }) => { + const { input } = options.queryKey[1] as { input: { params: { provider_id: string } } } + const providerId = input.params.provider_id + const separator = providerId.lastIndexOf('/') + return mockUseGetDataSourceAuth({ + pluginId: providerId.slice(0, separator), + provider: providerId.slice(separator + 1), + }) + }, + }) +}) // Note: zustand/react/shallow useShallow is imported directly (simple utility function) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx index 937bfcfd918dbf..8baa764a7be371 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx @@ -1,6 +1,7 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import type { DataSourceNotionPageMap, DataSourceNotionWorkspace } from '@/models/common' import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline' +import { useQuery } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import { useCallback, useEffect, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' @@ -15,7 +16,7 @@ import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useDocLink } from '@/context/i18n' import { DatasourceType } from '@/models/pipeline' import { ssePost } from '@/service/base' -import { useGetDataSourceAuth } from '@/service/use-datasource' +import { consoleQuery } from '@/service/console' import Header from '../base/header' import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store' import PageSelector from './page-selector' @@ -49,10 +50,11 @@ const OnlineDocuments = ({ })), ) - const { data: dataSourceAuth } = useGetDataSourceAuth({ - pluginId: nodeData.plugin_id, - provider: nodeData.provider_name, - }) + const { data: dataSourceAuth } = useQuery( + consoleQuery.auth.plugin.datasource.byProviderId.get.queryOptions({ + input: { params: { provider_id: `${nodeData.plugin_id}/${nodeData.provider_name}` } }, + }), + ) const dataSourceStore = useDataSourceStore() diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/__tests__/index.spec.tsx index 38f8efbad42222..7d7aeac3602658 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/__tests__/index.spec.tsx @@ -57,9 +57,21 @@ vi.mock('@/service/base', () => ({ ssePost: (...args: unknown[]) => mocks.ssePost(...args), })) -vi.mock('@/service/use-datasource', () => ({ - useGetDataSourceAuth: (...args: unknown[]) => mocks.useGetDataSourceAuth(...args), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + // The credential query is keyed by `${pluginId}/${provider}`; report it as the two ids. + 'auth.plugin.datasource.byProviderId.get': (options: { queryKey: unknown[] }) => { + const { input } = options.queryKey[1] as { input: { params: { provider_id: string } } } + const providerId = input.params.provider_id + const separator = providerId.lastIndexOf('/') + return mocks.useGetDataSourceAuth({ + pluginId: providerId.slice(0, separator), + provider: providerId.slice(separator + 1), + }) + }, + }) +}) vi.mock('@/app/notifications', async (importOriginal) => { const actual = await importOriginal() diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx index 9652ba65e41c3a..c4e25b857a0ea7 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx @@ -1,6 +1,7 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import type { OnlineDriveFile } from '@/models/pipeline' import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useQueryState } from 'nuqs' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -14,7 +15,7 @@ import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useDocLink } from '@/context/i18n' import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline' import { ssePost } from '@/service/base' -import { useGetDataSourceAuth } from '@/service/use-datasource' +import { consoleQuery } from '@/service/console' import Header from '../base/header' import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store' import FileList from './file-list' @@ -64,10 +65,11 @@ const OnlineDrive = ({ const [isLoading, setIsLoading] = useState(false) const isLoadingRef = useRef(false) - const { data: dataSourceAuth } = useGetDataSourceAuth({ - pluginId: nodeData.plugin_id, - provider: nodeData.provider_name, - }) + const { data: dataSourceAuth } = useQuery( + consoleQuery.auth.plugin.datasource.byProviderId.get.queryOptions({ + input: { params: { provider_id: `${nodeData.plugin_id}/${nodeData.provider_name}` } }, + }), + ) const datasourceNodeRunURL = !isInPipeline ? `/rag/pipelines/${pipelineId}/workflows/published/datasource/nodes/${nodeId}/run` diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/__tests__/index.spec.tsx index b0c26ac25c9631..845ddeb54bedb5 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/__tests__/index.spec.tsx @@ -39,21 +39,45 @@ const { mockUseGetDataSourceAuth } = vi.hoisted(() => ({ mockUseGetDataSourceAuth: vi.fn(), })) -vi.mock('@/service/use-datasource', () => ({ - useGetDataSourceAuth: mockUseGetDataSourceAuth, -})) - -// Mock usePipeline hooks - API service hooks require mocking const { mockUseDraftPipelinePreProcessingParams, mockUsePublishedPipelinePreProcessingParams } = vi.hoisted(() => ({ mockUseDraftPipelinePreProcessingParams: vi.fn(), mockUsePublishedPipelinePreProcessingParams: vi.fn(), })) -vi.mock('@/service/use-pipeline', () => ({ - useDraftPipelinePreProcessingParams: mockUseDraftPipelinePreProcessingParams, - usePublishedPipelinePreProcessingParams: mockUsePublishedPipelinePreProcessingParams, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.draft.preProcessing.parameters.get': (o: { + input: { params: { pipeline_id: string }; query: { node_id: string } } + enabled?: boolean + }) => + mockUseDraftPipelinePreProcessingParams( + { pipeline_id: o.input.params.pipeline_id, node_id: o.input.query.node_id }, + o.enabled, + ), + 'rag.pipelines.byPipelineId.workflows.published.preProcessing.parameters.get': (o: { + input: { params: { pipeline_id: string }; query: { node_id: string } } + enabled?: boolean + }) => + mockUsePublishedPipelinePreProcessingParams( + { pipeline_id: o.input.params.pipeline_id, node_id: o.input.query.node_id }, + o.enabled, + ), + // The credential query is keyed by `${pluginId}/${provider}`; report it as the two ids. + 'auth.plugin.datasource.byProviderId.get': (options: { queryKey: unknown[] }) => { + const { input } = options.queryKey[1] as { input: { params: { provider_id: string } } } + const providerId = input.params.provider_id + const separator = providerId.lastIndexOf('/') + return mockUseGetDataSourceAuth({ + pluginId: providerId.slice(0, separator), + provider: providerId.slice(separator + 1), + }) + }, + }) +}) + +// Mock usePipeline hooks - API service hooks require mocking // Note: zustand/react/shallow useShallow is imported directly (simple utility function) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx index 7cf4a033154593..725693d347d852 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx @@ -6,25 +6,23 @@ import type { DataSourceNodeErrorResponse, DataSourceNodeProcessingResponse, } from '@/types/pipeline' +import { useQuery } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import * as React from 'react' -import { useCallback, useRef, useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' +import { selectPipelineStepParameters } from '@/app/components/rag-pipeline/utils/query-select' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useDocLink } from '@/context/i18n' import { CrawlStep } from '@/models/datasets' import { DatasourceType } from '@/models/pipeline' import { ssePost } from '@/service/base' -import { useGetDataSourceAuth } from '@/service/use-datasource' -import { - useDraftPipelinePreProcessingParams, - usePublishedPipelinePreProcessingParams, -} from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import Header from '../base/header' import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store' import CrawledResult from './base/crawled-result' @@ -67,22 +65,24 @@ const WebsiteCrawl = ({ })), ) - const { data: dataSourceAuth } = useGetDataSourceAuth({ - pluginId: nodeData.plugin_id, - provider: nodeData.provider_name, - }) + const { data: dataSourceAuth } = useQuery( + consoleQuery.auth.plugin.datasource.byProviderId.get.queryOptions({ + input: { params: { provider_id: `${nodeData.plugin_id}/${nodeData.provider_name}` } }, + }), + ) const dataSourceStore = useDataSourceStore() - const usePreProcessingParams = useRef( - !isInPipeline ? usePublishedPipelinePreProcessingParams : useDraftPipelinePreProcessingParams, - ) - const { data: paramsConfig, isFetching: isFetchingParams } = usePreProcessingParams.current( - { - pipeline_id: pipelineId!, - node_id: nodeId, - }, - !!pipelineId && !!nodeId, + const preProcessingParamsOptions = { + input: { params: { pipeline_id: pipelineId! }, query: { node_id: nodeId } }, + enabled: !!pipelineId && !!nodeId, + select: selectPipelineStepParameters, + } + const workflows = consoleQuery.rag.pipelines.byPipelineId.workflows + const { data: paramsConfig, isFetching: isFetchingParams } = useQuery( + isInPipeline + ? workflows.draft.preProcessing.parameters.get.queryOptions(preProcessingParamsOptions) + : workflows.published.preProcessing.parameters.get.queryOptions(preProcessingParamsOptions), ) const isInit = step === CrawlStep.init diff --git a/web/app/components/datasets/documents/create-from-pipeline/hooks/__tests__/use-datasource-actions.spec.ts b/web/app/components/datasets/documents/create-from-pipeline/hooks/__tests__/use-datasource-actions.spec.ts index 3ad4b2e8ef2248..8793cc8b09be15 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/hooks/__tests__/use-datasource-actions.spec.ts +++ b/web/app/components/datasets/documents/create-from-pipeline/hooks/__tests__/use-datasource-actions.spec.ts @@ -9,13 +9,16 @@ import { createDataSourceStore } from '../../data-source/store' import { useDatasourceActions } from '../use-datasource-actions' const mockRunPublishedPipeline = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - useRunPublishedPipeline: () => ({ - mutateAsync: mockRunPublishedPipeline, - isIdle: true, - isPending: false, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.published.run.post': () => ({ + mutateAsync: mockRunPublishedPipeline, + isIdle: true, + isPending: false, + }), + }) +}) vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn(), })) @@ -206,11 +209,10 @@ describe('useDatasourceActions', () => { }) expect(mockRunPublishedPipeline).toHaveBeenCalledWith( - expect.objectContaining({ - pipeline_id: 'pipeline-1', - is_preview: true, - start_node_id: 'node-1', - }), + { + params: { pipeline_id: 'pipeline-1' }, + body: expect.objectContaining({ is_preview: true, start_node_id: 'node-1' }), + }, expect.anything(), ) }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/hooks/use-datasource-actions.ts b/web/app/components/datasets/documents/create-from-pipeline/hooks/use-datasource-actions.ts index e6e65cb72e0513..f4f0924f08c5dd 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/hooks/use-datasource-actions.ts +++ b/web/app/components/datasets/documents/create-from-pipeline/hooks/use-datasource-actions.ts @@ -8,15 +8,16 @@ import type { CustomFile as File, FileIndexingEstimateResponse, } from '@/models/datasets' -import type { - OnlineDriveFile, - PublishedPipelineRunPreviewResponse, - PublishedPipelineRunResponse, -} from '@/models/pipeline' +import type { OnlineDriveFile, PublishedPipelineRunResponse } from '@/models/pipeline' +import { useMutation } from '@tanstack/react-query' import { useCallback, useRef } from 'react' import { trackEvent } from '@/app/components/base/amplitude' +import { + isPipelineProcessingRun, + selectPreviewRunEstimate, +} from '@/app/components/rag-pipeline/utils/query-select' import { DatasourceType } from '@/models/pipeline' -import { useRunPublishedPipeline } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import { buildLocalFileDatasourceInfo, buildOnlineDocumentDatasourceInfo, @@ -65,7 +66,13 @@ export const useDatasourceActions = ({ const isPreview = useRef(false) const formRef = useRef<{ submit: () => void } | null>(null) - const { mutateAsync: runPublishedPipeline, isIdle, isPending } = useRunPublishedPipeline() + const { + mutateAsync: runPublishedPipeline, + isIdle, + isPending, + } = useMutation( + consoleQuery.rag.pipelines.byPipelineId.workflows.published.run.post.mutationOptions(), + ) // Build datasource info for preview (single item) const buildPreviewDatasourceInfo = useCallback(() => { @@ -162,16 +169,19 @@ export const useDatasourceActions = ({ const datasourceInfoList = buildPreviewDatasourceInfo() await runPublishedPipeline( { - pipeline_id: pipelineId, - inputs: data, - start_node_id: datasource.nodeId, - datasource_type: datasourceType as DatasourceType, - datasource_info_list: datasourceInfoList, - is_preview: true, + params: { pipeline_id: pipelineId }, + body: { + inputs: data, + start_node_id: datasource.nodeId, + datasource_type: datasourceType as DatasourceType, + datasource_info_list: datasourceInfoList, + is_preview: true, + response_mode: 'blocking', + }, }, { onSuccess: (res) => { - setEstimateData((res as PublishedPipelineRunPreviewResponse).data.outputs) + if (!isPipelineProcessingRun(res)) setEstimateData(selectPreviewRunEstimate(res)) }, }, ) @@ -196,17 +206,21 @@ export const useDatasourceActions = ({ const datasourceInfoList = buildProcessDatasourceInfo() await runPublishedPipeline( { - pipeline_id: pipelineId, - inputs: data, - start_node_id: datasource.nodeId, - datasource_type: datasourceType as DatasourceType, - datasource_info_list: datasourceInfoList, - is_preview: false, + params: { pipeline_id: pipelineId }, + body: { + inputs: data, + start_node_id: datasource.nodeId, + datasource_type: datasourceType as DatasourceType, + datasource_info_list: datasourceInfoList, + is_preview: false, + response_mode: 'blocking', + }, }, { onSuccess: (res) => { - setBatchId((res as PublishedPipelineRunResponse).batch || '') - setDocuments((res as PublishedPipelineRunResponse).documents || []) + if (!isPipelineProcessingRun(res)) return + setBatchId(res.batch) + setDocuments(res.documents) handleNextStep() trackEvent('dataset_document_added', { data_source_type: datasourceType, diff --git a/web/app/components/datasets/documents/create-from-pipeline/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/index.tsx index 60f4b51ccb4c3e..263f83825c135c 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/index.tsx @@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' +import { selectPublishedPipelineNodes } from '@/app/components/rag-pipeline/utils/query-select' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom, @@ -21,8 +22,6 @@ import { deploymentEditionAtom } from '@/features/system-features/state' import { DatasourceType } from '@/models/pipeline' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/console' -import { useFileUploadConfig } from '@/service/use-common' -import { usePublishedPipelineInfo } from '@/service/use-pipeline' import { getDatasetACLCapabilities } from '@/utils/permission' import { useDataSourceStore } from './data-source/store' import DataSourceProvider from './data-source/store/provider' @@ -50,7 +49,7 @@ const CreateFormPipeline = () => { }), ) const dataset = useDatasetDetailContextWithSelector((s) => s.dataset) - const pipelineId = dataset?.pipeline_id + const pipelineId = dataset?.pipeline_id ?? undefined const { data: currentUserId } = useSuspenseQuery({ ...userProfileQueryOptions(), select: (data) => data.profile.id, @@ -75,10 +74,14 @@ const CreateFormPipeline = () => { const [documents, setDocuments] = useState([]) // Data fetching - const { data: pipelineInfo, isFetching: isFetchingPipelineInfo } = usePublishedPipelineInfo( - pipelineId || '', + const { data: pipelineNodes, isFetching: isFetchingPipelineInfo } = useQuery( + consoleQuery.rag.pipelines.byPipelineId.workflows.publish.get.queryOptions({ + input: { params: { pipeline_id: pipelineId || '' } }, + enabled: !!pipelineId, + select: selectPublishedPipelineNodes, + }), ) - const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: fileUploadConfigResponse } = useQuery(consoleQuery.files.upload.get.queryOptions()) const fileUploadConfig = useMemo( () => ({ @@ -265,7 +268,7 @@ const CreateFormPipeline = () => { []} + pipelineNodes={(pipelineNodes || []) as Node[]} supportBatchUpload={supportBatchUpload} showBatchUploadUpgrade={deploymentEdition === 'CLOUD' && plan === 'sandbox'} isShowVectorSpaceFull={isShowVectorSpaceFull} diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/chunk-preview.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/chunk-preview.spec.tsx index 43a2b325e59f48..b38907c0739e55 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/chunk-preview.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/chunk-preview.spec.tsx @@ -97,7 +97,6 @@ const createMockOnlineDriveFile = (overrides?: Partial): Online const createMockEstimateData = ( overrides?: Partial, ): FileIndexingEstimateResponse => ({ - total_nodes: 5, tokens: 1000, total_price: 0.01, currency: 'USD', diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/file-preview.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/file-preview.spec.tsx index 445644c7e266f8..178d9e8f2d7073 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/file-preview.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/file-preview.spec.tsx @@ -6,13 +6,6 @@ import FilePreview from '../file-preview' const mockFileData = { content: 'file content here with some text' } let mockIsFetching = false -vi.mock('@/service/use-common', () => ({ - useFilePreview: () => ({ - data: mockIsFetching ? undefined : mockFileData, - isFetching: mockIsFetching, - }), -})) - vi.mock('../../../../common/document-file-icon', () => ({ default: () => , })) @@ -21,6 +14,16 @@ vi.mock('../loading', () => ({ default: () =>
, })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.byFileId.preview.get': () => ({ + data: mockIsFetching ? undefined : mockFileData, + isFetching: mockIsFetching, + }), + }) +}) + describe('FilePreview', () => { const defaultProps = { file: { diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/online-document-preview.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/online-document-preview.spec.tsx index 821f3744b545dd..c7fa957196f37a 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/online-document-preview.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/online-document-preview.spec.tsx @@ -33,9 +33,13 @@ vi.mock('@/context/dataset-detail', () => ({ // Mock usePreviewOnlineDocument hook - needs mock to control mutation behavior const mockMutateAsync = vi.fn() const mockUsePreviewOnlineDocument = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - usePreviewOnlineDocument: () => mockUsePreviewOnlineDocument(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.published.datasource.nodes.byNodeId.preview.post': () => + mockUsePreviewOnlineDocument(), + }) +}) // Mock data source store - needs mock to control store state const mockCurrentCredentialId = 'credential-123' @@ -124,12 +128,12 @@ describe('OnlineDocumentPreview', () => { await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith( { - workspaceID: 'ws-123', - pageID: 'pg-456', - pageType: 'page', - pipelineId: 'pipeline-123', - datasourceNodeId: 'node-789', - credentialId: mockCurrentCredentialId, + params: { pipeline_id: 'pipeline-123', node_id: 'node-789' }, + body: { + datasource_type: 'online_document', + credential_id: mockCurrentCredentialId, + inputs: { workspace_id: 'ws-123', page_id: 'pg-456', type: 'page' }, + }, }, expect.objectContaining({ onSuccess: expect.any(Function), @@ -166,7 +170,7 @@ describe('OnlineDocumentPreview', () => { await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - pipelineId: '', + params: expect.objectContaining({ pipeline_id: '' }), }), expect.anything(), ) @@ -338,7 +342,7 @@ describe('OnlineDocumentPreview', () => { await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - credentialId: 'custom-credential', + body: expect.objectContaining({ credential_id: 'custom-credential' }), }), expect.anything(), ) diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/chunk-preview.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/chunk-preview.tsx index c4a989400755f6..252857f13725d4 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/chunk-preview.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/chunk-preview.tsx @@ -198,7 +198,7 @@ const ChunkPreview = ({ characterCount={item.content.length} > - {item.child_chunks.map((child, index) => { + {(item.child_chunks ?? []).map((child, index) => { const indexForLabel = index + 1 return ( { const { t } = useTranslation(['datasetPipeline']) - const { data: fileData, isFetching } = useFilePreview(file.id || '') + const { data: fileData, isFetching } = useQuery( + consoleQuery.files.byFileId.preview.get.queryOptions({ + input: file.id ? { params: { file_id: file.id } } : skipToken, + }), + ) const fileName = useMemo(() => { if (!file) return '' diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/online-document-preview.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/online-document-preview.tsx index 333783c018120e..af5348f0590bb8 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/online-document-preview.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/online-document-preview.tsx @@ -1,13 +1,16 @@ 'use client' import type { NotionPage } from '@/models/common' import { RiCloseLine } from '@remixicon/react' +import { useMutation } from '@tanstack/react-query' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Markdown } from '@/app/components/base/markdown' +import { selectOnlineDocumentPreview } from '@/app/components/rag-pipeline/utils/query-select' import { toast } from '@/app/notifications' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' -import { usePreviewOnlineDocument } from '@/service/use-pipeline' +import { DatasourceType } from '@/models/pipeline' +import { consoleQuery } from '@/service/console' import { formatNumberAbbreviated } from '@/utils/format' import { useDataSourceStore } from '../data-source/store' import Loading from './loading' @@ -26,23 +29,29 @@ const OnlineDocumentPreview = ({ const { t } = useTranslation(['datasetPipeline']) const [content, setContent] = useState('') const pipelineId = useDatasetDetailContextWithSelector((state) => state.dataset?.pipeline_id) - const { mutateAsync: getOnlineDocumentContent, isPending } = usePreviewOnlineDocument() + const { mutateAsync: getOnlineDocumentContent, isPending } = useMutation( + consoleQuery.rag.pipelines.byPipelineId.workflows.published.datasource.nodes.byNodeId.preview.post.mutationOptions(), + ) const dataSourceStore = useDataSourceStore() useEffect(() => { const { currentCredentialId } = dataSourceStore.getState() getOnlineDocumentContent( { - workspaceID: currentPage.workspace_id, - pageID: currentPage.page_id, - pageType: currentPage.type, - pipelineId: pipelineId || '', - datasourceNodeId, - credentialId: currentCredentialId, + params: { pipeline_id: pipelineId || '', node_id: datasourceNodeId }, + body: { + datasource_type: DatasourceType.onlineDocument, + credential_id: currentCredentialId, + inputs: { + workspace_id: currentPage.workspace_id, + page_id: currentPage.page_id, + type: currentPage.type, + }, + }, }, { onSuccess(data) { - setContent(data.content) + setContent(selectOnlineDocumentPreview(data).content) }, onError(error) { toast.error(error.message) diff --git a/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/hooks.spec.ts b/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/hooks.spec.ts index a52658d7bb4049..15f3f812e223d6 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/hooks.spec.ts +++ b/web/app/components/datasets/documents/create-from-pipeline/process-documents/__tests__/hooks.spec.ts @@ -1,4 +1,3 @@ -import type { PipelineProcessingParamsRequest } from '@/models/pipeline' import { renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { useInputVariables } from '../hooks' @@ -10,10 +9,19 @@ vi.mock('@/context/dataset-detail', () => ({ useDatasetDetailContextWithSelector: (selector: (value: unknown) => unknown) => mockUseDatasetDetailContextWithSelector(selector), })) -vi.mock('@/service/use-pipeline', () => ({ - usePublishedPipelineProcessingParams: (params: PipelineProcessingParamsRequest) => - mockUsePublishedPipelineProcessingParams(params), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.published.processing.parameters.get': (o: { + input: { params: { pipeline_id: string }; query: { node_id: string } } + enabled?: boolean + }) => + mockUsePublishedPipelineProcessingParams({ + pipeline_id: o.input.params.pipeline_id, + node_id: o.input.query.node_id, + }), + }) +}) describe('useInputVariables', () => { beforeEach(() => { diff --git a/web/app/components/datasets/documents/create-from-pipeline/process-documents/hooks.ts b/web/app/components/datasets/documents/create-from-pipeline/process-documents/hooks.ts index dd5655d062b7e8..08d60249be5082 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/process-documents/hooks.ts +++ b/web/app/components/datasets/documents/create-from-pipeline/process-documents/hooks.ts @@ -1,13 +1,17 @@ +import { useQuery } from '@tanstack/react-query' +import { selectPipelineStepParameters } from '@/app/components/rag-pipeline/utils/query-select' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' -import { usePublishedPipelineProcessingParams } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' export const useInputVariables = (datasourceNodeId: string) => { const pipelineId = useDatasetDetailContextWithSelector((state) => state.dataset?.pipeline_id) - const { data: paramsConfig, isFetching: isFetchingParams } = usePublishedPipelineProcessingParams( - { - pipeline_id: pipelineId!, - node_id: datasourceNodeId, - }, + const { data: paramsConfig, isFetching: isFetchingParams } = useQuery( + consoleQuery.rag.pipelines.byPipelineId.workflows.published.processing.parameters.get.queryOptions( + { + input: { params: { pipeline_id: pipelineId! }, query: { node_id: datasourceNodeId } }, + select: selectPipelineStepParameters, + }, + ), ) return { diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx index 8d703d513ad6fa..ada869aa1e8053 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx @@ -1,13 +1,15 @@ import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ReactElement } from 'react' import type { Mock } from 'vite-plus/test' -import type { DocumentIndexingStatus, IndexingStatusResponse } from '@/models/datasets' +import type { IndexingStatusResponse } from '@/models/datasets' import type { InitialDocumentDetail } from '@/models/pipeline' +import { QueryClient } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import * as React from 'react' import { IndexingType } from '@/app/components/datasets/create/step-two' import { DatasourceType } from '@/models/pipeline' +import { consoleQuery } from '@/service/console' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' import { RETRIEVE_METHOD } from '@/types/app' @@ -51,26 +53,23 @@ vi.mock('@/app/components/datasets/common/vector-space-admission-alert', () => ( ), })) -// Mock useIndexingStatusBatch hook +// Mock the batch indexing status request let mockFetchIndexingStatus: Mock let mockIndexingStatusData: IndexingStatusResponse[] = [] -vi.mock('@/service/knowledge/use-dataset', () => ({ - useIndexingStatusBatch: () => ({ - mutateAsync: mockFetchIndexingStatus, - }), - useProcessRule: () => ({ - data: { - mode: 'custom', - rules: { parent_mode: 'paragraph' }, - }, - }), -})) - -// Mock useInvalidDocumentList hook -const mockInvalidDocumentList = vi.fn() -vi.mock('@/service/knowledge/use-document', () => ({ - useInvalidDocumentList: () => mockInvalidDocumentList, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.batch.byBatch.indexingStatus.get': () => ({ + mutateAsync: (...args: unknown[]) => mockFetchIndexingStatus(...args), + }), + 'datasets.processRule.get': () => ({ + data: { + mode: 'custom', + rules: { parent_mode: 'paragraph' }, + }, + }), + }) +}) // Mock useDatasetApiAccessUrl hook vi.mock('@/hooks/use-api-access-url', () => ({ @@ -91,9 +90,9 @@ const createMockDocument = ( name: 'test-document.txt', data_source_type: DatasourceType.localFile, data_source_info: {}, - enable: true, + enabled: true, error: '', - indexing_status: 'waiting' as DocumentIndexingStatus, + indexing_status: 'waiting' as IndexingStatusResponse['indexing_status'], position: 0, ...overrides, }) @@ -105,7 +104,7 @@ const createMockIndexingStatus = ( overrides: Partial = {}, ): IndexingStatusResponse => ({ id: `doc-${Math.random().toString(36).slice(2, 9)}`, - indexing_status: 'waiting' as DocumentIndexingStatus, + indexing_status: 'waiting' as IndexingStatusResponse['indexing_status'], processing_started_at: Date.now(), parsing_completed_at: 0, cleaning_completed_at: 0, @@ -155,6 +154,7 @@ function render(ui: ReactElement) { describe('EmbeddingProcess', () => { beforeEach(() => { + vi.spyOn(QueryClient.prototype, 'invalidateQueries') vi.clearAllMocks() vi.useFakeTimers({ shouldAdvanceTime: true }) @@ -662,7 +662,9 @@ describe('EmbeddingProcess', () => { expect(link).toHaveAttribute('href', '/datasets/my-dataset-123/documents') fireEvent.click(link) - expect(mockInvalidDocumentList).toHaveBeenCalled() + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key(), + }) }) }) @@ -847,23 +849,10 @@ describe('EmbeddingProcess', () => { describe('Edge Cases', () => { // Tests for boundary conditions - it('should throw error when documents array is empty', () => { - // The component accesses documents[0].id for useProcessRule (line 81-82), - // which throws TypeError when documents array is empty. - // This test documents this known limitation. + it('should render without documents', () => { const props = createDefaultProps({ documents: [] }) - // Suppress console errors for expected error - const consoleError = vi - .spyOn(console, 'error') - .mockImplementation(Function.prototype as () => void) - - // Act & Assert - explicitly assert the error behavior - expect(() => { - render() - }).toThrow(TypeError) - - consoleError.mockRestore() + expect(() => render()).not.toThrow() }) it('should handle empty indexing status response', async () => { @@ -908,7 +897,7 @@ describe('EmbeddingProcess', () => { mockIndexingStatusData = [ createMockIndexingStatus({ id: 'doc-1', - indexing_status: undefined as unknown as DocumentIndexingStatus, + indexing_status: undefined as unknown as IndexingStatusResponse['indexing_status'], }), ] const props = createDefaultProps({ documents: [doc1] }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx index 6dcf5747a8748c..0dabd932adf1c6 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx @@ -1,4 +1,5 @@ import type { IndexingType } from '@/app/components/datasets/create/step-two' +import type { DataSourceNotionPage } from '@/models/common' import type { IndexingStatusResponse } from '@/models/datasets' import type { InitialDocumentDetail } from '@/models/pipeline' import type { RETRIEVE_METHOD } from '@/types/app' @@ -14,7 +15,7 @@ import { RiLoader2Fill, RiTerminalBoxLine, } from '@remixicon/react' -import { useQuery } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useEffect, useMemo, useState } from 'react' @@ -29,10 +30,11 @@ import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' import { DatasourceType } from '@/models/pipeline' import Link from '@/next/link' import { consoleQuery } from '@/service/console' -import { useIndexingStatusBatch, useProcessRule } from '@/service/knowledge/use-dataset' -import { useInvalidDocumentList } from '@/service/knowledge/use-document' import RuleDetail from './rule-detail' +const isNotionPageIcon = (value: unknown): value is DataSourceNotionPage['page_icon'] => + typeof value === 'object' && value !== null && 'type' in value + type EmbeddingProcessProps = { datasetId: string batchId: string @@ -60,25 +62,30 @@ const EmbeddingProcess = ({ [], ) const [shouldPoll, setShouldPoll] = useState(true) - const { mutateAsync: fetchIndexingStatus } = useIndexingStatusBatch({ datasetId, batchId }) + const { mutateAsync: fetchIndexingStatus } = useMutation( + consoleQuery.datasets.byDatasetId.batch.byBatch.indexingStatus.get.mutationOptions(), + ) useEffect(() => { let timeoutId: ReturnType const fetchData = async () => { - await fetchIndexingStatus(undefined, { - onSuccess: (res) => { - const indexingStatusDetailList = res.data - setIndexingStatusDetail(indexingStatusDetailList) - const isCompleted = indexingStatusDetailList.every((indexingStatusDetail) => - ['completed', 'error', 'paused'].includes(indexingStatusDetail.indexing_status), - ) - if (isCompleted) setShouldPoll(false) - }, - onSettled: () => { - if (shouldPoll) timeoutId = setTimeout(fetchData, 2500) + await fetchIndexingStatus( + { params: { dataset_id: datasetId, batch: batchId } }, + { + onSuccess: (res) => { + const indexingStatusDetailList = res.data + setIndexingStatusDetail(indexingStatusDetailList) + const isCompleted = indexingStatusDetailList.every((indexingStatusDetail) => + ['completed', 'error', 'paused'].includes(indexingStatusDetail.indexing_status), + ) + if (isCompleted) setShouldPoll(false) + }, + onSettled: () => { + if (shouldPoll) timeoutId = setTimeout(fetchData, 2500) + }, }, - }) + ) } fetchData() @@ -90,9 +97,18 @@ const EmbeddingProcess = ({ // get rule const firstDocument = documents[0] - const { data: ruleDetail } = useProcessRule(firstDocument!.id) + const { data: ruleDetail } = useQuery( + consoleQuery.datasets.processRule.get.queryOptions({ + input: { query: { document_id: firstDocument?.id } }, + enabled: !!firstDocument, + }), + ) - const invalidDocumentList = useInvalidDocumentList() + const queryClient = useQueryClient() + const invalidDocumentList = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key(), + }) const documentsHref = `/datasets/${datasetId}/documents` const apiReferenceUrl = useDatasetApiAccessUrl() @@ -145,7 +161,8 @@ const EmbeddingProcess = ({ const getIcon = (id: string) => { const doc = documents.find((document) => document.id === id) - return doc?.data_source_info.notion_page_icon + const icon = doc?.data_source_info?.notion_page_icon + return typeof icon === 'string' || isNotionPageIcon(icon) ? icon : undefined } const isSourceEmbedding = (detail: IndexingStatusResponse) => ['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'].includes( @@ -235,7 +252,7 @@ const EmbeddingProcess = ({ diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/index.tsx index 8034b74fe3cfd1..8915459f609405 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/index.tsx @@ -16,7 +16,9 @@ const Processing = ({ batchId, documents }: ProcessingProps) => { const { t } = useTranslation(['datasetCreation', 'datasetPipeline']) const docLink = useDocLink() const datasetId = useDatasetDetailContextWithSelector((s) => s.dataset?.id) - const indexingType = useDatasetDetailContextWithSelector((s) => s.dataset?.indexing_technique) + const indexingType = useDatasetDetailContextWithSelector( + (s) => s.dataset?.indexing_technique ?? undefined, + ) const retrievalMethod = useDatasetDetailContextWithSelector( (s) => s.dataset?.retrieval_model_dict?.search_method, ) diff --git a/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/preview-panel.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/preview-panel.spec.tsx index 0eae8df4f6b19d..d4b66f5ddb1a6c 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/preview-panel.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/preview-panel.spec.tsx @@ -13,6 +13,21 @@ import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { DatasourceType } from '@/models/pipeline' import { StepOnePreview, StepTwoPreview } from '../preview-panel' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.published.datasource.nodes.byNodeId.preview.post': + () => ({ + mutateAsync: vi.fn().mockResolvedValue({ content: 'Mock document content' }), + isPending: false, + }), + 'files.byFileId.preview.get': vi.fn(() => ({ + data: { content: 'Mock file content for testing' }, + isFetching: false, + })), + }) +}) + // Mock context hooks (底层依赖) vi.mock('@/context/dataset-detail', () => ({ useDatasetDetailContextWithSelector: vi.fn((selector: (state: unknown) => unknown) => { @@ -28,19 +43,6 @@ vi.mock('@/context/dataset-detail', () => ({ })) // Mock API hooks (底层依赖) -vi.mock('@/service/use-common', () => ({ - useFilePreview: vi.fn(() => ({ - data: { content: 'Mock file content for testing' }, - isFetching: false, - })), -})) - -vi.mock('@/service/use-pipeline', () => ({ - usePreviewOnlineDocument: vi.fn(() => ({ - mutateAsync: vi.fn().mockResolvedValue({ content: 'Mock document content' }), - isPending: false, - })), -})) // Mock data source store vi.mock('../../data-source/store', () => ({ diff --git a/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/step-one-content.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/step-one-content.spec.tsx index 3ef21cb78d51f8..3f9a7c05d14267 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/step-one-content.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/steps/__tests__/step-one-content.spec.tsx @@ -180,33 +180,6 @@ vi.mock('@/next/navigation', () => ({ })) // Mock pipeline service hooks -vi.mock('@/service/use-pipeline', () => ({ - useNotionWorkspaces: vi.fn(() => ({ - data: [], - isLoading: false, - })), - useNotionPages: vi.fn(() => ({ - data: { pages: [] }, - isLoading: false, - })), - useCrawlResult: vi.fn(() => ({ - data: { data: [] }, - isLoading: false, - })), - useSupportedOauth: vi.fn(() => ({ - data: [], - isLoading: false, - })), - useOnlineDriveCredentialList: vi.fn(() => ({ - data: [], - isLoading: false, - })), - useOnlineDriveFileList: vi.fn(() => ({ - data: { data: [] }, - isLoading: false, - })), -})) - function render(...args: Parameters) { args[0] = {args[0]} return renderWithoutPricing(...args) diff --git a/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx index 3a6d3d2e7c7d46..b0ab3e348ff7f2 100644 --- a/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx @@ -1,5 +1,7 @@ +import { QueryClient } from '@tanstack/react-query' import { act, fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQuery } from '@/service/console' import { renderWithAccountProfile as render } from '@/test/console/account-profile' import { DatasetACLPermission } from '@/utils/permission' @@ -22,9 +24,6 @@ const mocks = vi.hoisted(() => { detailRefetch: vi.fn(), checkProgress: vi.fn(), batchImport: vi.fn(), - invalidDocumentList: vi.fn(), - invalidSegmentList: vi.fn(), - invalidChildSegmentList: vi.fn(), useDocumentTitle: vi.fn(), toastNotify: vi.fn(), } @@ -57,37 +56,25 @@ vi.mock('@/context/dataset-detail', () => ({ selector({ dataset: mocks.state.dataset }), })) -vi.mock('@/service/knowledge/use-document', () => ({ - useDocumentDetail: () => ({ - data: mocks.state.documentDetail, - error: mocks.state.documentError, - refetch: mocks.detailRefetch, - }), - useDocumentMetadata: () => ({ - data: mocks.state.documentMetadata, - }), - useInvalidDocumentList: () => mocks.invalidDocumentList, -})) - -vi.mock('@/service/knowledge/use-segment', () => ({ - useCheckSegmentBatchImportProgress: () => ({ - mutateAsync: mocks.checkProgress, - }), - useSegmentBatchImport: () => ({ - mutateAsync: mocks.batchImport, - }), - useSegmentListKey: ['segment-list'], - useChildSegmentListKey: ['child-segment-list'], -})) - -vi.mock('@/service/use-base', () => ({ - useInvalid: (key: unknown) => { - const keyStr = JSON.stringify(key) - if (keyStr === JSON.stringify(['segment-list'])) return mocks.invalidSegmentList - if (keyStr === JSON.stringify(['child-segment-list'])) return mocks.invalidChildSegmentList - return vi.fn() - }, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.get': (options: { + input: { query: { metadata: string } } + }) => + options.input.query.metadata === 'only' + ? { data: mocks.state.documentMetadata } + : { + data: mocks.state.documentDetail, + error: mocks.state.documentError, + refetch: mocks.detailRefetch, + }, + 'datasets.batchImportStatus.byJobId.get': () => ({ mutateAsync: mocks.checkProgress }), + 'datasets.byDatasetId.documents.byDocumentId.segments.batchImport.post': () => ({ + mutateAsync: mocks.batchImport, + }), + }) +}) vi.mock('@/app/notifications', () => ({ default: { notify: mocks.toastNotify }, @@ -255,6 +242,23 @@ vi.mock('@/app/components/base/float-right-container', () => ({ ) : null, })) +const expectDocumentListInvalidated = () => + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params: { dataset_id: 'ds-1' } }, + }), + }) + +const segmentListKey = consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.get.key({ + input: { params: { dataset_id: 'ds-1', document_id: 'doc-1' } }, +}) +const childChunkListKey = + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.get.key( + { + input: { params: { dataset_id: 'ds-1', document_id: 'doc-1' } }, + }, + ) + // --- Lazy import (after all vi.mock calls) --- const { default: DocumentDetail } = await import('../index') @@ -284,6 +288,7 @@ vi.mock('@/context/permission-state', async () => { describe('DocumentDetail', () => { beforeEach(() => { vi.clearAllMocks() + vi.spyOn(QueryClient.prototype, 'invalidateQueries') vi.useFakeTimers() mocks.state.dataset = { embedding_available: true, @@ -468,13 +473,13 @@ describe('DocumentDetail', () => { it('should invalidate document list on any operation', () => { render() fireEvent.click(screen.getByTestId('op-rename')) - expect(mocks.invalidDocumentList).toHaveBeenCalled() + expectDocumentListInvalidated() }) it('should navigate back on delete operation', () => { render() fireEvent.click(screen.getByTestId('op-delete')) - expect(mocks.invalidDocumentList).toHaveBeenCalled() + expectDocumentListInvalidated() expect(mocks.push).toHaveBeenCalled() }) @@ -488,12 +493,18 @@ describe('DocumentDetail', () => { render() fireEvent.click(screen.getByTestId('op-rename')) - expect(mocks.invalidSegmentList).not.toHaveBeenCalled() + expect(QueryClient.prototype.invalidateQueries).not.toHaveBeenCalledWith({ + queryKey: segmentListKey, + }) act(() => { vi.advanceTimersByTime(5000) }) - expect(mocks.invalidSegmentList).toHaveBeenCalled() - expect(mocks.invalidChildSegmentList).toHaveBeenCalled() + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: segmentListKey, + }) + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: childChunkListKey, + }) }) it('should not invalidate chunk lists on operation with no name', () => { @@ -504,7 +515,9 @@ describe('DocumentDetail', () => { act(() => { vi.advanceTimersByTime(5000) }) - expect(mocks.invalidSegmentList).not.toHaveBeenCalled() + expect(QueryClient.prototype.invalidateQueries).not.toHaveBeenCalledWith({ + queryKey: segmentListKey, + }) }) }) @@ -535,7 +548,7 @@ describe('DocumentDetail', () => { expect(mocks.batchImport).toHaveBeenCalledWith( { - url: '/datasets/ds-1/documents/doc-1/segments/batch_import', + params: { dataset_id: 'ds-1', document_id: 'doc-1' }, body: { upload_file_id: 'file-1' }, }, expect.objectContaining({ @@ -544,6 +557,59 @@ describe('DocumentDetail', () => { }), ) }) + + type JobCallbacks = { onSuccess: (job: { job_id: string; job_status: string }) => void } + + it('should poll the import job until it finishes', async () => { + mocks.batchImport.mockImplementation((_input: unknown, callbacks: JobCallbacks) => + callbacks.onSuccess({ job_id: 'job-1', job_status: 'waiting' }), + ) + mocks.checkProgress + .mockImplementationOnce((_input: unknown, callbacks: JobCallbacks) => + callbacks.onSuccess({ job_id: 'job-1', job_status: 'processing' }), + ) + .mockImplementationOnce((_input: unknown, callbacks: JobCallbacks) => + callbacks.onSuccess({ job_id: 'job-1', job_status: 'completed' }), + ) + render() + fireEvent.click(screen.getByTestId('batch-btn')) + + await act(async () => { + fireEvent.click(screen.getByTestId('batch-confirm')) + }) + await act(async () => { + vi.advanceTimersByTime(2500) + }) + await act(async () => { + vi.advanceTimersByTime(2500) + }) + + expect(mocks.checkProgress).toHaveBeenCalledTimes(2) + expect(mocks.checkProgress).toHaveBeenCalledWith( + { params: { job_id: 'job-1' } }, + expect.any(Object), + ) + }) + + it('should stop polling when the job reports an unknown status', async () => { + mocks.batchImport.mockImplementation((_input: unknown, callbacks: JobCallbacks) => + callbacks.onSuccess({ job_id: 'job-1', job_status: 'waiting' }), + ) + mocks.checkProgress.mockImplementation((_input: unknown, callbacks: JobCallbacks) => + callbacks.onSuccess({ job_id: 'job-1', job_status: 'queued' }), + ) + render() + fireEvent.click(screen.getByTestId('batch-btn')) + + await act(async () => { + fireEvent.click(screen.getByTestId('batch-confirm')) + }) + await act(async () => { + vi.advanceTimersByTime(5000) + }) + + expect(mocks.checkProgress).toHaveBeenCalledTimes(1) + }) }) describe('isFullDocMode', () => { diff --git a/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx index b8af9cf95877e5..e64e26f100710a 100644 --- a/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx +++ b/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx @@ -43,11 +43,14 @@ vi.mock('../completed', () => ({ // Mock useAddSegment const mockAddSegment = vi.fn() -vi.mock('@/service/knowledge/use-segment', () => ({ - useAddSegment: () => ({ - mutateAsync: mockAddSegment, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.segment.post': () => ({ + mutateAsync: mockAddSegment, + }), + }) +}) vi.mock('../completed/common/action-buttons', () => ({ ActionButtons: ({ @@ -333,8 +336,7 @@ describe('NewSegmentModal', () => { await waitFor(() => { expect(mockAddSegment).toHaveBeenCalledWith( expect.objectContaining({ - datasetId: 'test-dataset-id', - documentId: 'test-document-id', + params: { dataset_id: 'test-dataset-id', document_id: 'test-document-id' }, body: expect.objectContaining({ content: 'Valid content', }), diff --git a/web/app/components/datasets/documents/detail/batch-modal/__tests__/csv-uploader.spec.tsx b/web/app/components/datasets/documents/detail/batch-modal/__tests__/csv-uploader.spec.tsx index d8de2327aa854a..061dcdb61b1c59 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/__tests__/csv-uploader.spec.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/__tests__/csv-uploader.spec.tsx @@ -4,6 +4,15 @@ import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { Theme } from '@/types/app' import CSVUploader from '../csv-uploader' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ + data: { file_size_limit: 15 }, + }), + }) +}) + // Mock upload service const mockUpload = vi.fn() vi.mock('@/service/base', () => ({ @@ -11,11 +20,6 @@ vi.mock('@/service/base', () => ({ })) // Mock useFileUploadConfig -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ - data: { file_size_limit: 15 }, - }), -})) // Mock useTheme vi.mock('@/hooks/use-theme', () => ({ diff --git a/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx b/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx index ea879a192e7689..df0565031a2b75 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx @@ -4,6 +4,7 @@ import type { FileItem } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { RiDeleteBinLine } from '@remixicon/react' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,7 +13,7 @@ import SimplePieChart from '@/app/components/base/simple-pie-chart' import { toast } from '@/app/notifications' import useTheme from '@/hooks/use-theme' import { upload } from '@/service/base' -import { useFileUploadConfig } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { Theme } from '@/types/app' type Props = Readonly<{ @@ -25,7 +26,7 @@ const CSVUploader: FC = ({ file, updateFile }) => { const dropRef = useRef(null) const dragRef = useRef(null) const fileUploader = useRef(null) - const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: fileUploadConfigResponse } = useQuery(consoleQuery.files.upload.get.queryOptions()) const fileUploadConfig = useMemo( () => ({ ...fileUploadConfigResponse, diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx index 5fff6576e2a3a1..eba3633d12f8b3 100644 --- a/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx @@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { ChunkingMode as ChunkingModeEnum } from '@/models/datasets' +import { consoleQuery } from '@/service/console' import Completed from '../index' import { SegmentListContext, useSegmentListContext } from '../segment-list-context' @@ -21,9 +22,6 @@ const { mockEventEmitter, mockSegmentListData, mockChildSegmentListData, - mockInvalidChunkListAll, - mockInvalidChunkListEnabled, - mockInvalidChunkListDisabled, mockOnChangeSwitch, mockOnDelete, } = vi.hoisted(() => ({ @@ -55,9 +53,6 @@ const { total: 0, total_pages: 0, }, - mockInvalidChunkListAll: vi.fn(), - mockInvalidChunkListEnabled: vi.fn(), - mockInvalidChunkListDisabled: vi.fn(), mockOnChangeSwitch: vi.fn(), mockOnDelete: vi.fn(), })) @@ -86,37 +81,28 @@ vi.mock('@/context/event-emitter', () => ({ useEventEmitterContextContext: () => ({ eventEmitter: mockEventEmitter }), })) -vi.mock('@/service/knowledge/use-segment', () => ({ - useSegmentList: () => ({ - isLoading: false, - data: mockSegmentListData, - }), - useChildSegmentList: () => ({ - isLoading: false, - data: mockChildSegmentListData, - }), - useSegmentListKey: ['segment', 'chunkList'], - useChunkListAllKey: ['segment', 'chunkList', { enabled: 'all' }], - useChunkListEnabledKey: ['segment', 'chunkList', { enabled: true }], - useChunkListDisabledKey: ['segment', 'chunkList', { enabled: false }], - useChildSegmentListKey: ['segment', 'childChunkList'], - useEnableSegment: () => ({ mutateAsync: mockOnChangeSwitch }), - useDisableSegment: () => ({ mutateAsync: mockOnChangeSwitch }), - useDeleteSegment: () => ({ mutateAsync: mockOnDelete }), - useUpdateSegment: () => ({ mutateAsync: vi.fn() }), - useDeleteChildSegment: () => ({ mutateAsync: vi.fn() }), - useUpdateChildSegment: () => ({ mutateAsync: vi.fn() }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.segments.get': () => ({ + isLoading: false, + data: mockSegmentListData, + }), + 'datasets.byDatasetId.documents.byDocumentId.segment.byAction.patch': () => ({ + mutateAsync: mockOnChangeSwitch, + }), + 'datasets.byDatasetId.documents.byDocumentId.segments.delete': () => ({ + mutateAsync: mockOnDelete, + }), + 'datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.patch': () => ({ + mutateAsync: vi.fn(), + }), + }) +}) -vi.mock('@/service/use-base', () => ({ - useInvalid: (key: unknown[]) => { - const keyStr = JSON.stringify(key) - if (keyStr.includes('"enabled":"all"')) return mockInvalidChunkListAll - if (keyStr.includes('"enabled":true')) return mockInvalidChunkListEnabled - if (keyStr.includes('"enabled":false')) return mockInvalidChunkListDisabled - return vi.fn() - }, -})) +const segmentListsKey = consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.get.key({ + input: { params: { dataset_id: 'test-dataset-id', document_id: 'test-document-id' } }, +}) let capturedRefreshCallback: (() => void) | null = null vi.mock('../hooks/use-child-segment-data', () => ({ @@ -268,6 +254,9 @@ const createMockSegmentDetail = ( completed_at: 1700000200, error: null, stopped_at: 0, + answer: null, + summary: null, + updated_by: null, updated_at: 1700000000, attachments: [], child_chunks: [], @@ -728,6 +717,9 @@ describe('Batch Action Callbacks', () => { completed_at: 1700000002, error: null, stopped_at: 0, + answer: null, + summary: null, + updated_by: null, updated_at: 1700000003, attachments: [], child_chunks: [], @@ -879,130 +871,39 @@ describe('refreshChunkListDataWithDetailChanged callback', () => { expect(capturedRefreshCallback).toBeDefined() }) - it('should call invalidation functions when triggered with default status "all"', () => { - render(, { wrapper: createWrapper() }) - - // Call the captured callback - status is 'all' by default - if (capturedRefreshCallback) capturedRefreshCallback() - - // With status 'all', should call both disabled and enabled invalidation - expect(mockInvalidChunkListDisabled).toHaveBeenCalled() - expect(mockInvalidChunkListEnabled).toHaveBeenCalled() - }) - - it('should handle multiple callback invocations', () => { - render(, { wrapper: createWrapper() }) - - // Call the captured callback multiple times - if (capturedRefreshCallback) { - capturedRefreshCallback() - capturedRefreshCallback() - capturedRefreshCallback() - } - - // Should be called multiple times - expect(mockInvalidChunkListDisabled).toHaveBeenCalledTimes(3) - expect(mockInvalidChunkListEnabled).toHaveBeenCalledTimes(3) - }) - - it('should call correct invalidation functions when status is changed to enabled', async () => { - // Use general mode which has the status filter - mockDocForm.current = ChunkingModeEnum.text - mockParentMode.current = 'paragraph' - - render(, { wrapper: createWrapper() }) - - // Change status to enabled - const statusEnabledButton = screen.getByTestId('status-enabled') - fireEvent.click(statusEnabledButton) - - // Wait for state to update and re-render - await waitFor(() => { - // The callback should be re-captured with new status - expect(capturedRefreshCallback).toBeDefined() + const expectOtherStatusListsMarkedStale = () => + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: segmentListsKey, + refetchType: 'none', }) - // Call the callback with status 'true' - if (capturedRefreshCallback) capturedRefreshCallback() - - // With status true, should call all and disabled invalidation - expect(mockInvalidChunkListAll).toHaveBeenCalled() - expect(mockInvalidChunkListDisabled).toHaveBeenCalled() - }) - - it('should call correct invalidation functions when status is changed to disabled', async () => { - // Use general mode which has the status filter - mockDocForm.current = ChunkingModeEnum.text - mockParentMode.current = 'paragraph' - + it('should mark the other status lists stale with the default status "all"', () => { + vi.spyOn(QueryClient.prototype, 'invalidateQueries') render(, { wrapper: createWrapper() }) - // Change status to disabled - const statusDisabledButton = screen.getByTestId('status-disabled') - fireEvent.click(statusDisabledButton) + capturedRefreshCallback?.() - // Wait for state to update and re-render - await waitFor(() => { - // The callback should be re-captured with new status - expect(capturedRefreshCallback).toBeDefined() - }) - - // Call the callback with status 'false' - if (capturedRefreshCallback) capturedRefreshCallback() - - // With status false, should call all and enabled invalidation - expect(mockInvalidChunkListAll).toHaveBeenCalled() - expect(mockInvalidChunkListEnabled).toHaveBeenCalled() + expectOtherStatusListsMarkedStale() }) -}) -// refreshChunkListDataWithDetailChanged Branch Coverage Tests - -describe('refreshChunkListDataWithDetailChanged branch coverage', () => { - // This test simulates the behavior of refreshChunkListDataWithDetailChanged - // with different selectedStatus values to ensure branch coverage - - it('should handle status "true" branch correctly', () => { - // Simulate the behavior when selectedStatus is true - const mockInvalidAll = vi.fn() - const mockInvalidDisabled = vi.fn() - - // Create a refreshMap similar to the component - const refreshMap: Record void> = { - true: (() => { - mockInvalidAll() - mockInvalidDisabled() - })!, - } - - // Execute the 'true' branch - // Execute the 'true' branch - refreshMap.true!() - - expect(mockInvalidAll).toHaveBeenCalled() - expect(mockInvalidDisabled).toHaveBeenCalled() - }) - - it('should handle status "false" branch correctly', () => { - // Simulate the behavior when selectedStatus is false - const mockInvalidAll = vi.fn() - const mockInvalidEnabled = vi.fn() - - // Create a refreshMap similar to the component - const refreshMap: Record void> = { - false: (() => { - mockInvalidAll() - mockInvalidEnabled() - })!, - } + it.each(['status-enabled', 'status-disabled'])( + 'should mark the other status lists stale after filtering by %s', + async (statusButton) => { + // General mode has the status filter + mockDocForm.current = ChunkingModeEnum.text + mockParentMode.current = 'paragraph' + vi.spyOn(QueryClient.prototype, 'invalidateQueries') + render(, { wrapper: createWrapper() }) - // Execute the 'false' branch - // Execute the 'false' branch - refreshMap.false!() + fireEvent.click(screen.getByTestId(statusButton)) + await waitFor(() => { + expect(capturedRefreshCallback).toBeDefined() + }) + capturedRefreshCallback?.() - expect(mockInvalidAll).toHaveBeenCalled() - expect(mockInvalidEnabled).toHaveBeenCalled() - }) + expectOtherStatusListsMarkedStale() + }, + ) }) // Batch Action Callback Coverage Tests @@ -1093,46 +994,6 @@ describe('Inline callback and hook initialization coverage', () => { expect(screen.getByTestId('drawer-group'))!.toBeInTheDocument() }) - // Covers lines 74-90: refreshChunkListDataWithDetailChanged with status true - it('should invoke correct invalidation for enabled status', async () => { - render(, { wrapper: createWrapper() }) - - fireEvent.click(screen.getByTestId('status-enabled')) - - await waitFor(() => { - expect(capturedRefreshCallback).toBeDefined() - }) - - mockInvalidChunkListAll.mockClear() - mockInvalidChunkListDisabled.mockClear() - mockInvalidChunkListEnabled.mockClear() - - capturedRefreshCallback!() - - expect(mockInvalidChunkListAll).toHaveBeenCalled() - expect(mockInvalidChunkListDisabled).toHaveBeenCalled() - }) - - // Covers lines 74-90: refreshChunkListDataWithDetailChanged with status false - it('should invoke correct invalidation for disabled status', async () => { - render(, { wrapper: createWrapper() }) - - fireEvent.click(screen.getByTestId('status-disabled')) - - await waitFor(() => { - expect(capturedRefreshCallback).toBeDefined() - }) - - mockInvalidChunkListAll.mockClear() - mockInvalidChunkListDisabled.mockClear() - mockInvalidChunkListEnabled.mockClear() - - capturedRefreshCallback!() - - expect(mockInvalidChunkListAll).toHaveBeenCalled() - expect(mockInvalidChunkListEnabled).toHaveBeenCalled() - }) - // Covers line 101: clearSelection callback it('should clear selection via batch cancel', async () => { render(, { wrapper: createWrapper() }) diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx index 4b476364495c82..8c0d24a8dd6a20 100644 --- a/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx @@ -39,11 +39,14 @@ vi.mock('../index', () => ({ // Mock useAddChildSegment const mockAddChildSegment = vi.fn() -vi.mock('@/service/knowledge/use-segment', () => ({ - useAddChildSegment: () => ({ - mutateAsync: mockAddChildSegment, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.post': () => ({ + mutateAsync: mockAddChildSegment, + }), + }) +}) vi.mock('../common/action-buttons', () => ({ ActionButtons: ({ @@ -238,9 +241,11 @@ describe('NewChildSegmentModal', () => { await waitFor(() => { expect(mockAddChildSegment).toHaveBeenCalledWith( expect.objectContaining({ - datasetId: 'test-dataset-id', - documentId: 'test-document-id', - segmentId: 'chunk-1', + params: { + dataset_id: 'test-dataset-id', + document_id: 'test-document-id', + segment_id: 'chunk-1', + }, body: expect.objectContaining({ content: 'Valid content', }), diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/permissions.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/permissions.spec.tsx index e596021277ecd8..75fb658241adae 100644 --- a/web/app/components/datasets/documents/detail/completed/__tests__/permissions.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/__tests__/permissions.spec.tsx @@ -40,6 +40,9 @@ const segment: SegmentDetailModel = { completed_at: 1700000000, error: null, stopped_at: 0, + answer: null, + summary: null, + updated_by: null, updated_at: 1700000000, attachments: [], child_chunks: [childChunk], diff --git a/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-child-segment-data.spec.ts b/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-child-segment-data.spec.ts index df266158f865c1..036ebf35ea8147 100644 --- a/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-child-segment-data.spec.ts +++ b/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-child-segment-data.spec.ts @@ -9,6 +9,7 @@ import type { import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook } from '@testing-library/react' import * as React from 'react' +import { consoleQuery } from '@/service/console' import { useChildSegmentData } from '../use-child-segment-data' // Type for mutation callbacks @@ -31,14 +32,13 @@ const { mockChildSegmentListData, mockDeleteChildSegment, mockUpdateChildSegment, - mockInvalidChildSegmentList, } = vi.hoisted(() => ({ mockParentMode: { current: 'paragraph' as ParentMode }, mockDatasetId: { current: 'test-dataset-id' }, mockDocumentId: { current: 'test-document-id' }, mockNotify: vi.fn(), mockEventEmitter: { emit: vi.fn(), on: vi.fn(), off: vi.fn() }, - mockQueryClient: { setQueryData: vi.fn() }, + mockQueryClient: { setQueryData: vi.fn(), invalidateQueries: vi.fn() }, mockChildSegmentListData: { current: { data: [] as ChildChunkDetail[], total: 0, total_pages: 0 } as | ChildSegmentsResponse @@ -46,13 +46,22 @@ const { }, mockDeleteChildSegment: vi.fn(), mockUpdateChildSegment: vi.fn(), - mockInvalidChildSegmentList: vi.fn(), })) -vi.mock('@tanstack/react-query', async () => { - const actual = await vi.importActual('@tanstack/react-query') +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') return { - ...actual, + ...mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.get': + (options: { enabled?: boolean }) => ({ + isLoading: false, + data: options.enabled === false ? undefined : mockChildSegmentListData.current, + }), + 'datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.byChildChunkId.delete': + () => ({ mutateAsync: mockDeleteChildSegment }), + 'datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.byChildChunkId.patch': + () => ({ mutateAsync: mockUpdateChildSegment }), + }), useQueryClient: () => mockQueryClient, } }) @@ -80,19 +89,15 @@ vi.mock('@/context/event-emitter', () => ({ useEventEmitterContextContext: () => ({ eventEmitter: mockEventEmitter }), })) -vi.mock('@/service/knowledge/use-segment', () => ({ - useChildSegmentList: () => ({ - isLoading: false, - data: mockChildSegmentListData.current, - }), - useChildSegmentListKey: ['segment', 'childChunkList'], - useDeleteChildSegment: () => ({ mutateAsync: mockDeleteChildSegment }), - useUpdateChildSegment: () => ({ mutateAsync: mockUpdateChildSegment }), -})) +const childChunkListsKey = + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.get.key( + { + input: { params: { dataset_id: 'test-dataset-id', document_id: 'test-document-id' } }, + }, + ) -vi.mock('@/service/use-base', () => ({ - useInvalid: () => mockInvalidChildSegmentList, -})) +const expectChildChunkListsRefetched = () => + expect(mockQueryClient.invalidateQueries).toHaveBeenCalledWith({ queryKey: childChunkListsKey }) const createQueryClient = () => new QueryClient({ @@ -142,6 +147,9 @@ const createMockSegment = (overrides: Partial = {}): Segment completed_at: 1700000200, error: null, stopped_at: 0, + answer: null, + summary: null, + updated_by: null, updated_at: 1700000000, attachments: [], child_chunks: [], @@ -192,7 +200,7 @@ describe('useChildSegmentData', () => { result.current.resetChildList() }) - expect(mockInvalidChildSegmentList).toHaveBeenCalled() + expectChildChunkListsRefetched() }) }) @@ -247,7 +255,7 @@ describe('useChildSegmentData', () => { await result.current.onDeleteChildChunk('seg-1', 'child-1') }) - expect(mockInvalidChildSegmentList).toHaveBeenCalled() + expectChildChunkListsRefetched() }) it('should notify error on failure', async () => { @@ -409,7 +417,7 @@ describe('useChildSegmentData', () => { result.current.onSaveNewChildChunk(createMockChildChunk()) }) - expect(mockInvalidChildSegmentList).toHaveBeenCalled() + expectChildChunkListsRefetched() }) }) @@ -453,12 +461,19 @@ describe('useChildSegmentData', () => { result.current.viewNewlyAddedChildChunk() }) - expect(mockInvalidChildSegmentList).toHaveBeenCalled() + expectChildChunkListsRefetched() }) }) describe('Query disabled states', () => { it('should disable query when not in fullDocMode', () => { + mockChildSegmentListData.current = { + data: [createMockChildChunk()], + total: 1, + total_pages: 1, + page: 1, + limit: 20, + } const { result } = renderHook( () => useChildSegmentData({ @@ -475,6 +490,13 @@ describe('useChildSegmentData', () => { }) it('should disable query when segments is empty', () => { + mockChildSegmentListData.current = { + data: [createMockChildChunk()], + total: 1, + total_pages: 1, + page: 1, + limit: 20, + } const { result } = renderHook( () => useChildSegmentData({ diff --git a/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-segment-list-data.spec.ts b/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-segment-list-data.spec.ts index 54689dd37864f7..eff3a316c65ba2 100644 --- a/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-segment-list-data.spec.ts +++ b/web/app/components/datasets/documents/detail/completed/hooks/__tests__/use-segment-list-data.spec.ts @@ -11,6 +11,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook } from '@testing-library/react' import * as React from 'react' import { ChunkingMode as ChunkingModeEnum } from '@/models/datasets' +import { consoleQuery } from '@/service/console' import { segmentImportStatus } from '@/types/dataset' import { useSegmentListData } from '../use-segment-list-data' @@ -49,10 +50,6 @@ const { mockDisableSegment, mockDeleteSegment, mockUpdateSegment, - mockInvalidSegmentList, - mockInvalidChunkListAll, - mockInvalidChunkListEnabled, - mockInvalidChunkListDisabled, mockPathname, } = vi.hoisted(() => ({ mockDocForm: { current: 'text' as ChunkingMode }, @@ -61,13 +58,12 @@ const { mockDocumentId: { current: 'test-document-id' }, mockNotify: vi.fn(), mockEventEmitter: { emit: vi.fn(), on: vi.fn(), off: vi.fn() }, - mockQueryClient: { setQueryData: vi.fn() }, + mockQueryClient: { setQueryData: vi.fn(), invalidateQueries: vi.fn() }, mockSegmentListData: { current: { data: [] as SegmentDetailModel[], total: 0, total_pages: 0, - has_more: false, limit: 20, page: 1, } as SegmentsResponse | undefined, @@ -76,10 +72,6 @@ const { mockDisableSegment: vi.fn(), mockDeleteSegment: vi.fn(), mockUpdateSegment: vi.fn(), - mockInvalidSegmentList: vi.fn(), - mockInvalidChunkListAll: vi.fn(), - mockInvalidChunkListEnabled: vi.fn(), - mockInvalidChunkListDisabled: vi.fn(), mockPathname: { current: '/datasets/test/documents/test' }, })) @@ -87,10 +79,27 @@ vi.mock('@/next/navigation', () => ({ usePathname: () => mockPathname.current, })) -vi.mock('@tanstack/react-query', async () => { - const actual = await vi.importActual('@tanstack/react-query') +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') return { - ...actual, + ...mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.segments.get': () => ({ + isLoading: false, + data: mockSegmentListData.current, + }), + 'datasets.byDatasetId.documents.byDocumentId.segment.byAction.patch': () => ({ + mutateAsync: (input: { params: { action: string } }, callbacks: unknown) => + input.params.action === 'enable' + ? mockEnableSegment(input, callbacks) + : mockDisableSegment(input, callbacks), + }), + 'datasets.byDatasetId.documents.byDocumentId.segments.delete': () => ({ + mutateAsync: mockDeleteSegment, + }), + 'datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.patch': () => ({ + mutateAsync: mockUpdateSegment, + }), + }), useQueryClient: () => mockQueryClient, } }) @@ -118,30 +127,18 @@ vi.mock('@/context/event-emitter', () => ({ useEventEmitterContextContext: () => ({ eventEmitter: mockEventEmitter }), })) -vi.mock('@/service/knowledge/use-segment', () => ({ - useSegmentList: () => ({ - isLoading: false, - data: mockSegmentListData.current, - }), - useSegmentListKey: ['segment', 'chunkList'], - useChunkListAllKey: ['segment', 'chunkList', { enabled: 'all' }], - useChunkListEnabledKey: ['segment', 'chunkList', { enabled: true }], - useChunkListDisabledKey: ['segment', 'chunkList', { enabled: false }], - useEnableSegment: () => ({ mutateAsync: mockEnableSegment }), - useDisableSegment: () => ({ mutateAsync: mockDisableSegment }), - useDeleteSegment: () => ({ mutateAsync: mockDeleteSegment }), - useUpdateSegment: () => ({ mutateAsync: mockUpdateSegment }), -})) +const segmentListsKey = consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.get.key({ + input: { params: { dataset_id: 'test-dataset-id', document_id: 'test-document-id' } }, +}) -vi.mock('@/service/use-base', () => ({ - useInvalid: (key: unknown[]) => { - const keyObj = key[2] as { enabled?: boolean | 'all' } | undefined - if (keyObj?.enabled === 'all') return mockInvalidChunkListAll - if (keyObj?.enabled === true) return mockInvalidChunkListEnabled - if (keyObj?.enabled === false) return mockInvalidChunkListDisabled - return mockInvalidSegmentList - }, -})) +const expectSegmentListsRefetched = () => + expect(mockQueryClient.invalidateQueries).toHaveBeenCalledWith({ queryKey: segmentListsKey }) + +const expectOtherStatusListsMarkedStale = () => + expect(mockQueryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: segmentListsKey, + refetchType: 'none', + }) const createQueryClient = () => new QueryClient({ @@ -179,6 +176,9 @@ const createMockSegment = (overrides: Partial = {}): Segment completed_at: 1700000200, error: null, stopped_at: 0, + answer: null, + summary: null, + updated_by: null, updated_at: 1700000000, attachments: [], child_chunks: [], @@ -209,7 +209,6 @@ describe('useSegmentListData', () => { data: [], total: 0, total_pages: 0, - has_more: false, limit: 20, page: 1, } @@ -254,7 +253,6 @@ describe('useSegmentListData', () => { data: [], total: 10, total_pages: 1, - has_more: false, limit: 20, page: 1, } @@ -272,7 +270,6 @@ describe('useSegmentListData', () => { data: [], total: 5, total_pages: 1, - has_more: false, limit: 20, page: 1, } @@ -297,7 +294,6 @@ describe('useSegmentListData', () => { data: [], total: 3, total_pages: 1, - has_more: false, limit: 20, page: 1, } @@ -323,7 +319,6 @@ describe('useSegmentListData', () => { data: [], total: 7, total_pages: 1, - has_more: false, limit: 20, page: 1, } @@ -366,12 +361,12 @@ describe('useSegmentListData', () => { }) expect(clearSelection).toHaveBeenCalled() - expect(mockInvalidSegmentList).toHaveBeenCalled() + expectSegmentListsRefetched() }) }) describe('refreshChunkListWithStatusChanged', () => { - it('should invalidate disabled and enabled when status is all', async () => { + it('should mark the other status lists stale when status is all', async () => { mockEnableSegment.mockImplementation( async (_params, { onSuccess }: { onSuccess: () => void }) => { onSuccess() @@ -389,12 +384,15 @@ describe('useSegmentListData', () => { }, ) + mockQueryClient.invalidateQueries.mockClear() await act(async () => { await result.current.onChangeSwitch(true, 'seg-1') }) - expect(mockInvalidChunkListDisabled).toHaveBeenCalled() - expect(mockInvalidChunkListEnabled).toHaveBeenCalled() + expectOtherStatusListsMarkedStale() + expect(mockQueryClient.invalidateQueries).not.toHaveBeenCalledWith({ + queryKey: segmentListsKey, + }) }) it('should invalidate segment list when status is not all', async () => { @@ -415,11 +413,12 @@ describe('useSegmentListData', () => { }, ) + mockQueryClient.invalidateQueries.mockClear() await act(async () => { await result.current.onChangeSwitch(true, 'seg-1') }) - expect(mockInvalidSegmentList).toHaveBeenCalled() + expectSegmentListsRefetched() }) }) @@ -484,7 +483,7 @@ describe('useSegmentListData', () => { }) expect(mockEnableSegment).toHaveBeenCalledWith( - expect.objectContaining({ segmentIds: ['seg-1', 'seg-2'] }), + expect.objectContaining({ query: { segment_id: ['seg-1', 'seg-2'] } }), expect.any(Object), ) expect(mockNotify).toHaveBeenCalledWith({ @@ -767,7 +766,6 @@ describe('useSegmentListData', () => { data: [], total: 10, total_pages: 1, - has_more: false, limit: 20, page: 1, } @@ -795,7 +793,6 @@ describe('useSegmentListData', () => { data: [], total: 5, total_pages: 1, - has_more: false, limit: 20, page: 1, } @@ -853,7 +850,7 @@ describe('useSegmentListData', () => { // Initial call from effect expect(clearSelection).toHaveBeenCalled() - expect(mockInvalidSegmentList).toHaveBeenCalled() + expectSegmentListsRefetched() }) }) @@ -901,8 +898,7 @@ describe('useSegmentListData', () => { await result.current.handleUpdateSegment('seg-1', 'content', '', [], []) }) - expect(mockInvalidChunkListDisabled).toHaveBeenCalled() - expect(mockInvalidChunkListEnabled).toHaveBeenCalled() + expectOtherStatusListsMarkedStale() }) it('should call correct invalidation for status true', async () => { @@ -928,8 +924,7 @@ describe('useSegmentListData', () => { await result.current.handleUpdateSegment('seg-1', 'content', '', [], []) }) - expect(mockInvalidChunkListAll).toHaveBeenCalled() - expect(mockInvalidChunkListDisabled).toHaveBeenCalled() + expectOtherStatusListsMarkedStale() }) it('should call correct invalidation for status false', async () => { @@ -955,8 +950,7 @@ describe('useSegmentListData', () => { await result.current.handleUpdateSegment('seg-1', 'content', '', [], []) }) - expect(mockInvalidChunkListAll).toHaveBeenCalled() - expect(mockInvalidChunkListEnabled).toHaveBeenCalled() + expectOtherStatusListsMarkedStale() }) }) diff --git a/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts b/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts index 50aded92da4f1c..068e97cdf5e5f0 100644 --- a/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts +++ b/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts @@ -4,18 +4,12 @@ import type { SegmentDetailModel, SegmentUpdater, } from '@/models/datasets' -import { useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' -import { - useChildSegmentList, - useChildSegmentListKey, - useDeleteChildSegment, - useUpdateChildSegment, -} from '@/service/knowledge/use-segment' -import { useInvalid } from '@/service/use-base' +import { consoleQuery } from '@/service/console' import { useDocumentContext } from '../../context' type UseChildSegmentDataOptions = { @@ -35,7 +29,7 @@ type UseChildSegmentDataOptions = { type UseChildSegmentDataReturn = { childSegments: ChildChunkDetail[] isLoadingChildSegmentList: boolean - childChunkListData: ReturnType['data'] + childChunkListData: ChildSegmentsResponse | undefined childSegmentListRef: React.RefObject needScrollToBottom: React.RefObject // Operations @@ -81,24 +75,30 @@ export const useChildSegmentData = ( [currentPage, limit, searchValue], ) const segmentId = segments[0]?.id || '' - // Build query key for optimistic updates - const currentQueryKey = useMemo( - () => [...useChildSegmentListKey, datasetId, documentId, segmentId, queryParams], - [datasetId, documentId, segmentId, queryParams], - ) - // Fetch child segment list - const { isLoading: isLoadingChildSegmentList, data: childChunkListData } = useChildSegmentList( - { - datasetId, - documentId, - segmentId, - params: queryParams, - }, - !isFullDocMode || segments.length === 0, - ) + const childSegmentListQuery = + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.get.queryOptions( + { + input: { + params: { dataset_id: datasetId, document_id: documentId, segment_id: segmentId }, + query: queryParams, + }, + enabled: isFullDocMode && segments.length > 0, + }, + ) + // Query key for optimistic updates + const currentQueryKey = childSegmentListQuery.queryKey + const { isLoading: isLoadingChildSegmentList, data: childChunkListData } = + useQuery(childSegmentListQuery) // Derive child segments from query data const childSegments = useMemo(() => childChunkListData?.data || [], [childChunkListData]) - const invalidChildSegmentList = useInvalid(useChildSegmentListKey) + const invalidChildSegmentList = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.get.key( + { input: { params: { dataset_id: datasetId, document_id: documentId } } }, + ), + }) + }, [queryClient, datasetId, documentId]) // Scroll to bottom when child segments change useEffect(() => { if (childSegmentListRef.current && needScrollToBottom.current) { @@ -126,12 +126,23 @@ export const useChildSegmentData = ( [queryClient, currentQueryKey], ) // Mutations - const { mutateAsync: deleteChildSegment } = useDeleteChildSegment() - const { mutateAsync: updateChildSegment } = useUpdateChildSegment() + const { mutateAsync: deleteChildSegment } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.byChildChunkId.delete.mutationOptions(), + ) + const { mutateAsync: updateChildSegment } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.byChildChunkId.patch.mutationOptions(), + ) const onDeleteChildChunk = useCallback( async (segmentIdParam: string, childChunkId: string) => { await deleteChildSegment( - { datasetId, documentId, segmentId: segmentIdParam, childChunkId }, + { + params: { + dataset_id: datasetId, + document_id: documentId, + segment_id: segmentIdParam, + child_chunk_id: childChunkId, + }, + }, { onSuccess: () => { toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) @@ -171,7 +182,15 @@ export const useChildSegmentData = ( params.content = content eventEmitter?.emit('update-child-segment') await updateChildSegment( - { datasetId, documentId, segmentId: segmentIdParam, childChunkId, body: params }, + { + params: { + dataset_id: datasetId, + document_id: documentId, + segment_id: segmentIdParam, + child_chunk_id: childChunkId, + }, + body: params, + }, { onSuccess: (res) => { onCloseChildSegmentDetail() diff --git a/web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts b/web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts index e702db61890e37..861f7094e9ecf6 100644 --- a/web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts +++ b/web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts @@ -1,25 +1,14 @@ import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types' import type { SegmentDetailModel, SegmentsResponse, SegmentUpdater } from '@/models/datasets' import type { SegmentImportStatus } from '@/types/dataset' -import { useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { ChunkingMode } from '@/models/datasets' import { usePathname } from '@/next/navigation' -import { - useChunkListAllKey, - useChunkListDisabledKey, - useChunkListEnabledKey, - useDeleteSegment, - useDisableSegment, - useEnableSegment, - useSegmentList, - useSegmentListKey, - useUpdateSegment, -} from '@/service/knowledge/use-segment' -import { useInvalid } from '@/service/use-base' +import { consoleQuery } from '@/service/console' import { segmentImportStatus } from '@/types/dataset' import { formatNumber } from '@/utils/format' import { useDocumentContext } from '../../context' @@ -38,7 +27,7 @@ type UseSegmentListDataOptions = { type UseSegmentListDataReturn = { segments: SegmentDetailModel[] isLoadingSegmentList: boolean - segmentListData: ReturnType['data'] + segmentListData: SegmentsResponse | undefined totalText: string isFullDocMode: boolean segmentListRef: React.RefObject @@ -58,6 +47,7 @@ type UseSegmentListDataReturn = { resetList: () => void viewNewlyAddedChunk: () => void invalidSegmentList: () => void + markOtherStatusListsStale: () => void updateSegmentInCache: ( segmentId: string, updater: (seg: SegmentDetailModel) => SegmentDetailModel, @@ -99,24 +89,35 @@ export const useSegmentListData = ( }), [isFullDocMode, currentPage, limit, searchValue, selectedStatus], ) - // Build query key for optimistic updates - const currentQueryKey = useMemo( - () => [...useSegmentListKey, datasetId, documentId, queryParams], - [datasetId, documentId, queryParams], - ) - // Fetch segment list - const { isLoading: isLoadingSegmentList, data: segmentListData } = useSegmentList({ - datasetId, - documentId, - params: queryParams, - }) + const segmentListQuery = + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.get.queryOptions({ + input: { + params: { dataset_id: datasetId, document_id: documentId }, + query: { ...queryParams, enabled: String(queryParams.enabled) }, + }, + }) + // Query key for optimistic updates + const currentQueryKey = segmentListQuery.queryKey + const { isLoading: isLoadingSegmentList, data: segmentListData } = useQuery(segmentListQuery) // Derive segments from query data const segments = useMemo(() => segmentListData?.data || [], [segmentListData]) - // Invalidation hooks - const invalidSegmentList = useInvalid(useSegmentListKey) - const invalidChunkListAll = useInvalid(useChunkListAllKey) - const invalidChunkListEnabled = useInvalid(useChunkListEnabledKey) - const invalidChunkListDisabled = useInvalid(useChunkListDisabledKey) + const invalidSegmentList = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.get.key({ + input: { params: { dataset_id: datasetId, document_id: documentId } }, + }), + }) + }, [queryClient, datasetId, documentId]) + // After an optimistic edit, the lists under the other status filters are stale; the current + // list already shows the change, so it is not refetched. + const markOtherStatusListsStale = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.get.key({ + input: { params: { dataset_id: datasetId, document_id: documentId } }, + }), + refetchType: 'none', + }) + }, [queryClient, datasetId, documentId]) // Scroll to bottom when needed useEffect(() => { if (segmentListRef.current && needScrollToBottom.current) { @@ -143,31 +144,11 @@ export const useSegmentListData = ( clearSelection() invalidSegmentList() }, [clearSelection, invalidSegmentList]) + // A toggled segment leaves a filtered list, so that list is refetched. const refreshChunkListWithStatusChanged = useCallback(() => { - if (selectedStatus === 'all') { - invalidChunkListDisabled() - invalidChunkListEnabled() - } else { - invalidSegmentList() - } - }, [selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidSegmentList]) - const refreshChunkListDataWithDetailChanged = useCallback(() => { - const refreshMap: Record void> = { - all: () => { - invalidChunkListDisabled() - invalidChunkListEnabled() - }, - true: () => { - invalidChunkListAll() - invalidChunkListDisabled() - }, - false: () => { - invalidChunkListAll() - invalidChunkListEnabled() - }, - } - refreshMap[String(selectedStatus)]?.() - }, [selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidChunkListAll]) + if (selectedStatus === 'all') markOtherStatusListsStale() + else invalidSegmentList() + }, [selectedStatus, markOtherStatusListsStale, invalidSegmentList]) // Optimistic update helper using React Query's setQueryData const updateSegmentInCache = useCallback( (segmentId: string, updater: (seg: SegmentDetailModel) => SegmentDetailModel) => { @@ -195,16 +176,27 @@ export const useSegmentListData = ( [queryClient, currentQueryKey], ) // Mutations - const { mutateAsync: enableSegment } = useEnableSegment() - const { mutateAsync: disableSegment } = useDisableSegment() - const { mutateAsync: deleteSegment } = useDeleteSegment() - const { mutateAsync: updateSegment } = useUpdateSegment() + const { mutateAsync: switchSegments } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segment.byAction.patch.mutationOptions(), + ) + const { mutateAsync: deleteSegment } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.delete.mutationOptions(), + ) + const { mutateAsync: updateSegment } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.patch.mutationOptions(), + ) const onChangeSwitch = useCallback( async (enable: boolean, segId?: string) => { - const operationApi = enable ? enableSegment : disableSegment const targetIds = segId ? [segId] : selectedSegmentIds - await operationApi( - { datasetId, documentId, segmentIds: targetIds }, + await switchSegments( + { + params: { + dataset_id: datasetId, + document_id: documentId, + action: enable ? 'enable' : 'disable', + }, + query: { segment_id: targetIds }, + }, { onSuccess: () => { // A single segment is toggled from its switch, which already shows the result. @@ -223,8 +215,7 @@ export const useSegmentListData = ( datasetId, documentId, selectedSegmentIds, - disableSegment, - enableSegment, + switchSegments, t, updateSegmentsInCache, refreshChunkListWithStatusChanged, @@ -234,7 +225,10 @@ export const useSegmentListData = ( async (segId?: string) => { const targetIds = segId ? [segId] : selectedSegmentIds await deleteSegment( - { datasetId, documentId, segmentIds: targetIds }, + { + params: { dataset_id: datasetId, document_id: documentId }, + query: { segment_id: targetIds }, + }, { onSuccess: () => { toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) @@ -292,7 +286,10 @@ export const useSegmentListData = ( if (needRegenerate) params.regenerate_child_chunks = needRegenerate eventEmitter?.emit('update-segment') await updateSegment( - { datasetId, documentId, segmentId, body: params }, + { + params: { dataset_id: datasetId, document_id: documentId, segment_id: segmentId }, + body: params, + }, { onSuccess(res) { toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) @@ -311,7 +308,7 @@ export const useSegmentListData = ( updated_at: res.data.updated_at, child_chunks: res.data.child_chunks, })) - refreshChunkListDataWithDetailChanged() + markOtherStatusListsStale() eventEmitter?.emit('update-segment-success') }, onSettled() { @@ -328,7 +325,7 @@ export const useSegmentListData = ( eventEmitter, onCloseSegmentDetail, updateSegmentInCache, - refreshChunkListDataWithDetailChanged, + markOtherStatusListsStale, t, ], ) @@ -371,6 +368,7 @@ export const useSegmentListData = ( resetList, viewNewlyAddedChunk, invalidSegmentList, + markOtherStatusListsStale, updateSegmentInCache, } } diff --git a/web/app/components/datasets/documents/detail/completed/index.tsx b/web/app/components/datasets/documents/detail/completed/index.tsx index 7d88f4dec09d85..dccd170b5861e2 100644 --- a/web/app/components/datasets/documents/detail/completed/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/index.tsx @@ -7,12 +7,6 @@ import { Pagination } from '@langgenius/dify-ui/pagination' import { Separator } from '@langgenius/dify-ui/separator' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { - useChunkListAllKey, - useChunkListDisabledKey, - useChunkListEnabledKey, -} from '@/service/knowledge/use-segment' -import { useInvalid } from '@/service/use-base' import { useDocumentContext } from '../context' import BatchAction from './common/batch-action' import { DrawerGroup } from './components/drawer-group' @@ -70,34 +64,6 @@ const Completed: FC = ({ // Selection state const selectionState = useSegmentSelection() - // Invalidation hooks for child segment data - const invalidChunkListAll = useInvalid(useChunkListAllKey) - const invalidChunkListEnabled = useInvalid(useChunkListEnabledKey) - const invalidChunkListDisabled = useInvalid(useChunkListDisabledKey) - - const refreshChunkListDataWithDetailChanged = useCallback(() => { - const refreshMap: Record void> = { - all: () => { - invalidChunkListDisabled() - invalidChunkListEnabled() - }, - true: () => { - invalidChunkListAll() - invalidChunkListDisabled() - }, - false: () => { - invalidChunkListAll() - invalidChunkListEnabled() - }, - } - refreshMap[String(searchFilter.selectedStatus)]?.() - }, [ - searchFilter.selectedStatus, - invalidChunkListDisabled, - invalidChunkListEnabled, - invalidChunkListAll, - ]) - // Segment list data const segmentListDataHook = useSegmentListData({ searchValue: searchFilter.searchValue, @@ -142,7 +108,7 @@ const Completed: FC = ({ currChunkId: modalState.currChunkId, isFullDocMode: segmentListDataHook.isFullDocMode, onCloseChildSegmentDetail: modalState.onCloseChildSegmentDetail, - refreshChunkListDataWithDetailChanged, + refreshChunkListDataWithDetailChanged: segmentListDataHook.markOtherStatusListsStale, updateSegmentInCache: segmentListDataHook.updateSegmentInCache, }) diff --git a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx index ca125802639487..fb7f20463c8069 100644 --- a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx +++ b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx @@ -3,12 +3,13 @@ import type { ChildChunkDetail, SegmentUpdater } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' import { Separator } from '@langgenius/dify-ui/separator' import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' +import { useMutation } from '@tanstack/react-query' import { memo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { ChunkingMode } from '@/models/datasets' import { useParams } from '@/next/navigation' -import { useAddChildSegment } from '@/service/knowledge/use-segment' +import { consoleQuery } from '@/service/console' import { formatNumber } from '@/utils/format' import { useDocumentContext } from '../context' import { ActionButtons } from './common/action-buttons' @@ -47,7 +48,9 @@ const NewChildSegmentModal: FC = ({ if (actionType === 'esc' || !addAnother) onCancel() } - const { mutateAsync: addChildSegment } = useAddChildSegment() + const { mutateAsync: addChildSegment } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.post.mutationOptions(), + ) const handleSave = async () => { const params: SegmentUpdater = { content: '' } @@ -59,7 +62,10 @@ const NewChildSegmentModal: FC = ({ setLoading(true) await addChildSegment( - { datasetId, documentId, segmentId: chunkId, body: params }, + { + params: { dataset_id: datasetId, document_id: documentId, segment_id: chunkId }, + body: params, + }, { onSuccess(res) { toast.success( diff --git a/web/app/components/datasets/documents/detail/completed/segment-card/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/completed/segment-card/__tests__/index.spec.tsx index 9b87c5ac2e9e64..f32adf16b997de 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-card/__tests__/index.spec.tsx @@ -143,6 +143,9 @@ const createMockSegmentDetail = ( completed_at: 1700000200, error: null, stopped_at: 0, + answer: null, + summary: null, + updated_by: null, updated_at: 1700000000, attachments: [], child_chunks: [], diff --git a/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx b/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx index d863dee4cdd089..0870dfaea24ac5 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx @@ -131,7 +131,8 @@ const SegmentCard: FC = ({ const images = useMemo(() => { return attachments.map((attachment) => ({ name: attachment.name, - mimeType: attachment.mime_type, + // An unknown MIME type is empty, as in File.type. + mimeType: attachment.mime_type ?? '', sourceUrl: attachment.source_url, size: attachment.size, extension: attachment.extension, @@ -276,7 +277,7 @@ const SegmentCard: FC = ({
{ }) vi.mock('@/service/datasets') -vi.mock('@/service/knowledge/use-dataset') +const { mockProcessRuleQuery } = vi.hoisted(() => ({ mockProcessRuleQuery: vi.fn() })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.processRule.get': mockProcessRuleQuery, + }) +}) vi.mock('@/app/notifications', () => ({ toast: mockToast, })) @@ -35,7 +40,6 @@ vi.mock('@/app/notifications', () => ({ const mockFetchIndexingStatus = vi.mocked(datasetsService.fetchIndexingStatus) const mockPauseDocIndexing = vi.mocked(datasetsService.pauseDocIndexing) const mockResumeDocIndexing = vi.mocked(datasetsService.resumeDocIndexing) -const mockUseProcessRule = vi.mocked(useDataset.useProcessRule) const createConsoleQueryClient = () => new QueryClient({ @@ -98,11 +102,11 @@ describe('EmbeddingDetail', () => { beforeEach(() => { vi.clearAllMocks() - mockUseProcessRule.mockReturnValue({ + mockProcessRuleQuery.mockReturnValue({ data: mockProcessRule(), isLoading: false, error: null, - } as ReturnType) + }) }) describe('Rendering', () => { diff --git a/web/app/components/datasets/documents/detail/embedding/hooks/use-embedding-status.ts b/web/app/components/datasets/documents/detail/embedding/hooks/use-embedding-status.ts index 52c44a1865335d..0deec51d98e54d 100644 --- a/web/app/components/datasets/documents/detail/embedding/hooks/use-embedding-status.ts +++ b/web/app/components/datasets/documents/detail/embedding/hooks/use-embedding-status.ts @@ -1,4 +1,3 @@ -import type { CommonResponse } from '@/models/common' import type { IndexingStatusResponse } from '@/models/datasets' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef } from 'react' @@ -68,7 +67,10 @@ export const useEmbeddingStatus = ({ const isCompleted = status === 'completed' const isPaused = status === 'paused' const isError = status === 'error' - const percent = calculatePercent(query.data?.completed_segments, query.data?.total_segments) + const percent = calculatePercent( + query.data?.completed_segments ?? undefined, + query.data?.total_segments ?? undefined, + ) // Handle completion callback useEffect(() => { @@ -116,7 +118,7 @@ export const usePauseIndexing = ({ onSuccess, onError, }: UsePauseResumeOptions) => { - return useMutation({ + return useMutation({ mutationKey: [NAME_SPACE, 'pause', datasetId, documentId], mutationFn: () => pauseDocIndexing({ datasetId: datasetId!, documentId: documentId! }), onSuccess, @@ -130,7 +132,7 @@ export const useResumeIndexing = ({ onSuccess, onError, }: UsePauseResumeOptions) => { - return useMutation({ + return useMutation({ mutationKey: [NAME_SPACE, 'resume', datasetId, documentId], mutationFn: () => resumeDocIndexing({ datasetId: datasetId!, documentId: documentId! }), onSuccess, diff --git a/web/app/components/datasets/documents/detail/embedding/index.tsx b/web/app/components/datasets/documents/detail/embedding/index.tsx index bf32cf5c5b88e0..c38dfad2a150f1 100644 --- a/web/app/components/datasets/documents/detail/embedding/index.tsx +++ b/web/app/components/datasets/documents/detail/embedding/index.tsx @@ -1,11 +1,12 @@ import type { FC } from 'react' import type { IndexingType } from '../../../create/step-two' import type { RETRIEVE_METHOD } from '@/types/app' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' -import { useProcessRule } from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' import { useDocumentContext } from '../context' import { ProgressBar, RuleDetail, SegmentProgress, StatusHeader } from './components' import { useEmbeddingStatus, usePauseIndexing, useResumeIndexing } from './hooks' @@ -44,7 +45,12 @@ const EmbeddingDetail: FC = ({ documentId, onComplete: detailUpdate, }) - const { data: ruleDetail } = useProcessRule(documentId) + const { data: ruleDetail } = useQuery( + consoleQuery.datasets.processRule.get.queryOptions({ + input: { query: { document_id: documentId } }, + enabled: !!documentId, + }), + ) const handleSuccess = useCallback(() => { toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) }, [t]) @@ -97,8 +103,8 @@ const EmbeddingDetail: FC = ({ isError={isError} /> = ({ datasetId, documentId }) => { const hideBatchModal = () => setBatchModalVisible(false) const resetImportStatus = () => setImportStatus(undefined) - const { mutateAsync: checkSegmentBatchImportProgress } = useCheckSegmentBatchImportProgress() + const { mutateAsync: checkSegmentBatchImportProgress } = useMutation( + consoleQuery.datasets.batchImportStatus.byJobId.get.mutationOptions(), + ) const checkProcess = async (jobID: string) => { await checkSegmentBatchImportProgress( - { jobID }, + { params: { job_id: jobID } }, { onSuccess: (res) => { - setImportStatus(res.job_status) - if ( - res.job_status === segmentImportStatus.waiting || - res.job_status === segmentImportStatus.processing - ) + const status = isSegmentImportStatus(res.job_status) ? res.job_status : undefined + setImportStatus(status) + if (status === segmentImportStatus.waiting || status === segmentImportStatus.processing) setTimeout(() => checkProcess(res.job_id), 2500) - if (res.job_status === segmentImportStatus.error) + if (status === segmentImportStatus.error) toast.error(`${t(($) => $['list.batchModal.runError'], { ns: 'datasetDocuments' })}`) }, onError: (e) => { @@ -113,16 +102,18 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { ) } - const { mutateAsync: segmentBatchImport } = useSegmentBatchImport() + const { mutateAsync: segmentBatchImport } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.batchImport.post.mutationOptions(), + ) const runBatch = async (csv: FileItem) => { await segmentBatchImport( { - url: `/datasets/${datasetId}/documents/${documentId}/segments/batch_import`, + params: { dataset_id: datasetId, document_id: documentId }, body: { upload_file_id: csv.file.id! }, }, { onSuccess: (res) => { - setImportStatus(res.job_status) + setImportStatus(isSegmentImportStatus(res.job_status) ? res.job_status : undefined) checkProcess(res.job_id) }, onError: (e) => { @@ -139,26 +130,33 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { data: documentDetail, error, refetch: detailMutate, - } = useDocumentDetail({ - datasetId, - documentId, - params: { metadata: 'without' }, - refetchInterval: (query) => { - const status = query.state.data?.display_status - if (!status || NON_TERMINAL_DISPLAY_STATUSES.has(status)) return 2500 - return false - }, - }) + } = useQuery( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.queryOptions({ + input: { + params: { dataset_id: datasetId, document_id: documentId }, + query: { metadata: 'without' }, + }, + refetchInterval: (query) => { + const status = query.state.data?.display_status + if (!isDocumentDisplayStatus(status) || NON_TERMINAL_DISPLAY_STATUSES.has(status)) + return 2500 + return false + }, + }), + ) const documentTitle = documentDetail?.name || t(($) => $['datasetMenus.documents'], { ns: 'common' }) const datasetTitle = dataset?.name || t(($) => $['menus.datasets'], { ns: 'navigation' }) useDocumentTitle(`${documentTitle} · ${datasetTitle}`) - const { data: documentMetadata } = useDocumentMetadata({ - datasetId, - documentId, - params: { metadata: 'only' }, - }) + const { data: documentMetadata } = useQuery( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.queryOptions({ + input: { + params: { dataset_id: datasetId, document_id: documentId }, + query: { metadata: 'only' }, + }, + }), + ) const backToPrev = useCallback(() => { const queryString = searchParams.toString() @@ -168,13 +166,21 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { const isDetailLoading = !documentDetail && !error - const embedding = NON_TERMINAL_DISPLAY_STATUSES.has( - documentDetail?.display_status as DocumentDisplayStatus, - ) + const displayStatus = isDocumentDisplayStatus(documentDetail?.display_status) + ? documentDetail.display_status + : undefined + const embedding = !!displayStatus && NON_TERMINAL_DISPLAY_STATUSES.has(displayStatus) - const invalidChunkList = useInvalid(useSegmentListKey) - const invalidChildChunkList = useInvalid(useChildSegmentListKey) - const invalidDocumentList = useInvalidDocumentList(datasetId) + const queryClient = useQueryClient() + const invalidDocumentList = useCallback( + () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params: { dataset_id: datasetId } }, + }), + }), + [datasetId, queryClient], + ) const handleOperate = useCallback( (operateName?: string) => { @@ -186,13 +192,23 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { // If operation is not rename, refresh the chunk list after 5 seconds if (operateName) { setTimeout(() => { - invalidChunkList() - invalidChildChunkList() + const params = { dataset_id: datasetId, document_id: documentId } + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.get.key({ + input: { params }, + }), + }) + queryClient.invalidateQueries({ + queryKey: + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.get.key( + { input: { params } }, + ), + }) }, 5000) } } }, - [invalidDocumentList, backToPrev, detailMutate, invalidChunkList, invalidChildChunkList], + [invalidDocumentList, backToPrev, detailMutate, queryClient, datasetId, documentId], ) const parentMode = useMemo(() => { @@ -250,15 +266,14 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { ], ) - const docDetail = useMemo( - () => - ({ - ...documentDetail, - ...documentMetadata, - doc_type: documentMetadata?.doc_type === 'others' ? '' : documentMetadata?.doc_type, - }) as FullDocumentDetail, - [documentDetail, documentMetadata], - ) + const docDetail = useMemo((): FullDocumentDetail | undefined => { + if (!documentDetail) return undefined + return { + ...documentDetail, + ...documentMetadata, + doc_type: documentMetadata?.doc_type === 'others' ? '' : documentMetadata?.doc_type, + } + }, [documentDetail, documentMetadata]) const backButtonLabel = t(($) => $['operation.back'], { ns: 'common' }) const metadataToggleLabel = `${ @@ -309,7 +324,7 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { )} {documentDetail && ( = ({ datasetId, documentId }) => { {embedding ? ( ) : ( @@ -377,7 +392,7 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { showNewSegmentModal={newSegmentModalVisible} onNewSegmentModalChange={setNewSegmentModalVisible} importStatus={importStatus} - archived={documentDetail?.archived} + archived={documentDetail?.archived ?? undefined} /> )}
@@ -389,13 +404,15 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { isMobile={isMobile} panelClassName="justify-start!" > - + {docDetail && ( + + )}
= ({ setAttachments(attachments) }, []) - const { mutateAsync: addSegment } = useAddSegment() + const { mutateAsync: addSegment } = useMutation( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segment.post.mutationOptions(), + ) const handleSave = useCallback(async () => { const params: SegmentUpdater = { content: '', attachment_ids: [] } @@ -93,7 +96,7 @@ const NewSegmentModal: FC = ({ setLoading(true) await addSegment( - { datasetId, documentId, body: params }, + { params: { dataset_id: datasetId, document_id: documentId }, body: params }, { onSuccess() { toast.success( diff --git a/web/app/components/datasets/documents/detail/settings/__tests__/document-settings.spec.tsx b/web/app/components/datasets/documents/detail/settings/__tests__/document-settings.spec.tsx index a07df9e606d7e0..ba4c829955f7df 100644 --- a/web/app/components/datasets/documents/detail/settings/__tests__/document-settings.spec.tsx +++ b/web/app/components/datasets/documents/detail/settings/__tests__/document-settings.spec.tsx @@ -1,5 +1,8 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { QueryClient } from '@tanstack/react-query' +import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQuery } from '@/service/console' +import { renderWithConsoleQuery as render } from '@/test/console/query-data' import DocumentSettings from '../document-settings' const mockPush = vi.fn() @@ -29,8 +32,6 @@ vi.mock('@/hooks/use-document-title', () => ({ default: mockUseDocumentTitle, })) -const mockInvalidDocumentList = vi.fn() -const mockInvalidDocumentDetail = vi.fn() let mockDocumentDetail: Record | null = { name: 'test-document', data_source_type: 'upload_file', @@ -40,14 +41,26 @@ let mockDocumentDetail: Record | null = { } let mockError: Error | null = null -vi.mock('@/service/knowledge/use-document', () => ({ - useDocumentDetail: () => ({ - data: mockDocumentDetail, - error: mockError, - }), - useInvalidDocumentList: () => mockInvalidDocumentList, - useInvalidDocumentDetail: () => mockInvalidDocumentDetail, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.get': () => ({ + data: mockDocumentDetail, + error: mockError, + }), + }) +}) + +const expectDocumentsInvalidated = (datasetId: string) => { + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params: { dataset_id: datasetId } }, + }), + }) + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key(), + }) +} // Mock useDefaultModel vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ @@ -119,6 +132,7 @@ vi.mock('nuqs', async (importOriginal) => { describe('DocumentSettings', () => { beforeEach(() => { + vi.spyOn(QueryClient.prototype, 'invalidateQueries') vi.clearAllMocks() mockDocumentDetail = { name: 'test-document', @@ -209,8 +223,7 @@ describe('DocumentSettings', () => { fireEvent.click(screen.getByTestId('save-btn')) - expect(mockInvalidDocumentList).toHaveBeenCalled() - expect(mockInvalidDocumentDetail).toHaveBeenCalled() + expectDocumentsInvalidated(defaultProps.datasetId) expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-1/documents/document-1') }) diff --git a/web/app/components/datasets/documents/detail/settings/document-settings.tsx b/web/app/components/datasets/documents/detail/settings/document-settings.tsx index e8ee6f6ec9244a..d3686ca6084bb1 100644 --- a/web/app/components/datasets/documents/detail/settings/document-settings.tsx +++ b/web/app/components/datasets/documents/detail/settings/document-settings.tsx @@ -1,8 +1,10 @@ -import type { DataSourceProvider, NotionPage } from '@/models/common' +import type { + StepTwoFile, + StepTwoNotionPage, +} from '@/app/components/datasets/create/step-two/types' +import type { DataSourceProvider } from '@/models/common' import type { CrawlOptions, - CustomFile, - DataSourceInfo, DataSourceType, LegacyDataSourceInfo, LocalFileInfo, @@ -10,6 +12,7 @@ import type { UploadFileIdInfo, WebsiteCrawlInfo, } from '@/models/datasets' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -17,6 +20,7 @@ import { useContext } from 'use-context-selector' import AppUnavailable from '@/app/components/base/app-unavailable' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import StepTwo from '@/app/components/datasets/create/step-two' +import { getNotionPageIcon } from '@/app/components/datasets/documents/document-source' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { @@ -26,11 +30,31 @@ import { import DatasetDetailContext from '@/context/dataset-detail' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter } from '@/next/navigation' -import { - useDocumentDetail, - useInvalidDocumentDetail, - useInvalidDocumentList, -} from '@/service/knowledge/use-document' +import { consoleQuery } from '@/service/console' + +// data_source_info is written by the data source that imported the document. +type DocumentSourceInfo = Record | null | undefined + +const isCount = (value: unknown): value is number | string => + typeof value === 'number' || typeof value === 'string' + +// Website imports store the crawl options they ran with next to the crawled page. +const toCrawlOptions = (info: DocumentSourceInfo): CrawlOptions | undefined => { + if (!info) return undefined + const { includes, excludes, crawl_sub_pages, only_main_content, use_sitemap, limit, max_depth } = + info + if ( + typeof includes !== 'string' || + typeof excludes !== 'string' || + typeof crawl_sub_pages !== 'boolean' || + typeof only_main_content !== 'boolean' || + typeof use_sitemap !== 'boolean' || + !isCount(limit) || + !isCount(max_depth) + ) + return undefined + return { includes, excludes, crawl_sub_pages, only_main_content, use_sitemap, limit, max_depth } +} type DocumentSettingsProps = { datasetId: string @@ -47,8 +71,17 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { setSettingsDestination('provider') }, [setSettingsDestination]) - const invalidDocumentList = useInvalidDocumentList(datasetId) - const invalidDocumentDetail = useInvalidDocumentDetail() + const queryClient = useQueryClient() + const invalidDocumentList = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params: { dataset_id: datasetId } }, + }), + }) + const invalidDocumentDetail = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key(), + }) const saveHandler = () => { invalidDocumentList() invalidDocumentDetail() @@ -57,11 +90,14 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { const cancelHandler = () => router.back() - const { data: documentDetail, error } = useDocumentDetail({ - datasetId, - documentId, - params: { metadata: 'without' }, - }) + const { data: documentDetail, error } = useQuery( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.queryOptions({ + input: { + params: { dataset_id: datasetId, document_id: documentId }, + query: { metadata: 'without' }, + }, + }), + ) const settingsTitle = t(($) => $['documentSettings.title'], { ns: 'datasetPipeline' }) const documentTitle = documentDetail?.name || t(($) => $['datasetMenus.documents'], { ns: 'common' }) @@ -70,22 +106,20 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { const dataSourceInfo = documentDetail?.data_source_info - // Type guards for DataSourceInfo union - const isLegacyDataSourceInfo = ( - info: DataSourceInfo | undefined, - ): info is LegacyDataSourceInfo => { + // Type guards for the data source info shapes + const isLegacyDataSourceInfo = (info: DocumentSourceInfo): info is LegacyDataSourceInfo => { return !!info && 'upload_file' in info } - const isWebsiteCrawlInfo = (info: DataSourceInfo | undefined): info is WebsiteCrawlInfo => { + const isWebsiteCrawlInfo = (info: DocumentSourceInfo): info is WebsiteCrawlInfo => { return !!info && 'source_url' in info && 'title' in info } - const isOnlineDocumentInfo = (info: DataSourceInfo | undefined): info is OnlineDocumentInfo => { + const isOnlineDocumentInfo = (info: DocumentSourceInfo): info is OnlineDocumentInfo => { return !!info && 'page' in info } - const isLocalFileInfo = (info: DataSourceInfo | undefined): info is LocalFileInfo => { + const isLocalFileInfo = (info: DocumentSourceInfo): info is LocalFileInfo => { return !!info && 'related_id' in info && 'transfer_method' in info } - const isUploadFileIdInfo = (info: DataSourceInfo | undefined): info is UploadFileIdInfo => { + const isUploadFileIdInfo = (info: DocumentSourceInfo): info is UploadFileIdInfo => { return !!info && 'upload_file_id' in info } @@ -95,14 +129,16 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { const localFileInfo = isLocalFileInfo(dataSourceInfo) ? dataSourceInfo : undefined const uploadFileIdInfo = isUploadFileIdInfo(dataSourceInfo) ? dataSourceInfo : undefined - const currentPage = useMemo(() => { + const currentPage = useMemo(() => { if (legacyInfo) { + // Only icon objects carry the icon type the import payload needs. + const icon = getNotionPageIcon(dataSourceInfo, false) return { workspace_id: legacyInfo.notion_workspace_id ?? '', page_id: legacyInfo.notion_page_id ?? '', - page_name: documentDetail?.name, - page_icon: legacyInfo.notion_page_icon, - type: documentDetail?.data_source_type, + page_name: documentDetail?.name ?? '', + page_icon: typeof icon === 'object' ? icon : null, + type: documentDetail?.data_source_type ?? '', } } if (onlineDocumentInfo) { @@ -115,34 +151,30 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { } } return undefined - }, [documentDetail?.data_source_type, documentDetail?.name, legacyInfo, onlineDocumentInfo]) - - const files = useMemo(() => { + }, [ + dataSourceInfo, + documentDetail?.data_source_type, + documentDetail?.name, + legacyInfo, + onlineDocumentInfo, + ]) + + const files = useMemo(() => { // Handle upload_file_id format if (uploadFileIdInfo) { - return [ - { - id: uploadFileIdInfo.upload_file_id, - name: documentDetail?.name || '', - } as unknown as CustomFile, - ] + return [{ id: uploadFileIdInfo.upload_file_id, name: documentDetail?.name || '' }] } // Handle legacy upload_file format if (legacyInfo?.upload_file) { - return [legacyInfo.upload_file as unknown as CustomFile] + const { id, name, extension } = legacyInfo.upload_file + return [{ id, name, extension }] } // Handle local file info format if (localFileInfo) { const { related_id, name, extension } = localFileInfo - return [ - { - id: related_id, - name, - extension, - } as unknown as CustomFile, - ] + return [{ id: related_id, name, extension }] } return [] @@ -160,13 +192,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { ] }, [websiteInfo]) - const crawlOptions = - dataSourceInfo && - typeof dataSourceInfo === 'object' && - 'includes' in dataSourceInfo && - 'excludes' in dataSourceInfo - ? (dataSourceInfo as unknown as CrawlOptions) - : undefined + const crawlOptions = toCrawlOptions(dataSourceInfo) const websiteCrawlProvider = (websiteInfo?.provider ?? legacyInfo?.provider) as | DataSourceProvider @@ -191,7 +217,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { onSetting={handleOpenAccountSetting} datasetId={datasetId} dataSourceType={documentDetail.data_source_type as DataSourceType} - notionPages={currentPage ? [currentPage as unknown as NotionPage] : []} + notionPages={currentPage ? [currentPage] : []} notionCredentialId={ legacyInfo?.credential_id || onlineDocumentInfo?.credential_id || '' } diff --git a/web/app/components/datasets/documents/detail/settings/pipeline-settings/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/settings/pipeline-settings/__tests__/index.spec.tsx index 0e94ab4d31ec2e..61c1d944e38f8f 100644 --- a/web/app/components/datasets/documents/detail/settings/pipeline-settings/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/settings/pipeline-settings/__tests__/index.spec.tsx @@ -2,6 +2,7 @@ import type { PipelineExecutionLogResponse } from '@/models/pipeline' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { DatasourceType } from '@/models/pipeline' +import { consoleQuery } from '@/service/console' import PipelineSettings from '../index' // Mock Next.js router @@ -26,24 +27,31 @@ vi.mock('@/context/dataset-detail', () => ({ const mockUsePipelineExecutionLog = vi.fn() const mockMutateAsync = vi.fn() const mockUseRunPublishedPipeline = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - usePipelineExecutionLog: (params: { dataset_id: string; document_id: string }) => - mockUsePipelineExecutionLog(params), - useRunPublishedPipeline: () => mockUseRunPublishedPipeline(), - // For ProcessDocuments component - usePublishedPipelineProcessingParams: () => ({ - data: { variables: [] }, - isFetching: false, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.byDocumentId.pipelineExecutionLog.get': (o: { + input: { params: { dataset_id: string; document_id: string } } + }) => mockUsePipelineExecutionLog(o.input.params), + 'rag.pipelines.byPipelineId.workflows.published.run.post': () => mockUseRunPublishedPipeline(), + // For ProcessDocuments component + 'rag.pipelines.byPipelineId.workflows.published.processing.parameters.get': () => ({ + data: { variables: [] }, + isFetching: false, + }), + }) +}) -// Mock document invalidation hooks -const mockInvalidDocumentList = vi.fn() -const mockInvalidDocumentDetail = vi.fn() -vi.mock('@/service/knowledge/use-document', () => ({ - useInvalidDocumentList: () => mockInvalidDocumentList, - useInvalidDocumentDetail: () => mockInvalidDocumentDetail, -})) +const expectDocumentsInvalidated = (datasetId: string) => { + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params: { dataset_id: datasetId } }, + }), + }) + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key(), + }) +} // Mock Form component in ProcessDocuments - internal dependencies are too complex vi.mock('../../../../create-from-pipeline/process-documents/form', () => ({ @@ -164,8 +172,7 @@ describe('PipelineSettings', () => { mockPush.mockClear() mockBack.mockClear() mockMutateAsync.mockClear() - mockInvalidDocumentList.mockClear() - mockInvalidDocumentDetail.mockClear() + vi.spyOn(QueryClient.prototype, 'invalidateQueries') // Default: successful data fetch mockUsePipelineExecutionLog.mockReturnValue({ @@ -377,9 +384,11 @@ describe('PipelineSettings', () => { await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - is_preview: false, - pipeline_id: mockPipelineId, - original_document_id: 'document-456', + params: { pipeline_id: mockPipelineId }, + body: expect.objectContaining({ + is_preview: false, + original_document_id: 'document-456', + }), }), expect.any(Object), ) @@ -416,8 +425,7 @@ describe('PipelineSettings', () => { ) await waitFor(() => { - expect(mockInvalidDocumentList).toHaveBeenCalled() - expect(mockInvalidDocumentDetail).toHaveBeenCalled() + expectDocumentsInvalidated('dataset-123') }) }) }) @@ -446,8 +454,8 @@ describe('PipelineSettings', () => { await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - is_preview: true, - pipeline_id: mockPipelineId, + params: { pipeline_id: mockPipelineId }, + body: expect.objectContaining({ is_preview: true }), }), expect.any(Object), ) @@ -455,7 +463,7 @@ describe('PipelineSettings', () => { }) it('should update estimateData on successful preview', async () => { - const mockOutputs = { chunks: [], total_tokens: 50 } + const mockOutputs = { total_nodes: 1, total_segments: 0, preview: [], qa_preview: [] } mockMutateAsync.mockImplementation((_req, opts) => { opts?.onSuccess?.({ data: { outputs: mockOutputs } }) return Promise.resolve({ data: { outputs: mockOutputs } }) @@ -496,12 +504,15 @@ describe('PipelineSettings', () => { await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith( { - pipeline_id: mockPipelineId, - inputs: {}, - start_node_id: 'node-xyz', - datasource_type: DatasourceType.localFile, - datasource_info_list: [{ related_id: 'file-1', name: 'test.pdf', extension: 'pdf' }], - is_preview: true, + params: { pipeline_id: mockPipelineId }, + body: { + inputs: {}, + start_node_id: 'node-xyz', + datasource_type: DatasourceType.localFile, + datasource_info_list: [{ related_id: 'file-1', name: 'test.pdf', extension: 'pdf' }], + is_preview: true, + response_mode: 'blocking', + }, }, expect.any(Object), ) @@ -583,7 +594,7 @@ describe('PipelineSettings', () => { }) it('should update estimateData after successful preview', async () => { - const mockEstimateData = { chunks: [], total_tokens: 50 } + const mockEstimateData = { total_nodes: 1, total_segments: 0, preview: [], qa_preview: [] } mockMutateAsync.mockImplementation((_req, opts) => { opts?.onSuccess?.({ data: { outputs: mockEstimateData } }) return Promise.resolve({ data: { outputs: mockEstimateData } }) @@ -609,7 +620,7 @@ describe('PipelineSettings', () => { await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith( - expect.objectContaining({ is_preview: false }), + expect.objectContaining({ body: expect.objectContaining({ is_preview: false }) }), expect.any(Object), ) }) @@ -624,7 +635,7 @@ describe('PipelineSettings', () => { await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledWith( - expect.objectContaining({ is_preview: true }), + expect.objectContaining({ body: expect.objectContaining({ is_preview: true }) }), expect.any(Object), ) }) diff --git a/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx b/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx index 35fc30795e9689..0e39765dbd5b77 100644 --- a/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx +++ b/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx @@ -1,16 +1,21 @@ import type { NotionPage } from '@/models/common' import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse } from '@/models/datasets' -import type { OnlineDriveFile, PublishedPipelineRunPreviewResponse } from '@/models/pipeline' +import type { OnlineDriveFile } from '@/models/pipeline' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppUnavailable from '@/app/components/base/app-unavailable' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { + isPipelineProcessingRun, + selectPipelineExecutionLog, + selectPreviewRunEstimate, +} from '@/app/components/rag-pipeline/utils/query-select' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { DatasourceType } from '@/models/pipeline' import { useRouter } from '@/next/navigation' -import { useInvalidDocumentDetail, useInvalidDocumentList } from '@/service/knowledge/use-document' -import { usePipelineExecutionLog, useRunPublishedPipeline } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import ChunkPreview from '../../../create-from-pipeline/preview/chunk-preview' import LeftHeader from './left-header' import ProcessDocuments from './process-documents' @@ -35,10 +40,12 @@ const PipelineSettings = ({ datasetId, documentId }: PipelineSettingsProps) => { data: lastRunData, isFetching: isFetchingLastRunData, isError, - } = usePipelineExecutionLog({ - dataset_id: datasetId, - document_id: documentId, - }) + } = useQuery( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.pipelineExecutionLog.get.queryOptions({ + input: { params: { dataset_id: datasetId, document_id: documentId } }, + select: selectPipelineExecutionLog, + }), + ) const files = useMemo(() => { const files: CustomFile[] = [] @@ -93,7 +100,13 @@ const PipelineSettings = ({ datasetId, documentId }: PipelineSettingsProps) => { return onlineDriveFiles }, [lastRunData]) - const { mutateAsync: runPublishedPipeline, isIdle, isPending } = useRunPublishedPipeline() + const { + mutateAsync: runPublishedPipeline, + isIdle, + isPending, + } = useMutation( + consoleQuery.rag.pipelines.byPipelineId.workflows.published.run.post.mutationOptions(), + ) const handlePreviewChunks = useCallback( async (data: Record) => { @@ -103,16 +116,19 @@ const PipelineSettings = ({ datasetId, documentId }: PipelineSettingsProps) => { datasourceInfoList.push(documentInfo) await runPublishedPipeline( { - pipeline_id: pipelineId!, - inputs: data, - start_node_id: lastRunData.datasource_node_id, - datasource_type: lastRunData.datasource_type, - datasource_info_list: datasourceInfoList, - is_preview: true, + params: { pipeline_id: pipelineId! }, + body: { + inputs: data, + start_node_id: lastRunData.datasource_node_id, + datasource_type: lastRunData.datasource_type, + datasource_info_list: datasourceInfoList, + is_preview: true, + response_mode: 'blocking', + }, }, { onSuccess: (res) => { - setEstimateData((res as PublishedPipelineRunPreviewResponse).data.outputs) + if (!isPipelineProcessingRun(res)) setEstimateData(selectPreviewRunEstimate(res)) }, }, ) @@ -120,8 +136,17 @@ const PipelineSettings = ({ datasetId, documentId }: PipelineSettingsProps) => { [lastRunData, pipelineId, runPublishedPipeline], ) - const invalidDocumentList = useInvalidDocumentList(datasetId) - const invalidDocumentDetail = useInvalidDocumentDetail() + const queryClient = useQueryClient() + const invalidDocumentList = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params: { dataset_id: datasetId } }, + }), + }) + const invalidDocumentDetail = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key(), + }) const handleProcess = useCallback( async (data: Record) => { if (!lastRunData) return @@ -130,13 +155,16 @@ const PipelineSettings = ({ datasetId, documentId }: PipelineSettingsProps) => { datasourceInfoList.push(documentInfo) await runPublishedPipeline( { - pipeline_id: pipelineId!, - inputs: data, - start_node_id: lastRunData.datasource_node_id, - datasource_type: lastRunData.datasource_type, - datasource_info_list: datasourceInfoList, - original_document_id: documentId, - is_preview: false, + params: { pipeline_id: pipelineId! }, + body: { + inputs: data, + start_node_id: lastRunData.datasource_node_id, + datasource_type: lastRunData.datasource_type, + datasource_info_list: datasourceInfoList, + original_document_id: documentId, + is_preview: false, + response_mode: 'blocking', + }, }, { onSuccess: () => { diff --git a/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/__tests__/hooks.spec.ts b/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/__tests__/hooks.spec.ts index bd4e5488e6a4d2..abacd44aba673e 100644 --- a/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/__tests__/hooks.spec.ts +++ b/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/__tests__/hooks.spec.ts @@ -19,10 +19,19 @@ const mockUsePublishedPipelineProcessingParams = vi.fn( (_params: { pipeline_id: string; node_id: string }) => mockParamsReturn, ) -vi.mock('@/service/use-pipeline', () => ({ - usePublishedPipelineProcessingParams: (params: { pipeline_id: string; node_id: string }) => - mockUsePublishedPipelineProcessingParams(params), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.published.processing.parameters.get': (o: { + input: { params: { pipeline_id: string }; query: { node_id: string } } + enabled?: boolean + }) => + mockUsePublishedPipelineProcessingParams({ + pipeline_id: o.input.params.pipeline_id, + node_id: o.input.query.node_id, + }), + }) +}) describe('useInputVariables', () => { beforeEach(() => { diff --git a/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/__tests__/index.spec.tsx index 95a9ce32367ea5..e5e708024cc14a 100644 --- a/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/__tests__/index.spec.tsx @@ -14,12 +14,15 @@ vi.mock('@/context/dataset-detail', () => ({ // Mock API call for pipeline processing params const mockParamsConfig = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - usePublishedPipelineProcessingParams: () => ({ - data: mockParamsConfig(), - isFetching: false, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.published.processing.parameters.get': () => ({ + data: mockParamsConfig(), + isFetching: false, + }), + }) +}) // Mock Form component - internal dependencies (useAppForm, BaseField) are too complex // Keep the mock minimal and focused on testing the integration diff --git a/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/hooks.ts b/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/hooks.ts index dd5655d062b7e8..08d60249be5082 100644 --- a/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/hooks.ts +++ b/web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/hooks.ts @@ -1,13 +1,17 @@ +import { useQuery } from '@tanstack/react-query' +import { selectPipelineStepParameters } from '@/app/components/rag-pipeline/utils/query-select' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' -import { usePublishedPipelineProcessingParams } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' export const useInputVariables = (datasourceNodeId: string) => { const pipelineId = useDatasetDetailContextWithSelector((state) => state.dataset?.pipeline_id) - const { data: paramsConfig, isFetching: isFetchingParams } = usePublishedPipelineProcessingParams( - { - pipeline_id: pipelineId!, - node_id: datasourceNodeId, - }, + const { data: paramsConfig, isFetching: isFetchingParams } = useQuery( + consoleQuery.rag.pipelines.byPipelineId.workflows.published.processing.parameters.get.queryOptions( + { + input: { params: { pipeline_id: pipelineId! }, query: { node_id: datasourceNodeId } }, + select: selectPipelineStepParameters, + }, + ), ) return { diff --git a/web/app/components/datasets/documents/document-source.ts b/web/app/components/datasets/documents/document-source.ts new file mode 100644 index 00000000000000..575b00b43302ab --- /dev/null +++ b/web/app/components/datasets/documents/document-source.ts @@ -0,0 +1,39 @@ +import type { DataSourceNotionPage } from '@/models/common' + +// data_source_info is written by the data source that imported the document, so its keys are +// read defensively for each source type. +type DocumentSourceInfo = Record | null | undefined + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +const getString = (record: Record | undefined, key: string) => { + const value = record?.[key] + return typeof value === 'string' ? value : undefined +} + +const isNotionPageIcon = ( + value: unknown, +): value is NonNullable => + isRecord(value) && typeof value.type === 'string' + +const toNotionIcon = (value: unknown) => + typeof value === 'string' || isNotionPageIcon(value) ? value : undefined + +/** The file extension of a legacy uploaded-file document. */ +export const getUploadFileExtension = (info: DocumentSourceInfo) => + isRecord(info?.upload_file) ? getString(info.upload_file, 'extension') : undefined + +/** The file extension recorded by a pipeline local-file data source. */ +export const getLocalFileExtension = (info: DocumentSourceInfo) => + getString(info ?? undefined, 'extension') + +/** The file name recorded by a pipeline online-drive data source. */ +export const getOnlineDriveFileName = (info: DocumentSourceInfo) => + getString(info ?? undefined, 'name') + +/** The page icon of a Notion import, or of a pipeline online-document page. */ +export const getNotionPageIcon = (info: DocumentSourceInfo, fromPipeline: boolean) => { + if (!fromPipeline) return toNotionIcon(info?.notion_page_icon) + return isRecord(info?.page) ? toNotionIcon(info.page.page_icon) : undefined +} diff --git a/web/app/components/datasets/documents/index.tsx b/web/app/components/datasets/documents/index.tsx index c07d66c5ab3993..2d9e9db045994d 100644 --- a/web/app/components/datasets/documents/index.tsx +++ b/web/app/components/datasets/documents/index.tsx @@ -1,28 +1,24 @@ 'use client' import type { FC } from 'react' -import { useSuspenseQuery } from '@tanstack/react-query' +import { keepPreviousData, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback } from 'react' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toCreationDataSourceType } from '@/app/components/datasets/dataset-fields' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { DataSourceType } from '@/models/datasets' import { useRouter } from '@/next/navigation' -import { - useDocumentList, - useInvalidDocumentDetail, - useInvalidDocumentList, -} from '@/service/knowledge/use-document' -import { useChildSegmentListKey, useSegmentListKey } from '@/service/knowledge/use-segment' -import { useInvalid } from '@/service/use-base' +import { consoleQuery } from '@/service/console' import { getDatasetACLCapabilities } from '@/utils/permission' import useEditDocumentMetadata from '../metadata/hooks/use-edit-dataset-metadata' import DocumentsHeader from './components/documents-header' import EmptyElement from './components/empty-element' import List from './components/list' import { useDocumentsPageState } from './hooks/use-documents-page-state' +import { toDocumentListQuery } from './status-filter' type IDocumentsProps = { datasetId: string @@ -68,42 +64,65 @@ const Documents: FC = ({ datasetId }) => { } = useDocumentsPageState() // Fetch document list - const { data: documentsRes, isLoading: isListLoading } = useDocumentList({ - datasetId, - query: { - page: currPage + 1, - limit, - keyword: debouncedSearchValue, - status: normalizedStatusFilterValue, - sort: sortValue, - }, - refetchInterval: (query) => { - const shouldForcePolling = - normalizedStatusFilterValue !== 'all' && - FORCED_POLLING_STATUSES.has(normalizedStatusFilterValue) - const documents = query.state.data?.data - if (!documents) return POLLING_INTERVAL - const hasIncompleteDocuments = documents.some( - ({ indexing_status }) => !TERMINAL_INDEXING_STATUSES.has(indexing_status), - ) - return shouldForcePolling || hasIncompleteDocuments ? POLLING_INTERVAL : false - }, - }) + const { data: documentsRes, isLoading: isListLoading } = useQuery( + consoleQuery.datasets.byDatasetId.documents.get.queryOptions({ + input: { + params: { dataset_id: datasetId }, + query: toDocumentListQuery({ + page: currPage + 1, + limit, + keyword: debouncedSearchValue, + status: normalizedStatusFilterValue, + sort: sortValue, + }), + }, + placeholderData: keepPreviousData, + refetchInterval: (query) => { + const shouldForcePolling = + normalizedStatusFilterValue !== 'all' && + FORCED_POLLING_STATUSES.has(normalizedStatusFilterValue) + const documents = query.state.data?.data + if (!documents) return POLLING_INTERVAL + const hasIncompleteDocuments = documents.some( + ({ indexing_status }) => !TERMINAL_INDEXING_STATUSES.has(indexing_status ?? ''), + ) + return shouldForcePolling || hasIncompleteDocuments ? POLLING_INTERVAL : false + }, + }), + ) // Invalidation hooks - const invalidDocumentList = useInvalidDocumentList(datasetId) - const invalidDocumentDetail = useInvalidDocumentDetail() - const invalidChunkList = useInvalid(useSegmentListKey) - const invalidChildChunkList = useInvalid(useChildSegmentListKey) + const queryClient = useQueryClient() + const invalidDocumentList = useCallback( + () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params: { dataset_id: datasetId } }, + }), + }), + [queryClient, datasetId], + ) const handleUpdate = useCallback(() => { invalidDocumentList() - invalidDocumentDetail() + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key(), + }) setTimeout(() => { - invalidChunkList() - invalidChildChunkList() + const params = { dataset_id: datasetId } + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.get.key({ + input: { params }, + }), + }) + queryClient.invalidateQueries({ + queryKey: + consoleQuery.datasets.byDatasetId.documents.byDocumentId.segments.bySegmentId.childChunks.get.key( + { input: { params } }, + ), + }) }, 5000) - }, [invalidDocumentList, invalidDocumentDetail, invalidChunkList, invalidChildChunkList]) + }, [queryClient, datasetId, invalidDocumentList]) // Metadata editing hook const { @@ -177,7 +196,7 @@ const Documents: FC = ({ datasetId }) => {
{ return QUERY_STATUS_ALIASES[sanitized] || sanitized } + +export const toDocumentListQuery = ({ + status, + sort, + ...query +}: { + keyword: string + page: number + limit: number + sort?: string + status?: string +}) => { + const normalizedStatus = normalizeStatusForQuery(status) + return { + ...query, + ...(sort && { sort }), + ...(normalizedStatus && normalizedStatus !== 'all' && { status: normalizedStatus }), + } +} diff --git a/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx b/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx index 6dd7d1fafd132d..ef47e12b20859b 100644 --- a/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx @@ -59,11 +59,20 @@ const mockEnable = vi.fn() const mockDisable = vi.fn() const mockDelete = vi.fn() -vi.mock('@/service/knowledge/use-document', () => ({ - useDocumentEnable: () => ({ mutateAsync: mockEnable }), - useDocumentDisable: () => ({ mutateAsync: mockDisable }), - useDocumentDelete: () => ({ mutateAsync: mockDelete }), -})) +const statusMocks: Record unknown> = { + enable: (input) => mockEnable(input), + disable: (input) => mockDisable(input), +} +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.status.byAction.batch.patch': () => ({ + mutateAsync: (input: { params: { action: string } }) => + statusMocks[input.params.action]!(input), + }), + 'datasets.byDatasetId.documents.delete': () => ({ mutateAsync: mockDelete }), + }) +}) beforeEach(() => { vi.clearAllMocks() @@ -274,7 +283,10 @@ describe('StatusItem', () => { await act(async () => { vi.advanceTimersByTime(600) }) - expect(mockEnable).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockEnable).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', action: 'enable' }, + query: { document_id: ['doc-1'] }, + }) vi.useRealTimers() }) @@ -302,7 +314,10 @@ describe('StatusItem', () => { await act(async () => { vi.advanceTimersByTime(600) }) - expect(mockDisable).toHaveBeenCalledWith({ datasetId: 'dataset-1', documentId: 'doc-1' }) + expect(mockDisable).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-1', action: 'disable' }, + query: { document_id: ['doc-1'] }, + }) vi.useRealTimers() }) diff --git a/web/app/components/datasets/documents/status-item/index.tsx b/web/app/components/datasets/documents/status-item/index.tsx index fcd1755d864d30..fe1b7e7413e5b2 100644 --- a/web/app/components/datasets/documents/status-item/index.tsx +++ b/web/app/components/datasets/documents/status-item/index.tsx @@ -1,22 +1,18 @@ import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot' import type { OperationName } from '../types' -import type { CommonResponse } from '@/models/common' import type { DocumentDisplayStatus } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useMutation } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' -import { - useDocumentDelete, - useDocumentDisable, - useDocumentEnable, -} from '@/service/knowledge/use-document' +import { consoleQuery } from '@/service/console' import { asyncRunSafe } from '@/utils' import s from '../style.module.css' import { useIndexStatus } from './hooks' @@ -61,23 +57,21 @@ const StatusItem = ({ const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP const statusItem = DOC_INDEX_STATUS_MAP[localStatus] const { enabled = false, archived = false, id = '' } = detail || {} - const { mutateAsync: enableDocument } = useDocumentEnable() - const { mutateAsync: disableDocument } = useDocumentDisable() - const { mutateAsync: deleteDocument } = useDocumentDelete() + const documents = consoleQuery.datasets.byDatasetId.documents + const { mutateAsync: changeDocumentStatus } = useMutation( + documents.status.byAction.batch.patch.mutationOptions(), + ) + const { mutateAsync: deleteDocuments } = useMutation(documents.delete.mutationOptions()) const onOperate = async (operationName: OperationName) => { if (!canEdit) return - let opApi = deleteDocument - switch (operationName) { - case 'enable': - opApi = enableDocument - break - case 'disable': - opApi = disableDocument - break - } - const [e] = await asyncRunSafe( - opApi({ datasetId, documentId: id }) as Promise, + const [e] = await asyncRunSafe( + operationName === 'enable' || operationName === 'disable' + ? changeDocumentStatus({ + params: { dataset_id: datasetId, action: operationName }, + query: { document_id: [id] }, + }) + : deleteDocuments({ params: { dataset_id: datasetId }, query: { document_id: [id] } }), ) if (!e) { onUpdate?.(operationName) diff --git a/web/app/components/datasets/external-api/external-api-modal/index.tsx b/web/app/components/datasets/external-api/external-api-modal/index.tsx index 81440f45d202d5..87953f313c0882 100644 --- a/web/app/components/datasets/external-api/external-api-modal/index.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/index.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react' import type { CreateExternalAPIReq, FormSchema } from '../declarations' +import type { ExternalAPIItem } from '@/models/datasets' import { AlertDialog, AlertDialogCancelButton, @@ -22,7 +23,7 @@ import Form from './Form' type AddExternalAPIModalProps = { data?: CreateExternalAPIReq - onSave: (formValue: CreateExternalAPIReq) => void + onSave: (api: ExternalAPIItem) => void onCancel: () => void onEdit?: (formValue: CreateExternalAPIReq) => Promise datasetBindings?: { diff --git a/web/app/components/datasets/external-api/external-knowledge-api-card/__tests__/index.spec.tsx b/web/app/components/datasets/external-api/external-knowledge-api-card/__tests__/index.spec.tsx index 5e989cf31ce302..95dd57b412653a 100644 --- a/web/app/components/datasets/external-api/external-knowledge-api-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/external-api/external-knowledge-api-card/__tests__/index.spec.tsx @@ -410,29 +410,5 @@ describe('ExternalKnowledgeAPICard', () => { ) expect(screen.getByText('Test External API'))!.toBeInTheDocument() }) - - it('should handle delete response with unsuccessful result', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - vi.mocked(checkUsageExternalAPI).mockResolvedValue({ is_using: false, count: 0 }) - vi.mocked(deleteExternalAPI).mockResolvedValue({ result: 'error' }) - - render() - const deleteButton = screen.getByRole('button', { name: deleteButtonName }) - - fireEvent.click(deleteButton!) - - await waitFor(() => { - expect(screen.getByRole('button', { name: /confirm/i }))!.toBeInTheDocument() - }) - - const confirmButton = screen.getByRole('button', { name: /confirm/i }) - fireEvent.click(confirmButton) - - await waitFor(() => { - expect(consoleSpy).toHaveBeenCalledWith('Failed to delete external API') - }) - - consoleSpy.mockRestore() - }) }) }) diff --git a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx index 4744a0dbd84af2..05c88ed86ba0c2 100644 --- a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx +++ b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx @@ -55,8 +55,8 @@ const ExternalKnowledgeAPICard: React.FC = ({ const formValue: CreateExternalAPIReq = { name: response.name, settings: { - endpoint: response.settings.endpoint, - api_key: response.settings.api_key, + endpoint: response.settings?.endpoint ?? '', + api_key: response.settings?.api_key ?? '', }, } @@ -68,15 +68,7 @@ const ExternalKnowledgeAPICard: React.FC = ({ try { await updateExternalAPI({ apiTemplateId: api.id, - body: { - ...response, - name: updatedData.name, - settings: { - ...response.settings, - endpoint: updatedData.settings.endpoint, - api_key: updatedData.settings.api_key, - }, - }, + body: { name: updatedData.name, settings: updatedData.settings }, }) await queryClient.invalidateQueries({ queryKey: externalKnowledgeApiQueryKey }) } catch (error) { @@ -106,13 +98,9 @@ const ExternalKnowledgeAPICard: React.FC = ({ if (!canManageExternalKnowledgeApi) return try { - const response = await deleteExternalAPI({ apiTemplateId: api.id }) - if (response && response.result === 'success') { - setShowConfirm(false) - await queryClient.invalidateQueries({ queryKey: externalKnowledgeApiQueryKey }) - } else { - console.error('Failed to delete external API') - } + await deleteExternalAPI({ apiTemplateId: api.id }) + setShowConfirm(false) + await queryClient.invalidateQueries({ queryKey: externalKnowledgeApiQueryKey }) } catch (error) { console.error('Error deleting external knowledge API:', error) } diff --git a/web/app/components/datasets/extra-info/api-access/__tests__/card.spec.tsx b/web/app/components/datasets/extra-info/api-access/__tests__/card.spec.tsx index de3ec933c225f3..ad086531c80cd6 100644 --- a/web/app/components/datasets/extra-info/api-access/__tests__/card.spec.tsx +++ b/web/app/components/datasets/extra-info/api-access/__tests__/card.spec.tsx @@ -28,10 +28,15 @@ vi.mock('@/context/dataset-detail', () => ({ }), })) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useEnableDatasetServiceApi: () => ({ mutateAsync: mockEnableApi }), - useDisableDatasetServiceApi: () => ({ mutateAsync: mockDisableApi }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.apiKeys.byStatus.post': () => ({ + mutateAsync: (input: { params: { status: string } }) => + input.params.status === 'enable' ? mockEnableApi(input) : mockDisableApi(input), + }), + }) +}) vi.mock('@/hooks/use-api-access-url', () => ({ useDatasetApiAccessUrl: () => 'https://docs.dify.ai/api-reference/datasets', @@ -69,7 +74,9 @@ describe('API access card', () => { await user.click(screen.getByRole('switch')) - expect(mockEnableApi).toHaveBeenCalledWith('dataset-123') + expect(mockEnableApi).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-123', status: 'enable' }, + }) expect(mockMutateDatasetRes).toHaveBeenCalledOnce() }) @@ -80,7 +87,9 @@ describe('API access card', () => { await user.click(screen.getByRole('switch')) - expect(mockDisableApi).toHaveBeenCalledWith('dataset-123') + expect(mockDisableApi).toHaveBeenCalledWith({ + params: { dataset_id: 'dataset-123', status: 'disable' }, + }) expect(mockMutateDatasetRes).toHaveBeenCalledOnce() }) diff --git a/web/app/components/datasets/extra-info/api-access/card.tsx b/web/app/components/datasets/extra-info/api-access/card.tsx index f88f7f7cb9cd0b..2564e9e551249a 100644 --- a/web/app/components/datasets/extra-info/api-access/card.tsx +++ b/web/app/components/datasets/extra-info/api-access/card.tsx @@ -1,7 +1,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useCallback } from 'react' @@ -11,10 +11,7 @@ import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' import Link from '@/next/link' -import { - useDisableDatasetServiceApi, - useEnableDatasetServiceApi, -} from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' import { getDatasetACLCapabilities } from '@/utils/permission' type CardProps = { @@ -31,8 +28,9 @@ const Card = ({ apiEnabled }: CardProps) => { select: (data) => data.profile.id, }) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) - const { mutateAsync: enableDatasetServiceApi } = useEnableDatasetServiceApi() - const { mutateAsync: disableDatasetServiceApi } = useDisableDatasetServiceApi() + const { mutateAsync: updateDatasetServiceApiStatus } = useMutation( + consoleQuery.datasets.byDatasetId.apiKeys.byStatus.post.mutationOptions(), + ) const datasetACLCapabilities = React.useMemo( () => @@ -49,12 +47,12 @@ const Card = ({ apiEnabled }: CardProps) => { const onToggle = useCallback( async (state: boolean) => { - let result: 'success' | 'fail' - if (state) result = (await enableDatasetServiceApi(datasetId ?? '')).result - else result = (await disableDatasetServiceApi(datasetId ?? '')).result + const { result } = await updateDatasetServiceApiStatus({ + params: { dataset_id: datasetId ?? '', status: state ? 'enable' : 'disable' }, + }) if (result === 'success') mutateDatasetRes?.() }, - [datasetId, enableDatasetServiceApi, mutateDatasetRes, disableDatasetServiceApi], + [datasetId, updateDatasetServiceApiStatus, mutateDatasetRes], ) return ( diff --git a/web/app/components/datasets/hit-testing/__tests__/index.spec.tsx b/web/app/components/datasets/hit-testing/__tests__/index.spec.tsx index a4e1d82f81bbaf..6b4695b4eed7ad 100644 --- a/web/app/components/datasets/hit-testing/__tests__/index.spec.tsx +++ b/web/app/components/datasets/hit-testing/__tests__/index.spec.tsx @@ -9,6 +9,7 @@ import userEvent from '@testing-library/user-event' import { describe, expect, it, vi } from 'vite-plus/test' import { seedAccountProfileQuery } from '@/test/console/account-profile' import { render } from '@/test/console/render' +import { createHitTestingRecordFixture } from '@/test/fixtures/hit-testing' import { RETRIEVE_METHOD } from '@/types/app' import HitTestingPage from '../index' @@ -146,29 +147,12 @@ const mockRecordsRefetch = vi.fn() const mockHitTestingMutateAsync = vi.fn() const mockExternalHitTestingMutateAsync = vi.fn() -vi.mock('@/service/knowledge/use-dataset', () => ({ - useDatasetTestingRecords: vi.fn(() => ({ - data: { - data: [], - total: 0, - page: 1, - limit: 10, - has_more: false, - }, - refetch: mockRecordsRefetch, - isLoading: false, - })), -})) +const { mockRecordsQuery } = vi.hoisted(() => ({ mockRecordsQuery: vi.fn() })) -vi.mock('@/service/knowledge/use-hit-testing', () => ({ - useHitTesting: vi.fn(() => ({ - mutateAsync: mockHitTestingMutateAsync, - isLoading: false, - })), - useExternalKnowledgeBaseHitTesting: vi.fn(() => ({ - mutateAsync: mockExternalHitTestingMutateAsync, - isLoading: false, - })), +// The page owns one mutation per retrieval kind, told apart by mutation key. +const { mockHitTestingState, mockExternalHitTestingState } = vi.hoisted(() => ({ + mockHitTestingState: vi.fn(), + mockExternalHitTestingState: vi.fn(), })) // Mock breakpoints hook @@ -339,12 +323,8 @@ const createMockSegment = (overrides = {}) => ({ }) const createMockHitTesting = (overrides = {}): HitTesting => ({ + ...createHitTestingRecordFixture(), segment: createMockSegment() as HitTesting['segment'], - content: createMockSegment() as HitTesting['content'], - score: 0.85, - tsne_position: { x: 0.5, y: 0.5 }, - child_chunks: null, - files: [], ...overrides, }) @@ -393,24 +373,19 @@ describe('HitTestingPage', () => { userProfile: { id: 'user-1' }, workspacePermissionKeys: [], } - - const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetTestingRecords).mockReturnValue({ + mockRecordsQuery.mockReturnValue({ data: { data: [], total: 0, page: 1, limit: 10, has_more: false }, refetch: mockRecordsRefetch, isLoading: false, - } as unknown as ReturnType) - - const { useHitTesting, useExternalKnowledgeBaseHitTesting } = - await import('@/service/knowledge/use-hit-testing') - vi.mocked(useHitTesting).mockReturnValue({ + }) + vi.mocked(mockHitTestingState).mockReturnValue({ mutateAsync: mockHitTestingMutateAsync, isLoading: false, - } as unknown as ReturnType) - vi.mocked(useExternalKnowledgeBaseHitTesting).mockReturnValue({ + }) + vi.mocked(mockExternalHitTestingState).mockReturnValue({ mutateAsync: mockExternalHitTestingMutateAsync, isLoading: false, - } as unknown as ReturnType) + }) const useBreakpoints = await import('@/hooks/use-breakpoints') vi.mocked(useBreakpoints.default).mockReturnValue( @@ -419,26 +394,25 @@ describe('HitTestingPage', () => { }) it('disables record loading and query input without retrieval recall permission', async () => { - const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset') mockDataset.permission_keys = [] renderWithProviders() expect(screen.queryByRole('textbox')).not.toBeInTheDocument() - expect(vi.mocked(useDatasetTestingRecords)).toHaveBeenCalledWith( - 'dataset-1', - { limit: 10, page: 1 }, - { enabled: false }, + expect(mockRecordsQuery).toHaveBeenCalledWith( + expect.objectContaining({ + input: { params: { dataset_id: 'dataset-1' }, query: { limit: 10, page: 1 } }, + enabled: false, + }), ) }) it('shows the records loading state', async () => { - const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetTestingRecords).mockReturnValue({ + mockRecordsQuery.mockReturnValue({ data: undefined, refetch: mockRecordsRefetch, isLoading: true, - } as unknown as ReturnType) + }) renderWithProviders() @@ -458,19 +432,17 @@ describe('HitTestingPage', () => { 'announces loading and failure in the same status region for %s retrieval', async (provider) => { mockDataset.provider = provider - const { useHitTesting, useExternalKnowledgeBaseHitTesting } = - await import('@/service/knowledge/use-hit-testing') const setRetrievalState = (state: { isPending: boolean; isError: boolean }) => { if (provider === 'external') { - vi.mocked(useExternalKnowledgeBaseHitTesting).mockReturnValue({ + vi.mocked(mockExternalHitTestingState).mockReturnValue({ mutateAsync: mockExternalHitTestingMutateAsync, ...state, - } as unknown as ReturnType) + }) } else { - vi.mocked(useHitTesting).mockReturnValue({ + vi.mocked(mockHitTestingState).mockReturnValue({ mutateAsync: mockHitTestingMutateAsync, ...state, - } as unknown as ReturnType) + }) } } const { rerender } = renderWithProviders() @@ -495,9 +467,8 @@ describe('HitTestingPage', () => { const user = userEvent.setup() const { default: useBreakpoints } = await import('@/hooks/use-breakpoints') vi.mocked(useBreakpoints).mockReturnValue('mobile' as ReturnType) - const { useHitTesting } = await import('@/service/knowledge/use-hit-testing') const response: HitTestingResponse = { - query: { content: 'Test query', tsne_position: { x: 0, y: 0 } }, + query: { content: 'Test query' }, records: [createMockHitTesting()], } mockHitTestingMutateAsync.mockImplementationOnce(async (_params, options) => { @@ -509,21 +480,21 @@ describe('HitTestingPage', () => { await user.click(screen.getByRole('button', { name: /input.testing/ })) expect(await screen.findByRole('dialog', { name: '1 Retrieved Chunks' })).toBeInTheDocument() - vi.mocked(useHitTesting).mockReturnValue({ + vi.mocked(mockHitTestingState).mockReturnValue({ mutateAsync: mockHitTestingMutateAsync, isPending: true, isError: false, - } as unknown as ReturnType) + }) rerender() const pendingDialog = screen.getByRole('dialog', { name: 'datasetHitTesting.title' }) expect(within(pendingDialog).queryByText('1 Retrieved Chunks')).not.toBeInTheDocument() - vi.mocked(useHitTesting).mockReturnValue({ + vi.mocked(mockHitTestingState).mockReturnValue({ mutateAsync: mockHitTestingMutateAsync, isPending: false, isError: true, - } as unknown as ReturnType) + }) rerender() const failedDialog = screen.getByRole('dialog', { name: 'datasetHitTesting.title' }) @@ -533,11 +504,11 @@ describe('HitTestingPage', () => { await user.click(within(failedDialog).getByRole('button', { name: /operation.close/ })) await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) - vi.mocked(useHitTesting).mockReturnValue({ + vi.mocked(mockHitTestingState).mockReturnValue({ mutateAsync: mockHitTestingMutateAsync, isPending: false, isError: false, - } as unknown as ReturnType) + }) mockHitTestingMutateAsync.mockImplementationOnce(async (_params, options) => { const emptyResponse = { ...response, records: [] } options?.onSuccess?.(emptyResponse) @@ -551,7 +522,7 @@ describe('HitTestingPage', () => { it('announces zero retrieved chunks after an empty response', async () => { const user = userEvent.setup() const response: HitTestingResponse = { - query: { content: 'No match', tsne_position: { x: 0, y: 0 } }, + query: { content: 'No match' }, records: [], } mockHitTestingMutateAsync.mockImplementation(async (_params, options) => { @@ -568,8 +539,7 @@ describe('HitTestingPage', () => { it('loads a history record into the query input when selected', async () => { const user = userEvent.setup() - const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetTestingRecords).mockReturnValue({ + mockRecordsQuery.mockReturnValue({ data: { data: [ createMockRecord({ @@ -585,7 +555,7 @@ describe('HitTestingPage', () => { }, refetch: mockRecordsRefetch, isLoading: false, - } as unknown as ReturnType) + }) renderWithProviders() await user.click(screen.getByText('Record query text')) @@ -595,8 +565,7 @@ describe('HitTestingPage', () => { it('requests the next records page through pagination', async () => { const user = userEvent.setup() - const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetTestingRecords).mockReturnValue({ + mockRecordsQuery.mockReturnValue({ data: { data: Array.from({ length: 10 }, (_, index) => createMockRecord({ id: `record-${index}` })), total: 25, @@ -606,22 +575,23 @@ describe('HitTestingPage', () => { }, refetch: mockRecordsRefetch, isLoading: false, - } as unknown as ReturnType) + }) renderWithProviders() await user.click(screen.getByRole('button', { name: /pagination.next/ })) - expect(vi.mocked(useDatasetTestingRecords)).toHaveBeenLastCalledWith( - 'dataset-1', - { limit: 10, page: 2 }, - { enabled: true }, + expect(mockRecordsQuery).toHaveBeenLastCalledWith( + expect.objectContaining({ + input: { params: { dataset_id: 'dataset-1' }, query: { limit: 10, page: 2 } }, + enabled: true, + }), ) }) it('saves retrieval settings, submits a query, refreshes history, and renders results', async () => { const user = userEvent.setup() const response: HitTestingResponse = { - query: { content: 'Test query', tsne_position: { x: 0, y: 0 } }, + query: { content: 'Test query' }, records: [createMockHitTesting()], } mockHitTestingMutateAsync.mockImplementation(async (_params, options) => { @@ -709,14 +679,25 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal() return { ...actual, + useMutation: (options: { mutationKey: [string, string] }) => { + if (options.mutationKey[0] === 'hit-testing') + return mockHitTestingState() ?? { mutateAsync: mockHitTestingMutateAsync, isPending: false } + return ( + mockExternalHitTestingState() ?? { + mutateAsync: mockExternalHitTestingMutateAsync, + isPending: false, + } + ) + }, useQuery: (options: { queryKey: OperationKey< 'query', { params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] } > - }) => - options.queryKey[0].includes('modelTypes') - ? { data: [], isPending: false } - : actual.useQuery(options), + }) => { + if (options.queryKey[0].includes('modelTypes')) return { data: [], isPending: false } + if (options.queryKey[0].includes('queries')) return mockRecordsQuery(options) + return actual.useQuery(options) + }, } }) diff --git a/web/app/components/datasets/hit-testing/components/__tests__/chunk-detail-dialog.spec.tsx b/web/app/components/datasets/hit-testing/components/__tests__/chunk-detail-dialog.spec.tsx index 3d22bc2483aa1b..a3d5c35b06995a 100644 --- a/web/app/components/datasets/hit-testing/components/__tests__/chunk-detail-dialog.spec.tsx +++ b/web/app/components/datasets/hit-testing/components/__tests__/chunk-detail-dialog.spec.tsx @@ -2,41 +2,19 @@ import type { HitTesting } from '@/models/datasets' import { render, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, expect, it } from 'vite-plus/test' +import { createHitTestingRecordFixture } from '@/test/fixtures/hit-testing' import { ResultItem } from '../result-item' function makePayload( - overrides: Partial> & { + overrides: Partial> & { segment?: Partial } = {}, ): HitTesting { - const segment: HitTesting['segment'] = { - id: 'segment-1', - position: 1, - word_count: 100, - content: 'Original content', - sign_content: '', - keywords: [], - document: { - id: 'document-1', - name: 'file.pdf', - data_source_type: 'upload_file', - doc_type: 'book', - }, - answer: '', - tokens: 20, - hit_count: 1, - index_node_hash: 'hash-1', - ...overrides.segment, - } - return { + return createHitTestingRecordFixture({ score: 0.85, - tsne_position: { x: 0, y: 0 }, - child_chunks: [], - files: [], ...overrides, - segment, - content: segment, - } + segment: { content: 'Original content', ...overrides.segment }, + }) } async function openDetail(payload: HitTesting) { diff --git a/web/app/components/datasets/hit-testing/components/__tests__/result-dialogs.browser.spec.tsx b/web/app/components/datasets/hit-testing/components/__tests__/result-dialogs.browser.spec.tsx index 7e1e0bb3e102e2..54609ad7c49839 100644 --- a/web/app/components/datasets/hit-testing/components/__tests__/result-dialogs.browser.spec.tsx +++ b/web/app/components/datasets/hit-testing/components/__tests__/result-dialogs.browser.spec.tsx @@ -1,37 +1,22 @@ import type { ExternalKnowledgeBaseHitTesting, HitTesting } from '@/models/datasets' import { page, userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' +import { createHitTestingRecordFixture } from '@/test/fixtures/hit-testing' import { ResultItem } from '../result-item' import { ResultItemExternal } from '../result-item-external' function createResult(overrides: Partial = {}): HitTesting { - const segment: HitTesting['segment'] = { - id: 'segment-1', - document: { - id: 'document-1', - name: 'handbook.pdf', - data_source_type: 'upload_file', - doc_type: 'book', - }, - content: 'Retrieval preview content', - sign_content: '', - position: 1, - word_count: 25, - tokens: 5, - keywords: [], - hit_count: 1, - index_node_hash: 'hash', - answer: '', - } - return { - segment, - content: segment, + return createHitTestingRecordFixture({ score: 0.9, - tsne_position: { x: 0, y: 0 }, - child_chunks: [], - files: [], ...overrides, - } + segment: { + content: 'Retrieval preview content', + word_count: 25, + tokens: 5, + document: { name: 'handbook.pdf' }, + ...overrides.segment, + }, + }) } function createImageUrl() { @@ -128,7 +113,7 @@ it('keeps external details scrollable and Close reachable on a narrow viewport t expect(closeBounds.right).toBeLessThanOrEqual(window.innerWidth) expect(closeBounds.top).toBeGreaterThanOrEqual(0) expect(closeBounds.bottom).toBeLessThanOrEqual(window.innerHeight) - const body = dialog.getByText(payload.content) + const body = dialog.getByText(payload.content!) const bodyElement = body.element() expect(bodyElement.scrollWidth).toBeLessThanOrEqual(bodyElement.clientWidth + 1) expect(bodyElement.scrollHeight).toBeGreaterThan(bodyElement.clientHeight) diff --git a/web/app/components/datasets/hit-testing/components/__tests__/result-item-external.spec.tsx b/web/app/components/datasets/hit-testing/components/__tests__/result-item-external.spec.tsx index d27bda2b55452c..730a63ec2bd7fe 100644 --- a/web/app/components/datasets/hit-testing/components/__tests__/result-item-external.spec.tsx +++ b/web/app/components/datasets/hit-testing/components/__tests__/result-item-external.spec.tsx @@ -35,9 +35,9 @@ describe('ResultItemExternal', () => { expect(within(trigger).getByText('0.85')).toBeInTheDocument() expect(within(trigger).getByText('Test Document Title')).toBeInTheDocument() - await user.click(within(trigger).getByText(payload.content)) + await user.click(within(trigger).getByText(payload.content!)) const dialog = await screen.findByRole('dialog', { name: dialogName }) - expect(within(dialog).getByText(payload.content)).toBeInTheDocument() + expect(within(dialog).getByText(payload.content!)).toBeInTheDocument() expect(within(dialog).getByText('Chunk-03')).toBeInTheDocument() expect(within(dialog).getByText('0.85')).toBeInTheDocument() await user.click(within(dialog).getByRole('button', { name: 'common.operation.close' })) diff --git a/web/app/components/datasets/hit-testing/components/__tests__/result-item.spec.tsx b/web/app/components/datasets/hit-testing/components/__tests__/result-item.spec.tsx index 278523a5e31abf..b056d074c34109 100644 --- a/web/app/components/datasets/hit-testing/components/__tests__/result-item.spec.tsx +++ b/web/app/components/datasets/hit-testing/components/__tests__/result-item.spec.tsx @@ -2,41 +2,19 @@ import type { HitTesting } from '@/models/datasets' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, expect, it } from 'vite-plus/test' +import { createHitTestingRecordFixture } from '@/test/fixtures/hit-testing' import { ResultItem } from '../result-item' function makePayload( - overrides: Partial> & { + overrides: Partial> & { segment?: Partial } = {}, ): HitTesting { - const segment: HitTesting['segment'] = { - id: 'segment-1', - position: 1, - word_count: 100, - content: 'Preview content', - sign_content: '', - keywords: [], - document: { - id: 'document-1', - name: 'file.pdf', - data_source_type: 'upload_file', - doc_type: 'book', - }, - answer: '', - tokens: 20, - hit_count: 1, - index_node_hash: 'hash-1', - ...overrides.segment, - } - return { + return createHitTestingRecordFixture({ score: 0.95, - tsne_position: { x: 0, y: 0 }, - child_chunks: [], - files: [], ...overrides, - segment, - content: segment, - } + segment: { content: 'Preview content', ...overrides.segment }, + }) } const dialogName = 'datasetHitTesting.chunkDetail' diff --git a/web/app/components/datasets/hit-testing/components/result-item-external.tsx b/web/app/components/datasets/hit-testing/components/result-item-external.tsx index f3e0bb975f4df5..9f8b67d4047f31 100644 --- a/web/app/components/datasets/hit-testing/components/result-item-external.tsx +++ b/web/app/components/datasets/hit-testing/components/result-item-external.tsx @@ -22,7 +22,9 @@ type ResultItemExternalProps = { export const ResultItemExternal = memo(({ payload, positionId }: ResultItemExternalProps) => { const { t } = useTranslation(['common', 'datasetHitTesting']) const descriptionId = useId() - const { content, title, score } = payload + const content = payload.content ?? '' + const title = payload.title ?? '' + const score = payload.score ?? null return ( diff --git a/web/app/components/datasets/hit-testing/components/result-item-meta.tsx b/web/app/components/datasets/hit-testing/components/result-item-meta.tsx index a59117bb3e92d2..5320e12c88a264 100644 --- a/web/app/components/datasets/hit-testing/components/result-item-meta.tsx +++ b/web/app/components/datasets/hit-testing/components/result-item-meta.tsx @@ -9,7 +9,7 @@ type ResultItemMetaProps = { readonly labelPrefix: string readonly positionId: number readonly wordCount: number - readonly score: number + readonly score: number | null readonly className?: string } diff --git a/web/app/components/datasets/hit-testing/index.tsx b/web/app/components/datasets/hit-testing/index.tsx index 437d43fcb13dcb..88fe93b4f335b7 100644 --- a/web/app/components/datasets/hit-testing/index.tsx +++ b/web/app/components/datasets/hit-testing/index.tsx @@ -3,9 +3,11 @@ import type { FC } from 'react' import type { ExternalKnowledgeBaseHitTesting, + ExternalKnowledgeBaseHitTestingRequest, ExternalKnowledgeBaseHitTestingResponse, HitTesting, HitTestingRecord, + HitTestingRequest, HitTestingResponse, Query, } from '@/models/datasets' @@ -20,7 +22,7 @@ import { DrawerViewport, } from '@langgenius/dify-ui/drawer' import { Pagination } from '@langgenius/dify-ui/pagination' -import { useSuspenseQuery } from '@tanstack/react-query' +import { keepPreviousData, useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { useAtomValue } from 'jotai' import * as React from 'react' @@ -29,16 +31,13 @@ import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import FloatRightContainer from '@/app/components/base/float-right-container' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { toRetrievalConfig } from '@/app/components/datasets/dataset-fields' import docStyle from '@/app/components/datasets/documents/detail/completed/style.module.css' import DatasetDetailContext from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { useDatasetTestingRecords } from '@/service/knowledge/use-dataset' -import { - useExternalKnowledgeBaseHitTesting, - useHitTesting, -} from '@/service/knowledge/use-hit-testing' +import { consoleClient, consoleQuery } from '@/service/console' import { getDatasetACLCapabilities } from '@/utils/permission' import { CardSkelton } from '../documents/detail/completed/skeleton/general-list-skeleton' import EmptyRecords from './components/empty-records' @@ -83,10 +82,12 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { data: recordsRes, refetch: recordsRefetch, isLoading: isRecordsLoading, - } = useDatasetTestingRecords( - datasetId, - { limit, page: currPage + 1 }, - { enabled: canRunRetrievalRecall }, + } = useQuery( + consoleQuery.datasets.byDatasetId.queries.get.queryOptions({ + input: { params: { dataset_id: datasetId }, query: { limit, page: currPage + 1 } }, + enabled: !!datasetId && canRunRetrievalRecall, + placeholderData: keepPreviousData, + }), ) const total = recordsRes?.total || 0 @@ -94,8 +95,8 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { const isExternal = currentDataset?.provider === 'external' - const [retrievalConfig, setRetrievalConfig] = useState( - currentDataset?.retrieval_model_dict as RetrievalConfig, + const [retrievalConfig, setRetrievalConfig] = useState( + () => currentDataset && toRetrievalConfig(currentDataset.retrieval_model_dict), ) const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false) const [ @@ -107,12 +108,26 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { mutateAsync: hitTestingMutation, isPending: isHitTestingPending, isError: isHitTestingError, - } = useHitTesting(datasetId) + } = useMutation({ + mutationKey: ['hit-testing', datasetId], + mutationFn: (body: HitTestingRequest) => + consoleClient.datasets.byDatasetId.hitTesting.post({ + params: { dataset_id: datasetId }, + body, + }), + }) const { mutateAsync: externalKnowledgeBaseHitTestingMutation, isPending: isExternalKnowledgeBaseHitTestingPending, isError: isExternalKnowledgeBaseHitTestingError, - } = useExternalKnowledgeBaseHitTesting(datasetId) + } = useMutation({ + mutationKey: ['external-hit-testing', datasetId], + mutationFn: (body: ExternalKnowledgeBaseHitTestingRequest) => + consoleClient.datasets.byDatasetId.externalHitTesting.post({ + params: { dataset_id: datasetId }, + body, + }), + }) const isRetrievalLoading = isHitTestingPending || isExternalKnowledgeBaseHitTestingPending const result = isExternal ? externalHitResult : hitResult @@ -191,23 +206,25 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { {t(($) => $.desc, { ns: 'datasetHitTesting' })}

- setIsShowModifyRetrievalModal(true)} - retrievalConfig={retrievalConfig} - isEconomy={currentDataset?.indexing_technique === 'economy'} - hitTestingMutation={hitTestingMutation} - externalKnowledgeBaseHitTestingMutation={externalKnowledgeBaseHitTestingMutation} - canRunRetrievalRecall={canRunRetrievalRecall} - /> + {retrievalConfig && ( + setIsShowModifyRetrievalModal(true)} + retrievalConfig={retrievalConfig} + isEconomy={currentDataset?.indexing_technique === 'economy'} + hitTestingMutation={hitTestingMutation} + externalKnowledgeBaseHitTestingMutation={externalKnowledgeBaseHitTestingMutation} + canRunRetrievalRecall={canRunRetrievalRecall} + /> + )}

{t(($) => $.records, { ns: 'datasetHitTesting' })}

@@ -279,16 +296,18 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { - setIsShowModifyRetrievalModal(false)} - onSave={(value) => { - setRetrievalConfig(value) - setIsShowModifyRetrievalModal(false) - }} - /> + {retrievalConfig && ( + setIsShowModifyRetrievalModal(false)} + onSave={(value) => { + setRetrievalConfig(value) + setIsShowModifyRetrievalModal(false) + }} + /> + )} diff --git a/web/app/components/datasets/list/__tests__/datasets.spec.tsx b/web/app/components/datasets/list/__tests__/datasets.spec.tsx index 8e282b8ecc6240..d60b2a86e538ee 100644 --- a/web/app/components/datasets/list/__tests__/datasets.spec.tsx +++ b/web/app/components/datasets/list/__tests__/datasets.spec.tsx @@ -1,11 +1,12 @@ +import type { InfiniteData } from '@tanstack/react-query' import type { DataSet, DataSetListResponse } from '@/models/datasets' -import type { useDatasetList } from '@/service/knowledge/use-dataset' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { IndexingType } from '@/app/components/datasets/create/step-two' import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' -import { RETRIEVE_METHOD } from '@/types/app' +import { renderWithConsoleQuery } from '@/test/console/query-data' +import { createDatasetFixture } from '@/test/fixtures/dataset' import Datasets from '../datasets' vi.mock('@/next/navigation', () => ({ @@ -24,30 +25,9 @@ vi.mock('@/hooks/use-knowledge', () => ({ }), })) +type DatasetListItem = DataSetListResponse['data'][number] + const mockFetchNextPage = vi.fn() -const mockInvalidDatasetList = vi.fn() - -vi.mock('@/service/knowledge/use-dataset', () => ({ - useDatasetList: vi.fn(() => ({ - data: { - pages: [ - { - data: [ - createMockDataset({ id: 'dataset-1', name: 'Dataset 1' }), - createMockDataset({ id: 'dataset-2', name: 'Dataset 2' }), - ], - }, - ], - }, - fetchNextPage: mockFetchNextPage, - hasNextPage: false, - isFetching: false, - isFetchingNextPage: false, - isLoading: false, - isPlaceholderData: false, - })), - useInvalidDatasetList: () => mockInvalidDatasetList, -})) vi.mock('../dataset-card/hooks/use-dataset-card-state', () => ({ useDatasetCardState: () => ({ @@ -94,8 +74,9 @@ vi.mock('../dataset-card', () => ({ ), })) -function createMockDataset(overrides: Partial = {}): DataSet { +function createMockDataset(overrides: Partial = {}): DatasetListItem { return { + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', @@ -123,12 +104,9 @@ function createMockDataset(overrides: Partial = {}): DataSet { icon_background: '#FFF4ED', icon_url: '', }, - retrieval_model_dict: { - search_method: RETRIEVE_METHOD.semantic, - }, author_name: 'Test User', ...overrides, - } as DataSet + } } let intersectionObserverCallback: IntersectionObserverCallback | null = null @@ -170,7 +148,7 @@ describe('Datasets', () => { ...page, })), pageParams: pages.map((_, index) => index + 1), - }) as unknown as ReturnType['data'] + }) satisfies InfiniteData const defaultProps = { datasetList: createDatasetListData(), @@ -194,19 +172,19 @@ describe('Datasets', () => { describe('Rendering', () => { it('should not render NewDatasetCard in dataset list', () => { - render() + renderWithConsoleQuery() expect(screen.queryByText(/createDataset/)).not.toBeInTheDocument() }) it('should render dataset cards from data', () => { - render() + renderWithConsoleQuery() expect(screen.getByText('Dataset 1')).toBeInTheDocument() expect(screen.getByText('Dataset 2')).toBeInTheDocument() }) it('should pass step-by-step tour targets to the first dataset card only', () => { - render( + renderWithConsoleQuery( { }) it('should render empty element when there are no datasets', () => { - render( + renderWithConsoleQuery( { }) it('should render anchor div for infinite scroll', () => { - render() + renderWithConsoleQuery() const anchor = document.querySelector('.h-0') expect(anchor).toBeInTheDocument() }) @@ -250,7 +228,7 @@ describe('Datasets', () => { describe('Loading States', () => { it('should show dataset card skeletons while initial dataset list is loading', () => { - render( + renderWithConsoleQuery( , ) @@ -259,7 +237,7 @@ describe('Datasets', () => { }) it('should not show dataset card skeletons after an empty dataset list has loaded', () => { - render( + renderWithConsoleQuery( { }) it('should show dataset card skeletons when placeholder data is empty and the next query is fetching', () => { - render( + renderWithConsoleQuery( { }) it('should keep rendered dataset cards when placeholder data has results during refetch', () => { - render( + renderWithConsoleQuery( { it.each([null, undefined, createDatasetListData([])])( 'shows the empty state when dataset data is unavailable or empty (%j)', (datasetList) => { - render( + renderWithConsoleQuery( { describe('IntersectionObserver', () => { it('should setup IntersectionObserver on mount', () => { - render() + renderWithConsoleQuery() expect(mockObserve).toHaveBeenCalled() }) it('should call fetchNextPage when isIntersecting, hasNextPage, and not isFetching', () => { - render() + renderWithConsoleQuery() if (intersectionObserverCallback) { intersectionObserverCallback( @@ -338,7 +316,7 @@ describe('Datasets', () => { }) it('should NOT call fetchNextPage when isIntersecting is false', () => { - render() + renderWithConsoleQuery() if (intersectionObserverCallback) { intersectionObserverCallback( @@ -351,7 +329,7 @@ describe('Datasets', () => { }) it('should NOT call fetchNextPage when hasNextPage is false', () => { - render() + renderWithConsoleQuery() if (intersectionObserverCallback) { intersectionObserverCallback( @@ -364,7 +342,7 @@ describe('Datasets', () => { }) it('should NOT call fetchNextPage when isFetching is true', () => { - render() + renderWithConsoleQuery() if (intersectionObserverCallback) { intersectionObserverCallback( @@ -377,7 +355,9 @@ describe('Datasets', () => { }) it('should NOT call fetchNextPage when placeholder data is showing', () => { - render() + renderWithConsoleQuery( + , + ) if (intersectionObserverCallback) { intersectionObserverCallback( @@ -390,7 +370,7 @@ describe('Datasets', () => { }) it('should disconnect observer on unmount', () => { - const { unmount } = render() + const { unmount } = renderWithConsoleQuery() unmount() @@ -400,7 +380,7 @@ describe('Datasets', () => { describe('Landmarks', () => { it('keeps knowledge cards in the page content without creating another navigation landmark', () => { - render( + renderWithConsoleQuery( { describe('Edge Cases', () => { it('should handle multiple pages of data', () => { - render( + renderWithConsoleQuery( + renderWithConsoleState(ui, { wrapper: createQueryClientWrapper(createTestQueryClient()) }) + const mockPush = vi.fn() const mockReplace = vi.fn() let mockConsoleState = { @@ -51,20 +56,28 @@ vi.mock('@/hooks/use-knowledge', () => ({ }), })) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useDatasetList: vi.fn(() => ({ +const { mockDatasetListQuery } = vi.hoisted(() => ({ + mockDatasetListQuery: vi.fn((_query: { include_all?: boolean; keyword?: string }) => ({ data: { pages: [{ data: [], total: 1 }] }, fetchNextPage: vi.fn(), hasNextPage: false, isFetching: false, isFetchingNextPage: false, })), - useInvalidDatasetList: () => vi.fn(), - useDatasetApiBaseUrl: () => ({ - data: { api_base_url: 'https://api.example.com' }, - }), })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + // The page reads the list's first request. + 'datasets.get': (options: { input: (page: number) => { query: object } }) => + mockDatasetListQuery(options.input(1).query), + 'datasets.apiBaseInfo.get': () => ({ + data: { api_base_url: 'https://api.example.com' }, + }), + }) +}) + // Mock Datasets component vi.mock('../datasets', () => ({ default: ({ @@ -159,14 +172,13 @@ describe('List', () => { isCurrentWorkspaceOwner: true, workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'], } - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetList).mockReturnValue({ + mockDatasetListQuery.mockReturnValue({ data: { pages: [{ data: [], total: 1 }] }, fetchNextPage: vi.fn(), hasNextPage: false, isFetching: false, isFetchingNextPage: false, - } as unknown as ReturnType) + }) }) describe('Rendering', () => { @@ -201,11 +213,9 @@ describe('List', () => { describe('Props', () => { it('should query datasets with includeAll disabled initially', async () => { - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - render() - expect(useDatasetList).toHaveBeenCalledWith( + expect(mockDatasetListQuery).toHaveBeenCalledWith( expect.objectContaining({ include_all: false, }), @@ -213,11 +223,9 @@ describe('List', () => { }) it('should query datasets with empty keywords initially', async () => { - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - render() - expect(useDatasetList).toHaveBeenCalledWith( + expect(mockDatasetListQuery).toHaveBeenCalledWith( expect.objectContaining({ keyword: '', }), @@ -225,11 +233,9 @@ describe('List', () => { }) it('should query datasets with empty tags initially', async () => { - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - render() - expect(useDatasetList).toHaveBeenCalledWith( + expect(mockDatasetListQuery).toHaveBeenCalledWith( expect.objectContaining({ tag_ids: [], }), @@ -281,14 +287,13 @@ describe('List', () => { }) it('should render first empty state when there are no datasets and no active filters', async () => { - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetList).mockReturnValue({ + mockDatasetListQuery.mockReturnValue({ data: { pages: [{ data: [], total: 0 }] }, fetchNextPage: vi.fn(), hasNextPage: false, isFetching: false, isFetchingNextPage: false, - } as unknown as ReturnType) + }) render() @@ -302,14 +307,13 @@ describe('List', () => { isCurrentWorkspaceOwner: true, workspacePermissionKeys: ['dataset.create_and_management'], } - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetList).mockReturnValue({ + mockDatasetListQuery.mockReturnValue({ data: { pages: [{ data: [], total: 0 }] }, fetchNextPage: vi.fn(), hasNextPage: false, isFetching: false, isFetchingNextPage: false, - } as unknown as ReturnType) + }) render() @@ -325,14 +329,13 @@ describe('List', () => { isCurrentWorkspaceOwner: true, workspacePermissionKeys: [], } - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetList).mockReturnValue({ + mockDatasetListQuery.mockReturnValue({ data: { pages: [{ data: [], total: 0 }] }, fetchNextPage: vi.fn(), hasNextPage: false, isFetching: false, isFetchingNextPage: false, - } as unknown as ReturnType) + }) render() @@ -342,14 +345,13 @@ describe('List', () => { }) it('should not render first empty state before the first dataset page resolves', async () => { - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetList).mockReturnValue({ + mockDatasetListQuery.mockReturnValue({ data: { pages: [] }, fetchNextPage: vi.fn(), hasNextPage: false, isFetching: false, isFetchingNextPage: false, - } as unknown as ReturnType) + }) render() @@ -358,17 +360,13 @@ describe('List', () => { }) it('should keep the regular list for empty filtered results', async () => { - const { useDatasetList } = await import('@/service/knowledge/use-dataset') - vi.mocked(useDatasetList).mockImplementation( - (params) => - ({ - data: { pages: [{ data: [], total: params.include_all ? 0 : 1 }] }, - fetchNextPage: vi.fn(), - hasNextPage: false, - isFetching: false, - isFetchingNextPage: false, - }) as unknown as ReturnType, - ) + mockDatasetListQuery.mockImplementation((params) => ({ + data: { pages: [{ data: [], total: params.include_all ? 0 : 1 }] }, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetching: false, + isFetchingNextPage: false, + })) render() @@ -381,7 +379,6 @@ describe('List', () => { it('announces empty search results only after the current filters finish loading', async () => { const user = userEvent.setup() - const { useDatasetList } = await import('@/service/knowledge/use-dataset') const result = { data: { pages: [{ data: [], total: 1 }] }, fetchNextPage: vi.fn(), @@ -391,9 +388,7 @@ describe('List', () => { isPlaceholderData: false, isError: false, } - vi.mocked(useDatasetList).mockImplementation( - () => result as unknown as ReturnType, - ) + mockDatasetListQuery.mockImplementation(() => result) const { rerender } = render() const status = screen.getByRole('status') const search = screen.getByRole('searchbox') @@ -402,7 +397,7 @@ describe('List', () => { await user.type(search, 'missing') expect(status).toBeEmptyDOMElement() await waitFor(() => - expect(useDatasetList).toHaveBeenLastCalledWith( + expect(mockDatasetListQuery).toHaveBeenLastCalledWith( expect.objectContaining({ keyword: 'missing' }), ), ) diff --git a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx index 83d1e32ce43331..1ac43e4522097e 100644 --- a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx @@ -243,14 +243,14 @@ describe('DatasetCard Integration', () => { it('should apply opacity-30 when embedding_available is false', () => { const dataset = createMockDataset({ embedding_available: false }) render() - const descDiv = screen.getByTitle(dataset.description) + const descDiv = screen.getByTitle(dataset.description ?? '') expect(descDiv).toHaveClass('opacity-30') }) it('should not apply opacity-30 when embedding_available is true', () => { const dataset = createMockDataset({ embedding_available: true }) render() - const descDiv = screen.getByTitle(dataset.description) + const descDiv = screen.getByTitle(dataset.description ?? '') expect(descDiv).not.toHaveClass('opacity-30') }) }) @@ -388,9 +388,7 @@ describe('DatasetCard Component', () => { const dataset = createMockDataset({ name: 'Preview Only Dataset', permission_keys: [DatasetACLPermission.Preview], - tags: [ - { id: 'tag-preview', name: 'Readonly Tag', type: 'knowledge' as const, binding_count: '' }, - ], + tags: [{ id: 'tag-preview', name: 'Readonly Tag', type: 'knowledge' as const }], }) render() diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-header.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-header.spec.tsx index 4cc4714082dcf0..1636b64759407a 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-header.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-header.spec.tsx @@ -2,6 +2,7 @@ import type { DataSet } from '@/models/datasets' import { render, screen } from '@testing-library/react' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import DatasetCardHeader from '../dataset-card-header' @@ -23,10 +24,10 @@ vi.mock('react-i18next', async () => { }) const createDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', - indexing_status: 'completed', provider: 'vendor', permission: DatasetPermission.allTeamMembers, data_source_type: DataSourceType.FILE, @@ -34,7 +35,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ embedding_available: true, app_count: 5, document_count: 10, - total_document_count: 10, word_count: 1000, updated_at: 1609545600, updated_by: 'user-1', @@ -57,9 +57,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ retrieval_model_dict: { search_method: RETRIEVE_METHOD.semantic, } as DataSet['retrieval_model_dict'], - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - } as DataSet['retrieval_model'], external_knowledge_info: { external_knowledge_id: '', external_knowledge_api_id: '', diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-modals.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-modals.spec.tsx index ae783f03a78242..cb8eac0ed1ff54 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-modals.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-modals.spec.tsx @@ -3,6 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vite-plus/test' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import DatasetCardModals from '../dataset-card-modals' // Mock RenameDatasetModal since it's from a different feature folder @@ -26,10 +27,10 @@ vi.mock('../../../../rename-modal', () => ({ describe('DatasetCardModals', () => { const mockDataset: DataSet = { + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', - indexing_status: 'completed', provider: 'vendor', permission: DatasetPermission.allTeamMembers, data_source_type: DataSourceType.FILE, @@ -37,7 +38,6 @@ describe('DatasetCardModals', () => { embedding_available: true, app_count: 5, document_count: 10, - total_document_count: 10, word_count: 1000, updated_at: 1609545600, updated_by: 'user-1', @@ -57,7 +57,6 @@ describe('DatasetCardModals', () => { icon_url: '', }, retrieval_model_dict: {} as DataSet['retrieval_model_dict'], - retrieval_model: {} as DataSet['retrieval_model'], external_knowledge_info: { external_knowledge_id: '', external_knowledge_api_id: '', diff --git a/web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx b/web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx index 1ac1a0fad38cda..1c1230c75a4934 100644 --- a/web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx +++ b/web/app/components/datasets/list/dataset-card/components/dataset-card-header.tsx @@ -4,6 +4,7 @@ import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' +import { toChunkingMode } from '@/app/components/datasets/dataset-fields' import { useKnowledge } from '@/hooks/use-knowledge' import { DOC_FORM_ICON_CLASS_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets' @@ -41,12 +42,11 @@ const DocModeInfo = ({ dataset, isExternalProvider, isShowDocModeInfo }: DocMode const indexingText = dataset.indexing_technique ? formatIndexingTechniqueAndMethod( - dataset.indexing_technique as 'economy' | 'high_quality', - dataset.retrieval_model_dict?.search_method as Parameters< - typeof formatIndexingTechniqueAndMethod - >[1], + dataset.indexing_technique, + dataset.retrieval_model_dict.search_method, ) : '' + const chunkingMode = toChunkingMode(dataset.doc_form) return (
@@ -55,12 +55,12 @@ const DocModeInfo = ({ dataset, isExternalProvider, isShowDocModeInfo }: DocMode {t(($) => $['cornerLabel.pipeline'], { ns: 'dataset' })} )} - {isShowDocModeInfo && !!dataset.doc_form && ( + {isShowDocModeInfo && !!chunkingMode && ( $[`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`], { ns: 'dataset' })} + title={t(($) => $[`chunkingMode.${DOC_FORM_TEXT[chunkingMode]}`], { ns: 'dataset' })} > - {t(($) => $[`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`], { ns: 'dataset' })} + {t(($) => $[`chunkingMode.${DOC_FORM_TEXT[chunkingMode]}`], { ns: 'dataset' })} )} {isShowDocModeInfo && dataset.indexing_technique && indexingText && ( @@ -93,9 +93,8 @@ const DatasetCardHeader = ({ dataset, nameId }: DatasetCardHeaderProps) => { (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published), ) - const chunkingModeIcon = dataset.doc_form - ? DOC_FORM_ICON_CLASS_WITH_BG[dataset.doc_form] - : React.Fragment + const chunkingMode = toChunkingMode(dataset.doc_form) + const chunkingModeIcon = chunkingMode ? DOC_FORM_ICON_CLASS_WITH_BG[chunkingMode] : React.Fragment const iconClassName = isExternalProvider ? DOC_FORM_ICON_CLASS_WITH_BG.external : chunkingModeIcon const iconInfo = useMemo( diff --git a/web/app/components/datasets/list/dataset-card/components/description.tsx b/web/app/components/datasets/list/dataset-card/components/description.tsx index c278c346429822..3980a305f96bdd 100644 --- a/web/app/components/datasets/list/dataset-card/components/description.tsx +++ b/web/app/components/datasets/list/dataset-card/components/description.tsx @@ -12,7 +12,7 @@ const Description = ({ dataset }: DescriptionProps) => ( 'line-clamp-2 h-10 px-4 py-1 system-xs-regular text-text-tertiary', !dataset.embedding_available && 'opacity-30', )} - title={dataset.description} + title={dataset.description ?? undefined} > {dataset.description}
diff --git a/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts b/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts index 78f58ea34070d7..b882c73a89a08d 100644 --- a/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts +++ b/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts @@ -19,9 +19,12 @@ vi.mock('@/service/base', () => ({ sseGeneratorPost: vi.fn(), })) -vi.mock('@/service/use-pipeline', () => ({ - useExportPipelineDSL: () => ({ mutateAsync: vi.fn() }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.exports.get': () => ({ mutateAsync: vi.fn() }), + }) +}) const dataset = { id: 'dataset-1', diff --git a/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts b/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts index 979891f50fdec3..6e0dfe3eba3478 100644 --- a/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts +++ b/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts @@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/console' -import { useExportPipelineDSL } from '@/service/use-pipeline' import { downloadBlob } from '@/utils/download' type ModalState = { @@ -61,7 +60,9 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO const { mutateAsync: deleteDatasetMutation } = useMutation( consoleQuery.datasets.byDatasetId.delete.mutationOptions(), ) - const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL() + const { mutateAsync: exportPipelineConfig } = useMutation( + consoleQuery.rag.pipelines.byPipelineId.exports.get.mutationOptions(), + ) // Export pipeline handler const handleExportPipeline = useCallback( @@ -72,8 +73,8 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO try { setExporting(true) const { data } = await exportPipelineConfig({ - pipelineId: pipeline_id, - include, + params: { pipeline_id }, + query: { include_secret: String(include) }, }) const file = new Blob([data], { type: 'application/yaml' }) downloadBlob({ data: file, fileName: `${name}.pipeline` }) diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index 4cb8787499ba04..778d59eb2d9e88 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -132,7 +132,7 @@ const DatasetCard = ({ )} > + type Props = Readonly<{ - datasetList: ReturnType['data'] | null - fetchNextPage: ReturnType['fetchNextPage'] - hasNextPage: ReturnType['hasNextPage'] - isFetching: ReturnType['isFetching'] - isFetchingNextPage: ReturnType['isFetchingNextPage'] - isLoading: ReturnType['isLoading'] - isPlaceholderData: ReturnType['isPlaceholderData'] + datasetList: DatasetListQuery['data'] | null + fetchNextPage: DatasetListQuery['fetchNextPage'] + hasNextPage: DatasetListQuery['hasNextPage'] + isFetching: DatasetListQuery['isFetching'] + isFetchingNextPage: DatasetListQuery['isFetchingNextPage'] + isLoading: DatasetListQuery['isLoading'] + isPlaceholderData: DatasetListQuery['isPlaceholderData'] emptyElement?: ReactNode onOpenTagManagement?: () => void stepByStepTourActionMenuHighlightPart?: string @@ -39,7 +43,10 @@ const Datasets = ({ stepByStepTourCardTarget, }: Props) => { const { t } = useTranslation(['common']) - const invalidDatasetList = useInvalidDatasetList() + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) const anchorRef = useRef(null) const observerRef = useRef(null) const pages = datasetList?.pages ?? [] diff --git a/web/app/components/datasets/list/index.tsx b/web/app/components/datasets/list/index.tsx index 2ed69a90d75dc1..e8e33598e49652 100644 --- a/web/app/components/datasets/list/index.tsx +++ b/web/app/components/datasets/list/index.tsx @@ -1,10 +1,11 @@ 'use client' +import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query' // Libraries import { useBoolean } from 'ahooks' import { useDebouncedValue } from 'foxact/use-debounced-value' import { useAtomValue, useSetAtom } from 'jotai' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { activeStepByStepTourGuideGroupAtom, @@ -21,11 +22,7 @@ import { isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state' import { TagManagementModal } from '@/features/tag-management/components/tag-management-modal' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter } from '@/next/navigation' -import { - useDatasetApiBaseUrl, - useDatasetList, - useInvalidDatasetList, -} from '@/service/knowledge/use-dataset' +import { consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' // Components import FilterEmptyState from '../../base/filter-empty-state' @@ -41,7 +38,10 @@ function List() { const [showTagManagementModal, setShowTagManagementModal] = useState(false) const [showExternalApiPanel, setShowExternalApiPanel] = useState(false) const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false) - const invalidDatasetList = useInvalidDatasetList() + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) useDocumentTitle(t(($) => $.knowledge, { ns: 'dataset' })) const [keywords, setKeywords] = useState('') @@ -57,14 +57,23 @@ function List() { workspacePermissionKeys, 'dataset.external.connect', ) - const { data: apiBaseInfo } = useDatasetApiBaseUrl() - const datasetListQuery = useDatasetList({ - initialPage: 1, - tag_ids: tagIDs, - limit: 30, - include_all: includeAll, - keyword: searchKeywords, - }) + const { data: apiBaseInfo } = useQuery(consoleQuery.datasets.apiBaseInfo.get.queryOptions()) + const datasetListQuery = useInfiniteQuery( + consoleQuery.datasets.get.infiniteOptions({ + input: (page: number) => ({ + query: { + tag_ids: tagIDs, + limit: 30, + include_all: includeAll, + keyword: searchKeywords, + page, + }, + }), + getNextPageParam: (lastPage) => (lastPage.has_more ? lastPage.page + 1 : undefined), + initialPageParam: 1, + placeholderData: keepPreviousData, + }), + ) const pages = datasetListQuery.data?.pages ?? [] const hasResolvedFirstPage = pages.length > 0 const hasAnyDataset = (pages[0]?.total ?? 0) > 0 diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx index b848e9e69c54d6..ec5f3fb5cc8c87 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx @@ -7,19 +7,22 @@ import EditMetadataBatchModal from '../modal' // Mock service/API calls const mockDoAddMetaData = vi.fn().mockResolvedValue({}) -vi.mock('@/service/knowledge/use-metadata', () => ({ - useCreateMetaData: () => ({ - mutate: mockDoAddMetaData, - }), - useDatasetMetaData: () => ({ - data: { - doc_metadata: [ - { id: 'existing-1', name: 'existing_field', type: DataType.string }, - { id: 'existing-2', name: 'another_field', type: DataType.number }, - ], - }, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.metadata.post': () => ({ + mutateAsync: mockDoAddMetaData, + }), + 'datasets.byDatasetId.metadata.get': () => ({ + data: { + doc_metadata: [ + { id: 'existing-1', name: 'existing_field', type: DataType.string }, + { id: 'existing-2', name: 'another_field', type: DataType.number }, + ], + }, + }), + }) +}) // Mock check name hook to control validation let mockCheckNameResult = { errorMsg: '' } diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx index b1151d4b045788..6ddb341938ceae 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx @@ -7,12 +7,13 @@ import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dif import { IconButton } from '@langgenius/dify-ui/icon-button' import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip' import { Separator } from '@langgenius/dify-ui/separator' +import { useMutation } from '@tanstack/react-query' import { produce } from 'immer' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' -import { useCreateMetaData } from '@/service/knowledge/use-metadata' +import { consoleQuery } from '@/service/console' import useCheckMetadataName from '../hooks/use-check-metadata-name' import { DatasetMetadataPicker } from '../metadata-dataset/dataset-metadata-picker' import { UpdateType } from '../types' @@ -86,7 +87,9 @@ const EditMetadataBatchModal: FC = ({ [list, templeList], ) const { checkName } = useCheckMetadataName() - const { mutate: doAddMetaData } = useCreateMetaData(datasetId) + const { mutateAsync: createMetaData } = useMutation( + consoleQuery.datasets.byDatasetId.metadata.post.mutationOptions(), + ) const handleAddMetaData = useCallback( async (payload: BuiltInMetadataItem) => { const errorMsg = checkName(payload.name).errorMsg @@ -94,10 +97,10 @@ const EditMetadataBatchModal: FC = ({ toast.error(errorMsg) return Promise.reject(new Error(errorMsg)) } - await doAddMetaData(payload) + await createMetaData({ params: { dataset_id: datasetId }, body: payload }) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) }, - [checkName, doAddMetaData, t], + [checkName, createMetaData, datasetId, t], ) const [addedList, setAddedList] = useState([]) const handleAddedListChange = useCallback( diff --git a/web/app/components/datasets/metadata/hooks/__tests__/use-batch-edit-document-metadata.spec.ts b/web/app/components/datasets/metadata/hooks/__tests__/use-batch-edit-document-metadata.spec.ts index 540d7a5c68ff56..634bd60798a177 100644 --- a/web/app/components/datasets/metadata/hooks/__tests__/use-batch-edit-document-metadata.spec.ts +++ b/web/app/components/datasets/metadata/hooks/__tests__/use-batch-edit-document-metadata.spec.ts @@ -28,11 +28,14 @@ type MetadataItemWithEdit = { // Mock useBatchUpdateDocMetadata const mockMutateAsync = vi.fn().mockResolvedValue({}) -vi.mock('@/service/knowledge/use-metadata', () => ({ - useBatchUpdateDocMetadata: () => ({ - mutateAsync: mockMutateAsync, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.metadata.post': () => ({ + mutateAsync: mockMutateAsync, + }), + }) +}) vi.mock('@/app/notifications', () => ({ toast: { @@ -315,17 +318,19 @@ describe('useBatchEditDocumentMetadata', () => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - metadata_list: expect.arrayContaining([ - expect.objectContaining({ - document_id: 'doc-1', - metadata_list: expect.arrayContaining([ - expect.objectContaining({ - id: '1', - value: 'New Value', - }), - ]), - }), - ]), + body: expect.objectContaining({ + operation_data: expect.arrayContaining([ + expect.objectContaining({ + document_id: 'doc-1', + metadata_list: expect.arrayContaining([ + expect.objectContaining({ + id: '1', + value: 'New Value', + }), + ]), + }), + ]), + }), }), ) }) @@ -400,15 +405,17 @@ describe('useBatchEditDocumentMetadata', () => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - metadata_list: expect.arrayContaining([ - expect.objectContaining({ - metadata_list: expect.arrayContaining([ - expect.objectContaining({ - name: 'new_field', - }), - ]), - }), - ]), + body: expect.objectContaining({ + operation_data: expect.arrayContaining([ + expect.objectContaining({ + metadata_list: expect.arrayContaining([ + expect.objectContaining({ + name: 'new_field', + }), + ]), + }), + ]), + }), }), ) }) @@ -453,7 +460,7 @@ describe('useBatchEditDocumentMetadata', () => { // Both documents should have the field after applying to all expect(mockMutateAsync).toHaveBeenCalled() const callArgs = mockMutateAsync.mock.calls[0]![0] - expect(callArgs.metadata_list.length).toBe(2) + expect(callArgs.body.operation_data.length).toBe(2) }) it('should not add missing metadata for multiple value items when isApplyToAllSelectDocument is true', async () => { @@ -545,14 +552,16 @@ describe('useBatchEditDocumentMetadata', () => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - metadata_list: expect.arrayContaining([ - expect.objectContaining({ - metadata_list: expect.arrayContaining([ - expect.objectContaining({ id: '1', value: 'New Value 1' }), - expect.objectContaining({ id: '2', value: 200 }), - ]), - }), - ]), + body: expect.objectContaining({ + operation_data: expect.arrayContaining([ + expect.objectContaining({ + metadata_list: expect.arrayContaining([ + expect.objectContaining({ id: '1', value: 'New Value 1' }), + expect.objectContaining({ id: '2', value: 200 }), + ]), + }), + ]), + }), }), ) }) @@ -574,12 +583,14 @@ describe('useBatchEditDocumentMetadata', () => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - dataset_id: 'ds-1', - metadata_list: expect.arrayContaining([ - expect.objectContaining({ - document_id: 'doc-1', - }), - ]), + params: { dataset_id: 'ds-1' }, + body: expect.objectContaining({ + operation_data: expect.arrayContaining([ + expect.objectContaining({ + document_id: 'doc-1', + }), + ]), + }), }), ) }) @@ -600,12 +611,14 @@ describe('useBatchEditDocumentMetadata', () => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - metadata_list: expect.arrayContaining([ - expect.objectContaining({ - document_id: 'doc-not-in-list', - partial_update: true, - }), - ]), + body: expect.objectContaining({ + operation_data: expect.arrayContaining([ + expect.objectContaining({ + document_id: 'doc-not-in-list', + partial_update: true, + }), + ]), + }), }), ) }) @@ -646,7 +659,7 @@ describe('useBatchEditDocumentMetadata', () => { }) const callArgs = mockMutateAsync.mock.calls[0]![0] - const sentItem = callArgs.metadata_list[0].metadata_list[0] + const sentItem = callArgs.body.operation_data[0].metadata_list[0] // Only id, name, type, value should be present expect(Object.keys(sentItem).sort()).toEqual(['id', 'name', 'type', 'value'].sort()) @@ -688,7 +701,7 @@ describe('useBatchEditDocumentMetadata', () => { }) const callArgs = mockMutateAsync.mock.calls[0]![0] - const sentItem = callArgs.metadata_list[0].metadata_list[0] + const sentItem = callArgs.body.operation_data[0].metadata_list[0] // value should be null, not undefined expect(sentItem.value).toBeNull() diff --git a/web/app/components/datasets/metadata/hooks/__tests__/use-edit-dataset-metadata.spec.ts b/web/app/components/datasets/metadata/hooks/__tests__/use-edit-dataset-metadata.spec.ts index dea5dc81291bfa..77150858d2d455 100644 --- a/web/app/components/datasets/metadata/hooks/__tests__/use-edit-dataset-metadata.spec.ts +++ b/web/app/components/datasets/metadata/hooks/__tests__/use-edit-dataset-metadata.spec.ts @@ -8,37 +8,40 @@ const mockDoRenameMetaData = vi.fn().mockResolvedValue({}) const mockDoDeleteMetaData = vi.fn().mockResolvedValue({}) const mockToggleBuiltInStatus = vi.fn().mockResolvedValue({}) -vi.mock('@/service/knowledge/use-metadata', () => ({ - useDatasetMetaData: () => ({ - data: { - doc_metadata: [ - { id: '1', name: 'field_one', type: DataType.string, count: 5 }, - { id: '2', name: 'field_two', type: DataType.number, count: 3 }, - ], - built_in_field_enabled: false, - }, - }), - useCreateMetaData: () => ({ - mutate: mockDoAddMetaData, - }), - useRenameMeta: () => ({ - mutate: mockDoRenameMetaData, - }), - useDeleteMetaData: () => ({ - mutateAsync: mockDoDeleteMetaData, - }), - useUpdateBuiltInStatus: () => ({ - mutateAsync: mockToggleBuiltInStatus, - }), - useBuiltInMetaDataFields: () => ({ - data: { - fields: [ - { name: 'created_at', type: DataType.time }, - { name: 'modified_at', type: DataType.time }, - ], - }, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.metadata.get': () => ({ + data: { + doc_metadata: [ + { id: '1', name: 'field_one', type: DataType.string, count: 5 }, + { id: '2', name: 'field_two', type: DataType.number, count: 3 }, + ], + built_in_field_enabled: false, + }, + }), + 'datasets.byDatasetId.metadata.post': () => ({ + mutateAsync: mockDoAddMetaData, + }), + 'datasets.byDatasetId.metadata.byMetadataId.patch': () => ({ + mutateAsync: mockDoRenameMetaData, + }), + 'datasets.byDatasetId.metadata.byMetadataId.delete': () => ({ + mutateAsync: mockDoDeleteMetaData, + }), + 'datasets.byDatasetId.metadata.builtIn.byAction.post': () => ({ + mutateAsync: mockToggleBuiltInStatus, + }), + 'datasets.metadata.builtIn.get': () => ({ + data: { + fields: [ + { name: 'created_at', type: DataType.time }, + { name: 'modified_at', type: DataType.time }, + ], + }, + }), + }) +}) vi.mock('@/app/notifications', () => ({ default: { @@ -264,7 +267,9 @@ describe('useEditDatasetMetadata', () => { await result.current.handleDeleteMetaData('1') }) - expect(mockDoDeleteMetaData).toHaveBeenCalledWith('1') + expect(mockDoDeleteMetaData).toHaveBeenCalledWith({ + params: { dataset_id: 'ds-1', metadata_id: '1' }, + }) }) it('should call onUpdateDocList after delete', async () => { @@ -291,7 +296,9 @@ describe('useEditDatasetMetadata', () => { await result.current.setBuiltInEnabled(true) }) - expect(mockToggleBuiltInStatus).toHaveBeenCalledWith(true) + expect(mockToggleBuiltInStatus).toHaveBeenCalledWith({ + params: { dataset_id: 'ds-1', action: 'enable' }, + }) }) }) diff --git a/web/app/components/datasets/metadata/hooks/__tests__/use-metadata-document.spec.ts b/web/app/components/datasets/metadata/hooks/__tests__/use-metadata-document.spec.ts index 9d1ae7459596e9..513d1eca1573ad 100644 --- a/web/app/components/datasets/metadata/hooks/__tests__/use-metadata-document.spec.ts +++ b/web/app/components/datasets/metadata/hooks/__tests__/use-metadata-document.spec.ts @@ -16,28 +16,31 @@ type DocDetail = { const mockMutateAsync = vi.fn().mockResolvedValue({}) const mockDoAddMetaData = vi.fn().mockResolvedValue({}) -vi.mock('@/service/knowledge/use-metadata', () => ({ - useBatchUpdateDocMetadata: () => ({ - mutateAsync: mockMutateAsync, - }), - useCreateMetaData: () => ({ - mutateAsync: mockDoAddMetaData, - }), - useDocumentMetaData: () => ({ - data: { - doc_metadata: [ - { id: '1', name: 'field_one', type: DataType.string, value: 'Value 1' }, - { id: '2', name: 'field_two', type: DataType.number, value: 42 }, - { id: 'built-in', name: 'created_at', type: DataType.time, value: 1609459200 }, - ], - }, - }), - useDatasetMetaData: () => ({ - data: { - built_in_field_enabled: true, - }, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.metadata.post': () => ({ + mutateAsync: mockMutateAsync, + }), + 'datasets.byDatasetId.metadata.post': () => ({ + mutateAsync: mockDoAddMetaData, + }), + 'datasets.byDatasetId.documents.byDocumentId.get': () => ({ + data: { + doc_metadata: [ + { id: '1', name: 'field_one', type: DataType.string, value: 'Value 1' }, + { id: '2', name: 'field_two', type: DataType.number, value: 42 }, + { id: 'built-in', name: 'created_at', type: DataType.time, value: 1609459200 }, + ], + }, + }), + 'datasets.byDatasetId.metadata.get': () => ({ + data: { + built_in_field_enabled: true, + }, + }), + }) +}) // Mock useDatasetDetailContext vi.mock('@/context/dataset-detail', () => ({ diff --git a/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts b/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts index d08b4d75fef9ef..fb528975de98de 100644 --- a/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts +++ b/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts @@ -5,11 +5,12 @@ import type { MetadataItemWithValue, } from '../types' import type { SimpleDocumentDetail } from '@/models/datasets' +import { useMutation } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { t } from 'i18next' import { useMemo } from 'react' import { toast } from '@/app/notifications' -import { useBatchUpdateDocMetadata } from '@/service/knowledge/use-metadata' +import { consoleQuery } from '@/service/console' import { UpdateType } from '../types' type Props = Readonly<{ @@ -126,16 +127,18 @@ const useBatchEditDocumentMetadata = ({ }) return res } - const { mutateAsync } = useBatchUpdateDocMetadata() + const { mutateAsync: updateDocumentMetadata } = useMutation( + consoleQuery.datasets.byDatasetId.documents.metadata.post.mutationOptions(), + ) const handleSave = async ( editedList: MetadataItemInBatchEdit[], addedList: MetadataItemInBatchEdit[], isApplyToAllSelectDocument: boolean, ) => { const backendList = formateToBackendList(editedList, addedList, isApplyToAllSelectDocument) - await mutateAsync({ - dataset_id: datasetId, - metadata_list: backendList, + await updateDocumentMetadata({ + params: { dataset_id: datasetId }, + body: { operation_data: backendList }, }) onUpdate() hideEditModal() diff --git a/web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts b/web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts index 333127acb5009a..e882146f980779 100644 --- a/web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts +++ b/web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts @@ -1,16 +1,10 @@ import type { BuiltInMetadataItem, MetadataItemWithValueLength } from '../types' import type { DataSet } from '@/models/datasets' +import { useMutation, useQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { useCallback, useEffect, useState } from 'react' import { toast } from '@/app/notifications' -import { - useBuiltInMetaDataFields, - useCreateMetaData, - useDatasetMetaData, - useDeleteMetaData, - useRenameMeta, - useUpdateBuiltInStatus, -} from '@/service/knowledge/use-metadata' +import { consoleQuery } from '@/service/console' import { isShowManageMetadataLocalStorageKey } from '../types' import useCheckMetadataName from './use-check-metadata-name' @@ -31,8 +25,14 @@ const useEditDatasetMetadata = ({ localStorage.removeItem(isShowManageMetadataLocalStorageKey) } }, []) - const { data: datasetMetaData } = useDatasetMetaData(datasetId) - const { mutate: doAddMetaData } = useCreateMetaData(datasetId) + const { data: datasetMetaData } = useQuery( + consoleQuery.datasets.byDatasetId.metadata.get.queryOptions({ + input: { params: { dataset_id: datasetId } }, + }), + ) + const { mutateAsync: createMetaData } = useMutation( + consoleQuery.datasets.byDatasetId.metadata.post.mutationOptions(), + ) const { checkName } = useCheckMetadataName() const handleAddMetaData = useCallback( async (payload: BuiltInMetadataItem) => { @@ -41,11 +41,13 @@ const useEditDatasetMetadata = ({ toast.error(errorMsg) return Promise.reject(new Error(errorMsg)) } - await doAddMetaData(payload) + await createMetaData({ params: { dataset_id: datasetId }, body: payload }) }, - [checkName, doAddMetaData], + [checkName, createMetaData, datasetId], + ) + const { mutateAsync: renameMetaData } = useMutation( + consoleQuery.datasets.byDatasetId.metadata.byMetadataId.patch.mutationOptions(), ) - const { mutate: doRenameMetaData } = useRenameMeta(datasetId) const handleRename = useCallback( async (payload: MetadataItemWithValueLength) => { const errorMsg = checkName(payload.name).errorMsg @@ -53,25 +55,34 @@ const useEditDatasetMetadata = ({ toast.error(errorMsg) return Promise.reject(new Error(errorMsg)) } - await doRenameMetaData(payload) + await renameMetaData({ + params: { dataset_id: datasetId, metadata_id: payload.id }, + body: { name: payload.name }, + }) onUpdateDocList() }, - [checkName, doRenameMetaData, onUpdateDocList], + [checkName, renameMetaData, datasetId, onUpdateDocList], + ) + const { mutateAsync: deleteMetaData } = useMutation( + consoleQuery.datasets.byDatasetId.metadata.byMetadataId.delete.mutationOptions(), ) - const { mutateAsync: doDeleteMetaData } = useDeleteMetaData(datasetId) const handleDeleteMetaData = useCallback( async (metaDataId: string) => { - await doDeleteMetaData(metaDataId) + await deleteMetaData({ params: { dataset_id: datasetId, metadata_id: metaDataId } }) onUpdateDocList() }, - [doDeleteMetaData, onUpdateDocList], + [deleteMetaData, datasetId, onUpdateDocList], ) const [builtInEnabled, setBuiltInEnabled] = useState(datasetMetaData?.built_in_field_enabled) useEffect(() => { setBuiltInEnabled(datasetMetaData?.built_in_field_enabled) }, [datasetMetaData]) - const { mutateAsync: toggleBuiltInStatus } = useUpdateBuiltInStatus(datasetId) - const { data: builtInMetaData } = useBuiltInMetaDataFields() + const { mutateAsync: updateBuiltInStatus } = useMutation( + consoleQuery.datasets.byDatasetId.metadata.builtIn.byAction.post.mutationOptions(), + ) + const { data: builtInMetaData } = useQuery( + consoleQuery.datasets.metadata.builtIn.get.queryOptions(), + ) return { isShowEditModal, showEditModal, @@ -83,7 +94,9 @@ const useEditDatasetMetadata = ({ builtInMetaData: builtInMetaData?.fields, builtInEnabled, setBuiltInEnabled: async (enable: boolean) => { - await toggleBuiltInStatus(enable) + await updateBuiltInStatus({ + params: { dataset_id: datasetId, action: enable ? 'enable' : 'disable' }, + }) setBuiltInEnabled(enable) }, } diff --git a/web/app/components/datasets/metadata/hooks/use-metadata-document.ts b/web/app/components/datasets/metadata/hooks/use-metadata-document.ts index f80bfeecb306ff..1922ad44b646b9 100644 --- a/web/app/components/datasets/metadata/hooks/use-metadata-document.ts +++ b/web/app/components/datasets/metadata/hooks/use-metadata-document.ts @@ -1,17 +1,13 @@ import type { BuiltInMetadataItem, MetadataItemWithValue } from '../types' import type { FullDocumentDetail } from '@/models/datasets' +import { useMutation, useQuery } from '@tanstack/react-query' import { get } from 'es-toolkit/compat' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { useDatasetDetailContext } from '@/context/dataset-detail' import { useLanguages, useMetadataMap } from '@/hooks/use-metadata' -import { - useBatchUpdateDocMetadata, - useCreateMetaData, - useDatasetMetaData, - useDocumentMetaData, -} from '@/service/knowledge/use-metadata' +import { consoleQuery } from '@/service/console' import { DataType } from '../types' import useCheckMetadataName from './use-check-metadata-name' @@ -24,18 +20,26 @@ const useMetadataDocument = ({ datasetId, documentId, docDetail }: Props) => { const { t } = useTranslation(['common']) const { dataset } = useDatasetDetailContext() const embeddingAvailable = !!dataset?.embedding_available - const { mutateAsync } = useBatchUpdateDocMetadata() + const { mutateAsync: updateDocumentMetadata } = useMutation( + consoleQuery.datasets.byDatasetId.documents.metadata.post.mutationOptions(), + ) const { checkName } = useCheckMetadataName() const [isEdit, setIsEdit] = useState(false) - const { data: documentDetail } = useDocumentMetaData({ - datasetId, - documentId, - }) + const { data: documentDetail } = useQuery( + consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.queryOptions({ + input: { + params: { dataset_id: datasetId, document_id: documentId }, + query: { metadata: 'only' }, + }, + }), + ) const allList = documentDetail?.doc_metadata || [] const list = allList.filter((item) => item.id !== 'built-in') const builtList = allList.filter((item) => item.id === 'built-in') const [tempList, setTempList] = useState(list) - const { mutateAsync: doAddMetaData } = useCreateMetaData(datasetId) + const { mutateAsync: createMetaData } = useMutation( + consoleQuery.datasets.byDatasetId.metadata.post.mutationOptions(), + ) const handleSelectMetaData = useCallback((metaData: MetadataItemWithValue) => { setTempList((prev) => { const index = prev.findIndex((item) => item.id === metaData.id) @@ -50,21 +54,16 @@ const useMetadataDocument = ({ datasetId, documentId, docDetail }: Props) => { toast.error(errorMsg) return Promise.reject(new Error(errorMsg)) } - await doAddMetaData(payload) + await createMetaData({ params: { dataset_id: datasetId }, body: payload }) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) }, - [checkName, doAddMetaData, t], + [checkName, createMetaData, datasetId, t], ) const hasData = list.length > 0 const handleSave = async () => { - await mutateAsync({ - dataset_id: datasetId, - metadata_list: [ - { - document_id: documentId, - metadata_list: tempList, - }, - ], + await updateDocumentMetadata({ + params: { dataset_id: datasetId }, + body: { operation_data: [{ document_id: documentId, metadata_list: tempList }] }, }) setIsEdit(false) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) @@ -78,7 +77,11 @@ const useMetadataDocument = ({ datasetId, documentId, docDetail }: Props) => { setIsEdit(true) } // built in enabled is set in dataset - const { data: datasetMetaData } = useDatasetMetaData(datasetId) + const { data: datasetMetaData } = useQuery( + consoleQuery.datasets.byDatasetId.metadata.get.queryOptions({ + input: { params: { dataset_id: datasetId } }, + }), + ) const builtInEnabled = datasetMetaData?.built_in_field_enabled // old metadata and technical params const metadataMap = useMetadataMap() @@ -91,7 +94,8 @@ const useMetadataDocument = ({ datasetId, documentId, docDetail }: Props) => { return {} as any } const getTargetValue = (field: string) => { - const val = get(sourceData, field, '') + const val: unknown = get(sourceData, field, '') + if (typeof val !== 'string' && typeof val !== 'number') return '-' if (!val && val !== 0) return '-' if (fieldMap[field]?.inputType === 'select') return getTargetMap(field)[val] if (fieldMap[field]?.render) diff --git a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx index 51642381c0a21e..953c34ddff680f 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx @@ -6,13 +6,16 @@ import { DataType } from '../../types' import DatasetMetadataDrawer from '../dataset-metadata-drawer' // Mock service/API calls -vi.mock('@/service/knowledge/use-metadata', () => ({ - useDatasetMetaData: () => ({ - data: { - doc_metadata: [{ id: '1', name: 'existing_field', type: DataType.string }], - }, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.metadata.get': () => ({ + data: { + doc_metadata: [{ id: '1', name: 'existing_field', type: DataType.string }], + }, + }), + }) +}) // Mock check name hook vi.mock('../../hooks/use-check-metadata-name', () => ({ diff --git a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-picker.spec.tsx b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-picker.spec.tsx index b8cc289f1a2985..471bb51bd1e492 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-picker.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-picker.spec.tsx @@ -5,13 +5,16 @@ import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { DataType } from '../../types' import { DatasetMetadataPicker } from '../dataset-metadata-picker' -const { mockUseDatasetMetaData } = vi.hoisted(() => ({ - mockUseDatasetMetaData: vi.fn(), +const { mockDatasetMetadataQuery } = vi.hoisted(() => ({ + mockDatasetMetadataQuery: vi.fn(), })) -vi.mock('@/service/knowledge/use-metadata', () => ({ - useDatasetMetaData: mockUseDatasetMetaData, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.metadata.get': mockDatasetMetadataQuery, + }) +}) const metadataItems: MetadataItem[] = [ { id: '1', name: 'field_one', type: DataType.string }, @@ -39,7 +42,7 @@ function renderDatasetMetadataPicker( describe('DatasetMetadataPicker', () => { beforeEach(() => { vi.clearAllMocks() - mockUseDatasetMetaData.mockReturnValue({ + mockDatasetMetadataQuery.mockReturnValue({ data: { doc_metadata: metadataItems, }, @@ -244,7 +247,7 @@ describe('DatasetMetadataPicker', () => { describe('Edge Cases', () => { it('should keep action buttons available when metadata list is empty', async () => { const user = userEvent.setup() - mockUseDatasetMetaData.mockReturnValue({ + mockDatasetMetadataQuery.mockReturnValue({ data: { doc_metadata: [], }, diff --git a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-picker.tsx b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-picker.tsx index f95662e65850c5..65a2df81bc93c1 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-picker.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-picker.tsx @@ -15,9 +15,10 @@ import { ComboboxSeparator, } from '@langgenius/dify-ui/combobox' import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { useQuery } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useDatasetMetaData } from '@/service/knowledge/use-metadata' +import { consoleQuery } from '@/service/console' import { getIconClassName } from '../utils/get-icon' import { CreateContent } from './create-content' @@ -58,7 +59,11 @@ export function DatasetMetadataPicker({ onOpenMetadataManagement, }: DatasetMetadataPickerProps) { const { t } = useTranslation(['dataset']) - const { data: datasetMetaData } = useDatasetMetaData(datasetId) + const { data: datasetMetaData } = useQuery( + consoleQuery.datasets.byDatasetId.metadata.get.queryOptions({ + input: { params: { dataset_id: datasetId } }, + }), + ) const metadataItems = datasetMetaData?.doc_metadata ?? [] const [open, setOpen] = useState(false) const [view, setView] = useState(PickerView.select) diff --git a/web/app/components/datasets/metadata/metadata-document/__tests__/document-information.spec.tsx b/web/app/components/datasets/metadata/metadata-document/__tests__/document-information.spec.tsx index 4e0ce4e8e68ec4..51bb7aab4804d6 100644 --- a/web/app/components/datasets/metadata/metadata-document/__tests__/document-information.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-document/__tests__/document-information.spec.tsx @@ -6,12 +6,15 @@ vi.mock('@/context/dataset-detail', () => ({ useDatasetDetailContext: () => ({ dataset: { embedding_available: true } }), })) -vi.mock('@/service/knowledge/use-metadata', () => ({ - useBatchUpdateDocMetadata: () => ({ mutateAsync: vi.fn() }), - useCreateMetaData: () => ({ mutateAsync: vi.fn() }), - useDocumentMetaData: () => ({ data: { doc_metadata: [] } }), - useDatasetMetaData: () => ({ data: { built_in_field_enabled: false } }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.documents.metadata.post': () => ({ mutateAsync: vi.fn() }), + 'datasets.byDatasetId.metadata.post': () => ({ mutateAsync: vi.fn() }), + 'datasets.byDatasetId.documents.byDocumentId.get': () => ({ data: { doc_metadata: [] } }), + 'datasets.byDatasetId.metadata.get': () => ({ data: { built_in_field_enabled: false } }), + }) +}) vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }), @@ -33,7 +36,15 @@ vi.mock('react-i18next', async () => { type DocumentDetail = Parameters[0]['docDetail'] -const uploadFile = { name: 'handbook.txt', extension: 'txt', size: 497 } +const uploadFile = { + id: 'upload-1', + name: 'handbook.txt', + extension: 'txt', + size: 497, + mime_type: 'text/plain', + created_by: 'user-1', + created_at: Date.parse('2026-09-23T10:30:00Z') / 1000, +} // The Console detail endpoint separates the upload reference from its details. const documentDetail: Partial = { diff --git a/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx b/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx index 7f90e0ed95bd0d..4257edfc761328 100644 --- a/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx @@ -30,13 +30,16 @@ vi.mock('../../hooks/use-metadata-document', () => ({ })) // Mock service calls -vi.mock('@/service/knowledge/use-metadata', () => ({ - useDatasetMetaData: () => ({ - data: { - doc_metadata: [], - }, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.metadata.get': () => ({ + data: { + doc_metadata: [], + }, + }), + }) +}) // Mock check name hook vi.mock('../../hooks/use-check-metadata-name', () => ({ diff --git a/web/app/components/datasets/metadata/metadata-document/__tests__/info-group.spec.tsx b/web/app/components/datasets/metadata/metadata-document/__tests__/info-group.spec.tsx index d5a27763110a6b..6a3ebddb1b50b6 100644 --- a/web/app/components/datasets/metadata/metadata-document/__tests__/info-group.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-document/__tests__/info-group.spec.tsx @@ -21,13 +21,16 @@ vi.mock('@/next/navigation', () => ({ }), })) -vi.mock('@/service/knowledge/use-metadata', () => ({ - useDatasetMetaData: () => ({ - data: { - doc_metadata: [{ id: '1', name: 'test', type: DataType.string }], - }, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'datasets.byDatasetId.metadata.get': () => ({ + data: { + doc_metadata: [{ id: '1', name: 'test', type: DataType.string }], + }, + }), + }) +}) // Mock useTimestamp vi.mock('@/hooks/use-timestamp', () => ({ diff --git a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx index aa9f211e521949..a2a0c3fe1c0343 100644 --- a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import { mockEmojiData, renderWithEmoji as render } from '@/test/emoji-picker' +import { createDatasetFixture } from '@/test/fixtures/dataset' import RenameDatasetModal from '../index' const { mockToast } = vi.hoisted(() => { @@ -33,10 +34,10 @@ vi.mock('@/service/datasets', () => ({ describe('RenameDatasetModal', () => { // Create a base dataset with emoji icon const createMockDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', - indexing_status: 'completed', icon_info: { icon: '📊', icon_type: 'emoji', @@ -52,14 +53,12 @@ describe('RenameDatasetModal', () => { app_count: 0, doc_form: ChunkingMode.text, document_count: 5, - total_document_count: 5, word_count: 1000, provider: 'openai', embedding_model: 'text-embedding-ada-002', embedding_model_provider: 'openai', embedding_available: true, retrieval_model_dict: {} as DataSet['retrieval_model_dict'], - retrieval_model: {} as DataSet['retrieval_model'], tags: [], external_knowledge_info: { external_knowledge_id: '', diff --git a/web/app/components/datasets/rename-modal/index.tsx b/web/app/components/datasets/rename-modal/index.tsx index 079f6105da1c70..14784af506d520 100644 --- a/web/app/components/datasets/rename-modal/index.tsx +++ b/web/app/components/datasets/rename-modal/index.tsx @@ -1,4 +1,5 @@ 'use client' +import type { DatasetUpdatePayload } from '@dify/contracts/api/console/datasets/types.gen' import type { IconPickerValue } from '@/app/components/base/icon-picker' import type { DataSet } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' @@ -28,9 +29,9 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset const { t } = useTranslation(['common', 'datasetSettings']) const [loading, setLoading] = useState(false) const [name, setName] = useState(dataset.name) - const [description, setDescription] = useState(dataset.description) - const externalKnowledgeId = dataset.external_knowledge_info.external_knowledge_id - const externalKnowledgeApiId = dataset.external_knowledge_info.external_knowledge_api_id + const [description, setDescription] = useState(dataset.description ?? '') + const externalKnowledgeId = dataset.external_knowledge_info?.external_knowledge_id + const externalKnowledgeApiId = dataset.external_knowledge_info?.external_knowledge_api_id const [appIcon, setAppIcon] = useState( dataset.icon_info?.icon_type === 'image' ? { @@ -54,10 +55,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset } try { setLoading(true) - const body: Partial & { - external_knowledge_id?: string - external_knowledge_api_id?: string - } = { + const body: DatasetUpdatePayload = { name, description, icon_info: { diff --git a/web/app/components/datasets/settings/form/__tests__/index.spec.tsx b/web/app/components/datasets/settings/form/__tests__/index.spec.tsx index 00e85cf09dd130..2d7e71b0d05052 100644 --- a/web/app/components/datasets/settings/form/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/form/__tests__/index.spec.tsx @@ -8,6 +8,7 @@ import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datase import { expectLoadingButton } from '@/test/button' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import { DatasetACLPermission } from '@/utils/permission' import { IndexingType } from '../../../create/step-two' @@ -19,7 +20,6 @@ const { mockToastError } = vi.hoisted(() => ({ // Mock contexts const mockMutateDatasets = vi.fn() -const mockInvalidDatasetList = vi.fn() const mockUserProfile = { id: 'user-1', @@ -55,6 +55,7 @@ function render(ui: ReactElement) { } const createMockDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', @@ -66,7 +67,6 @@ const createMockDataset = (overrides: Partial = {}): DataSet => ({ icon_url: '', }, indexing_technique: IndexingType.QUALIFIED, - indexing_status: 'completed', data_source_type: DataSourceType.FILE, doc_form: ChunkingMode.text, embedding_model: 'text-embedding-ada-002', @@ -74,7 +74,6 @@ const createMockDataset = (overrides: Partial = {}): DataSet => ({ embedding_available: true, app_count: 0, document_count: 5, - total_document_count: 5, word_count: 1000, provider: 'vendor', tags: [], @@ -101,17 +100,6 @@ const createMockDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0.5, } as RetrievalConfig, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 3, - score_threshold_enabled: false, - score_threshold: 0.5, - } as RetrievalConfig, built_in_field_enabled: false, keyword_number: 10, created_by: 'user-1', @@ -143,41 +131,6 @@ vi.mock('@/service/datasets', () => ({ updateDatasetSetting: vi.fn().mockResolvedValue({}), })) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => mockInvalidDatasetList, -})) - -vi.mock('@/service/use-common', () => ({ - useMembers: () => ({ - data: { - accounts: [ - { - id: 'user-1', - name: 'User 1', - email: 'user1@example.com', - role: 'owner', - avatar: '', - avatar_url: '', - last_login_at: '', - created_at: '', - status: 'active', - }, - { - id: 'user-2', - name: 'User 2', - email: 'user2@example.com', - role: 'admin', - avatar: '', - avatar_url: '', - last_login_at: '', - created_at: '', - status: 'active', - }, - ], - }, - }), -})) - vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useCurrentProviderAndModel: () => ({ currentProvider: undefined, currentModel: undefined }), useDefaultModel: () => ({ data: undefined, mutate: vi.fn(), isLoading: false }), @@ -549,6 +502,35 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { { params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] } > }) => { + if (options.queryKey[0].join('.') === 'console.workspaces.current.members.get') + return (() => ({ + data: { + accounts: [ + { + id: 'user-1', + name: 'User 1', + email: 'user1@example.com', + role: 'owner', + avatar: '', + avatar_url: '', + last_login_at: '', + created_at: '', + status: 'active', + }, + { + id: 'user-2', + name: 'User 2', + email: 'user2@example.com', + role: 'admin', + avatar: '', + avatar_url: '', + last_login_at: '', + created_at: '', + status: 'active', + }, + ], + }, + }))() if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options) return { data: [], refetch: vi.fn(), isPending: false } }, diff --git a/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx b/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx index efa26a02b893f4..95f58d64577f06 100644 --- a/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx +++ b/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx @@ -8,6 +8,7 @@ import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datase import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' import { mockEmojiData } from '@/test/emoji-picker' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import { IndexingType } from '../../../../create/step-two' import BasicInfoSection from '../basic-info-section' @@ -61,6 +62,7 @@ vi.mock('@/app/components/base/image-uploader/hooks', () => ({ describe('BasicInfoSection', () => { const mockDataset: DataSet = { + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', @@ -72,7 +74,6 @@ describe('BasicInfoSection', () => { icon_url: '', }, indexing_technique: IndexingType.QUALIFIED, - indexing_status: 'completed', data_source_type: DataSourceType.FILE, doc_form: ChunkingMode.text, embedding_model: 'text-embedding-ada-002', @@ -80,7 +81,6 @@ describe('BasicInfoSection', () => { embedding_available: true, app_count: 0, document_count: 5, - total_document_count: 5, word_count: 1000, provider: 'vendor', tags: [], @@ -107,17 +107,6 @@ describe('BasicInfoSection', () => { score_threshold_enabled: false, score_threshold: 0.5, } as RetrievalConfig, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 3, - score_threshold_enabled: false, - score_threshold: 0.5, - } as RetrievalConfig, built_in_field_enabled: false, keyword_number: 10, created_by: 'user-1', @@ -137,8 +126,8 @@ describe('BasicInfoSection', () => { roles: [], avatar: '', avatar_url: '', - last_login_at: '', - created_at: '', + last_login_at: null, + created_at: null, status: 'active', }, { @@ -149,8 +138,8 @@ describe('BasicInfoSection', () => { roles: [], avatar: '', avatar_url: '', - last_login_at: '', - created_at: '', + last_login_at: null, + created_at: null, status: 'active', }, ] diff --git a/web/app/components/datasets/settings/form/components/__tests__/external-knowledge-section.spec.tsx b/web/app/components/datasets/settings/form/components/__tests__/external-knowledge-section.spec.tsx index 176829a9d3b544..6084cddaa0c563 100644 --- a/web/app/components/datasets/settings/form/components/__tests__/external-knowledge-section.spec.tsx +++ b/web/app/components/datasets/settings/form/components/__tests__/external-knowledge-section.spec.tsx @@ -2,6 +2,7 @@ import type { DataSet } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' import { render, screen } from '@testing-library/react' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import { IndexingType } from '../../../../create/step-two' import ExternalKnowledgeSection from '../external-knowledge-section' @@ -20,6 +21,7 @@ describe('ExternalKnowledgeSection', () => { } const mockDataset: DataSet = { + ...createDatasetFixture(), id: 'dataset-1', name: 'External Dataset', description: 'External dataset description', @@ -31,7 +33,6 @@ describe('ExternalKnowledgeSection', () => { icon_url: '', }, indexing_technique: IndexingType.QUALIFIED, - indexing_status: 'completed', data_source_type: DataSourceType.FILE, doc_form: ChunkingMode.text, embedding_model: 'text-embedding-ada-002', @@ -39,7 +40,6 @@ describe('ExternalKnowledgeSection', () => { embedding_available: true, app_count: 0, document_count: 5, - total_document_count: 5, word_count: 1000, provider: 'external', tags: [], @@ -56,7 +56,6 @@ describe('ExternalKnowledgeSection', () => { score_threshold_enabled: true, }, retrieval_model_dict: mockRetrievalConfig, - retrieval_model: mockRetrievalConfig, built_in_field_enabled: false, keyword_number: 10, created_by: 'user-1', diff --git a/web/app/components/datasets/settings/form/components/__tests__/indexing-section.spec.tsx b/web/app/components/datasets/settings/form/components/__tests__/indexing-section.spec.tsx index fa32f68d14ae0d..a9931f3f765164 100644 --- a/web/app/components/datasets/settings/form/components/__tests__/indexing-section.spec.tsx +++ b/web/app/components/datasets/settings/form/components/__tests__/indexing-section.spec.tsx @@ -11,6 +11,7 @@ import { } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import { renderWithConsoleQuery as render } from '@/test/console/query-data' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import { IndexingType } from '../../../../create/step-two' import IndexingSection from '../indexing-section' @@ -163,6 +164,7 @@ describe('IndexingSection', () => { } const mockDataset: DataSet = { + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', @@ -174,7 +176,6 @@ describe('IndexingSection', () => { icon_url: '', }, indexing_technique: IndexingType.QUALIFIED, - indexing_status: 'completed', data_source_type: DataSourceType.FILE, doc_form: ChunkingMode.text, embedding_model: 'text-embedding-ada-002', @@ -182,7 +183,6 @@ describe('IndexingSection', () => { embedding_available: true, app_count: 0, document_count: 5, - total_document_count: 5, word_count: 1000, provider: 'vendor', tags: [], @@ -199,7 +199,6 @@ describe('IndexingSection', () => { score_threshold_enabled: true, }, retrieval_model_dict: mockRetrievalConfig, - retrieval_model: mockRetrievalConfig, built_in_field_enabled: false, keyword_number: 10, created_by: 'user-1', diff --git a/web/app/components/datasets/settings/form/components/__tests__/reflow.browser.spec.tsx b/web/app/components/datasets/settings/form/components/__tests__/reflow.browser.spec.tsx index c140beafaa74c5..21dedbf42ee636 100644 --- a/web/app/components/datasets/settings/form/components/__tests__/reflow.browser.spec.tsx +++ b/web/app/components/datasets/settings/form/components/__tests__/reflow.browser.spec.tsx @@ -6,6 +6,7 @@ import { page } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' import datasetSettings from '@/i18n/locales/en-US/dataset-settings.json' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import { IndexingType } from '../../../../create/step-two' import ExternalKnowledgeSection from '../external-knowledge-section' @@ -55,6 +56,7 @@ const mockRetrievalConfig: RetrievalConfig = { } const mockDataset: DataSet = { + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', @@ -66,7 +68,6 @@ const mockDataset: DataSet = { icon_url: '', }, indexing_technique: IndexingType.ECONOMICAL, - indexing_status: 'completed', data_source_type: DataSourceType.FILE, doc_form: ChunkingMode.text, embedding_model: 'text-embedding-ada-002', @@ -74,7 +75,6 @@ const mockDataset: DataSet = { embedding_available: true, app_count: 0, document_count: 5, - total_document_count: 5, word_count: 1000, provider: 'vendor', tags: [], @@ -91,7 +91,6 @@ const mockDataset: DataSet = { score_threshold_enabled: true, }, retrieval_model_dict: mockRetrievalConfig, - retrieval_model: mockRetrievalConfig, built_in_field_enabled: false, keyword_number: 10, created_by: 'user-1', diff --git a/web/app/components/datasets/settings/form/components/external-knowledge-section.tsx b/web/app/components/datasets/settings/form/components/external-knowledge-section.tsx index f69a1ad8408e49..27a7ae67a4d544 100644 --- a/web/app/components/datasets/settings/form/components/external-knowledge-section.tsx +++ b/web/app/components/datasets/settings/form/components/external-knowledge-section.tsx @@ -67,11 +67,11 @@ const ExternalKnowledgeSection = ({ className="i-custom-vender-solid-development-api-connection-mod size-4 shrink-0 text-text-secondary" />
- {currentDataset.external_knowledge_info.external_knowledge_api_name} + {currentDataset.external_knowledge_info?.external_knowledge_api_name}
·
- {currentDataset.external_knowledge_info.external_knowledge_api_endpoint} + {currentDataset.external_knowledge_info?.external_knowledge_api_endpoint}
@@ -87,7 +87,7 @@ const ExternalKnowledgeSection = ({
- {currentDataset.external_knowledge_info.external_knowledge_id} + {currentDataset.external_knowledge_info?.external_knowledge_id}
diff --git a/web/app/components/datasets/settings/form/components/indexing-section.tsx b/web/app/components/datasets/settings/form/components/indexing-section.tsx index 3b864bebef344e..de7bcb3f17e5af 100644 --- a/web/app/components/datasets/settings/form/components/indexing-section.tsx +++ b/web/app/components/datasets/settings/form/components/indexing-section.tsx @@ -18,6 +18,7 @@ import { useDocLink } from '@/context/i18n' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { ChunkingMode } from '@/models/datasets' import { IndexingType } from '../../../create/step-two' +import { toChunkingMode } from '../../../dataset-fields' import ChunkStructure from '../../chunk-structure' import IndexMethod from '../../index-method' import SummaryIndexSetting from '../../summary-index-setting' @@ -71,6 +72,7 @@ const IndexingSection = ({ }) const isNonCloudEdition = deploymentEdition === 'COMMUNITY' || deploymentEdition === 'ENTERPRISE' const docLink = useDocLink() + const chunkingMode = toChunkingMode(currentDataset?.doc_form) const isShowIndexMethod = currentDataset && @@ -84,13 +86,14 @@ const IndexingSection = ({ const showSummaryIndexSetting = indexMethod === IndexingType.QUALIFIED && - embeddingChunkingModes.includes(currentDataset?.doc_form as ChunkingMode) && + !!chunkingMode && + embeddingChunkingModes.includes(chunkingMode) && isNonCloudEdition return ( <> {/* Chunk Structure */} - {!!currentDataset?.doc_form && ( + {!!chunkingMode && ( <>
@@ -111,7 +114,7 @@ const IndexingSection = ({
- +
@@ -134,7 +137,7 @@ const IndexingSection = ({ value={indexMethod!} disabled={!currentDataset?.embedding_available || readonly} onChange={setIndexMethod} - currentValue={currentDataset.indexing_technique} + currentValue={currentDataset.indexing_technique ?? undefined} keywordNumber={keywordNumber} onKeywordNumberChange={setKeywordNumber} /> diff --git a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts index 57afe0a8d5e60f..e3a34ddd8a92c9 100644 --- a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts +++ b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts @@ -2,6 +2,7 @@ import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/ import type { OperationKey } from '@orpc/tanstack-query' import type { DataSet } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' +import { QueryClient } from '@tanstack/react-query' import { act, waitFor } from '@testing-library/react' import { ChunkingMode, @@ -10,13 +11,18 @@ import { RerankingModeEnum, WeightedScoreEnum, } from '@/models/datasets' +import { consoleQuery } from '@/service/console' import { createAccountProfileQueryWrapper } from '@/test/console/account-profile' import { renderHook as renderHookWithConsoleState } from '@/test/console/render' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD } from '@/types/app' import { DatasetACLPermission } from '@/utils/permission' import { IndexingType } from '../../../../create/step-two' import { useFormState } from '../use-form-state' +const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries') +const datasetListKey = consoleQuery.datasets.get.key() + const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ mockToastSuccess: vi.fn(), mockToastError: vi.fn(), @@ -24,7 +30,6 @@ const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ // Mock contexts const mockMutateDatasets = vi.fn() -const mockInvalidDatasetList = vi.fn() const renderHook = (callback: () => ReturnType) => renderHookWithConsoleState(callback, { @@ -57,6 +62,7 @@ vi.mock('@/features/system-features/state', async () => { }) const createDefaultMockDataset = (): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Test Dataset', description: 'Test description', @@ -68,7 +74,6 @@ const createDefaultMockDataset = (): DataSet => ({ icon_url: '', }, indexing_technique: IndexingType.QUALIFIED, - indexing_status: 'completed', data_source_type: DataSourceType.FILE, doc_form: ChunkingMode.text, embedding_model: 'text-embedding-ada-002', @@ -76,7 +81,6 @@ const createDefaultMockDataset = (): DataSet => ({ embedding_available: true, app_count: 0, document_count: 5, - total_document_count: 5, word_count: 1000, provider: 'vendor', tags: [], @@ -103,17 +107,6 @@ const createDefaultMockDataset = (): DataSet => ({ score_threshold_enabled: false, score_threshold: 0.5, } as RetrievalConfig, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 3, - score_threshold_enabled: false, - score_threshold: 0.5, - } as RetrievalConfig, built_in_field_enabled: false, keyword_number: 10, created_by: 'user-1', @@ -144,41 +137,6 @@ vi.mock('@/service/datasets', () => ({ updateDatasetSetting: vi.fn().mockResolvedValue({}), })) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => mockInvalidDatasetList, -})) - -vi.mock('@/service/use-common', () => ({ - useMembers: () => ({ - data: { - accounts: [ - { - id: 'user-1', - name: 'User 1', - email: 'user1@example.com', - role: 'owner', - avatar: '', - avatar_url: '', - last_login_at: '', - created_at: '', - status: 'active', - }, - { - id: 'user-2', - name: 'User 2', - email: 'user2@example.com', - role: 'admin', - avatar: '', - avatar_url: '', - last_login_at: '', - created_at: '', - status: 'active', - }, - ], - }, - }), -})) - vi.mock('@/app/components/datasets/common/check-rerank-model', async (importOriginal) => ({ ...(await importOriginal()), isReRankModelSelected: () => true, @@ -199,7 +157,7 @@ describe('useFormState', () => { describe('Initial State', () => { it('should initialize with dataset values', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.name).toBe('Test Dataset') expect(result.current.description).toBe('Test description') @@ -209,7 +167,7 @@ describe('useFormState', () => { }) it('should initialize icon info from dataset', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.iconInfo).toEqual({ icon_type: 'emoji', @@ -220,7 +178,7 @@ describe('useFormState', () => { }) it('should initialize external retrieval settings', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.topK).toBe(3) expect(result.current.scoreThreshold).toBe(0.7) @@ -228,14 +186,14 @@ describe('useFormState', () => { }) it('should derive member list from API data', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.memberList).toHaveLength(2) expect(result.current.memberList[0]!.name).toBe('User 1') }) it('should return currentDataset from context', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.currentDataset).toBeDefined() expect(result.current.currentDataset?.id).toBe('dataset-1') @@ -246,7 +204,7 @@ describe('useFormState', () => { ...createDefaultMockDataset(), permission_keys: [DatasetACLPermission.Readonly], } - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.canEditSettings).toBe(false) expect('isCurrentWorkspaceDatasetOperator' in result.current).toBe(false) @@ -255,7 +213,7 @@ describe('useFormState', () => { describe('State Setters', () => { it('should update name when setName is called', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setName('New Name') @@ -265,7 +223,7 @@ describe('useFormState', () => { }) it('should update description when setDescription is called', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setDescription('New Description') @@ -275,7 +233,7 @@ describe('useFormState', () => { }) it('should update permission when setPermission is called', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setPermission(DatasetPermission.allTeamMembers) @@ -285,7 +243,7 @@ describe('useFormState', () => { }) it('should update indexMethod when setIndexMethod is called', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setIndexMethod(IndexingType.ECONOMICAL) @@ -295,7 +253,7 @@ describe('useFormState', () => { }) it('should update keywordNumber when setKeywordNumber is called', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setKeywordNumber(20) @@ -305,7 +263,7 @@ describe('useFormState', () => { }) it('should update selectedMemberIDs when setSelectedMemberIDs is called', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setSelectedMemberIDs(['user-1', 'user-2']) @@ -317,7 +275,7 @@ describe('useFormState', () => { describe('Icon Handlers', () => { it('should select emoji icon without owning picker close state', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.handleSelectAppIcon({ @@ -336,7 +294,7 @@ describe('useFormState', () => { }) it('should select image icon without owning picker close state', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.handleSelectAppIcon({ @@ -357,7 +315,7 @@ describe('useFormState', () => { describe('External Retrieval Settings Handler', () => { it('should update topK when provided', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.handleSettingsChange({ top_k: 5 }) @@ -367,7 +325,7 @@ describe('useFormState', () => { }) it('should update scoreThreshold when provided', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.handleSettingsChange({ score_threshold: 0.8 }) @@ -377,7 +335,7 @@ describe('useFormState', () => { }) it('should update scoreThresholdEnabled when provided', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.handleSettingsChange({ score_threshold_enabled: false }) @@ -387,7 +345,7 @@ describe('useFormState', () => { }) it('should update multiple settings at once', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.handleSettingsChange({ @@ -405,7 +363,7 @@ describe('useFormState', () => { describe('Summary Index Setting Handler', () => { it('should update summary index setting', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.handleSummaryIndexSettingChange({ @@ -419,7 +377,7 @@ describe('useFormState', () => { }) it('should merge with existing settings', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.handleSummaryIndexSettingChange({ @@ -445,7 +403,7 @@ describe('useFormState', () => { describe('handleSave', () => { it('should show error toast when name is empty', async () => { const { toast } = await import('@/app/notifications') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setName('') @@ -460,7 +418,7 @@ describe('useFormState', () => { it('should show error toast when name is whitespace only', async () => { const { toast } = await import('@/app/notifications') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setName(' ') @@ -475,7 +433,7 @@ describe('useFormState', () => { it('should call updateDatasetSetting with correct params', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) await act(async () => { await result.current.handleSave() @@ -497,7 +455,7 @@ describe('useFormState', () => { ...createDefaultMockDataset(), permission_keys: [DatasetACLPermission.Readonly], } - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) await act(async () => { await result.current.handleSave() @@ -508,7 +466,7 @@ describe('useFormState', () => { it('should show success toast on successful save', async () => { const { toast } = await import('@/app/notifications') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) await act(async () => { await result.current.handleSave() @@ -520,7 +478,7 @@ describe('useFormState', () => { }) it('should call mutateDatasets after successful save', async () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) await act(async () => { await result.current.handleSave() @@ -532,19 +490,19 @@ describe('useFormState', () => { }) it('should call invalidDatasetList after successful save', async () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) await act(async () => { await result.current.handleSave() }) await waitFor(() => { - expect(mockInvalidDatasetList).toHaveBeenCalled() + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: datasetListKey }) }) }) it('should set loading to true during save', async () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.loading).toBe(false) @@ -564,7 +522,7 @@ describe('useFormState', () => { () => new Promise((resolve) => setTimeout(resolve, 100)), ) - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) // Start first save act(() => { @@ -585,7 +543,7 @@ describe('useFormState', () => { const { toast } = await import('@/app/notifications') vi.mocked(updateDatasetSetting).mockRejectedValueOnce(new Error('Network error')) - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) await act(async () => { await result.current.handleSave() @@ -596,7 +554,7 @@ describe('useFormState', () => { it('should include partial_member_list when permission is partialMembers', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setPermission(DatasetPermission.partialMembers) @@ -621,7 +579,7 @@ describe('useFormState', () => { describe('Embedding Model', () => { it('should initialize embedding model from dataset', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.embeddingModel).toEqual({ provider: 'openai', @@ -630,7 +588,7 @@ describe('useFormState', () => { }) it('should update embedding model when setEmbeddingModel is called', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setEmbeddingModel({ @@ -648,14 +606,14 @@ describe('useFormState', () => { describe('Retrieval Config', () => { it('should initialize retrieval config from dataset', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) expect(result.current.retrievalConfig).toBeDefined() expect(result.current.retrievalConfig.search_method).toBe(RETRIEVE_METHOD.semantic) }) it('should update retrieval config when setRetrievalConfig is called', () => { - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) const newConfig: RetrievalConfig = { ...result.current.retrievalConfig, @@ -671,7 +629,7 @@ describe('useFormState', () => { it('should include weights in save request when weights are set', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) // Set retrieval config with weights const configWithWeights: RetrievalConfig = { @@ -716,7 +674,7 @@ describe('useFormState', () => { it('should send the configured score threshold when score_threshold_enabled is true', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setRetrievalConfig({ @@ -759,7 +717,7 @@ describe('useFormState', () => { it('should persist reranking_enable=true when a rerank model is configured but the stale flag is false', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) // Reproduces the reported bug: Hybrid Search renders no rerank switch, so the flag keeps // the stale `false` default even though a rerank model is selected. @@ -786,7 +744,7 @@ describe('useFormState', () => { it('should leave reranking_enable untouched when Hybrid Search uses weighted score', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setRetrievalConfig({ @@ -811,7 +769,7 @@ describe('useFormState', () => { it('should leave reranking_enable untouched for semantic search', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) act(() => { result.current.setRetrievalConfig({ @@ -857,7 +815,7 @@ describe('useFormState', () => { it('should include external knowledge info in save request for external provider', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) await act(async () => { await result.current.handleSave() @@ -879,7 +837,7 @@ describe('useFormState', () => { it('should use correct external retrieval settings', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const { result } = renderHook(() => useFormState()) + const { result } = renderHook(() => useFormState(mockDataset)) // Update external retrieval settings act(() => { @@ -918,6 +876,35 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { { params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] } > }) => { + if (options.queryKey[0].join('.') === 'console.workspaces.current.members.get') + return (() => ({ + data: { + accounts: [ + { + id: 'user-1', + name: 'User 1', + email: 'user1@example.com', + role: 'owner', + avatar: '', + avatar_url: '', + last_login_at: '', + created_at: '', + status: 'active', + }, + { + id: 'user-2', + name: 'User 2', + email: 'user2@example.com', + role: 'admin', + avatar: '', + avatar_url: '', + last_login_at: '', + created_at: '', + status: 'active', + }, + ], + }, + }))() if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options) return { data: [] } }, diff --git a/web/app/components/datasets/settings/form/hooks/use-form-state.ts b/web/app/components/datasets/settings/form/hooks/use-form-state.ts index ea36e4af0f3c6c..b0c65b101ebd26 100644 --- a/web/app/components/datasets/settings/form/hooks/use-form-state.ts +++ b/web/app/components/datasets/settings/form/hooks/use-form-state.ts @@ -2,9 +2,13 @@ import type { IconPickerValue } from '@/app/components/base/icon-picker' import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Member } from '@/models/common' -import type { IconInfo, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets' +import type { + DataSet, + IconInfo, + SummaryIndexSetting as SummaryIndexSettingType, +} from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' -import { useQuery, useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,9 +24,8 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { DatasetPermission } from '@/models/datasets' import { consoleQuery } from '@/service/console' import { updateDatasetSetting } from '@/service/datasets' -import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' -import { useMembers } from '@/service/use-common' import { getDatasetACLCapabilities } from '@/utils/permission' +import { toIconInfo, toRetrievalConfig, toSummaryIndexSetting } from '../../../dataset-fields' import { checkShowMultiModalTip } from '../../utils' const DEFAULT_APP_ICON: IconInfo = { @@ -32,9 +35,8 @@ const DEFAULT_APP_ICON: IconInfo = { icon_url: '', } -export const useFormState = () => { +export const useFormState = (currentDataset: DataSet) => { const { t } = useTranslation(['appDebug', 'common', 'datasetSettings']) - const currentDataset = useDatasetDetailContextWithSelector((state) => state.dataset) const mutateDatasets = useDatasetDetailContextWithSelector((state) => state.mutateDatasetRes) const { data: currentUserId } = useSuspenseQuery({ ...userProfileQueryOptions(), @@ -63,33 +65,37 @@ export const useFormState = () => { const [description, setDescription] = useState(currentDataset?.description ?? '') // Icon state - const [iconInfo, setIconInfo] = useState(currentDataset?.icon_info || DEFAULT_APP_ICON) + const [iconInfo, setIconInfo] = useState( + toIconInfo(currentDataset?.icon_info) ?? DEFAULT_APP_ICON, + ) // Permission state - const [permission, setPermission] = useState(currentDataset?.permission) + const [permission, setPermission] = useState( + currentDataset.permission, + ) const [selectedMemberIDs, setSelectedMemberIDs] = useState( currentDataset?.partial_member_list || [], ) // External retrieval state - const [topK, setTopK] = useState(currentDataset?.external_retrieval_model.top_k ?? 2) + const [topK, setTopK] = useState(currentDataset?.external_retrieval_model?.top_k ?? 2) const [scoreThreshold, setScoreThreshold] = useState( - currentDataset?.external_retrieval_model.score_threshold ?? 0.5, + currentDataset?.external_retrieval_model?.score_threshold ?? 0.5, ) const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState( - currentDataset?.external_retrieval_model.score_threshold_enabled ?? false, + currentDataset?.external_retrieval_model?.score_threshold_enabled ?? false, ) // Indexing and retrieval state - const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique) + const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique ?? undefined) const [keywordNumber, setKeywordNumber] = useState(currentDataset?.keyword_number ?? 10) - const [retrievalConfig, setRetrievalConfig] = useState( - currentDataset?.retrieval_model_dict as RetrievalConfig, + const [retrievalConfig, setRetrievalConfig] = useState(() => + toRetrievalConfig(currentDataset.retrieval_model_dict), ) const [embeddingModel, setEmbeddingModel] = useState( currentDataset?.embedding_model ? { - provider: currentDataset.embedding_model_provider, + provider: currentDataset.embedding_model_provider ?? '', model: currentDataset.embedding_model, } : { @@ -99,8 +105,12 @@ export const useFormState = () => { ) // Summary index state - const [summaryIndexSetting, setSummaryIndexSetting] = useState( - currentDataset?.summary_index_setting, + const [summaryIndexSetting, setSummaryIndexSetting] = useState< + SummaryIndexSettingType | undefined + >( + () => + currentDataset.summary_index_setting && + toSummaryIndexSetting(currentDataset.summary_index_setting), ) // Model lists @@ -116,8 +126,13 @@ export const useFormState = () => { select: (response) => response.data, }), ) - const { data: membersData } = useMembers() - const invalidDatasetList = useInvalidDatasetList() + const { data: membersData } = useQuery( + consoleQuery.workspaces.current.members.get.queryOptions({ input: {} }), + ) + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) // Derive member list from API data const memberList = useMemo(() => { @@ -196,10 +211,10 @@ export const useFormState = () => { summary_index_setting: summaryIndexSetting, } - if (currentDataset!.provider === 'external') { - body.external_knowledge_id = currentDataset!.external_knowledge_info.external_knowledge_id + if (currentDataset.provider === 'external') { + body.external_knowledge_id = currentDataset.external_knowledge_info?.external_knowledge_id body.external_knowledge_api_id = - currentDataset!.external_knowledge_info.external_knowledge_api_id + currentDataset.external_knowledge_info?.external_knowledge_api_id body.external_retrieval_model = { top_k: topK, score_threshold: scoreThreshold, @@ -216,7 +231,7 @@ export const useFormState = () => { }) } - await updateDatasetSetting({ datasetId: currentDataset!.id, body }) + await updateDatasetSetting({ datasetId: currentDataset.id, body }) toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) if (mutateDatasets) { diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index 543c529c0f5e36..5c1f6e2623fce8 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -1,17 +1,17 @@ 'use client' +import type { DataSet } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' import { Separator } from '@langgenius/dify-ui/separator' import { useTranslation } from 'react-i18next' +import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import BasicInfoSection from './components/basic-info-section' import ExternalKnowledgeSection from './components/external-knowledge-section' import IndexingSection from './components/indexing-section' import { useFormState } from './hooks/use-form-state' -const Form = () => { +const DatasetSettingsForm = ({ currentDataset }: { currentDataset: DataSet }) => { const { t } = useTranslation(['datasetSettings']) const { - // Context values - currentDataset, canEditSettings, // Loading state @@ -60,7 +60,7 @@ const Form = () => { // Actions handleSave, - } = useFormState() + } = useFormState(currentDataset) const isExternalProvider = currentDataset?.provider === 'external' const readonly = !canEditSettings @@ -132,4 +132,10 @@ const Form = () => { ) } +const Form = () => { + const currentDataset = useDatasetDetailContextWithSelector((state) => state.dataset) + if (!currentDataset) return null + return +} + export default Form diff --git a/web/app/components/datasets/settings/index-method/index.tsx b/web/app/components/datasets/settings/index-method/index.tsx index 9ec90aea7780a7..b617495079b385 100644 --- a/web/app/components/datasets/settings/index-method/index.tsx +++ b/web/app/components/datasets/settings/index-method/index.tsx @@ -7,7 +7,8 @@ import OptionCard from '../option-card' import KeywordNumber from './keyword-number' type IndexMethodProps = { - value: IndexingType + // Unset until the knowledge base indexes its first document. + value?: IndexingType onChange: (id: IndexingType) => void disabled?: boolean currentValue?: IndexingType diff --git a/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx b/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx index 6a7d578500b550..6e2c5e23169fa9 100644 --- a/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx @@ -27,8 +27,8 @@ const memberList: Member[] = [ avatar_url: '', role: 'owner', roles: [], - last_login_at: '', - created_at: '', + last_login_at: null, + created_at: null, status: 'active', }, { @@ -39,8 +39,8 @@ const memberList: Member[] = [ avatar_url: '', role: 'admin', roles: [], - last_login_at: '', - created_at: '', + last_login_at: null, + created_at: null, status: 'active', }, { @@ -51,8 +51,8 @@ const memberList: Member[] = [ avatar_url: '', role: 'normal', roles: [], - last_login_at: '', - created_at: '', + last_login_at: null, + created_at: null, status: 'active', }, ] diff --git a/web/app/components/explore/try-app/preview/basic-app-preview.tsx b/web/app/components/explore/try-app/preview/basic-app-preview.tsx index 86ceddfef54391..f1ad3320e10752 100644 --- a/web/app/components/explore/try-app/preview/basic-app-preview.tsx +++ b/web/app/components/explore/try-app/preview/basic-app-preview.tsx @@ -3,8 +3,10 @@ import type { TrialAppDetailResponse } from '@dify/contracts/api/console/trial-a import type { FC } from 'react' import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { Collection } from '@/app/components/tools/types' import type { ModelConfig } from '@/models/debug' import type { PromptVariable } from '@/types/app' +import { useQuery } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import { clone } from 'es-toolkit/object' import * as React from 'react' @@ -16,6 +18,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { CollectionType } from '@/app/components/tools/types' +import { selectToolCollections } from '@/app/components/tools/utils/query-select' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { ANNOTATION_DEFAULT, @@ -26,7 +29,7 @@ import { import ConfigContext from '@/context/debug-configuration' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { PromptMode } from '@/models/debug' -import { useAllToolProviders } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useGetTryAppDataSets } from '@/service/use-try-app' import { AgentStrategy, ModelModeType, Resolution, TransferMethod, TtsAutoPlay } from '@/types/app' import { correctModelProvider, correctToolProvider } from '@/utils' @@ -295,7 +298,7 @@ const normalizeExternalDataToolFormItem = (item: Record) => { const normalizeAgentTool = ( tool: Record, deletedTools: TrialAppDetailResponse['deleted_tools'] | undefined, - collectionList: ReturnType['data'] | undefined, + collectionList: Collection[] | undefined, ): AgentToolItem => { const providerId = getString(tool.provider_id) const providerName = getString(tool.provider_name) @@ -330,8 +333,12 @@ const BasicAppPreview: FC = ({ appId, appDetail }) => { const media = useBreakpoints() const isMobile = media === MediaType.mobile - const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = - useAllToolProviders() + const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = useQuery( + consoleQuery.workspaces.current.toolProviders.get.queryOptions({ + input: { query: {} }, + select: selectToolCollections, + }), + ) const collectionList = collectionListFromServer?.map((item) => { return { ...item, diff --git a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx index 7aeb3adfc36e6a..8d2afb150be9b2 100644 --- a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx +++ b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx @@ -7,14 +7,23 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render as renderWithoutPricing, screen, waitFor } from '@testing-library/react' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import { toast } from '@/app/notifications' -import { getDocDownloadUrl } from '@/service/common' +import { consoleClient } from '@/service/console' import { seedFeatures } from '@/test/console/query-data' import { downloadUrl } from '@/utils/download' import Compliance from '../compliance' const onPricingUrlUpdate = vi.hoisted(() => vi.fn()) -vi.mock('@/service/common', () => ({ getDocDownloadUrl: vi.fn() })) +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + compliance: { + download: { + get: vi.fn(), + }, + }, + }, +})) vi.mock('@/service/base', () => ({ request: vi.fn(() => new Promise(() => {})), sseGeneratorPost: vi.fn(), @@ -83,7 +92,9 @@ describe('Compliance', () => { it('keeps GDPR downloadable while plan-dependent documents wait for features', async () => { queryClient.clear() - vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: 'https://example.com/gdpr.pdf' }) + vi.mocked(consoleClient.compliance.download.get).mockResolvedValue({ + url: 'https://example.com/gdpr.pdf', + }) openMenuAndRender() expect(screen.queryByText('common.compliance.soc2Type1')).not.toBeInTheDocument() expect(screen.queryByText('common.compliance.soc2Type2')).not.toBeInTheDocument() @@ -94,7 +105,12 @@ describe('Compliance', () => { ) expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() fireEvent.click(screen.getByText('common.compliance.gdpr')) - await waitFor(() => expect(getDocDownloadUrl).toHaveBeenCalledWith('GDPR')) + await waitFor(() => + expect(consoleClient.compliance.download.get).toHaveBeenCalledWith( + { query: { doc_name: 'GDPR' } }, + { context: { silent: true } }, + ), + ) expect(onPricingUrlUpdate).not.toHaveBeenCalled() }) @@ -150,7 +166,7 @@ describe('Compliance', () => { it('should trigger download mutation successfully', async () => { // Arrange const mockUrl = 'http://example.com/doc.pdf' - vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: mockUrl }) + vi.mocked(consoleClient.compliance.download.get).mockResolvedValue({ url: mockUrl }) seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } }) // Act @@ -160,7 +176,7 @@ describe('Compliance', () => { // Assert await waitFor(() => { - expect(getDocDownloadUrl).toHaveBeenCalled() + expect(consoleClient.compliance.download.get).toHaveBeenCalled() expect(downloadUrl).toHaveBeenCalledWith({ url: mockUrl }) expect(toastSuccessSpy).toHaveBeenCalledWith('common.operation.downloadSuccess') }) @@ -168,7 +184,9 @@ describe('Compliance', () => { it('should handle download mutation error', async () => { // Arrange - vi.mocked(getDocDownloadUrl).mockRejectedValue(new Error('Download failed')) + vi.mocked(consoleClient.compliance.download.get).mockRejectedValue( + new Error('Download failed'), + ) seedFeatures(queryClient, { billing: { subscription: { plan: 'team' } } }) const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -179,7 +197,7 @@ describe('Compliance', () => { // Assert await waitFor(() => { - expect(getDocDownloadUrl).toHaveBeenCalled() + expect(consoleClient.compliance.download.get).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalledWith('common.operation.downloadFailed') }) expect(consoleSpy).toHaveBeenCalled() @@ -216,7 +234,7 @@ describe('Compliance', () => { it('should show spinner and guard against duplicate download when isPending is true', async () => { // Arrange let resolveDownload: (value: { url: string }) => void - vi.mocked(getDocDownloadUrl).mockImplementation( + vi.mocked(consoleClient.compliance.download.get).mockImplementation( () => new Promise((resolve) => { resolveDownload = resolve @@ -248,7 +266,7 @@ describe('Compliance', () => { it('should not call downloadCompliance again while pending', async () => { let resolveDownload: (value: { url: string }) => void - vi.mocked(getDocDownloadUrl).mockImplementation( + vi.mocked(consoleClient.compliance.download.get).mockImplementation( () => new Promise((resolve) => { resolveDownload = resolve @@ -267,7 +285,7 @@ describe('Compliance', () => { await waitFor( () => { expect(menuItem).toHaveAttribute('aria-disabled', 'true') - expect(getDocDownloadUrl).toHaveBeenCalledTimes(1) + expect(consoleClient.compliance.download.get).toHaveBeenCalledTimes(1) }, { timeout: 10000 }, ) @@ -283,7 +301,7 @@ describe('Compliance', () => { { timeout: 10000 }, ) // getDocDownloadUrl should still have only been called once - expect(getDocDownloadUrl).toHaveBeenCalledTimes(1) + expect(consoleClient.compliance.download.get).toHaveBeenCalledTimes(1) }, 20000) }) }) diff --git a/web/app/components/header/account-dropdown/compliance.tsx b/web/app/components/header/account-dropdown/compliance.tsx index 8e3be7096916bc..fb78893fba5127 100644 --- a/web/app/components/header/account-dropdown/compliance.tsx +++ b/web/app/components/header/account-dropdown/compliance.tsx @@ -22,8 +22,7 @@ import { settingsQueryParser, } from '@/app/components/header/account-setting/query-params' import { toast } from '@/app/notifications' -import { getDocDownloadUrl } from '@/service/common' -import { consoleQuery } from '@/service/console' +import { consoleClient, consoleQuery } from '@/service/console' import { downloadUrl } from '@/utils/download' import PremiumBadge from '../../base/premium-badge' import { MenuItemContent } from './menu-item-content' @@ -117,7 +116,10 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp mutationKey: ['downloadCompliance', docName], mutationFn: async () => { try { - const ret = await getDocDownloadUrl(docName) + const ret = await consoleClient.compliance.download.get( + { query: { doc_name: docName } }, + { context: { silent: true } }, + ) downloadUrl({ url: ret.url }) toast.success(t(($) => $['operation.downloadSuccess'], { ns: 'common' })) } catch (error) { diff --git a/web/app/components/header/account-setting/__tests__/index.spec.tsx b/web/app/components/header/account-setting/__tests__/index.spec.tsx index 2b3c21bdc81751..f4f8e222f58995 100644 --- a/web/app/components/header/account-setting/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/index.spec.tsx @@ -9,6 +9,14 @@ import { renderWithConsoleQuery } from '@/test/console/query-data' import { ACCOUNT_SETTING_TAB } from '../constants' import AccountSetting from '../index' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => ({ data: { accounts: [] }, refetch: vi.fn() }), + 'auth.plugin.datasource.list.get': () => ({ data: { result: [] } }), + }) +}) + let canReplaceLogo = true const mockFeatureFlags = vi.hoisted(() => ({ NEXT_PUBLIC_ENABLE_ACCESS_TOKEN: true })) @@ -71,15 +79,10 @@ vi.mock('@/app/components/header/account-setting/model-provider-page', () => ({ ), })) -vi.mock('@/service/use-datasource', () => ({ - useGetDataSourceListAuth: vi.fn(() => ({ data: { result: [] } })), -})) - vi.mock('@/service/use-common', async (importOriginal) => { const actual = await importOriginal() return { ...actual, - useMembers: vi.fn(() => ({ data: { accounts: [] }, refetch: vi.fn() })), } }) diff --git a/web/app/components/header/account-setting/__tests__/update-setting-dialog.spec.tsx b/web/app/components/header/account-setting/__tests__/update-setting-dialog.spec.tsx index 64582efcaee0cc..40400ed8e2fc4d 100644 --- a/web/app/components/header/account-setting/__tests__/update-setting-dialog.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/update-setting-dialog.spec.tsx @@ -17,10 +17,20 @@ const { getSettings, saveSettings } = vi.hoisted(() => ({ saveSettings: vi.fn(), })) +// The console client sends through `request`; adapt it back to path + params/body calls. vi.mock('@/service/base', async (importOriginal) => ({ ...(await importOriginal()), - get: getSettings, - post: saveSettings, + request: async (url: string, _init: RequestInit, options: { request?: Request }) => { + const { pathname, searchParams } = new URL(url) + const path = pathname.replace('/console/api', '') + const data = + options.request?.method === 'POST' + ? await saveSettings(path, { body: await options.request.json() }) + : await getSettings(path, { params: Object.fromEntries(searchParams) }) + return new Response(JSON.stringify(data ?? {}), { + headers: { 'Content-Type': 'application/json' }, + }) + }, })) const savedConfig: AutoUpdateConfig = { diff --git a/web/app/components/header/account-setting/__tests__/workspace-role-checkbox-list.spec.tsx b/web/app/components/header/account-setting/__tests__/workspace-role-checkbox-list.spec.tsx index ece5abc7512cc0..f33957a5916e4d 100644 --- a/web/app/components/header/account-setting/__tests__/workspace-role-checkbox-list.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/workspace-role-checkbox-list.spec.tsx @@ -1,10 +1,18 @@ import type { Role } from '@/models/access-control' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' import WorkspaceRoleCheckboxList from '../workspace-role-checkbox-list' -vi.mock('@/service/access-control/use-workspace-roles') +const { mockWorkspaceRoleListHook } = vi.hoisted(() => ({ + mockWorkspaceRoleListHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.get': () => mockWorkspaceRoleListHook(), + }) +}) const createRole = (overrides: Partial): Role => ({ id: 'role-1', @@ -27,7 +35,7 @@ const mockRoles = [ describe('WorkspaceRoleCheckboxList', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(useWorkspaceRoleList).mockReturnValue({ + vi.mocked(mockWorkspaceRoleListHook).mockReturnValue({ data: { pages: [ { @@ -47,7 +55,7 @@ describe('WorkspaceRoleCheckboxList', () => { hasNextPage: false, isFetchingNextPage: false, fetchNextPage: vi.fn(), - } as unknown as ReturnType) + }) }) it('should render checkboxes when multiple roles are allowed', () => { @@ -156,7 +164,7 @@ describe('WorkspaceRoleCheckboxList', () => { }) it('should show legacy role descriptions when only one role is allowed', () => { - vi.mocked(useWorkspaceRoleList).mockReturnValue({ + vi.mocked(mockWorkspaceRoleListHook).mockReturnValue({ data: { pages: [ { @@ -181,7 +189,7 @@ describe('WorkspaceRoleCheckboxList', () => { hasNextPage: false, isFetchingNextPage: false, fetchNextPage: vi.fn(), - } as unknown as ReturnType) + }) render( ({ openOAuthPopup: vi.fn(), })) -vi.mock('@/service/use-datasource', () => ({ - useGetDataSourceOAuthUrl: vi.fn(), - useInvalidDataSourceAuth: vi.fn(() => vi.fn()), - useInvalidDataSourceListAuth: vi.fn(() => vi.fn()), - useInvalidDefaultDataSourceListAuth: vi.fn(() => vi.fn()), -})) +const { mockGetPluginOAuthUrl } = vi.hoisted(() => ({ mockGetPluginOAuthUrl: vi.fn() })) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'oauth.plugin.byProviderId.datasource.getAuthorizationUrl.get': () => ({ + mutateAsync: mockGetPluginOAuthUrl, + }), + }) +}) vi.mock('../hooks', () => ({ useDataSourceAuthUpdate: vi.fn(), })) type UsePluginAuthActionReturn = ReturnType -type UseGetDataSourceOAuthUrlReturn = ReturnType type UseRenderI18nObjectReturn = ReturnType describe('Card Component', () => { - const mockGetPluginOAuthUrl = vi.fn() const mockRenderI18nObjectResult = vi.fn((obj: Record) => obj.en_US) const mockInvalidateDataSourceListAuth = vi.fn() const mockInvalidDefaultDataSourceListAuth = vi.fn() @@ -161,6 +157,8 @@ describe('Card Component', () => { en_US: 'Test Description', zh_Hans: '', }, + credential_schema: [], + oauth_schema: null, credentials_list: [ { id: 'c1', @@ -181,19 +179,11 @@ describe('Card Component', () => { mockPluginAuthActionReturn = createMockPluginAuthActionReturn() vi.mocked(useDataSourceAuthUpdate).mockReturnValue({ handleAuthUpdate: mockHandleAuthUpdate }) - vi.mocked(useInvalidDataSourceListAuth).mockReturnValue(mockInvalidateDataSourceListAuth) - vi.mocked(useInvalidDefaultDataSourceListAuth).mockReturnValue( - mockInvalidDefaultDataSourceListAuth, - ) - vi.mocked(useInvalidDataSourceAuth).mockReturnValue(mockInvalidateDataSourceAuth) vi.mocked(usePluginAuthAction).mockReturnValue(mockPluginAuthActionReturn) vi.mocked(useRenderI18nObject).mockReturnValue( mockRenderI18nObjectResult as unknown as UseRenderI18nObjectReturn, ) - vi.mocked(useGetDataSourceOAuthUrl).mockReturnValue({ - mutateAsync: mockGetPluginOAuthUrl, - } as unknown as UseGetDataSourceOAuthUrlReturn) }) const expectAuthUpdated = () => { @@ -341,7 +331,9 @@ describe('Card Component', () => { // Assert await waitFor(() => { - expect(mockGetPluginOAuthUrl).toHaveBeenCalledWith('c1') + expect(mockGetPluginOAuthUrl).toHaveBeenCalledWith( + expect.objectContaining({ query: { credential_id: 'c1' } }), + ) expect(openOAuthPopup).toHaveBeenCalledWith('https://oauth.url', mockHandleAuthUpdate) }) }) @@ -366,7 +358,9 @@ describe('Card Component', () => { // Assert await waitFor(() => { - expect(mockGetPluginOAuthUrl).toHaveBeenCalledWith('c1') + expect(mockGetPluginOAuthUrl).toHaveBeenCalledWith( + expect.objectContaining({ query: { credential_id: 'c1' } }), + ) }) expect(openOAuthPopup).not.toHaveBeenCalled() }) @@ -516,7 +510,12 @@ describe('Card Component', () => { const configurableItem: DataSourceAuth = { ...mockItem, credential_schema: [ - { name: 'api_key', type: FormTypeEnum.textInput, label: 'API Key', required: true }, + { + name: 'api_key', + type: FormTypeEnum.textInput, + label: { en_US: 'API Key' }, + required: true, + }, ], } @@ -537,12 +536,27 @@ describe('Card Component', () => { const configurableItem: DataSourceAuth = { ...mockItem, credential_schema: [ - { name: 'api_key', type: FormTypeEnum.textInput, label: 'API Key', required: true }, + { + name: 'api_key', + type: FormTypeEnum.textInput, + label: { en_US: 'API Key' }, + required: true, + }, ], oauth_schema: { client_schema: [ - { name: 'oauth_key', type: FormTypeEnum.textInput, label: 'OAuth Key', required: true }, + { + name: 'oauth_key', + type: FormTypeEnum.textInput, + label: { en_US: 'OAuth Key' }, + required: true, + }, ], + credentials_schema: [], + is_oauth_custom_client_enabled: false, + is_system_oauth_params_exists: false, + oauth_custom_client_params: null, + redirect_uri: '', }, } diff --git a/web/app/components/header/account-setting/data-source-page-new/__tests__/configure.spec.tsx b/web/app/components/header/account-setting/data-source-page-new/__tests__/configure.spec.tsx index 9e12f953514c19..e9ceba8393b6d9 100644 --- a/web/app/components/header/account-setting/data-source-page-new/__tests__/configure.spec.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/__tests__/configure.spec.tsx @@ -1,5 +1,8 @@ +import type { + DatasourceOAuthSchemaResponse, + ProviderConfig, +} from '@dify/contracts/api/console/auth/types.gen' import type { DataSourceAuth } from '../types' -import type { FormSchema } from '@/app/components/base/form/types' import type { AddApiKeyButtonProps, AddOAuthButtonProps, @@ -72,9 +75,23 @@ describe('Configure Component', () => { label: { en_US: 'Test Label', zh_Hans: 'zh_hans' }, description: { en_US: 'Test Description', zh_Hans: 'zh_hans' }, credentials_list: [], + credential_schema: [], + oauth_schema: null, } - const mockFormSchema: FormSchema = { + const createOAuthSchema = ( + overrides: Partial = {}, + ): DatasourceOAuthSchemaResponse => ({ + client_schema: [], + credentials_schema: [], + is_oauth_custom_client_enabled: false, + is_system_oauth_params_exists: false, + oauth_custom_client_params: null, + redirect_uri: '', + ...overrides, + }) + + const mockFormSchema: ProviderConfig = { name: 'api_key', label: { en_US: 'API Key', zh_Hans: 'zh_hans' }, type: FormTypeEnum.textInput, @@ -133,9 +150,9 @@ describe('Configure Component', () => { // Arrange const itemWithOAuth: DataSourceAuth = { ...mockItemBase, - oauth_schema: { + oauth_schema: createOAuthSchema({ client_schema: [mockFormSchema], - }, + }), } // Act @@ -152,9 +169,9 @@ describe('Configure Component', () => { const itemWithBoth: DataSourceAuth = { ...mockItemBase, credential_schema: [mockFormSchema], - oauth_schema: { + oauth_schema: createOAuthSchema({ client_schema: [mockFormSchema], - }, + }), } // Act @@ -172,9 +189,9 @@ describe('Configure Component', () => { it('should keep the permission dialog mounted after the OAuth menu closes', () => { const itemWithOAuth: DataSourceAuth = { ...mockItemBase, - oauth_schema: { + oauth_schema: createOAuthSchema({ client_schema: [mockFormSchema], - }, + }), } render( { const itemWithBoth: DataSourceAuth = { ...mockItemBase, credential_schema: [mockFormSchema], - oauth_schema: { + oauth_schema: createOAuthSchema({ client_schema: [mockFormSchema], - }, + }), } render() @@ -243,9 +260,9 @@ describe('Configure Component', () => { const itemWithBoth: DataSourceAuth = { ...mockItemBase, credential_schema: [mockFormSchema], - oauth_schema: { + oauth_schema: createOAuthSchema({ client_schema: [mockFormSchema], - }, + }), } // Act: Open the configuration menu @@ -270,7 +287,7 @@ describe('Configure Component', () => { const itemEmpty: DataSourceAuth = { ...mockItemBase, credential_schema: [], - oauth_schema: { client_schema: [] }, + oauth_schema: createOAuthSchema({ client_schema: [] }), } // Act rerender() @@ -282,9 +299,9 @@ describe('Configure Component', () => { // Arrange (Partial OAuth schema) const itemPartialOAuth: DataSourceAuth = { ...mockItemBase, - oauth_schema: { + oauth_schema: createOAuthSchema({ is_oauth_custom_client_enabled: true, - }, + }), } // Act rerender() @@ -297,7 +314,7 @@ describe('Configure Component', () => { let count = 0 const itemWithGlitchedSchema = { ...mockItemBase, - oauth_schema: { + oauth_schema: createOAuthSchema({ get client_schema() { count++ if (count % 2 !== 0) return [mockFormSchema] @@ -307,7 +324,7 @@ describe('Configure Component', () => { is_system_oauth_params_exists: false, oauth_custom_client_params: {}, redirect_uri: '', - }, + }), } as unknown as DataSourceAuth render() diff --git a/web/app/components/header/account-setting/data-source-page-new/__tests__/index.spec.tsx b/web/app/components/header/account-setting/data-source-page-new/__tests__/index.spec.tsx index 865b2c8c9af71a..7bf37e59c9e881 100644 --- a/web/app/components/header/account-setting/data-source-page-new/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/__tests__/index.spec.tsx @@ -9,11 +9,6 @@ import { usePluginAuthAction } from '@/app/components/plugins/plugin-auth' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { useRenderI18nObject } from '@/hooks/use-i18n' import { consoleQuery } from '@/service/console' -import { - useGetDataSourceListAuth, - useGetDataSourceOAuthUrl, - useInvalidDataSourceListAuth, -} from '@/service/use-datasource' import { useCheckInstalled, useInstalledPluginList, @@ -37,11 +32,17 @@ vi.mock('@/hooks/use-i18n', () => ({ useRenderI18nObject: vi.fn(), })) -vi.mock('@/service/use-datasource', () => ({ - useGetDataSourceListAuth: vi.fn(), - useGetDataSourceOAuthUrl: vi.fn(), - useInvalidDataSourceListAuth: vi.fn(), -})) +const { mockDataSourceListAuth } = vi.hoisted(() => ({ mockDataSourceListAuth: vi.fn() })) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'auth.plugin.datasource.list.get': () => mockDataSourceListAuth(), + 'oauth.plugin.byProviderId.datasource.getAuthorizationUrl.get': () => ({ + mutateAsync: vi.fn(), + }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useCheckInstalled: vi.fn(), @@ -106,6 +107,8 @@ describe('DataSourcePage Component', () => { name: 'Dify Source', label: { en_US: 'Dify Source', zh_Hans: 'zh_hans_dify_source' }, description: { en_US: 'Dify Description', zh_Hans: 'zh_hans_dify_description' }, + credential_schema: [], + oauth_schema: null, credentials_list: [], }, { @@ -117,6 +120,8 @@ describe('DataSourcePage Component', () => { name: 'Partner Source', label: { en_US: 'Partner Source', zh_Hans: 'zh_hans_partner_source' }, description: { en_US: 'Partner Description', zh_Hans: 'zh_hans_partner_description' }, + credential_schema: [], + oauth_schema: null, credentials_list: [], }, ] @@ -180,10 +185,6 @@ describe('DataSourcePage Component', () => { typeof useTheme >) vi.mocked(useRenderI18nObject).mockReturnValue((obj) => obj?.en_US || '') - vi.mocked(useGetDataSourceOAuthUrl).mockReturnValue({ - mutateAsync: vi.fn(), - } as unknown as ReturnType) - vi.mocked(useInvalidDataSourceListAuth).mockReturnValue(vi.fn()) vi.mocked(useInstalledPluginList).mockReturnValue({ data: { plugins: [], total: 0 }, } as unknown as ReturnType) @@ -217,7 +218,7 @@ describe('DataSourcePage Component', () => { describe('Initial View Rendering', () => { it('should render an empty view when no data is available and marketplace is disabled', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: { result: [] }, isLoading: false, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) @@ -250,7 +251,7 @@ describe('DataSourcePage Component', () => { it('should show data source placeholders while the list is loading', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: undefined, isLoading: true, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) @@ -272,7 +273,7 @@ describe('DataSourcePage Component', () => { describe('Data Source List Rendering', () => { it('should render Card components for each data source returned from the API', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: { result: mockProviders }, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) @@ -289,7 +290,7 @@ describe('DataSourcePage Component', () => { it('should map installed plugin operations onto the data source card header', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: { result: mockProviders }, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) vi.mocked(useInstalledPluginList).mockReturnValue({ @@ -315,7 +316,7 @@ describe('DataSourcePage Component', () => { it('should filter installed data sources and pass search text to marketplace', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: { result: mockProviders }, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) @@ -334,7 +335,7 @@ describe('DataSourcePage Component', () => { }) it('announces the filtered installed data source count while searching', async () => { - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: { result: mockProviders }, isLoading: false, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) @@ -359,7 +360,7 @@ describe('DataSourcePage Component', () => { describe('Marketplace Integration', () => { it('should render the InstallFromMarketplace component when enable_marketplace feature is enabled', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: { result: mockProviders }, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) @@ -375,7 +376,7 @@ describe('DataSourcePage Component', () => { it('should pass an empty array to InstallFromMarketplace if data result is missing but marketplace is enabled', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: undefined, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) @@ -390,7 +391,7 @@ describe('DataSourcePage Component', () => { it('should handle the case where data exists but result is an empty array', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: { result: [] }, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) @@ -406,7 +407,7 @@ describe('DataSourcePage Component', () => { it('should handle the case where enable_marketplace is false (edge case for coverage)', () => { // Arrange - vi.mocked(useGetDataSourceListAuth).mockReturnValue({ + mockDataSourceListAuth.mockReturnValue({ data: { result: [] }, } as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>) diff --git a/web/app/components/header/account-setting/data-source-page-new/__tests__/install-from-marketplace.spec.tsx b/web/app/components/header/account-setting/data-source-page-new/__tests__/install-from-marketplace.spec.tsx index 527b904c13e2e6..e802e8a3e289c3 100644 --- a/web/app/components/header/account-setting/data-source-page-new/__tests__/install-from-marketplace.spec.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/__tests__/install-from-marketplace.spec.tsx @@ -84,6 +84,8 @@ describe('InstallFromMarketplace Component', () => { name: 'name', label: { en_US: 'Label', zh_Hans: '标签' }, description: { en_US: 'Desc', zh_Hans: '描述' }, + credential_schema: [], + oauth_schema: null, credentials_list: [], }, ] diff --git a/web/app/components/header/account-setting/data-source-page-new/__tests__/utils.spec.ts b/web/app/components/header/account-setting/data-source-page-new/__tests__/utils.spec.ts new file mode 100644 index 00000000000000..1e5dd96889f463 --- /dev/null +++ b/web/app/components/header/account-setting/data-source-page-new/__tests__/utils.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vite-plus/test' +import { providerConfigToFormSchema } from '../utils' + +describe('providerConfigToFormSchema', () => { + it('should map provider configs to form schemas with en_US fallbacks', () => { + expect( + providerConfigToFormSchema({ + name: 'api_key', + type: 'secret-input', + label: { en_US: 'API Key', zh_Hans: null, ja_JP: 'APIキー' }, + help: null, + url: null, + options: [{ label: { en_US: 'One' }, value: '1' }], + }), + ).toEqual({ + name: 'api_key', + type: 'secret-input', + label: { en_US: 'API Key', zh_Hans: 'API Key', ja_JP: 'APIキー' }, + required: false, + multiple: undefined, + default: undefined, + scope: undefined, + url: undefined, + help: undefined, + placeholder: undefined, + options: [{ label: { en_US: 'One', zh_Hans: 'One' }, value: '1' }], + }) + }) + + it('should fall back to the config name when no label is declared', () => { + expect(providerConfigToFormSchema({ name: 'region', type: 'text-input' }).label).toBe('region') + }) +}) diff --git a/web/app/components/header/account-setting/data-source-page-new/card.tsx b/web/app/components/header/account-setting/data-source-page-new/card.tsx index 6352515e4aa937..a285438c32e650 100644 --- a/web/app/components/header/account-setting/data-source-page-new/card.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/card.tsx @@ -8,7 +8,8 @@ import { AlertDialogFooter, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' -import { memo, useCallback, useRef } from 'react' +import { useMutation } from '@tanstack/react-query' +import { memo, useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { ApiKeyModal, usePluginAuthAction } from '@/app/components/plugins/plugin-auth' @@ -17,11 +18,12 @@ import { CollectionType } from '@/app/components/tools/types' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import { useRenderI18nObject } from '@/hooks/use-i18n' import { openOAuthPopup } from '@/hooks/use-oauth' -import { useGetDataSourceOAuthUrl } from '@/service/use-datasource' +import { consoleQuery } from '@/service/console' import Configure from './configure' import { useDataSourceAuthUpdate } from './hooks' import Item from './item' import DataSourcePluginActions from './plugin-actions' +import { providerConfigToFormSchema } from './utils' const getPluginVersion = (uniqueIdentifier?: string) => { return uniqueIdentifier?.match(/:([^:@]+)@/)?.[1] @@ -39,6 +41,10 @@ const Card = ({ item, disabled, pluginDetail, onPluginUpdate }: CardProps) => { const renderI18nObject = useRenderI18nObject() const { icon, label, credentials_list, credential_schema } = item const providerLabel = renderI18nObject(label) + const credentialFormSchemas = useMemo( + () => credential_schema.map(providerConfigToFormSchema), + [credential_schema], + ) const fallbackPluginVersion = getPluginVersion(item.plugin_unique_identifier) const pluginPayload = { category: AuthCategory.datasource, @@ -64,9 +70,14 @@ const Card = ({ item, disabled, pluginDetail, onPluginUpdate }: CardProps) => { pendingOperationCredentialId, } = usePluginAuthAction(pluginPayload, handleAuthUpdate) const changeCredentialIdRef = useRef(undefined) - const { mutateAsync: getPluginOAuthUrl } = useGetDataSourceOAuthUrl(pluginPayload.provider) + const { mutateAsync: getPluginOAuthUrl } = useMutation( + consoleQuery.oauth.plugin.byProviderId.datasource.getAuthorizationUrl.get.mutationOptions(), + ) const handleOAuth = useCallback(async () => { - const { authorization_url } = await getPluginOAuthUrl(changeCredentialIdRef.current) + const { authorization_url } = await getPluginOAuthUrl({ + params: { provider_id: pluginPayload.provider }, + query: { credential_id: changeCredentialIdRef.current }, + }) if (authorization_url) { openOAuthPopup(authorization_url, handleAuthUpdate) @@ -198,7 +209,7 @@ const Card = ({ item, disabled, pluginDetail, onPluginUpdate }: CardProps) => { pendingOperationCredentialId.current = null }} onUpdate={handleAuthUpdate} - formSchemas={credential_schema} + formSchemas={credentialFormSchemas} editValues={editValues} onRemove={handleRemove} disabled={disabled || doingAction || !canManageCredential} diff --git a/web/app/components/header/account-setting/data-source-page-new/configure.tsx b/web/app/components/header/account-setting/data-source-page-new/configure.tsx index 40923c8f62c6ba..9c9334dc319a19 100644 --- a/web/app/components/header/account-setting/data-source-page-new/configure.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/configure.tsx @@ -11,6 +11,7 @@ import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { AddApiKeyButton, AddOAuthButton } from '@/app/components/plugins/plugin-auth' import { useRenderI18nObject } from '@/hooks/use-i18n' +import { providerConfigToFormSchema } from './utils' type ConfigureProps = { item: DataSourceAuth @@ -23,9 +24,13 @@ const Configure = ({ item, pluginPayload, onUpdate, disabled }: ConfigureProps) const renderI18nObject = useRenderI18nObject() const providerLabel = renderI18nObject(item.label) const [open, setOpen] = useState(false) - const canApiKey = item.credential_schema?.length - const oAuthData = item.oauth_schema || {} - const canOAuth = oAuthData.client_schema?.length + const canApiKey = item.credential_schema.length + const credentialFormSchemas = useMemo( + () => item.credential_schema.map(providerConfigToFormSchema), + [item.credential_schema], + ) + const oAuthData = item.oauth_schema + const canOAuth = oAuthData?.client_schema.length const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => { return { buttonText: t(($) => $['auth.addOAuth'], { ns: 'plugin' }), @@ -78,7 +83,7 @@ const Configure = ({ item, pluginPayload, onUpdate, disabled }: ConfigureProps) {!!canApiKey && ( @@ -89,17 +94,17 @@ const Configure = ({ item, pluginPayload, onUpdate, disabled }: ConfigureProps) ) } - if (!canOAuth) return renderConfigurePopover() + if (!oAuthData || !canOAuth) return renderConfigurePopover() return ( ({ auth: vi.fn(), list: vi.fn(), defaults: vi.fn() })) -vi.mock('@/service/use-datasource', () => ({ - useInvalidDataSourceAuth: () => mocks.auth, - useInvalidDataSourceListAuth: () => mocks.list, - useInvalidDefaultDataSourceListAuth: () => mocks.defaults, -})) - describe('datasource authentication reconciliation', () => { let client: QueryClient const catalogKey = consoleQuery.rag.pipelines.datasourcePlugins.get.queryKey() const unrelatedKey = consoleQuery.account.profile.get.key() + const datasource = consoleQuery.auth.plugin.datasource + const authKeys = [ + datasource.list.get.queryKey(), + datasource.defaultList.get.queryKey(), + datasource.byProviderId.get.queryKey({ + input: { params: { provider_id: 'langgenius/notion/notion' } }, + }), + ] + const otherProviderKey = datasource.byProviderId.get.queryKey({ + input: { params: { provider_id: 'langgenius/firecrawl/firecrawl' } }, + }) const wrapper = ({ children }: PropsWithChildren) => createElement(QueryClientProvider, { client }, children) beforeEach(() => { @@ -24,6 +28,7 @@ describe('datasource authentication reconciliation', () => { client = new QueryClient() client.setQueryData(catalogKey, []) client.setQueryData(unrelatedKey, { cached: true }) + for (const key of [...authKeys, otherProviderKey]) client.setQueryData(key, { result: [] }) }) afterEach(() => client.clear()) @@ -41,8 +46,7 @@ describe('datasource authentication reconciliation', () => { expect(client.getQueryState(catalogKey)?.isInvalidated).toBe(true) expect(client.getQueryState(unrelatedKey)?.isInvalidated).toBe(false) - expect(mocks.auth).toHaveBeenCalledOnce() - expect(mocks.list).toHaveBeenCalledOnce() - expect(mocks.defaults).toHaveBeenCalledOnce() + for (const key of authKeys) expect(client.getQueryState(key)?.isInvalidated).toBe(true) + expect(client.getQueryState(otherProviderKey)?.isInvalidated).toBe(false) }) }) diff --git a/web/app/components/header/account-setting/data-source-page-new/hooks/use-data-source-auth-update.ts b/web/app/components/header/account-setting/data-source-page-new/hooks/use-data-source-auth-update.ts index d78a317711b47d..42ff99bcf8fab1 100644 --- a/web/app/components/header/account-setting/data-source-page-new/hooks/use-data-source-auth-update.ts +++ b/web/app/components/header/account-setting/data-source-page-new/hooks/use-data-source-auth-update.ts @@ -1,11 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { useCallback } from 'react' import { consoleQuery } from '@/service/console' -import { - useInvalidDataSourceAuth, - useInvalidDataSourceListAuth, - useInvalidDefaultDataSourceListAuth, -} from '@/service/use-datasource' export const useDataSourceAuthUpdate = ({ pluginId, @@ -15,25 +10,22 @@ export const useDataSourceAuthUpdate = ({ provider: string }) => { const queryClient = useQueryClient() - const invalidateDataSourceListAuth = useInvalidDataSourceListAuth() - const invalidDefaultDataSourceListAuth = useInvalidDefaultDataSourceListAuth() - const invalidateDataSourceAuth = useInvalidDataSourceAuth({ - pluginId, - provider, - }) const handleAuthUpdate = useCallback(() => { - invalidateDataSourceListAuth() - invalidDefaultDataSourceListAuth() + queryClient.invalidateQueries({ + queryKey: consoleQuery.auth.plugin.datasource.list.get.queryKey(), + }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.auth.plugin.datasource.defaultList.get.queryKey(), + }) queryClient.invalidateQueries({ queryKey: consoleQuery.rag.pipelines.datasourcePlugins.get.key(), }) - invalidateDataSourceAuth() - }, [ - invalidateDataSourceListAuth, - queryClient, - invalidateDataSourceAuth, - invalidDefaultDataSourceListAuth, - ]) + queryClient.invalidateQueries({ + queryKey: consoleQuery.auth.plugin.datasource.byProviderId.get.queryKey({ + input: { params: { provider_id: `${pluginId}/${provider}` } }, + }), + }) + }, [pluginId, provider, queryClient]) return { handleAuthUpdate, diff --git a/web/app/components/header/account-setting/data-source-page-new/index.tsx b/web/app/components/header/account-setting/data-source-page-new/index.tsx index b9e6db7d53bc59..c0e10dccd14b93 100644 --- a/web/app/components/header/account-setting/data-source-page-new/index.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/index.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { memo, useCallback, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' @@ -11,7 +11,6 @@ import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/ta import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRenderI18nObject } from '@/hooks/use-i18n' import { consoleQuery } from '@/service/console' -import { useGetDataSourceListAuth, useInvalidDataSourceListAuth } from '@/service/use-datasource' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import { UpdateSettingDialog } from '../update-setting-dialog' import Card from './card' @@ -63,13 +62,14 @@ const DataSourcePage = ({ layout, onOpenMarketplace, stickyToolbar }: DataSource ...systemFeaturesQueryOptions(), select: (s) => s.enable_marketplace, }) - const { data, isLoading: isDataSourceListLoading } = useGetDataSourceListAuth() + const { data, isLoading: isDataSourceListLoading } = useQuery( + consoleQuery.auth.plugin.datasource.list.get.queryOptions(), + ) const { data: installedPluginList } = useInstalledPluginList({ category: PluginCategoryEnum.datasource, }) const pluginListWithLatestVersion = usePluginsWithLatestVersion(installedPluginList?.plugins) const invalidateInstalledPluginList = useInvalidateInstalledPluginList() - const invalidateDataSourceListAuth = useInvalidDataSourceListAuth() const dataSources = useMemo(() => data?.result ?? [], [data?.result]) const dataSourcePluginDetails = useMemo(() => { return pluginListWithLatestVersion.filter( @@ -96,11 +96,13 @@ const DataSourcePage = ({ layout, onOpenMarketplace, stickyToolbar }: DataSource }, [dataSources, renderI18nObject, searchText]) const handlePluginUpdate = useCallback(() => { invalidateInstalledPluginList() - invalidateDataSourceListAuth() + queryClient.invalidateQueries({ + queryKey: consoleQuery.auth.plugin.datasource.list.get.queryKey(), + }) queryClient.invalidateQueries({ queryKey: consoleQuery.rag.pipelines.datasourcePlugins.get.key(), }) - }, [queryClient, invalidateDataSourceListAuth, invalidateInstalledPluginList]) + }, [queryClient, invalidateInstalledPluginList]) const toolbar = (
- type: CredentialTypeEnum - name: string - id: string - is_default: boolean - avatar_url: string -} -export type DataSourceAuth = { - author: string - provider: string - plugin_id: string - plugin_unique_identifier: string - icon: string - name: string - label: TypeWithI18N - description: TypeWithI18N - credential_schema?: FormSchema[] - oauth_schema?: { - client_schema?: FormSchema[] - credentials_schema?: FormSchema[] - is_oauth_custom_client_enabled?: boolean - is_system_oauth_params_exists?: boolean - oauth_custom_client_params?: Record - redirect_uri?: string - } - credentials_list: DataSourceCredential[] -} +export type DataSourceCredential = DatasourceCredentialResponse +export type DataSourceAuth = DatasourceProviderAuthResponse diff --git a/web/app/components/header/account-setting/data-source-page-new/utils.ts b/web/app/components/header/account-setting/data-source-page-new/utils.ts new file mode 100644 index 00000000000000..ba85f1f1ed3705 --- /dev/null +++ b/web/app/components/header/account-setting/data-source-page-new/utils.ts @@ -0,0 +1,27 @@ +import type { I18nObject, ProviderConfig } from '@dify/contracts/api/console/auth/types.gen' +import type { FormOption, FormSchema, TypeWithI18N } from '@/app/components/base/form/types' + +// Untranslated locales fall back to en_US, matching renderI18nObject. +const toTypeWithI18N = ({ en_US, ja_JP, pt_BR, zh_Hans }: I18nObject): TypeWithI18N => ({ + en_US, + zh_Hans: zh_Hans ?? en_US, + ...(ja_JP != null && { ja_JP }), + ...(pt_BR != null && { pt_BR }), +}) + +export const providerConfigToFormSchema = (config: ProviderConfig): FormSchema => ({ + type: config.type, + name: config.name, + label: config.label ? toTypeWithI18N(config.label) : config.name, + required: config.required ?? false, + multiple: config.multiple, + default: config.default ?? undefined, + scope: config.scope ?? undefined, + url: config.url ?? undefined, + help: config.help ? toTypeWithI18N(config.help) : undefined, + placeholder: config.placeholder ? toTypeWithI18N(config.placeholder) : undefined, + options: config.options?.map((option): FormOption => ({ + label: toTypeWithI18N(option.label), + value: option.value, + })), +}) diff --git a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx index f9e14b93d39722..cbe621fd653d9a 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx @@ -6,12 +6,26 @@ import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vite-plus/test' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' -import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' -import { useMembers } from '@/service/use-common' import { renderWithConsoleQuery } from '@/test/console/query-data' import MembersPage from '../index' +const { mockMembersHook, mockWorkspaceRoleListHook, mockUpdateRolesOfMemberHook } = vi.hoisted( + () => ({ + mockMembersHook: vi.fn(), + mockWorkspaceRoleListHook: vi.fn(), + mockUpdateRolesOfMemberHook: vi.fn(), + }), +) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => mockMembersHook(), + 'workspaces.current.rbac.roles.get': () => mockWorkspaceRoleListHook(), + 'workspaces.current.rbac.members.byMemberId.rbacRoles.put': () => mockUpdateRolesOfMemberHook(), + }) +}) + let deploymentEdition: 'CLOUD' | 'COMMUNITY' = 'COMMUNITY' let memberFeatures: ConsoleQueryTestOptions['features'] = {} @@ -30,9 +44,6 @@ vi.mock('@/context/permission-state', async () => { }) vi.mock('@/hooks/use-format-time-from-now') -vi.mock('@/service/access-control/use-member-roles') -vi.mock('@/service/use-common') -vi.mock('@/service/access-control/use-workspace-roles') const { inviteMember } = vi.hoisted(() => ({ inviteMember: vi.fn() })) vi.mock('@/service/console', async (importOriginal) => { const actual = await importOriginal() @@ -44,7 +55,10 @@ vi.mock('@/service/console', async (importOriginal) => { return { current: { summary: target.workspaces.current.summary, + permission: target.workspaces.current.permission, + rbac: target.workspaces.current.rbac, members: { + get: target.workspaces.current.members.get, inviteEmail: { post: { mutationOptions: ( @@ -187,9 +201,9 @@ describe('MembersPage', () => { avatar: '', avatar_url: '', role: 'owner', - last_active_at: '1731000000', - last_login_at: '1731000000', - created_at: '1731000000', + last_active_at: 1731000000, + last_login_at: 1731000000, + created_at: 1731000000, status: 'active', roles: [createRole({ id: 'owner-role', name: 'Owner', is_builtin: true, role_tag: 'owner' })], }, @@ -200,9 +214,9 @@ describe('MembersPage', () => { avatar: '', avatar_url: '', role: 'admin', - last_active_at: '1731000000', - last_login_at: '1731000000', - created_at: '1731000000', + last_active_at: 1731000000, + last_login_at: 1731000000, + created_at: 1731000000, status: 'active', roles: [createRole({ id: 'admin-role', name: 'Admin', is_builtin: true })], }, @@ -219,17 +233,17 @@ describe('MembersPage', () => { workspacePermissionKeys: ['workspace.member.manage'], } as unknown as ConsoleStateFixture) - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: mockAccounts }, refetch: mockRefetch, - } as unknown as ReturnType) + }) mockUpdateRolesOfMember.mockImplementation((_payload, options) => { options?.onSuccess?.() return Promise.resolve() }) - vi.mocked(useUpdateRolesOfMember).mockReturnValue({ + vi.mocked(mockUpdateRolesOfMemberHook).mockReturnValue({ mutateAsync: mockUpdateRolesOfMember, - } as unknown as ReturnType) + }) inviteMember.mockResolvedValue({ result: 'success', @@ -238,14 +252,14 @@ describe('MembersPage', () => { { email: 'sent@example.com', status: 'success', url: 'http://invite/link' }, ], }) - vi.mocked(useWorkspaceRoleList).mockReturnValue({ + vi.mocked(mockWorkspaceRoleListHook).mockReturnValue({ data: { pages: [{ data: [createRole({ id: 'admin', name: 'Admin' })] }] }, isLoading: false, error: null, hasNextPage: false, isFetchingNextPage: false, fetchNextPage: vi.fn(), - } as unknown as ReturnType) + }) deploymentEdition = 'COMMUNITY' memberFeatures = { ...memberFeatures, is_allow_transfer_workspace: true } @@ -400,10 +414,10 @@ describe('MembersPage', () => { ...mockAccounts[1]!, status: 'pending', } - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [mockAccounts[0], pendingAccount] }, refetch: mockRefetch, - } as unknown as ReturnType) + }) renderMembersPage() @@ -475,7 +489,7 @@ describe('MembersPage', () => { isCurrentWorkspaceManager: true, workspacePermissionKeys: ['workspace.member.manage'], } as unknown as ConsoleStateFixture) - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [ mockAccounts[0], @@ -511,7 +525,7 @@ describe('MembersPage', () => { ], }, refetch: mockRefetch, - } as unknown as ReturnType) + }) renderMembersPage() @@ -525,13 +539,13 @@ describe('MembersPage', () => { it('should use created_at as fallback when last_active_at is empty', () => { const memberNoLastActive: Member = { ...mockAccounts[1]!, - last_active_at: '', - created_at: '1700000000', + last_active_at: null, + created_at: 1700000000, } - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [memberNoLastActive] }, refetch: mockRefetch, - } as unknown as ReturnType) + }) renderMembersPage() @@ -539,10 +553,10 @@ describe('MembersPage', () => { }) it('should not show plural s when only one account in billing layout', () => { - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [mockAccounts[0]] }, refetch: mockRefetch, - } as unknown as ReturnType) + }) deploymentEdition = 'CLOUD' memberFeatures = { billing: { subscription: { plan: 'sandbox' } }, @@ -556,10 +570,10 @@ describe('MembersPage', () => { }) it('should not show plural s when only one account in non-billing layout', () => { - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [mockAccounts[0]] }, refetch: mockRefetch, - } as unknown as ReturnType) + }) renderMembersPage() @@ -574,10 +588,10 @@ describe('MembersPage', () => { isCurrentWorkspaceOwner: false, isCurrentWorkspaceManager: false, } as unknown as ConsoleStateFixture) - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [{ ...mockAccounts[1], role: 'unknown_role' as Member['role'] }] }, refetch: mockRefetch, - } as unknown as ReturnType) + }) renderMembersPage() @@ -659,8 +673,8 @@ describe('MembersPage', () => { expect(mockUpdateRolesOfMember).toHaveBeenCalledWith( { - memberId: '2', - roleIds: ['role-next'], + params: { member_id: '2' }, + body: { role_ids: ['role-next'] }, }, expect.any(Object), ) @@ -686,8 +700,8 @@ describe('MembersPage', () => { expect(mockUpdateRolesOfMember).toHaveBeenCalledWith( { - memberId: '2', - roleIds: ['role-next', 'role-extra'], + params: { member_id: '2' }, + body: { role_ids: ['role-next', 'role-extra'] }, }, expect.any(Object), ) diff --git a/web/app/components/header/account-setting/members-page/__tests__/invite-button.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/invite-button.spec.tsx index 880b820d3d38f8..0aaa3892c39001 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/invite-button.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/invite-button.spec.tsx @@ -1,8 +1,8 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture' import { screen } from '@testing-library/react' import { vi } from 'vite-plus/test' -import { useWorkspacePermissions } from '@/service/use-workspace' -import { renderWithConsoleQuery } from '@/test/console/query-data' +import { consoleQuery } from '@/service/console' +import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import { InviteButton } from '../invite-button' const mockConsoleStateReader = vi.hoisted(() => vi.fn()) @@ -14,9 +14,8 @@ vi.mock('@/context/workspace-state', async () => { })) }) -vi.mock('@/service/use-workspace') - describe('InviteButton', () => { + let queryClient = createConsoleQueryClient() const setupPermissions = ({ isFetching, allowInvite, @@ -24,14 +23,22 @@ describe('InviteButton', () => { isFetching: boolean allowInvite?: boolean }) => { - vi.mocked(useWorkspacePermissions).mockReturnValue({ - data: allowInvite === undefined ? null : { allow_member_invite: allowInvite }, - isFetching, - } as unknown as ReturnType) + queryClient = createConsoleQueryClient() + const { queryKey } = consoleQuery.workspaces.current.permission.get.queryOptions() + if (isFetching) { + void queryClient.query({ queryKey, queryFn: () => new Promise(() => {}) }) + return + } + queryClient.setQueryData(queryKey, { + workspace_id: 'workspace-id', + allow_member_invite: allowInvite ?? false, + allow_owner_transfer: false, + }) } const renderInviteButton = (brandingEnabled: boolean) => renderWithConsoleQuery(, { + queryClient, systemFeatures: { branding: { enabled: brandingEnabled } }, }) diff --git a/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx index 2660b6e7a68394..b37176d71464cf 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/member-menu.spec.tsx @@ -3,17 +3,36 @@ import type { Member } from '@/models/common' import { QueryClient } from '@tanstack/react-query' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' -import { deleteMemberOrCancelInvitation } from '@/service/common' -import { commonQueryKeys } from '@/service/use-common' +import { consoleClient, consoleQuery } from '@/service/console' import { renderWithConsoleQuery } from '@/test/console/query-data' import MemberMenu from '../member-menu' -vi.mock('@/service/access-control/use-member-roles') -vi.mock('@/service/access-control/use-workspace-roles') -vi.mock('@/service/common', () => ({ - deleteMemberOrCancelInvitation: vi.fn(), +const { mockWorkspaceRoleListHook, mockUpdateRolesOfMemberHook } = vi.hoisted(() => ({ + mockWorkspaceRoleListHook: vi.fn(), + mockUpdateRolesOfMemberHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.get': () => mockWorkspaceRoleListHook(), + 'workspaces.current.rbac.members.byMemberId.rbacRoles.put': () => mockUpdateRolesOfMemberHook(), + }) +}) + +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + workspaces: { + current: { + members: { + byMemberId: { + delete: vi.fn(), + }, + }, + }, + }, + }, })) vi.mock('@/app/notifications', () => ({ toast: { @@ -47,9 +66,9 @@ const member: Member = { avatar_url: '', role: 'normal', roles: [roles[0]!], - last_active_at: '1731000000', - last_login_at: '1731000000', - created_at: '1731000000', + last_active_at: 1731000000, + last_login_at: 1731000000, + created_at: 1731000000, status: 'active', } @@ -72,11 +91,14 @@ describe('MemberMenu', () => { beforeEach(() => { vi.clearAllMocks() mockUpdateRolesOfMember.mockResolvedValue(undefined) - vi.mocked(deleteMemberOrCancelInvitation).mockResolvedValue({ result: 'success' }) - vi.mocked(useUpdateRolesOfMember).mockReturnValue({ + vi.mocked(consoleClient.workspaces.current.members.byMemberId.delete).mockResolvedValue({ + result: 'success', + tenant_id: 'tenant-1', + }) + vi.mocked(mockUpdateRolesOfMemberHook).mockReturnValue({ mutateAsync: mockUpdateRolesOfMember, - } as unknown as ReturnType) - vi.mocked(useWorkspaceRoleList).mockReturnValue({ + }) + vi.mocked(mockWorkspaceRoleListHook).mockReturnValue({ data: { pages: [ { @@ -96,7 +118,7 @@ describe('MemberMenu', () => { hasNextPage: false, isFetchingNextPage: false, fetchNextPage: vi.fn(), - } as unknown as ReturnType) + }) }) it('should show edit role copy when multiple roles are disabled', async () => { @@ -146,8 +168,8 @@ describe('MemberMenu', () => { expect(mockUpdateRolesOfMember).toHaveBeenCalledWith( { - memberId: 'member-1', - roleIds: ['role-2'], + params: { member_id: 'member-1' }, + body: { role_ids: ['role-2'] }, }, expect.any(Object), ) @@ -156,7 +178,9 @@ describe('MemberMenu', () => { it('should require confirmation before removing a member', async () => { const user = userEvent.setup() const queryClient = createQueryClient() - const membersQueryKey = [...commonQueryKeys.members, 'en-US'] + const { queryKey: membersQueryKey } = consoleQuery.workspaces.current.members.get.queryOptions({ + input: { query: { language: 'en' } }, + }) queryClient.setQueryData(membersQueryKey, { accounts: [member] }) renderWithConsoleQuery(, { @@ -170,13 +194,13 @@ describe('MemberMenu', () => { name: /workspaceMembers\.members\.removeFromTeamConfirmTitle:\{"memberName":"Member User"\}/i, }) expect(dialog).toHaveTextContent('workspaceMembers.members.removeFromTeamConfirmDescription') - expect(deleteMemberOrCancelInvitation).not.toHaveBeenCalled() + expect(consoleClient.workspaces.current.members.byMemberId.delete).not.toHaveBeenCalled() await user.click(within(dialog).getByRole('button', { name: /common\.operation\.confirm/i })) await waitFor(() => { - expect(deleteMemberOrCancelInvitation).toHaveBeenCalledWith({ - url: '/workspaces/current/members/member-1', + expect(consoleClient.workspaces.current.members.byMemberId.delete).toHaveBeenCalledWith({ + params: { member_id: 'member-1' }, }) }) expect(queryClient.getQueryState(membersQueryKey)?.isInvalidated).toBe(true) diff --git a/web/app/components/header/account-setting/members-page/__tests__/member-row.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/member-row.spec.tsx index ff3d8949b8da31..7a67759b3c5bd6 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/member-row.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/member-row.spec.tsx @@ -2,12 +2,21 @@ import type { Member } from '@/models/common' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' -import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' import { renderWithConsoleQuery } from '@/test/console/query-data' import MemberRow from '../member-row' +const { mockUpdateRolesOfMemberHook } = vi.hoisted(() => ({ + mockUpdateRolesOfMemberHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.members.byMemberId.rbacRoles.put': () => mockUpdateRolesOfMemberHook(), + }) +}) + vi.mock('@/hooks/use-format-time-from-now') -vi.mock('@/service/access-control/use-member-roles') const member: Member = { id: 'member-1', @@ -17,9 +26,9 @@ const member: Member = { avatar_url: '', role: 'normal', roles: [], - last_active_at: '1731000000', - last_login_at: '1731000000', - created_at: '1731000000', + last_active_at: 1731000000, + last_login_at: 1731000000, + created_at: 1731000000, status: 'active', } @@ -49,9 +58,9 @@ describe('MemberRow details entry points', () => { vi.mocked(useFormatTimeFromNow).mockReturnValue({ formatTimeFromNow: () => 'just now', }) - vi.mocked(useUpdateRolesOfMember).mockReturnValue({ + vi.mocked(mockUpdateRolesOfMemberHook).mockReturnValue({ mutateAsync: vi.fn(), - } as unknown as ReturnType) + }) }) it.each([ diff --git a/web/app/components/header/account-setting/members-page/assign-roles-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/assign-roles-modal/__tests__/index.spec.tsx index b9ba534e9d620b..be583d4e004494 100644 --- a/web/app/components/header/account-setting/members-page/assign-roles-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/assign-roles-modal/__tests__/index.spec.tsx @@ -1,10 +1,18 @@ import type { Role } from '@/models/access-control' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' import AssignRolesModal from '../index' -vi.mock('@/service/access-control/use-workspace-roles') +const { mockWorkspaceRoleListHook } = vi.hoisted(() => ({ + mockWorkspaceRoleListHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.get': () => mockWorkspaceRoleListHook(), + }) +}) const createRole = (overrides: Partial): Role => ({ id: 'role-1', @@ -27,7 +35,7 @@ const roles = [ describe('AssignRolesModal', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(useWorkspaceRoleList).mockReturnValue({ + vi.mocked(mockWorkspaceRoleListHook).mockReturnValue({ data: { pages: [ { @@ -47,7 +55,7 @@ describe('AssignRolesModal', () => { hasNextPage: false, isFetchingNextPage: false, fetchNextPage: vi.fn(), - } as unknown as ReturnType) + }) }) describe('Role selection', () => { diff --git a/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx index 3c2e99d19a7fa4..ebaa050c66afb6 100644 --- a/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx @@ -2,7 +2,7 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vite-plus/test' -import { updateWorkspaceInfo } from '@/service/common' +import { consoleClient } from '@/service/console' import { render } from '@/test/console/render' import EditWorkspaceModal from '../index' @@ -21,7 +21,16 @@ vi.mock('@/context/workspace-state', async () => { return createWorkspaceStateModuleMock(() => mockConsoleState.current) }) -vi.mock('@/service/common') +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + workspaces: { + info: { + post: vi.fn(), + }, + }, + }, +})) vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => toastMocks.mockNotify(args), @@ -92,7 +101,7 @@ describe('EditWorkspaceModal', () => { assign: mockAssign, origin: 'http://localhost', }) - vi.mocked(updateWorkspaceInfo).mockResolvedValue({ + vi.mocked(consoleClient.workspaces.info.post).mockResolvedValue({ result: 'success', tenant: { id: 'workspace-id' }, }) @@ -105,8 +114,7 @@ describe('EditWorkspaceModal', () => { await user.click(getSaveButton()) await waitFor(() => { - expect(updateWorkspaceInfo).toHaveBeenCalledWith({ - url: '/workspaces/info', + expect(consoleClient.workspaces.info.post).toHaveBeenCalledWith({ body: { name: 'Renamed Workspace' }, }) expect(mockAssign).toHaveBeenCalledWith('http://localhost') @@ -118,7 +126,7 @@ describe('EditWorkspaceModal', () => { it('should expose the saving label and prevent duplicate form submission', async () => { const user = userEvent.setup() let rejectUpdate!: (reason: Error) => void - vi.mocked(updateWorkspaceInfo).mockImplementationOnce( + vi.mocked(consoleClient.workspaces.info.post).mockImplementationOnce( () => new Promise((_, reject) => { rejectUpdate = reject @@ -134,7 +142,7 @@ describe('EditWorkspaceModal', () => { const savingButton = screen.getByRole('button', { name: /operation\.saving/i }) expect(savingButton).toHaveAttribute('aria-disabled', 'true') fireEvent.submit(savingButton.closest('form')!) - expect(updateWorkspaceInfo).toHaveBeenCalledOnce() + expect(consoleClient.workspaces.info.post).toHaveBeenCalledOnce() rejectUpdate(new Error('update failed')) await waitFor(() => { @@ -145,7 +153,7 @@ describe('EditWorkspaceModal', () => { it('should show error toast when update fails', async () => { const user = userEvent.setup() - vi.mocked(updateWorkspaceInfo).mockRejectedValue(new Error('update failed')) + vi.mocked(consoleClient.workspaces.info.post).mockRejectedValue(new Error('update failed')) renderModal() @@ -193,7 +201,7 @@ describe('EditWorkspaceModal', () => { fireEvent.submit(form!) - expect(updateWorkspaceInfo).not.toHaveBeenCalled() + expect(consoleClient.workspaces.info.post).not.toHaveBeenCalled() expect(mockNotify).not.toHaveBeenCalled() }) diff --git a/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx b/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx index 8fc680ee9876f0..de8e53c30d0ee9 100644 --- a/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx @@ -9,7 +9,7 @@ import { useId, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state' -import { updateWorkspaceInfo } from '@/service/common' +import { consoleClient } from '@/service/console' type IEditWorkspaceModalProps = { onCancel: () => void @@ -38,8 +38,7 @@ const EditWorkspaceModal = ({ onCancel }: IEditWorkspaceModalProps) => { if (isSubmitting || isSaveUnavailable) return setIsSubmitting(true) try { - await updateWorkspaceInfo({ - url: '/workspaces/info', + await consoleClient.workspaces.info.post({ body: { name: normalizedName, }, diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index 7741dd73d740d0..f295c9c0a88e6e 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -3,7 +3,7 @@ import type { MemberInviteResponse } from '@dify/contracts/api/console/workspace import type { Role } from '@/models/access-control' import type { Member } from '@/models/common' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' -import { useQuery, useSuspenseQuery } from '@tanstack/react-query' +import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -16,9 +16,7 @@ import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/wor import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { getAccessControlTemplateLanguage, LanguagesSupported } from '@/i18n/language' -import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' import { consoleQuery } from '@/service/console' -import { useMembers } from '@/service/use-common' import { hasPermission } from '@/utils/permission' import EditWorkspaceModal from './edit-workspace-modal' import { InviteModal } from './invite-modal' @@ -39,7 +37,9 @@ const MembersPage = () => { const currentWorkspace = useAtomValue(currentWorkspaceAtom) const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) - const { data, refetch } = useMembers(language) + const { data, refetch } = useQuery( + consoleQuery.workspaces.current.members.get.queryOptions({ input: { query: { language } } }), + ) const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const [invitationResults, setInvitationResults] = useState< MemberInviteResponse['invitation_results'] | null @@ -80,7 +80,9 @@ const MembersPage = () => { setDetailsMember(null) }, []) - const { mutateAsync: updateRolesOfMember } = useUpdateRolesOfMember() + const { mutateAsync: updateRolesOfMember } = useMutation( + consoleQuery.workspaces.current.rbac.members.byMemberId.rbacRoles.put.mutationOptions(), + ) const handleAssignRolesSubmit = (roles: Role[]) => { const roleIds = systemFeatures.rbac_enabled @@ -88,10 +90,7 @@ const MembersPage = () => { : roles.slice(0, 1).map((role) => role.id) updateRolesOfMember( - { - memberId: detailsMember!.id, - roleIds, - }, + { params: { member_id: detailsMember!.id }, body: { role_ids: roleIds } }, { onSuccess: () => { toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) @@ -205,7 +204,7 @@ const MembersPage = () => { @@ -14,8 +14,11 @@ export function InviteButton(props: InviteButtonProps) { const { t } = useTranslation(['workspaceMembers']) const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom) const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = - useWorkspacePermissions(currentWorkspaceId, systemFeatures.branding.enabled) + const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useQuery( + consoleQuery.workspaces.current.permission.get.queryOptions({ + enabled: systemFeatures.branding.enabled && !!currentWorkspaceId, + }), + ) if (systemFeatures.branding.enabled) { if (isFetchingWorkspacePermissions) { return diff --git a/web/app/components/header/account-setting/members-page/invite-modal/__tests__/focus.browser.spec.tsx b/web/app/components/header/account-setting/members-page/invite-modal/__tests__/focus.browser.spec.tsx index 5a39a9c872b5bc..a4a199f28c1577 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/__tests__/focus.browser.spec.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/__tests__/focus.browser.spec.tsx @@ -3,23 +3,28 @@ import type { QueryClient } from '@tanstack/react-query' import { userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { QueryClientTestProvider } from '@/test/console/query-provider' import MembersPage from '../../index' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.get': () => ({ + data: { + pages: [{ data: [{ id: 'admin', name: 'Admin', description: 'Manage members' }] }], + }, + isLoading: false, + error: null, + hasNextPage: false, + isFetchingNextPage: false, + fetchNextPage: vi.fn(), + }), + }) +}) + const { inviteMember } = vi.hoisted(() => ({ inviteMember: vi.fn() })) vi.mock('#i18n', () => ({ useLocale: () => 'en-US' })) -vi.mock('@/service/access-control/use-workspace-roles', () => ({ - useWorkspaceRoleList: () => ({ - data: { pages: [{ data: [{ id: 'admin', name: 'Admin', description: 'Manage members' }] }] }, - isLoading: false, - error: null, - hasNextPage: false, - isFetchingNextPage: false, - fetchNextPage: vi.fn(), - }), -})) let queryClient: QueryClient @@ -31,7 +36,12 @@ beforeEach(() => { workspacePermissionKeys: ['workspace.member.manage'], features: { workspace_members: { enabled: true, size: 1, limit: 10 } }, }).queryClient - queryClient.setQueryData([...commonQueryKeys.members, 'en'], { accounts: [] }) + queryClient.setQueryData( + consoleQuery.workspaces.current.members.get.queryOptions({ + input: { query: { language: 'en' } }, + }).queryKey, + { accounts: [] }, + ) vi.stubGlobal( 'fetch', vi.fn(async (input: RequestInfo | URL) => { diff --git a/web/app/components/header/account-setting/members-page/invite-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/invite-modal/__tests__/index.spec.tsx index 67d3a5b708b738..1472992364219b 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/__tests__/index.spec.tsx @@ -4,19 +4,28 @@ import { QueryClient } from '@tanstack/react-query' import { act, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vite-plus/test' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' import { createHttpError } from '@/service/http-error' import { seedCurrentWorkspaceQuery } from '@/test/console/current-workspace' import { seedFeatures, seedSystemFeatures } from '@/test/console/query-data' import { QueryClientTestProvider } from '@/test/console/query-provider' import { InviteModal } from '../index' +const { mockWorkspaceRoleListHook } = vi.hoisted(() => ({ + mockWorkspaceRoleListHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.get': () => mockWorkspaceRoleListHook(), + }) +}) + const { fetchFeatures, inviteMember } = vi.hoisted(() => ({ fetchFeatures: vi.fn(), inviteMember: vi.fn(), })) -vi.mock('@/service/access-control/use-workspace-roles') vi.mock('@/service/console', async (importOriginal) => { const actual = await importOriginal() const { workspaces } = await import('@dify/contracts/api/console/workspaces/orpc.gen') @@ -38,8 +47,10 @@ vi.mock('@/service/console', async (importOriginal) => { current: { ...actual.consoleQuery.workspaces.current, summary: actual.consoleQuery.workspaces.current.summary, + permission: actual.consoleQuery.workspaces.current.permission, + rbac: actual.consoleQuery.workspaces.current.rbac, members: { - ...actual.consoleQuery.workspaces.current.members, + get: actual.consoleQuery.workspaces.current.members.get, inviteEmail: { post: { mutationOptions: ( @@ -79,7 +90,7 @@ describe('InviteModal', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(useWorkspaceRoleList).mockReturnValue({ + vi.mocked(mockWorkspaceRoleListHook).mockReturnValue({ data: { pages: [ { @@ -122,7 +133,7 @@ describe('InviteModal', () => { hasNextPage: false, isFetchingNextPage: false, fetchNextPage: vi.fn(), - } as unknown as ReturnType) + }) }) const renderModal = async ({ diff --git a/web/app/components/header/account-setting/members-page/invite-modal/__tests__/role-selector.spec.tsx b/web/app/components/header/account-setting/members-page/invite-modal/__tests__/role-selector.spec.tsx index 36cd0f46ac809f..0a4153207338e7 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/__tests__/role-selector.spec.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/__tests__/role-selector.spec.tsx @@ -2,10 +2,18 @@ import type { Role, RoleListResponse } from '@/models/access-control' import { act, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vite-plus/test' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' import { RoleSelector } from '../role-selector' -vi.mock('@/service/access-control/use-workspace-roles') +const { mockWorkspaceRoleListHook } = vi.hoisted(() => ({ + mockWorkspaceRoleListHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.get': () => mockWorkspaceRoleListHook(), + }) +}) const createRole = (overrides: Partial): Role => ({ id: overrides.id ?? overrides.name ?? 'role-id', @@ -49,14 +57,14 @@ const mockUseWorkspaceRoleList = ({ hasNextPage?: boolean fetchNextPage?: () => void } = {}) => { - vi.mocked(useWorkspaceRoleList).mockReturnValue({ + vi.mocked(mockWorkspaceRoleListHook).mockReturnValue({ data: { pages: [createPage(data)], pageParams: [1] }, isLoading, error, hasNextPage, isFetchingNextPage: false, fetchNextPage, - } as unknown as ReturnType) + }) } const RoleSelectorWrapper = () => diff --git a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx index 1acee0fe48911f..1143467cb74658 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx @@ -21,7 +21,6 @@ import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { deploymentEditionAtom } from '@/features/system-features/state' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' import { InviteButton } from '../invite-button' import { mergeEmailRecipients } from './email-recipients' import { EmailRecipientsField } from './email-recipients-field' @@ -175,7 +174,9 @@ export function InviteModal({ isEmailSetup, onSend }: InviteModalProps) { onSuccess: (response) => { // The request alone decides success; seat and member refreshes run in the background. void queryClient.invalidateQueries({ queryKey: consoleQuery.features.get.queryKey() }) - void queryClient.invalidateQueries({ queryKey: commonQueryKeys.members }) + void queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.members.get.key(), + }) onSend(response.invitation_results) actionsRef.current?.close() }, diff --git a/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx b/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx index 7dc6771f7977a3..f81d20f3bf10da 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx @@ -15,11 +15,12 @@ import { SelectTrigger, SelectValue, } from '@langgenius/dify-ui/select' +import { useInfiniteQuery } from '@tanstack/react-query' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { getAccessControlTemplateLanguage } from '@/i18n/language' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' +import { workspaceRoleListInfiniteOptions } from '../../workspace-role-list-query' type RoleSelectorProps = { hasServerError?: boolean @@ -65,11 +66,13 @@ export function RoleSelector({ hasServerError = false, disabled = false }: RoleS hasNextPage, isFetchingNextPage, fetchNextPage, - } = useWorkspaceRoleList({ - page: 1, - limit: PAGE_SIZE, - language, - }) + } = useInfiniteQuery( + workspaceRoleListInfiniteOptions({ + page: 1, + limit: PAGE_SIZE, + language, + }), + ) useEffect(() => { const hasMore = hasNextPage ?? true diff --git a/web/app/components/header/account-setting/members-page/member-details-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/member-details-modal/__tests__/index.spec.tsx index 8987c29f9a3be5..e7e5bba4177d33 100644 --- a/web/app/components/header/account-setting/members-page/member-details-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/member-details-modal/__tests__/index.spec.tsx @@ -3,12 +3,20 @@ import type { Member } from '@/models/common' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vite-plus/test' -import { useRolesOfMember } from '@/service/access-control/use-member-roles' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' import MemberDetailsModal from '../index' -vi.mock('@/service/access-control/use-member-roles') -vi.mock('@/service/access-control/use-workspace-roles') +const { mockWorkspaceRoleListHook, mockRolesOfMemberHook } = vi.hoisted(() => ({ + mockWorkspaceRoleListHook: vi.fn(), + mockRolesOfMemberHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.get': () => mockWorkspaceRoleListHook(), + 'workspaces.current.rbac.members.byMemberId.rbacRoles.get': () => mockRolesOfMemberHook(), + }) +}) const createRole = (overrides: Partial): Role => ({ id: 'role-1', @@ -31,23 +39,23 @@ const member: Member = { avatar_url: '', role: 'admin', roles: [createRole({ id: 'role-1', name: 'Custom role' })], - last_active_at: '1731000000', - last_login_at: '1731000000', - created_at: '1731000000', + last_active_at: 1731000000, + last_login_at: 1731000000, + created_at: 1731000000, status: 'active', } describe('MemberDetailsModal', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(useRolesOfMember).mockReturnValue({ + vi.mocked(mockRolesOfMemberHook).mockReturnValue({ data: { account_id: member.id, roles: [createRole({ id: 'role-1', name: 'Custom role' })], }, isLoading: false, - } as unknown as ReturnType) - vi.mocked(useWorkspaceRoleList).mockReturnValue({ + }) + vi.mocked(mockWorkspaceRoleListHook).mockReturnValue({ data: { pages: [ { @@ -70,7 +78,7 @@ describe('MemberDetailsModal', () => { hasNextPage: false, isFetchingNextPage: false, fetchNextPage: vi.fn(), - } as unknown as ReturnType) + }) }) describe('Rendering', () => { @@ -114,10 +122,10 @@ describe('MemberDetailsModal', () => { }) it('should render role loading state without assigned role chips or count', () => { - vi.mocked(useRolesOfMember).mockReturnValue({ + vi.mocked(mockRolesOfMemberHook).mockReturnValue({ data: undefined, isLoading: true, - } as unknown as ReturnType) + }) render( { const user = userEvent.setup() const handleAssignSubmit = vi.fn() - vi.mocked(useRolesOfMember).mockReturnValue({ + vi.mocked(mockRolesOfMemberHook).mockReturnValue({ data: { account_id: member.id, roles: [ @@ -190,7 +198,7 @@ describe('MemberDetailsModal', () => { createRole({ id: 'role-2', name: 'Second role' }), ], }, - } as unknown as ReturnType) + }) render( getAccessControlTemplateLanguage(locale), [locale]) - const { data: rolesOfMember, isLoading: isLoadingRolesOfMember } = useRolesOfMember( - member.id, - language, + const { data: rolesOfMember, isLoading: isLoadingRolesOfMember } = useQuery( + consoleQuery.workspaces.current.rbac.members.byMemberId.rbacRoles.get.queryOptions({ + input: { params: { member_id: member.id }, query: { language } }, + select: (data) => ({ ...data, roles: (data.roles ?? []).map(toRole) }), + }), ) const [pendingRoles, setPendingRoles] = useState() diff --git a/web/app/components/header/account-setting/members-page/member-menu.tsx b/web/app/components/header/account-setting/members-page/member-menu.tsx index 635ab40b817c45..98b9a899bce73e 100644 --- a/web/app/components/header/account-setting/members-page/member-menu.tsx +++ b/web/app/components/header/account-setting/members-page/member-menu.tsx @@ -18,13 +18,12 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { useQueryClient } from '@tanstack/react-query' -import { memo, useCallback, useState } from 'react' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' -import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' -import { deleteMemberOrCancelInvitation } from '@/service/common' -import { commonQueryKeys } from '@/service/use-common' +import { consoleClient, consoleQuery } from '@/service/console' +import { createSelectedRolePlaceholder } from '../workspace-role-list-query' import AssignRolesModal from './assign-roles-modal' type MemberMenuProps = { @@ -53,7 +52,10 @@ const MemberMenu = ({ const canRemove = !isOwner && !isCurrentUser const showTransferOwnership = isOwner && canTransferOwnership - const selectedRoles = member.roles || [] + const selectedRoles = useMemo( + () => (member.roles ?? []).map(createSelectedRolePlaceholder), + [member.roles], + ) const memberName = member.name || member.email const assignRolesLabel = allowMultipleRoles ? t(($) => $['members.assignRoles'], { ns: 'workspaceMembers', defaultValue: 'Assign Roles' }) @@ -63,7 +65,9 @@ const MemberMenu = ({ setAssignModalOpen(true) }, []) - const { mutateAsync: updateRolesOfMember } = useUpdateRolesOfMember() + const { mutateAsync: updateRolesOfMember } = useMutation( + consoleQuery.workspaces.current.rbac.members.byMemberId.rbacRoles.put.mutationOptions(), + ) const handleAssignRolesSubmit = useCallback( (roles: Role[]) => { @@ -72,10 +76,7 @@ const MemberMenu = ({ : roles.slice(0, 1).map((role) => role.id) updateRolesOfMember( - { - memberId: member.id, - roleIds, - }, + { params: { member_id: member.id }, body: { role_ids: roleIds } }, { onSuccess: () => { toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) @@ -93,8 +94,12 @@ const MemberMenu = ({ const handleRemove = useCallback(async () => { setRemoving(true) try { - await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` }) - void queryClient.invalidateQueries({ queryKey: commonQueryKeys.members }) + await consoleClient.workspaces.current.members.byMemberId.delete({ + params: { member_id: member.id }, + }) + void queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.members.get.key(), + }) setRemoveConfirmOpen(false) } catch { } finally { diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx index 703fccb449276e..d12c0627405117 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx @@ -2,12 +2,22 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vite-plus/test' -import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common' -import { useMembers } from '@/service/use-common' +import { consoleClient } from '@/service/console' import { createAccountProfileQueryWrapper } from '@/test/console/account-profile' import { render } from '@/test/console/render' import TransferOwnershipModal from '../index' +const { mockMembersHook } = vi.hoisted(() => ({ + mockMembersHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => mockMembersHook(), + }) +}) + const toastMocks = vi.hoisted(() => ({ mockNotify: vi.fn(), })) @@ -21,8 +31,28 @@ vi.mock('@/context/workspace-state', async () => { return createWorkspaceStateModuleMock(() => mockConsoleState.current) }) -vi.mock('@/service/common') -vi.mock('@/service/use-common') +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + workspaces: { + current: { + members: { + byMemberId: { + ownerTransfer: { + post: vi.fn(), + }, + }, + ownerTransferCheck: { + post: vi.fn(), + }, + sendOwnerTransferConfirmEmail: { + post: vi.fn(), + }, + }, + }, + }, + }, +})) vi.mock('@/app/notifications', () => ({ default: { notify: (args: unknown) => toastMocks.mockNotify(args), @@ -55,9 +85,9 @@ describe('TransferOwnershipModal', () => { mockConsoleState.current = consoleState mockConsoleStateReader.mockReturnValue(consoleState) - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [] }, - } as unknown as ReturnType) + }) // Stub globalThis.location.reload (component calls globalThis.location.reload()) const mockReload = vi.fn() @@ -92,15 +122,17 @@ describe('TransferOwnershipModal', () => { isValid?: boolean token?: string } = {}) => { - vi.mocked(sendOwnerEmail).mockResolvedValue({ + vi.mocked( + consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post, + ).mockResolvedValue({ data: 'step-token', result: 'success', - } as unknown as Awaited>) - vi.mocked(verifyOwnerEmail).mockResolvedValue({ + }) + vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockResolvedValue({ + email: 'owner@example.com', is_valid: isValid, token, - result: 'success', - } as unknown as Awaited>) + }) } const goToTransferStep = async (user: ReturnType) => { @@ -120,9 +152,11 @@ describe('TransferOwnershipModal', () => { it('should complete ownership transfer flow through all steps', async () => { const user = userEvent.setup() mockEmailVerification() - vi.mocked(ownershipTransfer).mockResolvedValue({ + vi.mocked( + consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post, + ).mockResolvedValue({ result: 'success', - } as unknown as Awaited>) + }) renderModal() await goToTransferStep(user) @@ -131,7 +165,12 @@ describe('TransferOwnershipModal', () => { await waitFor( () => { - expect(ownershipTransfer).toHaveBeenCalledWith('new-owner-id', { token: 'final-token' }) + expect( + consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post, + ).toHaveBeenCalledWith({ + params: { member_id: 'new-owner-id' }, + body: { token: 'final-token' }, + }) expect(window.location.reload).toHaveBeenCalled() }, { timeout: 10000 }, @@ -140,10 +179,12 @@ describe('TransferOwnershipModal', () => { it('should handle timer countdown and resend', async () => { vi.useFakeTimers() - vi.mocked(sendOwnerEmail).mockResolvedValue({ + vi.mocked( + consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post, + ).mockResolvedValue({ data: 'token', result: 'success', - } as unknown as Awaited>) + }) renderModal() // Trigger the email send (which starts the timer) @@ -172,7 +213,9 @@ describe('TransferOwnershipModal', () => { await act(async () => { fireEvent.click(resendBtn) }) - expect(sendOwnerEmail).toHaveBeenCalledTimes(2) + expect( + consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post, + ).toHaveBeenCalledTimes(2) vi.useRealTimers() }) @@ -196,7 +239,9 @@ describe('TransferOwnershipModal', () => { it('should show error when verifying email throws an error', async () => { const user = userEvent.setup() mockEmailVerification() - vi.mocked(verifyOwnerEmail).mockRejectedValue(new Error('verification crash')) + vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockRejectedValue( + new Error('verification crash'), + ) renderModal() await goToTransferStep(user) @@ -213,7 +258,9 @@ describe('TransferOwnershipModal', () => { it('should not show a modal-level toast and should stay on start step when sending verification email fails', async () => { const user = userEvent.setup() - vi.mocked(sendOwnerEmail).mockRejectedValue(new Error('network error')) + vi.mocked( + consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post, + ).mockRejectedValue(new Error('network error')) renderModal() await user.click( screen.getByRole('button', { name: /members\.transferModal\.sendVerifyCode/i }), @@ -222,7 +269,9 @@ describe('TransferOwnershipModal', () => { // The base service layer surfaces the real backend error. The modal itself // must NOT show an additional toast (e.g. "Error sending verification code: undefined"). await waitFor(() => { - expect(sendOwnerEmail).toHaveBeenCalled() + expect( + consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post, + ).toHaveBeenCalled() }) expect(mockNotify).not.toHaveBeenCalled() // Should remain on the start step instead of advancing to the verify step. @@ -234,7 +283,9 @@ describe('TransferOwnershipModal', () => { it('should show error when ownership transfer fails', async () => { const user = userEvent.setup() mockEmailVerification() - vi.mocked(ownershipTransfer).mockRejectedValue(new Error('transfer failed')) + vi.mocked( + consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post, + ).mockRejectedValue(new Error('transfer failed')) renderModal() await goToTransferStep(user) await selectNewOwnerAndSubmit(user) @@ -249,19 +300,21 @@ describe('TransferOwnershipModal', () => { }) }) - it('should handle sendOwnerEmail returning null data', async () => { + it('should handle sendOwnerEmail returning an empty step token', async () => { const user = userEvent.setup() - vi.mocked(sendOwnerEmail).mockResolvedValue({ - data: null, + vi.mocked( + consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post, + ).mockResolvedValue({ + data: '', result: 'success', - } as unknown as Awaited>) + }) renderModal() await user.click( screen.getByRole('button', { name: /members\.transferModal\.sendVerifyCode/i }), ) - // Should advance to verify step even with null data + // Should advance to verify step even without a step token await waitFor(() => { expect(screen.getByText(/members\.transferModal\.verifyEmail/i)).toBeInTheDocument() }) @@ -269,7 +322,9 @@ describe('TransferOwnershipModal', () => { it('should swallow null rejection from sendOwnerEmail without showing a modal-level toast', async () => { const user = userEvent.setup() - vi.mocked(sendOwnerEmail).mockRejectedValue(null) + vi.mocked( + consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post, + ).mockRejectedValue(null) renderModal() await user.click( @@ -277,7 +332,9 @@ describe('TransferOwnershipModal', () => { ) await waitFor(() => { - expect(sendOwnerEmail).toHaveBeenCalled() + expect( + consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post, + ).toHaveBeenCalled() }) expect(mockNotify).not.toHaveBeenCalled() expect( @@ -288,7 +345,9 @@ describe('TransferOwnershipModal', () => { it('should show fallback error prefix when verifyOwnerEmail throws null', async () => { const user = userEvent.setup() mockEmailVerification() - vi.mocked(verifyOwnerEmail).mockRejectedValue(null) + vi.mocked(consoleClient.workspaces.current.members.ownerTransferCheck.post).mockRejectedValue( + null, + ) renderModal() await goToTransferStep(user) @@ -306,7 +365,9 @@ describe('TransferOwnershipModal', () => { it('should show fallback error prefix when ownershipTransfer throws null', async () => { const user = userEvent.setup() mockEmailVerification() - vi.mocked(ownershipTransfer).mockRejectedValue(null) + vi.mocked( + consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post, + ).mockRejectedValue(null) renderModal() await goToTransferStep(user) diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/member-selector.spec.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/member-selector.spec.tsx index 8ad3aec93e8426..4c11b4f49e5770 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/member-selector.spec.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/member-selector.spec.tsx @@ -1,10 +1,18 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vite-plus/test' -import { useMembers } from '@/service/use-common' import MemberSelector from '../member-selector' -vi.mock('@/service/use-common') +const { mockMembersHook } = vi.hoisted(() => ({ + mockMembersHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => mockMembersHook(), + }) +}) const mockAccounts = [ { id: '1', name: 'John Doe', email: 'john@example.com', avatar_url: '' }, @@ -27,9 +35,9 @@ describe('MemberSelector', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: mockAccounts }, - } as unknown as ReturnType) + }) }) it('should render placeholder when no value is selected', () => { @@ -106,23 +114,21 @@ describe('MemberSelector', () => { }) it('should handle missing data gracefully', () => { - vi.mocked(useMembers).mockReturnValue({ data: undefined } as unknown as ReturnType< - typeof useMembers - >) + vi.mocked(mockMembersHook).mockReturnValue({ data: undefined }) render() expect(getTrigger()).toBeInTheDocument() }) it('should filter by email when account name is empty', async () => { const user = userEvent.setup() - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [ ...mockAccounts, { id: '4', name: '', email: 'noname@example.com', avatar_url: '' }, ], }, - } as unknown as ReturnType) + }) render() await user.click(getTrigger()) @@ -158,13 +164,13 @@ describe('MemberSelector', () => { it('should fall back to empty string for account with undefined email when searching', async () => { const user = userEvent.setup() - vi.mocked(useMembers).mockReturnValue({ + vi.mocked(mockMembersHook).mockReturnValue({ data: { accounts: [ { id: '1', name: 'John', email: undefined as unknown as string, avatar_url: '' }, ], }, - } as unknown as ReturnType) + }) render() await user.click(getTrigger()) diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx index e9437fbbfad42e..1177333c15528a 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx @@ -9,7 +9,7 @@ import { Trans, useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' import { currentWorkspaceAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common' +import { consoleClient } from '@/service/console' import { getErrorMessage } from '@/utils/error-message' import MemberSelector from './member-selector' @@ -62,15 +62,16 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => { ) } const sendEmail = async () => { - const res = await sendOwnerEmail({}) + const res = await consoleClient.workspaces.current.members.sendOwnerTransferConfirmEmail.post({ + body: {}, + }) startCount() if (res.data) setStepToken(res.data) } const verifyEmailAddress = async (code: string, token: string, callback?: () => void) => { try { - const res = await verifyOwnerEmail({ - code, - token, + const res = await consoleClient.workspaces.current.members.ownerTransferCheck.post({ + body: { code, token }, }) if (res.is_valid) { setStepToken(res.token) @@ -97,8 +98,9 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => { const handleTransfer = async () => { setIsTransfer(true) try { - await ownershipTransfer(newOwner, { - token: stepToken, + await consoleClient.workspaces.current.members.byMemberId.ownerTransfer.post({ + params: { member_id: newOwner }, + body: { token: stepToken }, }) globalThis.location.reload() } catch (error) { diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx index f79fd972c1d76d..82df103d9d7890 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx @@ -3,10 +3,11 @@ import type { FC } from 'react' import { Avatar, AvatarFallback, AvatarImage } from '@langgenius/dify-ui/avatar' import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useMembers } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { getAvatarInitial } from '@/utils/avatar' type Props = Readonly<{ @@ -21,7 +22,7 @@ const MemberSelector: FC = ({ value, onSelect, exclude = [] }) => { const [searchValue, setSearchValue] = useState('') const searchLabel = t(($) => $['operation.search'], { ns: 'common' }) - const { data } = useMembers() + const { data } = useQuery(consoleQuery.workspaces.current.members.get.queryOptions({ input: {} })) const currentValue = useMemo(() => { if (!data?.accounts || !value) return null diff --git a/web/app/components/header/account-setting/model-provider-page/__tests__/hooks.spec.ts b/web/app/components/header/account-setting/model-provider-page/__tests__/hooks.spec.ts index 56eabeea9befcf..0ec65df363afeb 100644 --- a/web/app/components/header/account-setting/model-provider-page/__tests__/hooks.spec.ts +++ b/web/app/components/header/account-setting/model-provider-page/__tests__/hooks.spec.ts @@ -9,7 +9,6 @@ import type { } from '../declarations' import { act, renderHook } from '@testing-library/react' import { useLocale } from '#i18n' -import { fetchDefaultModal } from '@/service/common' import { consoleQuery } from '@/service/console' import { ConfigurationMethodEnum, @@ -49,18 +48,6 @@ vi.mock('@tanstack/react-query', () => ({ })), })) -vi.mock('@/service/common', () => ({ - fetchDefaultModal: vi.fn(), -})) - -vi.mock('@/service/use-common', () => ({ - commonQueryKeys: { - modelProviders: ['model-providers'], - modelProviderDetails: ['model-provider-details'], - defaultModel: (type: string) => ['default-model', type], - }, -})) - vi.mock('@/context/modal-context', () => ({ useModalContextSelector: vi.fn((selector) => { const state = { setShowModelModal: vi.fn() } @@ -166,6 +153,9 @@ describe('hooks', () => { provider: { provider: 'openai', icon_small: { en_US: 'icon', zh_Hans: 'icon' }, + label: { en_US: 'Provider', zh_Hans: 'Provider' }, + supported_model_types: [], + tenant_id: 'tenant-1', }, model, model_type: ModelTypeEnum.textGeneration, @@ -193,6 +183,9 @@ describe('hooks', () => { provider: { provider: 'anthropic', icon_small: { en_US: 'icon', zh_Hans: 'icon' }, + label: { en_US: 'Provider', zh_Hans: 'Provider' }, + supported_model_types: [], + tenant_id: 'tenant-1', }, model: 'claude-3', model_type: ModelTypeEnum.textGeneration, @@ -290,15 +283,6 @@ describe('hooks', () => { expect(result.current.data).toEqual(mockDefaultModel) expect(result.current.isLoading).toBe(false) - - // Coverage for queryFn - const queryCall = (useQuery as Mock).mock.calls.find( - (call) => Array.isArray(call[0].queryKey) && call[0].queryKey[0] === 'default-model', - ) - if (queryCall) { - await queryCall[0].queryFn() - expect(fetchDefaultModal).toHaveBeenCalled() - } }) it('should return undefined when data is not available', () => { @@ -327,7 +311,9 @@ describe('hooks', () => { expect(useQuery).toHaveBeenCalledWith( expect.objectContaining({ enabled: false, - queryKey: ['default-model', ModelTypeEnum.textEmbedding], + queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({ + input: { query: { model_type: ModelTypeEnum.textEmbedding } }, + }), }), ) expect(result.current.isLoading).toBe(false) @@ -686,7 +672,9 @@ describe('hooks', () => { }) expect(invalidateQueries).toHaveBeenCalledWith({ - queryKey: ['default-model', ModelTypeEnum.textGeneration], + queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({ + input: { query: { model_type: ModelTypeEnum.textGeneration } }, + }), }) }) @@ -721,7 +709,7 @@ describe('hooks', () => { queryKey: consoleQuery.workspaces.current.modelProviders.summary.get.key(), }) expect(invalidateQueries).toHaveBeenCalledWith({ - queryKey: ['model-provider-details'], + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), }) }) @@ -1024,7 +1012,7 @@ describe('hooks', () => { queryKey: consoleQuery.workspaces.current.modelProviders.summary.get.key(), }) expect(invalidateQueries).toHaveBeenCalledWith({ - queryKey: ['model-provider-details'], + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), }) expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: getModelListQueryKey(ModelTypeEnum.textGeneration), @@ -1172,7 +1160,7 @@ describe('hooks', () => { queryKey: consoleQuery.workspaces.current.modelProviders.summary.get.key(), }) expect(invalidateQueries).toHaveBeenCalledWith({ - queryKey: ['model-provider-details'], + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), }) expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: getModelListQueryKey(ModelTypeEnum.textGeneration), diff --git a/web/app/components/header/account-setting/model-provider-page/__tests__/utils.spec.ts b/web/app/components/header/account-setting/model-provider-page/__tests__/utils.spec.ts index 1bcff84d74c844..780b1ea4363d75 100644 --- a/web/app/components/header/account-setting/model-provider-page/__tests__/utils.spec.ts +++ b/web/app/components/header/account-setting/model-provider-page/__tests__/utils.spec.ts @@ -1,8 +1,10 @@ import { describe, expect, it, vi } from 'vite-plus/test' +import { createProviderResponseFixture } from '@/test/console/model-provider' import { FormTypeEnum, ModelTypeEnum } from '../declarations' import { genModelNameFormSchema, genModelTypeFormSchema, + modelProviderDetailsQueryOptions, modelTypeFormat, sizeFormat, } from '../utils' @@ -60,4 +62,58 @@ describe('utils', () => { expect(schema.placeholder!.en_US).toBe('Enter custom') }) }) + + describe('modelProviderDetailsQueryOptions', () => { + it('should map credential schemas and nullable provider fields to the UI provider', () => { + const select = modelProviderDetailsQueryOptions().select! + const { data } = select({ + data: [ + createProviderResponseFixture({ + help: null, + background: null, + provider_credential_schema: { + credential_form_schemas: [ + { + variable: 'api_key', + type: 'secret-input', + label: { en_US: 'API Key', zh_Hans: null }, + max_length: 0, + required: false, + show_on: [], + options: [{ label: { en_US: 'One' }, value: '1', show_on: [] }], + }, + ], + }, + system_configuration: { + enabled: true, + current_quota_type: null, + quota_configurations: [], + }, + }), + ], + }) + + expect(data[0]).toMatchObject({ + label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' }, + help: undefined, + background: undefined, + supported_model_types: [ModelTypeEnum.textGeneration], + system_configuration: { enabled: true, quota_configurations: [] }, + provider_credential_schema: { + credential_form_schemas: [ + { + name: 'api_key', + variable: 'api_key', + type: FormTypeEnum.secretInput, + label: { en_US: 'API Key', zh_Hans: 'API Key' }, + required: false, + show_on: [], + options: [{ label: { en_US: 'One', zh_Hans: 'One' }, value: '1', show_on: [] }], + }, + ], + }, + }) + expect(data[0]!.system_configuration.current_quota_type).toBeUndefined() + }) + }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/declarations.ts b/web/app/components/header/account-setting/model-provider-page/declarations.ts index 1a96f401d96f2e..554f2bc84c6266 100644 --- a/web/app/components/header/account-setting/model-provider-page/declarations.ts +++ b/web/app/components/header/account-setting/model-provider-page/declarations.ts @@ -1,3 +1,8 @@ +import type { + FormType as ContractFormType, + QuotaConfiguration as ContractQuotaConfiguration, + GetWorkspacesCurrentDefaultModelResponse, +} from '@dify/contracts/api/console/workspaces/types.gen' import type { Field } from '@/app/components/workflow/nodes/llm/types' export type FormValue = Record @@ -118,9 +123,10 @@ export type CredentialFormSchemaBase = { name: string variable: string label: TypeWithI18N - type: FormTypeEnum + // Model provider credential forms may also declare `switch`, which has no field renderer. + type: FormTypeEnum | ContractFormType required: boolean - default?: string + default?: string | number | boolean | unknown[] | Record | null tooltip?: TypeWithI18N show_on: FormShowOnObject[] url?: string @@ -193,14 +199,7 @@ export const QuotaUnitEnum = { export type QuotaUnitEnum = (typeof QuotaUnitEnum)[keyof typeof QuotaUnitEnum] -type QuotaConfiguration = { - quota_type: CurrentSystemQuotaTypeEnum - quota_unit: QuotaUnitEnum - quota_limit: number - quota_used: number - last_used: number - is_valid: boolean -} +type QuotaConfiguration = ContractQuotaConfiguration export type Credential = { credential_id: string @@ -225,19 +224,19 @@ export type ModelProvider = { provider: string label: TypeWithI18N description?: TypeWithI18N - help: { + help?: { title: TypeWithI18N url: TypeWithI18N } - icon_small: TypeWithI18N + icon_small?: TypeWithI18N icon_small_dark?: TypeWithI18N background?: string supported_model_types: ModelTypeEnum[] configurate_methods: ConfigurationMethodEnum[] - provider_credential_schema: { + provider_credential_schema?: { credential_form_schemas: CredentialFormSchema[] } - model_credential_schema: { + model_credential_schema?: { model: { label: TypeWithI18N placeholder: TypeWithI18N @@ -258,20 +257,13 @@ export type ModelProvider = { } system_configuration: { enabled: boolean - current_quota_type: CurrentSystemQuotaTypeEnum + current_quota_type?: CurrentSystemQuotaTypeEnum quota_configurations: QuotaConfiguration[] } allow_custom_token?: boolean } -export type DefaultModelResponse = { - model: string - model_type: ModelTypeEnum - provider: { - provider: string - icon_small: TypeWithI18N - } -} +export type DefaultModelResponse = NonNullable export type DefaultModel = { provider: string @@ -292,7 +284,7 @@ export type ModelParameterRule = { max?: number name: string precision?: number - required: false + required?: boolean type: string use_template?: string options?: string[] diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.ts b/web/app/components/header/account-setting/model-provider-page/hooks.ts index 66c633886f0365..e8e2d3fed01b61 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.ts @@ -23,11 +23,10 @@ import { import { PluginCategoryEnum } from '@/app/components/plugins/types' import { useModalContextSelector } from '@/context/modal-context' import { getModelLanguage } from '@/i18n/metadata' -import { fetchDefaultModal } from '@/service/common' import { consoleQuery } from '@/service/console' -import { commonQueryKeys, modelProviderDetailsQueryOptions } from '@/service/use-common' import { useExpandModelProviderList } from './atoms' import { CustomConfigurationStatusEnum, ModelStatusEnum, ModelTypeEnum } from './declarations' +import { modelProviderDetailsQueryOptions } from './utils' type UseDefaultModelAndModelList = ( defaultModel: DefaultModelResponse | undefined, @@ -95,14 +94,16 @@ export const useDefaultModel = ( type: ModelTypeEnum, { enabled = true }: ModelQueryOptions = {}, ) => { - const { data, refetch, isPending } = useQuery({ - queryKey: commonQueryKeys.defaultModel(type), - queryFn: () => fetchDefaultModal(`/workspaces/current/default-model?model_type=${type}`), - enabled, - }) + const { data, refetch, isPending } = useQuery( + consoleQuery.workspaces.current.defaultModel.get.queryOptions({ + input: { query: { model_type: type } }, + enabled, + }), + ) return { - data: data?.data, + // An unset default model comes back as null. + data: data?.data ?? undefined, mutate: refetch, isLoading: enabled && isPending, } @@ -208,7 +209,11 @@ export const useInvalidateDefaultModel = () => { return useCallback( (type: ModelTypeEnum) => { - queryClient.invalidateQueries({ queryKey: commonQueryKeys.defaultModel(type) }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.defaultModel.get.queryKey({ + input: { query: { model_type: type } }, + }), + }) }, [queryClient], ) @@ -220,7 +225,9 @@ export const useUpdateModelProviders = () => { queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.modelProviders.summary.get.key(), }) - queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelProviderDetails }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), + }) }, [queryClient]) return updateModelProviders diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth-service.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth-service.spec.tsx index d5bc1a5604740c..560bc3c4a21316 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth-service.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth-service.spec.tsx @@ -1,150 +1,170 @@ import type { CustomModel } from '../../../declarations' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { renderHook } from '@testing-library/react' +import { QueryClient, QueryClientProvider, skipToken } from '@tanstack/react-query' +import { act, renderHook } from '@testing-library/react' import { ModelTypeEnum } from '../../../declarations' import { useAuthService, useGetCredential } from '../use-auth-service' -vi.mock('@/service/use-models', () => ({ - useGetProviderCredential: vi.fn(), - useGetModelCredential: vi.fn(), - useAddProviderCredential: vi.fn(), - useEditProviderCredential: vi.fn(), - useDeleteProviderCredential: vi.fn(), - useActiveProviderCredential: vi.fn(), - useAddModelCredential: vi.fn(), - useEditModelCredential: vi.fn(), - useDeleteModelCredential: vi.fn(), - useActiveModelCredential: vi.fn(), +const mocks = vi.hoisted(() => ({ + providerCredentialOptions: vi.fn(), + modelCredentialOptions: vi.fn(), + addProviderCredential: vi.fn(), + editProviderCredential: vi.fn(), + deleteProviderCredential: vi.fn(), + switchProviderCredential: vi.fn(), + addModelCredential: vi.fn(), + editModelCredential: vi.fn(), + deleteModelCredential: vi.fn(), + switchModelCredential: vi.fn(), })) -const { - useGetProviderCredential, - useGetModelCredential, - useAddProviderCredential, - useEditProviderCredential, - useDeleteProviderCredential, - useActiveProviderCredential, - useAddModelCredential, - useEditModelCredential, - useDeleteModelCredential, - useActiveModelCredential, -} = await import('@/service/use-models') +const { createQueryOptions } = vi.hoisted(() => ({ + createQueryOptions: + (name: string, fetcher: (input: unknown) => void) => + ({ input }: { input: unknown }) => { + fetcher(input) + return { + queryKey: [name, input], + queryFn: typeof input === 'symbol' ? input : () => ({ source: name }), + } + }, +})) + +vi.mock('@/service/console', () => ({ + consoleQuery: { + workspaces: { + current: { + modelProviders: { + byProvider: { + credentials: { + get: { + queryOptions: createQueryOptions('provider', mocks.providerCredentialOptions), + }, + }, + models: { + credentials: { + get: { queryOptions: createQueryOptions('model', mocks.modelCredentialOptions) }, + }, + }, + }, + }, + }, + }, + }, + consoleClient: { + workspaces: { + current: { + modelProviders: { + byProvider: { + credentials: { + post: mocks.addProviderCredential, + put: mocks.editProviderCredential, + delete: mocks.deleteProviderCredential, + switch: { post: mocks.switchProviderCredential }, + }, + models: { + credentials: { + post: mocks.addModelCredential, + put: mocks.editModelCredential, + delete: mocks.deleteModelCredential, + switch: { post: mocks.switchModelCredential }, + }, + }, + }, + }, + }, + }, + }, +})) + +// The services are selected at runtime, so call sites pass a payload of either shape. +type CredentialService = (payload: unknown) => Promise describe('useAuthService hooks', () => { let queryClient: QueryClient const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) + const model: CustomModel = { model: 'gpt-4', model_type: ModelTypeEnum.textGeneration } beforeEach(() => { vi.clearAllMocks() queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - - const mockMutationReturn = { mutateAsync: vi.fn() } - vi.mocked(useAddProviderCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useEditProviderCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useDeleteProviderCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useActiveProviderCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useAddModelCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useEditModelCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useDeleteModelCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useActiveModelCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) }) - it('useGetCredential selects correct source and params', () => { - const mockData = { data: 'test' } - vi.mocked(useGetProviderCredential).mockReturnValue( - mockData as unknown as ReturnType, - ) - vi.mocked(useGetModelCredential).mockReturnValue( - mockData as unknown as ReturnType, - ) - - // Provider case - const { result: providerRes } = renderHook( - () => useGetCredential('openai', false, 'cred-123'), - { wrapper }, - ) - expect(useGetProviderCredential).toHaveBeenCalledWith(true, 'openai', 'cred-123') - expect(providerRes.current).toBe(mockData) - - // Model case - const mockModel = { model: 'gpt-4', model_type: ModelTypeEnum.textGeneration } as CustomModel - const { result: modelRes } = renderHook( - () => useGetCredential('openai', true, 'cred-123', mockModel, 'src'), - { wrapper }, - ) - expect(useGetModelCredential).toHaveBeenCalledWith( - true, - 'openai', - 'cred-123', - 'gpt-4', - ModelTypeEnum.textGeneration, - 'src', - ) - expect(modelRes.current).toBe(mockData) - - // Early return cases + it('should query the provider credential only when a credential id is given', () => { + renderHook(() => useGetCredential('openai', false, 'cred-123'), { wrapper }) + expect(mocks.providerCredentialOptions).toHaveBeenCalledWith({ + params: { provider: 'openai' }, + query: { credential_id: 'cred-123' }, + }) + renderHook(() => useGetCredential('openai', false), { wrapper }) - expect(useGetProviderCredential).toHaveBeenCalledWith(false, 'openai', undefined) - - // Branch: isModelCredential true but no id/model - renderHook(() => useGetCredential('openai', true), { wrapper }) - expect(useGetModelCredential).toHaveBeenCalledWith( - false, - 'openai', - undefined, - undefined, - undefined, - undefined, - ) + expect(mocks.providerCredentialOptions).toHaveBeenLastCalledWith(skipToken) }) - it('useAuthService provides correct services for provider and model', () => { + it('should query the model credential only when a model is given', () => { + renderHook(() => useGetCredential('openai', true, 'cred-123', model, 'custom-model'), { + wrapper, + }) + expect(mocks.modelCredentialOptions).toHaveBeenCalledWith({ + params: { provider: 'openai' }, + query: { + model: 'gpt-4', + model_type: ModelTypeEnum.textGeneration, + config_from: 'custom-model', + credential_id: 'cred-123', + }, + }) + + renderHook(() => useGetCredential('openai', true, 'cred-123'), { wrapper }) + expect(mocks.modelCredentialOptions).toHaveBeenLastCalledWith(skipToken) + }) + + it('should send provider credential changes to the provider endpoints', async () => { const { result } = renderHook(() => useAuthService('openai'), { wrapper }) + const credential = { credential_id: 'cred-1', name: 'Main', credentials: { key: 'v' } } + + await act(async () => { + await (result.current.getAddCredentialService(false) as CredentialService)(credential) + await (result.current.getEditCredentialService(false) as CredentialService)(credential) + await (result.current.getDeleteCredentialService(false) as CredentialService)({ + credential_id: 'cred-1', + }) + await (result.current.getActiveCredentialService(false) as CredentialService)({ + credential_id: 'cred-1', + }) + }) + + const params = { provider: 'openai' } + expect(mocks.addProviderCredential).toHaveBeenCalledWith({ params, body: credential }) + expect(mocks.editProviderCredential).toHaveBeenCalledWith({ params, body: credential }) + expect(mocks.deleteProviderCredential).toHaveBeenCalledWith({ + params, + body: { credential_id: 'cred-1' }, + }) + expect(mocks.switchProviderCredential).toHaveBeenCalledWith({ + params, + body: { credential_id: 'cred-1' }, + }) + }) + + it('should send model credential changes to the model endpoints', async () => { + const { result } = renderHook(() => useAuthService('openai'), { wrapper }) + const credential = { ...model, credential_id: 'cred-1', credentials: { key: 'v' } } + const ref = { ...model, credential_id: 'cred-1' } + + await act(async () => { + await (result.current.getAddCredentialService(true) as CredentialService)(credential) + await (result.current.getEditCredentialService(true) as CredentialService)(credential) + await (result.current.getDeleteCredentialService(true) as CredentialService)(ref) + await (result.current.getActiveCredentialService(true) as CredentialService)(ref) + }) - // Provider services - expect(result.current.getAddCredentialService(false)).toBe( - vi.mocked(useAddProviderCredential).mock.results[0]!.value.mutateAsync, - ) - expect(result.current.getEditCredentialService(false)).toBe( - vi.mocked(useEditProviderCredential).mock.results[0]!.value.mutateAsync, - ) - expect(result.current.getDeleteCredentialService(false)).toBe( - vi.mocked(useDeleteProviderCredential).mock.results[0]!.value.mutateAsync, - ) - expect(result.current.getActiveCredentialService(false)).toBe( - vi.mocked(useActiveProviderCredential).mock.results[0]!.value.mutateAsync, - ) - - // Model services - expect(result.current.getAddCredentialService(true)).toBe( - vi.mocked(useAddModelCredential).mock.results[0]!.value.mutateAsync, - ) - expect(result.current.getEditCredentialService(true)).toBe( - vi.mocked(useEditModelCredential).mock.results[0]!.value.mutateAsync, - ) - expect(result.current.getDeleteCredentialService(true)).toBe( - vi.mocked(useDeleteModelCredential).mock.results[0]!.value.mutateAsync, - ) - expect(result.current.getActiveCredentialService(true)).toBe( - vi.mocked(useActiveModelCredential).mock.results[0]!.value.mutateAsync, - ) + const params = { provider: 'openai' } + expect(mocks.addModelCredential).toHaveBeenCalledWith({ params, body: credential }) + expect(mocks.editModelCredential).toHaveBeenCalledWith({ params, body: credential }) + // The model credential delete endpoint takes its fields as query parameters. + expect(mocks.deleteModelCredential).toHaveBeenCalledWith({ params, query: ref }) + expect(mocks.switchModelCredential).toHaveBeenCalledWith({ params, body: ref }) }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx index 14484fd2f4c3c2..d5fdd8b3c8d984 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx @@ -38,8 +38,19 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () useRefreshModel: () => ({ handleRefreshModel: mockHandleRefreshModel }), })) -vi.mock('@/service/use-models', () => ({ - useDeleteModel: () => ({ mutateAsync: mockDeleteModelService }), +vi.mock('@/service/console', () => ({ + consoleQuery: { + workspaces: { + current: { + modelProviders: { byProvider: { models: { delete: { mutationOptions: () => ({}) } } } }, + }, + }, + }, +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => ({ + ...(await importOriginal()), + useMutation: () => ({ mutateAsync: mockDeleteModelService }), })) vi.mock('../use-auth-service', () => ({ @@ -204,8 +215,8 @@ describe('useAuth', () => { }) expect(mockDeleteModelService).toHaveBeenCalledWith({ - model: 'gpt-4', - model_type: ModelTypeEnum.textGeneration, + params: { provider: 'openai' }, + query: { model: 'gpt-4', model_type: ModelTypeEnum.textGeneration }, }) expect(onRemove).toHaveBeenCalledWith('') }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts index a2dfc4f1b13c0f..7498cdde304155 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts @@ -1,17 +1,14 @@ -import type { CustomModel } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { + CustomModel, + ModelCredentialPayload, + ModelTypeEnum, + ProviderCredential, +} from '@/app/components/header/account-setting/model-provider-page/declarations' +import { skipToken, useMutation, useQuery } from '@tanstack/react-query' import { useCallback } from 'react' -import { - useActiveModelCredential, - useActiveProviderCredential, - useAddModelCredential, - useAddProviderCredential, - useDeleteModelCredential, - useDeleteProviderCredential, - useEditModelCredential, - useEditProviderCredential, - useGetModelCredential, - useGetProviderCredential, -} from '@/service/use-models' +import { consoleClient, consoleQuery } from '@/service/console' + +type ModelCredentialRef = { credential_id: string; model: string; model_type: ModelTypeEnum } export const useGetCredential = ( provider: string, @@ -20,32 +17,70 @@ export const useGetCredential = ( model?: CustomModel, configFrom?: string, ) => { - const providerData = useGetProviderCredential( - !isModelCredential && !!credentialId, - provider, - credentialId, - ) - const modelData = useGetModelCredential( - !!isModelCredential && (!!credentialId || !!model), - provider, - credentialId, - model?.model, - model?.model_type, - configFrom, - ) + const modelProvider = consoleQuery.workspaces.current.modelProviders.byProvider + const providerData = useQuery({ + ...modelProvider.credentials.get.queryOptions({ + input: + !isModelCredential && credentialId + ? { params: { provider }, query: { credential_id: credentialId } } + : skipToken, + }), + staleTime: 0, + gcTime: 0, + }) + const modelData = useQuery({ + ...modelProvider.models.credentials.get.queryOptions({ + input: + isModelCredential && model + ? { + params: { provider }, + query: { + model: model.model, + model_type: model.model_type, + config_from: configFrom, + credential_id: credentialId, + }, + } + : skipToken, + }), + staleTime: 0, + gcTime: 0, + }) return isModelCredential ? modelData : providerData } export const useAuthService = (provider: string) => { - const { mutateAsync: addProviderCredential } = useAddProviderCredential(provider) - const { mutateAsync: editProviderCredential } = useEditProviderCredential(provider) - const { mutateAsync: deleteProviderCredential } = useDeleteProviderCredential(provider) - const { mutateAsync: activeProviderCredential } = useActiveProviderCredential(provider) + const client = consoleClient.workspaces.current.modelProviders.byProvider + const params = { provider } + + const { mutateAsync: addProviderCredential } = useMutation({ + mutationFn: (body: ProviderCredential) => client.credentials.post({ params, body }), + }) + const { mutateAsync: editProviderCredential } = useMutation({ + mutationFn: (body: ProviderCredential) => client.credentials.put({ params, body }), + }) + const { mutateAsync: deleteProviderCredential } = useMutation({ + mutationFn: (body: { credential_id: string }) => client.credentials.delete({ params, body }), + }) + const { mutateAsync: activeProviderCredential } = useMutation({ + mutationFn: (body: { credential_id: string }) => + client.credentials.switch.post({ params, body }), + }) - const { mutateAsync: addModelCredential } = useAddModelCredential(provider) - const { mutateAsync: activeModelCredential } = useActiveModelCredential(provider) - const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider) - const { mutateAsync: editModelCredential } = useEditModelCredential(provider) + const { mutateAsync: addModelCredential } = useMutation({ + mutationFn: (body: ModelCredentialPayload) => client.models.credentials.post({ params, body }), + }) + const { mutateAsync: activeModelCredential } = useMutation({ + mutationFn: (body: ModelCredentialRef) => + client.models.credentials.switch.post({ params, body }), + }) + const { mutateAsync: deleteModelCredential } = useMutation({ + mutationFn: (query: ModelCredentialRef) => client.models.credentials.delete({ params, query }), + }) + const { mutateAsync: editModelCredential } = useMutation({ + mutationFn: (body: ModelCredentialPayload & { credential_id: string }) => + client.models.credentials.put({ params, body }), + }) const getAddCredentialService = useCallback( (isModel: boolean) => { diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts index e8fc5ec14981cf..bf4fc3e02512e3 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts @@ -6,6 +6,7 @@ import type { ModelModalModeEnum, ModelProvider, } from '../../declarations' +import { useMutation } from '@tanstack/react-query' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { @@ -13,7 +14,7 @@ import { useRefreshModel, } from '@/app/components/header/account-setting/model-provider-page/hooks' import { toast } from '@/app/notifications' -import { useDeleteModel } from '@/service/use-models' +import { consoleQuery } from '@/service/console' import { useAuthService } from './use-auth-service' type ProviderCredentialOperationPayload = { @@ -45,7 +46,9 @@ export const useAuth = ( getEditCredentialService, getAddCredentialService, } = useAuthService(provider.provider) - const { mutateAsync: deleteModelService } = useDeleteModel(provider.provider) + const { mutateAsync: deleteModelService } = useMutation( + consoleQuery.workspaces.current.modelProviders.byProvider.models.delete.mutationOptions(), + ) const handleOpenModelModal = useModelModalHandler() const { handleRefreshModel } = useRefreshModel() const pendingOperationCredentialId = useRef(null) @@ -156,7 +159,7 @@ export const useAuth = ( model: pendingOperationModel.current.model, model_type: pendingOperationModel.current.model_type, } - await deleteModelService(payload) + await deleteModelService({ params: { provider: provider.provider }, query: payload }) } handleRefreshModel(provider, undefined, true) onRemove?.(pendingOperationCredentialId.current ?? '') diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx index c9519ee5eae269..b56d037eb50bfa 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx @@ -133,7 +133,7 @@ function Form< if (isEditMode && (key === '__model_type' || key === '__model_name')) return setChangeKey(key) - const shouldClearVariable: Record = {} + const shouldClearVariable: FormValue = {} if (showOnVariableMap[key]?.length) { showOnVariableMap[key].forEach((clearVariable) => { const schema = formSchemas.find((it) => it.variable === clearVariable) diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/dialog.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/dialog.spec.tsx index b937f94b2d5f59..3da2ca569f81ad 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/dialog.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/dialog.spec.tsx @@ -150,7 +150,7 @@ describe('ModelModal dialog branches', () => { provider={createProvider({ help: { title: { en_US: 'English Help' }, - url: '' as unknown as ModelProvider['help']['url'], + url: '' as unknown as NonNullable['url'], } as ModelProvider['help'], })} configurateMethod={ConfigurationMethodEnum.predefinedModel} diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx index 9fafb857421bd5..2781de32fa9d05 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx @@ -185,7 +185,6 @@ const createProvider = (overrides?: Partial): ModelProvider => ({ quota_unit: QuotaUnitEnum.times, quota_limit: 0, quota_used: 0, - last_used: 0, is_valid: true, }, ], diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx index 229d7189d43321..d997c8d3a843cc 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx @@ -398,7 +398,7 @@ const ModelModal: FC = ({ target="_blank" rel="noopener noreferrer" className="mt-2 inline-block align-middle system-xs-regular text-text-accent" - onClick={(e) => !provider.help.url && e.preventDefault()} + onClick={(e) => !provider.help?.url && e.preventDefault()} > {provider.help.title?.[language] || provider.help.url[language] || diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx index 4296ca60102dce..581ee1ab25d450 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx @@ -42,16 +42,6 @@ let activeTextGenerationModelList: Array> = [ }, ] -vi.mock('@/service/use-common', () => ({ - useModelParameterRules: () => ({ - data: { - data: parameterRules, - }, - isLoading: isRulesLoading, - isPending: isRulesPending, - }), -})) - vi.mock('../../hooks', () => ({ useTextGenerationCurrentProviderAndModelAndModelList: () => ({ currentProvider, @@ -151,6 +141,19 @@ vi.mock('@/config', async (importOriginal) => { } }) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.modelProviders.byProvider.models.parameterRules.get': () => ({ + data: { + data: parameterRules, + }, + isLoading: isRulesLoading, + isPending: isRulesPending, + }), + }) +}) + describe('ModelParameterModal', () => { const openSettings = () => fireEvent.click(screen.getByRole('button', { name: /modelProvider\.modelSettings/i })) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx index 5da1cfc3c92f76..7e462e0b8715cc 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx @@ -17,14 +17,15 @@ import { PopoverTitle, PopoverTrigger, } from '@langgenius/dify-ui/popover' +import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE } from '@/config' -import { useModelParameterRules } from '@/service/use-common' import { ModelStatusEnum } from '../declarations' import { useTextGenerationCurrentProviderAndModelAndModelList } from '../hooks' import { ModelSelector, SplitModelSelector } from '../model-selector' +import { modelParameterRulesQueryOptions } from '../utils' import { ModelSettingsTrigger } from './model-settings-trigger' import ParameterItem from './parameter-item' import PresetsParameter from './presets-parameter' @@ -95,10 +96,8 @@ const ModelParameterModal: FC = ({ useTextGenerationCurrentProviderAndModelAndModelList({ provider, model: modelId }) const canFetchParameterRules = !!currentProvider && currentModel?.status === ModelStatusEnum.active - const { data: parameterRulesData, isLoading } = useModelParameterRules( - provider, - modelId, - canFetchParameterRules, + const { data: parameterRulesData, isLoading } = useQuery( + modelParameterRulesQueryOptions({ provider, model: modelId, enabled: canFetchParameterRules }), ) const isRulesLoading = canFetchParameterRules && !!provider && !!modelId && isLoading const selectableModelList = modelList ?? activeTextGenerationModelList diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx index 90b2f07f2e9876..04a3c89a749868 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx @@ -263,7 +263,13 @@ describe('ModelSelector', () => { defaultModel={{ model: 'missing-model', model_type: ModelTypeEnum.textGeneration, - provider: { provider, icon_small: { en_US: '', zh_Hans: '' } }, + provider: { + provider, + icon_small: { en_US: '', zh_Hans: '' }, + label: { en_US: provider, zh_Hans: provider }, + supported_model_types: [], + tenant_id: 'tenant-1', + }, }} />, ) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx index 4cae762dce4619..b055ff7a63f173 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx @@ -9,8 +9,9 @@ import type { ModelSelectorPreviewPayload } from '../popup-item' import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { modelProviderDetailsQueryOptions } from '@/app/components/header/account-setting/model-provider-page/utils' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' +import { createProviderResponseFixture } from '@/test/console/model-provider' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import { ConfigurationMethodEnum, @@ -178,8 +179,13 @@ const createPopupItemNode = (node: ReactElement) => node const renderPopupItem = (node: ReactElement) => { const queryClient = createConsoleQueryClient() - queryClient.setQueryData(commonQueryKeys.modelProviderDetails, { - data: [makeProvider()], + queryClient.setQueryData(modelProviderDetailsQueryOptions().queryKey, { + data: [ + createProviderResponseFixture({ + provider: 'openai', + custom_configuration: { status: 'active', current_credential_name: 'my-api-key' }, + }), + ], }) queryClient.setQueryData(consoleQuery.workspaces.current.modelProviders.summary.get.queryKey(), { ...mockProviderSummary(), diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/index.spec.tsx index 31919f124d7766..7502cc69ba1488 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ import { QueryClient } from '@tanstack/react-query' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { PluginCategoryEnum } from '@/app/components/plugins/types' -import { commonQueryKeys } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { createQueryClientWrapper } from '@/test/console/query-client' import { seedSystemFeatures } from '@/test/console/query-data' import { render } from '@/test/console/render' @@ -51,6 +51,14 @@ vi.mock('@/service/console', () => ({ workspaces: { current: { modelProviders: { + get: { + key: () => [['console', 'workspaces', 'current', 'modelProviders', 'get']], + queryOptions: (options: Record = {}) => ({ + ...options, + queryKey: [['console', 'workspaces', 'current', 'modelProviders', 'get']], + queryFn: async () => ({ data: [] }), + }), + }, summary: { get: { key: () => ['model-provider-summary'] } }, byProvider: { models: { @@ -280,7 +288,7 @@ describe('ProviderAddedCard', () => { expect(mockInvalidateInstalledPluginList).toHaveBeenCalledWith(PluginCategoryEnum.model) expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['model-provider-summary'] }) expect(mockInvalidateQueries).toHaveBeenCalledWith({ - queryKey: commonQueryKeys.modelProviderDetails, + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), }) expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/load-balancing-credential-save.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/load-balancing-credential-save.spec.tsx index f670d5c28e588e..0b22a003b232e0 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/load-balancing-credential-save.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/load-balancing-credential-save.spec.tsx @@ -22,10 +22,16 @@ import ModelLoadBalancingConfigs from '../model-load-balancing-configs' const { mockPost } = vi.hoisted(() => ({ mockPost: vi.fn() })) +// Requests go through the console client, whose browser transport uses `request`. vi.mock('@/service/base', async (importOriginal) => ({ ...(await importOriginal()), - get: vi.fn().mockResolvedValue({ credentials: {} }), - post: mockPost, + request: async (_url: string, _init: RequestInit, { request }: { request: Request }) => { + if (request.method !== 'POST') return Response.json({ credentials: {} }) + mockPost(new URL(request.url).pathname.replace(/^\/console\/api/, ''), { + body: await request.json(), + }) + return Response.json({ result: 'success' }) + }, })) vi.mock('../../hooks', async (importOriginal) => ({ diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx index c212fef3a991be..4888f0969a1cf7 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx @@ -1,7 +1,7 @@ import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ModelItem, ModelProvider } from '../../declarations' import { fireEvent, screen, waitFor } from '@testing-library/react' -import { disableModel, enableModel } from '@/service/common' +import { consoleClient } from '@/service/console' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' import { ModelStatusEnum } from '../../declarations' @@ -28,9 +28,26 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/common', () => ({ - enableModel: vi.fn(), - disableModel: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + workspaces: { + current: { + modelProviders: { + byProvider: { + models: { + enable: { + patch: vi.fn(), + }, + disable: { + patch: vi.fn(), + }, + }, + }, + }, + }, + }, + }, })) vi.mock('../../hooks', () => ({ @@ -119,7 +136,9 @@ describe('ModelListItem', () => { await waitFor( () => { - expect(disableModel).toHaveBeenCalled() + expect( + consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch, + ).toHaveBeenCalled() expect(onChange).toHaveBeenCalledWith('test-provider') }, { timeout: 2000 }, @@ -142,7 +161,9 @@ describe('ModelListItem', () => { await waitFor( () => { - expect(enableModel).toHaveBeenCalled() + expect( + consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch, + ).toHaveBeenCalled() expect(onChange).toHaveBeenCalledWith('test-provider') }, { timeout: 2000 }, @@ -292,8 +313,12 @@ describe('ModelListItem', () => { expect(statusSwitch).toHaveAttribute('aria-disabled', 'true') fireEvent.click(statusSwitch) expect(statusSwitch).toHaveAttribute('aria-checked', 'false') - expect(enableModel).not.toHaveBeenCalled() - expect(disableModel).not.toHaveBeenCalled() + expect( + consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch, + ).not.toHaveBeenCalled() + expect( + consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch, + ).not.toHaveBeenCalled() }) // isConfigurable=true: hover class on row diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx index 93ef2603728b77..975b56312eaaca 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx @@ -72,17 +72,41 @@ vi.mock('@/app/notifications', async (importOriginal) => { } }) -vi.mock('@/service/use-models', () => ({ - useGetModelCredential: () => ({ - isLoading: false, - data: mockCredentialData, - refetch: mockRefetch, - }), - useUpdateModelLoadBalancingConfig: () => ({ - mutateAsync: mockMutateAsync, - }), +vi.mock('@/service/console', () => ({ + consoleQuery: { + workspaces: { + current: { + modelProviders: { + byProvider: { + models: { + credentials: { get: { queryOptions: () => ({ operation: 'model-credential' }) } }, + post: { mutationOptions: () => ({ operation: 'update-load-balancing' }) }, + }, + }, + }, + }, + }, + }, })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useQuery: (options: { operation?: string }) => + options.operation === 'model-credential' + ? { isLoading: false, data: mockCredentialData, refetch: mockRefetch } + : actual.useQuery(options as Parameters[0]), + useMutation: (options: { operation?: string }) => + options.operation === 'update-load-balancing' + ? { + mutateAsync: ({ params, body }: { params: { provider: string }; body: unknown }) => + mockMutateAsync(body, params), + } + : actual.useMutation(options as Parameters[0]), + } +}) + vi.mock('../../model-auth/hooks/use-auth', () => ({ useAuth: () => ({ doingAction: false, @@ -274,6 +298,9 @@ describe('ModelLoadBalancingModal', () => { await waitFor(() => { expect(mockRefetch).toHaveBeenCalled() + expect(mockMutateAsync).toHaveBeenCalledWith(expect.anything(), { + provider: 'test-provider', + }) expect(mockMutateAsync).toHaveBeenCalled() const payload = mockMutateAsync.mock.calls[0]![0] as { load_balancing: { configs: Array<{ credentials: { api_key: string } }> } diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx index 7f84564a72d3b4..1ded58e4ff4cb4 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/provider-card-actions.spec.tsx @@ -7,7 +7,6 @@ import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { PluginSource } from '@/app/components/plugins/types' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' import { renderWithConsoleQuery } from '@/test/console/query-data' import ProviderCardActions from '../provider-card-actions' @@ -286,7 +285,7 @@ describe('ProviderCardActions', () => { queryKey: consoleQuery.workspaces.current.plugin.list.latestVersions.post.key(), }) expect(invalidateQueries).toHaveBeenCalledWith({ - queryKey: commonQueryKeys.modelProviderDetails, + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), }) expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['marketplacePlugins'] }) expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['marketplaceCollectionPlugins'] }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx index f2c6bfcec4c904..083e059a5ce97e 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx @@ -14,7 +14,6 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import { renderI18nObject } from '@/i18n/metadata' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { hasPermission } from '@/utils/permission' import { useModelProviderListExpanded, useSetModelProviderListExpanded } from '../atoms' @@ -128,7 +127,9 @@ const ProviderAddedCard: FC = ({ queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.modelProviders.summary.get.key(), }), - queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelProviderDetails }), + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), + }), invalidateInstalledPluginList(PluginCategoryEnum.model), ]) }, [invalidateInstalledPluginList, queryClient]) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/index.spec.tsx index b22ff59c73e4e2..2aef4ba9a0b34f 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/index.spec.tsx @@ -2,7 +2,8 @@ import type { ModelProviderSummaryResponse } from '@dify/contracts/api/console/w import type { ModelProvider } from '../../../declarations' import type { CredentialPanelState } from '../../use-credential-panel-state' import { act, fireEvent, screen, waitFor } from '@testing-library/react' -import { commonQueryKeys } from '@/service/use-common' +import { modelProviderDetailsQueryOptions } from '@/app/components/header/account-setting/model-provider-page/utils' +import { createProviderResponseFixture } from '@/test/console/model-provider' import { renderWithConsoleQuery } from '@/test/console/query-data' import { CustomConfigurationStatusEnum, PreferredProviderTypeEnum } from '../../../declarations' import ModelAuthDropdown from '../index' @@ -212,7 +213,7 @@ describe('ModelAuthDropdown', () => { }, system_configuration: { enabled: true }, } satisfies ModelProviderSummaryResponse - const fullProvider = createProvider() + const fullProvider = createProviderResponseFixture({ provider: 'test' }) render( { fireEvent.click(screen.getByRole('button', { name: /addApiKey/i })) expect(await screen.findByRole('status')).toBeInTheDocument() + // The console client loads its contract segment before the first request starts. + await waitFor(() => expect(resolveFirstRequest).toBeDefined()) await act(async () => { resolveFirstRequest?.(new Response(null, { status: 500 })) }) @@ -281,9 +284,9 @@ describe('ModelAuthDropdown', () => { }) it('should load provider detail on first click and reuse it on reopen', async () => { - const fullProvider = createProvider({ + const fullProvider = createProviderResponseFixture({ + provider: 'test', custom_configuration: { - status: CustomConfigurationStatusEnum.active, available_credentials: [{ credential_id: 'c1', credential_name: 'Key 1' }], current_credential_id: 'c1', current_credential_name: 'Key 1', @@ -344,17 +347,17 @@ describe('ModelAuthDropdown', () => { current_credential_usable: true, }, } as unknown as ModelProviderSummaryResponse - const firstProvider = createProvider({ + const firstProvider = createProviderResponseFixture({ + provider: 'test', custom_configuration: { - status: CustomConfigurationStatusEnum.active, available_credentials: [{ credential_id: 'c1', credential_name: 'Key 1' }], current_credential_id: 'c1', current_credential_name: 'Key 1', }, }) - const nextProvider = createProvider({ + const nextProvider = createProviderResponseFixture({ + provider: 'test', custom_configuration: { - status: CustomConfigurationStatusEnum.active, available_credentials: [{ credential_id: 'c2', credential_name: 'Key 2' }], current_credential_id: 'c2', current_credential_name: 'Key 2', @@ -379,7 +382,7 @@ describe('ModelAuthDropdown', () => { fireEvent.click(screen.getByRole('button', { name: /config/i })) await waitFor(() => expect(screen.getByText('Key 1')).toBeInTheDocument()) - rendered.queryClient.setQueryData(commonQueryKeys.modelProviderDetails, { + rendered.queryClient.setQueryData(modelProviderDetailsQueryOptions().queryKey, { data: [nextProvider], }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx index 045e54c04b7b33..b74cfda3071b87 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx @@ -8,11 +8,21 @@ const mockUpdateModelProviders = vi.fn() const mockUpdateModelList = vi.fn() let mockIsPending = false -vi.mock('@/service/use-models', () => ({ - useActiveProviderCredential: () => ({ - mutate: mockMutate, - isPending: mockIsPending, - }), +vi.mock('@/service/console', () => ({ + consoleQuery: { + workspaces: { + current: { + modelProviders: { + byProvider: { credentials: { switch: { post: { mutationOptions: () => ({}) } } } }, + }, + }, + }, + }, +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => ({ + ...(await importOriginal()), + useMutation: () => ({ mutate: mockMutate, isPending: mockIsPending }), })) vi.mock('../../../hooks', () => ({ @@ -85,7 +95,7 @@ describe('useActivateCredential', () => { expect(result.current.selectedCredentialId).toBe('cred-2') expect(mockMutate).toHaveBeenCalledWith( - { credential_id: 'cred-2' }, + { params: { provider: 'langgenius/openai/openai' }, body: { credential_id: 'cred-2' } }, expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function), diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts index 3c01efea48d100..7aed0a3cd6aeea 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts @@ -1,15 +1,18 @@ import type { Credential, ModelProvider } from '../../declarations' +import { useMutation } from '@tanstack/react-query' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' -import { useActiveProviderCredential } from '@/service/use-models' +import { consoleQuery } from '@/service/console' import { useUpdateModelList, useUpdateModelProviders } from '../../hooks' export function useActivateCredential(provider: ModelProvider) { const { t } = useTranslation(['common']) const updateModelProviders = useUpdateModelProviders() const updateModelList = useUpdateModelList() - const { mutate, isPending } = useActiveProviderCredential(provider.provider) + const { mutate, isPending } = useMutation( + consoleQuery.workspaces.current.modelProviders.byProvider.credentials.switch.post.mutationOptions(), + ) const [optimisticId, setOptimisticId] = useState() const currentId = provider.custom_configuration.current_credential_id const selectedCredentialId = optimisticId ?? currentId @@ -25,7 +28,10 @@ export function useActivateCredential(provider: ModelProvider) { if (credential.credential_id === selectedIdRef.current) return setOptimisticId(credential.credential_id) mutate( - { credential_id: credential.credential_id }, + { + params: { provider: provider.provider }, + body: { credential_id: credential.credential_id }, + }, { onSuccess: () => { updateModelProviders() @@ -38,7 +44,7 @@ export function useActivateCredential(provider: ModelProvider) { }, ) }, - [mutate, t, updateModelProviders, updateModelList], + [mutate, provider.provider, t, updateModelProviders, updateModelList], ) return { selectedCredentialId, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx index 272eb226a5a0a2..647dd9723ec442 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -11,8 +11,7 @@ import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { deploymentEditionAtom } from '@/features/system-features/state' -import { disableModel, enableModel } from '@/service/common' -import { consoleQuery } from '@/service/console' +import { consoleClient, consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' import { ModelStatusEnum } from '../declarations' import { useUpdateModelList } from '../hooks' @@ -67,15 +66,15 @@ const ModelListItem = ({ const toggleModelEnablingStatus = useCallback( async (enabled: boolean) => { if (enabled) - await enableModel( - `/workspaces/current/model-providers/${provider.provider}/models/enable`, - { model: model.model, model_type: model.model_type }, - ) + await consoleClient.workspaces.current.modelProviders.byProvider.models.enable.patch({ + params: { provider: provider.provider }, + body: { model: model.model, model_type: model.model_type }, + }) else - await disableModel( - `/workspaces/current/model-providers/${provider.provider}/models/disable`, - { model: model.model, model_type: model.model_type }, - ) + await consoleClient.workspaces.current.modelProviders.byProvider.models.disable.patch({ + params: { provider: provider.provider }, + body: { model: model.model, model_type: model.model_type }, + }) queryClient.invalidateQueries({ queryKey: modelProviderModelListQueryKey, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx index 95f1d9691bf158..71e51612b26df6 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx @@ -1,6 +1,7 @@ import type { Credential, CustomConfigurationModelFixedFields, + ModelCredential, ModelItem, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, @@ -17,12 +18,13 @@ import { import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' +import { useMutation, useQuery } from '@tanstack/react-query' import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth' import { toast } from '@/app/notifications' -import { useGetModelCredential, useUpdateModelLoadBalancingConfig } from '@/service/use-models' +import { consoleQuery } from '@/service/console' import { ConfigurationMethodEnum, FormTypeEnum } from '../declarations' import { useRefreshModel } from '../hooks' import { useAuth } from '../model-auth/hooks/use-auth' @@ -60,15 +62,41 @@ const ModelLoadBalancingModal = ({ const [loading, setLoading] = useState(false) const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model' - const { isLoading, data, refetch } = useGetModelCredential( - true, - provider.provider, - credential?.credential_id, - model.model, - model.model_type, - configFrom, + const { isLoading, data, refetch } = useQuery({ + ...consoleQuery.workspaces.current.modelProviders.byProvider.models.credentials.get.queryOptions( + { + input: { + params: { provider: provider.provider }, + query: { + model: model.model, + model_type: model.model_type, + config_from: configFrom, + credential_id: credential?.credential_id, + }, + }, + }, + ), + staleTime: 0, + gcTime: 0, + }) + const modelCredential = useMemo( + () => + data && { + ...data, + current_credential_id: data.current_credential_id ?? undefined, + current_credential_name: data.current_credential_name ?? undefined, + load_balancing: { + enabled: data.load_balancing.enabled, + configs: data.load_balancing.configs.map((config) => ({ + ...config, + credential_id: config.credential_id ?? undefined, + // Credential values are untyped JSON form values in the console API response. + credentials: config.credentials as ModelLoadBalancingConfigEntry['credentials'], + })), + }, + }, + [data], ) - const modelCredential = data const { load_balancing, current_credential_id, available_credentials, current_credential_name } = modelCredential ?? {} const originalConfig = load_balancing @@ -129,8 +157,8 @@ const ModelLoadBalancingModal = ({ }, [extendedSecretFormSchemas, originalConfigMap], ) - const { mutateAsync: updateModelLoadBalancingConfig } = useUpdateModelLoadBalancingConfig( - provider.provider, + const { mutateAsync: updateModelLoadBalancingConfig } = useMutation( + consoleQuery.workspaces.current.modelProviders.byProvider.models.post.mutationOptions(), ) const initialCustomModelCredential = useMemo(() => { if (!current_credential_id) return undefined @@ -147,14 +175,17 @@ const ModelLoadBalancingModal = ({ try { setLoading(true) const res = await updateModelLoadBalancingConfig({ - credential_id: customModelCredential?.credential_id || current_credential_id, - config_from: configFrom, - model: model.model, - model_type: model.model_type, - load_balancing: { - ...draftConfig, - configs: draftConfig!.configs.map(encodeConfigEntrySecretValues), - enabled: Boolean(draftConfig?.enabled), + params: { provider: provider.provider }, + body: { + credential_id: customModelCredential?.credential_id || current_credential_id, + config_from: configFrom, + model: model.model, + model_type: model.model_type, + load_balancing: { + ...draftConfig, + configs: draftConfig!.configs.map(encodeConfigEntrySecretValues), + enabled: Boolean(draftConfig?.enabled), + }, }, }) if (res.result === 'success') { diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx index 5807bf6888fb30..3f4a224c770f4c 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx @@ -31,7 +31,6 @@ import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-v import useTheme from '@/hooks/use-theme' import { consoleQuery } from '@/service/console' import { uninstallPlugin } from '@/service/plugins' -import { commonQueryKeys } from '@/service/use-common' import { normalizeInstalledPluginDetail } from '@/service/use-plugins' import { getMarketplaceUrl } from '@/utils/var' @@ -75,7 +74,9 @@ const ProviderCardActions: FC = (props) => { queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.plugin.list.latestVersions.post.key(), }), - queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelProviderDetails }), + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), + }), queryClient.invalidateQueries({ queryKey: ['marketplacePlugins'] }), queryClient.invalidateQueries({ queryKey: ['marketplaceCollectionPlugins'] }), ]) diff --git a/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx index b39b4db0b44e1e..a8715ea616babe 100644 --- a/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx @@ -135,6 +135,9 @@ const mockModel: DefaultModelResponse = { provider: { provider: 'openai', icon_small: { en_US: '', zh_Hans: '' }, + label: { en_US: 'Provider', zh_Hans: 'Provider' }, + supported_model_types: [], + tenant_id: 'tenant-1', }, } diff --git a/web/app/components/header/account-setting/model-provider-page/utils.ts b/web/app/components/header/account-setting/model-provider-page/utils.ts index 76164b790e0857..79d8578688d866 100644 --- a/web/app/components/header/account-setting/model-provider-page/utils.ts +++ b/web/app/components/header/account-setting/model-provider-page/utils.ts @@ -1,17 +1,25 @@ import type { + CredentialFormSchema as ContractCredentialFormSchema, FetchFrom, ModelFeature, ModelStatus, ModelType, ModelWithProviderEntityResponse, + ParameterRule, + ProviderResponse, } from '@dify/contracts/api/console/workspaces/types.gen' import type { StaticImageData } from 'next/image' import type { + CredentialFormSchema, CredentialFormSchemaSelect, CredentialFormSchemaTextInput, ModelItem, + ModelParameterRule, + ModelProvider, TypeWithI18N, } from './declarations' +import { skipToken } from '@tanstack/react-query' +import { consoleQuery } from '@/service/console' import { ModelProviderQuotaGetPaid } from '@/types/model-provider' import openaiLogo from './assets/openai-small.svg' import tongyiLogo from './assets/tongyi.svg' @@ -197,3 +205,132 @@ const normalizeModelProviderModel = (model: ModelWithProviderEntityResponse): Mo export const normalizeModelProviderModelsResponse = (response: { data: ModelWithProviderEntityResponse[] }): ModelItem[] => response.data.map(normalizeModelProviderModel) + +const isParameterDefault = (value: unknown): value is ModelParameterRule['default'] => + typeof value === 'number' || + typeof value === 'string' || + typeof value === 'boolean' || + (Array.isArray(value) && value.every((item) => typeof item === 'string')) + +export const normalizeModelParameterRule = (rule: ParameterRule): ModelParameterRule => ({ + name: rule.name, + type: rule.type, + label: normalizeModelLabel(rule.label), + help: rule.help ? normalizeModelLabel(rule.help) : undefined, + // `default` is untyped JSON in the console API response. + default: isParameterDefault(rule.default) ? rule.default : undefined, + min: rule.min ?? undefined, + max: rule.max ?? undefined, + precision: rule.precision ?? undefined, + required: rule.required, + use_template: rule.use_template ?? undefined, + options: rule.options, +}) + +export const modelParameterRulesQueryOptions = ({ + provider, + model, + enabled = true, +}: { + provider?: string + model?: string + enabled?: boolean +}) => + consoleQuery.workspaces.current.modelProviders.byProvider.models.parameterRules.get.queryOptions({ + input: provider && model && enabled ? { params: { provider }, query: { model } } : skipToken, + context: { silent: true }, + select: (response) => ({ data: response.data.map(normalizeModelParameterRule) }), + }) + +const normalizeCredentialFormSchema = ( + schema: ContractCredentialFormSchema, +): CredentialFormSchema => ({ + name: schema.variable, + variable: schema.variable, + type: schema.type, + label: normalizeModelLabel(schema.label), + required: schema.required ?? false, + default: schema.default, + show_on: schema.show_on ?? [], + max_length: schema.max_length, + placeholder: schema.placeholder ? normalizeModelLabel(schema.placeholder) : undefined, + options: (schema.options ?? []).map((option) => ({ + label: normalizeModelLabel(option.label), + value: option.value, + show_on: option.show_on ?? [], + })), +}) + +const normalizeModelProvider = (provider: ProviderResponse): ModelProvider => { + const { custom_configuration: customConfiguration } = provider + const modelCredentialSchema = provider.model_credential_schema + return { + provider: provider.provider, + label: normalizeModelLabel(provider.label), + description: provider.description ? normalizeModelLabel(provider.description) : undefined, + help: provider.help + ? { + title: normalizeModelLabel(provider.help.title), + url: normalizeModelLabel(provider.help.url), + } + : undefined, + icon_small: provider.icon_small ? normalizeModelLabel(provider.icon_small) : undefined, + icon_small_dark: provider.icon_small_dark + ? normalizeModelLabel(provider.icon_small_dark) + : undefined, + background: provider.background ?? undefined, + supported_model_types: provider.supported_model_types.map((type) => modelTypeMap[type]), + configurate_methods: provider.configurate_methods.map((method) => fetchFromMap[method]), + provider_credential_schema: provider.provider_credential_schema + ? { + credential_form_schemas: provider.provider_credential_schema.credential_form_schemas.map( + normalizeCredentialFormSchema, + ), + } + : undefined, + model_credential_schema: modelCredentialSchema + ? { + model: { + label: normalizeModelLabel(modelCredentialSchema.model.label), + placeholder: normalizeModelLabel( + modelCredentialSchema.model.placeholder ?? modelCredentialSchema.model.label, + ), + }, + credential_form_schemas: modelCredentialSchema.credential_form_schemas.map( + normalizeCredentialFormSchema, + ), + } + : undefined, + preferred_provider_type: provider.preferred_provider_type, + custom_configuration: { + status: customConfiguration.status, + current_credential_id: customConfiguration.current_credential_id ?? undefined, + current_credential_name: customConfiguration.current_credential_name ?? undefined, + available_credentials: customConfiguration.available_credentials ?? undefined, + custom_models: customConfiguration.custom_models?.map((model) => ({ + model: model.model, + model_type: modelTypeMap[model.model_type], + credentials: model.credentials ?? undefined, + available_model_credentials: model.available_model_credentials, + current_credential_id: model.current_credential_id ?? undefined, + current_credential_name: model.current_credential_name ?? undefined, + })), + can_added_models: customConfiguration.can_added_models?.map((model) => ({ + model: model.model, + model_type: modelTypeMap[model.model_type], + })), + }, + system_configuration: { + enabled: provider.system_configuration.enabled, + current_quota_type: provider.system_configuration.current_quota_type ?? undefined, + quota_configurations: provider.system_configuration.quota_configurations ?? [], + }, + } +} + +// ModelProvider is the UI's view of the provider list, with credential schemas as form schemas. +export const modelProviderDetailsQueryOptions = () => + consoleQuery.workspaces.current.modelProviders.get.queryOptions({ + input: {}, + select: (response) => ({ data: response.data.map(normalizeModelProvider) }), + }) diff --git a/web/app/components/header/account-setting/permissions-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/permissions-page/__tests__/index.spec.tsx index a18f10f5308f5b..d2e7604af4a48b 100644 --- a/web/app/components/header/account-setting/permissions-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/permissions-page/__tests__/index.spec.tsx @@ -1,14 +1,23 @@ import type { Role } from '@/models/access-control' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { - useCreateWorkspaceRole, - useUpdateWorkspaceRole, -} from '@/service/access-control/use-workspace-roles' import { render } from '@/test/console/render' import { useRoleGroups } from '../hooks' import PermissionsPage from '../index' +const { mockCreateWorkspaceRoleHook, mockUpdateWorkspaceRoleHook } = vi.hoisted(() => ({ + mockCreateWorkspaceRoleHook: vi.fn(), + mockUpdateWorkspaceRoleHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.post': () => mockCreateWorkspaceRoleHook(), + 'workspaces.current.rbac.roles.byRoleId.put': () => mockUpdateWorkspaceRoleHook(), + }) +}) + const mocks = vi.hoisted(() => ({ workspacePermissionKeys: [] as string[], createWorkspaceRole: vi.fn(), @@ -28,11 +37,6 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/access-control/use-workspace-roles', () => ({ - useCreateWorkspaceRole: vi.fn(), - useUpdateWorkspaceRole: vi.fn(), -})) - vi.mock('../hooks', () => ({ useRoleGroups: vi.fn(), })) @@ -154,14 +158,14 @@ describe('PermissionsPage', () => { hasNextPage: false, error: null, } as ReturnType) - vi.mocked(useCreateWorkspaceRole).mockReturnValue( + vi.mocked(mockCreateWorkspaceRoleHook).mockReturnValue( mockMutation(mocks.createWorkspaceRole) as unknown as ReturnType< - typeof useCreateWorkspaceRole + typeof mockCreateWorkspaceRoleHook >, ) - vi.mocked(useUpdateWorkspaceRole).mockReturnValue( + vi.mocked(mockUpdateWorkspaceRoleHook).mockReturnValue( mockMutation(mocks.updateWorkspaceRole) as unknown as ReturnType< - typeof useUpdateWorkspaceRole + typeof mockUpdateWorkspaceRoleHook >, ) }) @@ -202,9 +206,11 @@ describe('PermissionsPage', () => { expect(mocks.createWorkspaceRole).toHaveBeenCalledWith( { - name: 'create name', - description: 'create description', - permission_keys: ['workspace.member.manage'], + body: { + name: 'create name', + description: 'create description', + permission_keys: ['workspace.member.manage'], + }, }, expect.any(Object), ) @@ -221,10 +227,12 @@ describe('PermissionsPage', () => { expect(mocks.updateWorkspaceRole).toHaveBeenCalledWith( { - id: 'role-1', - name: 'edit name', - description: 'edit description', - permission_keys: ['workspace.member.manage'], + params: { role_id: 'role-1' }, + body: { + name: 'edit name', + description: 'edit description', + permission_keys: ['workspace.member.manage'], + }, }, expect.any(Object), ) diff --git a/web/app/components/header/account-setting/permissions-page/hooks.ts b/web/app/components/header/account-setting/permissions-page/hooks.ts index 19185eff73af20..19369b635e6190 100644 --- a/web/app/components/header/account-setting/permissions-page/hooks.ts +++ b/web/app/components/header/account-setting/permissions-page/hooks.ts @@ -1,6 +1,7 @@ import type { RoleListRequest } from '@/models/access-control' +import { useInfiniteQuery } from '@tanstack/react-query' import { useMemo } from 'react' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' +import { workspaceRoleListInfiniteOptions } from '../workspace-role-list-query' import { formatRoleGroups } from './helpers' export const useRoleGroups = (params: RoleListRequest) => { @@ -11,7 +12,7 @@ export const useRoleGroups = (params: RoleListRequest) => { fetchNextPage, hasNextPage, error, - } = useWorkspaceRoleList(params) + } = useInfiniteQuery(workspaceRoleListInfiniteOptions(params)) const roleGroups = useMemo(() => formatRoleGroups(roleList), [roleList]) diff --git a/web/app/components/header/account-setting/permissions-page/index.tsx b/web/app/components/header/account-setting/permissions-page/index.tsx index f2753daffaabd8..3e81421d49174c 100644 --- a/web/app/components/header/account-setting/permissions-page/index.tsx +++ b/web/app/components/header/account-setting/permissions-page/index.tsx @@ -3,16 +3,14 @@ import type { RoleModalMode, submitRoleData } from './role-modal' import type { Role } from '@/models/access-control' import { Button } from '@langgenius/dify-ui/button' +import { useMutation } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { getAccessControlTemplateLanguage } from '@/i18n/language' -import { - useCreateWorkspaceRole, - useUpdateWorkspaceRole, -} from '@/service/access-control/use-workspace-roles' +import { consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' import { useRoleGroups } from './hooks' import RoleList from './role-list' @@ -47,8 +45,12 @@ const PermissionsPage = ({ containerRef }: PermissionsPageProps) => { language, }) - const { mutateAsync: createWorkspaceRole } = useCreateWorkspaceRole() - const { mutateAsync: updateWorkspaceRole } = useUpdateWorkspaceRole() + const { mutateAsync: createWorkspaceRole } = useMutation( + consoleQuery.workspaces.current.rbac.roles.post.mutationOptions(), + ) + const { mutateAsync: updateWorkspaceRole } = useMutation( + consoleQuery.workspaces.current.rbac.roles.byRoleId.put.mutationOptions(), + ) const openCreate = useCallback(() => { setModalState({ mode: 'create' }) @@ -71,7 +73,7 @@ const PermissionsPage = ({ containerRef }: PermissionsPageProps) => { const roleId = modalState?.role?.id ?? '' if (mode === 'create') { createWorkspaceRole( - { name, description, permission_keys: permissionKeys }, + { body: { name, description, permission_keys: permissionKeys } }, { onSuccess: () => { closeModal() @@ -80,7 +82,10 @@ const PermissionsPage = ({ containerRef }: PermissionsPageProps) => { ) } else if (mode === 'edit') { updateWorkspaceRole( - { id: roleId, name, description, permission_keys: permissionKeys }, + { + params: { role_id: roleId }, + body: { name, description, permission_keys: permissionKeys }, + }, { onSuccess: () => { closeModal() diff --git a/web/app/components/header/account-setting/permissions-page/role-list/__tests__/index.spec.tsx b/web/app/components/header/account-setting/permissions-page/role-list/__tests__/index.spec.tsx index fab8752fdb4b4b..6368cc767b966b 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/__tests__/index.spec.tsx @@ -14,28 +14,6 @@ vi.mock('@/context/workspace-state', async () => { })) }) -vi.mock('@/service/access-control/use-workspace-roles', () => ({ - useCopyWorkspaceRole: () => ({ - mutateAsync: vi.fn(), - }), - useDeleteWorkspaceRole: () => ({ - mutateAsync: vi.fn(), - isPending: false, - }), - useGetMembersOfRole: () => ({ - data: { - data: [], - pagination: { - total_count: 0, - per_page: 1, - current_page: 1, - total_pages: 1, - }, - }, - isPending: false, - }), -})) - const createRole = (overrides: Partial = {}): Role => ({ id: 'role-1', tenant_id: 'tenant-1', @@ -57,6 +35,31 @@ vi.mock('@/context/permission-state', async () => { })) }) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.byRoleId.copy.post': () => ({ + mutateAsync: vi.fn(), + }), + 'workspaces.current.rbac.roles.byRoleId.delete': () => ({ + mutateAsync: vi.fn(), + isPending: false, + }), + 'workspaces.current.rbac.roles.byRoleId.members.get': () => ({ + data: { + data: [], + pagination: { + total_count: 0, + per_page: 1, + current_page: 1, + total_pages: 1, + }, + }, + isPending: false, + }), + }) +}) + describe('RoleList', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/web/app/components/header/account-setting/permissions-page/role-list/__tests__/row-menu.spec.tsx b/web/app/components/header/account-setting/permissions-page/role-list/__tests__/row-menu.spec.tsx index 0e975884a67085..3bc03870d67a6a 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/__tests__/row-menu.spec.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/__tests__/row-menu.spec.tsx @@ -26,7 +26,7 @@ const membersOfRoleQueryMock = vi.hoisted(() => { } return { - useGetMembersOfRole: vi.fn(() => ({ + getMembersOfRole: vi.fn((_options?: unknown) => ({ data: response, isPending: false, })), @@ -40,18 +40,6 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/access-control/use-workspace-roles', () => ({ - useCopyWorkspaceRole: () => ({ - mutate: mockCopyRole, - isPending: false, - }), - useDeleteWorkspaceRole: () => ({ - mutateAsync: mockDeleteRole, - isPending: false, - }), - useGetMembersOfRole: membersOfRoleQueryMock.useGetMembersOfRole, -})) - vi.mock('@/app/notifications', () => ({ toast: { success: vi.fn(), @@ -86,6 +74,22 @@ const openMenu = async () => { } } +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.rbac.roles.byRoleId.copy.post': () => ({ + mutate: mockCopyRole, + isPending: false, + }), + 'workspaces.current.rbac.roles.byRoleId.delete': () => ({ + mutateAsync: mockDeleteRole, + isPending: false, + }), + 'workspaces.current.rbac.roles.byRoleId.members.get': (options: unknown) => + membersOfRoleQueryMock.getMembersOfRole(options), + }) +}) + describe('RowMenu', () => { beforeEach(() => { vi.clearAllMocks() @@ -227,20 +231,22 @@ describe('RowMenu', () => { expect(dialog).toHaveTextContent( 'permission.role.copyMembersDescription:{"name":"Editor","count":3}', ) - expect(membersOfRoleQueryMock.useGetMembersOfRole).toHaveBeenCalledWith({ - roleId: role.id, - page: 1, - limit: 9999, - }) + expect(membersOfRoleQueryMock.getMembersOfRole).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: [ + expect.any(Array), + expect.objectContaining({ + input: { params: { role_id: role.id }, query: { page: 1, limit: 9999 } }, + }), + ], + }), + ) await user.click(within(dialog).getByRole('button', { name: 'common.operation.skip' })) expect(mockCopyRole).toHaveBeenCalledTimes(1) expect(mockCopyRole).toHaveBeenCalledWith( - { - roleId: role.id, - copy_member: false, - }, + { params: { role_id: role.id }, body: { copy_member: false } }, expect.objectContaining({ onSuccess: expect.any(Function), }), @@ -266,10 +272,7 @@ describe('RowMenu', () => { expect(mockCopyRole).toHaveBeenCalledTimes(1) expect(mockCopyRole).toHaveBeenCalledWith( - { - roleId: role.id, - copy_member: true, - }, + { params: { role_id: role.id }, body: { copy_member: true } }, expect.objectContaining({ onSuccess: expect.any(Function), }), diff --git a/web/app/components/header/account-setting/permissions-page/role-list/copy-members-confirm-dialog.tsx b/web/app/components/header/account-setting/permissions-page/role-list/copy-members-confirm-dialog.tsx index 94416edefa9e86..6f597cb3d44dca 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/copy-members-confirm-dialog.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/copy-members-confirm-dialog.tsx @@ -9,8 +9,9 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' +import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' -import { useGetMembersOfRole } from '@/service/access-control/use-workspace-roles' +import { consoleQuery } from '@/service/console' type CopyMembersConfirmDialogProps = { role: Role @@ -26,12 +27,12 @@ export function CopyMembersConfirmDialog({ onDuplicate, }: CopyMembersConfirmDialogProps) { const { t } = useTranslation(['common', 'permission']) - const { data: membersOfRole, isPending: isLoadingMembersOfRole } = useGetMembersOfRole({ - roleId: role.id, - page: 1, - limit: 9999, - }) - const memberCount = membersOfRole?.pagination.total_count ?? 0 + const { data: membersOfRole, isPending: isLoadingMembersOfRole } = useQuery( + consoleQuery.workspaces.current.rbac.roles.byRoleId.members.get.queryOptions({ + input: { params: { role_id: role.id }, query: { page: 1, limit: 9999 } }, + }), + ) + const memberCount = membersOfRole?.pagination?.total_count ?? 0 const isLoadingMemberCount = isLoadingMembersOfRole && !membersOfRole const isActionDisabled = isCopyingRole || isLoadingMemberCount diff --git a/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx b/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx index 84eacc27321361..855c0d187da657 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx @@ -17,14 +17,12 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' +import { useMutation } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { - useCopyWorkspaceRole, - useDeleteWorkspaceRole, -} from '@/service/access-control/use-workspace-roles' +import { consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' import { CopyMembersConfirmDialog } from './copy-members-confirm-dialog' @@ -51,7 +49,9 @@ const RowMenu = ({ roleCategory, role, onView, onEdit }: RowMenuProps) => { onEdit?.(role) }, [canManageRoles, onEdit, role]) - const { mutate: copyRole, isPending: isCopyingRole } = useCopyWorkspaceRole() + const { mutate: copyRole, isPending: isCopyingRole } = useMutation( + consoleQuery.workspaces.current.rbac.roles.byRoleId.copy.post.mutationOptions(), + ) const openCopyMembersConfirm = useCallback(() => { if (!canManageRoles) return @@ -64,10 +64,7 @@ const RowMenu = ({ roleCategory, role, onView, onEdit }: RowMenuProps) => { if (!canManageRoles) return copyRole( - { - roleId: role.id, - copy_member: copyMember, - }, + { params: { role_id: role.id }, body: { copy_member: copyMember } }, { onSuccess: () => { setShowCopyMembersConfirm(false) @@ -78,7 +75,9 @@ const RowMenu = ({ roleCategory, role, onView, onEdit }: RowMenuProps) => { [canManageRoles, copyRole, role.id], ) - const { mutateAsync: deleteRole, isPending: isDeletingRole } = useDeleteWorkspaceRole() + const { mutateAsync: deleteRole, isPending: isDeletingRole } = useMutation( + consoleQuery.workspaces.current.rbac.roles.byRoleId.delete.mutationOptions(), + ) const openDeleteConfirm = useCallback(() => { if (!canManageRoles) return @@ -89,11 +88,14 @@ const RowMenu = ({ roleCategory, role, onView, onEdit }: RowMenuProps) => { const handleDelete = useCallback(() => { if (!canManageRoles) return - deleteRole(role.id, { - onSuccess: () => { - setShowDeleteConfirm(false) + deleteRole( + { params: { role_id: role.id } }, + { + onSuccess: () => { + setShowDeleteConfirm(false) + }, }, - }) + ) }, [canManageRoles, deleteRole, role.id]) const hasViewAction = roleCategory === 'global_system_default' diff --git a/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx b/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx index 1f8ce6bfe083b4..83c24488dd0972 100644 --- a/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx +++ b/web/app/components/header/account-setting/workspace-role-checkbox-list.tsx @@ -11,12 +11,16 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from '@langgenius/dify-ui/scroll-area' +import { useInfiniteQuery } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { SearchInput } from '@/app/components/base/search-input' import { getAccessControlTemplateLanguage } from '@/i18n/language' -import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' +import { + createSelectedRolePlaceholder, + workspaceRoleListInfiniteOptions, +} from './workspace-role-list-query' type WorkspaceRoleCheckboxListProps = { selectedRoleIds: string[] @@ -38,18 +42,6 @@ const LEGACY_ROLE_DESCRIPTION_KEY_MAP = { type LegacyRoleKey = keyof typeof LEGACY_ROLE_DESCRIPTION_KEY_MAP -const createSelectedRolePlaceholder = (id: string): Role => ({ - id, - tenant_id: '', - type: 'workspace', - category: 'global_custom', - name: id, - description: '', - is_builtin: false, - permission_keys: [], - role_tag: '', -}) - const normalizeLegacyRoleKey = (value: string) => value.trim().toLowerCase() const isLegacyRoleKey = (value: string): value is LegacyRoleKey => @@ -83,12 +75,14 @@ const WorkspaceRoleCheckboxList = ({ hasNextPage, isFetchingNextPage, fetchNextPage, - } = useWorkspaceRoleList({ - page: 1, - limit: PAGE_SIZE, - include_owner: includeOwner ? 1 : 0, - language, - }) + } = useInfiniteQuery( + workspaceRoleListInfiniteOptions({ + page: 1, + limit: PAGE_SIZE, + include_owner: includeOwner ? 1 : 0, + language, + }), + ) useEffect(() => { const hasMore = hasNextPage ?? true @@ -131,7 +125,7 @@ const WorkspaceRoleCheckboxList = ({ if (selectedRoles) return selectedRoles return selectedRoleIds.map( - (roleId) => roleById.get(roleId) ?? createSelectedRolePlaceholder(roleId), + (roleId) => roleById.get(roleId) ?? createSelectedRolePlaceholder({ id: roleId }), ) }, [roleById, selectedRoleIds, selectedRoles]) diff --git a/web/app/components/header/account-setting/workspace-role-list-query.ts b/web/app/components/header/account-setting/workspace-role-list-query.ts new file mode 100644 index 00000000000000..848d4311253c6b --- /dev/null +++ b/web/app/components/header/account-setting/workspace-role-list-query.ts @@ -0,0 +1,64 @@ +import type { RbacRole, RbacRoleList } from '@dify/contracts/api/console/workspaces/types.gen' +import type { InfiniteData } from '@tanstack/react-query' +import type { Role, RoleListRequest, RoleListResponse } from '@/models/access-control' +import { consoleQuery } from '@/service/console' + +const ROLE_TYPES = new Set(['workspace', 'app', 'dataset']) +const ROLE_TAGS = new Set(['owner', 'admin']) + +// The API serializes every field but declares defaulted ones as optional strings. +export const toRole = (role: RbacRole): Role => ({ + id: role.id, + tenant_id: role.tenant_id ?? '', + type: ROLE_TYPES.has(role.type) ? (role.type as Role['type']) : 'workspace', + category: role.category === 'global_system_default' ? role.category : 'global_custom', + name: role.name, + description: role.description ?? '', + is_builtin: role.is_builtin ?? false, + permission_keys: role.permission_keys ?? [], + role_tag: + role.role_tag && ROLE_TAGS.has(role.role_tag) ? (role.role_tag as Role['role_tag']) : '', +}) + +// Stands in for a selected role whose full definition has not been loaded. +export const createSelectedRolePlaceholder = ({ + id, + name, +}: { + id: string + name?: string +}): Role => ({ + id, + tenant_id: '', + type: 'workspace', + category: 'global_custom', + name: name ?? id, + description: '', + is_builtin: false, + permission_keys: [], + role_tag: '', +}) + +const toRoleListPage = (page: RbacRoleList): RoleListResponse => ({ + data: (page.data ?? []).map(toRole), + pagination: { + total_count: page.pagination?.total_count ?? 0, + per_page: page.pagination?.per_page ?? 0, + current_page: page.pagination?.current_page ?? 0, + total_pages: page.pagination?.total_pages ?? 0, + }, +}) + +export const workspaceRoleListInfiniteOptions = ({ page = 1, ...query }: RoleListRequest) => + consoleQuery.workspaces.current.rbac.roles.get.infiniteOptions({ + input: (pageParam: number) => ({ query: { ...query, page: pageParam } }), + initialPageParam: page, + getNextPageParam: ({ pagination }) => { + const currentPage = pagination?.current_page ?? 0 + return currentPage < (pagination?.total_pages ?? 0) ? currentPage + 1 : undefined + }, + select: (data): InfiniteData => ({ + ...data, + pages: data.pages.map(toRoleListPage), + }), + }) diff --git a/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx b/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx index 7af17b95e3d4c3..bea04935c5449d 100644 --- a/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx +++ b/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx @@ -96,11 +96,22 @@ const mockRefetch = vi.fn() const mockUseAllToolProviders = vi.hoisted(() => vi.fn()) const mockUseAllCustomTools = vi.hoisted(() => vi.fn()) const mockUseAllWorkflowTools = vi.hoisted(() => vi.fn()) -vi.mock('@/service/use-tools', () => ({ - useAllToolProviders: (enabled?: boolean, type?: string) => mockUseAllToolProviders(enabled, type), - useAllCustomTools: (enabled?: boolean) => mockUseAllCustomTools(enabled), - useAllWorkflowTools: (enabled?: boolean) => mockUseAllWorkflowTools(enabled), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProviders.get': ({ + input, + enabled, + }: { + input: { query: { type?: string } } + enabled?: boolean + }) => mockUseAllToolProviders(enabled, input.query.type), + 'workspaces.current.tools.api.get': ({ enabled }: { enabled?: boolean }) => + mockUseAllCustomTools(enabled), + 'workspaces.current.tools.workflow.get': ({ enabled }: { enabled?: boolean }) => + mockUseAllWorkflowTools(enabled), + }) +}) const mockConsoleState = vi.hoisted(() => ({ workspacePermissionKeys: ['tool.manage', 'mcp.manage'] as string[], diff --git a/web/app/components/integrations/tool-provider-list.tsx b/web/app/components/integrations/tool-provider-list.tsx index e7ef086a3f4cda..10906bda1d0e42 100644 --- a/web/app/components/integrations/tool-provider-list.tsx +++ b/web/app/components/integrations/tool-provider-list.tsx @@ -11,7 +11,7 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from '@langgenius/dify-ui/scroll-area' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { isSearchResultEmpty } from '@/app/components/base/search-input/search-state' @@ -32,9 +32,13 @@ import { BuiltinMarketplacePanel } from '@/app/components/tools/marketplace/buil import MCPList from '@/app/components/tools/mcp' import ProviderDetail from '@/app/components/tools/provider/detail' import { ToolProviderGrid } from '@/app/components/tools/tool-provider-grid' +import { + selectToolCollections, + selectToolsWithProvider, +} from '@/app/components/tools/utils/query-select' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQuery } from '@/service/console' import { useCheckInstalled, useInvalidateInstalledPluginList } from '@/service/use-plugins' -import { useAllCustomTools, useAllToolProviders, useAllWorkflowTools } from '@/service/use-tools' import { useToolProviderCategory } from './hooks/use-tool-provider-category' import ToolProviderCreateAction from './tool-provider-create-action' import { ToolProviderToolbar } from './tool-provider-toolbar' @@ -85,12 +89,25 @@ const ProviderList = ({ category, contentInset = 'default', layout }: ProviderLi const handleCreatedMCPProviderHandled = useCallback(() => { setCreatedMCPProviderId(undefined) }, []) - const allToolProvidersQuery = useAllToolProviders( - activeTab === 'builtin' || activeTab === 'mcp', - activeTab === 'mcp' ? 'mcp' : undefined, + const allToolProvidersQuery = useQuery( + consoleQuery.workspaces.current.toolProviders.get.queryOptions({ + input: { query: activeTab === 'mcp' ? { type: 'mcp' } : {} }, + enabled: activeTab === 'builtin' || activeTab === 'mcp', + select: selectToolCollections, + }), + ) + const customToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: activeTab === 'api', + select: selectToolsWithProvider, + }), + ) + const workflowToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: activeTab === 'workflow', + select: selectToolsWithProvider, + }), ) - const customToolsQuery = useAllCustomTools(activeTab === 'api') - const workflowToolsQuery = useAllWorkflowTools(activeTab === 'workflow') const { refetch: refetchMcpTools } = allToolProvidersQuery const activeToolsQuery = activeTab === 'api' diff --git a/web/app/components/plugins/install-plugin/__tests__/utils.spec.ts b/web/app/components/plugins/install-plugin/__tests__/utils.spec.ts index 9c6dc2331782b4..f27ff67e83f8af 100644 --- a/web/app/components/plugins/install-plugin/__tests__/utils.spec.ts +++ b/web/app/components/plugins/install-plugin/__tests__/utils.spec.ts @@ -4,8 +4,10 @@ import { PluginCategoryEnum } from '../../types' import { convertRepoToUrl, parseGitHubUrl, + pluginDeclarationToCardPluginProps, pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps, + toPluginText, } from '../utils' // Mock es-toolkit/compat @@ -199,6 +201,45 @@ describe('pluginManifestToCardPluginProps', () => { }) }) +describe('pluginDeclarationToCardPluginProps', () => { + const manifest = { + author: null, + category: 'agent-strategy', + created_at: '2026-01-01T00:00:00Z', + description: { en_US: 'Description', zh_Hans: null }, + icon: 'icon.svg', + label: { en_US: 'Agent', zh_Hans: '代理' }, + meta: { minimum_dify_version: null, version: '0.0.1' }, + name: 'agent', + plugins: {}, + resource: { memory: 0 }, + version: '1.0.0', + } satisfies Parameters[0] + + it('reads the uploaded package as a card', () => { + expect( + pluginDeclarationToCardPluginProps(manifest, 'langgenius/agent:1.0.0@hash'), + ).toMatchObject({ + plugin_id: 'langgenius/agent:1.0.0@hash', + type: 'agent_strategy', + category: 'agent-strategy', + org: '', + author: undefined, + label: { en_US: 'Agent', zh_Hans: '代理' }, + description: { en_US: 'Description' }, + verified: false, + tags: [], + }) + }) + + it('leaves untranslated locales out of card text', () => { + expect(toPluginText({ en_US: 'Name', ja_JP: null, pt_BR: 'Nome' })).toEqual({ + en_US: 'Name', + pt_BR: 'Nome', + }) + }) +}) + describe('pluginManifestInMarketToPluginProps', () => { const createMockPluginManifestInMarket = ( overrides?: Partial, diff --git a/web/app/components/plugins/install-plugin/base/check-task-status.ts b/web/app/components/plugins/install-plugin/base/check-task-status.ts index 9206aa727a11a6..e03c64eee861bd 100644 --- a/web/app/components/plugins/install-plugin/base/check-task-status.ts +++ b/web/app/components/plugins/install-plugin/base/check-task-status.ts @@ -1,4 +1,3 @@ -import type { PluginStatus } from '../../types' import { checkTaskStatus as fetchCheckTaskStatus } from '@/service/plugins' import { sleep } from '@/utils' import { TaskStatus } from '../../types' @@ -24,9 +23,7 @@ function checkTaskStatus() { } const res = await fetchCheckTaskStatus(taskId) const { plugins } = res.task - const plugin = plugins.find( - (p: PluginStatus) => p.plugin_unique_identifier === pluginUniqueIdentifier, - ) + const plugin = plugins.find((p) => p.plugin_unique_identifier === pluginUniqueIdentifier) if (!plugin) { nextStatus = TaskStatus.failed return { diff --git a/web/app/components/plugins/install-plugin/hooks/__tests__/use-refresh-plugin-list.spec.ts b/web/app/components/plugins/install-plugin/hooks/__tests__/use-refresh-plugin-list.spec.ts index 300a1c64257a3f..0fd9b191a5555c 100644 --- a/web/app/components/plugins/install-plugin/hooks/__tests__/use-refresh-plugin-list.spec.ts +++ b/web/app/components/plugins/install-plugin/hooks/__tests__/use-refresh-plugin-list.spec.ts @@ -4,7 +4,6 @@ import { act, renderHook, waitFor } from '@testing-library/react' import { createElement } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' import { PluginCategoryEnum } from '../../../types' import useRefreshPluginList from '../use-refresh-plugin-list' @@ -13,11 +12,7 @@ const mocks = vi.hoisted(() => ({ installed: vi.fn(), checkInstalled: vi.fn(), defaultModel: vi.fn(), - toolProviders: vi.fn(), - builtInTools: vi.fn(), - dataSourceAuth: vi.fn(), triggers: vi.fn(), - recommended: vi.fn(), })) vi.mock('@/service/base', () => ({ request: mocks.request })) @@ -28,14 +23,6 @@ vi.mock('@/service/use-plugins', () => ({ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useInvalidateDefaultModel: () => mocks.defaultModel, })) -vi.mock('@/service/use-tools', () => ({ - useInvalidateAllToolProviders: () => mocks.toolProviders, - useInvalidateAllBuiltInTools: () => mocks.builtInTools, - useInvalidateRAGRecommendedPlugins: () => mocks.recommended, -})) -vi.mock('@/service/use-datasource', () => ({ - useInvalidDataSourceListAuth: () => mocks.dataSourceAuth, -})) vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => mocks.triggers, })) @@ -52,9 +39,21 @@ const strategyKeys = [ ] const providerKeys = [ current.modelProviders.summary.get.key(), - commonQueryKeys.modelProviderDetails, + consoleQuery.workspaces.current.modelProviders.get.key(), ] const datasourceKey = consoleQuery.rag.pipelines.datasourcePlugins.get.queryKey() +const dataSourceAuthKey = consoleQuery.auth.plugin.datasource.list.get.queryKey() +const toolKeys = [ + current.toolProviders.get.queryKey({ input: { query: {} } }), + current.toolProviders.get.queryKey({ input: { query: { type: 'mcp' } } }), + current.tools.builtin.get.queryKey(), + consoleQuery.rag.pipelines.recommendedPlugins.get.queryKey({ + input: { query: { type: 'tool' } }, + }), +] +const otherRecommendedKey = consoleQuery.rag.pipelines.recommendedPlugins.get.queryKey({ + input: { query: { type: 'datasource' } }, +}) const unrelatedKey = consoleQuery.account.profile.get.key() const modelTypes = ['llm', 'text-embedding', 'rerank', 'speech2text', 'tts'] @@ -72,6 +71,8 @@ describe('plugin installation refresh', () => { for (const key of [...strategyKeys, ...providerKeys, unrelatedKey]) client.setQueryData(key, { cached: true }) client.setQueryData(datasourceKey, []) + client.setQueryData(dataSourceAuthKey, { result: [] }) + for (const key of [...toolKeys, otherRecommendedKey]) client.setQueryData(key, []) for (const model_type of modelTypes) { client.setQueryData( current.models.modelTypes.byModelType.get.queryKey({ input: { params: { model_type } } }), @@ -108,14 +109,12 @@ describe('plugin installation refresh', () => { expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe( category === PluginCategoryEnum.datasource, ) - if (category === PluginCategoryEnum.tool) { - expect(mocks.toolProviders).toHaveBeenCalledOnce() - expect(mocks.builtInTools).toHaveBeenCalledOnce() - expect(mocks.recommended).toHaveBeenCalledWith('tool') - } + for (const key of toolKeys) + expect(client.getQueryState(key)?.isInvalidated).toBe(category === PluginCategoryEnum.tool) + expect(client.getQueryState(otherRecommendedKey)?.isInvalidated).toBe(false) if (category === PluginCategoryEnum.datasource) { expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe(true) - expect(mocks.dataSourceAuth).toHaveBeenCalledOnce() + expect(client.getQueryState(dataSourceAuthKey)?.isInvalidated).toBe(true) } if (category === PluginCategoryEnum.trigger) expect(mocks.triggers).toHaveBeenCalledOnce() }, @@ -139,12 +138,10 @@ describe('plugin installation refresh', () => { expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe(all) if (all) { expect(mocks.installed).toHaveBeenCalledWith() - expect(mocks.toolProviders).toHaveBeenCalledOnce() - expect(mocks.builtInTools).toHaveBeenCalledOnce() + for (const key of toolKeys) expect(client.getQueryState(key)?.isInvalidated).toBe(true) expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe(true) - expect(mocks.dataSourceAuth).toHaveBeenCalledOnce() + expect(client.getQueryState(dataSourceAuthKey)?.isInvalidated).toBe(true) expect(mocks.triggers).toHaveBeenCalledOnce() - expect(mocks.recommended).toHaveBeenCalledWith('tool') } }) @@ -159,7 +156,7 @@ describe('plugin installation refresh', () => { expect(mocks.checkInstalled).toHaveBeenCalledOnce() for (const key of [...strategyKeys, ...providerKeys]) expect(client.getQueryState(key)?.isInvalidated).toBe(false) - expect(mocks.toolProviders).not.toHaveBeenCalled() + for (const key of toolKeys) expect(client.getQueryState(key)?.isInvalidated).toBe(false) expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe(false) expect(mocks.triggers).not.toHaveBeenCalled() expect(mocks.request).not.toHaveBeenCalled() diff --git a/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx b/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx index 9b62ae969fdcb0..e8ae6fecbe32da 100644 --- a/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx +++ b/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx @@ -3,17 +3,10 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useInvalidateDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' -import { useInvalidDataSourceListAuth } from '@/service/use-datasource' import { useInvalidateCheckInstalled, useInvalidateInstalledPluginList, } from '@/service/use-plugins' -import { - useInvalidateAllBuiltInTools, - useInvalidateAllToolProviders, - useInvalidateRAGRecommendedPlugins, -} from '@/service/use-tools' import { useInvalidateAllTriggerPlugins } from '@/service/use-triggers' import { PluginCategoryEnum } from '../../types' @@ -60,14 +53,7 @@ const useRefreshPluginList = () => { const invalidateDefaultModel = useInvalidateDefaultModel() const queryClient = useQueryClient() - const invalidateAllToolProviders = useInvalidateAllToolProviders() - const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools() - - const invalidateDataSourceListAuth = useInvalidDataSourceListAuth() - const invalidateAllTriggerPlugins = useInvalidateAllTriggerPlugins() - - const invalidateRAGRecommendedPlugins = useInvalidateRAGRecommendedPlugins() return { refreshPluginList: ( manifest?: PluginManifestInMarket | Plugin | PluginDeclaration | PluginCategoryPayload | null, @@ -80,9 +66,17 @@ const useRefreshPluginList = () => { // tool page, tool select if ((manifest && PluginCategoryEnum.tool.includes(manifest.category)) || refreshAllType) { - invalidateAllToolProviders() - invalidateAllBuiltInTools() - invalidateRAGRecommendedPlugins('tool') + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.toolProviders.get.key(), + }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.rag.pipelines.recommendedPlugins.get.queryKey({ + input: { query: { type: 'tool' } }, + }), + }) // TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins } @@ -96,7 +90,9 @@ const useRefreshPluginList = () => { queryClient.invalidateQueries({ queryKey: consoleQuery.rag.pipelines.datasourcePlugins.get.key(), }) - invalidateDataSourceListAuth() + queryClient.invalidateQueries({ + queryKey: consoleQuery.auth.plugin.datasource.list.get.queryKey(), + }) } // model select @@ -104,7 +100,9 @@ const useRefreshPluginList = () => { queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.modelProviders.summary.get.key(), }) - queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelProviderDetails }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.modelProviders.get.key(), + }) refetchLLMModelList() refetchEmbeddingModelList() refetchRerankModelList() diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/__tests__/github-item.spec.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/__tests__/github-item.spec.tsx index 4f97a93443c93f..6e8e41a8b8f93c 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/__tests__/github-item.spec.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/__tests__/github-item.spec.tsx @@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import GithubItem from '../github-item' const mockUseUploadGitHub = vi.fn() -const mockPluginManifestToCardPluginProps = vi.fn() +const mockPluginDeclarationToCardPluginProps = vi.fn() const mockLoadedItem = vi.fn() vi.mock('@/service/use-plugins', () => ({ @@ -14,8 +14,8 @@ vi.mock('@/service/use-plugins', () => ({ })) vi.mock('../../../utils', () => ({ - pluginManifestToCardPluginProps: (manifest: unknown) => - mockPluginManifestToCardPluginProps(manifest), + pluginDeclarationToCardPluginProps: (manifest: unknown, uniqueIdentifier: string) => + mockPluginDeclarationToCardPluginProps(manifest, uniqueIdentifier), })) vi.mock('../../../base/loading', () => ({ @@ -88,7 +88,7 @@ describe('GithubItem', () => { }, error: null, }) - mockPluginManifestToCardPluginProps.mockReturnValue(payload) + mockPluginDeclarationToCardPluginProps.mockReturnValue(payload) render( { ) await waitFor(() => { + expect(mockPluginDeclarationToCardPluginProps).toHaveBeenCalledWith( + { name: 'manifest' }, + 'plugin-1', + ) expect(onFetchedPayload).toHaveBeenCalledWith(payload) expect(screen.getByTestId('loaded-item')).toBeInTheDocument() }) diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx index e2cf39e537ba3a..a66a82c2f33832 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { useEffect } from 'react' import { useUploadGitHub } from '@/service/use-plugins' import Loading from '../../base/loading' -import { pluginManifestToCardPluginProps } from '../../utils' +import { pluginDeclarationToCardPluginProps } from '../../utils' import LoadedItem from './loaded-item' type Props = Readonly<{ @@ -35,8 +35,7 @@ const Item: FC = ({ useEffect(() => { if (data) { const payload = { - ...pluginManifestToCardPluginProps(data.manifest), - plugin_id: data.unique_identifier, + ...pluginDeclarationToCardPluginProps(data.manifest, data.unique_identifier), } onFetchedPayload(payload) setPayload({ ...payload, from: dependency.type }) diff --git a/web/app/components/plugins/install-plugin/utils.ts b/web/app/components/plugins/install-plugin/utils.ts index 3d0405e4246904..38a18b03a340c4 100644 --- a/web/app/components/plugins/install-plugin/utils.ts +++ b/web/app/components/plugins/install-plugin/utils.ts @@ -1,3 +1,7 @@ +import type { + I18nObject, + PluginDeclarationResponse, +} from '@dify/contracts/api/console/workspaces/types.gen' import type { GitHubUrlInfo, Plugin, PluginDeclaration, PluginManifestInMarket } from '../types' import { isEmpty } from 'es-toolkit/compat' @@ -31,6 +35,47 @@ export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaratio } } +/** Plugin card text from a translated object; untranslated locales are left out. */ +export const toPluginText = ({ en_US, zh_Hans, ja_JP, pt_BR }: I18nObject): Plugin['label'] => ({ + en_US, + ...(zh_Hans != null && { zh_Hans }), + ...(ja_JP != null && { ja_JP }), + ...(pt_BR != null && { pt_BR }), +}) + +/** Card props for the manifest of a package the console has just uploaded. */ +export const pluginDeclarationToCardPluginProps = ( + manifest: PluginDeclarationResponse, + uniqueIdentifier: string, +): Plugin => ({ + plugin_id: uniqueIdentifier, + type: manifest.category === 'agent-strategy' ? 'agent_strategy' : manifest.category, + category: manifest.category, + name: manifest.name, + version: manifest.version, + latest_version: '', + latest_package_identifier: '', + // A manifest without an author shows no organization. + org: manifest.author ?? '', + author: manifest.author ?? undefined, + label: toPluginText(manifest.label), + brief: toPluginText(manifest.description), + description: toPluginText(manifest.description), + icon: manifest.icon, + icon_dark: manifest.icon_dark ?? undefined, + verified: manifest.verified ?? false, + introduction: '', + repository: '', + install_count: 0, + endpoint: { + settings: [], + }, + tags: (manifest.tags ?? []).map((tag) => ({ name: tag })), + badges: [], + verification: { authorized_category: 'langgenius' }, + from: 'package', +}) + export const pluginManifestInMarketToPluginProps = ( pluginManifest: PluginManifestInMarket, ): Plugin => { diff --git a/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx index b0c8297af63b03..8f1356f844cfca 100644 --- a/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx +++ b/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx @@ -40,10 +40,6 @@ vi.mock('@/service/use-plugins-auth', () => ({ useGetPluginCredentialSchema: () => ({ data: undefined, isLoading: false }), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidToolsByType: () => vi.fn(), -})) - const mockIsCurrentWorkspaceManager = vi.fn() vi.mock('@/hooks/use-oauth', () => ({ diff --git a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx index c2c9e3cbf6f0a3..93dc802bc8153b 100644 --- a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx +++ b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx @@ -33,10 +33,6 @@ vi.mock('@/service/use-plugins-auth', () => ({ useGetPluginCredentialSchema: () => ({ data: undefined, isLoading: false }), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidToolsByType: () => vi.fn(), -})) - const mockIsCurrentWorkspaceManager = vi.fn() const mockUserProfile = { id: 'test-user', diff --git a/web/app/components/plugins/plugin-auth/hooks/__tests__/use-credential.spec.ts b/web/app/components/plugins/plugin-auth/hooks/__tests__/use-credential.spec.ts index f7f65d5a753eca..ee4942f53898e7 100644 --- a/web/app/components/plugins/plugin-auth/hooks/__tests__/use-credential.spec.ts +++ b/web/app/components/plugins/plugin-auth/hooks/__tests__/use-credential.spec.ts @@ -1,6 +1,9 @@ import type { PluginPayload } from '../../types' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { renderHook } from '@testing-library/react' +import { createElement } from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQuery } from '@/service/console' import { AuthCategory, CredentialTypeEnum } from '../../types' import { useAddPluginCredentialHook, @@ -30,7 +33,6 @@ const mockUseGetPluginOAuthClientSchema = vi.fn().mockReturnValue({ data: null, const mockUseInvalidPluginOAuthClientSchema = vi.fn().mockReturnValue(vi.fn()) const mockUseSetPluginOAuthCustomClient = vi.fn().mockReturnValue({ mutateAsync: vi.fn() }) const mockUseDeletePluginOAuthCustomClient = vi.fn().mockReturnValue({ mutateAsync: vi.fn() }) -const mockInvalidToolsByType = vi.fn() vi.mock('@/service/use-plugins-auth', () => ({ useGetPluginCredentialInfo: (...args: unknown[]) => mockUseGetPluginCredentialInfo(...args), @@ -50,10 +52,6 @@ vi.mock('@/service/use-plugins-auth', () => ({ mockUseDeletePluginOAuthCustomClient(...args), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidToolsByType: () => mockInvalidToolsByType, -})) - const toolPayload = { category: AuthCategory.tool, provider: 'test-provider', @@ -90,13 +88,22 @@ describe('use-credential hooks', () => { describe('useInvalidPluginCredentialInfoHook', () => { it('should return a function that invalidates both credential info and tools', () => { - const { result } = renderHook(() => useInvalidPluginCredentialInfoHook(toolPayload)) + const queryClient = new QueryClient() + const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() + const customToolsKey = consoleQuery.workspaces.current.tools.api.get.queryKey() + queryClient.setQueryData(builtInToolsKey, []) + queryClient.setQueryData(customToolsKey, []) + const { result } = renderHook(() => useInvalidPluginCredentialInfoHook(toolPayload), { + wrapper: ({ children }) => + createElement(QueryClientProvider, { client: queryClient }, children), + }) result.current() const invalidFn = mockUseInvalidPluginCredentialInfo.mock.results[0]!.value expect(invalidFn).toHaveBeenCalled() - expect(mockInvalidToolsByType).toHaveBeenCalled() + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) + expect(queryClient.getQueryState(customToolsKey)?.isInvalidated).toBe(false) }) }) diff --git a/web/app/components/plugins/plugin-auth/hooks/use-credential.ts b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts index 80e7151ab21033..a9150a50cb45a2 100644 --- a/web/app/components/plugins/plugin-auth/hooks/use-credential.ts +++ b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts @@ -1,4 +1,6 @@ import type { CredentialTypeEnum, PluginPayload } from '../types' +import { useQueryClient } from '@tanstack/react-query' +import { getToolListQueryKey } from '@/app/components/tools/utils/tool-list-query-key' import { useAddPluginCredential, useDeletePluginCredential, @@ -13,7 +15,6 @@ import { useSetPluginOAuthCustomClient, useUpdatePluginCredential, } from '@/service/use-plugins-auth' -import { useInvalidToolsByType } from '@/service/use-tools' import { useGetApi } from './use-get-api' export const useGetPluginCredentialInfoHook = ( @@ -41,12 +42,12 @@ export const useDeletePluginCredentialHook = (pluginPayload: PluginPayload) => { export const useInvalidPluginCredentialInfoHook = (pluginPayload: PluginPayload) => { const apiMap = useGetApi(pluginPayload) const invalidPluginCredentialInfo = useInvalidPluginCredentialInfo(apiMap.getCredentialInfo) - const providerType = pluginPayload.providerType - const invalidToolsByType = useInvalidToolsByType(providerType) + const queryClient = useQueryClient() + const toolListQueryKey = getToolListQueryKey(pluginPayload.providerType) return () => { invalidPluginCredentialInfo() - invalidToolsByType() + if (toolListQueryKey) queryClient.invalidateQueries({ queryKey: toolListQueryKey }) } } diff --git a/web/app/components/plugins/plugin-detail-panel/action-list.tsx b/web/app/components/plugins/plugin-detail-panel/action-list.tsx index 10dd51dc2a5477..761e90a6e8c747 100644 --- a/web/app/components/plugins/plugin-detail-panel/action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/action-list.tsx @@ -1,9 +1,14 @@ import type { PluginDetail } from '@/app/components/plugins/types' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import ToolItem from '@/app/components/tools/provider/tool-item' -import { useAllToolProviders, useBuiltinTools } from '@/service/use-tools' +import { + selectBuiltinProviderTools, + selectToolCollections, +} from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' type Props = Readonly<{ detail: PluginDetail @@ -13,11 +18,22 @@ const ActionList = ({ detail }: Props) => { const { t } = useTranslation(['plugin']) const providerBriefInfo = detail.declaration?.tool?.identity const providerKey = providerBriefInfo ? `${detail.plugin_id}/${providerBriefInfo.name}` : '' - const { data: collectionList = [] } = useAllToolProviders() + const { data: collectionList = [] } = useQuery( + consoleQuery.workspaces.current.toolProviders.get.queryOptions({ + input: { query: {} }, + select: selectToolCollections, + }), + ) const provider = useMemo(() => { return collectionList.find((collection) => collection.name === providerKey) }, [collectionList, providerKey]) - const { data } = useBuiltinTools(providerKey) + const { data } = useQuery( + consoleQuery.workspaces.current.toolProvider.builtin.byProvider.tools.get.queryOptions({ + input: { params: { provider: providerKey } }, + enabled: !!providerKey, + select: selectBuiltinProviderTools, + }), + ) if (!providerKey || !data || !provider) return null @@ -40,7 +56,6 @@ const ActionList = ({ detail }: Props) => { collection={provider} tool={tool} isBuiltIn={true} - isModel={false} /> ))}
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx index d44cba5a1fc624..5de55844abe43e 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx @@ -8,6 +8,18 @@ import { useState } from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { AppSelector } from '../index' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ + data: undefined, + isError: false, + isFetching: false, + refetch: vi.fn(), + }), + }) +}) + const apps = [ { id: 'app-1', @@ -41,67 +53,62 @@ const apps = [ const mockAppDetailQuery = vi.hoisted(() => vi.fn()) const mockUseAppWorkflow = vi.hoisted(() => vi.fn()) -vi.mock('@/service/console', () => ({ - consoleQuery: { - apps: { - get: { - infiniteOptions: ({ - input, - getNextPageParam, - initialPageParam, - placeholderData, - }: { - input: (pageParam: number) => { query: { name?: string } } - getNextPageParam: (lastPage: { has_more: boolean; page: number }) => number | undefined - initialPageParam: number - placeholderData: unknown - }) => ({ - queryKey: ['apps', input(1).query], - queryFn: ({ pageParam = initialPageParam }: { pageParam?: number }) => { - const query = input(Number(pageParam)).query - const keyword = query.name?.toLowerCase() ?? '' - const filteredApps = keyword - ? apps.filter((app) => app.name.toLowerCase().includes(keyword)) - : apps - - return { - data: filteredApps, - has_more: false, - page: Number(pageParam), - } - }, - getNextPageParam, - initialPageParam, - placeholderData, - }), - }, - byAppId: { +vi.mock('@/service/console', async () => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleQuery: { + files: { upload: { get: consoleOperationStub('files.upload.get') } }, + apps: { get: { - queryOptions: ({ input }: { input: unknown }) => { - const appId = - typeof input === 'object' && input && 'params' in input - ? (input.params as { app_id: string }).app_id - : undefined - return { - queryKey: ['apps', appId], - queryFn: () => mockAppDetailQuery(appId), - enabled: !!appId, - } + infiniteOptions: ({ + input, + getNextPageParam, + initialPageParam, + placeholderData, + }: { + input: (pageParam: number) => { query: { name?: string } } + getNextPageParam: (lastPage: { has_more: boolean; page: number }) => number | undefined + initialPageParam: number + placeholderData: unknown + }) => ({ + queryKey: ['apps', input(1).query], + queryFn: ({ pageParam = initialPageParam }: { pageParam?: number }) => { + const query = input(Number(pageParam)).query + const keyword = query.name?.toLowerCase() ?? '' + const filteredApps = keyword + ? apps.filter((app) => app.name.toLowerCase().includes(keyword)) + : apps + + return { + data: filteredApps, + has_more: false, + page: Number(pageParam), + } + }, + getNextPageParam, + initialPageParam, + placeholderData, + }), + }, + byAppId: { + get: { + queryOptions: ({ input }: { input: unknown }) => { + const appId = + typeof input === 'object' && input && 'params' in input + ? (input.params as { app_id: string }).app_id + : undefined + return { + queryKey: ['apps', appId], + queryFn: () => mockAppDetailQuery(appId), + enabled: !!appId, + } + }, }, }, }, }, - }, -})) - -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ - data: undefined, - isError: false, - isFetching: false, - refetch: vi.fn(), - }), -})) + } +}) vi.mock('@/service/use-workflow', () => ({ useAppWorkflow: (appId: string) => mockUseAppWorkflow(appId), diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts b/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts index b709e55deb3994..a24346d9ba750e 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts @@ -13,24 +13,22 @@ const mockFileUploadConfigRefetch = vi.fn() const mockWorkflowRefetch = vi.fn() vi.mock('@tanstack/react-query', () => ({ - useQuery: () => ({ - data: mockAppDetailData, - isError: false, - isFetching: false, - refetch: mockAppRefetch, - }), -})) - -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ - data: { - file_size_limit: 15, - image_file_size_limit: 10, - }, - isError: mockFileUploadConfigIsError, - isFetching: mockFileUploadConfigIsFetching, - refetch: mockFileUploadConfigRefetch, - }), + // The file upload limits come from their own query; every other query is the app detail. + useQuery: (options: { queryKey?: unknown[] }) => + Array.isArray(options.queryKey?.[0]) && + options.queryKey[0].join('.') === 'console.files.upload.get' + ? { + data: { file_size_limit: 15, image_file_size_limit: 10 }, + isError: mockFileUploadConfigIsError, + isFetching: mockFileUploadConfigIsFetching, + refetch: mockFileUploadConfigRefetch, + } + : { + data: mockAppDetailData, + isError: false, + isFetching: false, + refetch: mockAppRefetch, + }, })) vi.mock('@/service/use-workflow', () => ({ diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/use-app-inputs-form-schema.ts b/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/use-app-inputs-form-schema.ts index ca668d30b26634..55a9345a8829a2 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/use-app-inputs-form-schema.ts +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/use-app-inputs-form-schema.ts @@ -12,7 +12,6 @@ import { useMemo } from 'react' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { BlockEnum, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' import { consoleQuery } from '@/service/console' -import { useFileUploadConfig } from '@/service/use-common' import { useAppWorkflow } from '@/service/use-workflow' import { Resolution } from '@/types/app' @@ -180,7 +179,7 @@ export function useAppInputsFormSchema({ }: UseAppInputsFormSchemaParams): UseAppInputsFormSchemaResult { const isBasicApp = isBasicAppMode(appDetail.mode) - const fileUploadConfigQuery = useFileUploadConfig() + const fileUploadConfigQuery = useQuery(consoleQuery.files.upload.get.queryOptions()) const { data: fileUploadConfig } = fileUploadConfigQuery const appQuery = useQuery( consoleQuery.apps.byAppId.get.queryOptions({ diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx index 0ef3bb63d0c5e3..3069b579325d9c 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx @@ -24,17 +24,20 @@ vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), })) -vi.mock('@/service/use-tools', () => ({ - useAllToolProviders: () => ({ - data: [ - { - name: 'tool-plugin/provider-a', - type: 'builtin', - allow_delete: true, - }, - ], - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProviders.get': () => ({ + data: [ + { + name: 'tool-plugin/provider-a', + type: 'builtin', + allow_delete: true, + }, + ], + }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useVersionListOfPlugin: () => ({ data: { data: { versions: [] } } }), diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx index f318f7864f28c3..a95283c041a2ce 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx @@ -15,11 +15,12 @@ import { OperationDropdown } from '@/app/components/plugins/plugin-detail-panel/ import { BUILTIN_TOOLS_ARRAY } from '@/app/components/plugins/readme-panel/constants' import { useReadmePanelStore } from '@/app/components/plugins/readme-panel/store' import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker' +import { selectToolCollections } from '@/app/components/tools/utils/query-select' import { API_PREFIX } from '@/config' import { useGetLanguage } from '@/context/i18n' import { userProfileQueryOptions } from '@/features/account-profile/client' import useTheme from '@/hooks/use-theme' -import { useAllToolProviders } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { getMarketplaceUrl } from '@/utils/var' import Verified from '../../base/badges/verified' import DeprecationNotice from '../../base/deprecation-notice' @@ -134,7 +135,13 @@ const DetailHeader = ({ const isTool = category === PluginCategoryEnum.tool const providerBriefInfo = tool?.identity const providerKey = `${plugin_id}/${providerBriefInfo?.name}` - const { data: collectionList = [] } = useAllToolProviders(isTool) + const { data: collectionList = [] } = useQuery( + consoleQuery.workspaces.current.toolProviders.get.queryOptions({ + input: { query: {} }, + enabled: isTool, + select: selectToolCollections, + }), + ) const provider = useMemo(() => { return collectionList.find((collection) => collection.name === providerKey) }, [collectionList, providerKey]) diff --git a/web/app/components/plugins/plugin-detail-panel/model-list.tsx b/web/app/components/plugins/plugin-detail-panel/model-list.tsx index 7f4c42c271c8e3..4b166bb5c6b47c 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-list.tsx @@ -1,9 +1,10 @@ import type { PluginDetail } from '@/app/components/plugins/types' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' -import { useModelProviderModelList } from '@/service/use-models' +import { consoleQuery } from '@/service/console' type Props = Readonly<{ detail: PluginDetail @@ -11,8 +12,10 @@ type Props = Readonly<{ const ModelList = ({ detail }: Props) => { const { t } = useTranslation(['plugin']) - const { data: res } = useModelProviderModelList( - `${detail.plugin_id}/${detail.declaration.model.provider}`, + const { data: res } = useQuery( + consoleQuery.workspaces.current.modelProviders.byProvider.models.get.queryOptions({ + input: { params: { provider: `${detail.plugin_id}/${detail.declaration.model.provider}` } }, + }), ) if (!res) return null diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx index 43d519c28683cf..9ec9c0655b81ac 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx @@ -12,10 +12,6 @@ import LLMParamsPanel from '../llm-params-panel' // Mock useModelParameterRules hook const mockUseModelParameterRules = vi.fn() -vi.mock('@/service/use-common', () => ({ - useModelParameterRules: (provider: string, modelId: string) => - mockUseModelParameterRules(provider, modelId), -})) // Mock config constants with inline data vi.mock('@/config', () => ({ @@ -251,6 +247,19 @@ const setupModelParameterRulesMock = ( // ==================== Tests ==================== +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.modelProviders.byProvider.models.parameterRules.get': (options: { + queryKey: [unknown, { input?: { params: { provider: string }; query: { model: string } } }] + }) => + mockUseModelParameterRules( + options.queryKey[1].input?.params?.provider, + options.queryKey[1].input?.query?.model, + ), + }) +}) + describe('LLMParamsPanel', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx index 6179bfa4a057c1..e00599d634059b 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx @@ -4,6 +4,7 @@ import type { } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { ParameterValue } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item' import { cn } from '@langgenius/dify-ui/cn' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -11,8 +12,8 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import ParameterItem from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item' import PresetsParameter from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter' import { getSupportedPresetConfig } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter-utils' +import { modelParameterRulesQueryOptions } from '@/app/components/header/account-setting/model-provider-page/utils' import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE } from '@/config' -import { useModelParameterRules } from '@/service/use-common' type Props = Readonly<{ isAdvancedMode: boolean @@ -30,7 +31,9 @@ const LLMParamsPanel = ({ onCompletionParamsChange, }: Props) => { const { t } = useTranslation(['modelProvider']) - const { data: parameterRulesData, isLoading } = useModelParameterRules(provider, modelId) + const { data: parameterRulesData, isLoading } = useQuery( + modelParameterRulesQueryOptions({ provider, model: modelId }), + ) const isRulesLoading = !!provider && !!modelId && isLoading const parameterRules: ModelParameterRule[] = useMemo(() => { diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/focus-restoration.spec.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/focus-restoration.spec.tsx index 97e0bc22ca0af5..b633fccbf85ffb 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/focus-restoration.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/focus-restoration.spec.tsx @@ -1,23 +1,26 @@ import type { ToolValue } from '@/app/components/workflow/block-selector/types' import type { ToolWithProvider } from '@/app/components/workflow/types' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' import { CollectionType } from '@/app/components/tools/types' import { MCPToolAvailabilityProvider } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' +import { renderWithConsoleQuery } from '@/test/console/query-data' import MultipleToolSelector from '../index' const mockTools = vi.hoisted(() => ({ builtIn: [] as ToolWithProvider[], })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockTools.builtIn, isFetched: true }), - useAllCustomTools: () => ({ data: [], isFetched: true }), - useAllMCPTools: () => ({ data: [], isFetched: true }), - useAllWorkflowTools: () => ({ data: [], isFetched: true }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockTools.builtIn, isFetched: true }), + 'workspaces.current.tools.api.get': () => ({ data: [], isFetched: true }), + 'workspaces.current.tools.mcp.get': () => ({ data: [], isFetched: true }), + 'workspaces.current.tools.workflow.get': () => ({ data: [], isFetched: true }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useCheckInstalled: () => ({ data: undefined }), @@ -77,7 +80,7 @@ function renderSelector(initialValue: ToolValue[]) { ) } - return render() + return renderWithConsoleQuery() } describe('MultipleToolSelector focus restoration', () => { diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/index.spec.tsx index f6efaa7dce11da..b1461060777209 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/index.spec.tsx @@ -14,11 +14,14 @@ import MultipleToolSelector from '../index' // Mock useAllMCPTools hook const mockMCPToolsData = vi.fn<() => ToolWithProvider[] | undefined>(() => undefined) -vi.mock('@/service/use-tools', () => ({ - useAllMCPTools: () => ({ - data: mockMCPToolsData(), - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.mcp.get': () => ({ + data: mockMCPToolsData(), + }), + }) +}) // Track edit tool index for unique test IDs let editToolIndex = 0 diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx index f9678e939589c8..c1d9a13c04ce65 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx @@ -7,11 +7,13 @@ import { IconButton } from '@langgenius/dify-ui/icon-button' import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip' import { Separator } from '@langgenius/dify-ui/separator' import { useRefWithInit } from '@langgenius/dify-ui/use-ref-with-init' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' -import { useAllMCPTools } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' type Props = Readonly<{ disabled?: boolean @@ -46,7 +48,9 @@ const MultipleToolSelector = ({ const { t } = useTranslation(['appDebug', 'plugin']) const { allowed: isMCPToolAllowed } = useMCPToolAvailability() - const { data: mcpTools } = useAllMCPTools() + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const addToolButtonRef = React.useRef(null) const toolItemTriggerByKeyRef = useRefWithInit(() => new Map()) const pendingFocusTargetRef = React.useRef<{ toolKey?: string } | null>(null) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/__tests__/use-tool-selector.spec.ts b/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/__tests__/use-tool-selector.spec.ts index 4ea540d54b5592..ca1c24c6b297a7 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/__tests__/use-tool-selector.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/__tests__/use-tool-selector.spec.ts @@ -2,8 +2,13 @@ import type { ToolValue } from '@/app/components/workflow/block-selector/types' import { act, renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { CollectionType } from '@/app/components/tools/types' +import { createQueryClientWrapper } from '@/test/console/query-client' +import { createTestQueryClient } from '@/test/query-client' import { useToolSelector } from '../use-tool-selector' +const queryClient = createTestQueryClient() +const queryClientWrapper = createQueryClientWrapper(queryClient) + const mockToolParams = [ { name: 'param1', form: 'llm', type: 'string', required: true, label: { en_US: 'Param 1' } }, { name: 'param2', form: 'form', type: 'number', required: false, label: { en_US: 'Param 2' } }, @@ -27,13 +32,18 @@ const mockTools = [ ] let areToolQueriesFetched = true -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockTools, isFetched: areToolQueriesFetched }), - useAllCustomTools: () => ({ data: [], isFetched: areToolQueriesFetched }), - useAllWorkflowTools: () => ({ data: [], isFetched: areToolQueriesFetched }), - useAllMCPTools: () => ({ data: [], isFetched: areToolQueriesFetched }), - useInvalidateAllBuiltInTools: () => vi.fn().mockResolvedValue(undefined), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ + data: mockTools, + isFetched: areToolQueriesFetched, + }), + 'workspaces.current.tools.api.get': () => ({ data: [], isFetched: areToolQueriesFetched }), + 'workspaces.current.tools.workflow.get': () => ({ data: [], isFetched: areToolQueriesFetched }), + 'workspaces.current.tools.mcp.get': () => ({ data: [], isFetched: areToolQueriesFetched }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useInvalidateInstalledPluginList: () => vi.fn().mockResolvedValue(undefined), @@ -88,7 +98,9 @@ describe('useToolSelector', () => { }) it('should initialize with default panel states', () => { - const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect })) + const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) expect(result.current.isShow).toBe(false) expect(result.current.isShowChooseTool).toBe(false) @@ -96,8 +108,9 @@ describe('useToolSelector', () => { }) it('should find current provider from tool value', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) expect(result.current.currentProvider).toBeDefined() @@ -105,8 +118,9 @@ describe('useToolSelector', () => { }) it('should find current tool from provider', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) expect(result.current.currentTool).toBeDefined() @@ -114,8 +128,9 @@ describe('useToolSelector', () => { }) it('should compute tool settings and params correctly', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) // param2 has form='form' (not 'llm'), so it goes to settings @@ -128,8 +143,9 @@ describe('useToolSelector', () => { }) it('should show tab slider when both settings and params exist', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) expect(result.current.showTabSlider).toBe(true) @@ -138,7 +154,9 @@ describe('useToolSelector', () => { }) it('should toggle panel visibility', () => { - const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect })) + const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) act(() => { result.current.setIsShow(true) @@ -152,7 +170,9 @@ describe('useToolSelector', () => { }) it('should switch tab type', () => { - const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect })) + const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) act(() => { result.current.setCurrType('params') @@ -161,8 +181,9 @@ describe('useToolSelector', () => { }) it('should handle description change', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) act(() => { @@ -177,8 +198,9 @@ describe('useToolSelector', () => { }) it('should handle enabled change', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) act(() => { @@ -193,8 +215,9 @@ describe('useToolSelector', () => { }) it('should handle authorization item click', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) act(() => { @@ -209,7 +232,9 @@ describe('useToolSelector', () => { }) it('should not call onSelect if value is undefined', () => { - const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect })) + const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) act(() => { result.current.handleEnabledChange(true) @@ -218,11 +243,13 @@ describe('useToolSelector', () => { }) it('should return empty arrays when no provider matches', () => { - const { result } = renderHook(() => - useToolSelector({ - value: { ...toolValue, provider_name: 'nonexistent' }, - onSelect: mockOnSelect, - }), + const { result } = renderHook( + () => + useToolSelector({ + value: { ...toolValue, provider_name: 'nonexistent' }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(result.current.currentProvider).toBeUndefined() @@ -232,7 +259,9 @@ describe('useToolSelector', () => { }) it('should skip plugin checks after resolving the current provider and tool', () => { - renderHook(() => useToolSelector({ value: toolValue, onSelect: mockOnSelect })) + renderHook(() => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ providerPluginId: 'org/test-plugin', @@ -241,11 +270,13 @@ describe('useToolSelector', () => { }) it('should keep plugin checks enabled when the current tool cannot be resolved', () => { - renderHook(() => - useToolSelector({ - value: { ...toolValue, tool_name: 'missing-tool' }, - onSelect: mockOnSelect, - }), + renderHook( + () => + useToolSelector({ + value: { ...toolValue, tool_name: 'missing-tool' }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ @@ -255,15 +286,17 @@ describe('useToolSelector', () => { }) it('should keep marketplace fallback enabled after tool queries settle without resolving the provider', () => { - renderHook(() => - useToolSelector({ - value: { - ...toolValue, - provider_name: 'org/market-plugin/search', - plugin_id: 'org/market-plugin', - }, - onSelect: mockOnSelect, - }), + renderHook( + () => + useToolSelector({ + value: { + ...toolValue, + provider_name: 'org/market-plugin/search', + plugin_id: 'org/market-plugin', + }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ @@ -275,15 +308,17 @@ describe('useToolSelector', () => { it('should keep marketplace fallback enabled when legacy provider lists are still pending but plugin id is known', () => { areToolQueriesFetched = false - renderHook(() => - useToolSelector({ - value: { - ...toolValue, - provider_name: 'org/market-plugin/search', - plugin_id: 'org/market-plugin', - }, - onSelect: mockOnSelect, - }), + renderHook( + () => + useToolSelector({ + value: { + ...toolValue, + provider_name: 'org/market-plugin/search', + plugin_id: 'org/market-plugin', + }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ @@ -293,16 +328,18 @@ describe('useToolSelector', () => { }) it('should skip marketplace checks for unresolved non-plugin workflow tools', () => { - renderHook(() => - useToolSelector({ - value: { - ...toolValue, - provider_name: 'author/tool-b', - plugin_id: undefined, - type: CollectionType.workflow, - }, - onSelect: mockOnSelect, - }), + renderHook( + () => + useToolSelector({ + value: { + ...toolValue, + provider_name: 'author/tool-b', + plugin_id: undefined, + type: CollectionType.workflow, + }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/use-tool-selector.ts b/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/use-tool-selector.ts index 973c670a76fc28..dd8241f033667e 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/use-tool-selector.ts +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/use-tool-selector.ts @@ -3,23 +3,19 @@ import type { ReasoningConfigValue } from '../components/reasoning-config-form' import type { ToolParameter } from '@/app/components/tools/types' import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types' import type { ResourceVarInputs } from '@/app/components/workflow/nodes/_base/types' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useMemo, useState } from 'react' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas, } from '@/app/components/tools/utils/to-form-schema' +import { consoleQuery } from '@/service/console' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, -} from '@/service/use-tools' import { getIconFromMarketPlace } from '@/utils/get-icon' import { matchesProviderReference } from '@/utils/provider-reference' import { usePluginInstalledCheck } from './use-plugin-installed-check' @@ -37,15 +33,27 @@ export function useToolSelector({ value, onSelect, onSelectMultiple }: UseToolSe const [isShowChooseTool, setIsShowChooseTool] = useState(false) const [currType, setCurrType] = useState('settings') - const buildInToolsQuery = useAllBuiltInTools() - const customToolsQuery = useAllCustomTools() - const workflowToolsQuery = useAllWorkflowTools() - const mcpToolsQuery = useAllMCPTools() + const buildInToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const customToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const workflowToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const mcpToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const { data: buildInTools } = buildInToolsQuery const { data: customTools } = customToolsQuery const { data: workflowTools } = workflowToolsQuery const { data: mcpTools } = mcpToolsQuery - const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools() + const queryClient = useQueryClient() const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const currentProvider = useMemo(() => { @@ -245,7 +253,9 @@ export function useToolSelector({ value, onSelect, onSelectMultiple }: UseToolSe const handleInstall = useCallback(async () => { try { - await invalidateAllBuiltinTools() + await queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }) } catch (error) { console.error('Failed to invalidate built-in tools cache', error) } @@ -254,7 +264,7 @@ export function useToolSelector({ value, onSelect, onSelectMultiple }: UseToolSe } catch (error) { console.error('Failed to invalidate installed plugin list cache', error) } - }, [invalidateAllBuiltinTools, invalidateInstalledPluginList]) + }, [invalidateInstalledPluginList, queryClient]) const settingsValue = useMemo((): ResourceVarInputs => { return getPlainValue( diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx index 62299d05c68fc5..3720a0912d072f 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx @@ -33,7 +33,7 @@ const createMockPlugin = (overrides: Partial = {}): PluginStatus = labels: { en_US: 'Test Plugin', zh_Hans: '测试插件', - } as Record, + }, taskId: 'task-1', ...overrides, }) @@ -482,7 +482,7 @@ describe('PluginTaskList Component', () => { it('should display plugin name from labels', () => { const plugin = createMockPlugin({ status: TaskStatus.running, - labels: { en_US: 'My Test Plugin' } as Record, + labels: { en_US: 'My Test Plugin' }, }) render() @@ -505,11 +505,11 @@ describe('PluginTaskList Component', () => { const runningPlugins = [ createMockPlugin({ status: TaskStatus.running, - labels: { en_US: 'Plugin A' } as Record, + labels: { en_US: 'Plugin A' }, }), createMockPlugin({ status: TaskStatus.running, - labels: { en_US: 'Plugin B' } as Record, + labels: { en_US: 'Plugin B' }, }), ] @@ -780,7 +780,7 @@ describe('PluginTasks Component', () => { it('should handle plugins with empty labels', () => { const plugin = createMockPlugin({ status: TaskStatus.running, - labels: {} as Record, + labels: { en_US: '' }, }) setupMocks([plugin]) diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/components/error-plugin-item.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/components/error-plugin-item.tsx index 2dd9c1565fc7a6..6ff9e169bad9fb 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/components/error-plugin-item.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/components/error-plugin-item.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' import { PluginInstallPermissionProvider } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider' import useWorkspacePluginInstallPermission from '@/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' +import { toPluginText } from '@/app/components/plugins/install-plugin/utils' import { PluginSource } from '@/app/components/plugins/types' import { fetchPluginInfoFromMarketPlace } from '@/service/plugins' import PluginItem from './plugin-item' @@ -46,7 +47,7 @@ const ErrorPluginItem: FC = ({ plugin, getIconUrl, languag version: info.latest_version, latest_version: info.latest_version, latest_package_identifier: info.latest_package_identifier, - label: plugin.labels, + label: toPluginText(plugin.labels), brief: {}, description: {}, icon: plugin.icon, diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/installation-task-clearing.browser.spec.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/installation-task-clearing.browser.spec.tsx index 44c16b72074052..46c28d2324972f 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/installation-task-clearing.browser.spec.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/installation-task-clearing.browser.spec.tsx @@ -18,7 +18,7 @@ vi.mock('@/service/use-plugins', () => ({ status: TaskStatus.success, message: '', icon: '', - labels: { en_US: 'Installed Plugin' } as Record, + labels: { en_US: 'Installed Plugin' }, } satisfies Omit, ], }, diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index da651caa08906e..895a9e68adc7c6 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -2,6 +2,9 @@ import type { AgentStrategyProviderEntity, DatasourceProviderEntity, EndpointProviderDeclarationResponse, + PluginInstallTask, + PluginInstallTaskPluginStatus, + PluginInstallTaskStartResponse, } from '@dify/contracts/api/console/workspaces/types.gen' import type { FormTypeEnum } from '../base/form/types' import type { CredentialFormSchemaBase } from '../header/account-setting/model-provider-page/declarations' @@ -350,12 +353,7 @@ export type GitHubRepoReleaseResponse = { assets: GitHubAsset[] } -export type InstallPackageResponse = { - plugin_unique_identifier?: string - all_installed: boolean - task_id: string - task?: PluginTaskStart -} +export type InstallPackageResponse = PluginInstallTaskStartResponse export type InstallStatusResponse = { status: TaskStatus @@ -368,23 +366,6 @@ export type InstallStatus = { isFromMarketPlace?: boolean } -export type updatePackageResponse = { - all_installed: boolean - task_id: string - task?: PluginTaskStart -} - -export type uploadGitHubResponse = { - unique_identifier: string - manifest: PluginDeclaration -} - -export type DebugInfo = { - key: string - host: string - port: number -} - export const TaskStatus = { pending: 'pending', running: 'running', @@ -394,34 +375,14 @@ export const TaskStatus = { export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus] -export type PluginStatus = { - plugin_unique_identifier: string - plugin_id: string - source: PluginSource - status: TaskStatus - message: string - icon: string - labels: Record +export type PluginStatus = PluginInstallTaskPluginStatus & { + // The task the status belongs to, added when the task list is read. taskId: string } -export type PluginTask = { - id: string - created_at: string - updated_at: string - status: string - total_plugins: number - completed_plugins: number - plugins: PluginStatus[] -} - -export type PluginTaskStart = Omit & { - plugins: Array & { taskId?: string }> -} +export type PluginTask = Omit & { plugins: PluginStatus[] } -export type TaskStatusResponse = { - task: PluginTask -} +export type PluginTaskStart = PluginInstallTask export type MetaData = { repo: string diff --git a/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx index 2f3bcf7cf8c4cd..84e50a959e8964 100644 --- a/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx @@ -1,10 +1,14 @@ +import { QueryClient } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQuery } from '@/service/console' import { renderWithAccountProfile as render } from '@/test/console/account-profile' import Conversion from '../conversion' const mockConvert = vi.fn() -const mockInvalidDatasetDetail = vi.fn() +const datasetDetailKey = consoleQuery.datasets.byDatasetId.get.queryKey({ + input: { params: { dataset_id: 'ds-123' } }, +}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => ({ @@ -28,20 +32,15 @@ vi.mock('@/context/dataset-detail', () => ({ ) => selector(mockDatasetDetailState), })) -vi.mock('@/service/use-pipeline', () => ({ - useConvertDatasetToPipeline: () => ({ - mutateAsync: mockConvert, - isPending: false, - }), -})) - -vi.mock('@/service/knowledge/use-dataset', () => ({ - datasetDetailQueryKeyPrefix: ['dataset-detail'], -})) - -vi.mock('@/service/use-base', () => ({ - useInvalid: () => mockInvalidDatasetDetail, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.transform.datasets.byDatasetId.post': () => ({ + mutateAsync: mockConvert, + isPending: false, + }), + }) +}) const { mockToast } = vi.hoisted(() => { const mockToast = Object.assign(vi.fn(), { @@ -75,6 +74,7 @@ vi.mock('@/context/permission-state', async () => { describe('Conversion', () => { beforeEach(() => { vi.clearAllMocks() + vi.spyOn(QueryClient.prototype, 'invalidateQueries') mockDatasetDetailState = { dataset: { permission_keys: ['dataset.acl.edit'], @@ -161,7 +161,7 @@ describe('Conversion', () => { fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(mockConvert).toHaveBeenCalledWith( - 'ds-123', + { params: { dataset_id: 'ds-123' } }, expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function), @@ -182,7 +182,9 @@ describe('Conversion', () => { fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(mockToast.success).toHaveBeenCalledWith('datasetPipeline.conversion.successMessage') - expect(mockInvalidDatasetDetail).toHaveBeenCalled() + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: datasetDetailKey, + }) }) it('should handle failed conversion', async () => { diff --git a/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx index 799e9ec048f727..ed40afc2fa0334 100644 --- a/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx @@ -76,10 +76,13 @@ vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({ const mockImportDSL = vi.fn() const mockImportDSLConfirm = vi.fn() -vi.mock('@/service/use-pipeline', () => ({ - useImportPipelineDSL: () => ({ mutateAsync: mockImportDSL }), - useImportPipelineDSLConfirm: () => ({ mutateAsync: mockImportDSLConfirm }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.imports.post': () => ({ mutateAsync: mockImportDSL }), + 'rag.pipelines.imports.byImportId.confirm.post': () => ({ mutateAsync: mockImportDSLConfirm }), + }) +}) vi.mock('@/service/workflow', () => ({ fetchWorkflowDraft: vi.fn().mockResolvedValue({ @@ -752,7 +755,7 @@ describe('UpdateDSLModal', () => { fireEvent.click(confirmButton) await waitFor(() => { - expect(mockImportDSLConfirm).toHaveBeenCalledWith('import-id') + expect(mockImportDSLConfirm).toHaveBeenCalledWith({ params: { import_id: 'import-id' } }) }) vi.useRealTimers() diff --git a/web/app/components/rag-pipeline/components/conversion.tsx b/web/app/components/rag-pipeline/components/conversion.tsx index 0329fd163b0241..51a6ec5fdfaeef 100644 --- a/web/app/components/rag-pipeline/components/conversion.tsx +++ b/web/app/components/rag-pipeline/components/conversion.tsx @@ -8,7 +8,7 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useCallback, useState } from 'react' @@ -18,15 +18,13 @@ import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { useParams } from '@/next/navigation' -import { datasetDetailQueryKeyPrefix } from '@/service/knowledge/use-dataset' -import { useInvalid } from '@/service/use-base' -import { useConvertDatasetToPipeline } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import { getDatasetACLCapabilities } from '@/utils/permission' import PipelineScreenShot from './screenshot' const Conversion = () => { const { t } = useTranslation(['common', 'datasetPipeline']) - const { datasetId } = useParams() + const { datasetId } = useParams<{ datasetId: string }>() const dataset = useDatasetDetailContextWithSelector((state) => state.dataset) const { data: currentUserId } = useSuspenseQuery({ ...userProfileQueryOptions(), @@ -34,8 +32,17 @@ const Conversion = () => { }) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const [showConfirmModal, setShowConfirmModal] = useState(false) - const { mutateAsync: convert, isPending } = useConvertDatasetToPipeline() - const invalidDatasetDetail = useInvalid([...datasetDetailQueryKeyPrefix, datasetId]) + const { mutateAsync: convert, isPending } = useMutation( + consoleQuery.rag.pipelines.transform.datasets.byDatasetId.post.mutationOptions(), + ) + const queryClient = useQueryClient() + const invalidDatasetDetail = React.useCallback(() => { + queryClient.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.get.queryKey({ + input: { params: { dataset_id: datasetId } }, + }), + }) + }, [queryClient, datasetId]) const datasetACLCapabilities = React.useMemo( () => getDatasetACLCapabilities(dataset?.permission_keys, { @@ -49,20 +56,23 @@ const Conversion = () => { const handleConvert = useCallback(() => { if (!canConvertDataset || !datasetId) return - convert(datasetId as string, { - onSuccess: (res) => { - if (res.status === 'success') { - toast.success(t(($) => $['conversion.successMessage'], { ns: 'datasetPipeline' })) - setShowConfirmModal(false) - invalidDatasetDetail() - } else if (res.status === 'failed') { + convert( + { params: { dataset_id: datasetId } }, + { + onSuccess: (res) => { + if (res.status === 'success') { + toast.success(t(($) => $['conversion.successMessage'], { ns: 'datasetPipeline' })) + setShowConfirmModal(false) + invalidDatasetDetail() + } else if (res.status === 'failed') { + toast.error(t(($) => $['conversion.errorMessage'], { ns: 'datasetPipeline' })) + } + }, + onError: () => { toast.error(t(($) => $['conversion.errorMessage'], { ns: 'datasetPipeline' })) - } + }, }, - onError: () => { - toast.error(t(($) => $['conversion.errorMessage'], { ns: 'datasetPipeline' })) - }, - }) + ) }, [canConvertDataset, convert, datasetId, invalidDatasetDetail, t]) const handleShowConfirmModal = useCallback(() => { if (!canConvertDataset) return diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/__tests__/utils.spec.ts b/web/app/components/rag-pipeline/components/panel/input-field/editor/__tests__/utils.spec.ts index e4e53a4c5ba4a5..4bbf6b57d9e519 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/__tests__/utils.spec.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/__tests__/utils.spec.ts @@ -45,6 +45,28 @@ describe('input-field editor utils', () => { }) }) + it('keeps number and checkbox defaults typed for the submit schema', () => { + const numberField = convertToInputFieldFormData({ + type: PipelineInputVarType.number, + label: 'Chunk size', + variable: 'chunk_size', + default_value: 5, + required: true, + }) + const checkboxField = convertToInputFieldFormData({ + type: PipelineInputVarType.checkbox, + label: 'Enabled', + variable: 'enabled', + default_value: false, + required: true, + }) + + expect(numberField.default).toBe(5) + expect(checkboxField.default).toBe(false) + expect(convertFormDataToINputField(numberField).default_value).toBe(5) + expect(convertFormDataToINputField(checkboxField).default_value).toBe(false) + }) + it('should fall back to the default input variable template', () => { const result = convertToInputFieldFormData() diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/hooks.spec.ts b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/hooks.spec.ts index c89fcf1e692b5f..6e7be1f00e8737 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/hooks.spec.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/hooks.spec.ts @@ -3,6 +3,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { PipelineInputVarType } from '@/models/pipeline' import { useConfigurations, useHiddenConfigurations, useHiddenFieldNames } from '../hooks' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ data: {} }), + }) +}) + vi.mock('@/app/components/base/file-uploader/hooks', () => ({ useFileSizeLimit: () => ({ imgSizeLimit: 10 * 1024 * 1024, @@ -12,10 +19,6 @@ vi.mock('@/app/components/base/file-uploader/hooks', () => ({ }), })) -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ data: {} }), -})) - vi.mock('@/app/components/workflow/constants', () => ({ DEFAULT_FILE_UPLOAD_SETTING: { allowed_file_upload_methods: ['local_file', 'remote_url'], diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/index.spec.tsx index 94e9ba1650f51b..64c685a8e36280 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/index.spec.tsx @@ -21,14 +21,6 @@ const mockFileUploadConfig = { workflow_file_upload_limit: 10, } -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ - data: mockFileUploadConfig, - isLoading: false, - error: null, - }), -})) - const createFormData = (overrides?: Partial): FormData => ({ type: PipelineInputVarType.textInput, label: 'Test Label', @@ -90,6 +82,17 @@ beforeEach(() => { toastErrorSpy.mockClear() }) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ + data: mockFileUploadConfig, + isLoading: false, + error: null, + }), + }) +}) + describe('InputFieldForm', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/hooks.ts b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/hooks.ts index 4e4c5624e58419..2051b6216134fe 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/hooks.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/hooks.ts @@ -1,13 +1,14 @@ import type { DeepKeys } from '@tanstack/react-form' import type { FormData } from './types' import type { InputFieldConfiguration } from '@/app/components/base/form/form-scenarios/input-field/types' +import { useQuery } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' import { InputFieldType } from '@/app/components/base/form/form-scenarios/input-field/types' import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants' import { PipelineInputVarType } from '@/models/pipeline' -import { useFileUploadConfig } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { formatFileSize } from '@/utils/format' import { TEXT_MAX_LENGTH } from './schema' @@ -217,7 +218,7 @@ export const useHiddenConfigurations = (props: { options: string[] | undefined } const { options } = props - const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: fileUploadConfigResponse } = useQuery(consoleQuery.files.upload.get.queryOptions()) const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileUploadConfigResponse) diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx index 2acce00d697493..df0dc6e359420c 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx @@ -2,13 +2,14 @@ import type { FormData, InputFieldFormProps } from './types' import type { MoreInfo } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { Separator } from '@langgenius/dify-ui/separator' +import { useQuery } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' import { useAppForm } from '@/app/components/base/form' import { ChangeType } from '@/app/components/workflow/types' import { toast } from '@/app/notifications' -import { useFileUploadConfig } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import HiddenFields from './hidden-fields' import InitialFields from './initial-fields' import { createInputFieldSchema } from './schema' @@ -22,7 +23,7 @@ const InputFieldForm = ({ isEditMode = true, }: InputFieldFormProps) => { const { t } = useTranslation(['appDebug', 'common']) - const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: fileUploadConfigResponse } = useQuery(consoleQuery.files.upload.get.queryOptions()) const { maxFileUploadLimit } = useFileSizeLimit(fileUploadConfigResponse) const inputFieldForm = useAppForm({ defaultValues: initialData, diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/types.ts b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/types.ts index f0412078a41e0c..28f2007fdc78bf 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/form/types.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/form/types.ts @@ -7,7 +7,7 @@ export type FormData = { label: string variable: string maxLength?: number - default?: string + default?: string | number | boolean required: boolean tooltips?: string options?: string[] diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/utils.ts b/web/app/components/rag-pipeline/components/panel/input-field/editor/utils.ts index c3921e7911be94..e81f9edb196e38 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/utils.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/utils.ts @@ -35,6 +35,7 @@ export const convertToInputFieldFormData = (data?: InputVar): FormData => { allowedTypesAndExtensions: {}, } + // Number and checkbox variables keep their typed default for the submit schema. if (default_value !== undefined && default_value !== null) formData.default = default_value if (tooltips !== undefined && tooltips !== null) formData.tooltips = tooltips if (placeholder !== undefined && placeholder !== null) formData.placeholder = placeholder diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/data-source.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/data-source.spec.tsx index e4ee6f52ec500c..c0f714b76b0d1f 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/data-source.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/data-source.spec.tsx @@ -5,7 +5,7 @@ import DataSource from '../data-source' const { mockOnSelect, mockUseDraftPipelinePreProcessingParams } = vi.hoisted(() => ({ mockOnSelect: vi.fn(), - mockUseDraftPipelinePreProcessingParams: vi.fn(() => ({ + mockUseDraftPipelinePreProcessingParams: vi.fn((_params?: unknown, _enabled?: boolean) => ({ data: { variables: [{ variable: 'source' }], }, @@ -17,9 +17,19 @@ vi.mock('@/app/components/workflow/store', () => ({ selector({ pipelineId: 'pipeline-1' }), })) -vi.mock('@/service/use-pipeline', () => ({ - useDraftPipelinePreProcessingParams: mockUseDraftPipelinePreProcessingParams, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.draft.preProcessing.parameters.get': (o: { + input: { params: { pipeline_id: string }; query: { node_id: string } } + enabled?: boolean + }) => + mockUseDraftPipelinePreProcessingParams( + { pipeline_id: o.input.params.pipeline_id, node_id: o.input.query.node_id }, + o.enabled, + ), + }) +}) vi.mock('../../../test-run/preparation/data-source-options', () => ({ default: ({ diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/index.spec.tsx index 3d3f7d0780c9fa..1a1b39ac6b46db 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/index.spec.tsx @@ -61,18 +61,29 @@ vi.mock('zustand/react/shallow', () => ({ let mockPreProcessingParamsData: { variables: RAGPipelineVariables } | undefined let mockProcessingParamsData: { variables: RAGPipelineVariables } | undefined -vi.mock('@/service/use-pipeline', () => ({ - useDraftPipelinePreProcessingParams: (_params: unknown, enabled: boolean) => ({ - data: enabled ? mockPreProcessingParamsData : undefined, - isLoading: false, - error: null, - }), - useDraftPipelineProcessingParams: (_params: unknown, enabled: boolean) => ({ - data: enabled ? mockProcessingParamsData : undefined, - isLoading: false, - error: null, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.draft.preProcessing.parameters.get': ({ + enabled, + }: { + enabled?: boolean + }) => ({ + data: enabled ? mockPreProcessingParamsData : undefined, + isLoading: false, + error: null, + }), + 'rag.pipelines.byPipelineId.workflows.draft.processing.parameters.get': ({ + enabled, + }: { + enabled?: boolean + }) => ({ + data: enabled ? mockProcessingParamsData : undefined, + isLoading: false, + error: null, + }), + }) +}) let mockDatasourceOptions: DataSourceOption[] = [] diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/process-documents.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/process-documents.spec.tsx index 4a0219b54b8845..aac315d5017948 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/process-documents.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/process-documents.spec.tsx @@ -3,7 +3,7 @@ import { renderEditor as render } from '@/app/components/workflow/editor-resourc import ProcessDocuments from '../process-documents' const mockUseDraftPipelineProcessingParams = vi.hoisted(() => - vi.fn(() => ({ + vi.fn((_params?: unknown, _enabled?: boolean) => ({ data: { variables: [{ variable: 'chunkSize' }], }, @@ -15,9 +15,19 @@ vi.mock('@/app/components/workflow/store', () => ({ selector({ pipelineId: 'pipeline-1' }), })) -vi.mock('@/service/use-pipeline', () => ({ - useDraftPipelineProcessingParams: mockUseDraftPipelineProcessingParams, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.draft.processing.parameters.get': (o: { + input: { params: { pipeline_id: string }; query: { node_id: string } } + enabled?: boolean + }) => + mockUseDraftPipelineProcessingParams( + { pipeline_id: o.input.params.pipeline_id, node_id: o.input.query.node_id }, + o.enabled, + ), + }) +}) vi.mock('../form', () => ({ default: ({ variables }: { variables: Array<{ variable: string }> }) => ( diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/data-source.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/data-source.tsx index e1d679a793efa0..3b2c61213c302d 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/data-source.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/data-source.tsx @@ -1,9 +1,11 @@ import type { Datasource } from '../../test-run/types' +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' +import { selectPipelineStepParameters } from '@/app/components/rag-pipeline/utils/query-select' import { knowledgePipelineIdentityAtom } from '@/app/components/workflow/editor-resource/state' -import { useDraftPipelinePreProcessingParams } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import DataSourceOptions from '../../test-run/preparation/data-source-options' import Form from './form' @@ -15,12 +17,14 @@ type DatasourceProps = { const DataSource = ({ onSelect: setDatasource, dataSourceNodeId }: DatasourceProps) => { const { t } = useTranslation(['datasetPipeline']) const pipelineId = useAtomValue(knowledgePipelineIdentityAtom).pipelineId - const { data: paramsConfig } = useDraftPipelinePreProcessingParams( - { - pipeline_id: pipelineId!, - node_id: dataSourceNodeId, - }, - !!pipelineId && !!dataSourceNodeId, + const { data: paramsConfig } = useQuery( + consoleQuery.rag.pipelines.byPipelineId.workflows.draft.preProcessing.parameters.get.queryOptions( + { + input: { params: { pipeline_id: pipelineId! }, query: { node_id: dataSourceNodeId } }, + enabled: !!pipelineId && !!dataSourceNodeId, + select: selectPipelineStepParameters, + }, + ), ) return ( diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/process-documents.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/process-documents.tsx index c346816f8384de..c0912976a89a7f 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/process-documents.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/process-documents.tsx @@ -1,8 +1,10 @@ +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' +import { selectPipelineStepParameters } from '@/app/components/rag-pipeline/utils/query-select' import { knowledgePipelineIdentityAtom } from '@/app/components/workflow/editor-resource/state' -import { useDraftPipelineProcessingParams } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import Form from './form' type ProcessDocumentsProps = { @@ -12,12 +14,12 @@ type ProcessDocumentsProps = { const ProcessDocuments = ({ dataSourceNodeId }: ProcessDocumentsProps) => { const { t } = useTranslation(['datasetPipeline']) const pipelineId = useAtomValue(knowledgePipelineIdentityAtom).pipelineId - const { data: paramsConfig } = useDraftPipelineProcessingParams( - { - pipeline_id: pipelineId!, - node_id: dataSourceNodeId, - }, - !!pipelineId && !!dataSourceNodeId, + const { data: paramsConfig } = useQuery( + consoleQuery.rag.pipelines.byPipelineId.workflows.draft.processing.parameters.get.queryOptions({ + input: { params: { pipeline_id: pipelineId! }, query: { node_id: dataSourceNodeId } }, + enabled: !!pipelineId && !!dataSourceNodeId, + select: selectPipelineStepParameters, + }), ) return ( diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/hooks.spec.ts b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/hooks.spec.ts index 598ec4fb8627c2..85be5a74383e39 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/hooks.spec.ts +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/hooks.spec.ts @@ -2,7 +2,7 @@ import { renderEditorHook as renderHook } from '@/app/components/workflow/editor import { useInputVariables } from '../hooks' const mockUseDraftPipelineProcessingParams = vi.hoisted(() => - vi.fn(() => ({ + vi.fn((_params?: unknown, _enabled?: boolean) => ({ data: { variables: [{ variable: 'chunkSize' }] }, isFetching: true, })), @@ -13,9 +13,19 @@ vi.mock('@/app/components/workflow/store', () => ({ selector({ pipelineId: 'pipeline-1' }), })) -vi.mock('@/service/use-pipeline', () => ({ - useDraftPipelineProcessingParams: mockUseDraftPipelineProcessingParams, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.draft.processing.parameters.get': (o: { + input: { params: { pipeline_id: string }; query: { node_id: string } } + enabled?: boolean + }) => + mockUseDraftPipelineProcessingParams({ + pipeline_id: o.input.params.pipeline_id, + node_id: o.input.query.node_id, + }), + }) +}) describe('useInputVariables', () => { beforeEach(() => { diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx index 70e5d0b0d57749..dcf3f929cca365 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx @@ -1,11 +1,8 @@ import type { ZodSchema } from 'zod' +import type { useInputVariables } from '../hooks' import type { CustomActionsProps } from '@/app/components/base/form/components/form/actions' import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types' -import type { - PipelineProcessingParamsResponse, - RAGPipelineVariable, - RAGPipelineVariables, -} from '@/models/pipeline' +import type { RAGPipelineVariable, RAGPipelineVariables } from '@/models/pipeline' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, screen } from '@testing-library/react' import * as React from 'react' @@ -36,15 +33,20 @@ vi.mock('@/app/components/workflow/store', () => ({ }, })) -let mockParamsConfig: PipelineProcessingParamsResponse | undefined +type ProcessingParams = ReturnType['paramsConfig'] + +let mockParamsConfig: ProcessingParams | undefined let mockIsFetchingParams = false -vi.mock('@/service/use-pipeline', () => ({ - useDraftPipelineProcessingParams: () => ({ - data: mockParamsConfig, - isFetching: mockIsFetchingParams, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.draft.processing.parameters.get': () => ({ + data: mockParamsConfig, + isFetching: mockIsFetchingParams, + }), + }) +}) const mockUseInitialData = vi.fn() const mockUseConfigurations = vi.fn() @@ -168,7 +170,7 @@ const createMockSchema = (): ZodSchema => const setupMocks = (options?: { pipelineId?: string | null - paramsConfig?: PipelineProcessingParamsResponse + paramsConfig?: ProcessingParams isFetchingParams?: boolean initialData?: Record configurations?: BaseConfiguration[] diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/hooks.ts b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/hooks.ts index cd5809012a1ed0..9e2a49a6d88f57 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/hooks.ts +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/hooks.ts @@ -1,13 +1,17 @@ +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' +import { selectPipelineStepParameters } from '@/app/components/rag-pipeline/utils/query-select' import { knowledgePipelineIdentityAtom } from '@/app/components/workflow/editor-resource/state' -import { useDraftPipelineProcessingParams } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' export const useInputVariables = (datasourceNodeId: string) => { const pipelineId = useAtomValue(knowledgePipelineIdentityAtom).pipelineId - const { data: paramsConfig, isFetching: isFetchingParams } = useDraftPipelineProcessingParams({ - pipeline_id: pipelineId!, - node_id: datasourceNodeId, - }) + const { data: paramsConfig, isFetching: isFetchingParams } = useQuery( + consoleQuery.rag.pipelines.byPipelineId.workflows.draft.processing.parameters.get.queryOptions({ + input: { params: { pipeline_id: pipelineId! }, query: { node_id: datasourceNodeId } }, + select: selectPipelineStepParameters, + }), + ) return { isFetchingParams, diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx index 1174ee6a88c976..491564bd600bc8 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx @@ -5,11 +5,15 @@ import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { renderEditor as renderWithoutPricing } from '@/app/components/workflow/editor-resource/__tests__/render' +import { consoleQuery } from '@/service/console' import { seedAccountProfileQuery } from '@/test/console/account-profile' import { seedFeatures, seedSystemFeatures } from '@/test/console/query-data' import Publisher from '../index' import { Popup } from '../popup' +const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries') +const datasetListKey = consoleQuery.datasets.get.key() + const onPricingUrlUpdate = vi.hoisted(() => vi.fn()) vi.mock('@/features/system-features/state', async () => { @@ -142,25 +146,20 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ const mockPublishWorkflow = vi.fn() const mockPublishAsCustomizedPipeline = vi.fn() -const mockInvalidPublishedPipelineInfo = vi.fn() -const mockInvalidDatasetList = vi.fn() -const mockInvalidCustomizedTemplateList = vi.fn() - -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => mockInvalidDatasetList, -})) -vi.mock('@/service/use-base', () => ({ - useInvalid: () => mockInvalidPublishedPipelineInfo, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.customized.publish.post': () => ({ + mutateAsync: mockPublishAsCustomizedPipeline, + }), + }) +}) -vi.mock('@/service/use-pipeline', () => ({ - publishedPipelineInfoQueryKeyPrefix: ['pipeline', 'published'], - useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList, - usePublishAsCustomizedPipeline: () => ({ - mutateAsync: mockPublishAsCustomizedPipeline, - }), -})) +const expectInvalidated = (...path: string[]) => + expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ + queryKey: expect.arrayContaining([['console', ...path]]), + }) vi.mock('@/service/use-workflow', () => ({ usePublishWorkflow: () => ({ @@ -235,6 +234,7 @@ function publishWithKeyboard() { describe('publisher', () => { beforeEach(() => { + vi.spyOn(QueryClient.prototype, 'invalidateQueries') vi.clearAllMocks() vi.spyOn(console, 'error').mockImplementation(() => {}) mockPublishedAt.mockReturnValue(null) @@ -552,10 +552,12 @@ describe('publisher', () => { await waitFor(() => { expect(mockPublishAsCustomizedPipeline).toHaveBeenCalledWith({ - pipelineId: 'test-pipeline-id', - name: 'Test Pipeline', - icon_info: { type: 'emoji', emoji: '📚', background: '#fff' }, - description: 'Test description', + params: { pipeline_id: 'test-pipeline-id' }, + body: { + name: 'Test Pipeline', + icon_info: { type: 'emoji', emoji: '📚', background: '#fff' }, + description: 'Test description', + }, }) }) }) @@ -620,8 +622,8 @@ describe('publisher', () => { await waitFor(() => { expect(mockMutateDatasetRes).toHaveBeenCalled() - expect(mockInvalidPublishedPipelineInfo).toHaveBeenCalled() - expect(mockInvalidDatasetList).toHaveBeenCalled() + expectInvalidated('rag', 'pipelines', 'byPipelineId', 'workflows', 'publish', 'get') + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: datasetListKey }) }) }) @@ -672,7 +674,7 @@ describe('publisher', () => { fireEvent.click(screen.getByTestId('modal-confirm')) await waitFor(() => { - expect(mockInvalidCustomizedTemplateList).toHaveBeenCalled() + expectInvalidated('rag', 'pipeline', 'templates', 'get') }) }) }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx index e0c0ec7413272e..e7f29f4383a725 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx @@ -52,10 +52,6 @@ const mockHandleCheckBeforePublish = vi.fn().mockResolvedValue(true) const mockSetPublishedAt = vi.fn() const mockMutateDatasetRes = vi.fn() -const mockInvalidPublishedPipelineInfo = vi.fn() -const mockInvalidDatasetList = vi.fn() -const mockInvalidCustomizedTemplateList = vi.fn() - let mockPublishedAt: string | undefined = '2024-01-01T00:00:00Z' let mockDraftUpdatedAt: string | undefined = '2024-06-01T00:00:00Z' let mockPipelineId: string | undefined = 'pipeline-123' @@ -158,21 +154,14 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ }), })) -vi.mock('@/service/knowledge/use-dataset', () => ({ - useInvalidDatasetList: () => mockInvalidDatasetList, -})) - -vi.mock('@/service/use-base', () => ({ - useInvalid: () => mockInvalidPublishedPipelineInfo, -})) - -vi.mock('@/service/use-pipeline', () => ({ - publishedPipelineInfoQueryKeyPrefix: ['published-pipeline'], - useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList, - usePublishAsCustomizedPipeline: () => ({ - mutateAsync: mockPublishAsCustomizedPipeline, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.customized.publish.post': () => ({ + mutateAsync: mockPublishAsCustomizedPipeline, + }), + }) +}) vi.mock('@/service/use-workflow', () => ({ usePublishWorkflow: () => ({ diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx index c9958bfc744b08..e94d27822cfb23 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx @@ -3,6 +3,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiArrowDownSLine } from '@remixicon/react' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { useAtomValue } from 'jotai' import { memo, useCallback, useState } from 'react' @@ -13,10 +14,7 @@ import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-syn import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import Link from '@/next/link' -import { - useInvalidCustomizedTemplateList, - usePublishAsCustomizedPipeline, -} from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import PublishAsKnowledgePipelineModal from '../../publish-as-knowledge-pipeline-modal' import { Popup } from './popup' @@ -28,8 +26,10 @@ const Publisher = () => { const canPipelineRelease = useHooksStore((s) => s.accessControl.canReleaseAndVersion) const docLink = useDocLink() const pipelineId = useAtomValue(knowledgePipelineIdentityAtom).pipelineId - const { mutateAsync: publishAsCustomizedPipeline } = usePublishAsCustomizedPipeline() - const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList() + const queryClient = useQueryClient() + const { mutateAsync: publishAsCustomizedPipeline } = useMutation( + consoleQuery.rag.pipelines.byPipelineId.customized.publish.post.mutationOptions(), + ) const [showPublishAsKnowledgePipelineModal, setShowPublishAsKnowledgePipelineModal] = useState(false) const [isPublishingAsCustomizedPipeline, setIsPublishingAsCustomizedPipeline] = useState(false) @@ -56,10 +56,8 @@ const Publisher = () => { try { setIsPublishingAsCustomizedPipeline(true) await publishAsCustomizedPipeline({ - pipelineId: pipelineId || '', - name, - icon_info: icon, - description, + params: { pipeline_id: pipelineId || '' }, + body: { name, icon_info: icon, description }, }) toast.success( t(($) => $['publishTemplate.success.message'], { ns: 'datasetPipeline' }), @@ -80,7 +78,11 @@ const Publisher = () => { ), }, ) - invalidCustomizedTemplateList() + queryClient.invalidateQueries({ + queryKey: consoleQuery.rag.pipeline.templates.get.queryKey({ + input: { query: { type: 'customized' } }, + }), + }) } catch { toast.error(t(($) => $['publishTemplate.error.message'], { ns: 'datasetPipeline' })) } finally { @@ -91,7 +93,7 @@ const Publisher = () => { [ docLink, hidePublishAsKnowledgePipelineModal, - invalidCustomizedTemplateList, + queryClient, pipelineId, publishAsCustomizedPipeline, t, diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx index 7241f29c233bab..c9ce1409aff8df 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx @@ -14,7 +14,7 @@ import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Separator } from '@langgenius/dify-ui/separator' import { RiArrowRightUpLine, RiPlayCircleLine, RiTerminalBoxLine } from '@remixicon/react' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' -import { useQuery, useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { useAtomValue } from 'jotai' import { useQueryState } from 'nuqs' @@ -42,9 +42,6 @@ import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import Link from '@/next/link' import { useParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' -import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' -import { useInvalid } from '@/service/use-base' -import { publishedPipelineInfoQueryKeyPrefix } from '@/service/use-pipeline' import { usePublishWorkflow } from '@/service/use-workflow' import { getDatasetACLCapabilities } from '@/utils/permission' import { RAG_PIPELINE_PUBLISH_HOTKEY } from '../hotkeys' @@ -109,11 +106,10 @@ export function Popup({ const showConfirm = onShowConfirm ?? showLocalConfirm const hideConfirm = onHideConfirm ?? hideLocalConfirm const [publishing, { setFalse: hidePublishing, setTrue: showPublishing }] = useBoolean(false) - const invalidPublishedPipelineInfo = useInvalid([ - ...publishedPipelineInfoQueryKeyPrefix, - pipelineId, - ]) - const invalidDatasetList = useInvalidDatasetList() + const queryClient = useQueryClient() + const invalidDatasetList = useCallback(() => { + queryClient.invalidateQueries({ queryKey: consoleQuery.datasets.get.key() }) + }, [queryClient]) const handleHideConfirm = useCallback(() => { hideConfirm() onRequestClose?.() @@ -166,7 +162,11 @@ export function Popup({ ) workflowStore.getState().setPublishedAt(res.created_at) mutateDatasetRes?.() - invalidPublishedPipelineInfo() + queryClient.invalidateQueries({ + queryKey: consoleQuery.rag.pipelines.byPipelineId.workflows.publish.get.queryKey({ + input: { params: { pipeline_id: pipelineId || '' } }, + }), + }) invalidDatasetList() } } @@ -190,7 +190,7 @@ export function Popup({ t, workflowStore, mutateDatasetRes, - invalidPublishedPipelineInfo, + queryClient, invalidDatasetList, hidePublishing, handleHideConfirm, diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-init.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-init.spec.ts index 7d8e724cc9b6f0..5f4dee12cc339f 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-init.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-init.spec.ts @@ -64,7 +64,7 @@ describe('usePipelineInit', () => { dataset: { pipeline_id: 'test-pipeline-id', name: 'Test Knowledge', - icon_info: { icon: 'test-icon' }, + icon_info: { icon: 'test-icon', icon_type: 'emoji' }, }, } return selector(state) @@ -106,7 +106,12 @@ describe('usePipelineInit', () => { expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ knowledgeName: 'Test Knowledge', - knowledgeIcon: { icon: 'test-icon' }, + knowledgeIcon: { + icon: 'test-icon', + icon_type: 'emoji', + icon_background: undefined, + icon_url: undefined, + }, }) }) }) diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts index 72f72cdcf8d501..cac5525852eee9 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts @@ -79,10 +79,13 @@ vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({ }), })) -vi.mock('@/service/use-pipeline', () => ({ - useImportPipelineDSL: () => ({ mutateAsync: mockImportDSL }), - useImportPipelineDSLConfirm: () => ({ mutateAsync: mockImportDSLConfirm }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.imports.post': () => ({ mutateAsync: mockImportDSL }), + 'rag.pipelines.imports.byImportId.confirm.post': () => ({ mutateAsync: mockImportDSLConfirm }), + }) +}) vi.mock('@/service/workflow', () => ({ fetchWorkflowDraft: vi.fn().mockResolvedValue({ @@ -189,9 +192,11 @@ describe('useUpdateDSLModal', () => { }) expect(mockImportDSL).toHaveBeenCalledWith({ - mode: DSLImportMode.YAML_CONTENT, - yaml_content: 'test content', - pipeline_id: 'test-pipeline-id', + body: { + mode: DSLImportMode.YAML_CONTENT, + yaml_content: 'test content', + pipeline_id: 'test-pipeline-id', + }, }) }) @@ -476,7 +481,7 @@ describe('useUpdateDSLModal', () => { await (result.current.onUpdateDSLConfirm as unknown as AsyncFn)() }) - expect(mockImportDSLConfirm).toHaveBeenCalledWith('import-id') + expect(mockImportDSLConfirm).toHaveBeenCalledWith({ params: { import_id: 'import-id' } }) }) it('should notify success and call onCancel after successful confirm', async () => { diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline-init.ts b/web/app/components/rag-pipeline/hooks/use-pipeline-init.ts index f19d928c4d5b8d..85e4af35062529 100644 --- a/web/app/components/rag-pipeline/hooks/use-pipeline-init.ts +++ b/web/app/components/rag-pipeline/hooks/use-pipeline-init.ts @@ -1,7 +1,8 @@ import type { FetchWorkflowDraftResponse } from '@/types/workflow' import { ORPCError } from '@orpc/client' import { useAtomValue } from 'jotai' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { toIconInfo } from '@/app/components/datasets/dataset-fields' import { knowledgePipelineIdentityAtom } from '@/app/components/workflow/editor-resource/state' import { useWorkflowStore } from '@/app/components/workflow/store' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' @@ -16,7 +17,8 @@ export const usePipelineInit = () => { const [isLoading, setIsLoading] = useState(true) const { pipelineId } = useAtomValue(knowledgePipelineIdentityAtom) const knowledgeName = useDatasetDetailContextWithSelector((s) => s.dataset)?.name - const knowledgeIcon = useDatasetDetailContextWithSelector((s) => s.dataset)?.icon_info + const datasetIcon = useDatasetDetailContextWithSelector((s) => s.dataset?.icon_info) + const knowledgeIcon = useMemo(() => toIconInfo(datasetIcon), [datasetIcon]) useEffect(() => { workflowStore.setState({ knowledgeName, knowledgeIcon }) diff --git a/web/app/components/rag-pipeline/hooks/use-update-dsl-modal.ts b/web/app/components/rag-pipeline/hooks/use-update-dsl-modal.ts index 22fd574263f86b..54936f654e02c8 100644 --- a/web/app/components/rag-pipeline/hooks/use-update-dsl-modal.ts +++ b/web/app/components/rag-pipeline/hooks/use-update-dsl-modal.ts @@ -1,4 +1,5 @@ import type { MouseEventHandler } from 'react' +import { useMutation } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -10,7 +11,7 @@ import { initialEdges, initialNodes } from '@/app/components/workflow/utils' import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { DSLImportMode, DSLImportStatus } from '@/models/app' -import { useImportPipelineDSL, useImportPipelineDSLConfirm } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import { fetchWorkflowDraft } from '@/service/workflow' import { getErrorMessage } from '@/utils/error-message' @@ -30,8 +31,12 @@ export const useUpdateDSLModal = ({ onCancel, onImport }: UseUpdateDSLModalParam const { pipelineId } = useAtomValue(knowledgePipelineIdentityAtom) const workflowStore = useWorkflowStore() const { handleCheckPluginDependencies } = usePluginDependencies() - const { mutateAsync: importDSL } = useImportPipelineDSL() - const { mutateAsync: importDSLConfirm } = useImportPipelineDSLConfirm() + const { mutateAsync: importDSL } = useMutation( + consoleQuery.rag.pipelines.imports.post.mutationOptions(), + ) + const { mutateAsync: importDSLConfirm } = useMutation( + consoleQuery.rag.pipelines.imports.byImportId.confirm.post.mutationOptions(), + ) // File state const [currentFile, setDSLFile] = useState() const [fileContent, setFileContent] = useState() @@ -82,7 +87,10 @@ export const useUpdateDSLModal = ({ onCancel, onImport }: UseUpdateDSLModalParam [eventEmitter], ) const completeImport = useCallback( - async (pipelineId: string | undefined, status: DSLImportStatus = DSLImportStatus.COMPLETED) => { + async ( + pipelineId: string | null | undefined, + status: DSLImportStatus = DSLImportStatus.COMPLETED, + ) => { if (!pipelineId) { notifyError() return @@ -125,9 +133,11 @@ export const useUpdateDSLModal = ({ onCancel, onImport }: UseUpdateDSLModalParam if (!pipelineId || !fileContent) return setLoading(true) const response = await importDSL({ - mode: DSLImportMode.YAML_CONTENT, - yaml_content: fileContent, - pipeline_id: pipelineId, + body: { + mode: DSLImportMode.YAML_CONTENT, + yaml_content: fileContent, + pipeline_id: pipelineId, + }, }) const { id, status, pipeline_id, imported_dsl_version, current_dsl_version } = response if (isCompletedStatus(status)) await completeImport(pipeline_id, status) @@ -150,7 +160,9 @@ export const useUpdateDSLModal = ({ onCancel, onImport }: UseUpdateDSLModalParam const onUpdateDSLConfirm: MouseEventHandler = useCallback(async () => { if (!importId) return try { - const { status, pipeline_id, error } = await importDSLConfirm(importId) + const { status, pipeline_id, error } = await importDSLConfirm({ + params: { import_id: importId }, + }) if (status === DSLImportStatus.COMPLETED) { await completeImport(pipeline_id) return diff --git a/web/app/components/rag-pipeline/utils/__tests__/query-select.spec.ts b/web/app/components/rag-pipeline/utils/__tests__/query-select.spec.ts new file mode 100644 index 00000000000000..e16a155a2fc960 --- /dev/null +++ b/web/app/components/rag-pipeline/utils/__tests__/query-select.spec.ts @@ -0,0 +1,185 @@ +import { describe, expect, it } from 'vite-plus/test' +import { BlockEnum } from '@/app/components/workflow/types' +import { ChunkingMode } from '@/models/datasets' +import { DatasourceType, PipelineInputVarType } from '@/models/pipeline' +import { + isPipelineProcessingRun, + selectDatasourceSingleRun, + selectPipelineExecutionLog, + selectPipelineStepParameters, + selectPipelineTemplate, + selectPipelineTemplateList, + selectPreviewRunEstimate, + selectPublishedPipelineNodes, +} from '../query-select' + +const graphNode = { id: 'node-1', data: { type: 'datasource' }, position: { x: 0, y: 0 } } + +describe('pipeline query selectors', () => { + it('should narrow template icons and chunk structures', () => { + const { pipeline_templates } = selectPipelineTemplateList({ + pipeline_templates: [ + { + id: 'template-1', + name: 'Template', + description: '', + position: 1, + chunk_structure: 'hierarchical_model', + icon: { icon_type: 'emoji', icon: '📚', icon_background: '#fff' }, + }, + { + id: 'template-2', + name: 'Legacy', + description: '', + position: 2, + chunk_structure: '', + icon: { icon_type: 'unknown' }, + }, + ], + }) + + expect(pipeline_templates[0]).toMatchObject({ + chunk_structure: ChunkingMode.parentChild, + icon: { icon_type: 'emoji', icon: '📚', icon_background: '#fff' }, + }) + expect(pipeline_templates[1]).toMatchObject({ + chunk_structure: ChunkingMode.text, + icon: { icon_type: 'emoji', icon: '' }, + }) + }) + + it('should keep only placeable graph nodes and edges', () => { + const template = selectPipelineTemplate({ + id: 'template-1', + name: 'Template', + description: '', + export_data: 'dsl', + chunk_structure: 'text_model', + icon_info: {}, + graph: { + nodes: [graphNode, { id: 'broken' }], + edges: [{ id: 'edge-1', source: 'a', target: 'b' }, { id: 'broken' }], + }, + }) + + expect(template.graph.nodes).toEqual([graphNode]) + expect(template.graph.edges).toEqual([{ id: 'edge-1', source: 'a', target: 'b' }]) + expect(template.graph.viewport).toEqual({ x: 0, y: 0, zoom: 1 }) + expect(template.created_by).toBe('') + }) + + it('should read published pipeline nodes from the workflow graph', () => { + expect( + selectPublishedPipelineNodes({ + id: 'workflow-1', + graph: { nodes: [graphNode] }, + features: {}, + hash: 'hash', + version: 'v1', + marked_name: '', + marked_comment: '', + created_at: 0, + updated_at: 0, + tool_published: false, + environment_variables: [], + conversation_variables: [], + rag_pipeline_variables: [], + }), + ).toEqual([graphNode]) + }) + + it('should drop variables of unsupported types and unknown upload settings', () => { + const { variables } = selectPipelineStepParameters({ + variables: [ + { + belong_to_node_id: 'shared', + type: 'file', + label: 'File', + variable: 'file', + required: true, + default_value: 3, + allowed_file_types: ['document', 'spreadsheet'], + allowed_file_upload_methods: ['local_file', 'ftp'], + placeholder: null, + }, + { + belong_to_node_id: 'shared', + type: 'json', + label: 'JSON', + variable: 'json', + required: false, + }, + ], + }) + + expect(variables).toEqual([ + expect.objectContaining({ + type: PipelineInputVarType.singleFile, + default_value: 3, + allowed_file_types: ['document'], + allowed_file_upload_methods: ['local_file'], + placeholder: undefined, + }), + ]) + }) + + it('should default missing execution log fields', () => { + expect( + selectPipelineExecutionLog({ datasource_info: 'x', datasource_type: 'unknown' }), + ).toEqual({ + datasource_info: {}, + input_data: {}, + datasource_node_id: '', + datasource_type: DatasourceType.localFile, + }) + }) + + it('should tell processing runs from preview runs', () => { + const batch = { + batch: 'batch-1', + dataset: { id: 'dataset-1', name: 'Dataset', chunk_structure: 'text_model' }, + documents: [], + } + const preview = { + task_id: 'task-1', + workflow_run_id: 'run-1', + data: { + id: 'run-1', + workflow_id: 'workflow-1', + status: 'succeeded' as const, + elapsed_time: 1, + total_tokens: 0, + total_steps: 1, + created_at: 0, + finished_at: 1, + outputs: { total_nodes: 1, total_segments: 2, preview: [] }, + }, + } + + expect(isPipelineProcessingRun(batch)).toBe(true) + expect(isPipelineProcessingRun(preview)).toBe(false) + expect(selectPreviewRunEstimate(preview)).toEqual(preview.data.outputs) + expect( + selectPreviewRunEstimate({ ...preview, data: { ...preview.data, outputs: { text: 'x' } } }), + ).toBeUndefined() + }) + + it('should map a datasource single run to a node run result', () => { + const result = selectDatasourceSingleRun({ + id: 'execution-1', + node_type: 'not-a-block', + status: 'succeeded', + outputs: ['not', 'a', 'record'], + execution_metadata: { total_tokens: 3 }, + }) + + expect(result).toMatchObject({ + id: 'execution-1', + node_type: BlockEnum.DataSource, + status: 'succeeded', + outputs: undefined, + execution_metadata: { total_tokens: 3, total_price: 0, currency: '' }, + created_by: { id: '', name: '', email: '' }, + }) + }) +}) diff --git a/web/app/components/rag-pipeline/utils/query-select.ts b/web/app/components/rag-pipeline/utils/query-select.ts new file mode 100644 index 00000000000000..ad12dfaa859cfd --- /dev/null +++ b/web/app/components/rag-pipeline/utils/query-select.ts @@ -0,0 +1,231 @@ +import type { DocumentPipelineExecutionLogResponse } from '@dify/contracts/api/console/datasets/types.gen' +import type { + PipelineGenerateResponse, + PipelineTemplateDetailResponse, + PipelineTemplateListResponse as PipelineTemplateListContract, + PipelineVariableResponse, + PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponse, + RagPipelineImportCheckDependenciesResponse, + RagPipelinePublishedRunResponse, + RagPipelineStepParametersResponse, + WorkflowAppBlockingResponse, + WorkflowResponse, + WorkflowRunNodeExecutionResponse, +} from '@dify/contracts/api/console/rag/types.gen' +import type { Viewport } from 'reactflow' +import type { Dependency } from '@/app/components/plugins/types' +import type { Edge, Node } from '@/app/components/workflow/types' +import type { FileIndexingEstimateResponse, IconInfo } from '@/models/datasets' +import type { + PipelineExecutionLogResponse, + PipelineTemplate, + PipelineTemplateByIdResponse, + RAGPipelineVariable, +} from '@/models/pipeline' +import type { AppIconType } from '@/types/app' +import type { NodeRunResult } from '@/types/workflow' +import { BlockEnum, SupportUploadFileTypes } from '@/app/components/workflow/types' +import { ChunkingMode } from '@/models/datasets' +import { DatasourceType, PipelineInputVarType } from '@/models/pipeline' +import { TransferMethod } from '@/types/app' + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +const getString = (record: Record, key: string) => { + const value = record[key] + return typeof value === 'string' ? value : undefined +} + +const getNumber = (record: Record, key: string) => { + const value = record[key] + return typeof value === 'number' ? value : undefined +} + +const isOneOf = (values: Record, value: unknown): value is T => + typeof value === 'string' && Object.values(values).includes(value) + +const APP_ICON_TYPES = { image: 'image', emoji: 'emoji', link: 'link' } satisfies Record< + AppIconType, + AppIconType +> + +const toIconInfo = (icon: Record): IconInfo => { + const iconType = icon.icon_type + return { + icon: getString(icon, 'icon') ?? '', + icon_type: isOneOf(APP_ICON_TYPES, iconType) ? iconType : 'emoji', + icon_background: getString(icon, 'icon_background'), + icon_url: getString(icon, 'icon_url'), + } +} + +// Templates exported before a chunk structure was recorded default to general text chunks. +const toChunkingMode = (chunkStructure: string) => + isOneOf(ChunkingMode, chunkStructure) ? chunkStructure : ChunkingMode.text + +// The workflow graph is stored as free-form JSON; keep the nodes and edges React Flow can place. +const isGraphNode = (value: unknown): value is Node => + isRecord(value) && + typeof value.id === 'string' && + isRecord(value.data) && + isRecord(value.position) + +const isGraphEdge = (value: unknown): value is Edge => + isRecord(value) && typeof value.source === 'string' && typeof value.target === 'string' + +const toGraphNodes = (graph: Record) => + Array.isArray(graph.nodes) ? graph.nodes.filter(isGraphNode) : [] + +const toGraphEdges = (graph: Record) => + Array.isArray(graph.edges) ? graph.edges.filter(isGraphEdge) : [] + +const toViewport = (graph: Record): Viewport => { + const viewport = isRecord(graph.viewport) ? graph.viewport : {} + return { + x: getNumber(viewport, 'x') ?? 0, + y: getNumber(viewport, 'y') ?? 0, + zoom: getNumber(viewport, 'zoom') ?? 1, + } +} + +export const selectPipelineTemplateList = (data: PipelineTemplateListContract) => ({ + pipeline_templates: data.pipeline_templates.map((template): PipelineTemplate => ({ + id: template.id, + name: template.name, + description: template.description, + position: template.position, + icon: toIconInfo(template.icon), + chunk_structure: toChunkingMode(template.chunk_structure), + })), +}) + +export const selectPipelineTemplate = ( + template: PipelineTemplateDetailResponse, +): PipelineTemplateByIdResponse => ({ + id: template.id, + name: template.name, + description: template.description, + export_data: template.export_data, + created_by: template.created_by ?? '', + icon_info: toIconInfo(template.icon_info), + chunk_structure: toChunkingMode(template.chunk_structure), + graph: { + nodes: toGraphNodes(template.graph), + edges: toGraphEdges(template.graph), + viewport: toViewport(template.graph), + }, +}) + +export const selectPublishedPipelineNodes = (workflow: WorkflowResponse) => + toGraphNodes(workflow.graph) + +const toDefaultValue = (value: unknown) => + typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' + ? value + : undefined + +const toPipelineVariable = (variable: PipelineVariableResponse): RAGPipelineVariable[] => { + if (!isOneOf(PipelineInputVarType, variable.type)) return [] + return [ + { + belong_to_node_id: variable.belong_to_node_id, + type: variable.type, + label: variable.label, + variable: variable.variable, + required: variable.required, + max_length: variable.max_length ?? undefined, + default_value: toDefaultValue(variable.default_value), + placeholder: variable.placeholder ?? undefined, + unit: variable.unit ?? undefined, + tooltips: variable.tooltips ?? undefined, + options: variable.options ?? undefined, + allowed_file_types: variable.allowed_file_types?.filter((type) => + isOneOf(SupportUploadFileTypes, type), + ), + allowed_file_extensions: variable.allowed_file_extensions ?? undefined, + allowed_file_upload_methods: variable.allowed_file_upload_methods?.filter((method) => + isOneOf(TransferMethod, method), + ), + }, + ] +} + +// Variables of a type the input forms cannot render are left out. +export const selectPipelineStepParameters = (data: RagPipelineStepParametersResponse) => ({ + variables: data.variables.flatMap(toPipelineVariable), +}) + +export const selectPipelineExecutionLog = ( + log: DocumentPipelineExecutionLogResponse, +): PipelineExecutionLogResponse => ({ + datasource_info: isRecord(log.datasource_info) ? log.datasource_info : {}, + input_data: isRecord(log.input_data) ? log.input_data : {}, + datasource_node_id: log.datasource_node_id ?? '', + datasource_type: isOneOf(DatasourceType, log.datasource_type) + ? log.datasource_type + : DatasourceType.localFile, +}) + +export const selectPipelineDependencies = ( + data: RagPipelineImportCheckDependenciesResponse, +): Dependency[] => + (data.leaked_dependencies ?? []).map((dependency) => ({ + ...dependency, + // The dependency store represents an absent version with undefined. + value: { ...dependency.value, version: dependency.value.version ?? undefined }, + })) + +export const isPipelineProcessingRun = ( + response: RagPipelinePublishedRunResponse, +): response is PipelineGenerateResponse => 'batch' in response + +// Preview runs return the indexing estimate as the workflow outputs. +const isFileIndexingEstimate = (value: unknown): value is FileIndexingEstimateResponse => + isRecord(value) && typeof value.total_segments === 'number' && Array.isArray(value.preview) + +export const selectPreviewRunEstimate = (response: WorkflowAppBlockingResponse) => + isFileIndexingEstimate(response.data.outputs) ? response.data.outputs : undefined + +export const selectOnlineDocumentPreview = ( + data: PostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewResponse, +) => ({ content: getString(data, 'content') ?? '' }) + +const BLOCKS: Record = BlockEnum + +export const selectDatasourceSingleRun = ( + execution: WorkflowRunNodeExecutionResponse, +): NodeRunResult => { + const metadata = isRecord(execution.execution_metadata) ? execution.execution_metadata : undefined + return { + id: execution.id, + index: execution.index ?? 0, + predecessor_node_id: execution.predecessor_node_id ?? '', + node_id: execution.node_id ?? '', + node_type: isOneOf(BLOCKS, execution.node_type) ? execution.node_type : BlockEnum.DataSource, + title: execution.title ?? '', + inputs: execution.inputs, + inputs_truncated: execution.inputs_truncated ?? false, + process_data: execution.process_data, + process_data_truncated: execution.process_data_truncated ?? false, + outputs: isRecord(execution.outputs) ? execution.outputs : undefined, + outputs_truncated: execution.outputs_truncated ?? false, + status: execution.status ?? '', + error: execution.error ?? undefined, + elapsed_time: execution.elapsed_time ?? 0, + execution_metadata: metadata + ? { + total_tokens: getNumber(metadata, 'total_tokens') ?? 0, + total_price: getNumber(metadata, 'total_price') ?? 0, + currency: getString(metadata, 'currency') ?? '', + } + : undefined, + // Datasource nodes never run inside an iteration or loop. + metadata: { iterator_length: 0, iterator_index: 0, loop_length: 0, loop_index: 0 }, + created_at: execution.created_at ?? 0, + created_by: execution.created_by_account ?? { id: '', name: '', email: '' }, + finished_at: execution.finished_at ?? 0, + extras: execution.extras, + retry_index: execution.retry_index ?? undefined, + } +} diff --git a/web/app/components/snippet-list/__tests__/index.spec.tsx b/web/app/components/snippet-list/__tests__/index.spec.tsx index af15e86558e7e3..1131da3a687538 100644 --- a/web/app/components/snippet-list/__tests__/index.spec.tsx +++ b/web/app/components/snippet-list/__tests__/index.spec.tsx @@ -5,6 +5,20 @@ import { createConsoleQueryWrapper } from '@/test/console/query-data' import { renderWithNuqs } from '@/test/nuqs-testing' import SnippetList from '..' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => ({ + data: { + accounts: [ + { id: 'creator-1', name: 'Alice', avatar_url: null, status: 'active' }, + { id: 'creator-2', name: 'Bob', avatar_url: null, status: 'active' }, + ], + }, + }), + }) +}) + const mockUseInfiniteSnippetList = vi.hoisted(() => vi.fn()) const mockSetKeywords = vi.hoisted(() => vi.fn()) const mockSetTagIDs = vi.hoisted(() => vi.fn()) @@ -57,6 +71,18 @@ vi.mock('@/service/console', () => ({ systemFeatures: vi.fn(), }, consoleQuery: { + workspaces: { + current: { + members: { + get: { + queryOptions: (options: { input?: unknown }) => ({ + ...options, + queryKey: [['console', 'workspaces', 'current', 'members', 'get'], options], + }), + }, + }, + }, + }, account: { profile: { get: { @@ -99,17 +125,6 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/use-common', () => ({ - useMembers: () => ({ - data: { - accounts: [ - { id: 'creator-1', name: 'Alice', avatar_url: null, status: 'active' }, - { id: 'creator-2', name: 'Bob', avatar_url: null, status: 'active' }, - ], - }, - }), -})) - vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }), useSearchParams: () => new URLSearchParams(''), diff --git a/web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx b/web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx index 42495c505f60d9..1f2fe3525f4224 100644 --- a/web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx +++ b/web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx @@ -6,6 +6,24 @@ import { render } from 'vitest-browser-react' import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture' import SnippetList from '..' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => ({ + data: { + accounts: [ + { + avatar_url: null, + id: 'creator-1', + name: 'Alice', + status: 'active', + }, + ], + }, + }), + }) +}) + const mockUseInfiniteSnippetList = vi.hoisted(() => vi.fn()) const mockQueryState = vi.hoisted(() => ({ creatorIDs: [] as string[], @@ -48,21 +66,6 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/use-common', () => ({ - useMembers: () => ({ - data: { - accounts: [ - { - avatar_url: null, - id: 'creator-1', - name: 'Alice', - status: 'active', - }, - ], - }, - }), -})) - vi.mock('@/features/account-profile/client', () => ({ userProfileQueryOptions: () => ({ queryFn: async () => ({ profile: { id: 'creator-1' } }), @@ -83,6 +86,18 @@ vi.mock('@/service/console', () => ({ }, }, consoleQuery: { + workspaces: { + current: { + members: { + get: { + queryOptions: (options: { input?: unknown }) => ({ + ...options, + queryKey: [['console', 'workspaces', 'current', 'members', 'get'], options], + }), + }, + }, + }, + }, tags: { get: { key: () => ['tags'], diff --git a/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx b/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx index 39d21fb41070c8..89333ed951deb3 100644 --- a/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx +++ b/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx @@ -3,6 +3,40 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { render } from '@/test/console/render' import SnippetCard from '../snippet-card' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => ({ + data: { + accounts: [ + { + id: 'creator-id', + name: 'Creator', + email: 'creator@example.com', + avatar: '', + avatar_url: null, + role: 'editor', + last_login_at: '', + created_at: '', + status: 'active', + }, + { + id: 'updater-id', + name: 'Updater', + email: 'updater@example.com', + avatar: '', + avatar_url: null, + role: 'editor', + last_login_at: '', + created_at: '', + status: 'active', + }, + ], + }, + }), + }) +}) + const { mockDeleteMutate, mockDownloadBlob, @@ -38,37 +72,6 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/use-common', () => ({ - useMembers: () => ({ - data: { - accounts: [ - { - id: 'creator-id', - name: 'Creator', - email: 'creator@example.com', - avatar: '', - avatar_url: null, - role: 'editor', - last_login_at: '', - created_at: '', - status: 'active', - }, - { - id: 'updater-id', - name: 'Updater', - email: 'updater@example.com', - avatar: '', - avatar_url: null, - role: 'editor', - last_login_at: '', - created_at: '', - status: 'active', - }, - ], - }, - }), -})) - vi.mock('@/service/use-snippets', () => ({ useDeleteSnippetMutation: () => ({ mutate: mockDeleteMutate, diff --git a/web/app/components/snippet-list/components/snippet-card.tsx b/web/app/components/snippet-list/components/snippet-card.tsx index 800ce8bdb10327..34c044356ea27e 100644 --- a/web/app/components/snippet-list/components/snippet-card.tsx +++ b/web/app/components/snippet-list/components/snippet-card.tsx @@ -1,5 +1,4 @@ 'use client' - import type { SnippetListItem } from '@/types/snippet' import { AlertDialog, @@ -17,6 +16,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -29,7 +29,7 @@ import { toast } from '@/app/notifications' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { TagSelector } from '@/features/tag-management/components/tag-selector' import Link from '@/next/link' -import { useMembers } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { useDeleteSnippetMutation, useExportSnippetMutation, @@ -54,7 +54,9 @@ const SnippetCard = ({ const { t } = useTranslation(['snippet', 'datasetDocuments']) const { t: tCommon } = useTranslation(['common']) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) - const { data: membersData } = useMembers() + const { data: membersData } = useQuery( + consoleQuery.workspaces.current.members.get.queryOptions({ input: {} }), + ) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const updateSnippetMutation = useUpdateSnippetMutation() diff --git a/web/app/components/tools/edit-custom-collection-modal/__tests__/test-api.spec.tsx b/web/app/components/tools/edit-custom-collection-modal/__tests__/test-api.spec.tsx index 138651011af8d0..5358a24180e290 100644 --- a/web/app/components/tools/edit-custom-collection-modal/__tests__/test-api.spec.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/__tests__/test-api.spec.tsx @@ -22,7 +22,6 @@ describe('TestApi', () => { icon: { background: '', content: '' }, privacy_policy: '', custom_disclaimer: '', - id: 'test-id', labels: [], } diff --git a/web/app/components/tools/marketplace/__tests__/index.spec.tsx b/web/app/components/tools/marketplace/__tests__/index.spec.tsx index e49901b3012889..71649cfa384048 100644 --- a/web/app/components/tools/marketplace/__tests__/index.spec.tsx +++ b/web/app/components/tools/marketplace/__tests__/index.spec.tsx @@ -47,11 +47,6 @@ vi.mock('@/app/components/plugins/marketplace/hooks', () => ({ useMarketplacePlugins: (...args: unknown[]) => mockUseMarketplacePlugins(...args), })) -const mockUseAllToolProviders = vi.fn() -vi.mock('@/service/use-tools', () => ({ - useAllToolProviders: (...args: unknown[]) => mockUseAllToolProviders(...args), -})) - vi.mock('@/utils/var', () => ({ getMarketplaceUrl: vi.fn(() => 'https://marketplace.test/market'), })) diff --git a/web/app/components/tools/mcp/__tests__/create-card.spec.tsx b/web/app/components/tools/mcp/__tests__/create-card.spec.tsx index b8fc457486c2f9..fce5b5b99d77d0 100644 --- a/web/app/components/tools/mcp/__tests__/create-card.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/create-card.spec.tsx @@ -1,3 +1,4 @@ +import type { ToolProviderApiEntityResponse } from '@dify/contracts/api/console/workspaces/types.gen' import { fireEvent, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { @@ -9,14 +10,25 @@ import { render } from '@/test/console/render' import NewMCPCard, { NewMCPButton } from '../create-card' // Track the mock functions -const mockCreateMCP = vi.fn().mockResolvedValue({ id: 'new-mcp-id', name: 'New MCP' }) +const mockCreateMCP = vi.fn().mockResolvedValue({ + id: 'new-mcp-id', + name: 'New MCP', + author: 'tester', + label: { en_US: 'New MCP' }, + description: { en_US: '' }, + icon: '', + type: 'mcp', +} satisfies ToolProviderApiEntityResponse) // Mock the service -vi.mock('@/service/use-tools', () => ({ - useCreateMCP: () => ({ - mutateAsync: mockCreateMCP, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProvider.mcp.post': () => ({ + mutateAsync: mockCreateMCP, + }), + }) +}) // Mock the MCP Modal type MockMCPModalProps = { @@ -196,8 +208,10 @@ describe('NewMCPCard', () => { await waitFor(() => { expect(mockCreateMCP).toHaveBeenCalledWith({ - name: 'Test MCP', - server_url: 'https://test.com', + body: { + name: 'Test MCP', + server_url: 'https://test.com', + }, }) expect(handleCreate).toHaveBeenCalled() }) diff --git a/web/app/components/tools/mcp/__tests__/index.spec.tsx b/web/app/components/tools/mcp/__tests__/index.spec.tsx index 4dd304794d2bdf..d74d6a24a2b9b8 100644 --- a/web/app/components/tools/mcp/__tests__/index.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/index.spec.tsx @@ -23,23 +23,26 @@ const mockConsoleState = vi.hoisted(() => ({ workspacePermissionKeys: ['mcp.manage'] as string[], })) -vi.mock('@/service/use-tools', () => ({ - useAllMCPTools: (enabled?: boolean) => { - mockUseAllMCPTools(enabled) - return { - data: mockProviders, - isLoading: mockIsLoadingToolProviders, - refetch: mockRefetch, - } - }, - useUpdateMCP: () => ({ - mutateAsync: mockUpdateMCP, - }), - useDeleteMCP: () => ({ - mutateAsync: mockDeleteMCP, - isPending: false, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.mcp.get': ({ enabled }: { enabled?: boolean }) => { + mockUseAllMCPTools(enabled) + return { + data: mockProviders, + isLoading: mockIsLoadingToolProviders, + refetch: mockRefetch, + } + }, + 'workspaces.current.toolProvider.mcp.put': () => ({ + mutateAsync: mockUpdateMCP, + }), + 'workspaces.current.toolProvider.mcp.delete': () => ({ + mutateAsync: mockDeleteMCP, + isPending: false, + }), + }) +}) vi.mock('@/context/permission-state', async () => { const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') @@ -475,9 +478,11 @@ describe('MCPList', () => { }) expect(mockUpdateMCP).toHaveBeenCalledWith({ - name: 'Updated MCP', - server_url: 'https://updated.com', - provider_id: '1', + body: { + name: 'Updated MCP', + server_url: 'https://updated.com', + provider_id: '1', + }, }) expect(mockRefetch).toHaveBeenCalled() expect(screen.getByTestId('detail-panel')).toBeInTheDocument() @@ -528,7 +533,7 @@ describe('MCPList', () => { await Promise.resolve() }) - expect(mockDeleteMCP).toHaveBeenCalledWith('1') + expect(mockDeleteMCP).toHaveBeenCalledWith({ body: { provider_id: '1' } }) expect(mockRefetch).toHaveBeenCalled() expect(screen.queryByTestId('detail-panel')).not.toBeInTheDocument() }) diff --git a/web/app/components/tools/mcp/__tests__/mcp-server-modal.spec.tsx b/web/app/components/tools/mcp/__tests__/mcp-server-modal.spec.tsx index 6f6ce92961863f..154899a1158fee 100644 --- a/web/app/components/tools/mcp/__tests__/mcp-server-modal.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/mcp-server-modal.spec.tsx @@ -7,17 +7,19 @@ import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import MCPServerModal from '../mcp-server-modal' // Mock the services -vi.mock('@/service/use-tools', () => ({ - useCreateMCPServer: () => ({ - mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }), - isPending: false, - }), - useUpdateMCPServer: () => ({ - mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }), - isPending: false, - }), - useInvalidateMCPServerDetail: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.server.post': () => ({ + mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }), + isPending: false, + }), + 'apps.byAppId.server.put': () => ({ + mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }), + isPending: false, + }), + }) +}) describe('MCPServerModal', () => { const createWrapper = () => { diff --git a/web/app/components/tools/mcp/create-card.tsx b/web/app/components/tools/mcp/create-card.tsx index 03b9477f0afcb5..066fa39e19918a 100644 --- a/web/app/components/tools/mcp/create-card.tsx +++ b/web/app/components/tools/mcp/create-card.tsx @@ -1,29 +1,36 @@ 'use client' +import type { ComponentProps } from 'react' import type { ToolWithProvider } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' +import { useMutation } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry' import { useCanManageMCP } from '@/app/components/tools/hooks/use-tool-permissions' import { useDocLink } from '@/context/i18n' -import { useCreateMCP } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import CreateEntryCard from '../provider/create-entry-card' +import { selectToolProvider } from '../utils/query-select' import MCPModal from './modal' +type MCPModalConfirmPayload = Parameters['onConfirm']>[0] + type Props = Readonly<{ handleCreate: (provider: ToolWithProvider) => Promise | void }> function useMCPCreateAction({ handleCreate }: Props) { const canManageMCP = useCanManageMCP() - const { mutateAsync: createMCP } = useCreateMCP() + const { mutateAsync: createMCP } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.post.mutationOptions(), + ) const [showModal, setShowModal] = useState(false) - const create = async (info: Parameters[0]) => { + const create = async (info: MCPModalConfirmPayload) => { if (!canManageMCP) return - const provider = await createMCP(info) - await handleCreate(provider) + const provider = await createMCP({ body: info }) + await handleCreate(selectToolProvider(provider)) } return { diff --git a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx index 4359bd4c2ccdf9..1ccbeed6af093b 100644 --- a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx +++ b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx @@ -4,14 +4,19 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQuery } from '@/service/console' import { render } from '@/test/console/render' import MCPDetailContent from '../content' // Mutable mock functions const mockUpdateTools = vi.fn().mockResolvedValue({}) const mockAuthorizeMcp = vi.fn().mockResolvedValue({ result: 'success' }) -const mockInvalidateMCPTools = vi.fn() -const mockInvalidateAllMCPTools = vi.fn() +const mockInvalidateQueries = vi.fn() +const mcpToolsKey = consoleQuery.workspaces.current.tools.mcp.get.queryKey() +const mcpProviderToolsKey = + consoleQuery.workspaces.current.toolProvider.mcp.tools.byProviderId.get.queryKey({ + input: { params: { provider_id: 'mcp-1' } }, + }) const mockOpenOAuthPopup = vi.fn() // Mutable mock state @@ -27,22 +32,23 @@ let mockIsUpdating = false let mockIsAuthorizing = false // Mock the services -vi.mock('@/service/use-tools', () => ({ - useMCPTools: () => ({ - data: mockToolsData, - isFetching: mockIsFetching, - }), - useInvalidateMCPTools: () => mockInvalidateMCPTools, - useInvalidateAllMCPTools: () => mockInvalidateAllMCPTools, - useUpdateMCPTools: () => ({ - mutateAsync: mockUpdateTools, - isPending: mockIsUpdating, - }), - useAuthorizeMCP: () => ({ - mutateAsync: mockAuthorizeMcp, - isPending: mockIsAuthorizing, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProvider.mcp.tools.byProviderId.get': () => ({ + data: mockToolsData, + isFetching: mockIsFetching, + }), + 'workspaces.current.toolProvider.mcp.update.byProviderId.get': () => ({ + mutateAsync: mockUpdateTools, + isPending: mockIsUpdating, + }), + 'workspaces.current.toolProvider.mcp.auth.post': () => ({ + mutateAsync: mockAuthorizeMcp, + isPending: mockIsAuthorizing, + }), + }) +}) // Mock OAuth hook type OAuthArgs = readonly unknown[] @@ -118,6 +124,7 @@ describe('MCPDetailContent', () => { }, }, }) + vi.spyOn(queryClient, 'invalidateQueries').mockImplementation(mockInvalidateQueries) return ({ children }: { children: ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children) } @@ -151,8 +158,7 @@ describe('MCPDetailContent', () => { // Reset mocks mockUpdateTools.mockClear() mockAuthorizeMcp.mockClear() - mockInvalidateMCPTools.mockClear() - mockInvalidateAllMCPTools.mockClear() + mockInvalidateQueries.mockReset().mockResolvedValue(undefined) mockOpenOAuthPopup.mockClear() // Reset mock return values @@ -349,7 +355,7 @@ describe('MCPDetailContent', () => { await waitFor(() => { expect(onFirstCreate).toHaveBeenCalled() - expect(mockAuthorizeMcp).toHaveBeenCalledWith({ provider_id: 'mcp-1' }) + expect(mockAuthorizeMcp).toHaveBeenCalledWith({ body: { provider_id: 'mcp-1' } }) }) }) @@ -435,9 +441,9 @@ describe('MCPDetailContent', () => { fireEvent.click(getConfirmButton()) await waitFor(() => { - expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') - expect(mockInvalidateMCPTools).toHaveBeenCalledWith('mcp-1') - expect(mockInvalidateAllMCPTools).toHaveBeenCalled() + expect(mockUpdateTools).toHaveBeenCalledWith({ params: { provider_id: 'mcp-1' } }) + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: mcpProviderToolsKey }) + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: mcpToolsKey }) expect(onUpdate).toHaveBeenCalled() }) }) @@ -453,8 +459,8 @@ describe('MCPDetailContent', () => { fireEvent.click(getToolsBtn) await waitFor(() => { - expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') - expect(mockInvalidateAllMCPTools).toHaveBeenCalled() + expect(mockUpdateTools).toHaveBeenCalledWith({ params: { provider_id: 'mcp-1' } }) + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: mcpToolsKey }) }) }) }) @@ -533,7 +539,7 @@ describe('MCPDetailContent', () => { oauthCallback() await waitFor(() => { - expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') + expect(mockUpdateTools).toHaveBeenCalledWith({ params: { provider_id: 'mcp-1' } }) }) }) diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx index 8717e560ea2d95..e1ad0265c32672 100644 --- a/web/app/components/tools/mcp/detail/content.tsx +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -15,6 +15,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import copy from 'copy-to-clipboard' import * as React from 'react' @@ -23,13 +24,8 @@ import { useTranslation } from 'react-i18next' import Icon from '@/app/components/plugins/card/base/card-icon' import { useCanManageMCP } from '@/app/components/tools/hooks/use-tool-permissions' import { openOAuthPopup } from '@/hooks/use-oauth' -import { - useAuthorizeMCP, - useInvalidateAllMCPTools, - useInvalidateMCPTools, - useMCPTools, - useUpdateMCPTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' +import { selectToolProviderTools } from '../../utils/query-select' import ListLoading from './list-loading' import OperationDropdown from './operation-dropdown' import ToolItem from './tool-item' @@ -56,13 +52,20 @@ const MCPDetailContent: FC = ({ const { t } = useTranslation(['common', 'tools']) const canManageMCP = useCanManageMCP() - const { data, isFetching: isGettingTools } = useMCPTools( - detail.is_team_authorization ? detail.id : '', + const queryClient = useQueryClient() + const { data, isFetching: isGettingTools } = useQuery( + consoleQuery.workspaces.current.toolProvider.mcp.tools.byProviderId.get.queryOptions({ + input: { params: { provider_id: detail.id } }, + enabled: !!detail.is_team_authorization && !!detail.id, + select: selectToolProviderTools, + }), + ) + const { mutateAsync: updateTools, isPending: isUpdating } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.update.byProviderId.get.mutationOptions(), + ) + const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.auth.post.mutationOptions(), ) - const invalidateMCPTools = useInvalidateMCPTools() - const invalidateAllMCPTools = useInvalidateAllMCPTools() - const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools() - const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP() const toolList = data?.tools || [] const [isShowUpdateConfirm, { setTrue: showUpdateConfirm, setFalse: hideUpdateConfirm }] = @@ -71,19 +74,17 @@ const MCPDetailContent: FC = ({ const handleUpdateTools = useCallback(async () => { hideUpdateConfirm() if (!canManageMCP || !detail) return - await updateTools(detail.id) - invalidateMCPTools(detail.id) - invalidateAllMCPTools() + await updateTools({ params: { provider_id: detail.id } }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.toolProvider.mcp.tools.byProviderId.get.queryKey({ + input: { params: { provider_id: detail.id } }, + }), + }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.mcp.get.queryKey(), + }) onUpdate() - }, [ - canManageMCP, - detail, - hideUpdateConfirm, - invalidateAllMCPTools, - invalidateMCPTools, - onUpdate, - updateTools, - ]) + }, [canManageMCP, detail, hideUpdateConfirm, onUpdate, queryClient, updateTools]) const hasTriggeredAuthorizeRef = useRef(false) @@ -98,9 +99,7 @@ const MCPDetailContent: FC = ({ onFirstCreate() if (!detail) return try { - const res = await authorizeMcp({ - provider_id: detail.id, - }) + const res = await authorizeMcp({ body: { provider_id: detail.id } }) if (res.result === 'success') handleUpdateTools() else if (res.authorization_url) openOAuthPopup(res.authorization_url, handleOAuthCallback) } catch { diff --git a/web/app/components/tools/mcp/index.tsx b/web/app/components/tools/mcp/index.tsx index 6d6fc853af0bbc..f55292fabec2a4 100644 --- a/web/app/components/tools/mcp/index.tsx +++ b/web/app/components/tools/mcp/index.tsx @@ -13,13 +13,15 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' +import { useMutation, useQuery } from '@tanstack/react-query' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry' import { useCanManageMCP } from '@/app/components/tools/hooks/use-tool-permissions' import ToolCardSkeletonGrid from '@/app/components/tools/provider/tool-card-skeleton' -import { useAllMCPTools, useDeleteMCP, useUpdateMCP } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { toolsContentInsetClassNames, toolsUnifiedContentFrameClassName } from '../content-inset' +import { selectToolsWithProvider } from '../utils/query-select' import NewMCPCard from './create-card' import MCPDetailPanel from './detail/provider-detail' import MCPModal from './modal' @@ -54,7 +56,12 @@ const MCPList = ({ }: Props) => { const { t } = useTranslation(['common', 'tools']) const canManageMCP = useCanManageMCP() - const fallbackMCPToolsQuery = useAllMCPTools(providers === undefined) + const fallbackMCPToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: providers === undefined, + select: selectToolsWithProvider, + }), + ) const list = providers ?? fallbackMCPToolsQuery.data ?? EMPTY_MCP_TOOLS const isLoading = isLoadingProviders ?? fallbackMCPToolsQuery.isLoading const refetch = onRefresh ?? fallbackMCPToolsQuery.refetch @@ -76,8 +83,12 @@ const MCPList = ({ const editingProvider = list.find((provider) => provider.id === editingProviderID) const deletingProvider = list.find((provider) => provider.id === deletingProviderID) const detailProvider = editingProvider || deletingProvider ? undefined : currentProvider - const { mutateAsync: updateMCP } = useUpdateMCP({}) - const { mutateAsync: deleteMCP, isPending: isDeleting } = useDeleteMCP({}) + const { mutateAsync: updateMCP } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.put.mutationOptions(), + ) + const { mutateAsync: deleteMCP, isPending: isDeleting } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.delete.mutationOptions(), + ) const handleCreate = async (provider: ToolWithProvider) => { if (!canManageMCP) return @@ -121,8 +132,7 @@ const MCPList = ({ if (!canManageMCP || !editingProvider) return const res = (await updateMCP({ - ...form, - provider_id: editingProvider.id, + body: { ...form, provider_id: editingProvider.id }, })) as MutationResult if (res.result !== 'success') return @@ -141,7 +151,9 @@ const MCPList = ({ const handleDeleteConfirm = async () => { if (!canManageMCP || !deletingProvider) return - const res = (await deleteMCP(deletingProvider.id)) as MutationResult + const res = (await deleteMCP({ + body: { provider_id: deletingProvider.id }, + })) as MutationResult if (res.result !== 'success') return await refetch() diff --git a/web/app/components/tools/mcp/mcp-server-modal.tsx b/web/app/components/tools/mcp/mcp-server-modal.tsx index 6673108f797380..f4313504945026 100644 --- a/web/app/components/tools/mcp/mcp-server-modal.tsx +++ b/web/app/components/tools/mcp/mcp-server-modal.tsx @@ -5,14 +5,11 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Separator } from '@langgenius/dify-ui/separator' import { Textarea } from '@langgenius/dify-ui/textarea' import { RiCloseLine } from '@remixicon/react' +import { useMutation, useQueryClient } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item' -import { - useCreateMCPServer, - useInvalidateMCPServerDetail, - useUpdateMCPServer, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' type ModalProps = { appID: string @@ -33,9 +30,19 @@ type MCPServerParam = { const MCPServerModal = ({ appID, latestParams = [], data, show, onHide, appInfo }: ModalProps) => { const { t } = useTranslation(['common', 'tools']) - const { mutateAsync: createMCPServer, isPending: creating } = useCreateMCPServer() - const { mutateAsync: updateMCPServer, isPending: updating } = useUpdateMCPServer() - const invalidateMCPServerDetail = useInvalidateMCPServerDetail() + const queryClient = useQueryClient() + const { mutateAsync: createMCPServer, isPending: creating } = useMutation( + consoleQuery.apps.byAppId.server.post.mutationOptions(), + ) + const { mutateAsync: updateMCPServer, isPending: updating } = useMutation( + consoleQuery.apps.byAppId.server.put.mutationOptions(), + ) + const invalidateMCPServerDetail = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.apps.byAppId.server.get.queryKey({ + input: { params: { app_id: appID } }, + }), + }) const defaultDescription = data?.description || appInfo?.description || '' const [description, setDescription] = React.useState(defaultDescription) @@ -61,35 +68,28 @@ const MCPServerModal = ({ appID, latestParams = [], data, show, onHide, appInfo const submit = async () => { if (!data) { - const payload: { - appID: string + const body: { description?: string parameters: Record } = { - appID, parameters: getParamValue(), } - if (description.trim()) payload.description = description + if (description.trim()) body.description = description - await createMCPServer(payload) - invalidateMCPServerDetail(appID) + await createMCPServer({ params: { app_id: appID }, body }) + invalidateMCPServerDetail() onHide() } else { - const payload: { - appID: string - id: string - description: string - parameters: Record - } = { - appID, - id: data.id, - parameters: getParamValue(), - description, - } - - await updateMCPServer(payload) - invalidateMCPServerDetail(appID) + await updateMCPServer({ + params: { app_id: appID }, + body: { + id: data.id, + parameters: getParamValue(), + description, + }, + }) + invalidateMCPServerDetail() onHide() } } diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index d07104413b60f9..ab9c0390493686 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -39,14 +39,13 @@ type MCPModalConfirmPayload = { server_url: string icon_type: AppIconType icon: string - icon_background?: string | null + icon_background?: string server_identifier: string headers?: Record is_dynamic_registration?: boolean authentication?: { - client_id?: string + client_id: string client_secret?: string - grant_type?: string } configuration: { timeout: number diff --git a/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx b/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx index 92233e1e645125..22943655008ed2 100644 --- a/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx +++ b/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx @@ -73,7 +73,6 @@ vi.mock('@/app/components/tools/edit-custom-collection-modal', () => ({ schema: '{}', privacy_policy: '', custom_disclaimer: '', - id: 'test-id', labels: [], }) }} diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx index d95b94d48f5aec..7a279a0a786c3f 100644 --- a/web/app/components/tools/provider/__tests__/detail.spec.tsx +++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx @@ -3,8 +3,9 @@ import type { ReactElement } from 'react' import type { Collection } from '../../types' import { act, cleanup, fireEvent, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { modelProviderDetailsQueryOptions } from '@/app/components/header/account-setting/model-provider-page/utils' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' +import { createProviderResponseFixture } from '@/test/console/model-provider' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import { AuthType, CollectionType } from '../../types' import ProviderDetail from '../detail' @@ -37,11 +38,11 @@ const render = (ui: ReactElement) => { ], plugins: {}, }) - queryClient.setQueryData(commonQueryKeys.modelProviderDetails, { - data: [{ provider: 'model-collection-id' }], + queryClient.setQueryData(modelProviderDetailsQueryOptions().queryKey, { + data: [createProviderResponseFixture({ provider: 'model-collection-id' })], }) void queryClient.invalidateQueries({ - queryKey: commonQueryKeys.modelProviderDetails, + queryKey: modelProviderDetailsQueryOptions().queryKey, exact: true, refetchType: 'none', }) @@ -79,7 +80,6 @@ vi.mock('@/context/modal-context', () => ({ const mockFetchBuiltInToolList = vi.fn().mockResolvedValue([]) const mockFetchCustomToolList = vi.fn().mockResolvedValue([]) -const mockFetchModelToolList = vi.fn().mockResolvedValue([]) const mockFetchCustomCollection = vi.fn().mockResolvedValue({ credentials: { auth_type: 'none' }, }) @@ -98,7 +98,6 @@ const mockSaveWorkflowToolProvider = vi.fn().mockResolvedValue({}) vi.mock('@/service/tools', () => ({ fetchBuiltInToolList: (...args: unknown[]) => mockFetchBuiltInToolList(...args), fetchCustomToolList: (...args: unknown[]) => mockFetchCustomToolList(...args), - fetchModelToolList: (...args: unknown[]) => mockFetchModelToolList(...args), fetchCustomCollection: (...args: unknown[]) => mockFetchCustomCollection(...args), fetchWorkflowToolDetail: (...args: unknown[]) => mockFetchWorkflowToolDetail(...args), updateBuiltInToolCredential: (...args: unknown[]) => mockUpdateBuiltInToolCredential(...args), @@ -109,10 +108,6 @@ vi.mock('@/service/tools', () => ({ saveWorkflowToolProvider: (...args: unknown[]) => mockSaveWorkflowToolProvider(...args), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidateAllWorkflowTools: () => vi.fn(), -})) - vi.mock('@/utils/var', () => ({ basePath: '', })) @@ -276,7 +271,6 @@ describe('ProviderDetail', () => { }, ]) mockFetchCustomToolList.mockResolvedValue([]) - mockFetchModelToolList.mockResolvedValue([]) mockConsoleState.workspacePermissionKeys = [ 'tool.manage', 'credential.use', @@ -582,17 +576,6 @@ describe('ProviderDetail', () => { describe('Model Collection', () => { it('opens model modal when clicking auth button for model type', async () => { - mockFetchModelToolList.mockResolvedValue([ - { - name: 'model-tool-1', - label: { en_US: 'MT1' }, - description: { en_US: '' }, - parameters: [], - labels: [], - author: '', - output_schema: {}, - }, - ]) render( { }) }) - it('calls fetchModelToolList for model type', async () => { - render( - , - ) - await waitFor(() => { - expect(mockFetchModelToolList).toHaveBeenCalledWith('test-collection') - }) - }) - it('calls fetchCustomToolList for custom type', async () => { render( { it('opens and closes tool details from the keyboard', async () => { const user = userEvent.setup() - render() + render() const toolButton = screen.getByRole('button', { name: 'Tool label' }) toolButton.focus() @@ -62,7 +62,7 @@ describe('ToolItem', () => { it('does not open tool details when disabled', async () => { const user = userEvent.setup() - render() + render() const toolButton = screen.getByRole('button', { name: 'Tool label' }) expect(toolButton).toBeDisabled() diff --git a/web/app/components/tools/provider/detail.tsx b/web/app/components/tools/provider/detail.tsx index 9e7136d6692082..64f2ec50181e33 100644 --- a/web/app/components/tools/provider/detail.tsx +++ b/web/app/components/tools/provider/detail.tsx @@ -36,6 +36,7 @@ import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { modelProviderDetailsQueryOptions } from '@/app/components/header/account-setting/model-provider-page/utils' import Icon from '@/app/components/plugins/card/base/card-icon' import Description from '@/app/components/plugins/card/base/description' import OrgInfo from '@/app/components/plugins/card/base/org-info' @@ -53,7 +54,6 @@ import { fetchBuiltInToolList, fetchCustomCollection, fetchCustomToolList, - fetchModelToolList, fetchWorkflowToolDetail, removeBuiltInToolCredential, removeCustomCollection, @@ -61,8 +61,6 @@ import { updateBuiltInToolCredential, updateCustomCollection, } from '@/service/tools' -import { modelProviderDetailsQueryOptions } from '@/service/use-common' -import { useInvalidateAllWorkflowTools } from '@/service/use-tools' import { basePath } from '@/utils/var' import { AuthHeaderPrefix, AuthType, CollectionType } from '../types' import ToolItem from './tool-item' @@ -86,7 +84,6 @@ const ProviderDetail = ({ collection, onHide, onRefreshData }: Props) => { const canOpenCredentialSettings = isAuthed ? canUseCredential : canCreateCredential const canSaveCredentialSettings = isAuthed ? canManageCredential : canCreateCredential const canManageTools = useCanManageTools() - const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools() const [isDetailLoading, setIsDetailLoading] = useState(false) // built in provider @@ -206,7 +203,9 @@ const ProviderDetail = ({ collection, onHide, onRefreshData }: Props) => { if (!canManageTools) return await saveWorkflowToolProvider(data) - invalidateAllWorkflowTools() + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.workflow.get.queryKey(), + }) onRefreshData() getWorkflowToolProvider() setWorkflowToolDrawerOpen(false) @@ -234,9 +233,6 @@ const ProviderDetail = ({ collection, onHide, onRefreshData }: Props) => { if (collection.type === CollectionType.builtIn) { const list = await fetchBuiltInToolList(collection.name) setToolList(list) - } else if (collection.type === CollectionType.model) { - const list = await fetchModelToolList(collection.name) - setToolList(list) } else if (collection.type === CollectionType.workflow) { setToolList([]) } else { @@ -470,7 +466,6 @@ const ProviderDetail = ({ collection, onHide, onRefreshData }: Props) => { collection={collection} tool={tool} isBuiltIn={isBuiltIn} - isModel={isModel} /> ))} {collection.type === CollectionType.workflow && diff --git a/web/app/components/tools/provider/tool-item.tsx b/web/app/components/tools/provider/tool-item.tsx index eb1bd73307bc2a..f89a7b4329203a 100644 --- a/web/app/components/tools/provider/tool-item.tsx +++ b/web/app/components/tools/provider/tool-item.tsx @@ -12,10 +12,9 @@ type Props = Readonly<{ collection: Collection tool: Tool isBuiltIn: boolean - isModel: boolean }> -const ToolItem = ({ disabled, collection, tool, isBuiltIn, isModel }: Props) => { +const ToolItem = ({ disabled, collection, tool, isBuiltIn }: Props) => { const locale = useLocale() const language = getPluginLanguage(locale) const [showDetail, setShowDetail] = useState(false) @@ -53,7 +52,6 @@ const ToolItem = ({ disabled, collection, tool, isBuiltIn, isModel }: Props) => setShowDetail(false) }} isBuiltIn={isBuiltIn} - isModel={isModel} /> )} diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index 22884c218033c0..54b4c151d00f25 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -1,4 +1,6 @@ +import type { WorkflowTriggerResponse } from '@dify/contracts/api/console/apps/types.gen' import type { + ToolParameter as ContractToolParameter, DatasourceProviderType, ToolProviderType, } from '@dify/contracts/api/console/workspaces/types.gen' @@ -109,7 +111,7 @@ export type ToolParameter = { llm_description: string required: boolean multiple: boolean - default: string + default?: ContractToolParameter['default'] options?: { label: LocalizedText value: string @@ -178,8 +180,7 @@ export type CustomCollectionBackend = { schema: string privacy_policy: string custom_disclaimer: string - tools?: ParamItem[] - id: string + tools?: CustomParamSchema[] labels: string[] } @@ -191,7 +192,7 @@ type ParamItem = { type: string form: string required: boolean - default: string + default?: ContractToolParameter['default'] min?: number max?: number options?: { @@ -247,6 +248,7 @@ export type WorkflowToolProviderOutputSchema = { export type WorkflowToolProviderRequest = { name: string + label: string icon: Emoji description: string parameters: WorkflowToolProviderParameter[] @@ -290,3 +292,5 @@ export const MCPAuthMethod = { } as const export type MCPAuthMethod = (typeof MCPAuthMethod)[keyof typeof MCPAuthMethod] + +export type AppTrigger = WorkflowTriggerResponse diff --git a/web/app/components/tools/utils/__tests__/query-select.spec.ts b/web/app/components/tools/utils/__tests__/query-select.spec.ts new file mode 100644 index 00000000000000..746ecb55b7780e --- /dev/null +++ b/web/app/components/tools/utils/__tests__/query-select.spec.ts @@ -0,0 +1,215 @@ +import type { + ApiProviderDetailResponse, + ToolProviderApiEntityResponse, +} from '@dify/contracts/api/console/workspaces/types.gen' +import { describe, expect, it } from 'vite-plus/test' +import { + selectApiSchemaParameters, + selectCustomCollection, + selectMCPServerDetail, + selectRAGRecommendedPlugins, + selectToolsWithProvider, + selectWorkflowToolDetail, +} from '../query-select' + +const createProvider = ( + overrides: Partial = {}, +): ToolProviderApiEntityResponse => ({ + id: 'provider-1', + name: 'provider', + author: 'dify', + label: { en_US: 'Provider', zh_Hans: null }, + description: { en_US: 'Description', zh_Hans: '描述' }, + icon: '/icon.svg', + type: 'builtin', + ...overrides, +}) + +describe('tool query selectors', () => { + it('should fill untranslated labels and defaulted provider flags', () => { + const [provider] = selectToolsWithProvider([createProvider()]) + + expect(provider).toMatchObject({ + label: { en_US: 'Provider', zh_Hans: 'Provider' }, + description: { en_US: 'Description', zh_Hans: '描述' }, + is_team_authorization: false, + allow_delete: false, + labels: [], + team_credentials: {}, + tools: [], + }) + expect(provider!.label).not.toHaveProperty('ja_JP') + }) + + it('should keep emoji icons and drop unknown identity modes', () => { + const [provider] = selectToolsWithProvider([ + createProvider({ + icon: { background: '#fff', content: '🔧' }, + identity_mode: 'legacy', + plugin_id: null, + }), + ]) + + expect(provider!.icon).toEqual({ background: '#fff', content: '🔧' }) + expect(provider!.identity_mode).toBeUndefined() + expect(provider!.plugin_id).toBeUndefined() + }) + + it('should map tool parameters and keep non-string defaults', () => { + const [provider] = selectToolsWithProvider([ + createProvider({ + tools: [ + { + name: 'search', + author: 'dify', + label: { en_US: 'Search' }, + description: { en_US: 'Search the web' }, + parameters: [ + { + name: 'limit', + label: { en_US: 'Limit' }, + form: 'form', + type: 'number', + default: 10, + human_description: null, + options: [{ label: { en_US: 'Ten' }, value: '10' }], + }, + ], + }, + ], + }), + ]) + + const parameter = provider!.tools[0]!.parameters[0]! + expect(parameter).toMatchObject({ + default: 10, + required: false, + multiple: false, + llm_description: '', + human_description: { en_US: '', zh_Hans: '' }, + options: [{ label: { en_US: 'Ten', zh_Hans: 'Ten' }, value: '10' }], + }) + }) + + it('should keep only string MCP server parameters', () => { + expect( + selectMCPServerDetail({ + id: 'server-1', + name: 'server', + server_code: 'code', + description: '', + status: 'active', + parameters: { query: 'Question', count: 3 }, + }).parameters, + ).toEqual({ query: 'Question' }) + expect( + selectMCPServerDetail({ + id: 'server-1', + name: 'server', + server_code: 'code', + description: '', + status: 'active', + parameters: [], + }).parameters, + ).toBeUndefined() + }) + + it('should narrow the workflow tool output schema', () => { + const detail = selectWorkflowToolDetail({ + name: 'tool', + label: 'Tool', + description: '', + icon: { background: '#fff', content: '🔧' }, + parameters: [], + synced: true, + workflow_app_id: 'app-1', + workflow_tool_id: 'tool-1', + tool: { + name: 'tool', + author: 'dify', + label: { en_US: 'Tool' }, + description: { en_US: '' }, + output_schema: { + type: 'object', + properties: { text: { type: 'string', description: 'Text' }, broken: 'x' }, + }, + }, + }) + + expect(detail.privacy_policy).toBe('') + expect(detail.tool.output_schema).toEqual({ + type: 'object', + properties: { text: { type: 'string', description: 'Text' } }, + }) + }) + + it('should keep installable marketplace recommendations', () => { + const plugin = { + type: 'plugin', + org: 'langgenius', + name: 'notion', + plugin_id: 'langgenius/notion', + latest_package_identifier: 'langgenius/notion:1.0.0@hash', + } + const recommendations = selectRAGRecommendedPlugins({ + installed_recommended_plugins: [createProvider()], + uninstalled_recommended_plugins: [plugin, { plugin_id: 'broken' }], + }) + + expect(recommendations.installed_recommended_plugins[0]!.tools).toEqual([]) + expect(recommendations.uninstalled_recommended_plugins).toEqual([plugin]) + }) + + it('should label parsed operations without an id or summary as empty', () => { + expect( + selectApiSchemaParameters({ + schema_type: 'openapi', + credentials_schema: [], + warning: {}, + parameters_schema: [ + { author: 'dify', method: 'get', openapi: {}, server_url: 'https://api.example.com' }, + ], + }).parameters_schema, + ).toEqual([ + { + operation_id: '', + summary: '', + server_url: 'https://api.example.com', + method: 'get', + parameters: [], + }, + ]) + }) + + it('should read stored API provider credentials by their auth type', () => { + const detail: ApiProviderDetailResponse = { + schema_type: 'openapi', + schema: '{}', + tools: [], + icon: { background: '#FFF', content: '🔧' }, + } + + expect( + selectCustomCollection({ + ...detail, + credentials: { + auth_type: 'api_key_header', + api_key_header: 'Authorization', + api_key_header_prefix: 'bearer', + api_key_value: '[__HIDDEN__]', + }, + }).credentials, + ).toEqual({ + auth_type: 'api_key_header', + api_key_header: 'Authorization', + api_key_header_prefix: 'bearer', + api_key_value: '[__HIDDEN__]', + api_key_query_param: undefined, + }) + expect(selectCustomCollection(detail)).toMatchObject({ + credentials: { auth_type: 'none' }, + privacy_policy: '', + custom_disclaimer: '', + }) + }) +}) diff --git a/web/app/components/tools/utils/query-select.ts b/web/app/components/tools/utils/query-select.ts new file mode 100644 index 00000000000000..2fe2d7ff12e3fe --- /dev/null +++ b/web/app/components/tools/utils/query-select.ts @@ -0,0 +1,233 @@ +import type { AppMcpServerResponse } from '@dify/contracts/api/console/apps/types.gen' +import type { RagPipelineRecommendedPluginsResponse } from '@dify/contracts/api/console/rag/types.gen' +import type { + ApiProviderDetailResponse, + ApiSchemaParseResponse, + ApiToolBundle, + ToolParameter as ContractToolParameter, + I18nObject, + ToolApiEntity, + ToolApiListResponse, + ToolProviderApiEntityResponse, + ToolProviderListResponse, + WorkflowToolDetailResponse, +} from '@dify/contracts/api/console/workspaces/types.gen' +import type { MarketplacePlugin } from '@dify/contracts/marketplace' +import type { + Collection, + Credential, + CustomParamSchema, + Emoji, + MCPServerDetail, + Tool, + ToolParameter, + WorkflowToolProviderOutputSchema, + WorkflowToolProviderResponse, +} from '../types' +import type { RAGRecommendedPlugins, ToolWithProvider } from '@/app/components/workflow/types' +import { AuthHeaderPrefix, AuthType } from '../types' + +type LocalizedText = Collection['label'] +type ProviderIcon = ToolProviderApiEntityResponse['icon'] + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +const isOneOf = (values: Record, value: unknown): value is T => + typeof value === 'string' && Object.values(values).includes(value) + +const getString = (record: Record, key: string) => { + const value = record[key] + return typeof value === 'string' ? value : undefined +} + +// Untranslated locales fall back to en_US, matching renderI18nObject. +const toLocalizedText = ({ en_US, ja_JP, pt_BR, zh_Hans }: I18nObject): LocalizedText => ({ + en_US, + zh_Hans: zh_Hans ?? en_US, + ...(ja_JP != null && { ja_JP }), + ...(pt_BR != null && { pt_BR }), +}) + +const EMPTY_TEXT: LocalizedText = { en_US: '', zh_Hans: '' } + +// Providers return an icon URL, or an emoji record for API and workflow tools. +const toIcon = (icon: ProviderIcon): string | Emoji => + typeof icon === 'string' + ? icon + : { background: icon.background ?? '', content: icon.content ?? '' } + +const toIdentityMode = (mode: string | undefined) => + mode === 'off' || mode === 'idp_token' ? mode : undefined + +const toToolParameter = (parameter: ContractToolParameter): ToolParameter => ({ + ...parameter, + label: toLocalizedText(parameter.label), + human_description: parameter.human_description + ? toLocalizedText(parameter.human_description) + : EMPTY_TEXT, + llm_description: parameter.llm_description ?? '', + required: parameter.required ?? false, + multiple: parameter.multiple ?? false, + min: parameter.min ?? undefined, + max: parameter.max ?? undefined, + options: parameter.options?.map((option) => ({ + ...option, + label: toLocalizedText(option.label), + })), +}) + +const toTool = (tool: ToolApiEntity): Tool => ({ + ...tool, + label: toLocalizedText(tool.label), + description: toLocalizedText(tool.description), + labels: tool.labels ?? [], + output_schema: tool.output_schema ?? {}, + parameters: (tool.parameters ?? []).map(toToolParameter), +}) + +const toCollection = (provider: ToolProviderApiEntityResponse): Collection => ({ + ...provider, + label: toLocalizedText(provider.label), + description: toLocalizedText(provider.description), + icon: toIcon(provider.icon), + icon_dark: provider.icon_dark === undefined ? undefined : toIcon(provider.icon_dark), + team_credentials: provider.team_credentials ?? {}, + is_team_authorization: provider.is_team_authorization ?? false, + allow_delete: provider.allow_delete ?? false, + labels: provider.labels ?? [], + tools: provider.tools?.map(toTool), + plugin_id: provider.plugin_id ?? undefined, + server_url: provider.server_url ?? undefined, + server_identifier: provider.server_identifier ?? undefined, + masked_headers: provider.masked_headers ?? undefined, + authentication: provider.authentication + ? { + client_id: provider.authentication.client_id, + client_secret: provider.authentication.client_secret ?? undefined, + } + : undefined, + configuration: provider.configuration ?? undefined, + identity_mode: toIdentityMode(provider.identity_mode), + workflow_app_id: provider.workflow_app_id ?? undefined, +}) + +export const selectToolProvider = (provider: ToolProviderApiEntityResponse): ToolWithProvider => ({ + ...toCollection(provider), + tools: (provider.tools ?? []).map(toTool), + plugin_unique_identifier: provider.plugin_unique_identifier ?? undefined, +}) + +export const selectToolCollections = (data: ToolProviderListResponse) => data.map(toCollection) + +export const selectToolsWithProvider = (data: ToolProviderListResponse) => + data.map(selectToolProvider) + +export const selectToolProviderTools = (provider: ToolProviderApiEntityResponse) => ({ + tools: (provider.tools ?? []).map(toTool), +}) + +export const selectBuiltinProviderTools = (data: ToolApiListResponse) => data.map(toTool) + +export const selectMCPServerDetail = (detail: AppMcpServerResponse): MCPServerDetail => ({ + ...detail, + parameters: isRecord(detail.parameters) + ? Object.fromEntries( + Object.entries(detail.parameters).filter( + (entry): entry is [string, string] => typeof entry[1] === 'string', + ), + ) + : undefined, +}) + +const toOutputSchema = ( + schema: Record = {}, +): WorkflowToolProviderOutputSchema => ({ + type: typeof schema.type === 'string' ? schema.type : 'object', + properties: isRecord(schema.properties) + ? Object.fromEntries( + Object.entries(schema.properties).flatMap(([name, property]) => + isRecord(property) && typeof property.type === 'string' + ? [ + [ + name, + { + type: property.type, + description: + typeof property.description === 'string' ? property.description : '', + }, + ], + ] + : [], + ), + ) + : {}, +}) + +export const selectWorkflowToolDetail = ( + detail: WorkflowToolDetailResponse, +): WorkflowToolProviderResponse => ({ + ...detail, + privacy_policy: detail.privacy_policy ?? '', + tool: { + ...detail.tool, + label: toLocalizedText(detail.tool.label), + description: toLocalizedText(detail.tool.description), + labels: detail.tool.labels ?? [], + parameters: (detail.tool.parameters ?? []).map(toToolParameter), + output_schema: toOutputSchema(detail.tool.output_schema), + }, +}) + +// Uninstalled recommendations are marketplace manifests the console proxies unchanged; keep the +// ones that identify an installable package. +const isMarketplacePlugin = (value: Record): value is MarketplacePlugin => + typeof value.type === 'string' && + typeof value.org === 'string' && + typeof value.name === 'string' && + typeof value.plugin_id === 'string' && + typeof value.latest_package_identifier === 'string' + +export const selectRAGRecommendedPlugins = ( + data: RagPipelineRecommendedPluginsResponse, +): RAGRecommendedPlugins => ({ + installed_recommended_plugins: data.installed_recommended_plugins.map(selectToolProvider), + uninstalled_recommended_plugins: data.uninstalled_recommended_plugins.filter(isMarketplacePlugin), +}) + +// OpenAPI operations without an id or summary leave those labels empty in the editor. +const toCustomParamSchema = (bundle: ApiToolBundle): CustomParamSchema => ({ + operation_id: bundle.operation_id ?? '', + summary: bundle.summary ?? '', + server_url: bundle.server_url, + method: bundle.method, + parameters: (bundle.parameters ?? []).map(toToolParameter), +}) + +export const selectApiSchemaParameters = (data: ApiSchemaParseResponse) => ({ + schema_type: data.schema_type, + parameters_schema: data.parameters_schema.map(toCustomParamSchema), +}) + +// Stored API tool credentials hold the fields of their auth type. A provider saved without an +// auth type sends no credentials. +const toCredential = (credentials: Record): Credential => ({ + auth_type: isOneOf(AuthType, credentials.auth_type) ? credentials.auth_type : AuthType.none, + api_key_header: getString(credentials, 'api_key_header'), + api_key_value: getString(credentials, 'api_key_value'), + api_key_header_prefix: isOneOf(AuthHeaderPrefix, credentials.api_key_header_prefix) + ? credentials.api_key_header_prefix + : undefined, + api_key_query_param: getString(credentials, 'api_key_query_param'), +}) + +/** The custom tool editor state for a stored API provider. */ +export const selectCustomCollection = (detail: ApiProviderDetailResponse) => ({ + credentials: toCredential(detail.credentials ?? {}), + icon: detail.icon, + schema_type: detail.schema_type, + schema: detail.schema, + privacy_policy: detail.privacy_policy ?? '', + custom_disclaimer: detail.custom_disclaimer ?? '', + tools: detail.tools.map(toCustomParamSchema), +}) diff --git a/web/app/components/tools/utils/to-form-schema.ts b/web/app/components/tools/utils/to-form-schema.ts index a00a5f2a1fe8e2..fdcf75f8148667 100644 --- a/web/app/components/tools/utils/to-form-schema.ts +++ b/web/app/components/tools/utils/to-form-schema.ts @@ -46,7 +46,7 @@ export type ToolFormSchema = { _type: string form: string required: boolean - default?: string + default?: ToolParameter['default'] tooltip?: TypeWithI18N show_on: { variable: string; value: string }[] options?: { diff --git a/web/app/components/tools/utils/tool-list-query-key.ts b/web/app/components/tools/utils/tool-list-query-key.ts new file mode 100644 index 00000000000000..fa6293a87be4a7 --- /dev/null +++ b/web/app/components/tools/utils/tool-list-query-key.ts @@ -0,0 +1,20 @@ +import type { CollectionProviderType } from '../types' +import { consoleQuery } from '@/service/console' +import { CollectionType } from '../types' + +// Each provider type is listed by its own console operation; other provider types have no list. +export const getToolListQueryKey = (type?: CollectionProviderType) => { + const tools = consoleQuery.workspaces.current.tools + switch (type) { + case CollectionType.builtIn: + return tools.builtin.get.queryKey() + case CollectionType.custom: + return tools.api.get.queryKey() + case CollectionType.workflow: + return tools.workflow.get.queryKey() + case CollectionType.mcp: + return tools.mcp.get.queryKey() + default: + return undefined + } +} diff --git a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts index 5949c47dd1d78e..6f7c21cd497f47 100644 --- a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts +++ b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts @@ -1,11 +1,15 @@ +import type { ReactNode } from 'react' import type { WorkflowToolOutputVariable, WorkflowToolProviderRequest, WorkflowToolProviderResponse, } from '@/app/components/tools/types' import type { InputVar } from '@/app/components/workflow/types' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook } from '@testing-library/react' +import { createElement } from 'react' import { InputVarType } from '@/app/components/workflow/types' +import { consoleQuery } from '@/service/console' import { isParametersOutdated, useConfigureButton } from '../use-configure-button' const mockCreateWorkflowToolProvider = vi.fn() @@ -15,14 +19,29 @@ vi.mock('@/service/tools', () => ({ saveWorkflowToolProvider: (...args: unknown[]) => mockSaveWorkflowToolProvider(...args), })) -const mockInvalidateAllWorkflowTools = vi.fn() -const mockInvalidateWorkflowToolDetailByAppID = vi.fn() const mockUseWorkflowToolDetailByAppID = vi.fn() -vi.mock('@/service/use-tools', () => ({ - useInvalidateAllWorkflowTools: () => mockInvalidateAllWorkflowTools, - useInvalidateWorkflowToolDetailByAppID: () => mockInvalidateWorkflowToolDetailByAppID, - useWorkflowToolDetailByAppID: (...args: unknown[]) => mockUseWorkflowToolDetailByAppID(...args), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProvider.workflow.get.get': ({ + input, + enabled, + }: { + input: { query: { workflow_app_id: string } } + enabled?: boolean + }) => mockUseWorkflowToolDetailByAppID(input.query.workflow_app_id, enabled), + }) +}) + +let queryClient: QueryClient +const wrapper = ({ children }: { children: ReactNode }) => + createElement(QueryClientProvider, { client: queryClient }, children) +const workflowToolsKey = consoleQuery.workspaces.current.tools.workflow.get.queryKey() +const detailKey = consoleQuery.workspaces.current.toolProvider.workflow.get.get.queryKey({ + input: { query: { workflow_app_id: 'app-123' } }, +}) +const isInvalidated = (queryKey: readonly unknown[]) => + queryClient.getQueryState(queryKey)?.isInvalidated const mockToastNotify = vi.fn() vi.mock('@/app/notifications', () => ({ @@ -203,6 +222,9 @@ describe('isParametersOutdated', () => { describe('useConfigureButton', () => { beforeEach(() => { vi.clearAllMocks() + queryClient = new QueryClient() + for (const key of [workflowToolsKey, detailKey]) + queryClient.setQueryData(key as readonly unknown[], {}) mockUseWorkflowToolDetailByAppID.mockImplementation((_appId: string, enabled: boolean) => ({ data: enabled ? createMockDetail() : undefined, isLoading: false, @@ -215,31 +237,39 @@ describe('useConfigureButton', () => { describe('Initialization', () => { it('should return workflow tool state without owning drawer visibility', () => { - const { result } = renderHook(() => useConfigureButton(createDefaultOptions())) + const { result } = renderHook(() => useConfigureButton(createDefaultOptions()), { + wrapper, + }) expect(result.current.payload).toMatchObject({ workflow_app_id: 'app-123' }) }) it('should forward isLoading from query hook', () => { mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: undefined, isLoading: true }) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.isLoading).toBe(true) }) it('should call query hook with enabled=true when published', () => { - renderHook(() => useConfigureButton(createDefaultOptions({ published: true }))) + renderHook(() => useConfigureButton(createDefaultOptions({ published: true })), { + wrapper, + }) expect(mockUseWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123', true) }) it('should call query hook with enabled=false when not published', () => { - renderHook(() => useConfigureButton(createDefaultOptions({ published: false }))) + renderHook(() => useConfigureButton(createDefaultOptions({ published: false })), { + wrapper, + }) expect(mockUseWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123', false) }) it('should call query hook with enabled=false when controller is disabled', () => { - renderHook(() => - useConfigureButton(createDefaultOptions({ enabled: false, published: true })), + renderHook( + () => useConfigureButton(createDefaultOptions({ enabled: false, published: true })), + { wrapper }, ) expect(mockUseWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123', false) }) @@ -248,33 +278,39 @@ describe('useConfigureButton', () => { // Computed values describe('Computed - outdated', () => { it('should be false when not published (no detail)', () => { - const { result } = renderHook(() => useConfigureButton(createDefaultOptions())) + const { result } = renderHook(() => useConfigureButton(createDefaultOptions()), { + wrapper, + }) expect(result.current.outdated).toBe(false) }) it('should be true when parameters differ', () => { - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - inputs: [ - createMockInputVar({ variable: 'test_var' }), - createMockInputVar({ variable: 'extra_var' }), - ], - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + inputs: [ + createMockInputVar({ variable: 'test_var' }), + createMockInputVar({ variable: 'extra_var' }), + ], + }), + ), + { wrapper }, ) expect(result.current.outdated).toBe(true) }) it('should be false when parameters match', () => { - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - inputs: [createMockInputVar({ variable: 'test_var', required: true })], - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + inputs: [createMockInputVar({ variable: 'test_var', required: true })], + }), + ), + { wrapper }, ) expect(result.current.outdated).toBe(false) }) @@ -282,7 +318,9 @@ describe('useConfigureButton', () => { describe('Computed - payload', () => { it('should use prop values when not published', () => { - const { result } = renderHook(() => useConfigureButton(createDefaultOptions())) + const { result } = renderHook(() => useConfigureButton(createDefaultOptions()), { + wrapper, + }) expect(result.current.payload).toMatchObject({ icon: createMockEmoji(), @@ -304,8 +342,9 @@ describe('useConfigureButton', () => { }) it('should use detail values when published with detail', () => { - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.payload).toMatchObject({ @@ -326,8 +365,9 @@ describe('useConfigureButton', () => { it('should return empty parameters when published without detail', () => { mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: undefined, isLoading: false }) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.payload.parameters).toHaveLength(0) @@ -335,8 +375,9 @@ describe('useConfigureButton', () => { }) it('should build output parameters from detail output_schema', () => { - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.payload.outputParameters).toHaveLength(1) @@ -352,8 +393,9 @@ describe('useConfigureButton', () => { detail.tool.output_schema = undefined mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: detail, isLoading: false }) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.payload.outputParameters[0]).toMatchObject({ @@ -363,13 +405,15 @@ describe('useConfigureButton', () => { }) it('should convert paragraph type to string in existing parameters', () => { - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - inputs: [createMockInputVar({ variable: 'test_var', type: InputVarType.paragraph })], - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + inputs: [createMockInputVar({ variable: 'test_var', type: InputVarType.paragraph })], + }), + ), + { wrapper }, ) expect(result.current.payload.parameters[0]!.type).toBe('string') @@ -381,8 +425,9 @@ describe('useConfigureButton', () => { it('should publish before creating the provider', async () => { mockCreateWorkflowToolProvider.mockResolvedValue({}) const handlePublish = vi.fn().mockResolvedValue(undefined) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ handlePublish })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ handlePublish })), + { wrapper }, ) await act(async () => { @@ -402,8 +447,9 @@ describe('useConfigureButton', () => { it('should not create the provider when publishing fails', async () => { const handlePublish = vi.fn().mockRejectedValue(new Error('Publish failed')) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ handlePublish })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ handlePublish })), + { wrapper }, ) await act(async () => { @@ -422,8 +468,9 @@ describe('useConfigureButton', () => { mockCreateWorkflowToolProvider.mockResolvedValue({}) const onRefreshData = vi.fn() const onConfigured = vi.fn() - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ onRefreshData, onConfigured })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ onRefreshData, onConfigured })), + { wrapper }, ) await act(async () => { @@ -435,16 +482,18 @@ describe('useConfigureButton', () => { }) expect(mockCreateWorkflowToolProvider).toHaveBeenCalled() - expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled() + expect(isInvalidated(workflowToolsKey)).toBe(true) expect(onRefreshData).toHaveBeenCalled() - expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') + expect(isInvalidated(detailKey)).toBe(true) expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: expect.any(String) }) expect(onConfigured).toHaveBeenCalled() }) it('should show error toast on failure', async () => { mockCreateWorkflowToolProvider.mockRejectedValue(new Error('Create failed')) - const { result } = renderHook(() => useConfigureButton(createDefaultOptions())) + const { result } = renderHook(() => useConfigureButton(createDefaultOptions()), { + wrapper, + }) await act(async () => { await result.current.handleCreate( @@ -464,15 +513,17 @@ describe('useConfigureButton', () => { const handlePublish = vi.fn().mockResolvedValue(undefined) const onRefreshData = vi.fn() const onConfigured = vi.fn() - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - handlePublish, - onRefreshData, - onConfigured, - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + handlePublish, + onRefreshData, + onConfigured, + }), + ), + { wrapper }, ) await act(async () => { @@ -485,21 +536,23 @@ describe('useConfigureButton', () => { expect(handlePublish).toHaveBeenCalled() expect(mockSaveWorkflowToolProvider).toHaveBeenCalled() expect(onRefreshData).toHaveBeenCalled() - expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled() - expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') + expect(isInvalidated(workflowToolsKey)).toBe(true) + expect(isInvalidated(detailKey)).toBe(true) expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: expect.any(String) }) expect(onConfigured).toHaveBeenCalled() }) it('should show error toast when publish fails', async () => { const handlePublish = vi.fn().mockRejectedValue(new Error('Publish failed')) - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - handlePublish, - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + handlePublish, + }), + ), + { wrapper }, ) await act(async () => { @@ -514,8 +567,9 @@ describe('useConfigureButton', () => { it('should show error toast when save fails', async () => { mockSaveWorkflowToolProvider.mockRejectedValue(new Error('Save failed')) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) await act(async () => { @@ -535,38 +589,40 @@ describe('useConfigureButton', () => { const options = createDefaultOptions({ published: true, detailNeedUpdate: false }) const { rerender } = renderHook( (props: ReturnType) => useConfigureButton(props), - { initialProps: options }, + { initialProps: options, wrapper }, ) rerender({ ...options, detailNeedUpdate: true }) - expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') + expect(isInvalidated(detailKey)).toBe(true) }) it('should not invalidate when detailNeedUpdate stays false', () => { const options = createDefaultOptions({ published: true, detailNeedUpdate: false }) const { rerender } = renderHook( (props: ReturnType) => useConfigureButton(props), - { initialProps: options }, + { initialProps: options, wrapper }, ) rerender({ ...options }) - expect(mockInvalidateWorkflowToolDetailByAppID).not.toHaveBeenCalled() + expect(isInvalidated(detailKey)).toBe(false) }) it('should not invalidate detail while disabled', () => { - renderHook(() => - useConfigureButton( - createDefaultOptions({ - enabled: false, - published: true, - detailNeedUpdate: true, - }), - ), + renderHook( + () => + useConfigureButton( + createDefaultOptions({ + enabled: false, + published: true, + detailNeedUpdate: true, + }), + ), + { wrapper }, ) - expect(mockInvalidateWorkflowToolDetailByAppID).not.toHaveBeenCalled() + expect(isInvalidated(detailKey)).toBe(false) }) }) @@ -574,8 +630,9 @@ describe('useConfigureButton', () => { describe('Edge Cases', () => { it('should handle undefined detail from query gracefully', () => { mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: undefined, isLoading: false }) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.outdated).toBe(false) @@ -587,26 +644,30 @@ describe('useConfigureButton', () => { detail.tool.parameters = [] mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: detail, isLoading: false }) - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - inputs: [], - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + inputs: [], + }), + ), + { wrapper }, ) expect(result.current.outdated).toBe(false) }) it('should handle undefined inputs and outputs', () => { - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - inputs: undefined, - outputs: undefined, - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + inputs: undefined, + outputs: undefined, + }), + ), + { wrapper }, ) expect(result.current.payload.parameters).toHaveLength(0) @@ -615,12 +676,14 @@ describe('useConfigureButton', () => { it('should handle missing onRefreshData callback in create', async () => { mockCreateWorkflowToolProvider.mockResolvedValue({}) - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - onRefreshData: undefined, - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + onRefreshData: undefined, + }), + ), + { wrapper }, ) // Should not throw diff --git a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts index 44e9f6f96e9c12..3cbbda0154bccc 100644 --- a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts +++ b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts @@ -8,15 +8,13 @@ import type { } from '@/app/components/tools/types' import type { InputVar } from '@/app/components/workflow/types' import type { PublishWorkflowParams } from '@/types/workflow' -import { useEffect, useMemo, useRef } from 'react' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { selectWorkflowToolDetail } from '@/app/components/tools/utils/query-select' import { toast } from '@/app/notifications' +import { consoleQuery } from '@/service/console' import { createWorkflowToolProvider, saveWorkflowToolProvider } from '@/service/tools' -import { - useInvalidateAllWorkflowTools, - useInvalidateWorkflowToolDetailByAppID, - useWorkflowToolDetailByAppID, -} from '@/service/use-tools' // region Pure helpers @@ -129,24 +127,33 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { const { t } = useTranslation(['common']) // Data fetching via React Query - const { data: detail, isLoading } = useWorkflowToolDetailByAppID( - workflowAppId, - enabled && published, + const queryClient = useQueryClient() + const detailQueryKey = consoleQuery.workspaces.current.toolProvider.workflow.get.get.queryKey({ + input: { query: { workflow_app_id: workflowAppId } }, + }) + const { data: detail, isLoading } = useQuery( + consoleQuery.workspaces.current.toolProvider.workflow.get.get.queryOptions({ + input: { query: { workflow_app_id: workflowAppId } }, + enabled: enabled && published && !!workflowAppId, + select: selectWorkflowToolDetail, + }), ) - // Invalidation functions (store in ref for stable effect dependency) - const invalidateDetail = useInvalidateWorkflowToolDetailByAppID() - const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools() - - const invalidateDetailRef = useRef(invalidateDetail) - useEffect(() => { - invalidateDetailRef.current = invalidateDetail - }, [invalidateDetail]) + const invalidateDetail = () => queryClient.invalidateQueries({ queryKey: detailQueryKey }) + const invalidateAllWorkflowTools = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.workflow.get.queryKey(), + }) // Refetch when detailNeedUpdate becomes true useEffect(() => { - if (enabled && detailNeedUpdate) invalidateDetailRef.current(workflowAppId) - }, [detailNeedUpdate, enabled, workflowAppId]) + if (enabled && detailNeedUpdate) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.toolProvider.workflow.get.get.queryKey({ + input: { query: { workflow_app_id: workflowAppId } }, + }), + }) + }, [detailNeedUpdate, enabled, queryClient, workflowAppId]) // Computed values const outdated = useMemo(() => isParametersOutdated(detail, inputs), [detail, inputs]) @@ -188,7 +195,7 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { await createWorkflowToolProvider(data) invalidateAllWorkflowTools() onRefreshData?.() - invalidateDetail(workflowAppId) + invalidateDetail() toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) onConfigured?.() } catch (e) { @@ -208,7 +215,7 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { await saveWorkflowToolProvider(data) onRefreshData?.() invalidateAllWorkflowTools() - invalidateDetail(workflowAppId) + invalidateDetail() toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) onConfigured?.() } catch (e) { diff --git a/web/app/components/workflow-app/__tests__/index.spec.tsx b/web/app/components/workflow-app/__tests__/index.spec.tsx index 16a851138be9d5..22e797d61c1bfe 100644 --- a/web/app/components/workflow-app/__tests__/index.spec.tsx +++ b/web/app/components/workflow-app/__tests__/index.spec.tsx @@ -125,9 +125,12 @@ vi.mock('@/service/log', () => ({ fetchRunDetail: (...args: unknown[]) => mockFetchRunDetail(...args), })) -vi.mock('@/service/use-tools', () => ({ - useAppTriggers: () => appTriggersState, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.triggers.get': () => appTriggersState, + }) +}) vi.mock('../hooks/use-workflow-init', () => ({ useWorkflowInit: () => workflowInitState, diff --git a/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx b/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx index 71b3be6ec124e8..7e11c35180a03d 100644 --- a/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx +++ b/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx @@ -45,12 +45,11 @@ vi.mock('@/next/navigation', async (importOriginal) => ({ useSearchParams: () => ({ get: () => null }), })) -vi.mock('@/service/use-tools', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useAppTriggers: () => ({}), - } +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.triggers.get': () => ({}), + }) }) vi.mock('../hooks/use-get-run-and-trace-url', () => ({ diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx index 3e8be595d8475d..dc5c2f9e8ff37b 100644 --- a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx @@ -51,7 +51,6 @@ const mockHandleSyncWorkflowDraft = vi.fn() const mockPublishWorkflow = vi.fn() const mockUpdatePublishedWorkflow = vi.fn() const mockResetWorkflowVersionHistory = vi.fn() -const mockInvalidateAppTriggers = vi.fn() const mockInvalidateQueries = vi.fn() const mockSetPublishedAt = vi.fn() const mockSetLastPublishedHasUserInput = vi.fn() @@ -222,10 +221,6 @@ vi.mock('@/service/use-workflow', () => ({ useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory, })) -vi.mock('@/service/use-tools', () => ({ - useInvalidateAppTriggers: () => mockInvalidateAppTriggers, -})) - vi.mock('@/next/navigation', async (importOriginal) => ({ ...(await importOriginal()), useParams: () => ({ appId: 'app-id' }), @@ -628,7 +623,11 @@ describe('FeaturesTrigger', () => { releaseNotes: '', }) expect(mockUpdatePublishedWorkflow).toHaveBeenCalledWith('app-id') - expect(mockInvalidateAppTriggers).toHaveBeenCalledWith('app-id') + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.apps.byAppId.triggers.get.queryKey({ + input: { params: { app_id: 'app-id' } }, + }), + }) expect(mockSetPublishedAt).toHaveBeenCalledWith('2024-01-01T00:00:00Z') expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(true) expect(mockResetWorkflowVersionHistory).toHaveBeenCalled() @@ -795,7 +794,11 @@ describe('FeaturesTrigger', () => { message: 'common.api.actionSuccess', }) expect(mockUpdatePublishedWorkflow).not.toHaveBeenCalled() - expect(mockInvalidateAppTriggers).not.toHaveBeenCalled() + expect(mockInvalidateQueries).not.toHaveBeenCalledWith({ + queryKey: consoleQuery.apps.byAppId.triggers.get.queryKey({ + input: { params: { app_id: 'app-id' } }, + }), + }) expect(mockSetPublishedAt).not.toHaveBeenCalled() expect(mockSetLastPublishedHasUserInput).not.toHaveBeenCalled() expect(mockResetWorkflowVersionHistory).not.toHaveBeenCalled() diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 5e76864013953d..6912127aaecb20 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -35,7 +35,6 @@ import { toast } from '@/app/notifications' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useTheme from '@/hooks/use-theme' import { consoleQuery } from '@/service/console' -import { useInvalidateAppTriggers } from '@/service/use-tools' import { useInvalidateAppWorkflow, usePublishWorkflow, @@ -153,7 +152,6 @@ const FeaturesTrigger = () => { }, [nodes]) const resetWorkflowVersionHistory = useResetWorkflowVersionHistory() - const invalidateAppTriggers = useInvalidateAppTriggers() const handleShowFeatures = useCallback(() => { const { showFeaturesPanel, isRestoring, setShowFeaturesPanel } = workflowStore.getState() @@ -199,7 +197,11 @@ const FeaturesTrigger = () => { input: { params: { app_id: appID } }, }), }) - invalidateAppTriggers(appID) + void queryClient.invalidateQueries({ + queryKey: consoleQuery.apps.byAppId.triggers.get.queryKey({ + input: { params: { app_id: appID } }, + }), + }) if (rosterAgentIds.length > 0) { void queryClient.invalidateQueries({ queryKey: consoleQuery.agent.get.key(), @@ -234,7 +236,6 @@ const FeaturesTrigger = () => { appID, t, updatePublishedWorkflow, - invalidateAppTriggers, rosterAgentIds, queryClient, workflowStore, diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index c180e791381de7..8b4fea57306bfd 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -2,7 +2,7 @@ import type { Features as FeaturesData } from '@/app/components/base/features/types' import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useEffect, useMemo } from 'react' import { isWorkflowAppMode } from '@/app/components/apps/app-type' @@ -21,7 +21,6 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { useParams, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' import { fetchRunDetail } from '@/service/log' -import { useAppTriggers } from '@/service/use-tools' import { getAppACLCapabilities } from '@/utils/permission' import WorkflowAppMain from './components/workflow-main' import { useGetRunAndTraceUrl } from './hooks/use-get-run-and-trace-url' @@ -56,10 +55,14 @@ const WorkflowAppWithAdditionalContext = () => { [appDetail.maintainer, appDetail.permission_keys, currentUserId, workspacePermissionKeys], ) const isWorkflowMode = appDetail.mode === 'workflow' - const { data: triggersResponse } = useAppTriggers(isWorkflowMode ? appId : undefined, { - staleTime: 5 * 60 * 1000, // 5 minutes cache - refetchOnWindowFocus: false, - }) + const { data: triggersResponse } = useQuery( + consoleQuery.apps.byAppId.triggers.get.queryOptions({ + input: { params: { app_id: appId } }, + enabled: isWorkflowMode && !!appId, + staleTime: 5 * 60 * 1000, // 5 minutes cache + refetchOnWindowFocus: false, + }), + ) // Sync trigger statuses to store when data loads useEffect(() => { diff --git a/web/app/components/workflow/__tests__/service-mock-factory.ts b/web/app/components/workflow/__tests__/service-mock-factory.ts index 2705a6c5daae36..820c3275bb7472 100644 --- a/web/app/components/workflow/__tests__/service-mock-factory.ts +++ b/web/app/components/workflow/__tests__/service-mock-factory.ts @@ -3,8 +3,11 @@ * * Usage: * ```ts - * vi.mock('@/service/use-tools', async () => - * (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), + * vi.mock('@tanstack/react-query', async (importOriginal) => + * (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + * await importOriginal(), + * (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + * ), * ) * ``` */ @@ -24,12 +27,12 @@ type TriggerMockData = { triggerPlugins?: unknown[] } -export function createToolServiceMock(data?: ToolMockData) { +export function createToolOperationRoutes(data?: ToolMockData) { return { - useAllBuiltInTools: vi.fn(() => ({ data: data?.buildInTools ?? [] })), - useAllCustomTools: vi.fn(() => ({ data: data?.customTools ?? [] })), - useAllWorkflowTools: vi.fn(() => ({ data: data?.workflowTools ?? [] })), - useAllMCPTools: vi.fn(() => ({ data: data?.mcpTools ?? [] })), + 'workspaces.current.tools.builtin.get': () => ({ data: data?.buildInTools ?? [] }), + 'workspaces.current.tools.api.get': () => ({ data: data?.customTools ?? [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: data?.workflowTools ?? [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: data?.mcpTools ?? [] }), } } diff --git a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx index 9d34dc5bb7dec5..3dbc7bf2c0686b 100644 --- a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx @@ -181,12 +181,15 @@ vi.mock('@/context/event-emitter', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + }) +}) vi.mock('@/service/workflow', () => ({ fetchAllInspectVars: vi.fn().mockResolvedValue([]), diff --git a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx index a9a019821a3c77..46ce9e211ec805 100644 --- a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx @@ -37,13 +37,15 @@ vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) const createBlock = (type: BlockEnum, title: string): NodeDefault => ({ metaData: { diff --git a/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx index 2ac5ef970d3ce1..76c1c074890c61 100644 --- a/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx @@ -1,6 +1,7 @@ import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' +import { consoleQuery } from '@/service/console' import { renderWithConsoleQuery } from '@/test/console/query-data' import { BlockSelectorPanels } from '../tabs' import { TabType } from '../types' @@ -8,8 +9,7 @@ import { TabType } from '../types' const render = (ui: React.ReactElement) => renderWithConsoleQuery(ui, { systemFeatures: { enable_marketplace: true } }) -const { mockInvalidateBuiltInTools, mockToolsState } = vi.hoisted(() => ({ - mockInvalidateBuiltInTools: vi.fn(), +const { mockToolsState } = vi.hoisted(() => ({ mockToolsState: { buildInTools: [{ icon: '/tool.svg', name: 'tool' }] as | Array<{ icon: string | Record; name: string }> @@ -27,13 +27,15 @@ vi.mock('@/service/use-plugins', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockToolsState.buildInTools }), - useAllCustomTools: () => ({ data: mockToolsState.customTools }), - useAllWorkflowTools: () => ({ data: mockToolsState.workflowTools }), - useAllMCPTools: () => ({ data: mockToolsState.mcpTools }), - useInvalidateAllBuiltInTools: () => mockInvalidateBuiltInTools, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockToolsState.buildInTools }), + 'workspaces.current.tools.api.get': () => ({ data: mockToolsState.customTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: mockToolsState.workflowTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: mockToolsState.mcpTools }), + }) +}) vi.mock('@/utils/var', () => ({ basePath: '/console', @@ -219,12 +221,16 @@ describe('Tabs', () => { it('should force start content to render and invalidate built-in tools after featured installs', async () => { const user = userEvent.setup() - render() + const { queryClient } = render( + , + ) + const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() + queryClient.setQueryData(builtInToolsKey, []) await user.click(screen.getByRole('button', { name: 'Install featured tool' })) expect(screen.getByText('tools-content'))!.toBeInTheDocument() - expect(mockInvalidateBuiltInTools).toHaveBeenCalledTimes(1) + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) }) it('should compose start content directly without tab semantics in standalone mode', () => { diff --git a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx index 7425c259a4bc0a..c500e12e24c72e 100644 --- a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx @@ -13,27 +13,20 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types' import { CollectionType } from '@/app/components/tools/types' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' +import { consoleQuery } from '@/service/console' import { createCustomCollection } from '@/service/tools' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, - useInvalidateAllCustomTools, - useInvalidateAllMCPTools, - useInvalidateAllWorkflowTools, -} from '@/service/use-tools' import { renderWithConsoleQuery } from '@/test/console/query-data' import { Theme } from '@/types/app' import ToolPicker, { ToolPickerContent } from '../tool-picker' const mockNotify = vi.fn() -const mockInvalidateBuiltInTools = vi.fn() -const mockInvalidateCustomTools = vi.fn() -const mockInvalidateWorkflowTools = vi.fn() -const mockInvalidateMcpTools = vi.fn() +const toolListKeys = { + builtin: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + custom: consoleQuery.workspaces.current.tools.api.get.queryKey(), + workflow: consoleQuery.workspaces.current.tools.workflow.get.queryKey(), + mcp: consoleQuery.workspaces.current.tools.mcp.get.queryKey(), +} const mockCreateCustomCollection = vi.mocked(createCustomCollection) const mockInstallPackageFromMarketPlace = vi.fn() const mockCheckInstalled = vi.fn() @@ -44,14 +37,13 @@ const mockUseGetLanguage = vi.mocked(useGetLanguage) const mockUseTheme = vi.mocked(useTheme) const mockUseTags = vi.mocked(useTags) const mockUseMarketplacePlugins = vi.mocked(useMarketplacePlugins) -const mockUseAllBuiltInTools = vi.mocked(useAllBuiltInTools) -const mockUseAllCustomTools = vi.mocked(useAllCustomTools) -const mockUseAllWorkflowTools = vi.mocked(useAllWorkflowTools) -const mockUseAllMCPTools = vi.mocked(useAllMCPTools) -const mockUseInvalidateAllBuiltInTools = vi.mocked(useInvalidateAllBuiltInTools) -const mockUseInvalidateAllCustomTools = vi.mocked(useInvalidateAllCustomTools) -const mockUseInvalidateAllWorkflowTools = vi.mocked(useInvalidateAllWorkflowTools) -const mockUseInvalidateAllMCPTools = vi.mocked(useInvalidateAllMCPTools) +const { mockBuiltInToolsQuery, mockCustomToolsQuery, mockWorkflowToolsQuery, mockMCPToolsQuery } = + vi.hoisted(() => ({ + mockBuiltInToolsQuery: vi.fn(), + mockCustomToolsQuery: vi.fn(), + mockWorkflowToolsQuery: vi.fn(), + mockMCPToolsQuery: vi.fn(), + })) const mockUseFeaturedToolsRecommendations = vi.mocked(useFeaturedToolsRecommendations) vi.mock('@/context/i18n', () => ({ @@ -121,16 +113,17 @@ vi.mock('@/service/use-plugins', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: vi.fn(), - useAllCustomTools: vi.fn(), - useAllWorkflowTools: vi.fn(), - useAllMCPTools: vi.fn(), - useInvalidateAllBuiltInTools: vi.fn(), - useInvalidateAllCustomTools: vi.fn(), - useInvalidateAllWorkflowTools: vi.fn(), - useInvalidateAllMCPTools: vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => mockBuiltInToolsQuery(), + 'workspaces.current.tools.api.get': ({ enabled }: { enabled?: boolean }) => + mockCustomToolsQuery(enabled), + 'workspaces.current.tools.workflow.get': ({ enabled }: { enabled?: boolean }) => + mockWorkflowToolsQuery(enabled), + 'workspaces.current.tools.mcp.get': () => mockMCPToolsQuery(), + }) +}) vi.mock('@/app/notifications', () => ({ toast: { @@ -363,31 +356,19 @@ describe('ToolPicker', () => { mockUseMarketplacePlugins.mockReturnValue({ data: undefined, } as ReturnType) - mockUseAllBuiltInTools.mockReturnValue({ data: builtInTools } as ReturnType< - typeof useAllBuiltInTools - >) - mockUseAllCustomTools.mockImplementation( - (enabled = true) => - ({ - data: enabled ? customTools : [], - }) as ReturnType, - ) - mockUseAllWorkflowTools.mockImplementation( - (enabled = true) => - ({ - data: enabled ? workflowTools : [], - }) as ReturnType, - ) - mockUseAllMCPTools.mockReturnValue({ data: mcpTools } as ReturnType) - mockUseInvalidateAllBuiltInTools.mockReturnValue(mockInvalidateBuiltInTools) - mockUseInvalidateAllCustomTools.mockReturnValue(mockInvalidateCustomTools) - mockUseInvalidateAllWorkflowTools.mockReturnValue(mockInvalidateWorkflowTools) - mockUseInvalidateAllMCPTools.mockReturnValue(mockInvalidateMcpTools) + mockBuiltInToolsQuery.mockReturnValue({ data: builtInTools }) + mockCustomToolsQuery.mockImplementation((enabled = true) => ({ + data: enabled ? customTools : [], + })) + mockWorkflowToolsQuery.mockImplementation((enabled = true) => ({ + data: enabled ? workflowTools : [], + })) + mockMCPToolsQuery.mockReturnValue({ data: mcpTools }) mockUseFeaturedToolsRecommendations.mockReturnValue({ plugins: [], isLoading: false, } as ReturnType) - mockCreateCustomCollection.mockResolvedValue(undefined) + mockCreateCustomCollection.mockResolvedValue({ result: 'success' }) mockInstallPackageFromMarketPlace.mockResolvedValue({ all_installed: true, task_id: 'task-1', @@ -587,9 +568,9 @@ describe('ToolPicker', () => { plugin_id: 'available-plugin', label: { en_US: 'Available Plugin' }, }) - mockUseAllBuiltInTools.mockReturnValue({ + mockBuiltInToolsQuery.mockReturnValue({ data: [createToolProvider({ ...builtInTools[0], plugin_id: installedPlugin.plugin_id })], - } as ReturnType) + }) mockUseMarketplacePlugins.mockImplementation( (params) => ({ @@ -670,12 +651,12 @@ describe('ToolPicker', () => { expect(await screen.findByText('New Plugin')).toBeInTheDocument() expect(screen.getByText('Available Plugin')).toBeInTheDocument() - mockUseAllBuiltInTools.mockReturnValue({ + mockBuiltInToolsQuery.mockReturnValue({ data: [ ...builtInTools, createToolProvider({ id: 'new-provider', plugin_id: newlyInstalledPlugin.plugin_id }), ], - } as ReturnType) + }) rerender() expect(screen.queryByText('New Plugin')).not.toBeInTheDocument() @@ -686,10 +667,11 @@ describe('ToolPicker', () => { it('should create a custom collection from the add button and refresh custom tools', async () => { const user = userEvent.setup() - renderToolPicker({ + const { queryClient } = renderToolPicker({ isShow: true, supportAddCustomTool: true, }) + for (const key of Object.values(toolListKeys)) queryClient.setQueryData(key, []) await user.click(screen.getByRole('button', { name: 'tools.addToolModal.custom.tip' })) expect(screen.getByTestId('edit-custom-tool-modal')).toBeInTheDocument() @@ -699,7 +681,8 @@ describe('ToolPicker', () => { await waitFor(() => { expect(mockCreateCustomCollection).toHaveBeenCalledWith({ name: 'collection-a' }) }) - expect(mockInvalidateCustomTools).toHaveBeenCalledTimes(1) + expect(queryClient.getQueryState(toolListKeys.custom)?.isInvalidated).toBe(true) + expect(queryClient.getQueryState(toolListKeys.builtin)?.isInvalidated).toBe(false) expect(screen.queryByTestId('edit-custom-tool-modal')).not.toBeInTheDocument() }) @@ -711,8 +694,8 @@ describe('ToolPicker', () => { supportAddCustomTool: true, }) - expect(mockUseAllCustomTools).toHaveBeenCalledWith(true) - expect(mockUseAllWorkflowTools).toHaveBeenCalledWith(true) + expect(mockCustomToolsQuery).toHaveBeenCalledWith(true) + expect(mockWorkflowToolsQuery).toHaveBeenCalledWith(true) expect(screen.getByText('Custom Provider')).toBeInTheDocument() expect(screen.getByText('Built-in Provider')).toBeInTheDocument() expect(screen.getByText('MCP Provider')).toBeInTheDocument() @@ -731,10 +714,11 @@ describe('ToolPicker', () => { isLoading: false, } as ReturnType) - renderToolPicker({ + const { queryClient } = renderToolPicker({ isShow: true, selectedTools: [], }) + for (const key of Object.values(toolListKeys)) queryClient.setQueryData(key, []) const featuredPluginItem = await screen.findByText('Plugin One') await user.hover(featuredPluginItem) @@ -744,10 +728,8 @@ describe('ToolPicker', () => { await waitFor( () => { - expect(mockInvalidateBuiltInTools).toHaveBeenCalledTimes(1) - expect(mockInvalidateCustomTools).toHaveBeenCalledTimes(1) - expect(mockInvalidateWorkflowTools).toHaveBeenCalledTimes(1) - expect(mockInvalidateMcpTools).toHaveBeenCalledTimes(1) + for (const key of Object.values(toolListKeys)) + expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true) }, { timeout: 3000 }, ) diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx index a775e06c8f3b3f..f1f309435d4e1a 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx @@ -3,13 +3,15 @@ import type { ViewType } from '@/app/components/workflow/block-selector/types' import type { OnSelectBlock } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' +import { selectRAGRecommendedPlugins } from '@/app/components/tools/utils/query-select' import { useRAGRecommendationsCollapsed } from '@/app/components/workflow/block-selector/storage' import Link from '@/next/link' -import { useRAGRecommendedPlugins } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { getMarketplaceUrl } from '@/utils/var' import List from './list' @@ -31,7 +33,12 @@ export function RAGToolRecommendations({ data: ragRecommendedPlugins, isLoading: isLoadingRAGRecommendedPlugins, isFetching: isFetchingRAGRecommendedPlugins, - } = useRAGRecommendedPlugins('tool') + } = useQuery( + consoleQuery.rag.pipelines.recommendedPlugins.get.queryOptions({ + input: { query: { type: 'tool' } }, + select: selectRAGRecommendedPlugins, + }), + ) const recommendedPlugins = useMemo(() => { if (ragRecommendedPlugins) return ragRecommendedPlugins.installed_recommended_plugins diff --git a/web/app/components/workflow/block-selector/tool-panel.tsx b/web/app/components/workflow/block-selector/tool-panel.tsx index a08ae8cd2c624f..eccce8510d9855 100644 --- a/web/app/components/workflow/block-selector/tool-panel.tsx +++ b/web/app/components/workflow/block-selector/tool-panel.tsx @@ -1,17 +1,12 @@ import type { RagPipelineDatasourceProviderResponse } from '@dify/contracts/api/console/rag/types.gen' import type { OnSelectBlock, ToolWithProvider } from '../types' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import dynamic from 'next/dynamic' import { useEffect, useMemo } from 'react' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQuery } from '@/service/console' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, -} from '@/service/use-tools' import { basePath } from '@/utils/var' import { useWorkflowStore } from '../store' @@ -85,11 +80,23 @@ export function ToolPanel({ onSelect: OnSelectBlock dataSources: RagPipelineDatasourceProviderResponse[] }) { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() - const invalidateBuiltInTools = useInvalidateAllBuiltInTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) + const queryClient = useQueryClient() const { data: enableMarketplace } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: (systemFeatures) => systemFeatures.enable_marketplace, @@ -150,7 +157,11 @@ export function ToolPanel({ featuredPlugins={featuredPlugins} featuredLoading={isFeaturedLoading} showFeatured={enableMarketplace && !inRAGPipeline} - onFeaturedInstallSuccess={invalidateBuiltInTools} + onFeaturedInstallSuccess={() => + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }) + } /> ) } diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index ad888b267fc72d..b7c14e230bdafe 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -7,7 +7,7 @@ import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import dynamic from 'next/dynamic' import { useMemo, useState } from 'react' @@ -15,19 +15,11 @@ import { useTranslation } from 'react-i18next' import { PluginSearchInput } from '@/app/components/plugins/plugin-search-input' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQuery } from '@/service/console' import { createCustomCollection } from '@/service/tools' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, - useInvalidateAllCustomTools, - useInvalidateAllMCPTools, - useInvalidateAllWorkflowTools, -} from '@/service/use-tools' const ToolBrowser = dynamic(() => import('./tool-browser'), { loading: () =>
, @@ -73,20 +65,33 @@ export function ToolPickerContent({ ...systemFeaturesQueryOptions(), select: (s) => s.enable_marketplace, }) - const { data: buildInTools } = useAllBuiltInTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const installedPluginIds = useMemo( () => new Set(buildInTools?.map((provider) => provider.plugin_id || provider.id)), [buildInTools], ) const shouldFetchCustomTools = scope !== 'plugins' && scope !== 'workflow' - const { data: customTools } = useAllCustomTools(shouldFetchCustomTools) - const invalidateCustomTools = useInvalidateAllCustomTools() + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: shouldFetchCustomTools, + select: selectToolsWithProvider, + }), + ) + const queryClient = useQueryClient() const shouldFetchWorkflowTools = scope !== 'plugins' && scope !== 'custom' - const { data: workflowTools } = useAllWorkflowTools(shouldFetchWorkflowTools) - const { data: mcpTools } = useAllMCPTools() - const invalidateBuiltInTools = useInvalidateAllBuiltInTools() - const invalidateWorkflowTools = useInvalidateAllWorkflowTools() - const invalidateMcpTools = useInvalidateAllMCPTools() + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: shouldFetchWorkflowTools, + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const { plugins: featuredPlugins = [], isLoading: isFeaturedLoading } = useFeaturedToolsRecommendations(enable_marketplace) @@ -138,7 +143,9 @@ export function ToolPickerContent({ await createCustomCollection(data) hideEditCustomCollectionModal() - invalidateCustomTools() + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.api.get.queryKey(), + }) } if (isShowEditCollectionToolModal && canManageTools) { @@ -202,10 +209,11 @@ export function ToolPickerContent({ featuredLoading={isFeaturedLoading} showFeatured={scope === 'all' && enable_marketplace} onFeaturedInstallSuccess={async () => { - invalidateBuiltInTools() - invalidateCustomTools() - invalidateWorkflowTools() - invalidateMcpTools() + const tools = consoleQuery.workspaces.current.tools + queryClient.invalidateQueries({ queryKey: tools.builtin.get.queryKey() }) + queryClient.invalidateQueries({ queryKey: tools.api.get.queryKey() }) + queryClient.invalidateQueries({ queryKey: tools.workflow.get.queryKey() }) + queryClient.invalidateQueries({ queryKey: tools.mcp.get.queryKey() }) }} />
diff --git a/web/app/components/workflow/datasets-detail-store/__tests__/provider.spec.tsx b/web/app/components/workflow/datasets-detail-store/__tests__/provider.spec.tsx index 09ae01f95b71e0..a12b108ff96a3a 100644 --- a/web/app/components/workflow/datasets-detail-store/__tests__/provider.spec.tsx +++ b/web/app/components/workflow/datasets-detail-store/__tests__/provider.spec.tsx @@ -80,7 +80,6 @@ describe('datasets-detail-store provider', () => { await waitFor(() => { expect(mockFetchDatasets).toHaveBeenCalledWith({ - url: '/datasets', params: { page: 1, ids: ['dataset-1', 'dataset-2'], diff --git a/web/app/components/workflow/datasets-detail-store/__tests__/store.spec.tsx b/web/app/components/workflow/datasets-detail-store/__tests__/store.spec.tsx index 0b8c1e90692c9d..970468c27f4065 100644 --- a/web/app/components/workflow/datasets-detail-store/__tests__/store.spec.tsx +++ b/web/app/components/workflow/datasets-detail-store/__tests__/store.spec.tsx @@ -1,16 +1,17 @@ import type { DataSet } from '@/models/datasets' import { renderHook } from '@testing-library/react' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { DatasetsDetailContext } from '../provider' import { createDatasetsDetailStore, useDatasetsDetailStore } from '../store' const createDataset = (id: string, name = `dataset-${id}`): DataSet => ({ + ...createDatasetFixture(), id, name, - indexing_status: 'completed', icon_info: { icon: 'book', - icon_type: 'emoji' as DataSet['icon_info']['icon_type'], + icon_type: 'emoji', }, description: `${name} description`, permission: DatasetPermission.onlyMe, @@ -22,14 +23,12 @@ const createDataset = (id: string, name = `dataset-${id}`): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 0, - total_document_count: 0, word_count: 0, provider: 'provider', embedding_model: 'model', embedding_model_provider: 'provider', embedding_available: true, retrieval_model_dict: {} as DataSet['retrieval_model_dict'], - retrieval_model: {} as DataSet['retrieval_model'], tags: [], external_knowledge_info: { external_knowledge_id: '', diff --git a/web/app/components/workflow/datasets-detail-store/provider.tsx b/web/app/components/workflow/datasets-detail-store/provider.tsx index 19365e810ea5db..0d72f868942b21 100644 --- a/web/app/components/workflow/datasets-detail-store/provider.tsx +++ b/web/app/components/workflow/datasets-detail-store/provider.tsx @@ -23,7 +23,6 @@ const DatasetsDetailProvider: FC = ({ nodes, childr const updateDatasetsDetail = useCallback( async (datasetIds: string[]) => { const { data: datasetsDetail } = await fetchDatasets({ - url: '/datasets', params: { page: 1, ids: datasetIds }, }) if (datasetsDetail && datasetsDetail.length > 0) diff --git a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts index df354ccf3f0e69..09ea5c77ff6379 100644 --- a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts @@ -5,8 +5,11 @@ import { BlockEnum } from '../../types' import { useAvailableBlocks } from '../use-available-blocks' // Transitive imports of use-nodes-meta-data.ts — only useNodeMetaData uses these -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + ), ) vi.mock('@/context/i18n', () => ({ useGetLanguage: () => 'en' })) diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts index be597ff9f662a5..d2d5cb34224648 100644 --- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts @@ -58,13 +58,6 @@ vi.mock('reactflow', async () => { } }) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: toolServiceState.buildInTools }), - useAllCustomTools: () => ({ data: toolServiceState.customTools }), - useAllMCPTools: () => ({ data: toolServiceState.mcpTools }), - useAllWorkflowTools: () => ({ data: toolServiceState.workflowTools }), -})) - vi.mock('@/service/use-plugins', () => ({ useFetchPluginsInMarketPlaceByInfo: (infos: Array<{ organization: string; plugin: string }>) => ({ data: @@ -848,7 +841,8 @@ describe('useWorkflowRunValidation', () => { vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal() - return { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + const reactQuery = { ...actual, useQuery: (options: { queryKey: OperationKey< @@ -866,5 +860,11 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options) return { data: [] } }, - } + } as typeof actual + return mockConsoleOperationHooks(reactQuery, { + 'workspaces.current.tools.builtin.get': () => ({ data: toolServiceState.buildInTools }), + 'workspaces.current.tools.api.get': () => ({ data: toolServiceState.customTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: toolServiceState.mcpTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: toolServiceState.workflowTools }), + }) }) diff --git a/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts b/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts index 624a6cecb8c1a2..aebe5217275c78 100644 --- a/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts @@ -16,6 +16,7 @@ const mockToNodeOutputVars = vi.hoisted(() => vi.fn()) const schemaTypeDefinitions: SchemaTypeDefinition[] = [ { name: 'simple', + label: 'Simple', schema: { properties: {}, }, @@ -26,8 +27,11 @@ vi.mock('reactflow', async () => (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(), ) -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + ), ) vi.mock('@/service/use-workflow', () => ({ diff --git a/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts b/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts index 64f5ce1af0d795..2a3461aa72b12c 100644 --- a/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts @@ -21,6 +21,7 @@ const mockToNodeOutputVars = vi.hoisted(() => vi.fn()) const schemaTypeDefinitions: SchemaTypeDefinition[] = [ { name: 'simple', + label: 'Simple', schema: { properties: {}, }, @@ -41,8 +42,11 @@ vi.mock('@/service/use-workflow', () => ({ useEditInspectorVar: () => ({ mutateAsync: vi.fn() }), })) -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + ), ) vi.mock('@/service/workflow', () => ({ diff --git a/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts b/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts index 552159f2d64d89..134e2a4cf0bf3c 100644 --- a/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts @@ -1,11 +1,11 @@ import type { CommonNodeType } from '../../types' -import type { CollectionProviderType } from '@/app/components/tools/types' import { QueryClient } from '@tanstack/react-query' import { act } from '@testing-library/react' import { createDatasourceProvider } from '@/app/components/rag-pipeline/__tests__/datasource-fixtures' import { CollectionType } from '@/app/components/tools/types' import { consoleQuery } from '@/service/console' import { seedAccountProfileQuery } from '@/test/console/account-profile' +import { createTestQueryClient } from '@/test/query-client' import { renderWorkflowHook } from '../../__tests__/workflow-test-env' import { BlockEnum } from '../../types' import { useNodePluginInstallation } from '../use-node-plugin-installation' @@ -14,7 +14,6 @@ const mockBuiltInTools = vi.fn() const mockCustomTools = vi.fn() const mockWorkflowTools = vi.fn() const mockMcpTools = vi.fn() -const mockInvalidToolsByType = vi.fn() const mockTriggerPlugins = vi.fn() const mockInvalidateTriggers = vi.fn() let mockWorkspacePermissionKeys = ['plugin.install'] @@ -26,14 +25,19 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: (enabled: boolean) => mockBuiltInTools(enabled), - useAllCustomTools: (enabled: boolean) => mockCustomTools(enabled), - useAllWorkflowTools: (enabled: boolean) => mockWorkflowTools(enabled), - useAllMCPTools: (enabled: boolean) => mockMcpTools(enabled), - useInvalidToolsByType: (providerType?: CollectionProviderType) => - mockInvalidToolsByType(providerType), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': ({ enabled }: { enabled?: boolean }) => + mockBuiltInTools(enabled), + 'workspaces.current.tools.api.get': ({ enabled }: { enabled?: boolean }) => + mockCustomTools(enabled), + 'workspaces.current.tools.workflow.get': ({ enabled }: { enabled?: boolean }) => + mockWorkflowTools(enabled), + 'workspaces.current.tools.mcp.get': ({ enabled }: { enabled?: boolean }) => + mockMcpTools(enabled), + }) +}) vi.mock('@/service/use-triggers', () => ({ useAllTriggerPlugins: (enabled: boolean) => mockTriggerPlugins(enabled), @@ -103,7 +107,6 @@ describe('useNodePluginInstallation', () => { mockCustomTools.mockReturnValue({ data: undefined, isLoading: false }) mockWorkflowTools.mockReturnValue({ data: undefined, isLoading: false }) mockMcpTools.mockReturnValue({ data: undefined, isLoading: false }) - mockInvalidToolsByType.mockReturnValue(undefined) mockTriggerPlugins.mockReturnValue({ data: undefined, isLoading: false }) mockInvalidateTriggers.mockReset() }) @@ -128,11 +131,17 @@ describe('useNodePluginInstallation', () => { }) it('should report loading and invalidate built-in tools while the collection is resolving', () => { - const invalidateTools = vi.fn() mockBuiltInTools.mockReturnValue({ data: undefined, isLoading: true }) - mockInvalidToolsByType.mockReturnValue(invalidateTools) + const queryClient = createTestQueryClient() + seedAccountProfileQuery(queryClient) + const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() + const customToolsKey = consoleQuery.workspaces.current.tools.api.get.queryKey() + queryClient.setQueryData(builtInToolsKey, []) + queryClient.setQueryData(customToolsKey, []) - const { result } = renderWorkflowHook(() => useNodePluginInstallation(makeToolNode())) + const { result } = renderWorkflowHook(() => useNodePluginInstallation(makeToolNode()), { + queryClient, + }) expect(mockBuiltInTools).toHaveBeenCalledWith(true) expect(result.current.isChecking).toBe(true) @@ -145,7 +154,8 @@ describe('useNodePluginInstallation', () => { result.current.onInstallSuccess() }) - expect(invalidateTools).toHaveBeenCalled() + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) + expect(queryClient.getQueryState(customToolsKey)?.isInvalidated).toBe(false) }) it.each([ diff --git a/web/app/components/workflow/hooks/__tests__/use-nodes-meta-data.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-nodes-meta-data.spec.tsx index 8850b018f94d2d..d50303803ddd3d 100644 --- a/web/app/components/workflow/hooks/__tests__/use-nodes-meta-data.spec.tsx +++ b/web/app/components/workflow/hooks/__tests__/use-nodes-meta-data.spec.tsx @@ -19,11 +19,14 @@ const workflowToolsState = vi.hoisted( () => [] as Array<{ id: string; author: string; description: Record }>, ) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: buildInToolsState }), - useAllCustomTools: () => ({ data: customToolsState }), - useAllWorkflowTools: () => ({ data: workflowToolsState }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: buildInToolsState }), + 'workspaces.current.tools.api.get': () => ({ data: customToolsState }), + 'workspaces.current.tools.workflow.get': () => ({ data: workflowToolsState }), + }) +}) const createNode = (overrides: Partial = {}): Node => ({ diff --git a/web/app/components/workflow/hooks/__tests__/use-tool-icon.spec.ts b/web/app/components/workflow/hooks/__tests__/use-tool-icon.spec.ts index 02c6250c625f99..8f930fc1219b22 100644 --- a/web/app/components/workflow/hooks/__tests__/use-tool-icon.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-tool-icon.spec.ts @@ -9,23 +9,26 @@ vi.mock('reactflow', async () => (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(), ) -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock({ - buildInTools: [ - { - id: 'builtin-1', - name: 'builtin', - icon: '/builtin.svg', - icon_dark: '/builtin-dark.svg', - plugin_id: 'p1', - }, - ], - customTools: [{ id: 'custom-1', name: 'custom', icon: '/custom.svg', plugin_id: 'p2' }], - workflowTools: [ - { id: 'workflow-1', name: 'workflow-tool', icon: '/workflow.svg', plugin_id: 'p3' }, - ], - mcpTools: [{ id: 'mcp-1', name: 'mcp-tool', icon: '/mcp.svg', plugin_id: 'p4' }], - }), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes({ + buildInTools: [ + { + id: 'builtin-1', + name: 'builtin', + icon: '/builtin.svg', + icon_dark: '/builtin-dark.svg', + plugin_id: 'p1', + }, + ], + customTools: [{ id: 'custom-1', name: 'custom', icon: '/custom.svg', plugin_id: 'p2' }], + workflowTools: [ + { id: 'workflow-1', name: 'workflow-tool', icon: '/workflow.svg', plugin_id: 'p3' }, + ], + mcpTools: [{ id: 'mcp-1', name: 'mcp-tool', icon: '/mcp.svg', plugin_id: 'p4' }], + }), + ), ) vi.mock('@/service/use-triggers', async () => diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx index e0a9a9d5aab696..8d06f72ed57706 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx @@ -18,20 +18,23 @@ vi.mock('../use-nodes-interactions', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ - data: [ - { - id: 'provider-1', - icon: 'tool-icon', - tools: [], - }, - ] satisfies Partial[], - }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ + data: [ + { + id: 'provider-1', + icon: 'tool-icon', + tools: [], + }, + ] satisfies Partial[], + }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) const createNode = (overrides: Partial = {}): Node => ({ id: 'node-1', diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts index 55c4b83a3e01a3..4cbee00cb90951 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts @@ -6,8 +6,11 @@ vi.mock('reactflow', async () => (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(), ) -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + ), ) const { mockToNodeAvailableVars, mockGetVarType } = vi.hoisted(() => ({ diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 2fd5985a1f7522..31a097ec45b932 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next' import { useEdges, useStoreApi } from 'reactflow' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { normalizeModelProviderModelsResponse } from '@/app/components/header/account-setting/model-provider-page/utils' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { editorResourceAtom, editorResourceKindAtom, @@ -43,12 +44,6 @@ import { } from '@/features/agent-v2/agent-detail/configure/tool-provider-catalog' import { consoleQuery } from '@/service/console' import { fetchDatasets } from '@/service/datasets' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { CUSTOM_NODE } from '../constants' import { useDatasetsDetailStore } from '../datasets-detail-store/store' @@ -169,10 +164,22 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { const { t } = useTranslation(['agentV2', 'common', 'workflow', 'modelProvider']) const language = useGetLanguage() const { nodesMap: nodesExtraData } = useNodesMetaData() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const inlineAgentToolProviderCatalog = useMemo( () => createAgentToolProviderCatalog({ buildInTools, customTools, mcpTools, workflowTools }), [buildInTools, customTools, mcpTools, workflowTools], @@ -648,9 +655,19 @@ export const useChecklistBeforePublish = () => { select: (response) => response.data, }), ) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const resourceKind = useAtomValue(editorResourceKindAtom) const shouldCheckStartNode = resourceKind === 'app' @@ -748,7 +765,6 @@ export const useChecklistBeforePublish = () => { updateTimeRef.current = updateTimeRef.current + 1 const currUpdateTime = updateTimeRef.current const { data: datasetsDetail } = await fetchDatasets({ - url: '/datasets', params: { page: 1, ids: [...allDatasetIds] }, }) if (currUpdateTime < updateTimeRef.current) return null diff --git a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts index e4185009f26ae7..c56fe014d8fd27 100644 --- a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts +++ b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts @@ -1,17 +1,14 @@ import type { Node, WorkflowPluginCatalogs } from '@/app/components/workflow/types' import type { SchemaTypeDefinition } from '@/service/use-common' import type { NodeWithVar, VarInInspect } from '@/types/workflow' +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useMemo } from 'react' import { useStoreApi } from 'reactflow' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { editorResourceAtom } from '@/app/components/workflow/editor-resource/state' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useInvalidateConversationVarValues, useInvalidateSysVarValues, @@ -29,10 +26,22 @@ export const useSetWorkflowVarsWithValue = () => { const invalidateSysVarValues = useInvalidateSysVarValues(resource) const { handleCancelAllNodeSuccessStatus } = useNodesInteractionsWithoutSync() const { schemaTypeDefinitions } = useMatchSchemaType() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const allPluginInfoList = useMemo(() => { diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts index 06f2dae4ebbd9d..1fbbc4a455bd5a 100644 --- a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts @@ -1,10 +1,12 @@ import type { Node, ValueSelector } from '@/app/components/workflow/types' import type { SchemaTypeDefinition } from '@/service/use-common' import type { VarInInspect } from '@/types/workflow' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useAtomValue } from 'jotai' import { useCallback } from 'react' import { useStoreApi } from 'reactflow' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { editorResourceAtom } from '@/app/components/workflow/editor-resource/state' import { isConversationVar, @@ -13,12 +15,7 @@ import { toNodeOutputVars, } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { useWorkflowStore } from '@/app/components/workflow/store' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useDeleteAllInspectorVars, useDeleteInspectVar, @@ -48,10 +45,22 @@ export const useInspectVarsCrudCommon = () => { const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(resource) const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync() const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const getNodeInspectVars = useCallback( (nodeId: string) => { diff --git a/web/app/components/workflow/hooks/use-node-plugin-installation.ts b/web/app/components/workflow/hooks/use-node-plugin-installation.ts index 5c228a791c9a5a..c754f27f56c9aa 100644 --- a/web/app/components/workflow/hooks/use-node-plugin-installation.ts +++ b/web/app/components/workflow/hooks/use-node-plugin-installation.ts @@ -2,18 +2,13 @@ import type { DataSourceNodeType } from '../nodes/data-source/types' import type { ToolNodeType } from '../nodes/tool/types' import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types' import type { CommonNodeType } from '../types' -import { useQueryClient } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import useWorkspacePluginInstallPermission from '@/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { getToolListQueryKey } from '@/app/components/tools/utils/tool-list-query-key' import { consoleQuery } from '@/service/console' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidToolsByType, -} from '@/service/use-tools' import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers' import { useStore } from '../store' import { BlockEnum } from '../types' @@ -51,11 +46,32 @@ const useToolInstallation = ( const isWorkflow = enabled && data.provider_type === CollectionType.workflow const isMcp = enabled && data.provider_type === CollectionType.mcp - const builtInQuery = useAllBuiltInTools(isBuiltIn) - const customQuery = useAllCustomTools(isCustom) - const workflowQuery = useAllWorkflowTools(isWorkflow) - const mcpQuery = useAllMCPTools(isMcp) - const invalidateTools = useInvalidToolsByType(enabled ? data.provider_type : undefined) + const builtInQuery = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + enabled: isBuiltIn, + select: selectToolsWithProvider, + }), + ) + const customQuery = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: isCustom, + select: selectToolsWithProvider, + }), + ) + const workflowQuery = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: isWorkflow, + select: selectToolsWithProvider, + }), + ) + const mcpQuery = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: isMcp, + select: selectToolsWithProvider, + }), + ) + const queryClient = useQueryClient() + const toolListQueryKey = enabled ? getToolListQueryKey(data.provider_type) : undefined const collectionInfo = useMemo(() => { if (!enabled) return undefined @@ -112,8 +128,8 @@ const useToolInstallation = ( const canInstall = Boolean(data.plugin_unique_identifier) && canInstallPlugin const onInstallSuccess = useCallback(() => { - if (invalidateTools) invalidateTools() - }, [invalidateTools]) + if (toolListQueryKey) queryClient.invalidateQueries({ queryKey: toolListQueryKey }) + }, [queryClient, toolListQueryKey]) const shouldDim = (!!collectionInfo && !isResolved) || (isResolved && !matchedCollection) diff --git a/web/app/components/workflow/hooks/use-nodes-meta-data.ts b/web/app/components/workflow/hooks/use-nodes-meta-data.ts index b7d195a878ffb9..054377f3e63d13 100644 --- a/web/app/components/workflow/hooks/use-nodes-meta-data.ts +++ b/web/app/components/workflow/hooks/use-nodes-meta-data.ts @@ -1,14 +1,16 @@ import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store' import type { Node } from '@/app/components/workflow/types' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useHooksStore } from '@/app/components/workflow/hooks-store' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' import { getNodeCatalogType } from '@/app/components/workflow/utils/node' import { useGetLanguage } from '@/context/i18n' import { renderI18nObject } from '@/i18n/metadata' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' import { matchDataSource } from '../utils/plugin-install-check' @@ -25,9 +27,19 @@ export const useNodesMetaData = () => { export const useNodeMetaData = (node: Node) => { const language = useGetLanguage() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const availableNodesMetaData = useNodesMetaData() const { data } = node diff --git a/web/app/components/workflow/hooks/use-tool-icon.ts b/web/app/components/workflow/hooks/use-tool-icon.ts index 1b71c7fd0f69f1..f9a13bf1af4a7d 100644 --- a/web/app/components/workflow/hooks/use-tool-icon.ts +++ b/web/app/components/workflow/hooks/use-tool-icon.ts @@ -4,16 +4,13 @@ import type { DataSourceNodeType } from '../nodes/data-source/types' import type { ToolNodeType } from '../nodes/tool/types' import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types' import type { Node, ToolWithProvider } from '../types' +import { useQuery } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { resolveDatasourceIcon } from '@/app/components/rag-pipeline/utils/datasource-icon' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import useTheme from '@/hooks/use-theme' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useAllTriggerPlugins } from '@/service/use-triggers' import { canFindTool } from '@/utils' import { matchesProviderReference } from '@/utils/provider-reference' @@ -174,10 +171,22 @@ export const findNodeIcon = ({ } export const useToolIcon = (data?: Node['data']) => { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const { data: triggerPlugins } = useAllTriggerPlugins() const { theme } = useTheme() @@ -212,10 +221,22 @@ export const useToolIcon = (data?: Node['data']) => { } export const useGetToolIcon = () => { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const { data: triggerPlugins } = useAllTriggerPlugins() const workflowStore = useWorkflowStore() const { theme } = useTheme() diff --git a/web/app/components/workflow/hooks/use-workflow-search.tsx b/web/app/components/workflow/hooks/use-workflow-search.tsx index fa7e7cb9b5a105..c605b9562d9d80 100644 --- a/web/app/components/workflow/hooks/use-workflow-search.tsx +++ b/web/app/components/workflow/hooks/use-workflow-search.tsx @@ -3,17 +3,14 @@ import type { LLMNodeType } from '../nodes/llm/types' import type { CommonNodeType } from '../types' import type { Emoji } from '@/app/components/tools/types' +import { useQuery } from '@tanstack/react-query' import { useCallback, useEffect, useMemo } from 'react' import { useNodes } from 'reactflow' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import BlockIcon from '@/app/components/workflow/block-icon' import { registerWorkflowNodeSearch } from '@/app/components/workflow/goto-anything-search' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' import { BlockEnum } from '../types' import { setupNodeSelectionListener } from '../utils/node-navigation' @@ -27,10 +24,22 @@ export const useWorkflowSearch = () => { const { handleNodeSelect } = useNodesInteractions() // Filter and process nodes for search - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) // Extract tool icon logic - clean separation of concerns const getToolIcon = useCallback( diff --git a/web/app/components/workflow/hooks/use-workflow-variables.ts b/web/app/components/workflow/hooks/use-workflow-variables.ts index aebc76121d8f5b..91d9be9b46a01e 100644 --- a/web/app/components/workflow/hooks/use-workflow-variables.ts +++ b/web/app/components/workflow/hooks/use-workflow-variables.ts @@ -1,20 +1,17 @@ import type { Type } from '../nodes/llm/types' import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { isChatflowAtom } from '@/app/components/workflow/editor-resource/state' import { getVarType, toNodeAvailableVars, } from '@/app/components/workflow/nodes/_base/components/variable/utils' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type' import { useWorkflowStore } from '../store' @@ -23,10 +20,22 @@ export const useWorkflowVariables = () => { const workflowStore = useWorkflowStore() const { schemaTypeDefinitions } = useMatchSchemaType() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const getNodeAvailableVars = useCallback( ({ diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index b37856e42a62e9..3bdc50dd75805e 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -19,6 +19,7 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' +import { useQuery } from '@tanstack/react-query' import { useEventListener } from 'ahooks' import { isEqual } from 'es-toolkit/predicate' import { setAutoFreeze } from 'immer' @@ -47,17 +48,13 @@ import { useReactFlow, useStoreApi, } from 'reactflow' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { getEditorResourceId } from '@/app/components/workflow/editor-resource/resource' import { editorResourceAtom } from '@/app/components/workflow/editor-resource/state' import { toast } from '@/app/notifications' import { IS_DEV } from '@/config' import { useEventEmitterContextContext } from '@/context/event-emitter' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { fetchAllInspectVars } from '@/service/workflow' import CandidateNode from './candidate-node' import UserCursors from './collaboration/components/user-cursors' @@ -570,10 +567,26 @@ export const Workflow: FC = memo( const { schemaTypeDefinitions } = useMatchSchemaType() const { fetchInspectVars } = useSetWorkflowVarsWithValue() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const dataSourceList = useStore((s) => s.dataSourceList) // buildInTools, customTools, workflowTools, mcpTools, dataSourceList const resource = useAtomValue(editorResourceAtom) diff --git a/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx b/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx index 345f6fd89ed2b7..2bce7a5cdb71d1 100644 --- a/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx +++ b/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx @@ -5,7 +5,6 @@ import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__ import { useHooksStore } from '@/app/components/workflow/hooks-store' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' -import { useAllWorkflowTools } from '@/service/use-tools' import { BlockClassification } from '../../block-selector/types' import { useAvailableBlocks } from '../../hooks/use-available-blocks' import { useNodesInteractions } from '../../hooks/use-nodes-interactions' @@ -58,13 +57,17 @@ vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({ default: vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useAllWorkflowTools: vi.fn(), - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +const { mockUseAllWorkflowTools } = vi.hoisted(() => ({ mockUseAllWorkflowTools: vi.fn() })) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.workflow.get': () => mockUseAllWorkflowTools(), + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useFeaturedToolsRecommendations: () => ({ plugins: [], isLoading: false }), @@ -85,7 +88,6 @@ const mockUseNodesInteractions = vi.mocked(useNodesInteractions) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) const mockUseHooksStore = vi.mocked(useHooksStore) const mockUseNodes = vi.mocked(useNodes) -const mockUseAllWorkflowTools = vi.mocked(useAllWorkflowTools) async function renderDropdownContent({ showHelpLink = true, diff --git a/web/app/components/workflow/node-actions-menu/__tests__/index.spec.tsx b/web/app/components/workflow/node-actions-menu/__tests__/index.spec.tsx index d0a655d6fadebb..92909f0934efba 100644 --- a/web/app/components/workflow/node-actions-menu/__tests__/index.spec.tsx +++ b/web/app/components/workflow/node-actions-menu/__tests__/index.spec.tsx @@ -5,7 +5,6 @@ import { fireEvent, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' import { BlockEnum } from '@/app/components/workflow/types' -import { useAllWorkflowTools } from '@/service/use-tools' import { useNodesInteractions } from '../../hooks/use-nodes-interactions' import { useNodeMetaData } from '../../hooks/use-nodes-meta-data' import { useNodesReadOnly } from '../../hooks/use-workflow' @@ -38,9 +37,14 @@ vi.mock('../../hooks/use-workflow', async (importOriginal) => { } }) -vi.mock('@/service/use-tools', () => ({ - useAllWorkflowTools: vi.fn(), -})) +const { mockUseAllWorkflowTools } = vi.hoisted(() => ({ mockUseAllWorkflowTools: vi.fn() })) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.workflow.get': () => mockUseAllWorkflowTools(), + }) +}) vi.mock('../change-block-popup', () => ({ ChangeBlockPopup: () =>
, @@ -49,7 +53,6 @@ vi.mock('../change-block-popup', () => ({ const mockUseNodeMetaData = vi.mocked(useNodeMetaData) const mockUseNodesInteractions = vi.mocked(useNodesInteractions) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) -const mockUseAllWorkflowTools = vi.mocked(useAllWorkflowTools) const createQueryResult = (data: T): UseQueryResult => ({ diff --git a/web/app/components/workflow/node-actions-menu/use-node-actions-menu-model.ts b/web/app/components/workflow/node-actions-menu/use-node-actions-menu-model.ts index f3652a4b6c376c..4110a996fb519e 100644 --- a/web/app/components/workflow/node-actions-menu/use-node-actions-menu-model.ts +++ b/web/app/components/workflow/node-actions-menu/use-node-actions-menu-model.ts @@ -1,12 +1,14 @@ import type { Node } from '@/app/components/workflow/types' +import { useQuery } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { useEdges } from 'reactflow' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useHooksStore } from '@/app/components/workflow/hooks-store' import { useWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import { canRunBySingle } from '@/app/components/workflow/utils' -import { useAllWorkflowTools } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' import { useNodesInteractions } from '../hooks/use-nodes-interactions' import { useNodeMetaData } from '../hooks/use-nodes-meta-data' @@ -32,7 +34,11 @@ export function useNodeActionsMenuModel({ const { nodesReadOnly } = useNodesReadOnly() const canRunWorkflow = useHooksStore((s) => s.accessControl.canRun) const nodeMetaData = useNodeMetaData({ id, data } as Node) - const { data: workflowTools } = useAllWorkflowTools() + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const isChildNode = !!(data.isInIteration || data.isInLoop) const canRun = canRunWorkflow && !nodesReadOnly && canRunBySingle(data.type, isChildNode) diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx index d4b823c1c523b6..7c5446ee6e3831 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx @@ -4,18 +4,24 @@ import userEvent from '@testing-library/user-event' import { useState } from 'react' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' import { SupportUploadFileTypes } from '@/app/components/workflow/types' -import { useFileUploadConfig } from '@/service/use-common' import { TransferMethod } from '@/types/app' import FileTypeItem from '../file-type-item' import FileUploadSetting from '../file-upload-setting' -const mockUseFileUploadConfig = vi.mocked(useFileUploadConfig) -const mockUseFileSizeLimit = vi.mocked(useFileSizeLimit) - -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: vi.fn(), +const { mockFileUploadConfigHook } = vi.hoisted(() => ({ + mockFileUploadConfigHook: vi.fn(), })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': (options: unknown) => mockFileUploadConfigHook(options), + }) +}) + +const mockUseFileUploadConfig = mockFileUploadConfigHook +const mockUseFileSizeLimit = vi.mocked(useFileSizeLimit) + vi.mock('@/app/components/base/file-uploader/hooks', () => ({ useFileSizeLimit: vi.fn(), })) @@ -40,7 +46,9 @@ const createPayload = (overrides: Partial = {}): UploadFileSe describe('File upload support components', () => { beforeEach(() => { vi.clearAllMocks() - mockUseFileUploadConfig.mockReturnValue({ data: {} } as ReturnType) + mockUseFileUploadConfig.mockReturnValue({ data: {} } as ReturnType< + typeof mockFileUploadConfigHook + >) mockUseFileSizeLimit.mockReturnValue({ imgSizeLimit: 10 * 1024 * 1024, docSizeLimit: 20 * 1024 * 1024, diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx index 776ad6420fc906..82e5fc6f237414 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx @@ -78,15 +78,16 @@ vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useRAGRecommendedPlugins: () => ({ data: [] }), - useInvalidateAllToolProviders: () => vi.fn(), - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.recommendedPlugins.get': () => ({ data: [] }), + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/app/components/plugins/marketplace/query', () => ({ useMarketplacePlugins: () => ({ data: undefined }), diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx index daba060a0136b3..9ede1a5280a2f8 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx @@ -145,13 +145,15 @@ vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/app/components/plugins/marketplace/query', () => ({ useMarketplacePlugins: () => ({ data: undefined }), diff --git a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx index aa02e7413bb8b0..6572d6cbcc9d42 100644 --- a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx +++ b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx @@ -5,13 +5,14 @@ import { cn } from '@langgenius/dify-ui/cn' import { FieldItem, FieldLabel, Field as FormField } from '@langgenius/dify-ui/field' import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset' import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio-group' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import * as React from 'react' import { useCallback, useId } from 'react' import { useTranslation } from 'react-i18next' import Field from '@/app/components/app/configuration/config-var/config-modal/field' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' -import { useFileUploadConfig } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { TransferMethod } from '@/types/app' import { formatFileSize } from '@/utils/format' import { SupportUploadFileTypes } from '../../../types' @@ -50,7 +51,7 @@ const FileUploadSetting: FC = ({ allowed_file_types = [], allowed_file_extensions = [], } = payload - const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: fileUploadConfigResponse } = useQuery(consoleQuery.files.upload.get.queryOptions()) const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit, maxFileUploadLimit } = useFileSizeLimit(fileUploadConfigResponse) diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/use-match-schema-type.spec.tsx b/web/app/components/workflow/nodes/_base/components/variable/__tests__/use-match-schema-type.spec.tsx new file mode 100644 index 00000000000000..f7f14ad1c2f029 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/use-match-schema-type.spec.tsx @@ -0,0 +1,77 @@ +import type { ReactNode } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { renderHook, waitFor } from '@testing-library/react' +import useMatchSchemaType, { getMatchedSchemaType } from '../use-match-schema-type' + +// Built-in definitions as served by /spec/schema-definitions (api/core/schemas/builtin). +const schemaDefinitions = [ + { + name: 'file', + label: 'File', + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + size: { type: 'number' }, + extension: { type: 'string' }, + type: { type: 'string' }, + mime_type: { type: 'string' }, + transfer_method: { type: 'string' }, + url: { type: 'string' }, + related_id: { type: 'string' }, + }, + required: ['name'], + }, + }, + { + name: 'general_structure', + label: 'General Structure', + schema: { + type: 'array', + items: { type: 'string' }, + }, + }, +] + +vi.mock('@/service/base', async (importOriginal) => ({ + ...(await importOriginal()), + request: async () => Response.json(schemaDefinitions), +})) + +const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + +) + +describe('useMatchSchemaType', () => { + it('keeps the full definition schemas so typed structures still match', async () => { + const { result } = renderHook(() => useMatchSchemaType(), { wrapper }) + + await waitFor(() => expect(result.current.schemaTypeDefinitions).toBeDefined()) + const definitions = result.current.schemaTypeDefinitions + + expect( + getMatchedSchemaType( + { + type: 'object', + properties: { + name: { type: 'string' }, + size: { type: 'number' }, + extension: { type: 'string' }, + type: { type: 'string' }, + mime_type: { type: 'string' }, + transfer_method: { type: 'string' }, + url: { type: 'string' }, + related_id: { type: 'string' }, + }, + }, + definitions, + ), + ).toBe('file') + expect(getMatchedSchemaType({ type: 'array', items: { type: 'string' } }, definitions)).toBe( + 'general_structure', + ) + expect(getMatchedSchemaType({ type: 'object', properties: {} }, definitions)).toBe('') + }) +}) diff --git a/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.ts b/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.ts index 7f2b9f21245197..355ce736cbde0a 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.ts @@ -1,6 +1,7 @@ import type { AnyObj } from './match-schema-type' import type { SchemaTypeDefinition } from '@/service/use-common' -import { useSchemaTypeDefinitions } from '@/service/use-common' +import { useQuery } from '@tanstack/react-query' +import { consoleQuery } from '@/service/console' import matchTheSchemaType from './match-schema-type' export const getMatchedSchemaType = ( @@ -13,7 +14,9 @@ export const getMatchedSchemaType = ( } const useMatchSchemaType = () => { - const { data: schemaTypeDefinitions, isLoading } = useSchemaTypeDefinitions() + const { data: schemaTypeDefinitions, isLoading } = useQuery( + consoleQuery.spec.schemaDefinitions.get.queryOptions(), + ) return { isLoading, diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx index b58b36c5e6cf42..f4b786b7864917 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx @@ -207,11 +207,14 @@ vi.mock('@/app/components/workflow/run/hooks', () => ({ useLogs: () => mockLogsState, })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ - data: mockBuiltInTools, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ + data: mockBuiltInTools, + }), + }) +}) vi.mock('@/service/use-triggers', () => ({ useAllTriggerPlugins: () => ({ diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index e73379f67ce1d0..97c2ee70b58a7c 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -6,7 +6,7 @@ import { IconButton } from '@langgenius/dify-ui/icon-button' import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { debounce } from 'es-toolkit/compat' import { useAtomValue } from 'jotai' import { useQueryState } from 'nuqs' @@ -29,6 +29,7 @@ import { } from '@/app/components/plugins/plugin-auth' import { usePluginStore } from '@/app/components/plugins/plugin-detail-panel/store' import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import BlockIcon from '@/app/components/workflow/block-icon' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration' @@ -52,7 +53,7 @@ import { isSupportCustomRunForm, } from '@/app/components/workflow/utils' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { useAllBuiltInTools } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useAllTriggerPlugins } from '@/service/use-triggers' import { useAvailableBlocks } from '../../../../hooks/use-available-blocks' import useInspectVarsCrud from '../../../../hooks/use-inspect-vars-crud' @@ -362,7 +363,11 @@ const BasePanel: FC = ({ id, data, children }) => { }, [data.type, logParams]) const storeBuildInTools = useStore((s) => s.buildInTools) - const { data: buildInTools } = useAllBuiltInTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const currToolCollection = useMemo( () => getCurrentToolCollection(buildInTools, storeBuildInTools, data.provider_id), [buildInTools, storeBuildInTools, data.provider_id], diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts index 41f003f5e387cd..1ea67176b83648 100644 --- a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts @@ -93,12 +93,15 @@ vi.mock('reactflow', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/service/use-workflow', () => ({ useInvalidateLastRun: () => vi.fn(), @@ -106,20 +109,33 @@ vi.mock('@/service/use-workflow', () => ({ vi.mock('@/service/workflow', () => ({ fetchNodeInspectVars: vi.fn(), - getIterationSingleNodeRunUrl: vi.fn(), - getLoopSingleNodeRunUrl: vi.fn(), singleNodeRun: vi.fn(), })) -vi.mock('@/service/console', () => ({ - consoleClient: { - apps: { - byAppId: { - workflows: { draft: { nodes: { byNodeId: { trigger: { run: { post: vi.fn() } } } } } }, +vi.mock('@/service/console', async () => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleClient: { + apps: { + byAppId: { + workflows: { draft: { nodes: { byNodeId: { trigger: { run: { post: vi.fn() } } } } } }, + }, }, }, - }, -})) + consoleQuery: { + workspaces: { + current: { + tools: { + builtin: { get: consoleOperationStub('workspaces.current.tools.builtin.get') }, + api: { get: consoleOperationStub('workspaces.current.tools.api.get') }, + workflow: { get: consoleOperationStub('workspaces.current.tools.workflow.get') }, + mcp: { get: consoleOperationStub('workspaces.current.tools.mcp.get') }, + }, + }, + }, + }, + } +}) vi.mock('@/service/base', () => ({ ssePost: vi.fn(), diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index 58fe56f41235e5..c9e64c0ee3aff4 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -9,6 +9,7 @@ import type { Variable, } from '@/app/components/workflow/types' import type { NodeRunResult, NodeTracing } from '@/types/workflow' +import { useQuery } from '@tanstack/react-query' import { unionBy } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' import { produce } from 'immer' @@ -17,6 +18,7 @@ import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' import { trackEvent } from '@/app/components/base/amplitude' import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { getEditorResourceId, isChatflow } from '@/app/components/workflow/editor-resource/resource' import { getNodeInfoById, @@ -52,13 +54,7 @@ import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/ import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { ssePost } from '@/service/base' -import { consoleClient } from '@/service/console' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleClient, consoleQuery } from '@/service/console' import { useInvalidateLastRun } from '@/service/use-workflow' import { fetchNodeInspectVars, singleNodeRun } from '@/service/workflow' import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud' @@ -184,10 +180,22 @@ const useOneStepRun = ({ const workflowStore = useWorkflowStore() const { schemaTypeDefinitions } = useMatchSchemaType() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const getVar = (valueSelector: ValueSelector): Var | undefined => { const isSystem = valueSelector[0] === 'sys' @@ -785,8 +793,6 @@ const useOneStepRun = ({ postData.inputs = inputs postData.query = query postData.files = files || [] - } else { - postData.inputs = submitData } res = (await singleNodeRun(resource, id, postData)) as any } diff --git a/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx index f4d46ce88ab9f2..1fa6a94282bd38 100644 --- a/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx @@ -55,13 +55,6 @@ vi.mock('@langgenius/dify-ui/status-dot', () => ({ StatusDot: ({ status }: any) =>
{`indicator:${status}`}
, })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockBuiltInTools }), - useAllCustomTools: () => ({ data: mockCustomTools }), - useAllWorkflowTools: () => ({ data: mockWorkflowTools }), - useAllMCPTools: () => ({ data: mockMcpTools }), -})) - vi.mock('@/utils/get-icon', () => ({ getIconFromMarketPlace: () => mockMarketplaceIcon, })) @@ -372,7 +365,8 @@ describe('agent path', () => { vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal() - return { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + const reactQuery = { ...actual, useQuery: (options: { queryKey: OperationKey< @@ -391,5 +385,11 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { if (modelType === ModelTypeEnum.textEmbedding) return { data: mockTextEmbeddingModels } return { data: mockTtsModels } }, - } + } as typeof actual + return mockConsoleOperationHooks(reactQuery, { + 'workspaces.current.tools.builtin.get': () => ({ data: mockBuiltInTools }), + 'workspaces.current.tools.api.get': () => ({ data: mockCustomTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: mockWorkflowTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: mockMcpTools }), + }) }) diff --git a/web/app/components/workflow/nodes/agent/components/__tests__/tool-icon.spec.tsx b/web/app/components/workflow/nodes/agent/components/__tests__/tool-icon.spec.tsx index 810a1771523cfe..d645249e4ed8c0 100644 --- a/web/app/components/workflow/nodes/agent/components/__tests__/tool-icon.spec.tsx +++ b/web/app/components/workflow/nodes/agent/components/__tests__/tool-icon.spec.tsx @@ -14,12 +14,15 @@ let mockWorkflowTools: ToolProvider[] | undefined let mockMcpTools: ToolProvider[] | undefined let mockMarketplaceIcon: string | { content: string; background: string } | undefined -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockBuiltInTools }), - useAllCustomTools: () => ({ data: mockCustomTools }), - useAllWorkflowTools: () => ({ data: mockWorkflowTools }), - useAllMCPTools: () => ({ data: mockMcpTools }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockBuiltInTools }), + 'workspaces.current.tools.api.get': () => ({ data: mockCustomTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: mockWorkflowTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: mockMcpTools }), + }) +}) vi.mock('@langgenius/dify-ui/status-dot', () => ({ StatusDot: ({ status }: { status: string }) =>
{`indicator:${status}`}
, diff --git a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx index 7aae1bdaa066e6..aa5397b782b29e 100644 --- a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx +++ b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx @@ -2,15 +2,12 @@ import type { ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useQuery } from '@tanstack/react-query' import { memo, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' import { getIconFromMarketPlace } from '@/utils/get-icon' import { matchesProviderReference } from '@/utils/provider-reference' @@ -23,10 +20,22 @@ export type ToolIconProps = { export const ToolIcon = memo(({ providerName }: ToolIconProps) => { const containerRef = useRef(null) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools const currentProvider = useMemo(() => { const mergedTools = [ diff --git a/web/app/components/workflow/nodes/data-source/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/data-source/__tests__/panel.spec.tsx index e11c35edb739e2..c710b48928b61f 100644 --- a/web/app/components/workflow/nodes/data-source/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/data-source/__tests__/panel.spec.tsx @@ -14,6 +14,11 @@ const { update, get } = vi.hoisted(() => ({ vi.mock('@/service/base', async (importOriginal) => ({ ...(await importOriginal()), get, + // Console contract requests; only the schema type definitions are expected. + request: async (_url: string, _init: RequestInit, { request }: { request: Request }) => { + if (new URL(request.url).pathname.endsWith('/spec/schema-definitions')) return Response.json([]) + throw new Error(`Unexpected request: ${request.url}`) + }, })) vi.mock('../../../hooks/use-node-data-update', () => ({ useNodeDataUpdate: () => ({ handleNodeDataUpdateWithSyncDraft: update }), diff --git a/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.branches.spec.tsx b/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.branches.spec.tsx index 04485b7e498ec1..bd140b7daa2f72 100644 --- a/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.branches.spec.tsx +++ b/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.branches.spec.tsx @@ -1,5 +1,6 @@ +import type { WorkflowRunNodeExecutionResponse } from '@dify/contracts/api/console/rag/types.gen' import type { CustomRunFormProps, DataSourceNodeType } from '../../types' -import type { NodeRunResult, VarInInspect } from '@/types/workflow' +import type { VarInInspect } from '@/types/workflow' import { act, renderHook } from '@testing-library/react' import { useStoreApi } from 'reactflow' import { @@ -8,7 +9,6 @@ import { } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import { DatasourceType } from '@/models/pipeline' -import { useDatasourceSingleRun } from '@/service/use-pipeline' import { useInvalidateLastRun } from '@/service/use-workflow' import { fetchNodeInspectVars } from '@/service/workflow' import { useNodeDataUpdate } from '../../../../hooks/use-node-data-update' @@ -39,7 +39,7 @@ type DataSourceStoreState = { type DatasourceSingleRunOptions = { onError?: () => void - onSettled?: (data?: NodeRunResult) => void + onSettled?: (data?: WorkflowRunNodeExecutionResponse) => void } const mockHandleNodeDataUpdate = vi.hoisted(() => vi.fn()) @@ -76,9 +76,15 @@ vi.mock('../../../../hooks/use-nodes-sync-draft', async (importOriginal) => { } }) -vi.mock('@/service/use-pipeline', () => ({ - useDatasourceSingleRun: vi.fn(), -})) +const mockUseDatasourceSingleRunHook = vi.hoisted(() => vi.fn()) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.draft.datasource.variablesInspect.post': () => + mockUseDatasourceSingleRunHook(), + }) +}) vi.mock('@/service/use-workflow', () => ({ useInvalidateLastRun: vi.fn(), @@ -96,7 +102,6 @@ vi.mock('@/app/components/datasets/documents/create-from-pipeline/data-source/st const mockUseStoreApi = vi.mocked(useStoreApi) const mockUseNodeDataUpdateHook = vi.mocked(useNodeDataUpdate) const mockUseNodesSyncDraftHook = vi.mocked(useNodesSyncDraft) -const mockUseDatasourceSingleRunHook = vi.mocked(useDatasourceSingleRun) const mockUseInvalidLastRunHook = vi.mocked(useInvalidateLastRun) const mockFetchNodeInspectVarsFn = vi.mocked(fetchNodeInspectVars) const mockUseDataSourceStoreHook = vi.mocked(useDataSourceStore) @@ -171,7 +176,7 @@ describe('data-source/hooks/use-before-run-form branches', () => { mockUseDatasourceSingleRunHook.mockReturnValue({ mutateAsync: (...args: unknown[]) => mockMutateAsync(...args), isPending: false, - } as ReturnType) + }) mockUseInvalidLastRunHook.mockReturnValue(mockInvalidLastRun) mockFetchNodeInspectVarsFn.mockImplementation((...args: unknown[]) => mockFetchNodeInspectVars(...args), @@ -237,7 +242,7 @@ describe('data-source/hooks/use-before-run-form branches', () => { ] mockMutateAsync.mockImplementation((_payload: unknown, options: DatasourceSingleRunOptions) => { - options.onSettled?.({ status: NodeRunningStatus.Succeeded } as NodeRunResult) + options.onSettled?.({ id: 'node-run-1', status: NodeRunningStatus.Succeeded }) return Promise.resolve(undefined) }) @@ -254,7 +259,9 @@ describe('data-source/hooks/use-before-run-form branches', () => { await Promise.resolve() }) - expect(props.setRunResult).toHaveBeenCalledWith({ status: NodeRunningStatus.Succeeded }) + expect(props.setRunResult).toHaveBeenCalledWith( + expect.objectContaining({ id: 'node-run-1', status: NodeRunningStatus.Succeeded }), + ) expect(mockFetchNodeInspectVars).not.toHaveBeenCalled() expect(props.onSuccess).not.toHaveBeenCalled() }) @@ -270,7 +277,7 @@ describe('data-source/hooks/use-before-run-form branches', () => { ] mockMutateAsync.mockImplementation((payload: unknown, options: DatasourceSingleRunOptions) => { - options.onSettled?.({ status: NodeRunningStatus.Succeeded } as NodeRunResult) + options.onSettled?.({ id: 'node-run-1', status: NodeRunningStatus.Succeeded }) return Promise.resolve(payload) }) @@ -289,16 +296,18 @@ describe('data-source/hooks/use-before-run-form branches', () => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - datasource_type: DatasourceType.onlineDocument, - datasource_info: { - workspace_id: 'workspace-1', - page: { - id: 'doc-1', - title: 'Document', - url: 'https://example.com/doc', + body: expect.objectContaining({ + datasource_type: DatasourceType.onlineDocument, + datasource_info: { + workspace_id: 'workspace-1', + page: { + id: 'doc-1', + title: 'Document', + url: 'https://example.com/doc', + }, + credential_id: 'credential-1', }, - credential_id: 'credential-1', - }, + }), }), expect.any(Object), ) @@ -333,12 +342,14 @@ describe('data-source/hooks/use-before-run-form branches', () => { expect(mockMutateAsync).toHaveBeenCalledWith( expect.objectContaining({ - datasource_type: DatasourceType.websiteCrawl, - datasource_info: { - url: 'https://example.com', - title: 'Example', - credential_id: 'credential-1', - }, + body: expect.objectContaining({ + datasource_type: DatasourceType.websiteCrawl, + datasource_info: { + url: 'https://example.com', + title: 'Example', + credential_id: 'credential-1', + }, + }), }), expect.any(Object), ) diff --git a/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.spec.tsx b/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.spec.tsx index 7c4f76df6c4b71..de5de81da233c2 100644 --- a/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.spec.tsx +++ b/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.spec.tsx @@ -1,6 +1,7 @@ +import type { WorkflowRunNodeExecutionResponse } from '@dify/contracts/api/console/rag/types.gen' import type { CustomRunFormProps, DataSourceNodeType } from '../../types' -import type { NodeRunResult, VarInInspect } from '@/types/workflow' -import { act } from '@testing-library/react' +import type { VarInInspect } from '@/types/workflow' +import { act, waitFor } from '@testing-library/react' import { useStoreApi } from 'reactflow' import { useDataSourceStore, @@ -9,7 +10,6 @@ import { import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import { DatasourceType } from '@/models/pipeline' -import { useDatasourceSingleRun } from '@/service/use-pipeline' import { useInvalidateLastRun } from '@/service/use-workflow' import { fetchNodeInspectVars } from '@/service/workflow' import { TransferMethod } from '@/types/app' @@ -41,7 +41,7 @@ type DataSourceStoreState = { type DatasourceSingleRunOptions = { onError?: () => void - onSettled?: (data?: NodeRunResult) => void + onSettled?: (data?: WorkflowRunNodeExecutionResponse) => void } const mockHandleNodeDataUpdate = vi.hoisted(() => vi.fn()) @@ -78,9 +78,15 @@ vi.mock('../../../../hooks/use-nodes-sync-draft', async (importOriginal) => { } }) -vi.mock('@/service/use-pipeline', () => ({ - useDatasourceSingleRun: vi.fn(), -})) +const mockUseDatasourceSingleRunHook = vi.hoisted(() => vi.fn()) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.byPipelineId.workflows.draft.datasource.variablesInspect.post': () => + mockUseDatasourceSingleRunHook(), + }) +}) vi.mock('@/service/use-workflow', () => ({ useInvalidateLastRun: vi.fn(), @@ -98,7 +104,6 @@ vi.mock('@/app/components/datasets/documents/create-from-pipeline/data-source/st const mockUseStoreApi = vi.mocked(useStoreApi) const mockUseNodeDataUpdateHook = vi.mocked(useNodeDataUpdate) const mockUseNodesSyncDraftHook = vi.mocked(useNodesSyncDraft) -const mockUseDatasourceSingleRunHook = vi.mocked(useDatasourceSingleRun) const mockUseInvalidLastRunHook = vi.mocked(useInvalidateLastRun) const mockFetchNodeInspectVarsFn = vi.mocked(fetchNodeInspectVars) const mockUseDataSourceStoreHook = vi.mocked(useDataSourceStore) @@ -180,7 +185,7 @@ describe('data-source/hooks/use-before-run-form', () => { mockUseDatasourceSingleRunHook.mockReturnValue({ mutateAsync: (...args: unknown[]) => mockMutateAsync(...args), isPending: false, - } as ReturnType) + }) mockUseInvalidLastRunHook.mockReturnValue(mockInvalidLastRun) mockFetchNodeInspectVarsFn.mockImplementation((...args: unknown[]) => mockFetchNodeInspectVars(...args), @@ -253,7 +258,7 @@ describe('data-source/hooks/use-before-run-form', () => { ] mockMutateAsync.mockImplementation((payload: unknown, options: DatasourceSingleRunOptions) => { - options.onSettled?.({ status: NodeRunningStatus.Succeeded } as NodeRunResult) + options.onSettled?.({ id: 'node-run-1', status: NodeRunningStatus.Succeeded }) return Promise.resolve(payload) }) @@ -275,15 +280,17 @@ describe('data-source/hooks/use-before-run-form', () => { }), }) expect(mockMutateAsync).toHaveBeenCalledWith( - expect.objectContaining({ - pipeline_id: 'flow-id', - start_node_id: 'data-source-node', - datasource_type: DatasourceType.localFile, - datasource_info: expect.objectContaining({ - related_id: 'file-1', - transfer_method: TransferMethod.local_file, + { + params: { pipeline_id: 'flow-id' }, + body: expect.objectContaining({ + start_node_id: 'data-source-node', + datasource_type: DatasourceType.localFile, + datasource_info: expect.objectContaining({ + related_id: 'file-1', + transfer_method: TransferMethod.local_file, + }), }), - }), + }, expect.any(Object), ) expect(mockFetchNodeInspectVars).toHaveBeenCalledWith( @@ -321,8 +328,10 @@ describe('data-source/hooks/use-before-run-form', () => { }, ] + // TanStack Query settles a failed mutation with no data after onError. mockMutateAsync.mockImplementation((_payload: unknown, options: DatasourceSingleRunOptions) => { options.onError?.() + options.onSettled?.(undefined) return Promise.resolve(undefined) }) @@ -351,5 +360,7 @@ describe('data-source/hooks/use-before-run-form', () => { _singleRunningStatus: NodeRunningStatus.Failed, }), }) + // A failed run can still write default error outputs, so inspect vars refresh. + await waitFor(() => expect(mockFetchNodeInspectVars).toHaveBeenCalled()) }) }) diff --git a/web/app/components/workflow/nodes/data-source/hooks/use-before-run-form.ts b/web/app/components/workflow/nodes/data-source/hooks/use-before-run-form.ts index 94aa4782c65d04..bc1587ba3a3cc9 100644 --- a/web/app/components/workflow/nodes/data-source/hooks/use-before-run-form.ts +++ b/web/app/components/workflow/nodes/data-source/hooks/use-before-run-form.ts @@ -1,5 +1,6 @@ import type { CustomRunFormProps, DataSourceNodeType } from '../types' import type { NodeRunResult } from '@/types/workflow' +import { useMutation } from '@tanstack/react-query' import { useEffect, useMemo, useRef } from 'react' import { useStoreApi } from 'reactflow' import { useShallow } from 'zustand/react/shallow' @@ -7,8 +8,9 @@ import { useDataSourceStore, useDataSourceStoreWithSelector, } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store' +import { selectDatasourceSingleRun } from '@/app/components/rag-pipeline/utils/query-select' import { DatasourceType } from '@/models/pipeline' -import { useDatasourceSingleRun } from '@/service/use-pipeline' +import { consoleQuery } from '@/service/console' import { useInvalidateLastRun } from '@/service/use-workflow' import { fetchNodeInspectVars } from '@/service/workflow' import { TransferMethod } from '@/types/app' @@ -83,7 +85,7 @@ const useBeforeRunForm = ({ const invalidLastRun = useInvalidateLastRun(resource, nodeId) - const updateRunResult = async (data: NodeRunResult) => { + const updateRunResult = async (data: NodeRunResult | undefined) => { const isPaused = isPausedRef.current // The backend don't support pause the single run, so the frontend handle the pause state. @@ -91,7 +93,7 @@ const useBeforeRunForm = ({ const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded if (!canRunLastRun) { - setRunResult(data) + setRunResult(data ?? null) return } @@ -103,7 +105,9 @@ const useBeforeRunForm = ({ if (data?.status === NodeRunningStatus.Succeeded) onSuccess() } - const { mutateAsync: handleDatasourceSingleRun, isPending } = useDatasourceSingleRun() + const { mutateAsync: handleDatasourceSingleRun, isPending } = useMutation( + consoleQuery.rag.pipelines.byPipelineId.workflows.draft.datasource.variablesInspect.post.mutationOptions(), + ) const handleRun = () => { let datasourceInfo: Record = {} @@ -153,11 +157,13 @@ const useBeforeRunForm = ({ let hasError = false handleDatasourceSingleRun( { - pipeline_id: flowId, - start_node_id: nodeId, - start_node_title: datasourceNodeData.title, - datasource_type: datasourceType, - datasource_info: datasourceInfo, + params: { pipeline_id: flowId }, + body: { + start_node_id: nodeId, + start_node_title: datasourceNodeData.title, + datasource_type: datasourceType, + datasource_info: datasourceInfo, + }, }, { onError: () => { @@ -174,7 +180,8 @@ const useBeforeRunForm = ({ }) }, onSettled: (data) => { - updateRunResult(data!) + // A failed request settles without data; still refresh inspect vars. + updateRunResult(data ? selectDatasourceSingleRun(data) : undefined) if (!hasError && !isPausedRef.current) { handleNodeDataUpdate({ id: nodeId, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/email-input.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/email-input.spec.tsx index b97bf21c2fe3c6..f6dc30c658be80 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/email-input.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/email-input.spec.tsx @@ -47,9 +47,9 @@ const createMember = (overrides: Partial): Member => ({ status: 'active', role: 'normal', roles: [], - created_at: '2026-01-01T00:00:00Z', - last_active_at: '2026-01-02T00:00:00Z', - last_login_at: '2026-01-03T00:00:00Z', + created_at: 1767225600, + last_active_at: 1767312000, + last_login_at: 1767398400, ...overrides, }) diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/index.spec.tsx index d2a95ffd123537..17a3bf3d114de3 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/index.spec.tsx @@ -7,7 +7,7 @@ import Recipient from '../index' const mockUseTranslation = vi.hoisted(() => vi.fn()) const mockConsoleStateReader = vi.hoisted(() => vi.fn()) -const mockUseMembers = vi.hoisted(() => vi.fn()) +const mockMembersQuery = vi.hoisted(() => vi.fn()) const mockConsoleState = vi.hoisted(() => ({ userProfile: { email: 'owner@example.com' }, currentWorkspace: { name: "Dify's Lab" }, @@ -27,9 +27,12 @@ vi.mock('@/context/workspace-state', async () => { return createWorkspaceStateModuleMock(() => mockConsoleState) }) -vi.mock('@/service/use-common', () => ({ - useMembers: () => mockUseMembers(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.members.get': () => mockMembersQuery(), + }) +}) vi.mock('../member-selector', () => ({ __esModule: true, @@ -78,7 +81,7 @@ describe('Recipient', () => { ), }) mockConsoleStateReader.mockReturnValue(mockConsoleState) - mockUseMembers.mockReturnValue({ + mockMembersQuery.mockReturnValue({ data: { accounts: [ { id: 'member-1', email: 'member-1@example.com', name: 'Member One' }, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-list.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-list.spec.tsx index 6efcae2b96425a..439ffbc00c57ad 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-list.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-list.spec.tsx @@ -13,9 +13,9 @@ const createMember = (overrides: Partial): Member => ({ status: 'active', role: 'normal', roles: [], - created_at: '2026-01-01T00:00:00Z', - last_active_at: '2026-01-02T00:00:00Z', - last_login_at: '2026-01-03T00:00:00Z', + created_at: 1767225600, + last_active_at: 1767312000, + last_login_at: 1767398400, ...overrides, }) diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx index 98a230577bc0bd..d4f1e48dc51cbf 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx @@ -40,9 +40,9 @@ const members: Member[] = [ status: 'active', role: 'normal', roles: [], - created_at: '2026-01-01T00:00:00Z', - last_active_at: '2026-01-02T00:00:00Z', - last_login_at: '2026-01-03T00:00:00Z', + created_at: 1767225600, + last_active_at: 1767312000, + last_login_at: 1767398400, }, ] diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/index.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/index.tsx index 1b7e86ebc0484f..42550048df4f52 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/index.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/index.tsx @@ -2,14 +2,14 @@ import type { RecipientData, Recipient as RecipientItem } from '../../../types' import { cn } from '@langgenius/dify-ui/cn' import { Switch } from '@langgenius/dify-ui/switch' import { RiGroupLine } from '@remixicon/react' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useAtomValue } from 'jotai' import { memo, useId } from 'react' import { useTranslation } from 'react-i18next' import { currentWorkspaceAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { useMembers } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import EmailInput from './email-input' import MemberSelector from './member-selector' @@ -28,7 +28,9 @@ const Recipient = ({ data, onChange }: Props) => { select: (data) => data.profile.email, }) const currentWorkspace = useAtomValue(currentWorkspaceAtom) - const { data: members } = useMembers() + const { data: members } = useQuery( + consoleQuery.workspaces.current.members.get.queryOptions({ input: {} }), + ) const accounts = members?.accounts || [] const handleMemberSelect = (id: string) => { diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx index 575a1b18413854..ecd612c5ad516c 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx @@ -6,7 +6,7 @@ import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dif import { IconButton } from '@langgenius/dify-ui/icon-button' import { Separator } from '@langgenius/dify-ui/separator' import { RiArrowRightSFill } from '@remixicon/react' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { noop, unionBy } from 'es-toolkit/compat' import { useAtomValue } from 'jotai' import { memo, useCallback, useMemo, useState } from 'react' @@ -25,7 +25,7 @@ import { InputVarType, VarType } from '@/app/components/workflow/types' import { toast } from '@/app/notifications' import { currentWorkspaceAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { useMembers } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { useTestEmailSender } from '@/service/use-workflow' import { getHumanInputFormDependencySelectors, isOutput } from '../../utils' import EmailInput from './recipient/email-input' @@ -151,7 +151,9 @@ const EmailSenderContent = ({ config?.recipients?.items && config?.recipients?.items.length > 0 - const { data: members } = useMembers() + const { data: members } = useQuery( + consoleQuery.workspaces.current.members.get.queryOptions({ input: {} }), + ) const accounts = members?.accounts || [] const generatedInputs = useMemo(() => { diff --git a/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx index 26f374a43f147e..af62e5adfd17aa 100644 --- a/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx @@ -104,12 +104,15 @@ vi.mock('../../variable-assigner/hooks', () => ({ ], })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('../use-config', () => ({ default: vi.fn(), diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx index 93f55a1a3a2175..d057b66bf584a7 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx @@ -22,21 +22,18 @@ import { SelectTrigger, } from '@langgenius/dify-ui/select' import { RiDeleteBinLine } from '@remixicon/react' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useAtomValue } from 'jotai' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { isChatflowAtom } from '@/app/components/workflow/editor-resource/state' import { getVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils' import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value' import { useWorkflowStore } from '@/app/components/workflow/store' import { VarType } from '@/app/components/workflow/types' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import useMatchSchemaType from '../../../_base/components/variable/use-match-schema-type' import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../../constants' import { ComparisonOperator } from '../../types' @@ -106,10 +103,22 @@ const ConditionItem = ({ const [isHovered, setIsHovered] = useState(false) const [open, setOpen] = useState(false) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const workflowStore = useWorkflowStore() diff --git a/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts index 711bda51f3e74b..70f46e7b800752 100644 --- a/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts @@ -43,12 +43,15 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({ default: (...args: unknown[]) => mockUseNodeCrud(...args), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => mockUseAllBuiltInTools(), - useAllCustomTools: () => mockUseAllCustomTools(), - useAllWorkflowTools: () => mockUseAllWorkflowTools(), - useAllMCPTools: () => mockUseAllMCPTools(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => mockUseAllBuiltInTools(), + 'workspaces.current.tools.api.get': () => mockUseAllCustomTools(), + 'workspaces.current.tools.workflow.get': () => mockUseAllWorkflowTools(), + 'workspaces.current.tools.mcp.get': () => mockUseAllMCPTools(), + }) +}) vi.mock('@/app/components/workflow/nodes/_base/components/variable/utils', () => ({ toNodeOutputVars: (...args: unknown[]) => mockToNodeOutputVars(...args), diff --git a/web/app/components/workflow/nodes/iteration/use-config.ts b/web/app/components/workflow/nodes/iteration/use-config.ts index f2198127f5bf69..aee0815c42d641 100644 --- a/web/app/components/workflow/nodes/iteration/use-config.ts +++ b/web/app/components/workflow/nodes/iteration/use-config.ts @@ -1,17 +1,14 @@ import type { ErrorHandleMode, ValueSelector, Var } from '../../types' import type { IterationNodeType } from './types' import type { VarKindType } from '@/app/components/workflow/nodes/_base/types' +import { useQuery } from '@tanstack/react-query' import { isEqual } from 'es-toolkit/predicate' import { produce } from 'immer' import { useAtomValue } from 'jotai' import { useCallback } from 'react' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { isChatflowAtom } from '@/app/components/workflow/editor-resource/state' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' import { useNodesReadOnly, useWorkflow } from '../../hooks/use-workflow' import { useStore } from '../../store' @@ -58,10 +55,22 @@ const useConfig = (id: string, payload: IterationNodeType) => { // output const { getIterationNodeChildren } = useWorkflow() const iterationChildrenNodes = getIterationNodeChildren(id) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const allPluginInfoList = { buildInTools: buildInTools || [], diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/integration.spec.tsx index 20831bd761ff9d..dbe3df2f2660e0 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/integration.spec.tsx @@ -11,6 +11,7 @@ import { } from '@/test/console/account-profile' import { QueryClientTestProvider } from '@/test/console/query-provider' import { render } from '@/test/console/render' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' import { DatasetACLPermission, getDatasetACLCapabilities } from '@/utils/permission' import { DatasetsDetailContext } from '../../../datasets-detail-store/provider' @@ -54,9 +55,9 @@ const mockHasEditPermissionForDataset = vi.fn( ) => true, ) const createDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Dataset Name', - indexing_status: 'completed', icon_info: { icon: '📙', icon_background: '#FFF4ED', @@ -74,7 +75,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 1, - total_document_count: 1, word_count: 1000, provider: 'internal', embedding_model: 'text-embedding-3', @@ -91,17 +91,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 5, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', @@ -125,7 +114,6 @@ const createMetadata = (overrides: Partial = {}): MetadataInDoc = id: 'meta-1', name: 'topic', type: MetadataFilteringVariableType.string, - value: 'topic', ...overrides, }) diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/node.spec.tsx index 1d26fa990d5295..5bd9d4398a91bf 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/node.spec.tsx @@ -2,6 +2,7 @@ import type { KnowledgeRetrievalNodeType } from '../types' import type { DataSet } from '@/models/datasets' import { render, screen } from '@testing-library/react' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' import { DatasetsDetailContext } from '../../../datasets-detail-store/provider' import { createDatasetsDetailStore } from '../../../datasets-detail-store/store' @@ -10,9 +11,9 @@ import Node from '../node' import { MetadataFilteringModeEnum } from '../types' const createDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Dataset Name', - indexing_status: 'completed', icon_info: { icon: '📙', icon_background: '#FFF4ED', @@ -29,7 +30,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 1, - total_document_count: 1, word_count: 1000, provider: 'internal', embedding_model: 'text-embedding-3', @@ -46,17 +46,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 5, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/panel.spec.tsx index 09566126f1590c..b50ba5582cb357 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/panel.spec.tsx @@ -6,6 +6,7 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { BlockEnum, VarType } from '@/app/components/workflow/types' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' import Panel from '../panel' import { @@ -23,9 +24,9 @@ const mockAddKnowledge = vi.hoisted(() => vi.fn()) const mockMetadataFilter = vi.hoisted(() => vi.fn()) const createDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Dataset Name', - indexing_status: 'completed', icon_info: { icon: '📙', icon_background: '#FFF4ED', @@ -42,7 +43,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 1, - total_document_count: 1, word_count: 1000, provider: 'internal', embedding_model: 'text-embedding-3', @@ -59,17 +59,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 5, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', @@ -93,7 +82,6 @@ const createMetadata = (overrides: Partial = {}): MetadataInDoc = id: 'meta-1', name: 'topic', type: MetadataFilteringVariableType.string, - value: 'topic', ...overrides, }) diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/use-config.spec.ts index fc992727f0415e..16f4eaf77da89a 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/use-config.spec.ts @@ -1,5 +1,5 @@ import type { KnowledgeRetrievalNodeType } from '../types' -import type { DataSet, MetadataInDoc } from '@/models/datasets' +import type { DataSet, DataSetListResponse, MetadataInDoc } from '@/models/datasets' import { act, renderHook, waitFor } from '@testing-library/react' import { isEqual } from 'es-toolkit/predicate' import { useState } from 'react' @@ -14,6 +14,7 @@ import { BlockEnum, VarType } from '@/app/components/workflow/types' import { DATASET_DEFAULT } from '@/config' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import { fetchDatasets } from '@/service/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' import { useNodesReadOnly, useWorkflow } from '../../../hooks/use-workflow' import { @@ -67,6 +68,8 @@ vi.mock('@/service/datasets', () => ({ fetchDatasets: vi.fn(), })) +type DatasetListItem = DataSetListResponse['data'][number] + const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const mockUseWorkflow = vi.mocked(useWorkflow) @@ -79,10 +82,10 @@ const mockUseAvailableVarList = vi.mocked(useAvailableVarList) const mockUseDatasetsDetailStore = vi.mocked(useDatasetsDetailStore) const mockFetchDatasets = vi.mocked(fetchDatasets) -const createDataset = (overrides: Partial = {}): DataSet => ({ +const createDataset = (overrides: Partial = {}): DatasetListItem => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Dataset Name', - indexing_status: 'completed', icon_info: { icon: '📙', icon_background: '#FFF4ED', @@ -99,7 +102,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 1, - total_document_count: 1, word_count: 1000, provider: 'internal', embedding_model: 'text-embedding-3', @@ -116,17 +118,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 5, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', @@ -150,7 +141,6 @@ const createMetadata = (overrides: Partial = {}): MetadataInDoc = id: 'meta-1', name: 'topic', type: MetadataFilteringVariableType.string, - value: 'topic', ...overrides, }) @@ -317,7 +307,6 @@ describe('knowledge-retrieval/use-config', () => { }) expect(mockFetchDatasets).toHaveBeenCalledWith({ - url: '/datasets', params: { page: 1, ids: ['dataset-1'], diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx index b0139f2a4873c0..a0a7e5bbce221e 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx @@ -90,8 +90,7 @@ const ConditionItem = ({ const valueAndValueMethod = useMemo(() => { if ( (currentMetadata?.type === MetadataFilteringVariableType.string || - currentMetadata?.type === MetadataFilteringVariableType.number || - currentMetadata?.type === MetadataFilteringVariableType.select) && + currentMetadata?.type === MetadataFilteringVariableType.number) && typeof condition.value === 'string' ) { const regex = isCommonVariable ? COMMON_VARIABLE_REGEX : VARIABLE_REGEX @@ -163,8 +162,7 @@ const ConditionItem = ({
{!comparisonOperatorNotRequireValue(condition.comparison_operator) && - (currentMetadata?.type === MetadataFilteringVariableType.string || - currentMetadata?.type === MetadataFilteringVariableType.select) && ( + currentMetadata?.type === MetadataFilteringVariableType.string && ( ({ fetchDatasets: vi.fn(), })) +type DatasetListItem = DataSetListResponse['data'][number] + const mockFetchDatasets = vi.mocked(fetchDatasets) -const createDataset = (overrides: Partial = {}): DataSet => ({ +const createDataset = (overrides: Partial = {}): DatasetListItem => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Dataset Name', - indexing_status: 'completed', icon_info: { icon: '📙', icon_background: '#FFF4ED', @@ -34,7 +37,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 1, - total_document_count: 1, word_count: 1000, provider: 'internal', embedding_model: 'text-embedding-3', @@ -51,17 +53,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 5, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', @@ -160,7 +151,6 @@ describe('use-knowledge-dataset-selection', () => { }) expect(mockFetchDatasets).toHaveBeenCalledWith({ - url: '/datasets', params: { page: 1, ids: ['dataset-1'], diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-metadata-config.spec.ts b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-metadata-config.spec.ts index d4920ed834d00f..d0bc5577b4bd31 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-metadata-config.spec.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-metadata-config.spec.ts @@ -105,13 +105,11 @@ describe('use-knowledge-metadata-config', () => { id: 'meta-1', name: 'topic', type: MetadataFilteringVariableType.string, - value: 'topic', }) result.current.handleAddCondition({ id: 'meta-2', name: 'score', type: MetadataFilteringVariableType.number, - value: '10', }) }) diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-model-config.spec.ts b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-model-config.spec.ts index abb34e81dc84e9..3e34542502509c 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-model-config.spec.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-model-config.spec.ts @@ -5,13 +5,14 @@ import { act, renderHook, waitFor } from '@testing-library/react' import { BlockEnum } from '@/app/components/workflow/types' import { DATASET_DEFAULT } from '@/config' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { createDatasetFixture } from '@/test/fixtures/dataset' import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' import useKnowledgeModelConfig from '../use-knowledge-model-config' const createDataset = (overrides: Partial = {}): DataSet => ({ + ...createDatasetFixture(), id: 'dataset-1', name: 'Dataset Name', - indexing_status: 'completed', icon_info: { icon: '📙', icon_background: '#FFF4ED', @@ -28,7 +29,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ app_count: 0, doc_form: ChunkingMode.text, document_count: 1, - total_document_count: 1, word_count: 1000, provider: 'internal', embedding_model: 'text-embedding-3', @@ -45,17 +45,6 @@ const createDataset = (overrides: Partial = {}): DataSet => ({ score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: 5, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/use-knowledge-dataset-selection.ts b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/use-knowledge-dataset-selection.ts index 70466798adbc05..c106df2e940885 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/use-knowledge-dataset-selection.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/use-knowledge-dataset-selection.ts @@ -39,7 +39,6 @@ const useKnowledgeDatasetSelection = ({ const datasetIds = currentInputs.dataset_ids if (datasetIds.length > 0) { const { data: dataSetsWithDetail } = await fetchDatasets({ - url: '/datasets', params: { page: 1, ids: datasetIds, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts b/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts index 910dbdef8adce1..81d8f9a4fcb26e 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts @@ -123,8 +123,8 @@ export const getMultipleRetrievalConfig = ( : allHighQualityFullTextSearch ? DEFAULT_WEIGHTED_SCORE.allHighQualityFullTextSearch.semantic : DEFAULT_WEIGHTED_SCORE.other.semantic, - embedding_provider_name: selectedDatasets[0]!.embedding_model_provider, - embedding_model_name: selectedDatasets[0]!.embedding_model, + embedding_provider_name: selectedDatasets[0]!.embedding_model_provider ?? '', + embedding_model_name: selectedDatasets[0]!.embedding_model ?? '', }, keyword_setting: { keyword_weight: allHighQualityVectorSearch diff --git a/web/app/components/workflow/nodes/llm/components/__tests__/structure-output.browser.spec.tsx b/web/app/components/workflow/nodes/llm/components/__tests__/structure-output.browser.spec.tsx index 92f71c59dd274a..47f74404ae4da5 100644 --- a/web/app/components/workflow/nodes/llm/components/__tests__/structure-output.browser.spec.tsx +++ b/web/app/components/workflow/nodes/llm/components/__tests__/structure-output.browser.spec.tsx @@ -4,7 +4,6 @@ import { useState } from 'react' import { render } from 'vitest-browser-react' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' import { Type } from '../../types' import { StructureOutput } from '../structure-output' @@ -63,9 +62,14 @@ function renderOutput() { }).queryKey, { data: [] }, ) - queryClient.setQueryData(commonQueryKeys.defaultModel(ModelTypeEnum.textGeneration), { - data: null, - }) + queryClient.setQueryData( + consoleQuery.workspaces.current.defaultModel.get.queryKey({ + input: { query: { model_type: ModelTypeEnum.textGeneration } }, + }), + { + data: null, + }, + ) return render( diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/__tests__/json-schema-config.spec.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/__tests__/json-schema-config.spec.tsx index d045323c663899..66daf201631de5 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/__tests__/json-schema-config.spec.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/__tests__/json-schema-config.spec.tsx @@ -6,7 +6,6 @@ import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import { useState } from 'react' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { consoleQuery } from '@/service/console' -import { commonQueryKeys } from '@/service/use-common' import { QueryClientTestProvider } from '@/test/console/query-provider' import { createSystemFeaturesFixture } from '@/test/console/system-features' import { Type } from '../../../types' @@ -66,7 +65,12 @@ function renderOutput({ readOnly = false, onChange = vi.fn() } = {}) { }).queryKey, { data: [] }, ) - client.setQueryData(commonQueryKeys.defaultModel(ModelTypeEnum.textGeneration), { data: null }) + client.setQueryData( + consoleQuery.workspaces.current.defaultModel.get.queryKey({ + input: { query: { model_type: ModelTypeEnum.textGeneration } }, + }), + { data: null }, + ) client.setQueryData( consoleQuery.systemFeatures.get.queryOptions().queryKey, createSystemFeaturesFixture(), diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx index 29a9fd48aa5678..2482398293c67e 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx @@ -2,6 +2,7 @@ import type { SchemaRoot } from '../../../types' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { CompletionParams, Model } from '@/types/app' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { useMutation } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -10,7 +11,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { toast } from '@/app/notifications' import useTheme from '@/hooks/use-theme' -import { useGenerateStructuredOutputRules } from '@/service/use-common' +import { consoleQuery } from '@/service/console' import { ModelModeType, Theme } from '@/types/app' import { useMittContext } from '../visual-editor/context' import { useVisualEditorStore } from '../visual-editor/store' @@ -100,13 +101,19 @@ export function JsonSchemaGenerator({ onApply, crossAxisOffset }: JsonSchemaGene [resolvedModel, setModel], ) - const { mutateAsync: generateStructuredOutputRules, isPending: isGenerating } = - useGenerateStructuredOutputRules() + const { mutateAsync: generateStructuredOutputRules, isPending: isGenerating } = useMutation( + consoleQuery.ruleStructuredOutputGenerate.post.mutationOptions(), + ) const generateSchema = useCallback(async () => { const { output, error } = await generateStructuredOutputRules({ - instruction, - model_config: resolvedModel, + body: { + instruction, + model_config: { + ...resolvedModel, + mode: resolvedModel.mode === 'completion' ? 'completion' : 'chat', + }, + }, }) if (error) { toast.error(error) diff --git a/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx index 883480d4d7ae60..73a5978635fe65 100644 --- a/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx @@ -27,12 +27,15 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('../../../hooks/use-workflow', async (importOriginal) => { const actual = await importOriginal() diff --git a/web/app/components/workflow/nodes/loop/use-config.ts b/web/app/components/workflow/nodes/loop/use-config.ts index 55f91a1fb21190..4a5b30f03b1cfb 100644 --- a/web/app/components/workflow/nodes/loop/use-config.ts +++ b/web/app/components/workflow/nodes/loop/use-config.ts @@ -9,16 +9,13 @@ import type { HandleUpdateSubVariableCondition, LoopNodeType, } from './types' +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useEffect, useRef } from 'react' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { isChatflowAtom } from '@/app/components/workflow/editor-resource/state' import { useStore } from '@/app/components/workflow/store' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useNodesReadOnly, useWorkflow } from '../../hooks/use-workflow' import { toNodeOutputVars } from '../_base/components/variable/utils' import useNodeCrud from '../_base/hooks/use-node-crud' @@ -63,10 +60,22 @@ const useConfig = (id: string, payload: LoopNodeType) => { // output const { getLoopNodeChildren } = useWorkflow() const loopChildrenNodes = [{ id, data: payload } as any, ...getLoopNodeChildren(id)] - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const allPluginInfoList = { buildInTools: buildInTools || [], diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx index a04b388f3c79e4..d2a47b55ad5ecf 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx @@ -32,13 +32,15 @@ const mockToolCollections = vi.hoisted(() => ({ workflow: [] as ToolWithProvider[], })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockToolCollections.builtIn }), - useAllCustomTools: () => ({ data: mockToolCollections.custom }), - useAllWorkflowTools: () => ({ data: mockToolCollections.workflow }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockToolCollections.builtIn }), + 'workspaces.current.tools.api.get': () => ({ data: mockToolCollections.custom }), + 'workspaces.current.tools.workflow.get': () => ({ data: mockToolCollections.workflow }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useLanguage: () => 'en_US', diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx index 61e4f2e1c54f97..d827dc07b379fe 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx @@ -8,11 +8,13 @@ import type { } from '@/app/components/workflow/block-selector/types' import type { BlockEnum } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' +import { useQuery } from '@tanstack/react-query' import { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { CollectionType } from '@/app/components/tools/types' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' import BlockSelector from '../../../../block-selector' import { TabType } from '../../../../block-selector/types' @@ -38,9 +40,19 @@ const ImportFromTool: FC = ({ onImport }) => { const { t } = useTranslation(['workflowModels']) const language = useLanguage() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const handleSelectTool = useCallback( (_type: BlockEnum, toolInfo?: BlockDefaultValue) => { diff --git a/web/app/components/workflow/nodes/tool/hooks/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/tool/hooks/__tests__/use-config.spec.tsx index 4c5eb618e810bb..9b4268ac895a82 100644 --- a/web/app/components/workflow/nodes/tool/hooks/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/tool/hooks/__tests__/use-config.spec.tsx @@ -3,8 +3,13 @@ import { renderHook } from '@testing-library/react' import { CollectionType } from '@/app/components/tools/types' import { VarKindType } from '@/app/components/workflow/nodes/_base/types' import { BlockEnum } from '@/app/components/workflow/types' +import { createQueryClientWrapper } from '@/test/console/query-client' +import { createTestQueryClient } from '@/test/query-client' import useConfig from '../use-config' +const queryClient = createTestQueryClient() +const queryClientWrapper = createQueryClientWrapper(queryClient) + const mockSetInputs = vi.hoisted(() => vi.fn()) const mockSetControlPromptEditorRerenderKey = vi.hoisted(() => vi.fn()) const mockUseCurrentToolCollection = vi.hoisted(() => vi.fn()) @@ -41,10 +46,6 @@ vi.mock('@/service/tools', () => ({ updateBuiltInToolCredential: vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidToolsByType: () => vi.fn(), -})) - vi.mock('@/app/components/workflow/store', () => ({ useWorkflowStore: () => ({ getState: () => ({ @@ -162,6 +163,7 @@ describe('useConfig', () => { const { rerender } = renderHook(({ payload }) => useConfig('tool-node-1', payload), { initialProps: { payload: emptyPayload }, + wrapper: queryClientWrapper, }) expect(mockSetInputs).toHaveBeenCalledTimes(1) @@ -178,14 +180,16 @@ describe('useConfig', () => { }) it('should not update inputs when tool values are already populated on first render', () => { - renderHook(() => - useConfig( - 'tool-node-1', - createNodeData({ - tool_parameters: { query: createToolVarInput('existing query') }, - tool_configurations: { api_key: createToolVarInput('existing secret') }, - }), - ), + renderHook( + () => + useConfig( + 'tool-node-1', + createNodeData({ + tool_parameters: { query: createToolVarInput('existing query') }, + tool_configurations: { api_key: createToolVarInput('existing secret') }, + }), + ), + { wrapper: queryClientWrapper }, ) expect(mockSetInputs).not.toHaveBeenCalled() @@ -202,7 +206,7 @@ describe('useConfig', () => { }) mockGetConfiguredValue.mockReturnValue({}) - renderHook(() => useConfig('tool-node-1', createNodeData())) + renderHook(() => useConfig('tool-node-1', createNodeData()), { wrapper: queryClientWrapper }) expect(mockSetInputs).not.toHaveBeenCalled() }) diff --git a/web/app/components/workflow/nodes/tool/hooks/__tests__/use-current-tool-collection.spec.tsx b/web/app/components/workflow/nodes/tool/hooks/__tests__/use-current-tool-collection.spec.tsx index 3e0b36ecb9c008..c0ed4709e0a10d 100644 --- a/web/app/components/workflow/nodes/tool/hooks/__tests__/use-current-tool-collection.spec.tsx +++ b/web/app/components/workflow/nodes/tool/hooks/__tests__/use-current-tool-collection.spec.tsx @@ -9,12 +9,15 @@ const mockUseAllWorkflowTools = vi.hoisted(() => vi.fn()) const mockUseAllMCPTools = vi.hoisted(() => vi.fn()) const mockCanFindTool = vi.hoisted(() => vi.fn()) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => mockUseAllBuiltInTools(), - useAllCustomTools: () => mockUseAllCustomTools(), - useAllWorkflowTools: () => mockUseAllWorkflowTools(), - useAllMCPTools: () => mockUseAllMCPTools(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => mockUseAllBuiltInTools(), + 'workspaces.current.tools.api.get': () => mockUseAllCustomTools(), + 'workspaces.current.tools.workflow.get': () => mockUseAllWorkflowTools(), + 'workspaces.current.tools.mcp.get': () => mockUseAllMCPTools(), + }) +}) vi.mock('@/utils', () => ({ canFindTool: (...args: unknown[]) => mockCanFindTool(...args), diff --git a/web/app/components/workflow/nodes/tool/hooks/use-config.ts b/web/app/components/workflow/nodes/tool/hooks/use-config.ts index a782820cb07adc..df389818d350d1 100644 --- a/web/app/components/workflow/nodes/tool/hooks/use-config.ts +++ b/web/app/components/workflow/nodes/tool/hooks/use-config.ts @@ -1,6 +1,7 @@ import type { ToolNodeType } from '../types' import type { ResourceVarInputs } from '@/app/components/workflow/nodes/_base/types' import type { InputVar } from '@/app/components/workflow/types' +import { useQueryClient } from '@tanstack/react-query' import { capitalize } from 'es-toolkit/string' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -10,10 +11,10 @@ import { getConfiguredValue, toolParametersToFormSchemas, } from '@/app/components/tools/utils/to-form-schema' +import { getToolListQueryKey } from '@/app/components/tools/utils/tool-list-query-key' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { useWorkflowStore } from '@/app/components/workflow/store' import { updateBuiltInToolCredential } from '@/service/tools' -import { useInvalidToolsByType } from '@/service/use-tools' import { useNodesReadOnly } from '../../../hooks/use-workflow' import { isToolAuthorizationRequired } from '../auth' import { normalizeJsonSchemaType } from '../output-schema-utils' @@ -42,15 +43,16 @@ const useConfig = (id: string, payload: ToolNodeType) => { const isShowAuthBtn = isToolAuthorizationRequired(provider_type, currCollection) const [showSetAuth, setShowSetAuth] = useState(false) - const invalidToolsByType = useInvalidToolsByType(provider_type) + const queryClient = useQueryClient() const handleSaveAuth = useCallback( async (value: any) => { await updateBuiltInToolCredential(currCollection?.name as string, value) - invalidToolsByType() + const toolListQueryKey = getToolListQueryKey(provider_type) + if (toolListQueryKey) queryClient.invalidateQueries({ queryKey: toolListQueryKey }) setShowSetAuth(false) }, - [currCollection?.name, invalidToolsByType], + [currCollection?.name, provider_type, queryClient], ) const currTool = useMemo(() => { diff --git a/web/app/components/workflow/nodes/tool/hooks/use-current-tool-collection.ts b/web/app/components/workflow/nodes/tool/hooks/use-current-tool-collection.ts index 87199877414667..b792a67bb7b6e4 100644 --- a/web/app/components/workflow/nodes/tool/hooks/use-current-tool-collection.ts +++ b/web/app/components/workflow/nodes/tool/hooks/use-current-tool-collection.ts @@ -1,23 +1,32 @@ import type { ToolNodeType } from '../types' import type { ToolWithProvider } from '@/app/components/workflow/types' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' const useCurrentToolCollection = ( providerType: ToolNodeType['provider_type'], providerId: string, ) => { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const currentTools = useMemo(() => { switch (providerType) { diff --git a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx index c2b71d70919648..7e2169bee22173 100644 --- a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx +++ b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx @@ -104,13 +104,15 @@ vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/app/components/plugins/marketplace/query', () => ({ useMarketplacePlugins: () => ({ data: undefined }), diff --git a/web/app/components/workflow/plugin-dependency/__tests__/hooks.spec.tsx b/web/app/components/workflow/plugin-dependency/__tests__/hooks.spec.tsx index 4624e967ab3d24..ad8d79aca4615c 100644 --- a/web/app/components/workflow/plugin-dependency/__tests__/hooks.spec.tsx +++ b/web/app/components/workflow/plugin-dependency/__tests__/hooks.spec.tsx @@ -11,8 +11,21 @@ const mocks = vi.hoisted(() => ({ pipeline: vi.fn(), })) -vi.mock('@/service/console', () => ({ +vi.mock('@/service/console', async () => ({ consoleQuery: { + rag: { + pipelines: { + imports: { + byPipelineId: { + checkDependencies: { + get: (await import('@/test/console/operation-hooks')).consoleOperationStub( + 'rag.pipelines.imports.byPipelineId.checkDependencies.get', + ), + }, + }, + }, + }, + }, apps: { imports: { byAppId: { @@ -28,9 +41,14 @@ vi.mock('@/service/console', () => ({ }, }, })) -vi.mock('@/service/use-pipeline', () => ({ - useCheckPipelineDependencies: () => ({ mutateAsync: mocks.pipeline }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.imports.byPipelineId.checkDependencies.get': () => ({ + mutateAsync: mocks.pipeline, + }), + }) +}) vi.mock('@/app/notifications', () => ({ toast: { error: mocks.error } })) function renderDependencies() { diff --git a/web/app/components/workflow/plugin-dependency/hooks.ts b/web/app/components/workflow/plugin-dependency/hooks.ts index 3180798601c88d..0e9f2fb0515dbe 100644 --- a/web/app/components/workflow/plugin-dependency/hooks.ts +++ b/web/app/components/workflow/plugin-dependency/hooks.ts @@ -1,9 +1,9 @@ import { useMutation } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { selectPipelineDependencies } from '@/app/components/rag-pipeline/utils/query-select' import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' -import { useCheckPipelineDependencies } from '@/service/use-pipeline' import { getErrorMessage } from '@/utils/error-message' import { useStore as usePluginDependenciesStore } from './store' @@ -14,14 +14,19 @@ export const usePluginDependencies = () => { context: { silent: true }, }), ) - const { mutateAsync: checkPipelineDependencies } = useCheckPipelineDependencies() + const { mutateAsync: checkPipelineDependencies } = useMutation( + consoleQuery.rag.pipelines.imports.byPipelineId.checkDependencies.get.mutationOptions(), + ) const handleCheckPluginDependencies = useCallback( async (id: string, isPipeline = false) => { const { setDependencies } = usePluginDependenciesStore.getState() if (isPipeline) { - const { leaked_dependencies } = await checkPipelineDependencies(id) - setDependencies(leaked_dependencies) + setDependencies( + selectPipelineDependencies( + await checkPipelineDependencies({ params: { pipeline_id: id } }), + ), + ) return true } diff --git a/web/app/components/workflow/run/__tests__/status.spec.tsx b/web/app/components/workflow/run/__tests__/status.spec.tsx index 3c1a890970435e..c49571a241296d 100644 --- a/web/app/components/workflow/run/__tests__/status.spec.tsx +++ b/web/app/components/workflow/run/__tests__/status.spec.tsx @@ -1,11 +1,12 @@ -import type { WorkflowPausedDetailsResponse } from '@/models/log' +import type { WorkflowPauseDetailsResponse } from '@dify/contracts/api/console/workflow/types.gen' +import { skipToken } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' import { cloneElement, isValidElement } from 'react' import { createDocLinkMock, resolveDocLink } from '../../__tests__/i18n' import Status from '../status' const mockDocLink = createDocLinkMock() -const mockUseWorkflowPausedDetails = vi.fn() +const mockUseQuery = vi.fn() vi.mock('react-i18next', async () => { const { withSelectorKey, withSelectorKeyProps } = await import('@/test/i18n-mock') @@ -57,14 +58,28 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => mockDocLink, })) -vi.mock('@/service/use-log', () => ({ - useWorkflowPausedDetails: (params: { workflowRunId: string; enabled?: boolean }) => - mockUseWorkflowPausedDetails(params), +vi.mock('@tanstack/react-query', async (importOriginal) => ({ + ...(await importOriginal()), + useQuery: (options: unknown) => mockUseQuery(options), +})) + +vi.mock('@/service/console', () => ({ + consoleQuery: { + workflow: { + byWorkflowRunId: { + pauseDetails: { + get: { + queryOptions: (options: unknown) => options, + }, + }, + }, + }, + }, })) const createPausedDetails = ( - overrides: Partial = {}, -): WorkflowPausedDetailsResponse => ({ + overrides: Partial = {}, +): WorkflowPauseDetailsResponse => ({ paused_at: '2026-03-18T00:00:00Z', paused_nodes: [], ...overrides, @@ -73,7 +88,7 @@ const createPausedDetails = ( describe('Status', () => { beforeEach(() => { vi.clearAllMocks() - mockUseWorkflowPausedDetails.mockReturnValue({ data: undefined }) + mockUseQuery.mockReturnValue({ data: undefined }) }) it('renders the scheduled status before execution', () => { @@ -87,10 +102,7 @@ describe('Status', () => { expect(screen.getByText('appLog.status.running')).toBeInTheDocument() expect(document.querySelectorAll('.bg-text-quaternary')).toHaveLength(2) - expect(mockUseWorkflowPausedDetails).toHaveBeenCalledWith({ - workflowRunId: 'run-1', - enabled: false, - }) + expect(mockUseQuery).toHaveBeenCalledWith({ input: skipToken }) }) it('renders the listening label when the run is waiting for input', () => { @@ -170,14 +182,13 @@ describe('Status', () => { expect(screen.getByText('appLog.status.paused')).toBeInTheDocument() expect(screen.getByText('workflowHumanInput.nodes.humanInput.log.reason')).toBeInTheDocument() expect(document.querySelectorAll('.bg-text-quaternary')).toHaveLength(3) - expect(mockUseWorkflowPausedDetails).toHaveBeenCalledWith({ - workflowRunId: 'run-3', - enabled: true, + expect(mockUseQuery).toHaveBeenCalledWith({ + input: { params: { workflow_run_id: 'run-3' } }, }) }) it('renders paused human-input reasons and backstage URLs', () => { - mockUseWorkflowPausedDetails.mockReturnValue({ + mockUseQuery.mockReturnValue({ data: createPausedDetails({ paused_nodes: [ { diff --git a/web/app/components/workflow/run/status.tsx b/web/app/components/workflow/run/status.tsx index 64eb0793a1a5cc..69704b988176e9 100644 --- a/web/app/components/workflow/run/status.tsx +++ b/web/app/components/workflow/run/status.tsx @@ -2,11 +2,12 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { StatusDot } from '@langgenius/dify-ui/status-dot' +import { skipToken, useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import StatusContainer from '@/app/components/workflow/run/status-container' import { useDocLink } from '@/context/i18n' -import { useWorkflowPausedDetails } from '@/service/use-log' +import { consoleQuery } from '@/service/console' type ResultProps = { status: string @@ -31,10 +32,14 @@ const StatusPanel: FC = ({ }) => { const { t } = useTranslation(['runLog', 'workflow', 'appLog', 'workflowHumanInput']) const docLink = useDocLink() - const { data: pausedDetails } = useWorkflowPausedDetails({ - workflowRunId: workflowRunId || '', - enabled: status === 'paused', - }) + const { data: pausedDetails } = useQuery( + consoleQuery.workflow.byWorkflowRunId.pauseDetails.get.queryOptions({ + input: + status === 'paused' && workflowRunId + ? { params: { workflow_run_id: workflowRunId } } + : skipToken, + }), + ) const pausedReasons = useMemo(() => { const reasons: string[] = [] @@ -55,7 +60,7 @@ const StatusPanel: FC = ({ const hasHumanInputNode = paused_nodes.some((node) => node.pause_type.type === 'human_input') if (hasHumanInputNode) { paused_nodes.forEach((node) => { - if (node.pause_type.type === 'human_input') { + if (node.pause_type.type === 'human_input' && node.pause_type.backstage_input_url) { inputURLs.push(node.pause_type.backstage_input_url) } }) diff --git a/web/app/components/workflow/shortcuts/__tests__/workflow-canvas.browser.spec.tsx b/web/app/components/workflow/shortcuts/__tests__/workflow-canvas.browser.spec.tsx index fc04d89d9af099..f92bfdbfe894ff 100644 --- a/web/app/components/workflow/shortcuts/__tests__/workflow-canvas.browser.spec.tsx +++ b/web/app/components/workflow/shortcuts/__tests__/workflow-canvas.browser.spec.tsx @@ -94,7 +94,12 @@ vi.mock('../../hooks/use-nodes-meta-data', () => ({ author: 'Dify', }), })) -vi.mock('@/service/use-tools', () => ({ useAllWorkflowTools: () => ({ data: [] }) })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + }) +}) vi.mock('../../node-actions-menu/change-block-popup', () => ({ ChangeBlockPopup: () => null })) vi.mock('@/context/permission-state', async () => { diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 6c1c6f82547a92..5d5782ebe6db82 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -1,6 +1,7 @@ import type { RagPipelineDatasourceProviderResponse } from '@dify/contracts/api/console/rag/types.gen' +import type { MarketplacePlugin } from '@dify/contracts/marketplace' import type { Edge as ReactFlowEdge, Node as ReactFlowNode, Viewport, XYPosition } from 'reactflow' -import type { Plugin, PluginMeta } from '@/app/components/plugins/types' +import type { PluginMeta } from '@/app/components/plugins/types' import type { Collection, Tool } from '@/app/components/tools/types' import type { BlockClassification, @@ -519,7 +520,7 @@ export type ToolWithProvider = Collection & { export type RAGRecommendedPlugins = { installed_recommended_plugins: ToolWithProvider[] - uninstalled_recommended_plugins: Plugin[] + uninstalled_recommended_plugins: MarketplacePlugin[] } export const SupportUploadFileTypes = { diff --git a/web/app/components/workflow/variable-inspect/__tests__/value-content-sections.spec.tsx b/web/app/components/workflow/variable-inspect/__tests__/value-content-sections.spec.tsx index 6f4011ca420bd0..7e8e6fd48a4703 100644 --- a/web/app/components/workflow/variable-inspect/__tests__/value-content-sections.spec.tsx +++ b/web/app/components/workflow/variable-inspect/__tests__/value-content-sections.spec.tsx @@ -2,6 +2,7 @@ import type { FileUploadConfigResponse } from '@/models/common' import type { VarInInspect } from '@/types/workflow' import { fireEvent, render, screen } from '@testing-library/react' import { VarType } from '@/app/components/workflow/types' +import { createFileUploadConfigFixture } from '@/test/fixtures/file-upload-config' import { VarInInspectType } from '@/types/workflow' import { BoolArraySection, @@ -49,15 +50,16 @@ vi.mock('@/next/navigation', () => ({ })) describe('value-content sections', () => { - const createFileUploadConfig = (): FileUploadConfigResponse => ({ - batch_count_limit: 10, - image_file_batch_limit: 10, - single_chunk_attachment_limit: 10, - attachment_image_file_size_limit: 2, - file_size_limit: 15, - file_upload_limit: 5, - workflow_file_upload_limit: 5, - }) + const createFileUploadConfig = (): FileUploadConfigResponse => + createFileUploadConfigFixture({ + batch_count_limit: 10, + image_file_batch_limit: 10, + single_chunk_attachment_limit: 10, + attachment_image_file_size_limit: 2, + file_size_limit: 15, + file_upload_limit: 5, + workflow_file_upload_limit: 5, + }) const createVar = (overrides: Partial): VarInInspect => ({ diff --git a/web/app/forgot-password/ChangePasswordForm.spec.tsx b/web/app/forgot-password/ChangePasswordForm.spec.tsx index 83e640f8c6e08d..85cbabef25d09d 100644 --- a/web/app/forgot-password/ChangePasswordForm.spec.tsx +++ b/web/app/forgot-password/ChangePasswordForm.spec.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import useDocumentTitle from '@/hooks/use-document-title' -import { changePasswordWithToken } from '@/service/common' +import { consoleClient } from '@/service/console' import { useVerifyForgotPasswordToken } from '@/service/use-common' import ChangePasswordForm from './ChangePasswordForm' @@ -15,8 +15,15 @@ vi.mock('@/service/use-common', () => ({ useVerifyForgotPasswordToken: vi.fn(), })) -vi.mock('@/service/common', () => ({ - changePasswordWithToken: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + forgotPassword: { + resets: { + post: vi.fn(), + }, + }, + }, })) vi.mock('@/hooks/use-document-title', () => ({ @@ -28,7 +35,7 @@ vi.mock('@/utils/var', () => ({ basePath: '' })) type UseVerifyResult = ReturnType const mockUseVerify = vi.mocked(useVerifyForgotPasswordToken) -const mockChangePassword = vi.mocked(changePasswordWithToken) +const mockChangePassword = vi.mocked(consoleClient.forgotPassword.resets.post) const mockUseDocumentTitle = vi.mocked(useDocumentTitle) const VALID_PASSWORD = 'ValidPass123!' @@ -79,7 +86,6 @@ describe('ChangePasswordForm', () => { await waitFor(() => { expect(mockChangePassword).toHaveBeenCalledWith({ - url: '/forgot-password/resets', body: { token: T2, new_password: VALID_PASSWORD, @@ -165,7 +171,7 @@ describe('ChangePasswordForm', () => { it('prevents duplicate submissions while the password is changing', async () => { const user = userEvent.setup() let resolveRequest: - | ((value: Awaited>) => void) + | ((value: Awaited>) => void) | undefined mockChangePassword.mockReturnValue( new Promise((resolve) => { diff --git a/web/app/forgot-password/ChangePasswordForm.tsx b/web/app/forgot-password/ChangePasswordForm.tsx index 28f3fe5f77941a..2894613bf51544 100644 --- a/web/app/forgot-password/ChangePasswordForm.tsx +++ b/web/app/forgot-password/ChangePasswordForm.tsx @@ -17,7 +17,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { validPassword } from '@/config' import useDocumentTitle from '@/hooks/use-document-title' import { useSearchParams } from '@/next/navigation' -import { changePasswordWithToken } from '@/service/common' +import { consoleClient } from '@/service/console' import { useVerifyForgotPasswordToken } from '@/service/use-common' import { basePath } from '@/utils/var' @@ -56,8 +56,7 @@ const ChangePasswordForm = () => { setIsSubmitting(true) try { - await changePasswordWithToken({ - url: '/forgot-password/resets', + await consoleClient.forgotPassword.resets.post({ body: { token: resetToken, new_password: formValues.password, diff --git a/web/app/forgot-password/ForgotPasswordForm.spec.tsx b/web/app/forgot-password/ForgotPasswordForm.spec.tsx index b4dfe22a2d6477..4968ca03298182 100644 --- a/web/app/forgot-password/ForgotPasswordForm.spec.tsx +++ b/web/app/forgot-password/ForgotPasswordForm.spec.tsx @@ -1,18 +1,22 @@ -import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import useDocumentTitle from '@/hooks/use-document-title' -import { - fetchInitValidateStatus, - fetchSetupStatus, - sendForgotPasswordEmail, -} from '@/service/common' +import { consoleClient } from '@/service/console' import ForgotPasswordForm from './ForgotPasswordForm' -vi.mock('@/service/common', () => ({ - fetchSetupStatus: vi.fn(), - fetchInitValidateStatus: vi.fn(), - sendForgotPasswordEmail: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + setup: { + get: vi.fn(), + }, + init: { + get: vi.fn(), + }, + forgotPassword: { + post: vi.fn(), + }, + }, })) vi.mock('@/hooks/use-document-title', () => ({ @@ -20,16 +24,16 @@ vi.mock('@/hooks/use-document-title', () => ({ default: vi.fn(), })) -const mockFetchSetupStatus = vi.mocked(fetchSetupStatus) -const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus) -const mockSendForgotPasswordEmail = vi.mocked(sendForgotPasswordEmail) +const mockFetchSetupStatus = vi.mocked(consoleClient.setup.get) +const mockFetchInitValidateStatus = vi.mocked(consoleClient.init.get) +const mockSendForgotPasswordEmail = vi.mocked(consoleClient.forgotPassword.post) const mockUseDocumentTitle = vi.mocked(useDocumentTitle) const prepareLoadedState = () => { - mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' } as SetupStatusResponse) + mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' }) mockFetchInitValidateStatus.mockResolvedValue({ status: 'finished', - } as InitValidateStatusResponse) + }) } describe('ForgotPasswordForm', () => { @@ -90,7 +94,6 @@ describe('ForgotPasswordForm', () => { await waitFor(() => { expect(mockSendForgotPasswordEmail).toHaveBeenCalledWith({ - url: '/forgot-password', body: { email: 'test@example.com' }, }) }) @@ -115,7 +118,6 @@ describe('ForgotPasswordForm', () => { await waitFor(() => { expect(mockSendForgotPasswordEmail).toHaveBeenCalledWith({ - url: '/forgot-password', body: { email: 'test@example.com' }, }) }) @@ -177,7 +179,7 @@ describe('ForgotPasswordForm', () => { }) mockFetchInitValidateStatus.mockResolvedValue({ status: 'not_started', - } as InitValidateStatusResponse) + }) render() diff --git a/web/app/forgot-password/ForgotPasswordForm.tsx b/web/app/forgot-password/ForgotPasswordForm.tsx index 38f1c2e557117c..ac89007fe7bc0e 100644 --- a/web/app/forgot-password/ForgotPasswordForm.tsx +++ b/web/app/forgot-password/ForgotPasswordForm.tsx @@ -1,5 +1,4 @@ 'use client' -import type { InitValidateStatusResponse } from '@/models/common' import { Button, buttonVariants } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' @@ -11,11 +10,7 @@ import * as z from 'zod' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' -import { - fetchInitValidateStatus, - fetchSetupStatus, - sendForgotPasswordEmail, -} from '@/service/common' +import { consoleClient } from '@/service/console' import { basePath } from '@/utils/var' type ForgotPasswordFormValues = { @@ -43,10 +38,7 @@ const ForgotPasswordForm = () => { setIsSubmitting(true) try { - const res = await sendForgotPasswordEmail({ - url: '/forgot-password', - body: { email }, - }) + const res = await consoleClient.forgotPassword.post({ body: { email } }) if (res.result === 'success') setIsEmailSent(true) else console.error('Email verification failed') } catch (error) { @@ -57,8 +49,8 @@ const ForgotPasswordForm = () => { } useEffect(() => { - fetchSetupStatus().then(() => { - fetchInitValidateStatus().then((res: InitValidateStatusResponse) => { + consoleClient.setup.get().then(() => { + consoleClient.init.get().then((res) => { if (res.status === 'not_started') window.location.href = `${basePath}/init` }) diff --git a/web/app/init/InitPasswordPopup.tsx b/web/app/init/InitPasswordPopup.tsx index 266bb534418424..27cca751ddadca 100644 --- a/web/app/init/InitPasswordPopup.tsx +++ b/web/app/init/InitPasswordPopup.tsx @@ -1,5 +1,4 @@ 'use client' -import type { InitValidateStatusResponse } from '@/models/common' import { Button } from '@langgenius/dify-ui/button' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -7,7 +6,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter } from '@/next/navigation' -import { fetchInitValidateStatus, initValidate } from '@/service/common' +import { consoleClient } from '@/service/console' import { basePath } from '@/utils/var' const InitPasswordPopup = () => { @@ -23,7 +22,7 @@ const InitPasswordPopup = () => { const handleValidation = async () => { setLoading(true) try { - const response = await initValidate({ body: { password } }) + const response = await consoleClient.init.post({ body: { password } }) if (response.result === 'success') { setValidated(true) router.push('/install') // or render setup form @@ -37,7 +36,7 @@ const InitPasswordPopup = () => { } useEffect(() => { - fetchInitValidateStatus().then((res: InitValidateStatusResponse) => { + consoleClient.init.get().then((res) => { if (res.status === 'finished') window.location.href = `${basePath}/install` else setLoading(false) }) diff --git a/web/app/install/installForm.spec.tsx b/web/app/install/installForm.spec.tsx index 4c3fa08f4baf61..c7a50ad41f5578 100644 --- a/web/app/install/installForm.spec.tsx +++ b/web/app/install/installForm.spec.tsx @@ -1,8 +1,7 @@ import type { ReactElement } from 'react' -import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common' +import { consoleClient } from '@/service/console' import { expectLoadingButton } from '@/test/button' import { renderWithConsoleQuery } from '@/test/console/query-data' import { encryptPassword } from '@/utils/encryption' @@ -21,23 +20,32 @@ vi.mock('@/hooks/use-document-title', () => ({ default: vi.fn(), })) -vi.mock('@/service/common', () => ({ - fetchSetupStatus: vi.fn(), - fetchInitValidateStatus: vi.fn(), - setup: vi.fn(), - login: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + setup: { + get: vi.fn(), + post: vi.fn(), + }, + init: { + get: vi.fn(), + }, + login: { + post: vi.fn(), + }, + }, })) -const mockFetchSetupStatus = vi.mocked(fetchSetupStatus) -const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus) -const mockSetup = vi.mocked(setup) -const mockLogin = vi.mocked(login) +const mockFetchSetupStatus = vi.mocked(consoleClient.setup.get) +const mockFetchInitValidateStatus = vi.mocked(consoleClient.init.get) +const mockSetup = vi.mocked(consoleClient.setup.post) +const mockLogin = vi.mocked(consoleClient.login.post) const prepareLoadedState = () => { - mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' } as SetupStatusResponse) + mockFetchSetupStatus.mockResolvedValue({ step: 'not_started' }) mockFetchInitValidateStatus.mockResolvedValue({ status: 'finished', - } as InitValidateStatusResponse) + }) } describe('InstallForm', () => { @@ -161,7 +169,6 @@ describe('InstallForm', () => { await waitFor(() => { expect(mockLogin).toHaveBeenCalledWith({ - url: '/login', body: { email: 'admin@example.com', password: encryptPassword('Password123'), @@ -232,7 +239,7 @@ describe('InstallForm', () => { }) it('should redirect to sign in when setup is finished', async () => { - mockFetchSetupStatus.mockResolvedValue({ step: 'finished' } as SetupStatusResponse) + mockFetchSetupStatus.mockResolvedValue({ step: 'finished' }) render() diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index 77f644c14d718e..6bc92ac10d45c5 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -1,5 +1,4 @@ 'use client' -import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' import { zPostSetupBody } from '@dify/contracts/api/console/setup/zod.gen' import { Button } from '@langgenius/dify-ui/button' import { @@ -24,8 +23,7 @@ import { LICENSE_LINK } from '@/constants/link' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' import { useRouter } from '@/next/navigation' -import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common' -import { consoleQuery } from '@/service/console' +import { consoleClient, consoleQuery } from '@/service/console' import { encryptPassword as encodePassword } from '@/utils/encryption' const accountFormSchema = zPostSetupBody.pick({ email: true, name: true, password: true }).extend({ @@ -52,7 +50,7 @@ const InstallForm = () => { setIsSubmitting(true) try { // First, setup the admin account - await setup({ + await consoleClient.setup.post({ body: { ...value, language: i18n.language, @@ -60,8 +58,7 @@ const InstallForm = () => { }) // Then, automatically login with the same credentials - const loginRes = await login({ - url: '/login', + const loginRes = await consoleClient.login.post({ body: { email: value.email, password: encodePassword(value.password), @@ -82,11 +79,11 @@ const InstallForm = () => { } useEffect(() => { - fetchSetupStatus().then((res: SetupStatusResponse) => { + consoleClient.setup.get().then((res) => { if (res.step === 'finished') { push('/signin') } else { - fetchInitValidateStatus().then((res: InitValidateStatusResponse) => { + consoleClient.init.get().then((res) => { if (res.status === 'not_started') push('/init') }) } diff --git a/web/app/reset-password/__tests__/page.spec.tsx b/web/app/reset-password/__tests__/page.spec.tsx index a1aa6d94924823..dc78ef98173c9c 100644 --- a/web/app/reset-password/__tests__/page.spec.tsx +++ b/web/app/reset-password/__tests__/page.spec.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useLocale } from '#i18n' import { useRouter, useSearchParams } from '@/next/navigation' -import { sendResetPasswordCode } from '@/service/common' +import { consoleClient } from '@/service/console' import ResetPasswordPage from '../page' const mockPush = vi.fn() @@ -27,8 +27,13 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: vi.fn(), })) -vi.mock('@/service/common', () => ({ - sendResetPasswordCode: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + forgotPassword: { + post: vi.fn(), + }, + }, })) describe('ResetPasswordPage', () => { @@ -41,7 +46,10 @@ describe('ResetPasswordPage', () => { vi.mocked(useSearchParams).mockReturnValue( new URLSearchParams() as unknown as ReturnType, ) - vi.mocked(sendResetPasswordCode).mockResolvedValue({ result: 'success', data: 'reset-token' }) + vi.mocked(consoleClient.forgotPassword.post).mockResolvedValue({ + result: 'success', + data: 'reset-token', + }) }) it('labels the email field and submits it with Enter', async () => { @@ -54,7 +62,9 @@ describe('ResetPasswordPage', () => { await user.type(emailInput, 'user@example.com{Enter}') await waitFor(() => { - expect(sendResetPasswordCode).toHaveBeenCalledWith('user@example.com', 'en-US') + expect(consoleClient.forgotPassword.post).toHaveBeenCalledWith({ + body: { email: 'user@example.com', language: 'en-US' }, + }) }) expect(mockPush).toHaveBeenCalledWith( '/reset-password/check-code?token=reset-token&email=user%2540example.com', @@ -73,7 +83,7 @@ describe('ResetPasswordPage', () => { expect(emailInput).toHaveAttribute('aria-invalid', 'true') expect(emailInput).toHaveAccessibleDescription('login.error.emailInValid') expect(emailInput).toHaveFocus() - expect(sendResetPasswordCode).not.toHaveBeenCalled() + expect(consoleClient.forgotPassword.post).not.toHaveBeenCalled() await user.clear(emailInput) await user.click(screen.getByRole('button', { name: 'login.sendVerificationCode' })) @@ -84,6 +94,6 @@ describe('ResetPasswordPage', () => { expect(emailInput).toHaveAttribute('aria-invalid', 'true') expect(emailInput).toHaveAccessibleDescription('login.error.emailEmpty') expect(emailInput).toHaveFocus() - expect(sendResetPasswordCode).not.toHaveBeenCalled() + expect(consoleClient.forgotPassword.post).not.toHaveBeenCalled() }) }) diff --git a/web/app/reset-password/check-code/__tests__/page.spec.tsx b/web/app/reset-password/check-code/__tests__/page.spec.tsx index a1dd4bc6297560..352d2b23c85ab9 100644 --- a/web/app/reset-password/check-code/__tests__/page.spec.tsx +++ b/web/app/reset-password/check-code/__tests__/page.spec.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useLocale } from '#i18n' import { useRouter, useSearchParams } from '@/next/navigation' -import { verifyResetPasswordCode } from '@/service/common' +import { consoleClient } from '@/service/console' import CheckCodePage from '../page' const mockPush = vi.fn() @@ -25,9 +25,16 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: vi.fn(), })) -vi.mock('@/service/common', () => ({ - sendResetPasswordCode: vi.fn(), - verifyResetPasswordCode: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + forgotPassword: { + post: vi.fn(), + validity: { + post: vi.fn(), + }, + }, + }, })) describe('ResetPasswordCheckCodePage', () => { @@ -43,8 +50,8 @@ describe('ResetPasswordCheckCodePage', () => { token: 'email-token', }) as unknown as ReturnType, ) - vi.mocked(verifyResetPasswordCode).mockResolvedValue({ - result: 'success', + vi.mocked(consoleClient.forgotPassword.validity.post).mockResolvedValue({ + email: 'user@example.com', is_valid: true, token: 'verified-token', }) @@ -63,10 +70,12 @@ describe('ResetPasswordCheckCodePage', () => { await user.type(codeInput, '123456{Enter}') await waitFor(() => { - expect(verifyResetPasswordCode).toHaveBeenCalledWith({ - code: '123456', - email: 'user@example.com', - token: 'email-token', + expect(consoleClient.forgotPassword.validity.post).toHaveBeenCalledWith({ + body: { + code: '123456', + email: 'user@example.com', + token: 'email-token', + }, }) }) expect(mockPush).toHaveBeenCalledWith( @@ -87,6 +96,6 @@ describe('ResetPasswordCheckCodePage', () => { expect(codeInput).toHaveAttribute('aria-invalid', 'true') expect(codeInput).toHaveAccessibleDescription(error.textContent ?? '') expect(codeInput).toHaveFocus() - expect(verifyResetPasswordCode).not.toHaveBeenCalled() + expect(consoleClient.forgotPassword.validity.post).not.toHaveBeenCalled() }) }) diff --git a/web/app/reset-password/check-code/page.tsx b/web/app/reset-password/check-code/page.tsx index 55d988f992978a..cee7db31da3da8 100644 --- a/web/app/reset-password/check-code/page.tsx +++ b/web/app/reset-password/check-code/page.tsx @@ -10,7 +10,7 @@ import Countdown from '@/app/components/signin/countdown' import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' -import { sendResetPasswordCode, verifyResetPasswordCode } from '@/service/common' +import { consoleClient } from '@/service/console' type CheckCodeFormValues = { code: string @@ -31,7 +31,7 @@ export default function CheckCode() { if (loading) return try { setLoading(true) - const ret = await verifyResetPasswordCode({ email, code, token }) + const ret = await consoleClient.forgotPassword.validity.post({ body: { email, code, token } }) if (ret.is_valid) { const params = new URLSearchParams(searchParams) params.set('token', encodeURIComponent(ret.token)) @@ -46,10 +46,10 @@ export default function CheckCode() { const resendCode = async () => { try { - const res = await sendResetPasswordCode(email, locale) + const res = await consoleClient.forgotPassword.post({ body: { email, language: locale } }) if (res.result === 'success') { const params = new URLSearchParams(searchParams) - params.set('token', encodeURIComponent(res.data)) + params.set('token', encodeURIComponent(res.data ?? '')) router.replace(`/reset-password/check-code?${params.toString()}`) } } catch (error) { diff --git a/web/app/reset-password/page.tsx b/web/app/reset-password/page.tsx index 7f28da755ef07b..62bb3f9c8827e6 100644 --- a/web/app/reset-password/page.tsx +++ b/web/app/reset-password/page.tsx @@ -11,7 +11,7 @@ import { emailRegex } from '@/config' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' -import { sendResetPasswordCode } from '@/service/common' +import { consoleClient } from '@/service/console' import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '../components/signin/storage' export default function CheckCode() { @@ -28,11 +28,11 @@ export default function CheckCode() { if (loading) return try { setLoading(true) - const res = await sendResetPasswordCode(email, locale) + const res = await consoleClient.forgotPassword.post({ body: { email, language: locale } }) if (res.result === 'success') { setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`) const params = new URLSearchParams(searchParams) - params.set('token', encodeURIComponent(res.data)) + params.set('token', encodeURIComponent(res.data ?? '')) params.set('email', encodeURIComponent(email)) router.push(`/reset-password/check-code?${params.toString()}`) } else { diff --git a/web/app/reset-password/set-password/__tests__/page.spec.tsx b/web/app/reset-password/set-password/__tests__/page.spec.tsx index f8fdfbde7d7a62..59920c8f4a4e59 100644 --- a/web/app/reset-password/set-password/__tests__/page.spec.tsx +++ b/web/app/reset-password/set-password/__tests__/page.spec.tsx @@ -2,7 +2,7 @@ import { act, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' -import { changePasswordWithToken } from '@/service/common' +import { consoleClient } from '@/service/console' import ChangePasswordForm from '../page' const countdownState = vi.hoisted(() => ({ @@ -29,8 +29,15 @@ vi.mock('@/next/link', () => ({ ), })) -vi.mock('@/service/common', () => ({ - changePasswordWithToken: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + forgotPassword: { + resets: { + post: vi.fn(), + }, + }, + }, })) vi.mock('@/hooks/use-document-title', () => ({ @@ -40,7 +47,7 @@ vi.mock('@/hooks/use-document-title', () => ({ const mockReplace = vi.fn() const mockUseRouter = vi.mocked(useRouter) const mockUseSearchParams = vi.mocked(useSearchParams) -const mockChangePasswordWithToken = vi.mocked(changePasswordWithToken) +const mockChangePasswordWithToken = vi.mocked(consoleClient.forgotPassword.resets.post) const mockUseDocumentTitle = vi.mocked(useDocumentTitle) const redirectUrl = '/apps?template-id=template-1&utm_source=dify_blog' @@ -108,7 +115,6 @@ describe('Reset Password Set Password Page', () => { await waitFor(() => { expect(mockChangePasswordWithToken).toHaveBeenCalledWith({ - url: '/forgot-password/resets', body: { token: 'reset-token', new_password: 'ValidPass123!', diff --git a/web/app/reset-password/set-password/page.tsx b/web/app/reset-password/set-password/page.tsx index d9f86543a9fda3..cf9833de5e867d 100644 --- a/web/app/reset-password/set-password/page.tsx +++ b/web/app/reset-password/set-password/page.tsx @@ -19,7 +19,7 @@ import { validPassword } from '@/config' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' -import { changePasswordWithToken } from '@/service/common' +import { consoleClient } from '@/service/console' type PasswordFormValues = { password: string @@ -75,8 +75,7 @@ const ChangePasswordForm = () => { if (isSubmitting) return setIsSubmitting(true) try { - await changePasswordWithToken({ - url: '/forgot-password/resets', + await consoleClient.forgotPassword.resets.post({ body: { token, new_password: formValues.password, diff --git a/web/app/signin/__tests__/one-more-step.spec.tsx b/web/app/signin/__tests__/one-more-step.spec.tsx index 94572f750ca1c4..40fc652434f40e 100644 --- a/web/app/signin/__tests__/one-more-step.spec.tsx +++ b/web/app/signin/__tests__/one-more-step.spec.tsx @@ -4,24 +4,30 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { useRouter, useSearchParams } from '@/next/navigation' -import { useOneMoreStep } from '@/service/use-common' import OneMoreStep from '../one-more-step' +const { mockOneMoreStepHook } = vi.hoisted(() => ({ + mockOneMoreStepHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'account.init.post': (options: unknown) => mockOneMoreStepHook(options), + }) +}) + vi.mock('@/next/navigation', () => ({ useRouter: vi.fn(), useSearchParams: vi.fn(), })) -vi.mock('@/service/use-common', () => ({ - useOneMoreStep: vi.fn(), -})) - const mockReplace = vi.fn() const mockSubmitOneMoreStep = vi.fn() const mockUseRouter = useRouter as unknown as MockedFunction const mockUseSearchParams = useSearchParams as unknown as MockedFunction -const mockUseOneMoreStep = useOneMoreStep as unknown as MockedFunction +const mockUseOneMoreStep = mockOneMoreStepHook describe('OneMoreStep', () => { beforeEach(() => { @@ -37,7 +43,7 @@ describe('OneMoreStep', () => { mockUseOneMoreStep.mockReturnValue({ mutateAsync: mockSubmitOneMoreStep, isPending: false, - } as unknown as ReturnType) + }) mockSubmitOneMoreStep.mockResolvedValue({ result: 'success' }) }) diff --git a/web/app/signin/check-code/__tests__/page.spec.tsx b/web/app/signin/check-code/__tests__/page.spec.tsx index 39a42b8c9591ea..c8330f68010bbe 100644 --- a/web/app/signin/check-code/__tests__/page.spec.tsx +++ b/web/app/signin/check-code/__tests__/page.spec.tsx @@ -4,7 +4,7 @@ import { noop, QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common' +import { consoleClient } from '@/service/console' import { createHttpError } from '@/service/http-error' import { seedSystemFeatures } from '@/test/console/query-data' import CheckCode from '../page' @@ -76,9 +76,16 @@ vi.mock('@/next/navigation', () => ({ vi.mock('@/service/base', () => serviceBaseMocks) -vi.mock('@/service/common', () => ({ - emailLoginWithCode: vi.fn(), - sendEMailLoginCode: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + emailCodeLogin: { + validity: { + post: vi.fn(), + }, + post: vi.fn(), + }, + }, })) vi.mock('@/utils/encryption', () => ({ @@ -139,8 +146,10 @@ const accountProfile: GetAccountProfileResponse = { describe('CheckCode', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(emailLoginWithCode).mockReset().mockResolvedValue({ result: 'success' }) - vi.mocked(sendEMailLoginCode).mockReset() + vi.mocked(consoleClient.emailCodeLogin.validity.post) + .mockReset() + .mockResolvedValue({ result: 'success' }) + vi.mocked(consoleClient.emailCodeLogin.post).mockReset() turnstileWidgets.clear() navigationMocks.searchParams = new URLSearchParams({ email: 'user@example.com', @@ -225,18 +234,13 @@ describe('CheckCode', () => { expect(codeInput).toHaveAttribute('aria-invalid', 'true') expect(codeInput).toHaveAccessibleDescription('login.checkCode.invalidCode') expect(codeInput).toHaveFocus() - expect(emailLoginWithCode).not.toHaveBeenCalled() + expect(consoleClient.emailCodeLogin.validity.post).not.toHaveBeenCalled() }) it('keeps Community verification independent of Turnstile', async () => { const user = userEvent.setup() const queryClient = createQueryClient() - vi.mocked(emailLoginWithCode).mockResolvedValue({ - code: 'invalid_code', - data: '', - message: 'Invalid code', - result: 'fail', - }) + vi.mocked(consoleClient.emailCodeLogin.validity.post).mockResolvedValue({ result: 'fail' }) render( @@ -246,12 +250,14 @@ describe('CheckCode', () => { await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456') await user.click(screen.getByRole('button', { name: 'login.checkCode.verify' })) - expect(emailLoginWithCode).toHaveBeenCalledWith({ - code: '123456', - email: 'user@example.com', - language: expect.any(String), - timezone: 'Asia/Singapore', - token: 'email-login-token', + expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledWith({ + body: { + code: '123456', + email: 'user@example.com', + language: expect.any(String), + timezone: 'Asia/Singapore', + token: 'email-login-token', + }, }) expect(turnstileMocks.render).not.toHaveBeenCalled() }) @@ -259,8 +265,11 @@ describe('CheckCode', () => { it('does not resend while verification is in progress', async () => { const user = userEvent.setup() const queryClient = createQueryClient() - const verificationRequest = createDeferred>>() - vi.mocked(emailLoginWithCode).mockReturnValue(verificationRequest.promise) + const verificationRequest = + createDeferred>>() + vi.mocked(consoleClient.emailCodeLogin.validity.post).mockReturnValue( + verificationRequest.promise, + ) render( @@ -270,22 +279,17 @@ describe('CheckCode', () => { await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456') await user.click(screen.getByRole('button', { name: 'login.checkCode.verify' })) await waitFor(() => { - expect(emailLoginWithCode).toHaveBeenCalledOnce() + expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledOnce() }) const resendButton = screen.getByRole('button', { name: 'resend-code' }) expect(resendButton).toBeDisabled() resendButton.removeAttribute('disabled') fireEvent.click(resendButton) - expect(sendEMailLoginCode).not.toHaveBeenCalled() + expect(consoleClient.emailCodeLogin.post).not.toHaveBeenCalled() act(() => { - verificationRequest.resolve({ - code: 'invalid_code', - data: '', - message: 'Invalid code', - result: 'fail', - }) + verificationRequest.resolve({ result: 'fail' }) }) await waitFor(() => { expect(screen.getByRole('button', { name: 'login.checkCode.verify' })).toBeEnabled() @@ -295,8 +299,9 @@ describe('CheckCode', () => { it('does not verify while resend is in progress', async () => { const user = userEvent.setup() const queryClient = createQueryClient() - const resendRequest = createDeferred>>() - vi.mocked(sendEMailLoginCode).mockReturnValue(resendRequest.promise) + const resendRequest = + createDeferred>>() + vi.mocked(consoleClient.emailCodeLogin.post).mockReturnValue(resendRequest.promise) render( @@ -306,7 +311,7 @@ describe('CheckCode', () => { await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456') await user.click(screen.getByRole('button', { name: 'resend-code' })) await waitFor(() => { - expect(sendEMailLoginCode).toHaveBeenCalledOnce() + expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledOnce() }) const verifyButton = screen.getByRole('button', { name: 'login.checkCode.verify' }) @@ -314,7 +319,7 @@ describe('CheckCode', () => { const form = verifyButton.closest('form') if (!form) throw new Error('Verification form is missing') fireEvent.submit(form) - expect(emailLoginWithCode).not.toHaveBeenCalled() + expect(consoleClient.emailCodeLogin.validity.post).not.toHaveBeenCalled() act(() => { resendRequest.resolve({ data: '', result: 'fail' }) @@ -330,14 +335,9 @@ describe('CheckCode', () => { turnstileMocks.siteKey = 'cloud-site-key' const queryClient = createQueryClient() vi.spyOn(console, 'error').mockImplementation(() => {}) - vi.mocked(emailLoginWithCode) + vi.mocked(consoleClient.emailCodeLogin.validity.post) .mockRejectedValueOnce(new Error('invalid verification code')) - .mockResolvedValueOnce({ - code: 'invalid_code', - data: '', - message: 'Invalid code', - result: 'fail', - }) + .mockResolvedValueOnce({ result: 'fail' }) render( @@ -365,11 +365,11 @@ describe('CheckCode', () => { await user.click(verifyButton) await waitFor(() => { - expect(emailLoginWithCode).toHaveBeenCalledWith( - expect.objectContaining({ + expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenCalledWith({ + body: expect.objectContaining({ turnstile_token: 'signin_code_verify-token-1', }), - ) + }) }) expect(codeInput).toHaveValue('123456') await waitFor(() => { @@ -387,11 +387,11 @@ describe('CheckCode', () => { await user.click(verifyButton) await waitFor(() => { - expect(emailLoginWithCode).toHaveBeenLastCalledWith( - expect.objectContaining({ + expect(consoleClient.emailCodeLogin.validity.post).toHaveBeenLastCalledWith({ + body: expect.objectContaining({ turnstile_token: 'signin_code_verify-token-2', }), - ) + }) }) expect(codeInput).toHaveValue('123456') }) @@ -401,7 +401,10 @@ describe('CheckCode', () => { turnstileMocks.deploymentEdition = 'CLOUD' turnstileMocks.siteKey = 'cloud-site-key' const queryClient = createQueryClient() - vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' }) + vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({ + result: 'success', + data: 'new-login-token', + }) render( @@ -434,11 +437,13 @@ describe('CheckCode', () => { await user.click(await screen.findByRole('button', { name: 'verify-turnstile-signin_code' })) await waitFor(() => { - expect(sendEMailLoginCode).toHaveBeenCalledWith( - 'user@example.com', - expect.any(String), - 'signin_code-token-2', - ) + expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({ + body: { + email: 'user@example.com', + language: expect.any(String), + turnstile_token: 'signin_code-token-2', + }, + }) }) await waitFor(() => { expect( @@ -456,7 +461,10 @@ describe('CheckCode', () => { turnstileMocks.deploymentEdition = 'CLOUD' turnstileMocks.siteKey = 'cloud-site-key' const queryClient = createQueryClient() - vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' }) + vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({ + result: 'success', + data: 'new-login-token', + }) Object.defineProperty(window, 'turnstile', { configurable: true, value: undefined, @@ -491,11 +499,13 @@ describe('CheckCode', () => { await user.click(await screen.findByRole('button', { name: 'verify-turnstile-signin_code' })) await waitFor(() => { - expect(sendEMailLoginCode).toHaveBeenCalledWith( - 'user@example.com', - expect.any(String), - 'signin_code-token-1', - ) + expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({ + body: { + email: 'user@example.com', + language: expect.any(String), + turnstile_token: 'signin_code-token-1', + }, + }) }) }) @@ -503,7 +513,10 @@ describe('CheckCode', () => { const user = userEvent.setup() const queryClient = createQueryClient() turnstileMocks.siteKey = 'site-key-not-used-outside-cloud' - vi.mocked(sendEMailLoginCode).mockResolvedValue({ result: 'success', data: 'new-login-token' }) + vi.mocked(consoleClient.emailCodeLogin.post).mockResolvedValue({ + result: 'success', + data: 'new-login-token', + }) render( @@ -516,11 +529,9 @@ describe('CheckCode', () => { expect(resendButton).toBeEnabled() await user.click(resendButton) - expect(sendEMailLoginCode).toHaveBeenCalledWith( - 'user@example.com', - expect.any(String), - undefined, - ) + expect(consoleClient.emailCodeLogin.post).toHaveBeenCalledWith({ + body: { email: 'user@example.com', language: expect.any(String) }, + }) }) describe('Post-login profile bootstrap', () => { diff --git a/web/app/signin/check-code/page.tsx b/web/app/signin/check-code/page.tsx index b06e14fc3cdbc0..e78a63385427cd 100644 --- a/web/app/signin/check-code/page.tsx +++ b/web/app/signin/check-code/page.tsx @@ -10,13 +10,12 @@ import { useLocale } from '#i18n' import { trackEvent } from '@/app/components/base/amplitude' import Countdown from '@/app/components/signin/countdown' import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage' -import { toast } from '@/app/notifications' import { TURNSTILE_SITE_KEY } from '@/config' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' -import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common' +import { consoleClient } from '@/service/console' import { encryptVerificationCode } from '@/utils/encryption' import { replaceLoginRedirect } from '@/utils/login-redirect.client' import { getBrowserTimezone } from '@/utils/timezone' @@ -62,13 +61,15 @@ export default function CheckCode() { setLoading(true) shouldResetTurnstile = isTurnstileRequired - const ret = await emailLoginWithCode({ - email, - code: encryptVerificationCode(code), - token, - language, - timezone: getBrowserTimezone(), - ...(isTurnstileRequired ? { turnstile_token: verifyTurnstileToken } : {}), + const ret = await consoleClient.emailCodeLogin.validity.post({ + body: { + email, + code: encryptVerificationCode(code), + token, + language, + timezone: getBrowserTimezone(), + ...(isTurnstileRequired ? { turnstile_token: verifyTurnstileToken } : {}), + }, }) if (ret.result === 'success') { // Track login success event @@ -85,8 +86,6 @@ export default function CheckCode() { await queryClient.query(profileQueryOptions) replaceLoginRedirect(resolvePostLoginRedirect(searchParams), router.replace, basePath) } - } else { - toast.error(ret.data) } } catch (error) { console.error(error) @@ -106,11 +105,13 @@ export default function CheckCode() { const resendCode = async (turnstileToken?: string) => { setIsResending(true) try { - const ret = await sendEMailLoginCode( - email, - locale, - isTurnstileRequired ? turnstileToken : undefined, - ) + const ret = await consoleClient.emailCodeLogin.post({ + body: { + email, + language: locale, + ...(isTurnstileRequired ? { turnstile_token: turnstileToken } : {}), + }, + }) if (ret.result === 'success') { setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`) setCountdownGeneration((value) => value + 1) diff --git a/web/app/signin/components/__tests__/mail-and-code-auth.spec.tsx b/web/app/signin/components/__tests__/mail-and-code-auth.spec.tsx index 8eeacb304b0606..ba6cf9b1e93220 100644 --- a/web/app/signin/components/__tests__/mail-and-code-auth.spec.tsx +++ b/web/app/signin/components/__tests__/mail-and-code-auth.spec.tsx @@ -69,8 +69,13 @@ vi.mock('@/config', async () => { } }) -vi.mock('@/service/common', () => ({ - sendEMailLoginCode: (...args: unknown[]) => mocks.sendEMailLoginCode(...args), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + emailCodeLogin: { + post: (...args: unknown[]) => mocks.sendEMailLoginCode(...args), + }, + }, })) describe('MailAndCodeAuth', () => { @@ -149,11 +154,9 @@ describe('MailAndCodeAuth', () => { await user.click(screen.getByRole('button', { name: 'login.signup.verifyMail' })) await waitFor(() => { - expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith( - 'user@example.com', - 'en-US', - 'turnstile-token', - ) + expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith({ + body: { email: 'user@example.com', language: 'en-US', turnstile_token: 'turnstile-token' }, + }) }) expect(mocks.push).toHaveBeenCalledWith(expect.stringContaining('/signin/check-code?')) }) @@ -177,11 +180,13 @@ describe('MailAndCodeAuth', () => { await user.click(continueButton) await waitFor(() => { - expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith( - 'user@example.com', - 'en-US', - 'consumed-turnstile-token', - ) + expect(mocks.sendEMailLoginCode).toHaveBeenCalledWith({ + body: { + email: 'user@example.com', + language: 'en-US', + turnstile_token: 'consumed-turnstile-token', + }, + }) }) await waitFor(() => { expect(continueButton).toBeDisabled() @@ -195,11 +200,13 @@ describe('MailAndCodeAuth', () => { await user.click(continueButton) await waitFor(() => { - expect(mocks.sendEMailLoginCode).toHaveBeenLastCalledWith( - 'user@example.com', - 'en-US', - 'fresh-turnstile-token', - ) + expect(mocks.sendEMailLoginCode).toHaveBeenLastCalledWith({ + body: { + email: 'user@example.com', + language: 'en-US', + turnstile_token: 'fresh-turnstile-token', + }, + }) }) expect(mocks.push).toHaveBeenCalledWith(expect.stringContaining('/signin/check-code?')) }) diff --git a/web/app/signin/components/mail-and-code-auth.tsx b/web/app/signin/components/mail-and-code-auth.tsx index b57f56cbee2e82..3eb5d3de7ba355 100644 --- a/web/app/signin/components/mail-and-code-auth.tsx +++ b/web/app/signin/components/mail-and-code-auth.tsx @@ -10,7 +10,7 @@ import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/si import { emailRegex, TURNSTILE_SITE_KEY } from '@/config' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter, useSearchParams } from '@/next/navigation' -import { sendEMailLoginCode } from '@/service/common' +import { consoleClient } from '@/service/console' import Turnstile from './turnstile' type MailAndCodeAuthProps = { @@ -39,11 +39,13 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) { try { setLoading(true) shouldResetTurnstile = isTurnstileRequired - const ret = await sendEMailLoginCode( - email, - locale, - isTurnstileRequired ? turnstileToken : undefined, - ) + const ret = await consoleClient.emailCodeLogin.post({ + body: { + email, + language: locale, + ...(isTurnstileRequired ? { turnstile_token: turnstileToken } : {}), + }, + }) if (ret.result === 'success') { setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`) const params = new URLSearchParams(searchParams) diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index 48d890e2eaeccd..2aeee13575eb6b 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -13,9 +13,7 @@ import { toast } from '@/app/notifications' import { emailRegex } from '@/config' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' -import { login } from '@/service/common' -import { consoleQuery } from '@/service/console' -import { setWebAppAccessToken } from '@/service/webapp-auth' +import { consoleClient, consoleQuery } from '@/service/console' import { encryptPassword } from '@/utils/encryption' import { replaceLoginRedirect } from '@/utils/login-redirect.client' import { basePath } from '@/utils/var' @@ -52,15 +50,8 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup }: MailAndP } if (isInvite) loginData.invite_token = decodeURIComponent(searchParams.get('invite_token') as string) - const res = await login({ - url: '/login', - body: loginData, - }) + const res = await consoleClient.login.post({ body: loginData }) if (res.result === 'success') { - if (res?.data?.access_token) { - // Track login success event - setWebAppAccessToken(res.data.access_token) - } trackEvent('user_login_success', { method: 'email_password', is_invite: isInvite, diff --git a/web/app/signin/invite-settings/__tests__/page.spec.tsx b/web/app/signin/invite-settings/__tests__/page.spec.tsx index ff98926162ee5d..6ad5a105428244 100644 --- a/web/app/signin/invite-settings/__tests__/page.spec.tsx +++ b/web/app/signin/invite-settings/__tests__/page.spec.tsx @@ -1,15 +1,19 @@ +import type { useQuery } from '@tanstack/react-query' import type { MockedFunction } from 'vite-plus/test' -import { useQuery } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { useLocale } from '#i18n' import { useRouter, useSearchParams } from '@/next/navigation' -import { activateMember } from '@/service/common' -import { useInvitationCheck } from '@/service/use-common' +import { consoleClient } from '@/service/console' import { getBrowserTimezone } from '@/utils/timezone' import InviteSettingsPage from '../page' +const { mockInvitationCheckHook, mockProfileQuery } = vi.hoisted(() => ({ + mockInvitationCheckHook: vi.fn(), + mockProfileQuery: vi.fn(), +})) + vi.mock('react-i18next', async () => { const actual = await vi.importActual('react-i18next') const { createReactI18nextMock } = await import('@/test/i18n-mock') @@ -27,7 +31,12 @@ vi.mock('@tanstack/react-query', async () => { await vi.importActual('@tanstack/react-query') return { ...actual, - useQuery: vi.fn(), + // The invitation check is answered separately from the account profile query. + useQuery: (options: { queryKey: unknown[] }) => + Array.isArray(options.queryKey[0]) && + options.queryKey[0].join('.') === 'console.activate.check.get' + ? mockInvitationCheckHook(options) + : mockProfileQuery(options), useQueryClient: vi.fn(() => ({ resetQueries: vi.fn(), })), @@ -62,12 +71,13 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: vi.fn(), })) -vi.mock('@/service/common', () => ({ - activateMember: vi.fn(), -})) - -vi.mock('@/service/use-common', () => ({ - useInvitationCheck: vi.fn(), +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), + consoleClient: { + activate: { + post: vi.fn(), + }, + }, })) vi.mock('@/utils/timezone', () => ({ @@ -84,11 +94,9 @@ const mockRefetch = vi.fn() const mockUseLocale = useLocale as unknown as MockedFunction const mockUseRouter = useRouter as unknown as MockedFunction const mockUseSearchParams = useSearchParams as unknown as MockedFunction -const mockActivateMember = activateMember as unknown as MockedFunction -const mockUseQuery = vi.mocked(useQuery) -const mockUseInvitationCheck = useInvitationCheck as unknown as MockedFunction< - typeof useInvitationCheck -> +const mockActivateMember = vi.mocked(consoleClient.activate.post) +const mockUseQuery = mockProfileQuery +const mockUseInvitationCheck = mockInvitationCheckHook const mockGetBrowserTimezone = getBrowserTimezone as unknown as MockedFunction< typeof getBrowserTimezone > @@ -117,7 +125,7 @@ describe('InviteSettingsPage', () => { }, }, refetch: mockRefetch, - } as unknown as ReturnType) + }) mockUseQuery.mockReturnValue({ data: { profile: { @@ -152,7 +160,7 @@ describe('InviteSettingsPage', () => { }, }, refetch: mockRefetch, - } as unknown as ReturnType) + }) render() @@ -182,7 +190,6 @@ describe('InviteSettingsPage', () => { await waitFor(() => { expect(mockActivateMember).toHaveBeenCalledWith({ - url: '/activate', body: { token: 'invite-token', name: 'Invitee', @@ -204,7 +211,6 @@ describe('InviteSettingsPage', () => { await waitFor(() => { expect(mockActivateMember).toHaveBeenCalledWith({ - url: '/activate', body: { token: 'invite-token', name: 'Invitee', @@ -229,7 +235,7 @@ describe('InviteSettingsPage', () => { }, }, refetch: mockRefetch, - } as unknown as ReturnType) + }) render() @@ -238,7 +244,6 @@ describe('InviteSettingsPage', () => { await waitFor(() => { expect(mockActivateMember).toHaveBeenCalledWith({ - url: '/activate', body: { token: 'invite-token', }, @@ -259,7 +264,7 @@ describe('InviteSettingsPage', () => { }, }, refetch: mockRefetch, - } as unknown as ReturnType) + }) render() @@ -268,7 +273,6 @@ describe('InviteSettingsPage', () => { await waitFor(() => { expect(mockActivateMember).toHaveBeenCalledWith({ - url: '/activate', body: { token: 'invite-token', }, @@ -290,7 +294,7 @@ describe('InviteSettingsPage', () => { }, }, refetch: mockRefetch, - } as unknown as ReturnType) + }) render() @@ -299,7 +303,6 @@ describe('InviteSettingsPage', () => { await waitFor(() => { expect(mockActivateMember).toHaveBeenCalledWith({ - url: '/activate', body: { token: 'invite-token', name: 'Invitee', diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index 2882407050656e..9fb518203426b6 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -28,10 +28,8 @@ import { setLocaleOnClient } from '@/i18n/client' import { languages } from '@/i18n/language' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' -import { activateMember } from '@/service/common' -import { consoleQuery } from '@/service/console' +import { consoleClient, consoleQuery } from '@/service/console' import { isUnauthorizedError } from '@/service/http-error' -import { useInvitationCheck } from '@/service/use-common' import { replaceLoginRedirect } from '@/utils/login-redirect.client' import { getBrowserTimezone, timezones } from '@/utils/timezone' import { basePath } from '@/utils/var' @@ -106,7 +104,13 @@ export default function InviteSettingsPage() { token, }, } - const { data: checkRes, refetch: recheck } = useInvitationCheck(checkParams.params, !!token) + const { data: checkRes, refetch: recheck } = useQuery( + consoleQuery.activate.check.get.queryOptions({ + input: { query: checkParams.params }, + enabled: !!token, + retry: false, + }), + ) const isInvitationForCurrentAccount = isInvitationForAccount( checkRes?.data?.email, userResp?.profile.email, @@ -156,10 +160,7 @@ export default function InviteSettingsPage() { : { token, } - const res = await activateMember({ - url: '/activate', - body, - }) + const res = await consoleClient.activate.post({ body }) if (res.result === 'success') { // Tokens are now stored in cookies by the backend if (requiresAccountSetup) await setLocaleOnClient(language!, false) diff --git a/web/app/signin/normal-form.tsx b/web/app/signin/normal-form.tsx index ec95dab3b73327..bf2ec7e84cd0cb 100644 --- a/web/app/signin/normal-form.tsx +++ b/web/app/signin/normal-form.tsx @@ -10,7 +10,7 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' -import { invitationCheck } from '@/service/common' +import { consoleClient } from '@/service/console' import { isUnauthorizedError } from '@/service/http-error' import { replaceLoginRedirect } from '@/utils/login-redirect.client' import { basePath } from '@/utils/var' @@ -58,13 +58,7 @@ function NormalForm() { isError: isInviteCheckError, } = useQuery({ queryKey: ['signin', 'invite-check', inviteToken], - queryFn: () => - invitationCheck({ - url: '/activate/check', - params: { - token: inviteToken, - }, - }), + queryFn: () => consoleClient.activate.check.get({ query: { token: inviteToken } }), enabled: isInviteLink, retry: false, refetchOnWindowFocus: false, diff --git a/web/app/signin/one-more-step.tsx b/web/app/signin/one-more-step.tsx index d8ad7297c0e785..73cd12568f70a2 100644 --- a/web/app/signin/one-more-step.tsx +++ b/web/app/signin/one-more-step.tsx @@ -15,7 +15,7 @@ import { SelectLabel, SelectTrigger, } from '@langgenius/dify-ui/select' -import { useQueryClient } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useReducer } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/notifications' @@ -26,7 +26,6 @@ import Link from '@/next/link' import { useRouter, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' import { getHttpErrorStatus } from '@/service/http-error' -import { useOneMoreStep } from '@/service/use-common' import { replaceLoginRedirect } from '@/utils/login-redirect.client' import { timezones } from '@/utils/timezone' import { basePath } from '@/utils/var' @@ -77,7 +76,9 @@ const OneMoreStep = () => { interface_language: 'en-US', timezone: 'Asia/Shanghai', }) - const { mutateAsync: submitOneMoreStep, isPending } = useOneMoreStep() + const { mutateAsync: submitOneMoreStep, isPending } = useMutation( + consoleQuery.account.init.post.mutationOptions(), + ) const selectedLanguage = LANGUAGE_OPTIONS.find((item) => item.value === state.interface_language) const selectedTimezone = TIMEZONE_OPTIONS.find((item) => item.value === state.timezone) @@ -95,9 +96,11 @@ const OneMoreStep = () => { if (isPending) return try { await submitOneMoreStep({ - invitation_code: state.invitation_code.trim(), - interface_language: state.interface_language, - timezone: state.timezone, + body: { + invitation_code: state.invitation_code.trim(), + interface_language: state.interface_language, + timezone: state.timezone, + }, }) await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() }) replaceLoginRedirect(resolvePostLoginRedirect(searchParams), router.replace, basePath) diff --git a/web/app/signup/check-code/__tests__/page.spec.tsx b/web/app/signup/check-code/__tests__/page.spec.tsx index a92209311842e4..54219bb309a6f2 100644 --- a/web/app/signup/check-code/__tests__/page.spec.tsx +++ b/web/app/signup/check-code/__tests__/page.spec.tsx @@ -3,9 +3,21 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useLocale } from '#i18n' import { useRouter, useSearchParams } from '@/next/navigation' -import { useMailValidity, useSendMail } from '@/service/use-common' import CheckCode from '../page' +const { mockSendMailHook, mockMailValidityHook } = vi.hoisted(() => ({ + mockSendMailHook: vi.fn(), + mockMailValidityHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'emailRegister.sendEmail.post': (options: unknown) => mockSendMailHook(options), + 'emailRegister.validity.post': (options: unknown) => mockMailValidityHook(options), + }) +}) + const mockBack = vi.fn() const mockPush = vi.fn() const mockReplace = vi.fn() @@ -34,16 +46,11 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: vi.fn(), })) -vi.mock('@/service/use-common', () => ({ - useMailValidity: vi.fn(), - useSendMail: vi.fn(), -})) - const mockUseLocale = useLocale as unknown as MockedFunction const mockUseRouter = useRouter as unknown as MockedFunction const mockUseSearchParams = useSearchParams as unknown as MockedFunction -const mockUseMailValidity = useMailValidity as unknown as MockedFunction -const mockUseSendMail = useSendMail as unknown as MockedFunction +const mockUseMailValidity = mockMailValidityHook +const mockUseSendMail = mockSendMailHook describe('Signup Check Code Page', () => { beforeEach(() => { @@ -62,10 +69,10 @@ describe('Signup Check Code Page', () => { ) mockUseMailValidity.mockReturnValue({ mutateAsync: mockVerifyCode, - } as unknown as ReturnType) + }) mockUseSendMail.mockReturnValue({ mutateAsync: mockSubmitMail, - } as unknown as ReturnType) + }) mockVerifyCode.mockResolvedValue({ is_valid: true, token: 'verified-token' }) }) @@ -88,9 +95,11 @@ describe('Signup Check Code Page', () => { await waitFor(() => { expect(mockVerifyCode).toHaveBeenCalledWith({ - code: '123456', - email: 'user@example.com', - token: 'signup-token', + body: { + code: '123456', + email: 'user@example.com', + token: 'signup-token', + }, }) }) expect(mockVerifyCode).toHaveBeenCalledTimes(1) diff --git a/web/app/signup/check-code/page.tsx b/web/app/signup/check-code/page.tsx index 55d640e9c22bb7..4439016e04b411 100644 --- a/web/app/signup/check-code/page.tsx +++ b/web/app/signup/check-code/page.tsx @@ -1,9 +1,9 @@ 'use client' -import type { MailSendResponse, MailValidityResponse } from '@/service/use-common' import { Button } from '@langgenius/dify-ui/button' import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' +import { useMutation } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' @@ -11,7 +11,7 @@ import Countdown from '@/app/components/signin/countdown' import { toast } from '@/app/notifications' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' -import { useMailValidity, useSendMail } from '@/service/use-common' +import { consoleQuery } from '@/service/console' type CheckCodeFormValues = { code: string @@ -25,8 +25,12 @@ export default function CheckCode() { const [token, setToken] = useState(() => decodeURIComponent(searchParams.get('token') as string)) const [loading, setLoading] = useState(false) const locale = useLocale() - const { mutateAsync: submitMail } = useSendMail() - const { mutateAsync: verifyCode } = useMailValidity() + const { mutateAsync: submitMail } = useMutation( + consoleQuery.emailRegister.sendEmail.post.mutationOptions(), + ) + const { mutateAsync: verifyCode } = useMutation( + consoleQuery.emailRegister.validity.post.mutationOptions(), + ) const pageTitle = t(($) => $['checkCode.checkYourEmail'], { ns: 'login' }) useDocumentTitle(pageTitle) @@ -34,10 +38,10 @@ export default function CheckCode() { if (loading) return try { setLoading(true) - const res = await verifyCode({ email, code, token }) - if ((res as MailValidityResponse).is_valid) { + const res = await verifyCode({ body: { email, code, token } }) + if (res.is_valid) { const params = new URLSearchParams(searchParams) - params.set('token', encodeURIComponent((res as MailValidityResponse).token)) + params.set('token', encodeURIComponent(res.token)) router.push(`/signup/set-password?${params.toString()}`) } else { toast.error(t(($) => $['checkCode.invalidCode'], { ns: 'login' })) @@ -51,10 +55,10 @@ export default function CheckCode() { const resendCode = async () => { try { - const res = await submitMail({ email, language: locale }) - if ((res as MailSendResponse).result === 'success') { + const res = await submitMail({ body: { email, language: locale } }) + if (res.result === 'success') { const params = new URLSearchParams(searchParams) - const newToken = (res as MailSendResponse)?.data + const newToken = res.data params.set('token', encodeURIComponent(newToken)) setToken(newToken) router.replace(`/signup/check-code?${params.toString()}`) diff --git a/web/app/signup/components/input-mail.spec.tsx b/web/app/signup/components/input-mail.spec.tsx index dbfebc1934c319..8e0c2055a681c2 100644 --- a/web/app/signup/components/input-mail.spec.tsx +++ b/web/app/signup/components/input-mail.spec.tsx @@ -4,10 +4,20 @@ import userEvent from '@testing-library/user-event' import * as React from 'react' import { useLocale } from '#i18n' import { useSearchParams } from '@/next/navigation' -import { useSendMail } from '@/service/use-common' import { renderWithConsoleQuery } from '@/test/console/query-data' import SignupEmailForm from './input-mail' +const { mockSendMailHook } = vi.hoisted(() => ({ + mockSendMailHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'emailRegister.sendEmail.post': (options: unknown) => mockSendMailHook(options), + }) +}) + const mockSubmitMail = vi.fn() const mockOnSuccess = vi.fn() @@ -40,15 +50,11 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: vi.fn(), })) -vi.mock('@/service/use-common', () => ({ - useSendMail: vi.fn(), -})) - -type UseSendMailResult = ReturnType +type UseSendMailResult = ReturnType const mockUseLocale = useLocale as unknown as MockedFunction const mockUseSearchParams = useSearchParams as unknown as MockedFunction -const mockUseSendMail = useSendMail as unknown as MockedFunction +const mockUseSendMail = mockSendMailHook const renderForm = ({ brandingEnabled = false, @@ -122,8 +128,10 @@ describe('InputMail Form', () => { await user.type(input, 'test@example.com{Enter}') expect(mockSubmitMail).toHaveBeenCalledWith({ - email: 'test@example.com', - language: 'en-US', + body: { + email: 'test@example.com', + language: 'en-US', + }, }) expect(mockSubmitMail).toHaveBeenCalledTimes(1) diff --git a/web/app/signup/components/input-mail.tsx b/web/app/signup/components/input-mail.tsx index 0227e5e7e621d5..a6a285b9ad5ec4 100644 --- a/web/app/signup/components/input-mail.tsx +++ b/web/app/signup/components/input-mail.tsx @@ -1,10 +1,9 @@ 'use client' -import type { MailSendResponse } from '@/service/use-common' import { Button } from '@langgenius/dify-ui/button' import { Field, FieldError, FieldLabel, FieldValidity } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' import Split from '@/app/signin/split' @@ -12,7 +11,7 @@ import { emailRegex } from '@/config' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import Link from '@/next/link' import { useSearchParams } from '@/next/navigation' -import { useSendMail } from '@/service/use-common' +import { consoleQuery } from '@/service/console' type Props = { onSuccess: (email: string, payload: string) => void @@ -30,13 +29,14 @@ export default function SignupEmailForm({ onSuccess }: Props) { const signinHref = queryString ? `/signin?${queryString}` : '/signin' const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const { mutateAsync: submitMail, isPending } = useSendMail() + const { mutateAsync: submitMail, isPending } = useMutation( + consoleQuery.emailRegister.sendEmail.post.mutationOptions(), + ) const handleSubmit = async (email: string) => { if (isPending) return - const res = await submitMail({ email, language: locale }) - if ((res as MailSendResponse).result === 'success') - onSuccess(email, (res as MailSendResponse).data) + const res = await submitMail({ body: { email, language: locale } }) + if (res.result === 'success') onSuccess(email, res.data ?? '') } return ( diff --git a/web/app/signup/set-password/__tests__/page.spec.tsx b/web/app/signup/set-password/__tests__/page.spec.tsx index 8febabd4cc281d..800cc2e1c6f8a9 100644 --- a/web/app/signup/set-password/__tests__/page.spec.tsx +++ b/web/app/signup/set-password/__tests__/page.spec.tsx @@ -7,11 +7,21 @@ import Cookies from 'js-cookie' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { useLocale } from '#i18n' import { useRouter, useSearchParams } from '@/next/navigation' -import { useMailRegister } from '@/service/use-common' import { seedSystemFeatures } from '@/test/console/query-data' import { getBrowserTimezone } from '@/utils/timezone' import ChangePasswordForm from '../page' +const { mockMailRegisterHook } = vi.hoisted(() => ({ + mockMailRegisterHook: vi.fn(), +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'emailRegister.post': (options: unknown) => mockMailRegisterHook(options), + }) +}) + const { mockRememberCreateAppExternalAttribution, mockRememberRegistrationSuccess, @@ -32,10 +42,6 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: vi.fn(), })) -vi.mock('@/service/use-common', () => ({ - useMailRegister: vi.fn(), -})) - vi.mock('@/utils/timezone', () => ({ getBrowserTimezone: vi.fn(), })) @@ -59,7 +65,7 @@ const mockReplace = vi.fn() const mockUseLocale = useLocale as unknown as MockedFunction const mockUseSearchParams = useSearchParams as unknown as MockedFunction const mockUseRouter = useRouter as unknown as MockedFunction -const mockUseMailRegister = useMailRegister as unknown as MockedFunction +const mockUseMailRegister = mockMailRegisterHook const mockGetBrowserTimezone = getBrowserTimezone as unknown as MockedFunction< typeof getBrowserTimezone > @@ -89,7 +95,7 @@ describe('Signup Set Password Page', () => { mockUseMailRegister.mockReturnValue({ mutateAsync: mockRegister, isPending: false, - } as unknown as ReturnType) + }) mockGetBrowserTimezone.mockReturnValue('Asia/Shanghai') mockRegister.mockResolvedValue({ result: 'fail', data: {} }) }) @@ -117,11 +123,13 @@ describe('Signup Set Password Page', () => { await waitFor(() => { expect(mockRegister).toHaveBeenCalledWith({ - token: 'register-token', - new_password: 'ValidPass123!', - password_confirm: 'ValidPass123!', - language: 'zh-Hans', - timezone: 'Asia/Shanghai', + body: { + token: 'register-token', + new_password: 'ValidPass123!', + password_confirm: 'ValidPass123!', + language: 'zh-Hans', + timezone: 'Asia/Shanghai', + }, }) }) }) diff --git a/web/app/signup/set-password/page.tsx b/web/app/signup/set-password/page.tsx index 1d933b5df39850..35c9fe66234632 100644 --- a/web/app/signup/set-password/page.tsx +++ b/web/app/signup/set-password/page.tsx @@ -1,6 +1,5 @@ 'use client' import type { FormActions } from '@langgenius/dify-ui/form' -import type { MailRegisterResponse } from '@/service/use-common' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { @@ -12,7 +11,7 @@ import { } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Input } from '@langgenius/dify-ui/input' -import { useQueryClient } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import Cookies from 'js-cookie' import { useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' @@ -23,7 +22,6 @@ import { validPassword } from '@/config' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' -import { useMailRegister } from '@/service/use-common' import { rememberCreateAppExternalAttribution } from '@/utils/create-app-tracking' import { sendGAEvent } from '@/utils/gtag' import { replaceLoginRedirect } from '@/utils/login-redirect.client' @@ -56,7 +54,9 @@ const ChangePasswordForm = () => { const formActionsRef = useRef(null) const confirmPasswordRef = useRef(null) - const { mutateAsync: register, isPending } = useMailRegister() + const { mutateAsync: register, isPending } = useMutation( + consoleQuery.emailRegister.post.mutationOptions(), + ) const pageTitle = t(($) => $.changePassword, { ns: 'login' }) useDocumentTitle(pageTitle) @@ -65,13 +65,15 @@ const ChangePasswordForm = () => { if (isPending) return try { const res = await register({ - token, - new_password: formValues.password, - password_confirm: formValues.confirmPassword, - language: locale, - timezone: getBrowserTimezone(), + body: { + token, + new_password: formValues.password, + password_confirm: formValues.confirmPassword, + language: locale, + timezone: getBrowserTimezone(), + }, }) - const { result } = res as MailRegisterResponse + const { result } = res if (result === 'success') { const utmInfo = parseUtmInfo() rememberCreateAppExternalAttribution({ utmInfo }) diff --git a/web/context/modal-context-provider.tsx b/web/context/modal-context-provider.tsx index da470ea3e1f547..6fa3599c09ff6a 100644 --- a/web/context/modal-context-provider.tsx +++ b/web/context/modal-context-provider.tsx @@ -125,9 +125,10 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) => }, [showExternalKnowledgeAPIModal]) const handleSaveExternalApiModal = useCallback( - async (updatedFormValue: CreateExternalAPIReq) => { + // Callers refresh their API list; the created API is not passed back. + async () => { if (showExternalKnowledgeAPIModal?.onSaveCallback) - showExternalKnowledgeAPIModal.onSaveCallback(updatedFormValue) + showExternalKnowledgeAPIModal.onSaveCallback() setShowExternalKnowledgeAPIModal(null) }, [showExternalKnowledgeAPIModal], diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx index 29b5a741262043..592f47a5860edd 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx @@ -205,8 +205,31 @@ vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: trackEventMock, })) +// The agent tool provider catalog reads these lists; the page tests start with none. +const { emptyToolList } = vi.hoisted(() => ({ + emptyToolList: (path: string) => ({ + get: { + queryOptions: (options?: object) => ({ + ...options, + queryKey: [path], + queryFn: async () => [], + }), + }, + }), +})) + vi.mock('@/service/console', () => ({ consoleQuery: { + workspaces: { + current: { + tools: { + builtin: emptyToolList('tools-builtin'), + api: emptyToolList('tools-api'), + workflow: emptyToolList('tools-workflow'), + mcp: emptyToolList('tools-mcp'), + }, + }, + }, systemFeatures: { get: { queryKey: () => ['system-features'], diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx index 2c153d9b91299b..3cead400987acf 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx @@ -183,59 +183,75 @@ vi.mock('@/service/use-plugins', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: toolProviderState.builtInTools }), - useAllCustomTools: () => ({ data: toolProviderState.customTools }), - useAllMCPTools: () => ({ data: toolProviderState.mcpTools }), - useAllWorkflowTools: () => ({ data: toolProviderState.workflowTools }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: toolProviderState.builtInTools }), + 'workspaces.current.tools.api.get': () => ({ data: toolProviderState.customTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: toolProviderState.mcpTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: toolProviderState.workflowTools }), + }) +}) -vi.mock('@/service/console', () => ({ - consoleQuery: { - agent: { - get: { - key: () => ['agents'], +vi.mock('@/service/console', async () => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleQuery: { + workspaces: { + current: { + tools: { + builtin: { get: consoleOperationStub('workspaces.current.tools.builtin.get') }, + api: { get: consoleOperationStub('workspaces.current.tools.api.get') }, + workflow: { get: consoleOperationStub('workspaces.current.tools.workflow.get') }, + mcp: { get: consoleOperationStub('workspaces.current.tools.mcp.get') }, + }, + }, }, - byAgentId: { + agent: { get: { - queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-detail', - input.params.agent_id, - ], + key: () => ['agents'], }, - apiAccess: { + byAgentId: { get: { queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-api-access', + 'agent-detail', input.params.agent_id, ], }, - }, - composer: { - get: { - queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-composer', - input.params.agent_id, - ], + apiAccess: { + get: { + queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ + 'agent-api-access', + input.params.agent_id, + ], + }, }, - put: { - mutationOptions: composerPutMutationOptions, + composer: { + get: { + queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ + 'agent-composer', + input.params.agent_id, + ], + }, + put: { + mutationOptions: composerPutMutationOptions, + }, }, - }, - publish: { - post: { - mutationOptions: publishAgentMutationOptions, + publish: { + post: { + mutationOptions: publishAgentMutationOptions, + }, }, - }, - versions: { - get: { - key: () => ['agent-versions'], + versions: { + get: { + key: () => ['agent-versions'], + }, }, }, }, }, - }, -})) + } +}) function renderUseAgentConfigureSync({ agentName = 'Agent', diff --git a/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx index b5d1246bdd9db9..64df485a1f5dd2 100644 --- a/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx @@ -230,12 +230,15 @@ vi.mock('@/context/workspace-state', async () => { })) }) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockBuiltInTools }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: mockMCPTools.current }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockBuiltInTools }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: mockMCPTools.current }), + }) +}) vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/content-moderation.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/content-moderation.tsx index cff0a4e2056186..b1db60f186d082 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/content-moderation.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/content-moderation.tsx @@ -5,6 +5,7 @@ import type { Features } from '@/app/components/base/features/types' import type { ModerationConfig } from '@/models/debug' import { Button } from '@langgenius/dify-ui/button' import { Switch } from '@langgenius/dify-ui/switch' +import { useQuery } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '#i18n' @@ -15,7 +16,7 @@ import { useAppFeatures, useSetAppFeatures, } from '@/features/agent-v2/agent-composer/store-modules/app-features' -import { useCodeBasedExtensions } from '@/service/use-common' +import { codeBasedExtensionsQueryOptions } from '@/features/code-based-extension/query' import { ConfigureSection } from '../common/section' import { useAgentOrchestrateReadOnly } from '../read-only-context' @@ -38,7 +39,7 @@ function AgentContentModerationSettingsContent() { const setAppFeatures = useSetAppFeatures() const { setShowModerationSettingModal } = useModalContext() const moderation = useFeatures((state) => state.features.moderation) - const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation') + const { data: codeBasedExtensionList } = useQuery(codeBasedExtensionsQueryOptions('moderation')) const panelId = 'agent-configure-content-moderation-panel' const persistModeration = useCallback( diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx index 6f540c9c588c4d..8caf30f14f1a4f 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx @@ -69,51 +69,22 @@ vi.mock('@/utils/download', () => ({ downloadUrl: mocks.downloadUrl, })) -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ data: mocks.fileUploadConfig }), -})) - -vi.mock('@/service/console', () => ({ - consoleQuery: { - systemFeatures: { - get: { - queryKey: () => ['console', 'systemFeatures', 'get'], - queryOptions: (options?: Record) => ({ - queryKey: ['console', 'systemFeatures', 'get'], - queryFn: () => new Promise(() => {}), - ...options, - }), - }, - }, - agent: { - byAgentId: { - config: { - files: { - post: { - mutationOptions: () => ({ mutationFn: mocks.commitFileMutationFn }), - }, - byName: { - delete: { - mutationOptions: () => ({ mutationFn: mocks.deleteFileMutationFn }), - }, - preview: { - get: { - queryOptions: mocks.previewQueryOptions, - }, - }, - download: { - get: { - queryOptions: mocks.downloadQueryOptions, - }, - }, - }, - }, +vi.mock('@/service/console', async () => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleQuery: { + systemFeatures: { + get: { + queryKey: () => ['console', 'systemFeatures', 'get'], + queryOptions: (options?: Record) => ({ + queryKey: ['console', 'systemFeatures', 'get'], + queryFn: () => new Promise(() => {}), + ...options, + }), }, }, - }, - apps: { - byAppId: { - agent: { + agent: { + byAgentId: { config: { files: { post: { @@ -138,30 +109,59 @@ vi.mock('@/service/console', () => ({ }, }, }, - }, - trialApps: { - byAppId: { - agent: { - config: { - files: { - byName: { - preview: { get: { queryOptions: mocks.previewQueryOptions } }, - download: { get: { queryOptions: mocks.downloadQueryOptions } }, + apps: { + byAppId: { + agent: { + config: { + files: { + post: { + mutationOptions: () => ({ mutationFn: mocks.commitFileMutationFn }), + }, + byName: { + delete: { + mutationOptions: () => ({ mutationFn: mocks.deleteFileMutationFn }), + }, + preview: { + get: { + queryOptions: mocks.previewQueryOptions, + }, + }, + download: { + get: { + queryOptions: mocks.downloadQueryOptions, + }, + }, + }, }, }, }, }, }, - }, - files: { - upload: { - post: { - mutationOptions: () => ({ mutationFn: mocks.uploadFileMutationFn }), + trialApps: { + byAppId: { + agent: { + config: { + files: { + byName: { + preview: { get: { queryOptions: mocks.previewQueryOptions } }, + download: { get: { queryOptions: mocks.downloadQueryOptions } }, + }, + }, + }, + }, + }, + }, + files: { + upload: { + get: consoleOperationStub('files.upload.get'), + post: { + mutationOptions: () => ({ mutationFn: mocks.uploadFileMutationFn }), + }, }, }, }, - }, -})) + } +}) function ConfigSnapshotProbe() { const draft = useAtomValue(agentComposerDraftAtom) @@ -238,6 +238,13 @@ function renderAgentFiles({ } } +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ data: mocks.fileUploadConfig }), + }) +}) + describe('AgentFiles', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx index 7795dd7bdb199b..a0619acdd70060 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx @@ -19,7 +19,7 @@ import { } from '@langgenius/dify-ui/dialog' import { FileTreeIcon } from '@langgenius/dify-ui/file-tree' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' @@ -28,7 +28,6 @@ import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { toast } from '@/app/notifications' import { getFileIconType } from '@/features/agent-v2/file-icon' import { consoleQuery } from '@/service/console' -import { useFileUploadConfig } from '@/service/use-common' import { formatFileSize } from '@/utils/format' function toAgentFileNode(committedFile: AgentConfigFileItemResponse): AgentFileNode { @@ -52,7 +51,7 @@ function AgentFileUploader({ file, onChange }: { file?: File; onChange: (file?: const { t } = useTranslation(['agentV2']) const { t: tAppDebug } = useTranslation(['appDebug']) const { t: tCommon } = useTranslation(['common']) - const { data: fileUploadConfig } = useFileUploadConfig() + const { data: fileUploadConfig } = useQuery(consoleQuery.files.upload.get.queryOptions()) const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileUploadConfig) const fileInputRef = useRef(null) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx index ea289f167b67a5..5bb6077e0303ef 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx @@ -123,7 +123,6 @@ const createDatasetFromRef = (dataset: AgentKnowledgeDatasetConfig, index: numbe return { id, name, - indexing_status: 'completed', icon_info: { icon: '📙', icon_type: 'emoji', @@ -135,12 +134,16 @@ const createDatasetFromRef = (dataset: AgentKnowledgeDatasetConfig, index: numbe data_source_type: DataSourceType.FILE, indexing_technique: IndexingType.QUALIFIED, created_by: '', + author_name: null, + created_at: 0, updated_by: '', updated_at: 0, app_count: 0, doc_form: ChunkingMode.text, + chunk_structure: null, document_count: 0, - total_document_count: 0, + total_documents: 0, + total_available_documents: 0, word_count: 0, provider: '', embedding_model: '', @@ -157,17 +160,6 @@ const createDatasetFromRef = (dataset: AgentKnowledgeDatasetConfig, index: numbe score_threshold_enabled: false, score_threshold: 0, }, - retrieval_model: { - search_method: RETRIEVE_METHOD.semantic, - reranking_enable: false, - reranking_model: { - reranking_provider_name: '', - reranking_model_name: '', - }, - top_k: DATASET_DEFAULT.top_k, - score_threshold_enabled: false, - score_threshold: 0, - }, tags: [], external_knowledge_info: { external_knowledge_id: '', @@ -180,8 +172,11 @@ const createDatasetFromRef = (dataset: AgentKnowledgeDatasetConfig, index: numbe score_threshold: 0, score_threshold_enabled: false, }, + doc_metadata: [], built_in_field_enabled: false, + pipeline_id: null, runtime_mode: 'general', + is_published: false, enable_api: false, is_multimodal: false, } diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/model-config/__tests__/field.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/model-config/__tests__/field.spec.tsx index b3c594b6a20c55..d6eaa801baf9bf 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/model-config/__tests__/field.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/model-config/__tests__/field.spec.tsx @@ -5,6 +5,16 @@ import { act, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { AgentModelField } from '../field' +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.modelProviders.byProvider.models.parameterRules.get': () => ({ + data: { data: [] }, + isLoading: false, + }), + }) +}) + const providerSummary = vi.hoisted(() => vi.fn()) const modelList = vi.hoisted(() => vi.fn<() => Promise<{ data: ProviderWithModelsResponse[] }>>()) @@ -14,10 +24,6 @@ vi.mock('nuqs', async (importOriginal) => ({ useQueryState: () => [null, vi.fn()], })) -vi.mock('@/service/use-common', () => ({ - useModelParameterRules: () => ({ data: { data: [] }, isLoading: false }), -})) - vi.mock( '@/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state', () => ({ @@ -25,38 +31,51 @@ vi.mock( }), ) -vi.mock('@/service/console', () => ({ - consoleQuery: { - workspaces: { - current: { - models: { - modelTypes: { - byModelType: { +vi.mock('@/service/console', async () => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleQuery: { + files: { upload: { get: consoleOperationStub('files.upload.get') } }, + workspaces: { + current: { + models: { + modelTypes: { + byModelType: { + get: { + queryOptions: (options: object) => ({ + queryKey: ['model-list'], + queryFn: modelList, + ...options, + }), + }, + }, + }, + }, + modelProviders: { + byProvider: { + models: { + parameterRules: { + get: consoleOperationStub( + 'workspaces.current.modelProviders.byProvider.models.parameterRules.get', + ), + }, + }, + }, + summary: { get: { queryOptions: (options: object) => ({ - queryKey: ['model-list'], - queryFn: modelList, + queryKey: ['providers'], + queryFn: providerSummary, ...options, }), }, }, }, }, - modelProviders: { - summary: { - get: { - queryOptions: (options: object) => ({ - queryKey: ['providers'], - queryFn: providerSummary, - ...options, - }), - }, - }, - }, }, }, - }, -})) + } +}) function renderField(currentModel?: AgentComposerModel) { const queryClient = new QueryClient({ diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/hooks.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/hooks.ts index 330964fe154574..b8fdb5eb45efc6 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/hooks.ts +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/hooks.ts @@ -2,18 +2,15 @@ import type { ToolWithProvider } from '@/app/components/workflow/types' import type { AgentTool } from '@/features/agent-v2/agent-composer/form-state' +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { API_PREFIX } from '@/config' import { currentWorkspaceIdAtom } from '@/context/workspace-state' import useTheme from '@/hooks/use-theme' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { Theme } from '@/types/app' import { basePath } from '@/utils/var' import { @@ -50,16 +47,31 @@ export function useAgentPromptToolIconResolver( ) { const { theme } = useTheme() const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom) - const { data: builtInTools } = useAllBuiltInTools( - !providerTypes || providerTypes.has(CollectionType.builtIn) || providerTypes.has('plugin'), + const { data: builtInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + enabled: + !providerTypes || providerTypes.has(CollectionType.builtIn) || providerTypes.has('plugin'), + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: !providerTypes || providerTypes.has(CollectionType.custom), + select: selectToolsWithProvider, + }), ) - const { data: customTools } = useAllCustomTools( - !providerTypes || providerTypes.has(CollectionType.custom), + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: !providerTypes || providerTypes.has(CollectionType.workflow), + select: selectToolsWithProvider, + }), ) - const { data: workflowTools } = useAllWorkflowTools( - !providerTypes || providerTypes.has(CollectionType.workflow), + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: !providerTypes || providerTypes.has(CollectionType.mcp), + select: selectToolsWithProvider, + }), ) - const { data: mcpTools } = useAllMCPTools(!providerTypes || providerTypes.has(CollectionType.mcp)) const providerById = useMemo( () => diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx index 54647fa3d2af91..1539ca1b098bd7 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx @@ -15,23 +15,20 @@ import type { import type { AgentProviderToolDefaultValue } from '@/features/agent-v2/agent-composer/store-modules/tools' import { cn } from '@langgenius/dify-ui/cn' import { FileTreeIcon } from '@langgenius/dify-ui/file-tree' +import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { getMarketplaceCategoryUrl } from '@/app/components/plugins/marketplace/utils' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { parseToolProviderType } from '@/app/components/tools/provider-type' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import BlockIcon from '@/app/components/workflow/block-icon' import { ToolType } from '@/app/components/workflow/block-selector/types' import { BlockEnum } from '@/app/components/workflow/types' import { useGetLanguage } from '@/context/i18n' import { ENABLE_AGENT_CLI_TOOLS } from '@/features/agent-v2/agent-detail/configure/feature-flags' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { getProviderReference, matchesProviderReference } from '@/utils/provider-reference' import { useAgentPromptToolIconResolver } from './hooks' @@ -362,10 +359,22 @@ function AgentPromptToolRows({ const { getProviderIcon, getProviderIcons } = useAgentPromptToolIconResolver() const [activeTab, setActiveTab] = useState('all') const [expandedProviderIds, setExpandedProviderIds] = useState>(() => new Set()) - const { data: builtInTools = [] } = useAllBuiltInTools() - const { data: customTools = [] } = useAllCustomTools() - const { data: workflowTools = [] } = useAllWorkflowTools() - const { data: mcpTools = [] } = useAllMCPTools() + const { data: builtInTools = [] } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools = [] } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools = [] } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools = [] } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const configuredCliTools = ENABLE_AGENT_CLI_TOOLS ? configuredTools.filter((tool) => tool.kind === 'cli') : [] diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx index b00c970d2da5fe..835dd189af76ed 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx @@ -98,10 +98,6 @@ vi.mock('@/utils/download', () => ({ downloadUrl: mocks.downloadUrl, })) -vi.mock('@/service/use-common', () => ({ - useFileUploadConfig: () => ({ data: mocks.fileUploadConfig }), -})) - vi.mock('@/config', async (importOriginal) => ({ ...(await importOriginal()), API_PREFIX: 'http://localhost:5001/console/api', @@ -115,62 +111,24 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/console', async (importOriginal) => ({ - consoleQuery: { - features: (await importOriginal()).consoleQuery.features, - tags: { - get: { - queryOptions: mocks.workspaceSkillTagsQueryOptions, - }, - }, - agent: { - byAgentId: { - composer: { - get: { - key: vi.fn((_options: unknown): unknown[] => ['agent-composer']), - }, +vi.mock('@/service/console', async (importOriginal) => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleQuery: { + files: { upload: { get: consoleOperationStub('files.upload.get') } }, + features: (await importOriginal()).consoleQuery.features, + tags: { + get: { + queryOptions: mocks.workspaceSkillTagsQueryOptions, }, - config: { - skills: { - upload: { - post: { - mutationOptions: () => ({ mutationFn: mocks.uploadSkillMutationFn }), - }, - }, - byName: { - delete: { - mutationOptions: () => ({ mutationFn: mocks.deleteSkillMutationFn }), - }, - download: { - get: { - queryOptions: mocks.skillDownloadQueryOptions, - }, - }, - inspect: { - get: { - queryOptions: mocks.inspectQueryOptions, - }, - }, - files: { - preview: { - get: { - queryOptions: mocks.previewQueryOptions, - }, - }, - download: { - get: { - queryOptions: mocks.downloadQueryOptions, - }, - }, - }, + }, + agent: { + byAgentId: { + composer: { + get: { + key: vi.fn((_options: unknown): unknown[] => ['agent-composer']), }, }, - }, - }, - }, - apps: { - byAppId: { - agent: { config: { skills: { upload: { @@ -209,52 +167,94 @@ vi.mock('@/service/console', async (importOriginal) => ({ }, }, }, - }, - trialApps: { - byAppId: { - agent: { - config: { - skills: { - byName: { - download: { get: { queryOptions: mocks.skillDownloadQueryOptions } }, - inspect: { get: { queryOptions: mocks.inspectQueryOptions } }, - files: { - preview: { get: { queryOptions: mocks.previewQueryOptions } }, - download: { get: { queryOptions: mocks.downloadQueryOptions } }, + apps: { + byAppId: { + agent: { + config: { + skills: { + upload: { + post: { + mutationOptions: () => ({ mutationFn: mocks.uploadSkillMutationFn }), + }, + }, + byName: { + delete: { + mutationOptions: () => ({ mutationFn: mocks.deleteSkillMutationFn }), + }, + download: { + get: { + queryOptions: mocks.skillDownloadQueryOptions, + }, + }, + inspect: { + get: { + queryOptions: mocks.inspectQueryOptions, + }, + }, + files: { + preview: { + get: { + queryOptions: mocks.previewQueryOptions, + }, + }, + download: { + get: { + queryOptions: mocks.downloadQueryOptions, + }, + }, + }, }, }, }, }, }, }, - }, - workspaces: { - current: { - agents: { - byAgentId: { - skills: { - get: { - key: mocks.agentSkillBindingsKey, - queryOptions: mocks.agentSkillBindingsQueryOptions, - }, - put: { - mutationOptions: () => ({ - mutationFn: mocks.replaceAgentSkillBindingsMutationFn, - }), + trialApps: { + byAppId: { + agent: { + config: { + skills: { + byName: { + download: { get: { queryOptions: mocks.skillDownloadQueryOptions } }, + inspect: { get: { queryOptions: mocks.inspectQueryOptions } }, + files: { + preview: { get: { queryOptions: mocks.previewQueryOptions } }, + download: { get: { queryOptions: mocks.downloadQueryOptions } }, + }, + }, }, }, }, }, - skills: { - get: { - queryOptions: mocks.workspaceSkillsQueryOptions, - infiniteOptions: mocks.workspaceSkillsInfiniteOptions, + }, + workspaces: { + current: { + agents: { + byAgentId: { + skills: { + get: { + key: mocks.agentSkillBindingsKey, + queryOptions: mocks.agentSkillBindingsQueryOptions, + }, + put: { + mutationOptions: () => ({ + mutationFn: mocks.replaceAgentSkillBindingsMutationFn, + }), + }, + }, + }, + }, + skills: { + get: { + queryOptions: mocks.workspaceSkillsQueryOptions, + infiniteOptions: mocks.workspaceSkillsInfiniteOptions, + }, }, }, }, }, - }, -})) + } +}) async function openUploadSkillDialog(user: ReturnType) { await user.click( @@ -370,6 +370,13 @@ function renderAgentSkills({ } } +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'files.upload.get': () => ({ data: mocks.fileUploadConfig }), + }) +}) + describe('AgentSkills', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx index 4ca8ae336aef8a..20f3f0e89c1873 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx @@ -15,14 +15,13 @@ import { DialogTitle, } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { useRef, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' import { toast } from '@/app/notifications' import Link from '@/next/link' import { consoleQuery } from '@/service/console' -import { useFileUploadConfig } from '@/service/use-common' import { formatFileSize } from '@/utils/format' const skillPackageAccept = '.zip,.skill' @@ -71,7 +70,7 @@ function AgentSkillPackageUploader({ }) { const { t } = useTranslation(['agentV2']) const { t: tCommon } = useTranslation(['common']) - const { data: fileUploadConfig } = useFileUploadConfig() + const { data: fileUploadConfig } = useQuery(consoleQuery.files.upload.get.queryOptions()) const { skillSizeLimit } = useFileSizeLimit(fileUploadConfig) const fileInputRef = useRef(null) const dragDepthRef = useRef(0) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx index 276e198dcd4871..b6837be6ec2d55 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx @@ -15,6 +15,7 @@ import { agentComposerSavedDraftAtom, isAgentComposerDirtyAtom, } from '@/features/agent-v2/agent-composer/store' +import { consoleQuery } from '@/service/console' import { seedAccountProfileQuery } from '@/test/console/account-profile' import { AgentOrchestrateReadOnlyContext, @@ -45,7 +46,6 @@ const pluginInstallState = vi.hoisted(() => ({ latest_package_identifier: string } | undefined, - invalidateBuiltInTools: vi.fn(), invalidateInstalledPluginList: vi.fn(), })) @@ -158,14 +158,15 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal ), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: toolProviderState.builtInTools }), - useAllCustomTools: () => ({ data: toolProviderState.customTools }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => pluginInstallState.invalidateBuiltInTools, - useInvalidToolsByType: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: toolProviderState.builtInTools }), + 'workspaces.current.tools.api.get': () => ({ data: toolProviderState.customTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) const agentToolsDraft = { ...defaultAgentSoulConfigFormState, @@ -452,13 +453,14 @@ function renderAgentTools(initialDraft: AgentSoulConfigFormState = agentToolsDra }) seedAccountProfileQuery(queryClient, { id: 'user-1' }) - return render( + const view = render( , ) + return { ...view, queryClient } } function renderAgentToolsWithStore(initialDraft: AgentSoulConfigFormState = agentToolsDraft) { @@ -531,7 +533,6 @@ describe('AgentTools', () => { pluginAuthState.notAllowCustomCredential = false pluginInstallState.manifest = undefined pluginInstallState.fallbackManifest = undefined - pluginInstallState.invalidateBuiltInTools.mockResolvedValue(undefined) pluginInstallState.invalidateInstalledPluginList.mockResolvedValue(undefined) }) @@ -727,7 +728,9 @@ describe('AgentTools', () => { }, latest_package_identifier: 'langgenius/google:1.0.0@checksum', } - renderAgentTools(reflectedUninstalledPluginDraft) + const { queryClient } = renderAgentTools(reflectedUninstalledPluginDraft) + const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() + queryClient.setQueryData(builtInToolsKey, []) expect(screen.getByRole('button', { name: 'Google Tools' })).toBeInTheDocument() expect( @@ -750,7 +753,7 @@ describe('AgentTools', () => { await user.click(installButton) await waitFor(() => { - expect(pluginInstallState.invalidateBuiltInTools).toHaveBeenCalledTimes(1) + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) expect(pluginInstallState.invalidateInstalledPluginList).toHaveBeenCalledTimes(1) }) }) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx index 9e0efc56250b6d..df35738f0bb347 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx @@ -12,6 +12,7 @@ import type { import type { AgentProviderToolDefaultValue } from '@/features/agent-v2/agent-composer/store-modules/tools' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import { useAtomValue, useSetAtom } from 'jotai' import { memo, useCallback, useId, useMemo, useRef, useState } from 'react' @@ -19,6 +20,7 @@ import { useTranslation } from 'react-i18next' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { parseToolProviderType } from '@/app/components/tools/provider-type' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { ToolPickerContent } from '@/app/components/workflow/block-selector/tool-picker' import { useGetLanguage } from '@/context/i18n' import { @@ -27,14 +29,8 @@ import { setProviderToolCredentialAtom, } from '@/features/agent-v2/agent-composer/store-modules/tools' import { ENABLE_AGENT_CLI_TOOLS } from '@/features/agent-v2/agent-detail/configure/feature-flags' +import { consoleQuery } from '@/service/console' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, -} from '@/service/use-tools' import { createAgentToolProviderCatalog, getAgentProviderPluginId, @@ -398,7 +394,7 @@ export function AgentTools() { const { t } = useTranslation(['agentV2']) const readOnly = useAgentOrchestrateReadOnly() const setProviderToolCredential = useSetAtom(setProviderToolCredentialAtom) - const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools() + const queryClient = useQueryClient() const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const { providerById, resolvedProviderTypes } = useAgentToolProviderCatalog() const tools = useAtomValue(agentComposerToolsAtom) @@ -432,10 +428,12 @@ export function AgentTools() { ) const handlePluginInstalled = useCallback(() => { void Promise.allSettled([ - invalidateAllBuiltInTools(), + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }), invalidateInstalledPluginList(PluginCategoryEnum.tool), ]) - }, [invalidateAllBuiltInTools, invalidateInstalledPluginList]) + }, [invalidateInstalledPluginList, queryClient]) const visibleTools = useMemo( () => (ENABLE_AGENT_CLI_TOOLS ? tools : tools.filter((tool) => tool.kind !== 'cli')), [tools], @@ -603,12 +601,30 @@ export function AgentTemplateTools() { const providerTypes = new Set( visibleTools.filter((tool) => tool.kind === 'provider').map((tool) => tool.providerType), ) - const { data: buildInTools } = useAllBuiltInTools( - providerTypes.has(CollectionType.builtIn) || providerTypes.has('plugin'), + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + enabled: providerTypes.has(CollectionType.builtIn) || providerTypes.has('plugin'), + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: providerTypes.has(CollectionType.custom), + select: selectToolsWithProvider, + }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: providerTypes.has(CollectionType.workflow), + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: providerTypes.has(CollectionType.mcp), + select: selectToolsWithProvider, + }), ) - const { data: customTools } = useAllCustomTools(providerTypes.has(CollectionType.custom)) - const { data: workflowTools } = useAllWorkflowTools(providerTypes.has(CollectionType.workflow)) - const { data: mcpTools } = useAllMCPTools(providerTypes.has(CollectionType.mcp)) const catalog = useMemo( () => createAgentToolProviderCatalog({ buildInTools, customTools, workflowTools, mcpTools }), [buildInTools, customTools, workflowTools, mcpTools], diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/dialog.tsx index 5849b7ca62077a..17b5d8d66a3052 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/dialog.tsx @@ -94,7 +94,6 @@ export function ProviderToolSettingsDialog({ toolName={currentTarget.action.toolName} setting={toolSettings[currentTarget.action.id]} collection={toolCollection} - isModel={false} credentialId={currentTarget.tool.credentialId} onSave={handleSave} onHide={onClose} diff --git a/web/features/agent-v2/agent-detail/configure/tool-provider-catalog.ts b/web/features/agent-v2/agent-detail/configure/tool-provider-catalog.ts index 51a4b7e3a26728..68261d7ae798b8 100644 --- a/web/features/agent-v2/agent-detail/configure/tool-provider-catalog.ts +++ b/web/features/agent-v2/agent-detail/configure/tool-provider-catalog.ts @@ -3,16 +3,13 @@ import type { MarketplacePlugin } from '@dify/contracts/marketplace' import type { ToolWithProvider } from '@/app/components/workflow/types' import type { AgentProviderTool, AgentTool } from '@/features/agent-v2/agent-composer/form-state' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useGetLanguage } from '@/context/i18n' +import { consoleQuery } from '@/service/console' import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' import { getIconFromMarketPlace } from '@/utils/get-icon' import { getProviderReference } from '@/utils/provider-reference' @@ -73,10 +70,22 @@ export function createAgentToolProviderCatalog({ } export function useAgentToolProviderCatalog(): AgentToolProviderCatalog { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) return useMemo( () => createAgentToolProviderCatalog({ buildInTools, customTools, mcpTools, workflowTools }), diff --git a/web/features/code-based-extension/query.ts b/web/features/code-based-extension/query.ts new file mode 100644 index 00000000000000..3bc7b14f50e39e --- /dev/null +++ b/web/features/code-based-extension/query.ts @@ -0,0 +1,20 @@ +import type { + CodeBasedExtension, + CodeBasedExtensionForm, + CodeBasedExtensionItem, +} from '@/models/common' +import { consoleQuery } from '@/service/console' + +// `label` and `form_schema` are plugin-defined JSON in the console API response. +export const codeBasedExtensionsQueryOptions = (module: string) => + consoleQuery.codeBasedExtension.get.queryOptions({ + input: { query: { module } }, + select: (response): CodeBasedExtension => ({ + module: response.module, + data: response.data.map((item) => ({ + name: item.name, + label: (item.label ?? {}) as CodeBasedExtensionItem['label'], + form_schema: (item.form_schema ?? []) as CodeBasedExtensionForm[], + })), + }), + }) diff --git a/web/features/skills/__tests__/detail-page-builder.spec.tsx b/web/features/skills/__tests__/detail-page-builder.spec.tsx index 1fab9ebc3b631e..6436b04823b7b5 100644 --- a/web/features/skills/__tests__/detail-page-builder.spec.tsx +++ b/web/features/skills/__tests__/detail-page-builder.spec.tsx @@ -560,7 +560,10 @@ describe('SkillDetailPage builder', () => { expect(mocks.modelParameterRulesFetch).toHaveBeenCalledTimes(1) const request = mocks.modelParameterRulesFetch.mock.calls[0]![0] const url = new URL(request instanceof Request ? request.url : String(request)) - expect(url.pathname).toContain('/model-providers/langgenius/anthropic/anthropic/') + // Provider ids contain slashes and are sent percent-encoded. + expect(decodeURIComponent(url.pathname)).toContain( + '/model-providers/langgenius/anthropic/anthropic/', + ) expect(url.searchParams.get('model')).toBe('claude-sonnet-4-6') }) @@ -626,10 +629,13 @@ describe('SkillDetailPage builder', () => { ) renderSkillDetailPage() + // The settings button waits for the model list and its lazily loaded contract segment. await user.click( - await screen.findByRole('button', { - name: 'modelProvider.modelProvider.modelSettings', - }), + await screen.findByRole( + 'button', + { name: 'modelProvider.modelProvider.modelSettings' }, + { timeout: 5000 }, + ), ) const dialog = await screen.findByRole('dialog', { name: 'modelProvider.modelProvider.modelSettings', @@ -652,7 +658,8 @@ describe('SkillDetailPage builder', () => { }, }), ) - }) + // Opens the settings dialog, loads parameter rules and types through several fields. + }, 15000) it('uploads image attachments in Skill Builder', async () => { const user = userEvent.setup({ applyAccept: false }) diff --git a/web/features/skills/__tests__/detail-page.fixture.tsx b/web/features/skills/__tests__/detail-page.fixture.tsx index 382d28063dcf8b..5580980e422511 100644 --- a/web/features/skills/__tests__/detail-page.fixture.tsx +++ b/web/features/skills/__tests__/detail-page.fixture.tsx @@ -209,6 +209,7 @@ vi.mock('@/service/console', async (importOriginal) => { workspaces: { current: { models: actual.consoleQuery.workspaces.current.models, + modelProviders: actual.consoleQuery.workspaces.current.modelProviders, agents: { byAgentId: { skills: { @@ -688,6 +689,8 @@ export function resetDetailPageFixture() { mocks.request.mockImplementation(async (_url, _init, options) => { const req = options.request const pathname = new URL(req.url).pathname + // Model parameter rules go through the console client and land on the fetch mock. + if (pathname.endsWith('/models/parameter-rules')) return mocks.modelParameterRulesFetch(req) if (req.method === 'POST' && /\/skills\/[^/]+\/restore$/.test(pathname)) return Response.json(mocks.skillDetail) if (req.method === 'DELETE' && /\/skills\/[^/]+\/versions\/[^/]+$/.test(pathname)) diff --git a/web/hooks/use-pay.tsx b/web/hooks/use-pay.tsx index 8f0132b46c7445..e254f60c71bd1b 100644 --- a/web/hooks/use-pay.tsx +++ b/web/hooks/use-pay.tsx @@ -8,10 +8,11 @@ import { AlertDialogFooter, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' +import { skipToken, useQuery } from '@tanstack/react-query' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRouter, useSearchParams } from '@/next/navigation' -import { useNotionBinding } from '@/service/use-common' +import { consoleQuery } from '@/service/console' type ConfirmType = { type: 'info' | 'warning' @@ -76,7 +77,14 @@ const useCheckNotion = () => { const type = searchParams.get('type') const notionCode = searchParams.get('code') const notionError = searchParams.get('error') - const { data } = useNotionBinding(notionCode, canBinding) + const { data } = useQuery( + consoleQuery.oauth.dataSource.binding.byProvider.get.queryOptions({ + input: + notionCode && canBinding + ? { params: { provider: 'notion' }, query: { code: notionCode } } + : skipToken, + }), + ) useEffect(() => { if (data) router.replace('/') diff --git a/web/i18n/locales/ar-TN/dataset.json b/web/i18n/locales/ar-TN/dataset.json index 152a2f9a8fef14..6d7c9767e127c1 100644 --- a/web/i18n/locales/ar-TN/dataset.json +++ b/web/i18n/locales/ar-TN/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "بحث النص الكامل", "retrieval.hybrid_search.description": "تنفيذ البحث بالنص الكامل والبحث المتجه في وقت واحد، وإعادة الترتيب لتحديد أفضل تطابق لاستعلام المستخدم. يمكن للمستخدمين اختيار تعيين الأوزان أو التكوين لنموذج إعادة الترتيب.", "retrieval.hybrid_search.title": "بحث هجين", - "retrieval.invertedIndex.title": "الفهرس المعكوس", "retrieval.keyword_search.description": "الفهرس المعكوس هو هيكل يستخدم للاسترجاع الفعال. منظم حسب المصطلحات، يشير كل مصطلح إلى المستندات أو صفحات الويب التي تحتوي عليه.", "retrieval.keyword_search.title": "فهرس معكوس", "retrieval.semantic_search.description": "إنشاء تضمينات الاستعلام والبحث عن قطعة النص الأكثر تشابهًا مع تمثيلها المتجه.", diff --git a/web/i18n/locales/ar-TN/login.json b/web/i18n/locales/ar-TN/login.json index 3b04e6a872a0f1..496eddd191d0c6 100644 --- a/web/i18n/locales/ar-TN/login.json +++ b/web/i18n/locales/ar-TN/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "الاسم مطلوب", "error.passwordEmpty": "كلمة المرور مطلوبة", "error.passwordInvalid": "يجب أن تحتوي كلمة المرور على أحرف وأرقام، ويجب أن يكون الطول أكبر من 8", - "error.registrationNotAllowed": "الحساب غير موجود. يرجى الاتصال بمسؤول النظام للتسجيل.", "error.ssoFailed": "فشل تسجيل الدخول عبر SSO. يرجى المحاولة مرة أخرى أو الاتصال بالمسؤول.", "explore": "استكشاف Dify", "forget": "نسيت كلمة المرور؟", diff --git a/web/i18n/locales/az-AZ/dataset.json b/web/i18n/locales/az-AZ/dataset.json index bd3cd2e260c2de..4a55d59adff586 100644 --- a/web/i18n/locales/az-AZ/dataset.json +++ b/web/i18n/locales/az-AZ/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Tam mətn axtarışı", "retrieval.hybrid_search.description": "Tam mətn axtarışını və vektor axtarışlarını eyni vaxtda icra edin, istifadəçinin sorğusuna ən uyğun nəticəni seçmək üçün yenidən sıralayın. İstifadəçilər çəki əmsallarını təyin edə və ya yenidən sıralama modelini konfiqurasiya edə bilərlər.", "retrieval.hybrid_search.title": "Hibrid axtarış", - "retrieval.invertedIndex.title": "Tərs indeks", "retrieval.keyword_search.description": "Tərs indeks səmərəli məlumat axtarışı üçün istifadə olunan strukturdur. Terminlərə görə təşkil olunur və hər termin onu ehtiva edən sənədlərə və ya veb səhifələrə işarə edir.", "retrieval.keyword_search.title": "Tərs indeks", "retrieval.semantic_search.description": "Sorğunun vektor təsvirini yaradın və bu vektora ən çox bənzəyən vektorla təmsil olunan mətn hissəsini axtarın.", diff --git a/web/i18n/locales/az-AZ/login.json b/web/i18n/locales/az-AZ/login.json index aaf3c0e4abc5bf..f7d6e833f60e9e 100644 --- a/web/i18n/locales/az-AZ/login.json +++ b/web/i18n/locales/az-AZ/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Ad tələb olunur", "error.passwordEmpty": "Parol tələb olunur", "error.passwordInvalid": "Parolda hərflər və rəqəmlər olmalı, uzunluğu isə 8 simvoldan çox olmalıdır", - "error.registrationNotAllowed": "Hesab tapılmadı. Qeydiyyatdan keçmək üçün sistem inzibatçısı ilə əlaqə saxlayın.", "error.ssoFailed": "SSO ilə giriş uğursuz oldu. Yenidən cəhd edin və ya administratorla əlaqə saxlayın.", "explore": "Dify ilə tanış ol", "forget": "Parolunuzu unutmusunuz?", diff --git a/web/i18n/locales/de-DE/dataset.json b/web/i18n/locales/de-DE/dataset.json index 8eedea7275fba0..23aa1b011aef20 100644 --- a/web/i18n/locales/de-DE/dataset.json +++ b/web/i18n/locales/de-DE/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Volltextsuche", "retrieval.hybrid_search.description": "Führe Volltextsuche und Vektorsuchen gleichzeitig aus, ordne neu, um die beste Übereinstimmung für die Abfrage des Benutzers auszuwählen. Konfiguration des Rerank-Modell-APIs ist notwendig.", "retrieval.hybrid_search.title": "Hybridsuche", - "retrieval.invertedIndex.title": "Invertierter Index", "retrieval.keyword_search.description": "Der invertierte Index ist eine Struktur, die für einen effizienten Abruf verwendet wird. Jeder Begriff ist nach Begriffen geordnet und verweist auf Dokumente oder Webseiten, die ihn enthalten.", "retrieval.keyword_search.title": "Invertierter Index", "retrieval.semantic_search.description": "Erzeuge Abfrage-Einbettungen und suche nach dem Textstück, das seiner Vektorrepräsentation am ähnlichsten ist.", diff --git a/web/i18n/locales/de-DE/login.json b/web/i18n/locales/de-DE/login.json index 00541e80e76cc1..c72c8dcb52c63e 100644 --- a/web/i18n/locales/de-DE/login.json +++ b/web/i18n/locales/de-DE/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Name wird benötigt", "error.passwordEmpty": "Passwort wird benötigt", "error.passwordInvalid": "Das Passwort muss Buchstaben und Zahlen enthalten und länger als 8 Zeichen sein", - "error.registrationNotAllowed": "Konto nicht gefunden. Bitte wenden Sie sich an den Systemadministrator, um sich zu registrieren.", "error.ssoFailed": "SSO-Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut oder wenden Sie sich an Ihren Administrator.", "explore": "Dify erkunden", "forget": "Passwort vergessen?", diff --git a/web/i18n/locales/en-US/dataset.json b/web/i18n/locales/en-US/dataset.json index ed465f9940abf7..ccde333fc89348 100644 --- a/web/i18n/locales/en-US/dataset.json +++ b/web/i18n/locales/en-US/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Full-Text Search", "retrieval.hybrid_search.description": "Execute full-text search and vector searches simultaneously, re-rank to select the best match for the user's query. Users can choose to set weights or configure to a Rerank model.", "retrieval.hybrid_search.title": "Hybrid Search", - "retrieval.invertedIndex.title": "Inverted Index", "retrieval.keyword_search.description": "Inverted Index is a structure used for efficient retrieval. Organized by terms, each term points to documents or web pages containing it.", "retrieval.keyword_search.title": "Inverted Index", "retrieval.semantic_search.description": "Generate query embeddings and search for the text chunk most similar to its vector representation.", diff --git a/web/i18n/locales/en-US/login.json b/web/i18n/locales/en-US/login.json index dcf7b48cb8d4be..685ec31751e683 100644 --- a/web/i18n/locales/en-US/login.json +++ b/web/i18n/locales/en-US/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Name is required", "error.passwordEmpty": "Password is required", "error.passwordInvalid": "Password must contain letters and numbers, and the length must be greater than 8", - "error.registrationNotAllowed": "Account not found. Please contact the system admin to register.", "error.ssoFailed": "SSO login failed. Please try again or contact your administrator.", "explore": "Explore Dify", "forget": "Forgot your password?", diff --git a/web/i18n/locales/es-ES/dataset.json b/web/i18n/locales/es-ES/dataset.json index 92f147448c9934..52ee600d53622f 100644 --- a/web/i18n/locales/es-ES/dataset.json +++ b/web/i18n/locales/es-ES/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Búsqueda de Texto Completo", "retrieval.hybrid_search.description": "Ejecuta búsquedas de texto completo y búsquedas vectoriales simultáneamente, reordena para seleccionar la mejor coincidencia para la consulta del usuario. Es necesaria la configuración de las API del modelo de reordenamiento.", "retrieval.hybrid_search.title": "Búsqueda Híbrida", - "retrieval.invertedIndex.title": "Índice invertido", "retrieval.keyword_search.description": "El índice invertido es una estructura utilizada para una recuperación eficiente. Organizado por términos, cada término apunta a documentos o páginas web que lo contienen.", "retrieval.keyword_search.title": "Índice invertido", "retrieval.semantic_search.description": "Genera incrustaciones de consulta y busca el fragmento de texto más similar a su representación vectorial.", diff --git a/web/i18n/locales/es-ES/login.json b/web/i18n/locales/es-ES/login.json index 40457caea9df37..95a4ae3a432a0a 100644 --- a/web/i18n/locales/es-ES/login.json +++ b/web/i18n/locales/es-ES/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Se requiere un nombre", "error.passwordEmpty": "Se requiere una contraseña", "error.passwordInvalid": "La contraseña debe contener letras y números, y tener una longitud mayor a 8", - "error.registrationNotAllowed": "Cuenta no encontrada. Póngase en contacto con el administrador del sistema para registrarse.", "error.ssoFailed": "Error al iniciar sesión con SSO. Inténtelo de nuevo o póngase en contacto con su administrador.", "explore": "Explorar Dify", "forget": "¿Olvidaste tu contraseña?", diff --git a/web/i18n/locales/fa-IR/dataset.json b/web/i18n/locales/fa-IR/dataset.json index bf8e163eb0d618..d675c31ec25655 100644 --- a/web/i18n/locales/fa-IR/dataset.json +++ b/web/i18n/locales/fa-IR/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "جستجوی متن کامل", "retrieval.hybrid_search.description": "جستجوی متن کامل و برداری را همزمان اجرا می‌کند، دوباره رتبه‌بندی می‌کند تا بهترین تطابق برای درخواست کاربر انتخاب شود. کاربران می‌توانند وزن‌ها را تنظیم کنند یا به یک مدل دوباره رتبه‌بندی تنظیم کنند.", "retrieval.hybrid_search.title": "جستجوی هیبریدی", - "retrieval.invertedIndex.title": "فهرست معکوس", "retrieval.keyword_search.description": "شاخص معکوس ساختاری است که برای بازیابی کارآمد استفاده می شود. هر اصطلاح که بر اساس اصطلاحات سازماندهی شده است، به اسناد یا صفحات وب حاوی آن اشاره می کند.", "retrieval.keyword_search.title": "شاخص معکوس", "retrieval.semantic_search.description": "تولید جاسازی‌های جستجو و جستجوی بخش متنی که بیشترین شباهت را به نمایش برداری آن دارد.", diff --git a/web/i18n/locales/fa-IR/login.json b/web/i18n/locales/fa-IR/login.json index 443dd13c3f8f29..c84df67cda606e 100644 --- a/web/i18n/locales/fa-IR/login.json +++ b/web/i18n/locales/fa-IR/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "نام لازم است", "error.passwordEmpty": "رمز عبور لازم است", "error.passwordInvalid": "رمز عبور باید شامل حروف و اعداد باشد و طول آن بیشتر از ۸ کاراکتر باشد", - "error.registrationNotAllowed": "حساب پیدا نشد. لطفا برای ثبت نام با مدیر سیستم تماس حاصل فرمایید.", "error.ssoFailed": "ورود با SSO ناموفق بود. لطفاً دوباره تلاش کنید یا با مدیر سیستم تماس بگیرید.", "explore": "کاوش Dify", "forget": "رمز عبور خود را فراموش کرده‌اید؟", diff --git a/web/i18n/locales/fr-FR/dataset.json b/web/i18n/locales/fr-FR/dataset.json index 0a4816ad6f49f7..38a6a86ef6924a 100644 --- a/web/i18n/locales/fr-FR/dataset.json +++ b/web/i18n/locales/fr-FR/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Recherche en Texte Intégral", "retrieval.hybrid_search.description": "Exécutez une recherche en texte intégral et des recherches vectorielles en même temps, réorganisez pour sélectionner la meilleure correspondance pour la requête de l'utilisateur. La configuration de l'API du modèle de réorganisation est nécessaire.", "retrieval.hybrid_search.title": "Recherche Hybride", - "retrieval.invertedIndex.title": "Index inversé", "retrieval.keyword_search.description": "L’indice inversé est une structure utilisée pour une récupération efficace. Organisé par termes, chaque terme pointe vers des documents ou des pages web qui le contiennent.", "retrieval.keyword_search.title": "Index inversé", "retrieval.semantic_search.description": "Générez des embeddings de requête et recherchez le morceau de texte le plus similaire à sa représentation vectorielle.", diff --git a/web/i18n/locales/fr-FR/login.json b/web/i18n/locales/fr-FR/login.json index f3ff1b80ccdae0..7bb34e26a97af4 100644 --- a/web/i18n/locales/fr-FR/login.json +++ b/web/i18n/locales/fr-FR/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Le nom est requis", "error.passwordEmpty": "Un mot de passe est requis", "error.passwordInvalid": "Le mot de passe doit contenir des lettres et des chiffres, et la longueur doit être supérieure à 8.", - "error.registrationNotAllowed": "Compte introuvable. Veuillez contacter l’administrateur système pour vous inscrire.", "error.ssoFailed": "Échec de la connexion SSO. Veuillez réessayer ou contacter votre administrateur.", "explore": "Explorez Dify", "forget": "Mot de passe oublié ?", diff --git a/web/i18n/locales/hi-IN/dataset.json b/web/i18n/locales/hi-IN/dataset.json index 876f9f04eae54d..db59bfebb8c0f3 100644 --- a/web/i18n/locales/hi-IN/dataset.json +++ b/web/i18n/locales/hi-IN/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "पूर्ण-पाठ खोज", "retrieval.hybrid_search.description": "पूर्ण-पाठ खोज और वेक्टर खोजों को एक साथ निष्पादित करें, पुनः रैंकिंग करें और उपयोगकर्ता के प्रश्न के लिए सर्वोत्तम मिलान का चयन करें। रीरैंक मॉडल APIs की कॉन्फ़िगरेशन आवश्यक।", "retrieval.hybrid_search.title": "हाइब्रिड खोज", - "retrieval.invertedIndex.title": "उल्टा सूचकांक", "retrieval.keyword_search.description": "इनवर्टेड इंडेक्स एक संरचना है जो कुशल पुनर्प्राप्ति के लिए उपयोग की जाती है। यह शर्तों द्वारा व्यवस्थित होती है, प्रत्येक शर्त उन दस्तावेजों या वेब पृष्ठों की ओर इशारा करती है जिनमें यह मौजूद होती है।", "retrieval.keyword_search.title": "इनवर्टेड अनुक्रमणिका", "retrieval.semantic_search.description": "प्रश्न एम्बेडिंग्स उत्पन्न करें और उसके वेक्टर प्रतिनिधित्व के समान सबसे मिलते-जुलते टेक्स्ट चंक को खोजें।", diff --git a/web/i18n/locales/hi-IN/login.json b/web/i18n/locales/hi-IN/login.json index e5fd91e9a04ac8..ae1011051ce34b 100644 --- a/web/i18n/locales/hi-IN/login.json +++ b/web/i18n/locales/hi-IN/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "नाम आवश्यक है", "error.passwordEmpty": "पासवर्ड आवश्यक है", "error.passwordInvalid": "पासवर्ड में अक्षर और अंक होने चाहिए, और लंबाई 8 से अधिक होनी चाहिए", - "error.registrationNotAllowed": "खाता नहीं मिला. रजिस्टर करने के लिए कृपया सिस्टम एडमिन से संपर्क करें।", "error.ssoFailed": "SSO लॉगिन विफल रहा। कृपया पुनः प्रयास करें या अपने व्यवस्थापक से संपर्क करें।", "explore": "Dify का अन्वेषण करें", "forget": "क्या आप पासवर्ड भूल गए?", diff --git a/web/i18n/locales/id-ID/dataset.json b/web/i18n/locales/id-ID/dataset.json index b1f8882b988941..841cf1c24fc9db 100644 --- a/web/i18n/locales/id-ID/dataset.json +++ b/web/i18n/locales/id-ID/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Pencarian Teks Lengkap", "retrieval.hybrid_search.description": "Jalankan pencarian teks lengkap dan pencarian vektor secara bersamaan, peringkatkan ulang untuk memilih kecocokan terbaik untuk kueri pengguna. Pengguna dapat memilih untuk mengatur bobot atau mengonfigurasi ke model Rerank.", "retrieval.hybrid_search.title": "Pencarian Hibrida", - "retrieval.invertedIndex.title": "Indeks Terbalik", "retrieval.keyword_search.description": "Indeks Terbalik adalah struktur yang digunakan untuk pengambilan yang efisien. Diatur berdasarkan istilah, setiap istilah menunjuk ke dokumen atau halaman web yang berisinya.", "retrieval.keyword_search.title": "Indeks Terbalik", "retrieval.semantic_search.description": "Hasilkan penyematan kueri dan cari potongan teks yang paling mirip dengan representasi vektornya.", diff --git a/web/i18n/locales/id-ID/login.json b/web/i18n/locales/id-ID/login.json index 4cf103d09273aa..4fe32c1370bb63 100644 --- a/web/i18n/locales/id-ID/login.json +++ b/web/i18n/locales/id-ID/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Nama diperlukan", "error.passwordEmpty": "Kata sandi diperlukan", "error.passwordInvalid": "Kata sandi harus berisi huruf dan angka, dan panjangnya harus lebih besar dari 8", - "error.registrationNotAllowed": "Akun tidak ditemukan. Silakan hubungi admin sistem untuk mendaftar.", "error.ssoFailed": "Login SSO gagal. Silakan coba lagi atau hubungi administrator Anda.", "explore": "Jelajahi Dify", "forget": "Lupa Kata Sandi Anda?", diff --git a/web/i18n/locales/it-IT/dataset.json b/web/i18n/locales/it-IT/dataset.json index 52b7086b8af8f3..f7b313af17883d 100644 --- a/web/i18n/locales/it-IT/dataset.json +++ b/web/i18n/locales/it-IT/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Ricerca Full-Text", "retrieval.hybrid_search.description": "Esegui contemporaneamente la ricerca full-text e la ricerca vettoriale, riordina per selezionare la migliore corrispondenza per la query dell'utente. È necessaria la configurazione delle API del modello Rerank.", "retrieval.hybrid_search.title": "Ricerca Ibrida", - "retrieval.invertedIndex.title": "Indice Invertito", "retrieval.keyword_search.description": "L'indice invertito è una struttura utilizzata per un recupero efficiente. Organizzato per termini, ogni termine rimanda a documenti o pagine web che lo contengono.", "retrieval.keyword_search.title": "Indice invertito", "retrieval.semantic_search.description": "Genera embedding delle query e cerca il blocco di testo più simile alla sua rappresentazione vettoriale.", diff --git a/web/i18n/locales/it-IT/login.json b/web/i18n/locales/it-IT/login.json index 285db98bf29225..7dbb7d5c3dbe05 100644 --- a/web/i18n/locales/it-IT/login.json +++ b/web/i18n/locales/it-IT/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Il nome è obbligatorio", "error.passwordEmpty": "La password è obbligatoria", "error.passwordInvalid": "La password deve contenere lettere e numeri, e la lunghezza deve essere maggiore di 8", - "error.registrationNotAllowed": "Account non trovato. Si prega di contattare l'amministratore di sistema per registrarsi.", "error.ssoFailed": "Accesso SSO non riuscito. Riprova o contatta l'amministratore.", "explore": "Esplora Dify", "forget": "Hai dimenticato la password?", diff --git a/web/i18n/locales/ja-JP/dataset.json b/web/i18n/locales/ja-JP/dataset.json index 3829cd603439ec..4e77bef83a7616 100644 --- a/web/i18n/locales/ja-JP/dataset.json +++ b/web/i18n/locales/ja-JP/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "全文検索", "retrieval.hybrid_search.description": "全文検索とベクトル検索を同時に実行し、ユーザーのクエリに最適なマッチを選択するために Rerank 付けを行います。Rerank モデル API の設定が必要です。", "retrieval.hybrid_search.title": "ハイブリッド検索", - "retrieval.invertedIndex.title": "転置インデックス", "retrieval.keyword_search.description": "逆インデックスは効率的な検索のために使用される構造です。用語によって整理されており、各用語はそれを含む文書やウェブページを指し示します。", "retrieval.keyword_search.title": "逆インデックス", "retrieval.semantic_search.description": "クエリの埋め込みを生成し、そのベクトル表現に最も類似したテキストチャンクを検索します。", diff --git a/web/i18n/locales/ja-JP/login.json b/web/i18n/locales/ja-JP/login.json index 848bc3312c7f1a..1830414f4e98f8 100644 --- a/web/i18n/locales/ja-JP/login.json +++ b/web/i18n/locales/ja-JP/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "名前は必須です", "error.passwordEmpty": "パスワードは必須です", "error.passwordInvalid": "パスワードは文字と数字を含み、長さは 8 以上である必要があります", - "error.registrationNotAllowed": "アカウントが見つかりません。登録するためにシステム管理者に連絡してください。", "error.ssoFailed": "SSO ログインに失敗しました。もう一度お試しいただくか、管理者にお問い合わせください。", "explore": "Dify を探索する", "forget": "パスワードをお忘れですか?", diff --git a/web/i18n/locales/ko-KR/dataset.json b/web/i18n/locales/ko-KR/dataset.json index 258f229b994e40..682652b895a361 100644 --- a/web/i18n/locales/ko-KR/dataset.json +++ b/web/i18n/locales/ko-KR/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "전체 텍스트 검색", "retrieval.hybrid_search.description": "전체 텍스트 검색과 벡터 검색을 동시에 실행하고 사용자 쿼리에 가장 적합한 매치를 선택하기 위해 다시 랭크를 매깁니다. 재랭크 모델 API 설정이 필요합니다.", "retrieval.hybrid_search.title": "하이브리드 검색", - "retrieval.invertedIndex.title": "역인덱스", "retrieval.keyword_search.description": "역인덱스는 효율적인 검색을 위해 사용되는 구조입니다. 용어별로 구성된 각 용어는 해당 용어가 포함된 문서 또는 웹 페이지를 가리킵니다.", "retrieval.keyword_search.title": "반전 인덱스", "retrieval.semantic_search.description": "쿼리의 임베딩을 생성하고, 해당 벡터 표현에 가장 유사한 텍스트 청크를 검색합니다.", diff --git a/web/i18n/locales/ko-KR/login.json b/web/i18n/locales/ko-KR/login.json index e27229246be9a1..b7993f2025d331 100644 --- a/web/i18n/locales/ko-KR/login.json +++ b/web/i18n/locales/ko-KR/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "사용자 이름을 입력하세요.", "error.passwordEmpty": "비밀번호를 입력하세요.", "error.passwordInvalid": "비밀번호는 문자와 숫자를 포함하고 8 자 이상이어야 합니다.", - "error.registrationNotAllowed": "계정을 찾을 수 없습니다. 등록하려면 시스템 관리자에게 문의하십시오.", "error.ssoFailed": "SSO 로그인에 실패했습니다. 다시 시도하거나 관리자에게 문의하세요.", "explore": "Dify 를 탐색하세요", "forget": "비밀번호를 잊으셨나요?", diff --git a/web/i18n/locales/lo-LA/dataset.json b/web/i18n/locales/lo-LA/dataset.json index 4cb08c1e8712d0..486c4c2e74664f 100644 --- a/web/i18n/locales/lo-LA/dataset.json +++ b/web/i18n/locales/lo-LA/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "ຄົ້ນຫາຈາກຂໍ້ຄວາມເຕັມ", "retrieval.hybrid_search.description": "ດຳເນີນການຄົ້ນຫາຈາກຂໍ້ຄວາມເຕັມ ແລະ ເວັກເຕີພ້ອມໆກັນ, ຈາກນັ້ນຈັດລໍາດັບໃໝ່ເພື່ອເລືອກຜົນການຄົ້ນຫາທີ່ກົງກັບຄຳຖາມຂອງຜູ້ໃຊ້ຫຼາຍທີ່ສຸດ. ຜູ້ໃຊ້ສາມາດເລືອກກຳນົດນ້ຳໜັກ ຫຼື ຕັ້ງຄ່າຕົວແບບ Rerank ໄດ້.", "retrieval.hybrid_search.title": "ການຄົ້ນຫາແບບໄຮບຣິດ (ປະສົມ)", - "retrieval.invertedIndex.title": "ດັດຊະນີປີ້ນກັບ", "retrieval.keyword_search.description": "ດັດຊະນີປີ້ນກັບແມ່ນໂຄງສ້າງທີ່ໃຊ້ເພື່ອການດຶງຂໍ້ມູນທີ່ມີປະສິດທິພາບ. ຈັດລະບຽບຕາມຄຳສັບ, ໂດຍແຕ່ລະຄຳສັບຈະຊີ້ໄປຫາເອກະສານ ຫຼື ໜ້າເວັບທີ່ມີຄຳນັ້ນຢູ່.", "retrieval.keyword_search.title": "ດັດຊະນີປີ້ນກັບ", "retrieval.semantic_search.description": "ສ້າງ Embedding ຂອງຄຳຖາມ ແລະ ຄົ້ນຫາຕອນຂໍ້ມູນທີ່ມີຄວາມຄ້າຍຄືກັບເວັກເຕີຂອງມັນຫຼາຍທີ່ສຸດ.", diff --git a/web/i18n/locales/lo-LA/login.json b/web/i18n/locales/lo-LA/login.json index 69b7aab440d9c5..b2a48152f31de6 100644 --- a/web/i18n/locales/lo-LA/login.json +++ b/web/i18n/locales/lo-LA/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "ກະລຸນາປ້ອນຊື່", "error.passwordEmpty": "ກະລຸນາປ້ອນລະຫັດຜ່ານ", "error.passwordInvalid": "ລະຫັດຜ່ານຕ້ອງປະກອບມີຕົວອັກສອນ ແລະ ຕົວເລກ, ແລະ ຕ້ອງມີຄວາມຍາວຫຼາຍກວ່າ 8 ຕົວອັກສອນ", - "error.registrationNotAllowed": "ບໍ່ພົບບັນຊີ. ກະລຸນາຕິດຕໍ່ຜູ້ດູແລລະບົບເພື່ອລົງທະບຽນ.", "error.ssoFailed": "ການເຂົ້າສູ່ລະບົບ SSO ລົ້ມເຫຼວ. ກະລຸນາລອງໃໝ່ ຫຼື ຕິດຕໍ່ຜູ້ດູແລລະບົບຂອງທ່ານ.", "explore": "ສຳຫຼວດ Dify", "forget": "ລືມລະຫັດຜ່ານ?", diff --git a/web/i18n/locales/nl-NL/dataset.json b/web/i18n/locales/nl-NL/dataset.json index b3db4cdfe6d4a0..3c28796222a0cf 100644 --- a/web/i18n/locales/nl-NL/dataset.json +++ b/web/i18n/locales/nl-NL/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Full-Text Search", "retrieval.hybrid_search.description": "Execute full-text search and vector searches simultaneously, re-rank to select the best match for the user's query. Users can choose to set weights or configure to a Rerank model.", "retrieval.hybrid_search.title": "Hybrid Search", - "retrieval.invertedIndex.title": "Inverted Index", "retrieval.keyword_search.description": "Inverted Index is a structure used for efficient retrieval. Organized by terms, each term points to documents or web pages containing it.", "retrieval.keyword_search.title": "Inverted Index", "retrieval.semantic_search.description": "Generate query embeddings and search for the text chunk most similar to its vector representation.", diff --git a/web/i18n/locales/nl-NL/login.json b/web/i18n/locales/nl-NL/login.json index e552c1c97c85a9..879c30cfc0c49c 100644 --- a/web/i18n/locales/nl-NL/login.json +++ b/web/i18n/locales/nl-NL/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Name is required", "error.passwordEmpty": "Password is required", "error.passwordInvalid": "Password must contain letters and numbers, and the length must be greater than 8", - "error.registrationNotAllowed": "Account not found. Please contact the system admin to register.", "error.ssoFailed": "SSO-aanmelding mislukt. Probeer het opnieuw of neem contact op met je beheerder.", "explore": "Explore Dify", "forget": "Forgot your password?", diff --git a/web/i18n/locales/pl-PL/dataset.json b/web/i18n/locales/pl-PL/dataset.json index fe8f510579e24e..f4a3949990f3e1 100644 --- a/web/i18n/locales/pl-PL/dataset.json +++ b/web/i18n/locales/pl-PL/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Wyszukiwanie pełnotekstowe", "retrieval.hybrid_search.description": "Wykonaj jednocześnie pełnotekstowe wyszukiwanie i wyszukiwanie wektorowe, ponownie porządkuj, aby wybrać najlepsze dopasowanie dla zapytania użytkownika. Konieczna jest konfiguracja API Rerank model.", "retrieval.hybrid_search.title": "Wyszukiwanie hybrydowe", - "retrieval.invertedIndex.title": "Indeks odwrócony", "retrieval.keyword_search.description": "Inverted Index to struktura używana do efektywnego wyszukiwania. Uporządkowany według terminów, każdy termin wskazuje dokumenty lub strony internetowe, które go zawierają.", "retrieval.keyword_search.title": "Odwrócony indeks", "retrieval.semantic_search.description": "Generowanie osadzeń zapytań i wyszukiwanie fragmentów tekstu najbardziej podobnych do ich wektorowej reprezentacji.", diff --git a/web/i18n/locales/pl-PL/login.json b/web/i18n/locales/pl-PL/login.json index 1841cf639ca048..78ccb2da0971c3 100644 --- a/web/i18n/locales/pl-PL/login.json +++ b/web/i18n/locales/pl-PL/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Nazwa jest wymagana", "error.passwordEmpty": "Hasło jest wymagane", "error.passwordInvalid": "Hasło musi zawierać litery i cyfry, a jego długość musi być większa niż 8", - "error.registrationNotAllowed": "Nie znaleziono konta. Skontaktuj się z administratorem systemu, aby się zarejestrować.", "error.ssoFailed": "Logowanie SSO nie powiodło się. Spróbuj ponownie lub skontaktuj się z administratorem.", "explore": "Odkryj Dify", "forget": "Zapomniałeś hasła?", diff --git a/web/i18n/locales/pt-BR/dataset.json b/web/i18n/locales/pt-BR/dataset.json index aae7b7dfc7e41f..ba0a64272047ec 100644 --- a/web/i18n/locales/pt-BR/dataset.json +++ b/web/i18n/locales/pt-BR/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Pesquisa de Texto Completo", "retrieval.hybrid_search.description": "Execute pesquisas de texto completo e pesquisas vetoriais simultaneamente, reclassifique para selecionar a melhor correspondência para a consulta do usuário. A configuração da API do modelo de reclassificação é necessária.", "retrieval.hybrid_search.title": "Pesquisa Híbrida", - "retrieval.invertedIndex.title": "Índice Invertido", "retrieval.keyword_search.description": "O Índice Invertido é uma estrutura usada para recuperação eficiente. Organizado por termos, cada termo aponta para documentos ou páginas da web que o contêm.", "retrieval.keyword_search.title": "Índice invertido", "retrieval.semantic_search.description": "Gere incorporações de consulta e pesquise o trecho de texto mais semelhante à sua representação vetorial.", diff --git a/web/i18n/locales/pt-BR/login.json b/web/i18n/locales/pt-BR/login.json index dac67f3cdac36c..f8af4526f8d4db 100644 --- a/web/i18n/locales/pt-BR/login.json +++ b/web/i18n/locales/pt-BR/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "O nome é obrigatório", "error.passwordEmpty": "A senha é obrigatória", "error.passwordInvalid": "A senha deve conter letras e números e ter um comprimento maior que 8", - "error.registrationNotAllowed": "Conta não encontrada. Entre em contato com o administrador do sistema para se registrar.", "error.ssoFailed": "Falha no login via SSO. Tente novamente ou entre em contato com o administrador.", "explore": "Explorar o Dify", "forget": "Esqueceu sua senha?", diff --git a/web/i18n/locales/ro-RO/dataset.json b/web/i18n/locales/ro-RO/dataset.json index ef4cd5ddf7140f..988e24f9992dc3 100644 --- a/web/i18n/locales/ro-RO/dataset.json +++ b/web/i18n/locales/ro-RO/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Căutare Full-Text", "retrieval.hybrid_search.description": "Executați căutări full-text și căutări vectoriale în același timp, reclasificați pentru a selecta cea mai bună potrivire pentru interogarea utilizatorului. Configurarea API-ului modelului Rerank este necesară.", "retrieval.hybrid_search.title": "Căutare Hibridă", - "retrieval.invertedIndex.title": "Indice inversat", "retrieval.keyword_search.description": "Indexul inversat este o structură utilizată pentru o recuperare eficientă. Organizat pe termeni, fiecare termen indică documente sau pagini web care îl conțin.", "retrieval.keyword_search.title": "Indice inversat", "retrieval.semantic_search.description": "Generați încorporările interogărilor și căutați bucata de text cea mai similară cu reprezentarea sa vectorială.", diff --git a/web/i18n/locales/ro-RO/login.json b/web/i18n/locales/ro-RO/login.json index fb84c903aa3109..144c26b5f4adb1 100644 --- a/web/i18n/locales/ro-RO/login.json +++ b/web/i18n/locales/ro-RO/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Numele este obligatoriu", "error.passwordEmpty": "Parola este obligatorie", "error.passwordInvalid": "Parola trebuie să conțină litere și cifre, iar lungimea trebuie să fie mai mare de 8 caractere", - "error.registrationNotAllowed": "Contul nu a fost găsit. Vă rugăm să contactați administratorul de sistem pentru a vă înregistra.", "error.ssoFailed": "Autentificarea SSO a eșuat. Încercați din nou sau contactați administratorul.", "explore": "Explorează Dify", "forget": "Ai uitat parola?", diff --git a/web/i18n/locales/ru-RU/dataset.json b/web/i18n/locales/ru-RU/dataset.json index 69c19c35918af9..f7c6c5cb06b839 100644 --- a/web/i18n/locales/ru-RU/dataset.json +++ b/web/i18n/locales/ru-RU/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Полнотекстовый поиск", "retrieval.hybrid_search.description": "Выполняйте полнотекстовый поиск и векторный поиск одновременно, переранжируйте, чтобы выбрать наилучшее соответствие запросу пользователя. Пользователи могут выбрать установку весов или настройку модели переранжирования.", "retrieval.hybrid_search.title": "Гибридный поиск", - "retrieval.invertedIndex.title": "Обратный индекс", "retrieval.keyword_search.description": "Инвертированный индекс — это структура, используемая для эффективного извлечения. Каждый термин упорядочен по терминам и указывает на документы или веб-страницы, содержащие его.", "retrieval.keyword_search.title": "Инвертированный индекс", "retrieval.semantic_search.description": "Создайте встраивания запросов и найдите фрагмент текста, наиболее похожий на его векторное представление.", diff --git a/web/i18n/locales/ru-RU/login.json b/web/i18n/locales/ru-RU/login.json index 2930c307091392..9bdcc8b3d5ea26 100644 --- a/web/i18n/locales/ru-RU/login.json +++ b/web/i18n/locales/ru-RU/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Имя обязательно", "error.passwordEmpty": "Пароль обязателен", "error.passwordInvalid": "Пароль должен содержать буквы и цифры, а длина должна быть больше 8", - "error.registrationNotAllowed": "Аккаунт не найден. Пожалуйста, свяжитесь с системным администратором для регистрации.", "error.ssoFailed": "Не удалось войти через SSO. Попробуйте ещё раз или обратитесь к администратору.", "explore": "Изучить Dify", "forget": "Забыли пароль?", diff --git a/web/i18n/locales/sl-SI/dataset.json b/web/i18n/locales/sl-SI/dataset.json index 50ec81de46350f..54d411ddbcdf66 100644 --- a/web/i18n/locales/sl-SI/dataset.json +++ b/web/i18n/locales/sl-SI/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Iskanje celotnega besedila", "retrieval.hybrid_search.description": "Istočasno izvede iskanje celotnega besedila in vektorsko iskanje ter ponovno razvrsti zadetke, da izbere najboljše ujemanje za uporabnikovo poizvedbo. Uporabniki lahko določijo uteži ali konfigurirajo model za ponovno razvrščanje.", "retrieval.hybrid_search.title": "Hibridno iskanje", - "retrieval.invertedIndex.title": "Inverzni indeks", "retrieval.keyword_search.description": "Obrnjeni indeks je struktura, ki se uporablja za učinkovito iskanje. Vsak izraz, organiziran po izrazih, kaže na dokumente ali spletne strani, ki ga vsebujejo.", "retrieval.keyword_search.title": "Obrnjeni indeks", "retrieval.semantic_search.description": "Ustvari vdelke poizvedbe in poišči odstavke besedila, ki so najbolj podobni njegovi vektorski predstavitvi.", diff --git a/web/i18n/locales/sl-SI/login.json b/web/i18n/locales/sl-SI/login.json index 053dcc912a52be..3c58492a777dcb 100644 --- a/web/i18n/locales/sl-SI/login.json +++ b/web/i18n/locales/sl-SI/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Ime je obvezno", "error.passwordEmpty": "Geslo je obvezno", "error.passwordInvalid": "Geslo mora vsebovati črke in številke, dolžina pa mora biti več kot 8 znakov", - "error.registrationNotAllowed": "Računa ni mogoče najti. Za registracijo se obrnite na skrbnika sistema.", "error.ssoFailed": "Prijava SSO ni uspela. Poskusite znova ali se obrnite na skrbnika.", "explore": "Razišči Dify", "forget": "Ste pozabili geslo?", diff --git a/web/i18n/locales/th-TH/dataset.json b/web/i18n/locales/th-TH/dataset.json index 5454438a99d828..bad133bfeaf870 100644 --- a/web/i18n/locales/th-TH/dataset.json +++ b/web/i18n/locales/th-TH/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "การค้นหาข้อความแบบเต็ม", "retrieval.hybrid_search.description": "ดําเนินการค้นหาข้อความแบบเต็มและการค้นหาแบบเวกเตอร์พร้อมกันจัดอันดับใหม่เพื่อเลือกการจับคู่ที่ดีที่สุดสําหรับคําค้นหาของผู้ใช้ ผู้ใช้สามารถเลือกที่จะตั้งค่าน้ําหนักหรือกําหนดค่าเป็นโมเดล Rerank", "retrieval.hybrid_search.title": "การค้นหาแบบไฮบริด", - "retrieval.invertedIndex.title": "ดัชนีผกผัน", "retrieval.keyword_search.description": "Inverted Index เป็นโครงสร้างที่ใช้สําหรับการดึงข้อมูลอย่างมีประสิทธิภาพ จัดเรียงตามคําศัพท์ แต่ละคําชี้ไปที่เอกสารหรือหน้าเว็บที่มีคําดังกล่าว", "retrieval.keyword_search.title": "ดัชนีกลับด้าน", "retrieval.semantic_search.description": "สร้างการฝังแบบสอบถามและค้นหาส่วนข้อความที่คล้ายกับการแสดงเวกเตอร์มากที่สุด", diff --git a/web/i18n/locales/th-TH/login.json b/web/i18n/locales/th-TH/login.json index 52be3be2780b2a..7804d05e2511db 100644 --- a/web/i18n/locales/th-TH/login.json +++ b/web/i18n/locales/th-TH/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "ต้องระบุชื่อ", "error.passwordEmpty": "ต้องใช้รหัสผ่าน", "error.passwordInvalid": "รหัสผ่านต้องมีตัวอักษรและตัวเลข และความยาวต้องมากกว่า 8", - "error.registrationNotAllowed": "ไม่พบบัญชี โปรดติดต่อผู้ดูแลระบบเพื่อลงทะเบียน", "error.ssoFailed": "เข้าสู่ระบบด้วย SSO ไม่สำเร็จ โปรดลองอีกครั้งหรือติดต่อผู้ดูแลระบบของคุณ", "explore": "สํารวจ Dify", "forget": "ลืมรหัสผ่านใช่ไหม", diff --git a/web/i18n/locales/tr-TR/dataset.json b/web/i18n/locales/tr-TR/dataset.json index 16818656230291..8d56d6a1b65211 100644 --- a/web/i18n/locales/tr-TR/dataset.json +++ b/web/i18n/locales/tr-TR/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Tam Metin Arama", "retrieval.hybrid_search.description": "Tam metin arama ve vektör aramalarını aynı anda çalıştırın, kullanıcı sorgusu için en iyi eşleşmeyi seçmek için yeniden sıralayın. Kullanıcılar ağırlıklar ayarlayabilir veya bir Yeniden Sıralama modeli yapılandırabilir.", "retrieval.hybrid_search.title": "Hibrit Arama", - "retrieval.invertedIndex.title": "Ters İndeks", "retrieval.keyword_search.description": "Ters İndeks, verimli erişim için kullanılan bir yapıdır. Terimlere göre düzenlenen her terim, onu içeren belgelere veya web sayfalarına işaret eder.", "retrieval.keyword_search.title": "Ters Çevrilmiş İndeks", "retrieval.semantic_search.description": "Sorgu yerleştirmelerini oluşturun ve vektör temsiline en benzeyen metin parçasını arayın.", diff --git a/web/i18n/locales/tr-TR/login.json b/web/i18n/locales/tr-TR/login.json index 46f1d0e5f6bbb9..a24d79befd5dd3 100644 --- a/web/i18n/locales/tr-TR/login.json +++ b/web/i18n/locales/tr-TR/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "İsim gereklidir", "error.passwordEmpty": "Şifre gereklidir", "error.passwordInvalid": "Şifre harf ve rakamlardan oluşmalı ve uzunluğu 8 karakterden fazla olmalıdır", - "error.registrationNotAllowed": "Hesap bulunamadı. Kayıt olmak için lütfen sistem yöneticisi ile iletişime geçin.", "error.ssoFailed": "SSO ile giriş başarısız oldu. Lütfen tekrar deneyin veya yöneticinize başvurun.", "explore": "Dify'yi Keşfet", "forget": "Şifrenizi mi unuttunuz?", diff --git a/web/i18n/locales/uk-UA/dataset.json b/web/i18n/locales/uk-UA/dataset.json index efb0d0767b576f..b2d46f6d15dbc5 100644 --- a/web/i18n/locales/uk-UA/dataset.json +++ b/web/i18n/locales/uk-UA/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Повнотекстовий пошук", "retrieval.hybrid_search.description": "Виконуйте повнотекстовий пошук і векторний пошук одночасно, повторно ранжуючи, щоб вибрати найкращу відповідність на запит користувача. Необхідна конфігурація Rerank model API.", "retrieval.hybrid_search.title": "Гібридний пошук", - "retrieval.invertedIndex.title": "Перевернутий індекс", "retrieval.keyword_search.description": "Перевернутий індекс — це структура, яка використовується для ефективного пошуку. Упорядкований за термінами, кожен термін вказує на документи або веб-сторінки, що містять його.", "retrieval.keyword_search.title": "Перевернутий індекс", "retrieval.semantic_search.description": "Генерує вбудовування запитів і шукає фрагмент тексту, найбільш схожий на його векторне представлення.", diff --git a/web/i18n/locales/uk-UA/login.json b/web/i18n/locales/uk-UA/login.json index 646b95797ea5ae..b5ca10c1fcf337 100644 --- a/web/i18n/locales/uk-UA/login.json +++ b/web/i18n/locales/uk-UA/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Ім'я обов'язкове", "error.passwordEmpty": "Пароль є обов’язковим", "error.passwordInvalid": "Пароль повинен містити літери та цифри, а довжина повинна бути більшою за 8", - "error.registrationNotAllowed": "Аккаунт не знайдено. Будь ласка, зверніться до адміністратора системи для реєстрації.", "error.ssoFailed": "Не вдалося увійти через SSO. Спробуйте ще раз або зверніться до адміністратора.", "explore": "Досліджуйте Dify", "forget": "Забули пароль?", diff --git a/web/i18n/locales/vi-VN/dataset.json b/web/i18n/locales/vi-VN/dataset.json index fbf7506743ebfb..565d5c2c9d2043 100644 --- a/web/i18n/locales/vi-VN/dataset.json +++ b/web/i18n/locales/vi-VN/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "Tìm kiếm Toàn văn bản", "retrieval.hybrid_search.description": "Thực hiện tìm kiếm toàn văn bản và tìm kiếm vector đồng thời, sắp xếp lại để chọn kết quả phù hợp nhất với truy vấn của người dùng. Yêu cầu cấu hình API mô hình Rerank.", "retrieval.hybrid_search.title": "Tìm kiếm Kết hợp", - "retrieval.invertedIndex.title": "Chỉ mục đảo ngược", "retrieval.keyword_search.description": "Chỉ số đảo ngược là một cấu trúc được sử dụng để truy xuất hiệu quả. Được sắp xếp theo thuật ngữ, mỗi thuật ngữ trỏ đến các tài liệu hoặc trang web có chứa nó.", "retrieval.keyword_search.title": "Chỉ số đảo ngược", "retrieval.semantic_search.description": "Tạo các nhúng truy vấn và tìm kiếm đoạn văn bản tương tự nhất với biểu diễn vector của nó.", diff --git a/web/i18n/locales/vi-VN/login.json b/web/i18n/locales/vi-VN/login.json index 49f960dd92ad87..200b4cffdfdd20 100644 --- a/web/i18n/locales/vi-VN/login.json +++ b/web/i18n/locales/vi-VN/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "Vui lòng nhập tên", "error.passwordEmpty": "Vui lòng nhập mật khẩu", "error.passwordInvalid": "Mật khẩu phải chứa cả chữ và số, và có độ dài ít nhất 8 ký tự", - "error.registrationNotAllowed": "Không tìm thấy tài khoản. Vui lòng liên hệ với quản trị viên hệ thống để đăng ký.", "error.ssoFailed": "Đăng nhập SSO không thành công. Vui lòng thử lại hoặc liên hệ với quản trị viên.", "explore": "Khám phá Dify", "forget": "Quên mật khẩu?", diff --git a/web/i18n/locales/zh-Hans/dataset.json b/web/i18n/locales/zh-Hans/dataset.json index 57ce37b4197e62..0b2caaa64a1d0a 100644 --- a/web/i18n/locales/zh-Hans/dataset.json +++ b/web/i18n/locales/zh-Hans/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "全文检索", "retrieval.hybrid_search.description": "同时执行全文检索和向量检索,并应用重排序步骤,从两类查询结果中选择匹配用户问题的最佳结果,用户可以选择设置权重或配置重新排序模型。", "retrieval.hybrid_search.title": "混合检索", - "retrieval.invertedIndex.title": "倒排索引", "retrieval.keyword_search.description": "倒排索引是一种用于高效检索的结构。按术语组织,每个术语指向包含它的文档或网页", "retrieval.keyword_search.title": "倒排索引", "retrieval.semantic_search.description": "通过生成查询嵌入并查询与其向量表示最相似的文本分段", diff --git a/web/i18n/locales/zh-Hans/login.json b/web/i18n/locales/zh-Hans/login.json index 57a3e12d424e49..a896ec6c36fb80 100644 --- a/web/i18n/locales/zh-Hans/login.json +++ b/web/i18n/locales/zh-Hans/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "用户名不能为空", "error.passwordEmpty": "密码不能为空", "error.passwordInvalid": "密码必须包含字母和数字,且长度不小于 8 位", - "error.registrationNotAllowed": "账户不存在,请联系系统管理员注册账户", "error.ssoFailed": "SSO 登录失败。请重试或联系管理员。", "explore": "探索 Dify", "forget": "忘记密码?", diff --git a/web/i18n/locales/zh-Hant/dataset.json b/web/i18n/locales/zh-Hant/dataset.json index cf2d7ceefc37eb..b5e2e56508e9d5 100644 --- a/web/i18n/locales/zh-Hant/dataset.json +++ b/web/i18n/locales/zh-Hant/dataset.json @@ -143,7 +143,6 @@ "retrieval.full_text_search.title": "全文檢索", "retrieval.hybrid_search.description": "同時執行全文檢索和向量檢索,並應用重排序步驟,從兩類查詢結果中選擇匹配使用者問題的最佳結果,需配置 Rerank 模型 API", "retrieval.hybrid_search.title": "混合檢索", - "retrieval.invertedIndex.title": "反向索引", "retrieval.keyword_search.description": "倒掛索引是一種用於高效檢索的結構。依字詞組織,每個字詞都指向包含它的文件或網頁。", "retrieval.keyword_search.title": "倒掛索引", "retrieval.semantic_search.description": "透過生成查詢嵌入並查詢與其向量表示最相似的文字分段", diff --git a/web/i18n/locales/zh-Hant/login.json b/web/i18n/locales/zh-Hant/login.json index 2c5a2289838494..8611597170050d 100644 --- a/web/i18n/locales/zh-Hant/login.json +++ b/web/i18n/locales/zh-Hant/login.json @@ -30,7 +30,6 @@ "error.nameEmpty": "使用者名稱不能為空", "error.passwordEmpty": "密碼不能為空", "error.passwordInvalid": "密碼必須包含字母和數字,且長度不小於 8 位", - "error.registrationNotAllowed": "找不到帳戶。請聯繫系統管理員進行註冊。", "error.ssoFailed": "SSO 登入失敗。請重試或聯絡管理員。", "explore": "探索 Dify", "forget": "忘記密碼?", diff --git a/web/i18n/resources.generated.d.ts b/web/i18n/resources.generated.d.ts index 342c78af6bc150..dc699461944d26 100644 --- a/web/i18n/resources.generated.d.ts +++ b/web/i18n/resources.generated.d.ts @@ -2084,7 +2084,6 @@ export default interface Resources { 'retrieval.full_text_search.title': 'Full-Text Search' 'retrieval.hybrid_search.description': "Execute full-text search and vector searches simultaneously, re-rank to select the best match for the user's query. Users can choose to set weights or configure to a Rerank model." 'retrieval.hybrid_search.title': 'Hybrid Search' - 'retrieval.invertedIndex.title': 'Inverted Index' 'retrieval.keyword_search.description': 'Inverted Index is a structure used for efficient retrieval. Organized by terms, each term points to documents or web pages containing it.' 'retrieval.keyword_search.title': 'Inverted Index' 'retrieval.semantic_search.description': 'Generate query embeddings and search for the text chunk most similar to its vector representation.' @@ -2969,7 +2968,6 @@ export default interface Resources { 'error.nameEmpty': 'Name is required' 'error.passwordEmpty': 'Password is required' 'error.passwordInvalid': 'Password must contain letters and numbers, and the length must be greater than 8' - 'error.registrationNotAllowed': 'Account not found. Please contact the system admin to register.' 'error.ssoFailed': 'SSO login failed. Please try again or contact your administrator.' explore: 'Explore Dify' forget: 'Forgot your password?' diff --git a/web/models/access-control.ts b/web/models/access-control.ts index 81f1a06246eb59..64b910f9f25874 100644 --- a/web/models/access-control.ts +++ b/web/models/access-control.ts @@ -102,24 +102,6 @@ export type RoleListResponse = { pagination: Pagination } -export type CreateRoleRequest = { - name: string - description?: string - permission_keys?: PermissionKey[] -} - -export type UpdateRolesRequest = { - id: string - name: string - description?: string - permission_keys?: PermissionKey[] -} - -export type CopyWorkspaceRoleRequest = { - roleId: string - copy_member: boolean -} - export type AccessPolicyResourceType = 'app' | 'dataset' | 'agent' type AccessPolicyCategory = 'global_system_default' | 'global_custom' @@ -169,20 +151,6 @@ export type GetDatasetAccessPolicyByDatasetIdResponse = { items: AccessPolicyWithBindings[] } -export type RolesOfMemberResponse = { - account_id: string - roles: Role[] -} - -export type UpdateRolesOfMemberRequest = { - memberId: string - roleIds: string[] -} - -export type GetMembersOfRoleRequest = { - roleId: string -} & PaginationParameters - type Account = { account_id: string account_name: string @@ -201,11 +169,6 @@ type ResourceUserAccessSettingsResponse = { pagination: Pagination } -export type GetMembersOfRoleResponse = { - data: Account[] - pagination: Pagination -} - export type GetAppUserAccessSettingsResponse = ResourceUserAccessSettingsResponse export type GetDatasetUserAccessSettingsResponse = ResourceUserAccessSettingsResponse diff --git a/web/models/common.ts b/web/models/common.ts index c369667412474c..f79aa12db55dca 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -1,31 +1,13 @@ -import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' import type { AppConfigJsonValue } from '@dify/contracts/api/console/apps/types.gen' -import type { Role } from './access-control' +import type { UploadConfig } from '@dify/contracts/api/console/files/types.gen' +import type { AccountWithRoleResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { I18nText } from '@/i18n/language' -import type { Model } from '@/types/app' export type CommonResponse = { result: 'success' | 'fail' } -export type SetupStatusResponse = { - step: 'finished' | 'not_started' - setup_at?: Date -} - -export type InitValidateStatusResponse = { - status: 'finished' | 'not_started' -} - -export type Member = Pick & { - avatar: string - last_login_at?: string - last_active_at?: string - created_at?: string - status: 'pending' | 'active' | 'banned' | 'closed' - role: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator' - roles: Role[] -} +export type Member = AccountWithRoleResponse export type DataSourceNotionPage = { page_icon: null | { @@ -64,20 +46,7 @@ export const DataSourceProvider = { } as const export type DataSourceProvider = (typeof DataSourceProvider)[keyof typeof DataSourceProvider] -export type FileUploadConfigResponse = { - batch_count_limit: number - image_file_size_limit?: number | string // default is 10MB - image_file_batch_limit: number // default is 10, for dataset attachment upload only - single_chunk_attachment_limit: number // default is 10, for dataset attachment upload only - attachment_image_file_size_limit: number // default is 2MB, for dataset attachment upload only - file_size_limit: number // default is 15MB - knowledge_file_size_limit?: number // current workspace's knowledge upload limit in MB - audio_file_size_limit?: number // default is 50MB - video_file_size_limit?: number // default is 100MB - skill_file_size_limit?: number // default is 50MB - workflow_file_upload_limit?: number // default is 10 - file_upload_limit: number // default is 5 -} +export type FileUploadConfigResponse = UploadConfig export type CodeBasedExtensionForm = { type: string @@ -111,13 +80,3 @@ export type ExternalDataTool = { api_based_extension_id?: string } & Record } - -export type StructuredOutputRulesRequestBody = { - instruction: string - model_config: Model -} - -export type StructuredOutputRulesResponse = { - output: string - error?: string -} diff --git a/web/models/datasets.ts b/web/models/datasets.ts index 9d530c7ff2b591..6b9a6699b8fd0b 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -1,12 +1,34 @@ -import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' -import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' +import type { + ChildChunkListResponse, + ChildChunkResponse, + ConsoleSegmentListResponse, + DocumentDetailResponse as ContractDocumentDetailResponse, + DocumentResponse as ContractDocumentResponse, + IndexingEstimateResponse as ContractIndexingEstimateResponse, + ProcessRuleResponse as ContractProcessRuleResponse, + RelatedAppResponse as ContractRelatedAppResponse, + DatasetAndDocumentResponse, + DatasetDetailWithPartialMembersResponse, + DatasetDocMetadataResponse, + DatasetListResponse, + DatasetQueryContentResponse, + DatasetQueryDetailResponse, + DocumentStatusResponse, + DocumentWithSegmentsResponse, + ExternalHitTestingRecordResponse, + ExternalHitTestingResponse, + ExternalKnowledgeApiResponse, + HitTestingChildChunk as GeneratedHitTestingChildChunk, + HitTestingResponse as GeneratedHitTestingResponse, + HitTestingRecord as HitTestingRecordResponse, + NotionInfo as NotionInfoPayload, + PreProcessingRule as PreProcessingRulePayload, + RelatedAppListResponse, + SegmentResponse, +} from '@dify/contracts/api/console/datasets/types.gen' import type { DataSourceNotionPage, DataSourceProvider } from './common' -import type { DatasourceType } from './pipeline' import type { IndexingType } from '@/app/components/datasets/create/step-two' -import type { MetadataItemWithValue } from '@/app/components/datasets/metadata/types' -import type { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types' import type { AppIconType, RetrievalConfig, TransferMethod } from '@/types/app' -import type { SegmentImportStatus } from '@/types/dataset' import type { I18nKeysByPrefix } from '@/types/i18n' import { PermissionLevel } from './permission' @@ -32,12 +54,7 @@ export const ChunkingMode = { export type ChunkingMode = (typeof ChunkingMode)[keyof typeof ChunkingMode] -export type MetadataInDoc = { - value: string - id: string - type: MetadataFilteringVariableType - name: string -} +export type MetadataInDoc = DatasetDocMetadataResponse export type IconInfo = { icon: string @@ -53,98 +70,9 @@ export type SummaryIndexSetting = { summary_prompt?: string } -export type DataSet = { - id: string - name: string - indexing_status: DocumentIndexingStatus - icon_info: IconInfo - description: string - permission: DatasetPermission - data_source_type: DataSourceType - indexing_technique: IndexingType - author_name?: string - created_by: string - maintainer?: string - updated_by: string - updated_at: number - app_count: number - doc_form: ChunkingMode - document_count: number - total_document_count: number - total_available_documents?: number - word_count: number - provider: string - embedding_model: string - embedding_model_provider: string - embedding_available: boolean - retrieval_model_dict: RetrievalConfig - retrieval_model: RetrievalConfig - tags: Tag[] - partial_member_list?: string[] - external_knowledge_info: { - external_knowledge_id: string - external_knowledge_api_id: string - external_knowledge_api_name: string - external_knowledge_api_endpoint: string - } - external_retrieval_model: { - top_k: number - score_threshold: number - score_threshold_enabled: boolean - } - built_in_field_enabled: boolean - doc_metadata?: MetadataInDoc[] - keyword_number?: number - pipeline_id?: string - is_published?: boolean // Indicates if the pipeline is published - runtime_mode: 'rag_pipeline' | 'general' - enable_api: boolean // Indicates if the service API is enabled - is_multimodal: boolean // Indicates if the dataset supports multimodal - summary_index_setting?: SummaryIndexSetting - /** ACL permission keys */ - permission_keys?: string[] -} +export type DataSet = DatasetDetailWithPartialMembersResponse -export type ExternalAPIItem = { - id: string - tenant_id: string - name: string - description: string - settings: { - endpoint: string - api_key: string - } - dataset_bindings: { id: string; name: string }[] - created_by: string - created_at: string -} - -export type ExternalKnowledgeItem = { - id: string - name: string - description: string | null - provider: 'external' - permission: DatasetPermission - data_source_type: null - indexing_technique: null - app_count: number - document_count: number - word_count: number - created_by: string - created_at: string - updated_by: string - updated_at: string - tags: Tag[] -} - -export type ExternalAPIDeleteResponse = { - result: 'success' | 'error' -} - -export type ExternalAPIUsage = { - is_using: boolean - count: number -} +export type ExternalAPIItem = ExternalKnowledgeApiResponse export type CustomFile = File & { id?: string @@ -197,7 +125,6 @@ export type FileItem = { } export type FetchDatasetsParams = { - url: string params: { page: number ids?: string[] @@ -208,60 +135,16 @@ export type FetchDatasetsParams = { } } -export type DatasetListRequest = { - initialPage: number - tag_ids?: string[] - limit: number - include_all?: boolean - keyword?: string -} - -export type DataSetListResponse = { - data: DataSet[] - has_more: boolean - limit: number - page: number - total: number -} +export type DataSetListResponse = DatasetListResponse export type QA = { question: string answer: string } -type IndexingEstimateResponse = { - tokens: number - total_price: number - currency: string - total_segments: number - preview: Array<{ content: string; child_chunks: string[]; summary?: string }> - qa_preview?: QA[] -} - -export type FileIndexingEstimateResponse = { - total_nodes: number -} & IndexingEstimateResponse +export type FileIndexingEstimateResponse = ContractIndexingEstimateResponse -export type IndexingStatusResponse = { - id: string - indexing_status: DocumentIndexingStatus - processing_started_at: number - parsing_completed_at: number - cleaning_completed_at: number - splitting_completed_at: number - completed_at: any - paused_at: any - error: any - error_code?: 'vector_space_estimate_exceeded' | null - estimated_vector_space_mb?: number | null - vector_space_limit_mb?: number | null - stopped_at: any - completed_segments: number - total_segments: number -} -export type IndexingStatusBatchResponse = { - data: IndexingStatusResponse[] -} +export type IndexingStatusResponse = DocumentStatusResponse export const ProcessMode = { general: 'custom', @@ -272,28 +155,16 @@ export type ProcessMode = (typeof ProcessMode)[keyof typeof ProcessMode] export type ParentMode = 'full-doc' | 'paragraph' -export type ProcessRuleResponse = { - mode: ProcessMode - rules: Rules - limits: Limits - summary_index_setting?: SummaryIndexSetting -} +export type ProcessRuleResponse = ContractProcessRuleResponse export type Rules = { pre_processing_rules: PreProcessingRule[] segmentation: Segmentation - parent_mode: ParentMode - subchunk_segmentation: Segmentation + parent_mode?: ParentMode + subchunk_segmentation?: Segmentation } -type Limits = { - indexing_max_segmentation_tokens_length: number -} - -export type PreProcessingRule = { - id: string - enabled: boolean -} +export type PreProcessingRule = PreProcessingRulePayload type Segmentation = { separator: string @@ -301,16 +172,6 @@ type Segmentation = { chunk_overlap?: number } -export type DocumentIndexingStatus = - | 'waiting' - | 'parsing' - | 'cleaning' - | 'splitting' - | 'indexing' - | 'paused' - | 'error' - | 'completed' - export const DisplayStatusList = [ 'queuing', 'indexing', @@ -324,6 +185,9 @@ export const DisplayStatusList = [ export type DocumentDisplayStatus = (typeof DisplayStatusList)[number] +export const isDocumentDisplayStatus = (value: unknown): value is DocumentDisplayStatus => + DisplayStatusList.some((status) => status === value) + export type LegacyDataSourceInfo = { upload_file: { id: string @@ -376,70 +240,14 @@ export type OnlineDocumentInfo = { } } -export type OnlineDriveInfo = { - bucket: string - credential_id: string - id: string - name: string - type: 'file' | 'folder' -} - export type UploadFileIdInfo = { upload_file_id: string } -export type DataSourceInfo = - | LegacyDataSourceInfo - | LocalFileInfo - | OnlineDocumentInfo - | WebsiteCrawlInfo - | UploadFileIdInfo +export type InitialDocumentDetail = ContractDocumentResponse -type InitialDocumentDetail = { - id: string - batch: string - position: number - dataset_id: string - data_source_type: DataSourceType | DatasourceType - data_source_info: DataSourceInfo - dataset_process_rule_id: string - name: string - created_from: 'rag-pipeline' | 'api' | 'web' - created_by: string - created_at: number - indexing_status: DocumentIndexingStatus - display_status: DocumentDisplayStatus - completed_segments?: number - total_segments?: number - doc_form: ChunkingMode - doc_language: string - summary_index_status?: string -} - -export type SimpleDocumentDetail = InitialDocumentDetail & { - enabled: boolean - word_count: number - error?: string | null - archived: boolean - updated_at: number - hit_count: number - dataset_process_rule_id?: string - data_source_detail_dict?: { - upload_file: { - name: string - extension: string - } - } - doc_metadata?: MetadataItemWithValue[] -} - -export type DocumentListResponse = { - data: SimpleDocumentDetail[] - has_more: boolean - total: number - page: number - limit: number -} +// A document in the dataset document list. +export type SimpleDocumentDetail = DocumentWithSegmentsResponse type DocumentReq = { original_document_id?: string @@ -450,18 +258,14 @@ type DocumentReq = { summary_index_setting?: SummaryIndexSetting } -export type CreateDocumentReq = DocumentReq & { +export type CreateDocumentReq = Omit & { + indexing_technique: IndexingType data_source: DataSource retrieval_model: RetrievalConfig embedding_model: string embedding_model_provider: string } -export type IndexingEstimateParams = DocumentReq & - Partial & { - dataset_id: string - } - type DataSource = { type: DataSourceType info_list: { @@ -478,11 +282,7 @@ type DataSource = { } } -export type NotionInfo = { - workspace_id: string - pages: DataSourceNotionPage[] - credential_id: string -} +export type NotionInfo = NotionInfoPayload export type ProcessRule = { mode: ProcessMode @@ -490,49 +290,10 @@ export type ProcessRule = { summary_index_setting?: SummaryIndexSetting } -export type createDocumentResponse = { - dataset?: DataSet - batch: string - documents: InitialDocumentDetail[] -} - -export type FullDocumentDetail = SimpleDocumentDetail & { - batch: string - created_api_request_id: string - processing_started_at: number - parsing_completed_at: number - cleaning_completed_at: number - splitting_completed_at: number - tokens: number - indexing_latency: number - completed_at: number - paused_by: string - paused_at: number - stopped_at: number - indexing_status: string - disabled_at: number - disabled_by: string - archived_reason: 'rule_modified' | 're_upload' - archived_by: string - archived_at: number - doc_type?: DocType | null | 'others' - doc_metadata?: DocMetadata | null - segment_count: number - dataset_process_rule: ProcessRule - document_process_rule: ProcessRule - [key: string]: any -} +export type createDocumentResponse = DatasetAndDocumentResponse -type DocMetadata = { - title: string - language: string - author: string - publisher: string - publicationDate: string - ISBN: string - category: string - [key: string]: string -} +// A dataset document as the document detail query returns it. +export type FullDocumentDetail = ContractDocumentDetailResponse type CustomizableDocType = | 'book' @@ -545,10 +306,6 @@ type CustomizableDocType = type FixedDocType = 'synced_from_github' | 'synced_from_notion' | 'wikipedia_entry' export type DocType = CustomizableDocType | FixedDocType -export type DocumentDetailResponse = FullDocumentDetail - -type SegmentStatus = 'waiting' | 'completed' | 'error' | 'indexing' - export type Attachment = { id: string name: string @@ -558,149 +315,17 @@ export type Attachment = { source_url: string } -export type SegmentDetailModel = { - id: string - position: number - document_id: string - content: string - sign_content: string - word_count: number - tokens: number - keywords: string[] - index_node_id: string - index_node_hash: string - hit_count: number - enabled: boolean - disabled_at: number - disabled_by: string - status: SegmentStatus - created_by: string - created_at: number - indexing_at: number - completed_at: number - error: string | null - stopped_at: number - answer?: string - summary?: string - child_chunks?: ChildChunkDetail[] - updated_at: number - attachments: Attachment[] -} - -export type SegmentsResponse = { - data: SegmentDetailModel[] - has_more: boolean - limit: number - total: number - total_pages: number - page: number -} - -export type Query = { - content: string - content_type: 'text_query' | 'image_query' - file_info: Attachment | null -} - -export type HitTestingRecord = { - id: string - source: 'app' | 'hit_testing' | 'plugin' - source_app_id: string - created_by_role: 'account' | 'end_user' - created_by: string - created_at: number - queries: Query[] -} - -export type HitTestingChildChunk = { - id: string - content: string - position: number - score: number -} -export type HitTesting = { - segment: Segment - content: Segment - score: number - tsne_position: TsnePosition - child_chunks: HitTestingChildChunk[] | null - files: Attachment[] - summary?: string -} - -export type ExternalKnowledgeBaseHitTesting = { - content: string - title: string - score: number - metadata: { - 'x-amz-bedrock-kb-source-uri': string - 'x-amz-bedrock-kb-data-source-id': string - } -} - -type Segment = { - id: string - document: Document - content: string - sign_content: string - position: number - word_count: number - tokens: number - keywords: string[] - hit_count: number - index_node_hash: string - answer: string -} - -type Document = { - id: string - data_source_type: string - name: string - doc_type: DocType -} - -export type HitTestingRecordsResponse = { - data: HitTestingRecord[] - has_more: boolean - limit: number - total: number - page: number -} +export type SegmentDetailModel = SegmentResponse -type TsnePosition = { - x: number - y: number -} +export type SegmentsResponse = ConsoleSegmentListResponse -export type HitTestingResponse = { - query: { - content: string - tsne_position: TsnePosition - } - records: Array -} +export type Query = DatasetQueryContentResponse -export type ExternalKnowledgeBaseHitTestingResponse = { - query: { - content: string - } - records: Array -} +export type HitTestingRecord = DatasetQueryDetailResponse -export type RelatedApp = { - id: string - name: string - mode: AppMode - icon_type: AppIconType | null - icon: string - icon_background: string - icon_url: string -} +export type RelatedApp = ContractRelatedAppResponse -export type RelatedAppResponse = { - data: Array - total: number -} +export type RelatedAppResponse = RelatedAppListResponse export type SegmentUpdater = { content: string @@ -711,11 +336,6 @@ export type SegmentUpdater = { attachment_ids?: string[] } -export type ErrorDocsResponse = { - data: IndexingStatusResponse[] - total: number -} - export type SelectedDatasetsMode = { allHighQuality: boolean allHighQualityVectorSearch: boolean @@ -758,26 +378,9 @@ export const DEFAULT_WEIGHTED_SCORE = { }, } -type ChildChunkType = 'automatic' | 'customized' +export type ChildChunkDetail = ChildChunkResponse -export type ChildChunkDetail = { - id: string - position: number - segment_id: string - content: string - word_count: number - created_at: number - updated_at: number - type: ChildChunkType -} - -export type ChildSegmentsResponse = { - data: ChildChunkDetail[] - total: number - total_pages: number - page: number - limit: number -} +export type ChildSegmentsResponse = ChildChunkListResponse // Used in api url export const DocumentActionType = { @@ -791,17 +394,6 @@ export const DocumentActionType = { export type DocumentActionType = (typeof DocumentActionType)[keyof typeof DocumentActionType] -export type UpdateDocumentBatchParams = { - datasetId: string - documentId?: string - documentIds?: string[] | string -} - -export type BatchImportResponse = { - job_id: string - job_status: SegmentImportStatus -} - export const DOC_FORM_ICON_CLASS_WITH_BG: Record = { [ChunkingMode.text]: 'i-custom-public-knowledge-dataset-card-general', [ChunkingMode.qa]: 'i-custom-public-knowledge-dataset-card-qa', @@ -819,29 +411,6 @@ export const DOC_FORM_TEXT: Record = { // [ChunkingMode.graph]: 'graph', // todo: Graph RAG } -export type CreateDatasetReq = { - yaml_content?: string -} - -export type CreateDatasetResponse = { - id: string - name: string - description: string - permission: DatasetPermission - indexing_technique: IndexingType - created_by: string - created_at: number - updated_by: string - updated_at: number - pipeline_id: string - dataset_id: string -} - -export type IndexingStatusBatchRequest = { - datasetId: string - batchId: string -} - export type HitTestingRequest = { query: string attachment_ids: string[] @@ -856,3 +425,9 @@ export type ExternalKnowledgeBaseHitTestingRequest = { score_threshold_enabled: boolean } } + +export type HitTestingChildChunk = GeneratedHitTestingChildChunk +export type HitTesting = HitTestingRecordResponse +export type HitTestingResponse = GeneratedHitTestingResponse +export type ExternalKnowledgeBaseHitTesting = ExternalHitTestingRecordResponse +export type ExternalKnowledgeBaseHitTestingResponse = ExternalHitTestingResponse diff --git a/web/models/log.ts b/web/models/log.ts index f94a11d7875eca..0bf99b4fe2e973 100644 --- a/web/models/log.ts +++ b/web/models/log.ts @@ -1,36 +1,10 @@ +import type { + AgentIterationLogResponse, + AgentLogResponse, + AgentToolCallResponse, +} from '@dify/contracts/api/console/apps/types.gen' import type { Viewport } from 'reactflow' -import type { Metadata, ThoughtItem } from '@/app/components/base/chat/chat/type' import type { Edge, Node } from '@/app/components/workflow/types' -import type { VisionFile } from '@/types/app' - -type CompletionParamsType = { - max_tokens: number - temperature: number - top_p: number - stop: string[] - presence_penalty: number - frequency_penalty: number -} - -type LogModelConfig = { - name: string - provider: string - completion_params: CompletionParamsType -} - -type ModelConfigDetail = { - introduction: string - prompt_template: string - prompt_variables: Array<{ - key: string - name: string - description: string - type: string | number - default: string - options: string[] - }> - completion_params: CompletionParamsType -} export type LogAnnotation = { id: string @@ -50,161 +24,8 @@ export type Annotation = { created_at?: number } -type MessageContent = { - id: string - conversation_id: string - query: string - inputs: Record - message: { role: string; text: string; files?: VisionFile[] }[] - message_tokens: number - answer_tokens: number - answer: string - provider_response_latency: number - created_at: number - annotation: LogAnnotation - annotation_hit_history: { - annotation_id: string - annotation_create_account: { - id: string - name: string - email: string - } - created_at: number - } - feedbacks: Array<{ - rating: 'like' | 'dislike' | null - content: string | null - from_source?: 'admin' | 'user' - from_end_user_id?: string - }> - message_files: VisionFile[] - metadata: Metadata - agent_thoughts: ThoughtItem[] - workflow_run_id: string - parent_message_id: string | null -} - -export type CompletionConversationGeneralDetail = { - id: string - status: 'normal' | 'finished' | 'paused' - from_source: 'api' | 'console' - from_end_user_id: string - from_end_user_session_id: string - from_account_id: string - read_at: Date - created_at: number - updated_at: number - annotation: Annotation - user_feedback_stats: { - like: number - dislike: number - } - admin_feedback_stats: { - like: number - dislike: number - } - model_config: { - provider: string - model_id: string - configs: Pick - } - message: Pick -} - -export type CompletionConversationFullDetailResponse = { - id: string - status: 'normal' | 'finished' - from_source: 'api' | 'console' - from_end_user_id: string - from_account_id: string - // read_at: Date - created_at: number - model_config: { - provider: string - model_id: string - configs: ModelConfigDetail - } - message: MessageContent -} - -export type CompletionConversationsResponse = { - data: Array - has_more: boolean - limit: number - total: number - page: number -} - -export type CompletionConversationsRequest = { - keyword: string - start: string - end: string - annotation_status: string - page: number - limit: number // The default value is 20 and the range is 1-100 -} - -export type ChatConversationGeneralDetail = Omit< - CompletionConversationGeneralDetail, - 'message' | 'annotation' -> & { - summary: string - message_count: number - annotated: boolean -} - -export type ChatConversationsResponse = { - data: Array - has_more: boolean - limit: number - total: number - page: number -} - -export type ChatConversationsRequest = CompletionConversationsRequest & { message_count: number } - -export type ChatConversationFullDetailResponse = Omit< - CompletionConversationGeneralDetail, - 'message' | 'model_config' -> & { - message_count: number - model_config: { - provider: string - model_id: string - configs: ModelConfigDetail - model: LogModelConfig - } -} - -export type ChatMessagesRequest = { - conversation_id: string - first_id?: string - limit: number -} -export type ChatMessage = MessageContent - -export type ChatMessagesResponse = { - data: Array - has_more: boolean - limit: number -} - export type MessageRating = 'like' | 'dislike' | null -export type LogMessageFeedbacksRequest = { - message_id: string - rating: MessageRating - content?: string -} - -export type LogMessageFeedbacksResponse = { - result: 'success' | 'error' -} - -export type LogMessageAnnotationsRequest = Omit - -export type LogMessageAnnotationsResponse = LogMessageFeedbacksResponse - export const WorkflowRunTriggeredFrom = { DEBUGGING: 'debugging', APP_RUN: 'app-run', @@ -230,30 +51,10 @@ export type TriggerMetadata = { icon_dark?: string | null } -type WorkflowLogDetails = { +export type WorkflowLogDetails = { trigger_metadata?: TriggerMetadata } -export type WorkflowRunDetail = { - id: string - version: string - status: - | 'scheduled' - | 'running' - | 'succeeded' - | 'failed' - | 'stopped' - | 'partial-succeeded' - | 'paused' - error?: string - triggered_from?: WorkflowRunTriggeredFrom - elapsed_time: number - total_tokens: number - total_price: number - currency: string - total_steps: number - finished_at: number -} type AccountInfo = { id: string name: string @@ -265,24 +66,6 @@ type EndUserInfo = { is_anonymous: boolean session_id: string } -export type WorkflowAppLogDetail = { - id: string - workflow_run: WorkflowRunDetail - details?: WorkflowLogDetails - created_from: 'service-api' | 'web-app' | 'explore' - created_by_role: 'account' | 'end_user' - created_by_account?: AccountInfo - created_by_end_user?: EndUserInfo - created_at: number - read_at?: number -} -export type WorkflowLogsResponse = { - data: Array - has_more: boolean - limit: number - total: number - page: number -} export type WorkflowRunDetailResponse = { id: string version: string @@ -318,77 +101,6 @@ export type WorkflowRunDetailResponse = { exceptions_count?: number } -type AgentLogMeta = { - status: string - executor: string - start_time: string - elapsed_time: number - total_tokens: number - agent_mode: string - iterations: number - error?: string -} - -export type ToolCall = { - status: string - error?: string | null - time_cost?: number - tool_icon: any - tool_input?: any - tool_output?: any - tool_name?: string - tool_label?: any - tool_parameters?: any -} - -export type AgentIteration = { - created_at: string - files: string[] - thought: string - tokens: number - tool_calls: ToolCall[] - tool_raw: { - inputs: string - outputs: string - } -} - -type AgentLogFile = { - id: string - type: string - url: string - name: string - belongs_to: string -} - -export type AgentLogDetailRequest = { - conversation_id: string - message_id: string -} - -export type AgentLogDetailResponse = { - meta: AgentLogMeta - iterations: AgentIteration[] - files: AgentLogFile[] -} - -type PauseType = - | { - type: 'human_input' - form_id: string - backstage_input_url: string - } - | { - type: 'breakpoint' - } - -type PauseDetail = { - node_id: string - node_title: string - pause_type: PauseType -} - -export type WorkflowPausedDetailsResponse = { - paused_at: string - paused_nodes: PauseDetail[] -} +export type AgentLogDetailResponse = AgentLogResponse +export type AgentIteration = AgentIterationLogResponse +export type ToolCall = AgentToolCallResponse diff --git a/web/models/pipeline.ts b/web/models/pipeline.ts index bea17fe7b321b9..41a8d15aebcea1 100644 --- a/web/models/pipeline.ts +++ b/web/models/pipeline.ts @@ -1,20 +1,11 @@ -import type { Viewport } from 'reactflow' -import type { DSLImportMode, DSLImportStatus } from './app' -import type { - ChunkingMode, - DocumentIndexingStatus, - FileIndexingEstimateResponse, - IconInfo, -} from './datasets' -import type { Dependency } from '@/app/components/plugins/types' import type { - Edge, - EnvironmentVariable, - Node, - SupportUploadFileTypes, -} from '@/app/components/workflow/types' + PipelineDocument, + PipelineGenerateResponse, +} from '@dify/contracts/api/console/rag/types.gen' +import type { Viewport } from 'reactflow' +import type { ChunkingMode, IconInfo } from './datasets' +import type { Edge, Node, SupportUploadFileTypes } from '@/app/components/workflow/types' import type { TransferMethod } from '@/types/app' -import type { NodeRunResult } from '@/types/workflow' import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types' export const DatasourceType = { @@ -26,11 +17,6 @@ export const DatasourceType = { export type DatasourceType = (typeof DatasourceType)[keyof typeof DatasourceType] -export type PipelineTemplateListParams = { - type: 'built-in' | 'customized' - language?: string -} - export type PipelineTemplate = { id: string name: string @@ -40,15 +26,6 @@ export type PipelineTemplate = { chunk_structure: ChunkingMode } -export type PipelineTemplateListResponse = { - pipeline_templates: PipelineTemplate[] -} - -export type PipelineTemplateByIdRequest = { - template_id: string - type: 'built-in' | 'customized' -} - export type PipelineTemplateByIdResponse = { id: string name: string @@ -64,59 +41,6 @@ export type PipelineTemplateByIdResponse = { created_by: string } -export type UpdateTemplateInfoRequest = { - template_id: string - name: string - icon_info: IconInfo - description: string -} - -export type UpdateTemplateInfoResponse = { - pipeline_id: string - name: string - icon: IconInfo - description: string - position: number -} - -export type DeleteTemplateResponse = { - code: number -} - -export type ExportTemplateDSLResponse = { - data: string -} - -export type ImportPipelineDSLRequest = { - mode: DSLImportMode - yaml_content?: string - yaml_url?: string - pipeline_id?: string -} - -export type ImportPipelineDSLResponse = { - id: string - status: DSLImportStatus - pipeline_id: string - dataset_id: string - current_dsl_version: string - imported_dsl_version: string - error?: string -} - -export type ImportPipelineDSLConfirmResponse = { - status: DSLImportStatus - pipeline_id: string - dataset_id: string - current_dsl_version: string - imported_dsl_version: string - error: string -} - -export type PipelineCheckDependenciesResponse = { - leaked_dependencies: Dependency[] -} - export const PipelineInputVarType = { textInput: 'text-input', paragraph: 'paragraph', @@ -145,7 +69,7 @@ export type RAGPipelineVariable = { label: string variable: string max_length?: number - default_value?: string + default_value?: string | number | boolean placeholder?: string unit?: string required: boolean @@ -159,104 +83,9 @@ export type RAGPipelineVariable = { export type InputVar = Omit export type RAGPipelineVariables = RAGPipelineVariable[] -export type PipelineProcessingParamsRequest = { - pipeline_id: string - node_id: string -} - -export type PipelineProcessingParamsResponse = { - variables: RAGPipelineVariables -} - -export type PipelinePreProcessingParamsRequest = { - pipeline_id: string - node_id: string -} - -export type PipelinePreProcessingParamsResponse = { - variables: RAGPipelineVariables -} - -export type PublishedPipelineInfoResponse = { - id: string - graph: { - nodes: Node[] - edges: Edge[] - viewport: Viewport - } - created_at: number - created_by: { - id: string - name: string - email: string - } - hash: string - updated_at: number - updated_by: { - id: string - name: string - email: string - } - environment_variables?: EnvironmentVariable[] - rag_pipeline_variables?: RAGPipelineVariables - version: string - marked_name: string - marked_comment: string -} - -export type PublishedPipelineRunRequest = { - pipeline_id: string - inputs: Record - start_node_id: string - datasource_type: DatasourceType - datasource_info_list: Array> - original_document_id?: string - is_preview: boolean -} - -export type PublishedPipelineRunPreviewResponse = { - task_iod: string - workflow_run_id: string - data: { - id: string - status: string - created_at: number - elapsed_time: number - error: string - finished_at: number - outputs: FileIndexingEstimateResponse - total_steps: number - total_tokens: number - workflow_id: string - } -} - -export type PublishedPipelineRunResponse = { - batch: string - dataset: { - chunk_structure: ChunkingMode - description: string - id: string - name: string - } - documents: InitialDocumentDetail[] -} - -export type InitialDocumentDetail = { - data_source_info: Record - data_source_type: DatasourceType - enable: boolean - error: string - id: string - indexing_status: DocumentIndexingStatus - name: string - position: number -} +export type PublishedPipelineRunResponse = PipelineGenerateResponse -export type PipelineExecutionLogRequest = { - dataset_id: string - document_id: string -} +export type InitialDocumentDetail = PipelineDocument export type PipelineExecutionLogResponse = { datasource_info: Record @@ -265,25 +94,6 @@ export type PipelineExecutionLogResponse = { datasource_node_id: string } -export type OnlineDocumentPreviewRequest = { - workspaceID: string - pageID: string - pageType: string - pipelineId: string - datasourceNodeId: string - credentialId: string -} - -export type OnlineDocumentPreviewResponse = { - content: string -} - -export type ConversionResponse = { - pipeline_id: string - dataset_id: string - status: 'success' | 'failed' -} - export const OnlineDriveFileType = { file: 'file', folder: 'folder', @@ -298,13 +108,3 @@ export type OnlineDriveFile = { size?: number type: OnlineDriveFileType } - -export type DatasourceNodeSingleRunRequest = { - pipeline_id: string - start_node_id: string - start_node_title: string - datasource_type: DatasourceType - datasource_info: Record -} - -export type DatasourceNodeSingleRunResponse = NodeRunResult diff --git a/web/package.json b/web/package.json index 7010fdfb09f752..1d04988c6c259a 100644 --- a/web/package.json +++ b/web/package.json @@ -113,7 +113,6 @@ "next-themes": "catalog:", "nuqs": "catalog:", "qrcode.react": "catalog:", - "qs": "catalog:", "react": "catalog:", "react-dom": "catalog:", "react-easy-crop": "catalog:", @@ -169,7 +168,6 @@ "@types/js-cookie": "catalog:", "@types/negotiator": "catalog:", "@types/node": "catalog:", - "@types/qs": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", "@types/sortablejs": "catalog:", diff --git a/web/service/__tests__/use-models.spec.tsx b/web/service/__tests__/use-models.spec.tsx deleted file mode 100644 index 29638e20bc3f6d..00000000000000 --- a/web/service/__tests__/use-models.spec.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { ReactNode } from 'react' -import type { ProviderCredential } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { renderHook, waitFor } from '@testing-library/react' -import { useGetProviderCredential } from '../use-models' - -const mockGet = vi.hoisted(() => vi.fn()) - -vi.mock('../base', () => ({ - del: vi.fn(), - get: mockGet, - post: vi.fn(), - put: vi.fn(), -})) - -const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - staleTime: 1000 * 60 * 5, - }, - }, - }) - - return ({ children }: { children: ReactNode }) => ( - {children} - ) -} - -describe('useGetProviderCredential', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should refetch credential details after the editor is reopened', async () => { - const previousCredential: ProviderCredential = { - credential_id: 'credential-id', - name: 'Tongyi', - credentials: { - api_key: '[__HIDDEN__]', - endpoint: 'https://old.example.com', - enable_request_metadata: false, - }, - } - const updatedCredential: ProviderCredential = { - ...previousCredential, - credentials: { - api_key: '[__HIDDEN__]', - endpoint: 'https://new.example.com', - enable_request_metadata: true, - }, - } - mockGet.mockResolvedValueOnce(previousCredential).mockResolvedValueOnce(updatedCredential) - const wrapper = createWrapper() - - const firstEditor = renderHook( - () => useGetProviderCredential(true, 'langgenius/tongyi/tongyi', 'credential-id'), - { wrapper }, - ) - await waitFor(() => { - expect(firstEditor.result.current.data).toEqual(previousCredential) - }) - firstEditor.unmount() - - const reopenedEditor = renderHook( - () => useGetProviderCredential(true, 'langgenius/tongyi/tongyi', 'credential-id'), - { wrapper }, - ) - - await waitFor(() => { - expect(mockGet).toHaveBeenCalledTimes(2) - expect(reopenedEditor.result.current.data).toEqual(updatedCredential) - }) - }) -}) diff --git a/web/service/__tests__/use-pipeline.spec.tsx b/web/service/__tests__/use-pipeline.spec.tsx deleted file mode 100644 index 8dae4041b1d079..00000000000000 --- a/web/service/__tests__/use-pipeline.spec.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { ReactNode } from 'react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { act, renderHook } from '@testing-library/react' -import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' -import { DSLImportMode, DSLImportStatus } from '@/models/app' -import { post } from '../base' -import { useImportPipelineDSL, useImportPipelineDSLConfirm } from '../use-pipeline' - -vi.mock('../base', () => ({ - post: vi.fn(), - get: vi.fn(), - patch: vi.fn(), - del: vi.fn(), -})) - -const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, - }) - - return ({ children }: { children: ReactNode }) => ( - {children} - ) -} - -describe('use-pipeline imports', () => { - beforeEach(() => { - vi.clearAllMocks() - vi.mocked(post).mockResolvedValue({ - id: 'import-id', - status: DSLImportStatus.COMPLETED, - pipeline_id: 'pipeline-id', - dataset_id: 'dataset-id', - current_dsl_version: '0.1.0', - imported_dsl_version: '0.1.0', - }) - }) - - it('should import pipeline DSL silently so callers can own error toasts', async () => { - const { result } = renderHook(() => useImportPipelineDSL(), { wrapper: createWrapper() }) - const request = { - mode: DSLImportMode.YAML_CONTENT, - yaml_content: 'rag_pipeline: {}', - } - - await act(async () => { - await result.current.mutateAsync(request) - }) - - expect(post).toHaveBeenCalledWith('/rag/pipelines/imports', { body: request }, { silent: true }) - }) - - it('should confirm pipeline DSL import silently so callers can own error toasts', async () => { - const { result } = renderHook(() => useImportPipelineDSLConfirm(), { wrapper: createWrapper() }) - - await act(async () => { - await result.current.mutateAsync('import-id') - }) - - expect(post).toHaveBeenCalledWith( - '/rag/pipelines/imports/import-id/confirm', - {}, - { silent: true }, - ) - }) -}) diff --git a/web/service/__tests__/use-plugins.spec.tsx b/web/service/__tests__/use-plugins.spec.tsx index 043ec0b8e86fb7..80f22835aeb230 100644 --- a/web/service/__tests__/use-plugins.spec.tsx +++ b/web/service/__tests__/use-plugins.spec.tsx @@ -49,12 +49,20 @@ vi.mock('@/service/base', () => ({ request: mockRequest, })) -mockRequest.mockImplementation(async (url: string) => { - const data = await mockGet(url) - return new Response(JSON.stringify(data), { - headers: { 'Content-Type': 'application/json' }, - }) -}) +mockRequest.mockImplementation( + async (url: string, _init: RequestInit, options: { request?: Request }) => { + const method = options.request?.method ?? 'GET' + const data = + method === 'GET' + ? await mockGet(url) + : await mockPost(new URL(url).pathname.replace('/console/api', ''), { + body: await options.request?.json(), + }) + return new Response(JSON.stringify(data ?? {}), { + headers: { 'Content-Type': 'application/json' }, + }) + }, +) vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list', () => ({ default: () => ({ @@ -70,13 +78,7 @@ vi.mock('@/context/permission-state', async () => { })) }) -const { mockInvalidateAllBuiltInTools } = vi.hoisted(() => ({ - mockInvalidateAllBuiltInTools: vi.fn(), -})) - -vi.mock('../use-tools', () => ({ - useInvalidateAllBuiltInTools: () => mockInvalidateAllBuiltInTools, -})) +const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() const createQueryClient = () => new QueryClient({ @@ -1057,6 +1059,7 @@ describe('useInvalidateInstalledPluginList', () => { queryClient.setQueryData(modelInstalledIdsKey, { plugin_ids: ['langgenius/openai'] }) queryClient.setQueryData(toolInstalledIdsKey, { plugin_ids: ['langgenius/google'] }) + queryClient.setQueryData(builtInToolsKey, []) const { result: listResult, unmount } = renderHook( () => ({ @@ -1099,11 +1102,12 @@ describe('useInvalidateInstalledPluginList', () => { expect(toolCategoryQueries[0]?.state.isInvalidated).toBe(false) expect(queryClient.getQueryState(modelInstalledIdsKey)?.isInvalidated).toBe(true) expect(queryClient.getQueryState(toolInstalledIdsKey)?.isInvalidated).toBe(false) - expect(mockInvalidateAllBuiltInTools).not.toHaveBeenCalled() + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(false) }) it('also invalidates built-in tools for the tool category', async () => { const queryClient = createQueryClient() + queryClient.setQueryData(builtInToolsKey, []) mockGet.mockResolvedValue({ plugins: [], has_more: false }) const { result } = renderHook( @@ -1124,7 +1128,7 @@ describe('useInvalidateInstalledPluginList', () => { await result.current.invalidate(PluginCategoryEnum.tool) }) - expect(mockInvalidateAllBuiltInTools).toHaveBeenCalledOnce() + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) }) }) @@ -1171,7 +1175,6 @@ describe('usePluginTaskList', () => { result.current.handleInstallTaskStart({ all_installed: false, task_id: 'task-new', - plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test', task: startedTask, }) }) @@ -1229,7 +1232,6 @@ describe('usePluginTaskList', () => { result.current.handleInstallTaskStart({ all_installed: false, task_id: 'task-new', - plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test', task: { id: 'task-new', created_at: '2026-06-05T03:34:59.578653Z', @@ -1346,7 +1348,7 @@ describe('usePluginAutoUpgradeSettings', () => { vi.clearAllMocks() }) - it('does not expose frontend default settings before backend data resolves', () => { + it('does not expose frontend default settings before backend data resolves', async () => { const queryClient = createQueryClient() mockGet.mockReturnValue(new Promise(() => {})) @@ -1355,10 +1357,10 @@ describe('usePluginAutoUpgradeSettings', () => { }) expect(result.current.data).toBeUndefined() - expect(mockGet).toHaveBeenCalledWith('/workspaces/current/plugin/auto-upgrade/fetch', { - params: { - category: PluginCategoryEnum.model, - }, + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith( + `http://localhost:5001/console/api/workspaces/current/plugin/auto-upgrade/fetch?category=${PluginCategoryEnum.model}`, + ) }) }) diff --git a/web/service/__tests__/use-tools.spec.tsx b/web/service/__tests__/use-tools.spec.tsx deleted file mode 100644 index 35213ac72f09fe..00000000000000 --- a/web/service/__tests__/use-tools.spec.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { ReactNode } from 'react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { act, waitFor } from '@testing-library/react' -import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' -import { renderHook } from '@/test/console/render' -import { useAllToolProviders, useRefreshMCPServerCode } from '../use-tools' - -const { mockGet, mockPost } = vi.hoisted(() => ({ - mockGet: vi.fn(), - mockPost: vi.fn(), -})) - -vi.mock('@/service/base', () => ({ - del: vi.fn(), - get: mockGet, - post: mockPost, - put: vi.fn(), -})) - -vi.mock('@/service/console', () => ({ - consoleClient: { - apps: { - byAppId: { - server: { - refresh: { - post: mockPost, - }, - }, - }, - }, - }, -})) - -const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - mutations: { - retry: false, - }, - }, - }) - - return function Wrapper({ children }: { children: ReactNode }) { - return {children} - } -} - -describe('useRefreshMCPServerCode', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('refreshes the MCP server with POST and the app ID', async () => { - mockPost.mockResolvedValue({ id: 'server-1', server_code: 'new-code' }) - const { result } = renderHook(() => useRefreshMCPServerCode(), { - wrapper: createWrapper(), - }) - - await act(async () => { - await result.current.mutateAsync('app-1') - }) - - expect(mockPost).toHaveBeenCalledWith({ - params: { - app_id: 'app-1', - }, - }) - expect(mockGet).not.toHaveBeenCalled() - }) -}) - -describe('useAllToolProviders', () => { - beforeEach(() => { - vi.clearAllMocks() - mockGet.mockResolvedValue([]) - }) - - it('keeps filtered MCP providers in a separate query', async () => { - renderHook( - () => { - useAllToolProviders() - useAllToolProviders(true, 'mcp') - }, - { wrapper: createWrapper() }, - ) - - await waitFor(() => expect(mockGet).toHaveBeenCalledTimes(2)) - expect(mockGet).toHaveBeenCalledWith('/workspaces/current/tool-providers') - expect(mockGet).toHaveBeenCalledWith('/workspaces/current/tool-providers', { - params: { type: 'mcp' }, - }) - }) -}) diff --git a/web/service/_tools_util.spec.ts b/web/service/_tools_util.spec.ts deleted file mode 100644 index e3bec3efcf6880..00000000000000 --- a/web/service/_tools_util.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { buildProviderQuery } from './_tools_util' - -describe('makeProviderQuery', () => { - it('collectionName without special chars', () => { - expect(buildProviderQuery('ABC')).toBe('provider=ABC') - }) - it('should escape &', () => { - expect(buildProviderQuery('ABC&DEF')).toBe('provider=ABC%26DEF') - }) - it('should escape /', () => { - expect(buildProviderQuery('ABC/DEF')).toBe('provider=ABC%2FDEF') - }) - it('should escape ?', () => { - expect(buildProviderQuery('ABC?DEF')).toBe('provider=ABC%3FDEF') - }) -}) - -describe('Tools Utilities', () => { - describe('buildProviderQuery', () => { - it('should build query string with provider parameter', () => { - const result = buildProviderQuery('openai') - expect(result).toBe('provider=openai') - }) - - it('should handle provider names with special characters', () => { - const result = buildProviderQuery('provider-name') - expect(result).toBe('provider=provider-name') - }) - - it('should handle empty string', () => { - const result = buildProviderQuery('') - expect(result).toBe('provider=') - }) - - it('should URL encode special characters', () => { - const result = buildProviderQuery('provider name') - expect(result).toBe('provider=provider+name') - }) - - it('should handle Unicode characters', () => { - const result = buildProviderQuery('提供者') - expect(result).toContain('provider=') - expect(decodeURIComponent(result)).toBe('provider=提供者') - }) - - it('should handle provider names with slashes', () => { - const result = buildProviderQuery('langgenius/openai/gpt-4') - expect(result).toContain('provider=') - expect(decodeURIComponent(result)).toBe('provider=langgenius/openai/gpt-4') - }) - }) -}) diff --git a/web/service/_tools_util.ts b/web/service/_tools_util.ts deleted file mode 100644 index 89dede8163f4c6..00000000000000 --- a/web/service/_tools_util.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const buildProviderQuery = (collectionName: string): string => { - const query = new URLSearchParams() - query.set('provider', collectionName) - return query.toString() -} diff --git a/web/service/access-control/use-member-roles.ts b/web/service/access-control/use-member-roles.ts deleted file mode 100644 index c7202da7923b8b..00000000000000 --- a/web/service/access-control/use-member-roles.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { AccessControlTemplateLanguage } from '@/i18n/language' -import type { RolesOfMemberResponse, UpdateRolesOfMemberRequest } from '@/models/access-control' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { get, put } from '../base' -import { commonQueryKeys } from '../use-common' - -const NAME_SPACE = 'rbac-member-roles' - -export const useRolesOfMember = (memberId: string, language: AccessControlTemplateLanguage) => { - return useQuery({ - queryKey: [NAME_SPACE, 'member-roles', memberId, language], - queryFn: () => - get(`/workspaces/current/rbac/members/${memberId}/rbac-roles`, { - params: { - language, - }, - }), - }) -} - -export const useUpdateRolesOfMember = () => { - const queryClient = useQueryClient() - - return useMutation({ - mutationKey: [NAME_SPACE, 'update-member-roles'], - mutationFn: (request: UpdateRolesOfMemberRequest) => { - const { memberId, roleIds } = request - return put(`/workspaces/current/rbac/members/${memberId}/rbac-roles`, { - body: { role_ids: roleIds }, - }) - }, - onSuccess: (_, { memberId }) => - Promise.all([ - queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'member-roles', memberId] }), - queryClient.invalidateQueries({ queryKey: commonQueryKeys.members }), - ]), - }) -} diff --git a/web/service/access-control/use-workspace-roles.ts b/web/service/access-control/use-workspace-roles.ts deleted file mode 100644 index c47485eaa120de..00000000000000 --- a/web/service/access-control/use-workspace-roles.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { - CopyWorkspaceRoleRequest, - CreateRoleRequest, - GetMembersOfRoleRequest, - GetMembersOfRoleResponse, - Role, - RoleListRequest, - RoleListResponse, - UpdateRolesRequest, -} from '@/models/access-control' -import type { CommonResponse } from '@/models/common' -import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { del, get, post, put } from '../base' -import { commonQueryKeys } from '../use-common' - -const NAME_SPACE = 'rbac-role-management' - -export const useWorkspaceRoleList = (params: RoleListRequest) => { - const { page = 1, ...queryParams } = params - - return useInfiniteQuery({ - queryKey: [NAME_SPACE, 'workspace-role-list', queryParams], - queryFn: ({ pageParam }) => - get('/workspaces/current/rbac/roles', { - params: { - ...queryParams, - page: pageParam, - }, - }), - initialPageParam: page, - getNextPageParam: (lastPage) => { - const { current_page, total_pages } = lastPage.pagination - - if (current_page < total_pages) return current_page + 1 - - return undefined - }, - }) -} - -export const useCreateWorkspaceRole = () => { - const queryClient = useQueryClient() - - return useMutation({ - mutationKey: [NAME_SPACE, 'create-workspace-role'], - mutationFn: (data: CreateRoleRequest) => - post('/workspaces/current/rbac/roles', { - body: { ...data }, - }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'workspace-role-list'] }) - }, - }) -} - -export const useUpdateWorkspaceRole = () => { - const queryClient = useQueryClient() - - return useMutation({ - mutationKey: [NAME_SPACE, 'update-workspace-role'], - mutationFn: (data: UpdateRolesRequest) => - put(`/workspaces/current/rbac/roles/${data.id}`, { - body: { ...data }, - }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'workspace-role-list'] }) - }, - }) -} - -export const useDeleteWorkspaceRole = () => { - const queryClient = useQueryClient() - - return useMutation({ - mutationKey: [NAME_SPACE, 'delete-workspace-role'], - mutationFn: (id: string) => del(`/workspaces/current/rbac/roles/${id}`), - onSuccess: () => - Promise.all([ - queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'workspace-role-list'] }), - queryClient.invalidateQueries({ queryKey: commonQueryKeys.members }), - ]), - }) -} - -export const useCopyWorkspaceRole = () => { - const queryClient = useQueryClient() - - return useMutation({ - mutationKey: [NAME_SPACE, 'copy-workspace-role'], - mutationFn: ({ roleId, copy_member }: CopyWorkspaceRoleRequest) => - post(`/workspaces/current/rbac/roles/${roleId}/copy`, { - body: { copy_member }, - }), - onSuccess: (_role, variables) => { - const invalidations = [ - queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'workspace-role-list'] }), - ] - - if (variables.copy_member) - invalidations.push(queryClient.invalidateQueries({ queryKey: commonQueryKeys.members })) - - return Promise.all(invalidations) - }, - }) -} - -export const useGetMembersOfRole = (params: GetMembersOfRoleRequest) => { - const { roleId, ...paginationParams } = params - return useQuery({ - queryKey: [NAME_SPACE, 'members-of-role', roleId, paginationParams], - queryFn: () => - get(`/workspaces/current/rbac/roles/${roleId}/members`, { - params: { - ...paginationParams, - }, - }), - enabled: !!roleId, - }) -} diff --git a/web/service/annotation.spec.ts b/web/service/annotation.spec.ts index 06013c446e9293..14853d6855836e 100644 --- a/web/service/annotation.spec.ts +++ b/web/service/annotation.spec.ts @@ -1,9 +1,11 @@ import { AnnotationEnableStatus } from '@/app/components/app/annotation/type' import { updateAnnotationStatus } from './annotation' -import { post } from './base' +import { consoleClient } from './console' -vi.mock('./base', () => ({ - post: vi.fn(), +vi.mock('./console', () => ({ + consoleClient: { + apps: { byAppId: { annotationReply: { byAction: { post: vi.fn() } } } }, + }, })) describe('annotation service', () => { @@ -22,7 +24,8 @@ describe('annotation service', () => { 0, ) - expect(post).toHaveBeenCalledWith('apps/app-1/annotation-reply/enable', { + expect(consoleClient.apps.byAppId.annotationReply.byAction.post).toHaveBeenCalledWith({ + params: { app_id: 'app-1', action: 'enable' }, body: { embedding_model_name: 'model', embedding_provider_name: 'provider', diff --git a/web/service/annotation.ts b/web/service/annotation.ts index fe7a8dcb7e73d9..c54e41f6f4d15d 100644 --- a/web/service/annotation.ts +++ b/web/service/annotation.ts @@ -1,105 +1,31 @@ import type { - AnnotationCreateResponse, AnnotationEnableStatus, - AnnotationItemBasic, EmbeddingModelConfig, } from '@/app/components/app/annotation/type' import { ANNOTATION_DEFAULT } from '@/config' -import { del, get, post } from './base' +import { post } from './base' +import { consoleClient } from './console' -export const fetchAnnotationConfig = (appId: string) => { - return get(`apps/${appId}/annotation-setting`) -} export const updateAnnotationStatus = ( appId: string, action: AnnotationEnableStatus, embeddingModel?: EmbeddingModelConfig, score?: number, -) => { - let body: any = { - score_threshold: score ?? ANNOTATION_DEFAULT.score_threshold, - } - if (embeddingModel) { - body = { - ...body, - ...embeddingModel, - } - } - - return post(`apps/${appId}/annotation-reply/${action}`, { - body, - }) -} - -export const updateAnnotationScore = (appId: string, settingId: string, score: number) => { - return post(`apps/${appId}/annotation-settings/${settingId}`, { - body: { score_threshold: score }, +) => + consoleClient.apps.byAppId.annotationReply.byAction.post({ + params: { app_id: appId, action }, + body: { + score_threshold: score ?? ANNOTATION_DEFAULT.score_threshold, + // Disabling does not use the embedding model; the API still requires the fields. + embedding_provider_name: embeddingModel?.embedding_provider_name ?? '', + embedding_model_name: embeddingModel?.embedding_model_name ?? '', + }, }) -} - -export const queryAnnotationJobStatus = ( - appId: string, - action: AnnotationEnableStatus, - jobId: string, -) => { - return get(`apps/${appId}/annotation-reply/${action}/status/${jobId}`) -} - -export const fetchAnnotationList = (appId: string, params: Record) => { - return get(`apps/${appId}/annotations`, { params }) -} - -export const fetchExportAnnotationList = (appId: string) => { - return get(`apps/${appId}/annotations/export`) -} -export const addAnnotation = (appId: string, body: AnnotationItemBasic) => { - return post(`apps/${appId}/annotations`, { body }) -} - -export const annotationBatchImport = ({ - url, - body, -}: { - url: string - body: FormData -}): Promise<{ job_id: string; job_status: string }> => { - return post<{ job_id: string; job_status: string }>( - url, +// Multipart uploads are not described by the generated contract yet. +export const annotationBatchImport = (appId: string, body: FormData) => + post<{ job_id: string; job_status: string }>( + `/apps/${appId}/annotations/batch-import`, { body }, { bodyStringify: false, deleteContentType: true }, ) -} - -export const checkAnnotationBatchImportProgress = ({ - jobID, - appId, -}: { - jobID: string - appId: string -}): Promise<{ job_id: string; job_status: string }> => { - return get<{ job_id: string; job_status: string }>( - `/apps/${appId}/annotations/batch-import-status/${jobID}`, - ) -} - -export const editAnnotation = (appId: string, annotationId: string, body: AnnotationItemBasic) => { - return post(`apps/${appId}/annotations/${annotationId}`, { body }) -} - -export const delAnnotation = (appId: string, annotationId: string) => { - return del(`apps/${appId}/annotations/${annotationId}`) -} - -export const delAnnotations = (appId: string, annotationIds: string[]) => { - const params = annotationIds.map((id) => `annotation_id=${id}`).join('&') - return del(`/apps/${appId}/annotations?${params}`) -} - -export const fetchHitHistoryList = ( - appId: string, - annotationId: string, - params: Record, -) => { - return get(`apps/${appId}/annotations/${annotationId}/hit-histories`, { params }) -} diff --git a/web/service/apps.ts b/web/service/apps.ts deleted file mode 100644 index 2a723b29af690a..00000000000000 --- a/web/service/apps.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { post } from './base' - -type PublishToCreatorsPlatformResponse = { - redirect_url: string -} - -export const publishToCreatorsPlatform = ({ - appID, -}: { - appID: string -}): Promise => { - return post(`apps/${appID}/publish-to-creators-platform`, { - body: {}, - }) -} diff --git a/web/service/base.spec.ts b/web/service/base.spec.ts index 586e4fcb9f4c75..d2a80e994c10d7 100644 --- a/web/service/base.spec.ts +++ b/web/service/base.spec.ts @@ -3,18 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { toast } from '@/app/notifications' import { PUBLIC_API_PREFIX } from '@/config' // oxlint-disable-next-line no-restricted-imports -import { - del, - get, - handleStream, - patch, - post, - postPublic, - put, - sseGet, - ssePost, - upload, -} from './base' +import { del, get, handleStream, patch, post, postPublic, sseGet, ssePost, upload } from './base' const refreshAccessTokenOrReLoginMock = vi.hoisted(() => vi.fn()) @@ -754,7 +743,6 @@ describe('HTTP methods', () => { it('should export methods correctly', () => { expect(typeof get).toBe('function') expect(typeof post).toBe('function') - expect(typeof put).toBe('function') expect(typeof patch).toBe('function') expect(typeof del).toBe('function') }) diff --git a/web/service/base.ts b/web/service/base.ts index 5133179383d6ad..23a0cffd5bd9fd 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -1196,16 +1196,6 @@ export const postPublic = (url: string, options = {}, otherOptions?: IOtherOp return post(url, options, { ...otherOptions, isPublicAPI: true }) } -/** - * @deprecated For console JSON APIs, prefer generated contract clients (`consoleClient`/`consoleQuery`) - * only after the backend OpenAPI schema produces accurate method, path, input, and output types. - * Keep this helper for endpoints whose generated contract is missing or too loose, and for non-console - * flows such as public APIs, marketplace APIs, streaming, upload, or download. - */ -export const put = (url: string, options = {}, otherOptions?: IOtherOptions) => { - return request(url, Object.assign({}, options, { method: 'PUT' }), otherOptions) -} - /** * @deprecated For console JSON APIs, prefer generated contract clients (`consoleClient`/`consoleQuery`) * only after the backend OpenAPI schema produces accurate method, path, input, and output types. diff --git a/web/service/common.spec.ts b/web/service/common.spec.ts index 97394bb2bd486e..26f1919c3aa775 100644 --- a/web/service/common.spec.ts +++ b/web/service/common.spec.ts @@ -8,7 +8,6 @@ import { REGISTRATION_SUCCESS_STORAGE_KEY, } from '@/app/components/base/amplitude/registration-session-state' import { clearPageLeaveGuards, registerPageLeaveGuard } from '@/utils/page-leave-guard' -import { emailLoginWithCode, sendEMailLoginCode } from './common' import { useLogout } from './use-common' const mocks = vi.hoisted(() => ({ @@ -34,63 +33,6 @@ vi.mock('@/utils/var', async (importOriginal) => ({ }, })) -describe('sendEMailLoginCode', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('includes the Turnstile token when provided', async () => { - await sendEMailLoginCode('user@example.com', 'en-US', 'turnstile-token') - - expect(mocks.post).toHaveBeenCalledWith('/email-code-login', { - body: { - email: 'user@example.com', - language: 'en-US', - turnstile_token: 'turnstile-token', - }, - }) - }) - - it('omits the Turnstile token when it is not provided', async () => { - await sendEMailLoginCode('user@example.com', 'en-US') - - expect(mocks.post).toHaveBeenCalledWith('/email-code-login', { - body: { - email: 'user@example.com', - language: 'en-US', - }, - }) - }) -}) - -describe('emailLoginWithCode', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('includes the verification-specific Turnstile token when provided', async () => { - await emailLoginWithCode({ - code: 'encrypted-code', - email: 'user@example.com', - language: 'en-US', - timezone: 'Asia/Singapore', - token: 'email-login-token', - turnstile_token: 'verify-turnstile-token', - }) - - expect(mocks.post).toHaveBeenCalledWith('/email-code-login/validity', { - body: { - code: 'encrypted-code', - email: 'user@example.com', - language: 'en-US', - timezone: 'Asia/Singapore', - token: 'email-login-token', - turnstile_token: 'verify-turnstile-token', - }, - }) - }) -}) - describe('useLogout', () => { const wrapper = ({ children }: { children: ReactNode }) => createElement(QueryClientProvider, { client: new QueryClient() }, children) diff --git a/web/service/common.ts b/web/service/common.ts index f02a8f26913f0d..937202cbcacecd 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -1,19 +1,5 @@ -import type { EmailCodeLoginPayload } from '@dify/contracts/api/console/email-code-login/types.gen' -import type { - PostWorkspacesInfoData, - PostWorkspacesInfoResponse, -} from '@dify/contracts/api/console/workspaces/types.gen' -import type { - DefaultModelResponse, - ModelParameterRule, - ModelTypeEnum, -} from '@/app/components/header/account-setting/model-provider-page/declarations' -import type { - CommonResponse, - InitValidateStatusResponse, - SetupStatusResponse, -} from '@/models/common' -import { del, get, patch, post } from './base' +import type { CommonResponse } from '@/models/common' +import { post } from './base' type LoginSuccess = { result: 'success' @@ -26,15 +12,6 @@ type LoginFail = { message: string } type LoginResponse = LoginSuccess | LoginFail -export const login = ({ - url, - body, -}: { - url: string - body: Record -}): Promise => { - return post(url, { body }) -} export const webAppLogin = ({ url, body, @@ -45,133 +22,6 @@ export const webAppLogin = ({ return post(url, { body }, { isPublicAPI: true }) } -export const setup = ({ body }: { body: Record }): Promise => { - return post('/setup', { body }) -} - -export const initValidate = ({ body }: { body: Record }): Promise => { - return post('/init', { body }) -} - -export const fetchInitValidateStatus = (): Promise => { - return get('/init') -} - -export const fetchSetupStatus = (): Promise => { - return get('/setup') -} -export const deleteMemberOrCancelInvitation = ({ - url, -}: { - url: string -}): Promise => { - return del(url) -} - -export const sendOwnerEmail = (body: { - language?: string -}): Promise => - post( - '/workspaces/current/members/send-owner-transfer-confirm-email', - { body }, - ) - -export const verifyOwnerEmail = (body: { - code: string - token: string -}): Promise => - post( - '/workspaces/current/members/owner-transfer-check', - { body }, - ) - -export const ownershipTransfer = ( - memberID: string, - body: { token: string }, -): Promise => - post( - `/workspaces/current/members/${memberID}/owner-transfer`, - { body }, - ) - -export const updateWorkspaceInfo = ({ - url, - body, -}: { - url: PostWorkspacesInfoData['url'] - body: PostWorkspacesInfoData['body'] -}): Promise => { - return post(url, { body }) -} - -type InvitationCheckData = { - workspace_name: string - email: string - workspace_id: string - account_status?: string - requires_setup?: boolean -} - -type ActivateMemberBody = { - token: string - name?: string - interface_language?: string - timezone?: string -} - -export const invitationCheck = ({ - url, - params, -}: { - url: string - params: { workspace_id?: string; email?: string; token: string } -}): Promise => { - return get(url, { params }) -} - -export const activateMember = ({ - url, - body, -}: { - url: string - body: ActivateMemberBody -}): Promise => { - return post(url, { body }) -} - -export const fetchDefaultModal = (url: string): Promise<{ data: DefaultModelResponse }> => { - return get<{ data: DefaultModelResponse }>(url) -} - -export const fetchModelParameterRules = (url: string): Promise<{ data: ModelParameterRule[] }> => { - return get<{ data: ModelParameterRule[] }>(url) -} - -export const enableModel = ( - url: string, - body: { model: string; model_type: ModelTypeEnum }, -): Promise => patch(url, { body }) - -export const disableModel = ( - url: string, - body: { model: string; model_type: ModelTypeEnum }, -): Promise => patch(url, { body }) - -export const sendForgotPasswordEmail = ({ - url, - body, -}: { - url: string - body: { email: string } -}): Promise => - post(url, { body }) -export const changePasswordWithToken = ({ - url, - body, -}: { - url: string - body: { token: string; new_password: string; password_confirm: string } -}): Promise => post(url, { body }) export const changeWebAppPasswordWithToken = ({ url, body, @@ -193,37 +43,6 @@ export const uploadRemoteFileInfo = ( ) } -export const sendEMailLoginCode = ( - email: string, - language = 'en-US', - turnstileToken?: string, -): Promise => - post('/email-code-login', { - body: { - email, - language, - ...(turnstileToken === undefined ? {} : { turnstile_token: turnstileToken }), - }, - }) - -export const emailLoginWithCode = (data: EmailCodeLoginPayload): Promise => - post('/email-code-login/validity', { body: data }) - -export const sendResetPasswordCode = ( - email: string, - language = 'en-US', -): Promise => - post('/forgot-password', { - body: { email, language }, - }) - -export const verifyResetPasswordCode = (body: { - email: string - code: string - token: string -}): Promise => - post('/forgot-password/validity', { body }) - export const sendWebAppEMailLoginCode = ( email: string, language = 'en-US', @@ -262,46 +81,6 @@ export const verifyWebAppResetPasswordCode = (body: { { isPublicAPI: true }, ) -export const sendDeleteAccountCode = (): Promise => - get('/account/delete/verify') - -export const verifyDeleteAccountCode = (body: { - code: string - token: string -}): Promise => - post('/account/delete', { body }) - -export const submitDeleteAccountFeedback = (body: { - feedback: string - email: string -}): Promise => post('/account/delete/feedback', { body }) - -export const getDocDownloadUrl = (doc_name: string): Promise<{ url: string }> => - get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true }) - -export const sendVerifyCode = (body: { - email: string - phase: string - token?: string -}): Promise => - post('/account/change-email', { body }) - -export const verifyEmail = (body: { - email: string - code: string - token: string -}): Promise => - post( - '/account/change-email/validity', - { body }, - ) - -export const resetEmail = (body: { new_email: string; token: string }): Promise => - post('/account/change-email/reset', { body }) - -export const checkEmailExisted = (body: { email: string }): Promise => - post('/account/change-email/check-email-unique', { body }, { silent: true }) - export const getAvatar = async ({ avatar, }: { diff --git a/web/service/console/__tests__/dataset-metadata-policies.spec.ts b/web/service/console/__tests__/dataset-metadata-policies.spec.ts new file mode 100644 index 00000000000000..2e64d8bcaa44e0 --- /dev/null +++ b/web/service/console/__tests__/dataset-metadata-policies.spec.ts @@ -0,0 +1,116 @@ +import type { QueryClient, QueryKey } from '@tanstack/react-query' +import { MutationObserver } from '@tanstack/react-query' +import { consoleQuery } from '@/service/console' +import { createTestQueryClient } from '@/test/query-client' + +const { mockCall } = vi.hoisted(() => ({ mockCall: vi.fn() })) + +vi.mock('@/service/console/browser', () => ({ + consoleBrowserLink: { call: mockCall }, +})) + +const params = { dataset_id: 'dataset-1' } + +const datasetMetadataKey = consoleQuery.datasets.byDatasetId.metadata.get.queryKey({ + input: { params }, +}) +const documentListKey = consoleQuery.datasets.byDatasetId.documents.get.queryKey({ + input: { params, query: { page: 1, limit: 10 } }, +}) +const documentMetadataKey = consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.queryKey({ + input: { params: { ...params, document_id: 'doc-1' }, query: { metadata: 'only' } }, +}) +const otherDatasetMetadataKey = consoleQuery.datasets.byDatasetId.metadata.get.queryKey({ + input: { params: { dataset_id: 'dataset-2' } }, +}) + +const seed = (queryClient: QueryClient, keys: QueryKey[]) => { + keys.forEach((key) => queryClient.setQueryData(key, {})) +} + +const isInvalidated = (queryClient: QueryClient, key: QueryKey) => + queryClient.getQueryState(key)?.isInvalidated ?? false + +describe('dataset metadata cache policies', () => { + let queryClient: QueryClient + + beforeEach(() => { + vi.clearAllMocks() + mockCall.mockResolvedValue({}) + queryClient = createTestQueryClient() + seed(queryClient, [ + datasetMetadataKey, + documentListKey, + documentMetadataKey, + otherDatasetMetadataKey, + ]) + }) + + it('refreshes the dataset fields after a field is created', async () => { + await new MutationObserver( + queryClient, + consoleQuery.datasets.byDatasetId.metadata.post.mutationOptions(), + ).mutate({ params, body: { name: 'author', type: 'string' } }) + + expect(isInvalidated(queryClient, datasetMetadataKey)).toBe(true) + expect(isInvalidated(queryClient, documentListKey)).toBe(false) + expect(isInvalidated(queryClient, otherDatasetMetadataKey)).toBe(false) + }) + + it.each([ + [ + 'renaming a field', + () => + new MutationObserver( + queryClient, + consoleQuery.datasets.byDatasetId.metadata.byMetadataId.patch.mutationOptions(), + ).mutate({ params: { ...params, metadata_id: 'field-1' }, body: { name: 'writer' } }), + ], + [ + 'deleting a field', + () => + new MutationObserver( + queryClient, + consoleQuery.datasets.byDatasetId.metadata.byMetadataId.delete.mutationOptions(), + ).mutate({ params: { ...params, metadata_id: 'field-1' } }), + ], + [ + 'editing document values', + () => + new MutationObserver( + queryClient, + consoleQuery.datasets.byDatasetId.documents.metadata.post.mutationOptions(), + ).mutate({ params, body: { operation_data: [] } }), + ], + ])('refreshes the fields, document list and document details after %s', async (_, run) => { + await run() + + expect(isInvalidated(queryClient, datasetMetadataKey)).toBe(true) + expect(isInvalidated(queryClient, documentListKey)).toBe(true) + expect(isInvalidated(queryClient, documentMetadataKey)).toBe(true) + expect(isInvalidated(queryClient, otherDatasetMetadataKey)).toBe(false) + }) + + it('refreshes the dataset fields after the built-in fields are toggled', async () => { + await new MutationObserver( + queryClient, + consoleQuery.datasets.byDatasetId.metadata.builtIn.byAction.post.mutationOptions(), + ).mutate({ params: { ...params, action: 'enable' } }) + + expect(isInvalidated(queryClient, datasetMetadataKey)).toBe(true) + expect(isInvalidated(queryClient, documentListKey)).toBe(false) + }) + + it('keeps the cache when the request fails', async () => { + mockCall.mockRejectedValueOnce(new Error('failed')) + + await expect( + new MutationObserver( + queryClient, + consoleQuery.datasets.byDatasetId.metadata.byMetadataId.delete.mutationOptions(), + ).mutate({ params: { ...params, metadata_id: 'field-1' } }), + ).rejects.toThrow('failed') + + expect(isInvalidated(queryClient, datasetMetadataKey)).toBe(false) + }) +}) diff --git a/web/service/console/__tests__/dataset-query-policies.spec.ts b/web/service/console/__tests__/dataset-query-policies.spec.ts new file mode 100644 index 00000000000000..ce38465735dad0 --- /dev/null +++ b/web/service/console/__tests__/dataset-query-policies.spec.ts @@ -0,0 +1,33 @@ +import { ORPCError } from '@orpc/client' +import { consoleQuery } from '@/service/console' + +const detailOptions = () => + consoleQuery.datasets.byDatasetId.get.queryOptions({ + input: { params: { dataset_id: 'dataset-1' } }, + }) + +// Shared defaults are merged into the generated options at runtime. +const retryDetail = (failureCount: number, error: Error) => { + const options = detailOptions() + const retry = 'retry' in options ? options.retry : undefined + if (typeof retry !== 'function') throw new Error('expected a retry policy') + return retry(failureCount, error) +} + +describe('dataset query policies', () => { + it.each([403, 404])('does not retry a dataset detail answered with %i', (status) => { + expect(retryDetail(0, new ORPCError('ERROR', { status }))).toBe(false) + }) + + it('retries other dataset detail failures up to three times', () => { + const error = new ORPCError('INTERNAL_SERVER_ERROR', { status: 500 }) + + expect(retryDetail(2, error)).toBe(true) + expect(retryDetail(3, error)).toBe(false) + }) + + it('reads process rules once per import', () => { + const options = consoleQuery.datasets.processRule.get.queryOptions({ input: { query: {} } }) + expect('refetchOnWindowFocus' in options ? options.refetchOnWindowFocus : undefined).toBe(false) + }) +}) diff --git a/web/service/console/client.spec.ts b/web/service/console/client.spec.ts index a9a7123ed6ed79..9f985b6819176e 100644 --- a/web/service/console/client.spec.ts +++ b/web/service/console/client.spec.ts @@ -457,6 +457,17 @@ describe('normalizeConsoleOpenAPIURL', () => { expect(searchParams.getAll('tags')).toEqual(['search', 'rag']) expect(searchParams.has('tags[0]')).toBe(false) }) + + // Without repeated keys the API sees no IDs and clears every annotation of the app. + it('should serialize annotation batch delete IDs as repeated params', () => { + const url = normalizeConsoleOpenAPIURL( + 'https://example.com/console/api/apps/app-1/annotations?annotation_id%5B0%5D=a-1&annotation_id%5B1%5D=a-2', + ) + const searchParams = new URL(url).searchParams + + expect(searchParams.getAll('annotation_id')).toEqual(['a-1', 'a-2']) + expect(searchParams.has('annotation_id[0]')).toBe(false) + }) }) // Scenario: oRPC query defaults own shared Agent detail fetch behavior. diff --git a/web/service/console/openapi-url.ts b/web/service/console/openapi-url.ts index 6ab094868a5251..42761b1ae62ecd 100644 --- a/web/service/console/openapi-url.ts +++ b/web/service/console/openapi-url.ts @@ -5,7 +5,10 @@ const repeatedQueryArrayRules = [ { path: /\/agent\/[^/]+\/logs\/[^/]+\/messages$/, fields: ['sources', 'statuses'] }, { path: /\/apps$/, fields: ['tag_ids', 'creator_ids'] }, { path: /\/apps\/starred$/, fields: ['tag_ids', 'creator_ids'] }, + { path: /\/apps\/[^/]+\/annotations$/, fields: ['annotation_id'] }, { path: /\/datasets$/, fields: ['ids', 'tag_ids'] }, + { path: /\/datasets\/[^/]+\/documents$/, fields: ['document_id'] }, + { path: /\/datasets\/[^/]+\/documents\/status\/[^/]+\/batch$/, fields: ['document_id'] }, { path: /\/datasets\/[^/]+\/documents\/[^/]+\/segment\/[^/]+$/, fields: ['segment_id'] }, { path: /\/datasets\/[^/]+\/documents\/[^/]+\/segments$/, fields: ['segment_id', 'status'] }, { path: /\/trial-apps\/[^/]+\/datasets$/, fields: ['ids'] }, diff --git a/web/service/console/query-policies.ts b/web/service/console/query-policies.ts index 29236c03f0687c..35f746e6983d9b 100644 --- a/web/service/console/query-policies.ts +++ b/web/service/console/query-policies.ts @@ -10,7 +10,7 @@ import type { RouterUtils } from '@orpc/tanstack-query' import type { InfiniteData, QueryClient, QueryKey } from '@tanstack/react-query' import type { ConsoleClient } from './index' import { createTanstackQueryUtils } from '@orpc/tanstack-query' -import { isNotFoundError } from '@/service/http-error' +import { getHttpErrorStatus, isNotFoundError } from '@/service/http-error' export function createConsoleQuery(consoleClient: ConsoleClient) { const consoleQuery: RouterUtils = createTanstackQueryUtils(consoleClient, { @@ -75,13 +75,46 @@ export function createConsoleQuery(consoleClient: ConsoleClient) { }, }, }, + auth: { + plugin: { + datasource: { + list: { get: { queryOptions: { retry: false } } }, + defaultList: { get: { queryOptions: { retry: false } } }, + byProviderId: { get: { queryOptions: { retry: false } } }, + }, + }, + }, rag: { + pipeline: { + templates: { + byTemplateId: { get: { queryOptions: { staleTime: 0 } } }, + }, + }, pipelines: { datasourcePlugins: { get: { queryOptions: { staleTime: 0, retry: false }, }, }, + // DSL import failures are reported by the import flow. + imports: { + post: { mutationOptions: { context: { silent: true } } }, + byImportId: { + confirm: { post: { mutationOptions: { context: { silent: true } } } }, + }, + }, + byPipelineId: { + workflows: { + draft: { + processing: { parameters: { get: { queryOptions: { staleTime: 0 } } } }, + preProcessing: { parameters: { get: { queryOptions: { staleTime: 0 } } } }, + }, + published: { + processing: { parameters: { get: { queryOptions: { staleTime: 0 } } } }, + preProcessing: { parameters: { get: { queryOptions: { staleTime: 0 } } } }, + }, + }, + }, }, }, workspaces: { @@ -241,7 +274,89 @@ export function createConsoleQuery(consoleClient: ConsoleClient) { }, }, }, + // Role writes refresh the role list; role assignment changes also refresh members. rbac: { + roles: { + post: { + mutationOptions: { + onSettled: (_data, error, _variables, _result, context) => + error + ? undefined + : context.client.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.rbac.roles.get.key(), + }), + }, + }, + byRoleId: { + put: { + mutationOptions: { + onSettled: (_data, error, _variables, _result, context) => + error + ? undefined + : context.client.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.rbac.roles.get.key(), + }), + }, + }, + delete: { + mutationOptions: { + onSettled: (_data, error, _variables, _result, context) => + error + ? undefined + : Promise.all([ + context.client.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.rbac.roles.get.key(), + }), + context.client.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.members.get.key(), + }), + ]), + }, + }, + copy: { + post: { + mutationOptions: { + onSettled: (_data, error, variables, _result, context) => + error + ? undefined + : Promise.all([ + context.client.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.rbac.roles.get.key(), + }), + variables.body?.copy_member && + context.client.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.members.get.key(), + }), + ]), + }, + }, + }, + }, + }, + members: { + byMemberId: { + rbacRoles: { + put: { + mutationOptions: { + onSettled: (_data, error, variables, _result, context) => + error + ? undefined + : Promise.all([ + context.client.invalidateQueries({ + queryKey: + consoleQuery.workspaces.current.rbac.members.byMemberId.rbacRoles.get.key( + { input: { params: variables.params } }, + ), + }), + context.client.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.members.get.key(), + }), + ]), + }, + }, + }, + }, + }, accessPolicies: { post: { mutationOptions: { @@ -802,6 +917,137 @@ export function createConsoleQuery(consoleClient: ConsoleClient) { }, }, datasets: { + // Process rules are read once to describe a running import. + processRule: { get: { queryOptions: { refetchOnWindowFocus: false } } }, + byDatasetId: { + get: { + queryOptions: { + // A missing or inaccessible dataset sends the detail page back to the list. + retry: (failureCount, error) => { + const status = getHttpErrorStatus(error) + if (status === 403 || status === 404) return false + + return failureCount < 3 + }, + }, + }, + documents: { + byDocumentId: { + pipelineExecutionLog: { get: { queryOptions: { staleTime: 0 } } }, + }, + // Document metadata shows in the dataset fields, the document list and each + // document's detail. + metadata: { + post: { + mutationOptions: { + onSettled: (_data, error, variables, _result, context) => { + if (error) return + const params = { dataset_id: variables.params.dataset_id } + return Promise.all([ + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.metadata.get.queryKey({ + input: { params }, + }), + }), + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params }, + }), + }), + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key({ + input: { params }, + }), + }), + ]) + }, + }, + }, + }, + }, + metadata: { + post: { + mutationOptions: { + onSettled: (_data, error, variables, _result, context) => { + if (error) return + return context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.metadata.get.queryKey({ + input: { params: variables.params }, + }), + }) + }, + }, + }, + byMetadataId: { + patch: { + mutationOptions: { + onSettled: (_data, error, variables, _result, context) => { + if (error) return + const params = { dataset_id: variables.params.dataset_id } + return Promise.all([ + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.metadata.get.queryKey({ + input: { params }, + }), + }), + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params }, + }), + }), + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key({ + input: { params }, + }), + }), + ]) + }, + }, + }, + delete: { + mutationOptions: { + onSettled: (_data, error, variables, _result, context) => { + if (error) return + const params = { dataset_id: variables.params.dataset_id } + return Promise.all([ + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.metadata.get.queryKey({ + input: { params }, + }), + }), + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.get.key({ + input: { params }, + }), + }), + context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.documents.byDocumentId.get.key({ + input: { params }, + }), + }), + ]) + }, + }, + }, + }, + builtIn: { + byAction: { + post: { + mutationOptions: { + onSettled: (_data, error, variables, _result, context) => { + if (error) return + return context.client.invalidateQueries({ + queryKey: consoleQuery.datasets.byDatasetId.metadata.get.queryKey({ + input: { params: { dataset_id: variables.params.dataset_id } }, + }), + }) + }, + }, + }, + }, + }, + }, + }, apiKeys: { post: { mutationOptions: { diff --git a/web/service/datasets.ts b/web/service/datasets.ts index c1ae7b8e646cf3..9df01ac8be398d 100644 --- a/web/service/datasets.ts +++ b/web/service/datasets.ts @@ -1,25 +1,11 @@ +import type { DatasetUpdatePayload } from '@dify/contracts/api/console/datasets/types.gen' import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations' import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations' -import type { CommonResponse } from '@/models/common' -import type { - CreateDocumentReq, - createDocumentResponse, - DataSet, - DataSetListResponse, - ExternalAPIDeleteResponse, - ExternalAPIItem, - ExternalAPIUsage, - ExternalKnowledgeItem, - FetchDatasetsParams, - FileIndexingEstimateResponse, - IndexingEstimateParams, - IndexingStatusBatchResponse, - IndexingStatusResponse, - ProcessRuleResponse, -} from '@/models/datasets' -import qs from 'qs' +import type { FetchDatasetsParams } from '@/models/datasets' import { DataSourceProvider } from '@/models/common' -import { del, get, patch, post } from './base' +// oxlint-disable-next-line no-restricted-imports -- the zip download needs the raw Blob response +import { post } from './base' +import { consoleClient } from './console' // apis for documents in a dataset @@ -28,10 +14,6 @@ type CommonDocReq = { documentId: string } -export type DocumentDownloadResponse = { - url: string -} - export type DocumentDownloadZipRequest = { datasetId: string documentIds: string[] @@ -44,64 +26,39 @@ type BatchReq = { export type SortType = 'created_at' | 'hit_count' | '-created_at' | '-hit_count' -export type MetadataType = 'all' | 'only' | 'without' - export const updateDatasetSetting = ({ datasetId, body, }: { datasetId: string - body: Partial< - Pick< - DataSet, - | 'name' - | 'description' - | 'permission' - | 'partial_member_list' - | 'indexing_technique' - | 'retrieval_model' - | 'embedding_model' - | 'embedding_model_provider' - | 'icon_info' - | 'doc_form' - > - > -}): Promise => { - return patch(`/datasets/${datasetId}`, { body }) + body: DatasetUpdatePayload +}) => { + return consoleClient.datasets.byDatasetId.patch({ params: { dataset_id: datasetId }, body }) } -export const fetchDatasets = ({ - url, - params, -}: FetchDatasetsParams): Promise => { - const urlParams = qs.stringify(params, { indices: false }) - return get(`${url}?${urlParams}`) +export const fetchDatasets = ({ params }: FetchDatasetsParams) => { + return consoleClient.datasets.get({ query: params }) } -export const createEmptyDataset = ({ name }: { name: string }): Promise => { - return post('/datasets', { body: { name } }) +export const createEmptyDataset = ({ name }: { name: string }) => { + return consoleClient.datasets.post({ body: { name } }) } -export const checkIsUsedInApp = (id: string): Promise<{ is_using: boolean }> => { - return get<{ is_using: boolean }>( - `/datasets/${id}/use-check`, - {}, - { - silent: true, - }, +export const checkIsUsedInApp = (id: string) => { + return consoleClient.datasets.byDatasetId.useCheck.get( + { params: { dataset_id: id } }, + { context: { silent: true } }, ) } -export const deleteDataset = (datasetID: string): Promise => { - return del(`/datasets/${datasetID}`) +export const deleteDataset = (datasetID: string) => { + return consoleClient.datasets.byDatasetId.delete({ params: { dataset_id: datasetID } }) } -export const fetchExternalAPI = ({ - apiTemplateId, -}: { - apiTemplateId: string -}): Promise => { - return get(`/datasets/external-knowledge-api/${apiTemplateId}`) +export const fetchExternalAPI = ({ apiTemplateId }: { apiTemplateId: string }) => { + return consoleClient.datasets.externalKnowledgeApi.byExternalKnowledgeApiId.get({ + params: { external_knowledge_api_id: apiTemplateId }, + }) } export const updateExternalAPI = ({ @@ -109,119 +66,71 @@ export const updateExternalAPI = ({ body, }: { apiTemplateId: string - body: ExternalAPIItem -}): Promise => { - return patch(`/datasets/external-knowledge-api/${apiTemplateId}`, { body }) -} - -export const deleteExternalAPI = ({ - apiTemplateId, -}: { - apiTemplateId: string -}): Promise => { - return del(`/datasets/external-knowledge-api/${apiTemplateId}`) -} - -export const checkUsageExternalAPI = ({ - apiTemplateId, -}: { - apiTemplateId: string -}): Promise => { - return get(`/datasets/external-knowledge-api/${apiTemplateId}/use-check`) -} - -export const createExternalAPI = ({ - body, -}: { body: CreateExternalAPIReq -}): Promise => { - return post('/datasets/external-knowledge-api', { body }) +}) => { + return consoleClient.datasets.externalKnowledgeApi.byExternalKnowledgeApiId.patch({ + params: { external_knowledge_api_id: apiTemplateId }, + body, + }) } -export const createExternalKnowledgeBase = ({ - body, -}: { - body: CreateKnowledgeBaseReq -}): Promise => { - return post('/datasets/external', { body }) +export const deleteExternalAPI = ({ apiTemplateId }: { apiTemplateId: string }) => { + return consoleClient.datasets.externalKnowledgeApi.byExternalKnowledgeApiId.delete({ + params: { external_knowledge_api_id: apiTemplateId }, + }) } -export const fetchDefaultProcessRule = ({ url }: { url: string }): Promise => { - return get(url) +export const checkUsageExternalAPI = ({ apiTemplateId }: { apiTemplateId: string }) => { + return consoleClient.datasets.externalKnowledgeApi.byExternalKnowledgeApiId.useCheck.get({ + params: { external_knowledge_api_id: apiTemplateId }, + }) } -export const createFirstDocument = ({ - body, -}: { - body: CreateDocumentReq -}): Promise => { - return post('/datasets/init', { body }) +export const createExternalAPI = ({ body }: { body: CreateExternalAPIReq }) => { + return consoleClient.datasets.externalKnowledgeApi.post({ body }) } -export const createDocument = ({ - datasetId, - body, -}: { - datasetId: string - body: CreateDocumentReq -}): Promise => { - return post(`/datasets/${datasetId}/documents`, { body }) +export const createExternalKnowledgeBase = ({ body }: { body: CreateKnowledgeBaseReq }) => { + return consoleClient.datasets.external.post({ body }) } -export const fetchIndexingStatus = ({ - datasetId, - documentId, -}: CommonDocReq): Promise => { - return get( - `/datasets/${datasetId}/documents/${documentId}/indexing-status`, - {}, - ) +export const fetchIndexingStatus = ({ datasetId, documentId }: CommonDocReq) => { + return consoleClient.datasets.byDatasetId.documents.byDocumentId.indexingStatus.get({ + params: { dataset_id: datasetId, document_id: documentId }, + }) } -export const fetchIndexingStatusBatch = ({ - datasetId, - batchId, -}: BatchReq): Promise => { - return get( - `/datasets/${datasetId}/batch/${batchId}/indexing-status`, - {}, - ) +export const fetchIndexingStatusBatch = ({ datasetId, batchId }: BatchReq) => { + return consoleClient.datasets.byDatasetId.batch.byBatch.indexingStatus.get({ + params: { dataset_id: datasetId, batch: batchId }, + }) } export const renameDocumentName = ({ datasetId, documentId, name, -}: CommonDocReq & { name: string }): Promise => { - return post(`/datasets/${datasetId}/documents/${documentId}/rename`, { +}: CommonDocReq & { name: string }) => { + return consoleClient.datasets.byDatasetId.documents.byDocumentId.rename.post({ + params: { dataset_id: datasetId, document_id: documentId }, body: { name }, }) } -export const pauseDocIndexing = ({ - datasetId, - documentId, -}: CommonDocReq): Promise => { - return patch(`/datasets/${datasetId}/documents/${documentId}/processing/pause`) -} - -export const resumeDocIndexing = ({ - datasetId, - documentId, -}: CommonDocReq): Promise => { - return patch(`/datasets/${datasetId}/documents/${documentId}/processing/resume`) +export const pauseDocIndexing = ({ datasetId, documentId }: CommonDocReq) => { + return consoleClient.datasets.byDatasetId.documents.byDocumentId.processing.pause.patch({ + params: { dataset_id: datasetId, document_id: documentId }, + }) } -export const fetchDocumentDownloadUrl = ({ - datasetId, - documentId, -}: CommonDocReq): Promise => { - return get( - `/datasets/${datasetId}/documents/${documentId}/download`, - {}, - ) +export const resumeDocIndexing = ({ datasetId, documentId }: CommonDocReq) => { + return consoleClient.datasets.byDatasetId.documents.byDocumentId.processing.resume.patch({ + params: { dataset_id: datasetId, document_id: documentId }, + }) } +// Not migrated to the console contract: the endpoint streams a zip file, which the +// JSON-based console client cannot return as a Blob. export const downloadDocumentsZip = ({ datasetId, documentIds, @@ -233,13 +142,6 @@ export const downloadDocumentsZip = ({ }) } -// hit testing -export const fetchFileIndexingEstimate = ( - body: IndexingEstimateParams, -): Promise => { - return post('/datasets/indexing-estimate', { body }) -} - export const fetchNotionPagePreview = ({ pageID, pageType, @@ -248,82 +150,46 @@ export const fetchNotionPagePreview = ({ pageID: string pageType: string credentialID: string -}): Promise<{ content: string }> => { - return get<{ content: string }>(`notion/pages/${pageID}/${pageType}/preview`, { - params: { - credential_id: credentialID, - }, +}) => { + return consoleClient.notion.pages.byPageId.byPageType.preview.get({ + params: { page_id: pageID, page_type: pageType }, + query: { credential_id: credentialID }, }) } -export const createFirecrawlTask = (body: Record): Promise => { - return post('website/crawl', { - body: { - ...body, - provider: DataSourceProvider.fireCrawl, - }, - }) -} +type WebsiteCrawlProvider = 'firecrawl' | 'jinareader' | 'watercrawl' -export const checkFirecrawlTaskStatus = (jobId: string): Promise => { - return get( - `website/crawl/status/${jobId}`, - { - params: { - provider: DataSourceProvider.fireCrawl, - }, - }, - { - silent: true, - }, - ) +type WebsiteCrawlTaskBody = { + url: string + options: Record } -export const createJinaReaderTask = (body: Record): Promise => { - return post('website/crawl', { - body: { - ...body, - provider: DataSourceProvider.jinaReader, - }, - }) -} +const createWebsiteCrawlTask = (provider: WebsiteCrawlProvider, body: WebsiteCrawlTaskBody) => + consoleClient.website.crawl.post({ body: { ...body, provider } }) -export const checkJinaReaderTaskStatus = (jobId: string): Promise => { - return get( - `website/crawl/status/${jobId}`, - { - params: { - provider: 'jinareader', - }, - }, - { - silent: true, - }, +const checkWebsiteCrawlTaskStatus = (provider: WebsiteCrawlProvider, jobId: string) => + consoleClient.website.crawl.status.byJobId.get( + { params: { job_id: jobId }, query: { provider } }, + { context: { silent: true } }, ) -} -export const createWatercrawlTask = (body: Record): Promise => { - return post('website/crawl', { - body: { - ...body, - provider: DataSourceProvider.waterCrawl, - }, - }) -} +export const createFirecrawlTask = (body: WebsiteCrawlTaskBody) => + createWebsiteCrawlTask(DataSourceProvider.fireCrawl, body) -export const checkWatercrawlTaskStatus = (jobId: string): Promise => { - return get( - `website/crawl/status/${jobId}`, - { - params: { - provider: DataSourceProvider.waterCrawl, - }, - }, - { - silent: true, - }, - ) -} +export const checkFirecrawlTaskStatus = (jobId: string) => + checkWebsiteCrawlTaskStatus(DataSourceProvider.fireCrawl, jobId) + +export const createJinaReaderTask = (body: WebsiteCrawlTaskBody) => + createWebsiteCrawlTask(DataSourceProvider.jinaReader, body) + +export const checkJinaReaderTaskStatus = (jobId: string) => + checkWebsiteCrawlTaskStatus(DataSourceProvider.jinaReader, jobId) + +export const createWatercrawlTask = (body: WebsiteCrawlTaskBody) => + createWebsiteCrawlTask(DataSourceProvider.waterCrawl, body) + +export const checkWatercrawlTaskStatus = (jobId: string) => + checkWebsiteCrawlTaskStatus(DataSourceProvider.waterCrawl, jobId) export const retryErrorDocs = ({ datasetId, @@ -331,6 +197,9 @@ export const retryErrorDocs = ({ }: { datasetId: string document_ids: string[] -}): Promise => { - return post(`/datasets/${datasetId}/retry`, { body: { document_ids } }) +}) => { + return consoleClient.datasets.byDatasetId.retry.post({ + params: { dataset_id: datasetId }, + body: { document_ids }, + }) } diff --git a/web/service/debug.ts b/web/service/debug.ts index 42b1c2585b7364..375d2db0b01511 100644 --- a/web/service/debug.ts +++ b/web/service/debug.ts @@ -1,15 +1,11 @@ +import type { AdvancedPromptTemplateResponse } from '@dify/contracts/api/console/app/types.gen' import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' +import type { PostInstructionGenerateData } from '@dify/contracts/api/console/instruction-generate/types.gen' +import type { PostRuleGenerateData } from '@dify/contracts/api/console/rule-generate/types.gen' import type { IOnCompleted, IOnData, IOnError, IOnMessageReplace } from './base' -import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug' -import type { ModelModeType } from '@/types/app' -import { get, post, ssePost } from './base' - -type BasicAppFirstRes = { - prompt: string - variables: string[] - opening_statement: string - error?: string -} +import type { Model, ModelModeType } from '@/types/app' +import { ssePost } from './base' +import { consoleClient } from './console' export type GenRes = { modified: string @@ -19,8 +15,37 @@ export type GenRes = { error?: string } +const getString = (record: Record, key: string) => { + const value = record[key] + return typeof value === 'string' ? value : undefined +} + +/** + * The instruction generator answers with the generated rule, generated code or an LLM-written + * JSON object, depending on what is being edited; read the fields the editors understand. + */ +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +const toGenRes = (value: unknown): GenRes & { code?: string } => { + const fields = isRecord(value) ? value : {} + const variables = fields.variables + return { + modified: getString(fields, 'modified') ?? getString(fields, 'prompt') ?? '', + message: getString(fields, 'message'), + variables: Array.isArray(variables) + ? variables.filter((item): item is string => typeof item === 'string') + : undefined, + opening_statement: getString(fields, 'opening_statement'), + code: getString(fields, 'code'), + error: getString(fields, 'error') || undefined, + } +} + export const stopChatMessageResponding = async (appId: string, taskId: string) => { - return post(`apps/${appId}/chat-messages/${taskId}/stop`) + return consoleClient.apps.byAppId.chatMessages.byTaskId.stop.post({ + params: { app_id: appId, task_id: taskId }, + }) } export const sendCompletionMessage = async ( @@ -46,50 +71,77 @@ export const sendCompletionMessage = async ( ) } +const createAbortSignal = (getAbortController?: (abortController: AbortController) => void) => { + const abortController = new AbortController() + getAbortController?.(abortController) + return abortController.signal +} + export const fetchSuggestedQuestions = ( appId: string, messageId: string, - getAbortController?: any, + getAbortController?: (abortController: AbortController) => void, ) => { - return get( - `apps/${appId}/chat-messages/${messageId}/suggested-questions`, - {}, - { - getAbortController, - }, + return consoleClient.apps.byAppId.chatMessages.byMessageId.suggestedQuestions.get( + { params: { app_id: appId, message_id: messageId } }, + { signal: createAbortSignal(getAbortController) }, ) } export const fetchConversationMessages = ( appId: string, conversation_id: string, - getAbortController?: any, + getAbortController?: (abortController: AbortController) => void, ) => { - return get( - `apps/${appId}/chat-messages`, - { - params: { - conversation_id, - }, - }, - { - getAbortController, - }, + return consoleClient.apps.byAppId.chatMessages.get( + { params: { app_id: appId }, query: { conversation_id } }, + { signal: createAbortSignal(getAbortController) }, ) } -export const generateBasicAppFirstTimeRule = (body: Record) => { - return post('/rule-generate', { - body, - }) +// The model selector keeps an unset mode as '' and generators may omit the flow id; the +// backend validates both, so the UI request shapes are passed through as the contract bodies. +type RuleGenerateRequest = Omit & { + model_config: Model } -export const generateRule = (body: Record) => { - return post('/instruction-generate', { - body, - }) +type InstructionGenerateRequest = Omit< + PostInstructionGenerateData['body'], + 'model_config' | 'flow_id' +> & { + model_config: Model + flow_id?: string +} + +export const generateBasicAppFirstTimeRule = (body: RuleGenerateRequest) => { + return consoleClient.ruleGenerate.post({ body: body as PostRuleGenerateData['body'] }) } +export const generateRule = (body: InstructionGenerateRequest) => { + return consoleClient.instructionGenerate + .post({ body: body as PostInstructionGenerateData['body'] }) + .then(toGenRes) +} + +// A completion template without history prefixes keeps them empty, as the console's default does. +const toPromptTemplates = ({ + chat_prompt_config, + completion_prompt_config, + stop, +}: AdvancedPromptTemplateResponse) => ({ + chat_prompt_config: chat_prompt_config ?? undefined, + completion_prompt_config: completion_prompt_config + ? { + prompt: completion_prompt_config.prompt, + conversation_histories_role: completion_prompt_config.conversation_histories_role ?? { + user_prefix: '', + assistant_prefix: '', + }, + } + : undefined, + stop: stop ?? [], +}) + export const fetchPromptTemplate = ({ appMode, mode, @@ -101,20 +153,16 @@ export const fetchPromptTemplate = ({ modelName: string hasSetDataSet: boolean }) => { - return get< - Promise<{ - chat_prompt_config: ChatPromptConfig - completion_prompt_config: CompletionPromptConfig - stop: [] - }> - >('/app/prompt-templates', { - params: { - app_mode: appMode, - model_mode: mode, - model_name: modelName, - has_context: hasSetDataSet, - }, - }) + return consoleClient.app.promptTemplates + .get({ + query: { + app_mode: appMode, + model_mode: mode, + model_name: modelName, + has_context: String(hasSetDataSet), + }, + }) + .then(toPromptTemplates) } export const fetchTextGenerationMessage = ({ @@ -124,5 +172,7 @@ export const fetchTextGenerationMessage = ({ appId: string messageId: string }) => { - return get>(`/apps/${appId}/messages/${messageId}`) + return consoleClient.apps.byAppId.messages.byMessageId.get({ + params: { app_id: appId, message_id: messageId }, + }) } diff --git a/web/service/knowledge/use-create-dataset.ts b/web/service/knowledge/use-create-dataset.ts deleted file mode 100644 index aa146694536c05..00000000000000 --- a/web/service/knowledge/use-create-dataset.ts +++ /dev/null @@ -1,270 +0,0 @@ -import type { MutationOptions } from '@tanstack/react-query' -import type { IndexingType } from '@/app/components/datasets/create/step-two' -import type { DataSourceProvider, NotionPage } from '@/models/common' -import type { - ChunkingMode, - CrawlOptions, - CrawlResultItem, - CreateDatasetReq, - CreateDatasetResponse, - CreateDocumentReq, - createDocumentResponse, - CustomFile, - DataSourceType, - FileIndexingEstimateResponse, - IndexingEstimateParams, - NotionInfo, - ProcessRule, - ProcessRuleResponse, -} from '@/models/datasets' -import { useMutation } from '@tanstack/react-query' -import { groupBy } from 'es-toolkit/compat' -import { post } from '../base' -import { - createDocument, - createFirstDocument, - fetchDefaultProcessRule, - fetchFileIndexingEstimate, -} from '../datasets' - -const NAME_SPACE = 'knowledge/create-dataset' - -export const getNotionInfo = (notionPages: NotionPage[], credentialId: string) => { - const workspacesMap = groupBy(notionPages, 'workspace_id') - const workspaces = Object.keys(workspacesMap).map((workspaceId) => { - return { - workspaceId, - pages: workspacesMap[workspaceId], - } - }) - return workspaces.map((workspace) => { - return { - credential_id: credentialId, - workspace_id: workspace.workspaceId, - pages: workspace.pages!.map((page) => { - const { page_id, page_name, page_icon, type } = page - return { - page_id, - page_name, - page_icon, - type, - } - }), - } - }) as NotionInfo[] -} - -export const getWebsiteInfo = (opts: { - websiteCrawlProvider: DataSourceProvider - websiteCrawlJobId: string - websitePages: CrawlResultItem[] - crawlOptions?: CrawlOptions -}) => { - const { websiteCrawlProvider, websiteCrawlJobId, websitePages, crawlOptions } = opts - return { - provider: websiteCrawlProvider, - job_id: websiteCrawlJobId, - urls: websitePages.map((page) => page.source_url), - only_main_content: crawlOptions?.only_main_content, - } -} - -type GetFileIndexingEstimateParamsOptionBase = { - docForm: ChunkingMode - docLanguage: string - indexingTechnique: IndexingType - processRule: ProcessRule - dataset_id: string -} - -type GetFileIndexingEstimateParamsOptionFile = GetFileIndexingEstimateParamsOptionBase & { - dataSourceType: typeof DataSourceType.FILE - files: CustomFile[] -} - -const getFileIndexingEstimateParamsForFile = ({ - docForm, - docLanguage, - dataSourceType, - files, - indexingTechnique, - processRule, - dataset_id, -}: GetFileIndexingEstimateParamsOptionFile): IndexingEstimateParams => { - const fileIds = files.map((file) => file.id).filter((id): id is string => Boolean(id)) - - return { - info_list: { - data_source_type: dataSourceType, - file_info_list: { - file_ids: fileIds, - }, - }, - indexing_technique: indexingTechnique, - process_rule: processRule, - doc_form: docForm, - doc_language: docLanguage, - dataset_id, - } -} - -export const useFetchFileIndexingEstimateForFile = ( - options: GetFileIndexingEstimateParamsOptionFile, - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationFn: async () => { - return fetchFileIndexingEstimate(getFileIndexingEstimateParamsForFile(options)) - }, - ...mutationOptions, - }) -} - -type GetFileIndexingEstimateParamsOptionNotion = GetFileIndexingEstimateParamsOptionBase & { - dataSourceType: typeof DataSourceType.NOTION - notionPages: NotionPage[] - credential_id: string -} - -const getFileIndexingEstimateParamsForNotion = ({ - docForm, - docLanguage, - dataSourceType, - notionPages, - indexingTechnique, - processRule, - dataset_id, - credential_id, -}: GetFileIndexingEstimateParamsOptionNotion): IndexingEstimateParams => { - return { - info_list: { - data_source_type: dataSourceType, - notion_info_list: getNotionInfo(notionPages, credential_id), - }, - indexing_technique: indexingTechnique, - process_rule: processRule, - doc_form: docForm, - doc_language: docLanguage, - dataset_id, - } -} - -export const useFetchFileIndexingEstimateForNotion = ( - options: GetFileIndexingEstimateParamsOptionNotion, - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationFn: async () => { - return fetchFileIndexingEstimate(getFileIndexingEstimateParamsForNotion(options)) - }, - ...mutationOptions, - }) -} - -type GetFileIndexingEstimateParamsOptionWeb = GetFileIndexingEstimateParamsOptionBase & { - dataSourceType: typeof DataSourceType.WEB - websitePages: CrawlResultItem[] - crawlOptions?: CrawlOptions - websiteCrawlProvider: DataSourceProvider - websiteCrawlJobId: string -} - -const getFileIndexingEstimateParamsForWeb = ({ - docForm, - docLanguage, - dataSourceType, - websitePages, - crawlOptions, - websiteCrawlProvider, - websiteCrawlJobId, - indexingTechnique, - processRule, - dataset_id, -}: GetFileIndexingEstimateParamsOptionWeb): IndexingEstimateParams => { - return { - info_list: { - data_source_type: dataSourceType, - website_info_list: getWebsiteInfo({ - websiteCrawlProvider, - websiteCrawlJobId, - websitePages, - crawlOptions, - }), - }, - indexing_technique: indexingTechnique, - process_rule: processRule, - doc_form: docForm, - doc_language: docLanguage, - dataset_id, - } -} - -export const useFetchFileIndexingEstimateForWeb = ( - options: GetFileIndexingEstimateParamsOptionWeb, - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationFn: async () => { - return fetchFileIndexingEstimate(getFileIndexingEstimateParamsForWeb(options)) - }, - ...mutationOptions, - }) -} - -export const useCreateFirstDocument = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationFn: async (createDocumentReq: CreateDocumentReq) => { - return createFirstDocument({ body: createDocumentReq }) - }, - ...mutationOptions, - }) -} - -export const useCreateDocument = ( - datasetId: string, - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationFn: async (req: CreateDocumentReq) => { - return createDocument({ datasetId, body: req }) - }, - ...mutationOptions, - }) -} - -export const useFetchDefaultProcessRule = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationFn: async (url: string) => { - return fetchDefaultProcessRule({ url }) - }, - ...mutationOptions, - }) -} - -export const useCreatePipelineDataset = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'create-pipeline-empty-dataset'], - mutationFn: () => { - return post('/rag/pipeline/empty-dataset') - }, - ...mutationOptions, - }) -} - -export const useCreatePipelineDatasetFromCustomized = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'create-pipeline-dataset'], - mutationFn: (req: CreateDatasetReq) => { - return post('/rag/pipeline/dataset', { body: req }) - }, - ...mutationOptions, - }) -} diff --git a/web/service/knowledge/use-dataset.spec.ts b/web/service/knowledge/use-dataset.spec.ts deleted file mode 100644 index ca472887f0d6d4..00000000000000 --- a/web/service/knowledge/use-dataset.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { createHttpError } from '@/service/http-error' -import { get } from '../base' -import { useDatasetDetail, useDatasetRelatedApps } from './use-dataset' - -vi.mock('@tanstack/react-query', () => ({ - keepPreviousData: Symbol('keepPreviousData'), - useInfiniteQuery: vi.fn(), - useMutation: vi.fn(), - useQuery: vi.fn(), - useQueryClient: vi.fn(), -})) - -vi.mock('../base', () => ({ - get: vi.fn(), - post: vi.fn(), -})) - -vi.mock('../use-base', () => ({ - useInvalid: vi.fn(), -})) - -const mockUseQuery = vi.mocked(useQuery) -const mockGet = vi.mocked(get) - -type QueryOptions = Parameters[0] -type RetryFn = (failureCount: number, error: unknown) => boolean - -const getLastQueryOptions = () => { - return mockUseQuery.mock.calls.at(-1)?.[0] as QueryOptions -} - -const getRetryFn = () => { - return getLastQueryOptions().retry as RetryFn -} - -describe('knowledge dataset hooks', () => { - beforeEach(() => { - vi.clearAllMocks() - mockUseQuery.mockReturnValue({} as ReturnType) - }) - - describe('useDatasetDetail', () => { - it('should not retry forbidden or missing dataset detail errors', () => { - // Arrange & Act - useDatasetDetail('dataset-1') - const retry = getRetryFn() - - // Assert - expect(retry(0, createHttpError(new Response(null, { status: 403 })))).toBe(false) - expect(retry(0, createHttpError(new Response(null, { status: 404 })))).toBe(false) - }) - - it('should retry other dataset detail errors fewer than three times', () => { - // Arrange & Act - useDatasetDetail('dataset-1') - const retry = getRetryFn() - - // Assert - expect(retry(2, new Error('temporary failure'))).toBe(true) - expect(retry(3, new Error('temporary failure'))).toBe(false) - }) - - it('should fetch dataset detail without silent mode', () => { - // Arrange - mockGet.mockResolvedValue({ id: 'dataset-1' }) - - // Act - useDatasetDetail('dataset-1') - const queryFn = getLastQueryOptions().queryFn as () => unknown - queryFn() - - // Assert - expect(mockGet).toHaveBeenCalledWith('/datasets/dataset-1') - }) - }) - - describe('useDatasetRelatedApps', () => { - it('should use explicit enabled option when provided', () => { - // Arrange & Act - useDatasetRelatedApps('dataset-1', { enabled: false }) - - // Assert - expect(getLastQueryOptions().enabled).toBe(false) - }) - - it('should enable related apps query when dataset id exists and no option is provided', () => { - // Arrange & Act - useDatasetRelatedApps('dataset-1') - - // Assert - expect(getLastQueryOptions().enabled).toBe(true) - }) - }) -}) diff --git a/web/service/knowledge/use-dataset.ts b/web/service/knowledge/use-dataset.ts deleted file mode 100644 index 1785beffa6336a..00000000000000 --- a/web/service/knowledge/use-dataset.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { MutationOptions } from '@tanstack/react-query' -import type { CommonResponse } from '@/models/common' -import type { - DataSet, - DatasetListRequest, - DataSetListResponse, - ErrorDocsResponse, - FetchDatasetsParams, - HitTestingRecordsResponse, - IndexingStatusBatchRequest, - IndexingStatusBatchResponse, - ProcessRuleResponse, - RelatedAppResponse, -} from '@/models/datasets' -import { ORPCError } from '@orpc/client' -import { keepPreviousData, useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query' -import qs from 'qs' -import { get, post } from '../base' -import { useInvalid } from '../use-base' - -const NAME_SPACE = 'dataset' - -const datasetListQueryKey = [NAME_SPACE, 'list'] - -const normalizeDatasetsParams = (params: Partial = {}) => { - const { page = 1, limit, ids, tag_ids, include_all, keyword } = params - - return { - page, - ...(limit ? { limit } : {}), - ...(ids?.length ? { ids } : {}), - ...(tag_ids?.length ? { tag_ids } : {}), - ...(include_all !== undefined ? { include_all } : {}), - ...(keyword ? { keyword } : {}), - } -} - -type UseInfiniteDatasetsOptions = { - enabled?: boolean - refetchOnMount?: boolean | 'always' - staleTime?: number - refetchOnReconnect?: boolean - refetchOnWindowFocus?: boolean -} - -export const useInfiniteDatasets = ( - params: Partial, - options?: UseInfiniteDatasetsOptions, -) => { - const normalizedParams = normalizeDatasetsParams(params) - - return useInfiniteQuery({ - queryKey: [...datasetListQueryKey, 'infinite', normalizedParams], - queryFn: ({ pageParam = normalizedParams.page }) => { - const queryString = qs.stringify( - { - ...normalizedParams, - page: pageParam as number | undefined, - }, - { indices: false }, - ) - return get(`/datasets?${queryString}`) - }, - getNextPageParam: (lastPage) => (lastPage.has_more ? lastPage.page + 1 : undefined), - initialPageParam: normalizedParams.page, - staleTime: 0, - refetchOnMount: 'always', - ...options, - }) -} - -export const useDatasetList = (params: DatasetListRequest) => { - const { initialPage, tag_ids, limit, include_all, keyword } = params - return useInfiniteQuery({ - queryKey: [...datasetListQueryKey, initialPage, tag_ids, limit, include_all, keyword], - queryFn: ({ pageParam = 1 }) => { - const urlParams = qs.stringify( - { - tag_ids, - limit, - include_all, - keyword, - page: pageParam, - }, - { indices: false }, - ) - return get(`/datasets?${urlParams}`) - }, - getNextPageParam: (lastPage) => (lastPage.has_more ? lastPage.page + 1 : null), - initialPageParam: initialPage, - placeholderData: keepPreviousData, - }) -} - -export const useInvalidDatasetList = () => { - return useInvalid([...datasetListQueryKey]) -} - -export const datasetDetailQueryKeyPrefix = [NAME_SPACE, 'detail'] - -export const useDatasetDetail = (datasetId: string) => { - return useQuery({ - queryKey: [...datasetDetailQueryKeyPrefix, datasetId], - queryFn: () => get(`/datasets/${datasetId}`), - enabled: !!datasetId, - retry: (failureCount, error) => { - if (error instanceof ORPCError && [403, 404].includes(error.status)) return false - - return failureCount < 3 - }, - }) -} - -export const useDatasetRelatedApps = (datasetId: string, options?: { enabled?: boolean }) => { - return useQuery({ - queryKey: [NAME_SPACE, 'related-apps', datasetId], - queryFn: () => get(`/datasets/${datasetId}/related-apps`), - enabled: options?.enabled ?? !!datasetId, - }) -} - -export const useIndexingStatusBatch = ( - params: IndexingStatusBatchRequest, - mutationOptions: MutationOptions = {}, -) => { - const { datasetId, batchId } = params - return useMutation({ - mutationKey: [NAME_SPACE, 'indexing-status-batch', datasetId, batchId], - mutationFn: () => - get(`/datasets/${datasetId}/batch/${batchId}/indexing-status`), - ...mutationOptions, - }) -} - -export const useProcessRule = (documentId?: string) => { - return useQuery({ - queryKey: [NAME_SPACE, 'process-rule', documentId], - queryFn: () => - get('/datasets/process-rule', { params: { document_id: documentId } }), - enabled: !!documentId, - refetchOnWindowFocus: false, - }) -} - -export const useDatasetApiBaseUrl = () => { - return useQuery<{ api_base_url: string }>({ - queryKey: [NAME_SPACE, 'api-base-info'], - queryFn: () => get<{ api_base_url: string }>('/datasets/api-base-info'), - }) -} - -export const useEnableDatasetServiceApi = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'enable-api'], - mutationFn: (datasetId: string) => - post(`/datasets/${datasetId}/api-keys/enable`), - }) -} - -export const useDisableDatasetServiceApi = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'disable-api'], - mutationFn: (datasetId: string) => - post(`/datasets/${datasetId}/api-keys/disable`), - }) -} - -export const useDatasetTestingRecords = ( - datasetId?: string, - params?: { page: number; limit: number }, - options?: { enabled?: boolean }, -) => { - return useQuery({ - queryKey: [NAME_SPACE, 'testing-records', datasetId, params], - queryFn: () => get(`/datasets/${datasetId}/queries`, { params }), - enabled: !!datasetId && !!params && (options?.enabled ?? true), - placeholderData: keepPreviousData, - }) -} - -export const useDatasetErrorDocs = (datasetId?: string) => { - return useQuery({ - queryKey: [NAME_SPACE, 'error-docs', datasetId], - queryFn: () => get(`/datasets/${datasetId}/error-docs`), - enabled: !!datasetId, - }) -} diff --git a/web/service/knowledge/use-document.ts b/web/service/knowledge/use-document.ts index 8ff809e911c673..a74c7463aca31a 100644 --- a/web/service/knowledge/use-document.ts +++ b/web/service/knowledge/use-document.ts @@ -1,214 +1,6 @@ -import type { UseQueryOptions } from '@tanstack/react-query' -import type { - DocumentDownloadResponse, - DocumentDownloadZipRequest, - MetadataType, - SortType, -} from '../datasets' -import type { CommonResponse } from '@/models/common' -import type { - DocumentDetailResponse, - DocumentListResponse, - UpdateDocumentBatchParams, -} from '@/models/datasets' -import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query' -import { normalizeStatusForQuery } from '@/app/components/datasets/documents/status-filter' -import { DocumentActionType } from '@/models/datasets' -import { del, get, patch, post } from '../base' -import { - downloadDocumentsZip, - fetchDocumentDownloadUrl, - pauseDocIndexing, - resumeDocIndexing, -} from '../datasets' -import { useInvalid } from '../use-base' - -const NAME_SPACE = 'knowledge/document' - -export const useDocumentListKey = [NAME_SPACE, 'documentList'] -type DocumentListRefetchInterval = UseQueryOptions['refetchInterval'] - -export const useDocumentList = (payload: { - datasetId: string - query: { - keyword: string - page: number - limit: number - sort?: SortType - status?: string - } - refetchInterval?: DocumentListRefetchInterval -}) => { - const { query, datasetId, refetchInterval } = payload - const { keyword, page, limit, sort, status } = query - const normalizedStatus = normalizeStatusForQuery(status) - const params: Record = { - keyword, - page, - limit, - } - if (sort) params.sort = sort - if (normalizedStatus && normalizedStatus !== 'all') params.status = normalizedStatus - return useQuery({ - queryKey: [...useDocumentListKey, datasetId, params], - queryFn: () => - get(`/datasets/${datasetId}/documents`, { - params, - }), - placeholderData: keepPreviousData, - refetchInterval, - }) -} - -export const useInvalidDocumentList = (datasetId?: string) => { - return useInvalid(datasetId ? [...useDocumentListKey, datasetId] : useDocumentListKey) -} - -const useAutoDisabledDocumentKey = [NAME_SPACE, 'autoDisabledDocument'] -export const useAutoDisabledDocuments = (datasetId: string) => { - return useQuery({ - queryKey: [...useAutoDisabledDocumentKey, datasetId], - queryFn: () => get<{ document_ids: string[] }>(`/datasets/${datasetId}/auto-disable-logs`), - }) -} - -export const useInvalidDisabledDocument = () => { - return useInvalid(useAutoDisabledDocumentKey) -} - -const toBatchDocumentsIdParams = (documentIds: string[] | string) => { - const ids = Array.isArray(documentIds) ? documentIds : [documentIds] - return ids.map((id) => `document_id=${id}`).join('&') -} - -const useDocumentBatchAction = (action: DocumentActionType) => { - return useMutation({ - mutationFn: ({ datasetId, documentIds, documentId }: UpdateDocumentBatchParams) => { - return patch( - `/datasets/${datasetId}/documents/status/${action}/batch?${toBatchDocumentsIdParams(documentId || documentIds!)}`, - ) - }, - }) -} - -export const useDocumentEnable = () => { - return useDocumentBatchAction(DocumentActionType.enable) -} - -export const useDocumentDisable = () => { - return useDocumentBatchAction(DocumentActionType.disable) -} - -export const useDocumentArchive = () => { - return useDocumentBatchAction(DocumentActionType.archive) -} - -export const useDocumentUnArchive = () => { - return useDocumentBatchAction(DocumentActionType.unArchive) -} - -export const useDocumentDelete = () => { - return useMutation({ - mutationFn: ({ datasetId, documentIds, documentId }: UpdateDocumentBatchParams) => { - return del( - `/datasets/${datasetId}/documents?${toBatchDocumentsIdParams(documentId || documentIds!)}`, - ) - }, - }) -} - -export const useSyncDocument = () => { - return useMutation({ - mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => { - return get(`/datasets/${datasetId}/documents/${documentId}/notion/sync`) - }, - }) -} - -export const useDocumentSummary = () => { - return useMutation({ - mutationFn: ({ datasetId, documentIds, documentId }: UpdateDocumentBatchParams) => { - return post(`/datasets/${datasetId}/documents/generate-summary`, { - body: { - document_list: documentId ? [documentId] : documentIds!, - }, - }) - }, - }) -} - -export const useSyncWebsite = () => { - return useMutation({ - mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => { - return get(`/datasets/${datasetId}/documents/${documentId}/website-sync`) - }, - }) -} - -const useDocumentDetailKey = [NAME_SPACE, 'documentDetail', 'withoutMetaData'] -type DocumentDetailRefetchInterval = UseQueryOptions['refetchInterval'] - -export const useDocumentDetail = (payload: { - datasetId: string - documentId: string - params: { metadata: MetadataType } - refetchInterval?: DocumentDetailRefetchInterval -}) => { - const { datasetId, documentId, params, refetchInterval } = payload - return useQuery({ - queryKey: [...useDocumentDetailKey, 'withoutMetaData', datasetId, documentId, params], - queryFn: () => - get(`/datasets/${datasetId}/documents/${documentId}`, { params }), - refetchInterval, - }) -} - -export const useDocumentMetadata = (payload: { - datasetId: string - documentId: string - params: { metadata: MetadataType } -}) => { - const { datasetId, documentId, params } = payload - return useQuery({ - queryKey: [...useDocumentDetailKey, 'onlyMetaData', datasetId, documentId, params], - queryFn: () => - get(`/datasets/${datasetId}/documents/${documentId}`, { params }), - }) -} - -export const useInvalidDocumentDetail = () => { - return useInvalid(useDocumentDetailKey) -} - -export const useDocumentPause = () => { - return useMutation({ - mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => { - if (!datasetId || !documentId) throw new Error('datasetId and documentId are required') - return pauseDocIndexing({ datasetId, documentId }) as Promise - }, - }) -} - -export const useDocumentResume = () => { - return useMutation({ - mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => { - if (!datasetId || !documentId) throw new Error('datasetId and documentId are required') - return resumeDocIndexing({ datasetId, documentId }) as Promise - }, - }) -} - -export const useDocumentDownload = () => { - return useMutation({ - mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => { - if (!datasetId || !documentId) throw new Error('datasetId and documentId are required') - return fetchDocumentDownloadUrl({ - datasetId, - documentId, - }) as Promise - }, - }) -} +import type { DocumentDownloadZipRequest } from '../datasets' +import { useMutation } from '@tanstack/react-query' +import { downloadDocumentsZip } from '../datasets' export const useDocumentDownloadZip = () => { return useMutation({ @@ -219,15 +11,3 @@ export const useDocumentDownloadZip = () => { }, }) } - -export const useDocumentBatchRetryIndex = () => { - return useMutation({ - mutationFn: ({ datasetId, documentIds }: { datasetId: string; documentIds: string[] }) => { - return post(`/datasets/${datasetId}/retry`, { - body: { - document_ids: documentIds, - }, - }) - }, - }) -} diff --git a/web/service/knowledge/use-hit-testing.ts b/web/service/knowledge/use-hit-testing.ts deleted file mode 100644 index 94667a912c71f2..00000000000000 --- a/web/service/knowledge/use-hit-testing.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { - ExternalKnowledgeBaseHitTestingRequest, - ExternalKnowledgeBaseHitTestingResponse, - HitTestingRequest, - HitTestingResponse, -} from '@/models/datasets' -import { useMutation } from '@tanstack/react-query' -import { post } from '../base' - -const NAME_SPACE = 'hit-testing' - -export const useHitTesting = (datasetId: string) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'hit-testing', datasetId], - mutationFn: (params: HitTestingRequest) => - post(`/datasets/${datasetId}/hit-testing`, { - body: params, - }), - }) -} - -export const useExternalKnowledgeBaseHitTesting = (datasetId: string) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'external-knowledge-base-hit-testing', datasetId], - mutationFn: (params: ExternalKnowledgeBaseHitTestingRequest) => - post(`/datasets/${datasetId}/external-hit-testing`, { - body: params, - }), - }) -} diff --git a/web/service/knowledge/use-import.ts b/web/service/knowledge/use-import.ts deleted file mode 100644 index 4194f79e970f8f..00000000000000 --- a/web/service/knowledge/use-import.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { DataSourceNotionWorkspace } from '@/models/common' -import { useQuery, useQueryClient } from '@tanstack/react-query' -import { get } from '../base' - -type PreImportNotionPagesParams = { - datasetId: string - credentialId: string -} - -const PRE_IMPORT_NOTION_PAGES_QUERY_KEY = 'notion-pre-import-pages' - -export const usePreImportNotionPages = ({ - datasetId, - credentialId, -}: PreImportNotionPagesParams) => { - return useQuery({ - queryKey: [PRE_IMPORT_NOTION_PAGES_QUERY_KEY, datasetId, credentialId], - queryFn: async () => { - return get<{ notion_info: DataSourceNotionWorkspace[] }>('/notion/pre-import/pages', { - params: { - dataset_id: datasetId, - credential_id: credentialId, - }, - }) - }, - retry: 0, - }) -} - -export const useInvalidPreImportNotionPages = () => { - const queryClient = useQueryClient() - return ({ datasetId, credentialId }: PreImportNotionPagesParams) => { - queryClient.invalidateQueries({ - queryKey: [PRE_IMPORT_NOTION_PAGES_QUERY_KEY, datasetId, credentialId], - }) - } -} diff --git a/web/service/knowledge/use-metadata.ts b/web/service/knowledge/use-metadata.ts deleted file mode 100644 index adf9631fe7e0de..00000000000000 --- a/web/service/knowledge/use-metadata.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { - BuiltInMetadataItem, - MetadataBatchEditToServer, - MetadataItemWithValueLength, -} from '@/app/components/datasets/metadata/types' -import type { DocumentDetailResponse } from '@/models/datasets' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { del, get, patch, post } from '../base' -import { useInvalid } from '../use-base' -import { useDocumentListKey, useInvalidDocumentList } from './use-document' - -const NAME_SPACE = 'dataset-metadata' - -export const useDatasetMetaData = (datasetId: string) => { - return useQuery<{ doc_metadata: MetadataItemWithValueLength[]; built_in_field_enabled: boolean }>( - { - queryKey: [NAME_SPACE, 'dataset', datasetId], - queryFn: () => { - return get<{ - doc_metadata: MetadataItemWithValueLength[] - built_in_field_enabled: boolean - }>(`/datasets/${datasetId}/metadata`) - }, - }, - ) -} - -const useInvalidDatasetMetaData = (datasetId: string) => { - return useInvalid([NAME_SPACE, 'dataset', datasetId]) -} - -export const useCreateMetaData = (datasetId: string) => { - const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId) - return useMutation({ - mutationFn: async (payload: BuiltInMetadataItem) => { - await post(`/datasets/${datasetId}/metadata`, { - body: payload, - }) - await invalidDatasetMetaData() - return Promise.resolve(true) - }, - }) -} -const useInvalidAllDocumentMetaData = (datasetId: string) => { - const queryClient = useQueryClient() - return () => { - queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'document', datasetId], - exact: false, // invalidate all document metadata: [NAME_SPACE, 'document', datasetId, documentId] - }) - } -} - -const useInvalidAllMetaData = (datasetId: string) => { - const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId) - const invalidDocumentList = useInvalidDocumentList(datasetId) - const invalidateAllDocumentMetaData = useInvalidAllDocumentMetaData(datasetId) - - return async () => { - // meta data in dataset - await invalidDatasetMetaData() - // meta data in document list - invalidDocumentList() - // meta data in single document - await invalidateAllDocumentMetaData() // meta data in document - } -} - -export const useRenameMeta = (datasetId: string) => { - const invalidateAllMetaData = useInvalidAllMetaData(datasetId) - return useMutation({ - mutationFn: async (payload: MetadataItemWithValueLength) => { - await patch(`/datasets/${datasetId}/metadata/${payload.id}`, { - body: { - name: payload.name, - }, - }) - await invalidateAllMetaData() - }, - }) -} - -export const useDeleteMetaData = (datasetId: string) => { - const invalidateAllMetaData = useInvalidAllMetaData(datasetId) - return useMutation({ - mutationFn: async (metaDataId: string) => { - // datasetMetaData = datasetMetaData.filter(item => item.id !== metaDataId) - await del(`/datasets/${datasetId}/metadata/${metaDataId}`) - await invalidateAllMetaData() - }, - }) -} - -export const useBuiltInMetaDataFields = () => { - return useQuery<{ fields: BuiltInMetadataItem[] }>({ - queryKey: [NAME_SPACE, 'built-in'], - queryFn: () => { - return get('/datasets/metadata/built-in') - }, - }) -} - -export const useDocumentMetaData = ({ - datasetId, - documentId, -}: { - datasetId: string - documentId: string -}) => { - return useQuery({ - queryKey: [NAME_SPACE, 'document', datasetId, documentId], - queryFn: () => { - return get(`/datasets/${datasetId}/documents/${documentId}`, { - params: { metadata: 'only' }, - }) - }, - }) -} - -export const useBatchUpdateDocMetadata = () => { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: async (payload: { - dataset_id: string - metadata_list: MetadataBatchEditToServer - }) => { - const documentIds = payload.metadata_list.map((item) => item.document_id) - await post(`/datasets/${payload.dataset_id}/documents/metadata`, { - body: { - operation_data: payload.metadata_list, - }, - }) - // meta data in dataset - await queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'dataset', payload.dataset_id], - }) - // meta data in document list - await queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'document', payload.dataset_id], - }) - await queryClient.invalidateQueries({ - queryKey: [...useDocumentListKey, payload.dataset_id], - }) - - // meta data in single document - await Promise.all( - documentIds.map((documentId) => - queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'document', payload.dataset_id, documentId], - }), - ), - ) - }, - }) -} - -export const useUpdateBuiltInStatus = (datasetId: string) => { - const invalidDatasetMetaData = useInvalidDatasetMetaData(datasetId) - return useMutation({ - mutationFn: async (enabled: boolean) => { - await post(`/datasets/${datasetId}/metadata/built-in/${enabled ? 'enable' : 'disable'}`) - invalidDatasetMetaData() - }, - }) -} diff --git a/web/service/knowledge/use-segment.ts b/web/service/knowledge/use-segment.ts deleted file mode 100644 index 745992b082532d..00000000000000 --- a/web/service/knowledge/use-segment.ts +++ /dev/null @@ -1,216 +0,0 @@ -import type { CommonResponse } from '@/models/common' -import type { - BatchImportResponse, - ChildChunkDetail, - ChildSegmentsResponse, - ChunkingMode, - SegmentDetailModel, - SegmentsResponse, - SegmentUpdater, -} from '@/models/datasets' -import { useMutation, useQuery } from '@tanstack/react-query' -import { del, get, patch, post } from '../base' - -const NAME_SPACE = 'segment' - -export const useSegmentListKey = [NAME_SPACE, 'chunkList'] -export const useChunkListEnabledKey = [NAME_SPACE, 'chunkList', { enabled: true }] -export const useChunkListDisabledKey = [NAME_SPACE, 'chunkList', { enabled: false }] -export const useChunkListAllKey = [NAME_SPACE, 'chunkList', { enabled: 'all' }] - -export const useSegmentList = ( - payload: { - datasetId: string - documentId: string - params: { - page: number - limit: number - keyword: string - enabled: boolean | 'all' | '' - } - }, - disable?: boolean, -) => { - const { datasetId, documentId, params } = payload - - return useQuery({ - queryKey: [...useSegmentListKey, datasetId, documentId, params], - queryFn: () => { - return get(`/datasets/${datasetId}/documents/${documentId}/segments`, { - params, - }) - }, - enabled: !disable, - }) -} - -export const useUpdateSegment = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'update'], - mutationFn: (payload: { - datasetId: string - documentId: string - segmentId: string - body: SegmentUpdater - }) => { - const { datasetId, documentId, segmentId, body } = payload - return patch<{ data: SegmentDetailModel; doc_form: ChunkingMode }>( - `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, - { body }, - ) - }, - }) -} - -export const useAddSegment = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'add'], - mutationFn: (payload: { datasetId: string; documentId: string; body: SegmentUpdater }) => { - const { datasetId, documentId, body } = payload - return post<{ data: SegmentDetailModel; doc_form: ChunkingMode }>( - `/datasets/${datasetId}/documents/${documentId}/segment`, - { body }, - ) - }, - }) -} - -export const useEnableSegment = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'enable'], - mutationFn: (payload: { datasetId: string; documentId: string; segmentIds: string[] }) => { - const { datasetId, documentId, segmentIds } = payload - const query = segmentIds.map((id) => `segment_id=${id}`).join('&') - return patch( - `/datasets/${datasetId}/documents/${documentId}/segment/enable?${query}`, - ) - }, - }) -} - -export const useDisableSegment = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'disable'], - mutationFn: (payload: { datasetId: string; documentId: string; segmentIds: string[] }) => { - const { datasetId, documentId, segmentIds } = payload - const query = segmentIds.map((id) => `segment_id=${id}`).join('&') - return patch( - `/datasets/${datasetId}/documents/${documentId}/segment/disable?${query}`, - ) - }, - }) -} - -export const useDeleteSegment = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'delete'], - mutationFn: (payload: { datasetId: string; documentId: string; segmentIds: string[] }) => { - const { datasetId, documentId, segmentIds } = payload - const query = segmentIds.map((id) => `segment_id=${id}`).join('&') - return del(`/datasets/${datasetId}/documents/${documentId}/segments?${query}`) - }, - }) -} - -export const useChildSegmentListKey = [NAME_SPACE, 'childChunkList'] - -export const useChildSegmentList = ( - payload: { - datasetId: string - documentId: string - segmentId: string - params: { - page: number - limit: number - keyword: string - } - }, - disable?: boolean, -) => { - const { datasetId, documentId, segmentId, params } = payload - - return useQuery({ - queryKey: [...useChildSegmentListKey, datasetId, documentId, segmentId, params], - queryFn: () => { - return get( - `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, - { params }, - ) - }, - enabled: !disable, - }) -} - -export const useDeleteChildSegment = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'childChunk', 'delete'], - mutationFn: (payload: { - datasetId: string - documentId: string - segmentId: string - childChunkId: string - }) => { - const { datasetId, documentId, segmentId, childChunkId } = payload - return del( - `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`, - ) - }, - }) -} - -export const useAddChildSegment = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'childChunk', 'add'], - mutationFn: (payload: { - datasetId: string - documentId: string - segmentId: string - body: { content: string } - }) => { - const { datasetId, documentId, segmentId, body } = payload - return post<{ data: ChildChunkDetail }>( - `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, - { body }, - ) - }, - }) -} - -export const useUpdateChildSegment = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'childChunk', 'update'], - mutationFn: (payload: { - datasetId: string - documentId: string - segmentId: string - childChunkId: string - body: { content: string } - }) => { - const { datasetId, documentId, segmentId, childChunkId, body } = payload - return patch<{ data: ChildChunkDetail }>( - `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`, - { body }, - ) - }, - }) -} - -export const useSegmentBatchImport = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'batchImport'], - mutationFn: (payload: { url: string; body: { upload_file_id: string } }) => { - const { url, body } = payload - return post(url, { body }) - }, - }) -} - -export const useCheckSegmentBatchImportProgress = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'batchImport', 'checkProgress'], - mutationFn: (payload: { jobID: string }) => { - const { jobID } = payload - return get(`/datasets/batch_import_status/${jobID}`) - }, - }) -} diff --git a/web/service/log.ts b/web/service/log.ts index 32fdf19138475f..1595b5489234ef 100644 --- a/web/service/log.ts +++ b/web/service/log.ts @@ -1,47 +1,6 @@ -import type { - AgentLogDetailRequest, - AgentLogDetailResponse, - ChatMessagesRequest, - ChatMessagesResponse, - LogMessageAnnotationsRequest, - LogMessageAnnotationsResponse, - LogMessageFeedbacksRequest, - LogMessageFeedbacksResponse, - WorkflowRunDetailResponse, -} from '@/models/log' +import type { WorkflowRunDetailResponse } from '@/models/log' import type { NodeTracingListResponse } from '@/types/workflow' -import { get, post } from './base' - -// (Chat Application) Message list in one session -export const fetchChatMessages = ({ - url, - params, -}: { - url: string - params: ChatMessagesRequest -}): Promise => { - return get(url, { params }) -} - -export const updateLogMessageFeedbacks = ({ - url, - body, -}: { - url: string - body: LogMessageFeedbacksRequest -}): Promise => { - return post(url, { body }) -} - -export const updateLogMessageAnnotations = ({ - url, - body, -}: { - url: string - body: LogMessageAnnotationsRequest -}): Promise => { - return post(url, { body }) -} +import { get } from './base' export const fetchRunDetail = (url: string): Promise => { return get(url) @@ -50,13 +9,3 @@ export const fetchRunDetail = (url: string): Promise export const fetchTracingList = ({ url }: { url: string }): Promise => { return get(url) } - -export const fetchAgentLogDetail = ({ - appID, - params, -}: { - appID: string - params: AgentLogDetailRequest -}): Promise => { - return get(`/apps/${appID}/agent/logs`, { params }) -} diff --git a/web/service/plugins.ts b/web/service/plugins.ts index fdfcdb47d2c725..d55fdde02289c0 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -1,14 +1,8 @@ +import type { ParserMarketplaceUpgrade } from '@dify/contracts/api/console/workspaces/types.gen' import type { PluginInfoFromMarketPlace } from '@dify/contracts/marketplace' -import type { - Dependency, - InstallPackageResponse, - PluginManifestInMarket, - TaskStatusResponse, - updatePackageResponse, - uploadGitHubResponse, -} from '@/app/components/plugins/types' +import type { Dependency, PluginManifestInMarket } from '@/app/components/plugins/types' import { consoleClient } from '@/service/console' -import { get, getMarketplace, post, upload } from './base' +import { getMarketplace, upload } from './base' export const uploadFile = async (file: File, isBundle: boolean) => { const formData = new FormData() @@ -23,10 +17,8 @@ export const uploadFile = async (file: File, isBundle: boolean) => { ) } -export const updateFromMarketPlace = async (body: Record) => { - return post('/workspaces/current/plugin/upgrade/marketplace', { - body, - }) +export const updateFromMarketPlace = async (body: ParserMarketplaceUpgrade) => { + return consoleClient.workspaces.current.plugin.upgrade.marketplace.post({ body }) } export const updateFromGitHub = async ( @@ -36,7 +28,7 @@ export const updateFromGitHub = async ( originalPlugin: string, newPlugin: string, ) => { - return post('/workspaces/current/plugin/upgrade/github', { + return consoleClient.workspaces.current.plugin.upgrade.github.post({ body: { repo: repoUrl, version: selectedVersion, @@ -52,7 +44,7 @@ export const uploadGitHub = async ( selectedVersion: string, selectedPackage: string, ) => { - return post('/workspaces/current/plugin/upload/github', { + return consoleClient.workspaces.current.plugin.upload.github.post({ body: { repo: repoUrl, version: selectedVersion, @@ -84,7 +76,7 @@ export const fetchPluginInfoFromMarketPlace = async ({ org, name }: Record { - return get(`/workspaces/current/plugin/tasks/${taskId}`) + return consoleClient.workspaces.current.plugin.tasks.byTaskId.get({ params: { task_id: taskId } }) } export const uninstallPlugin = async ( diff --git a/web/service/tools.ts b/web/service/tools.ts index 08b6f54e5c1a27..31e7f5323ea20a 100644 --- a/web/service/tools.ts +++ b/web/service/tools.ts @@ -1,38 +1,65 @@ import type { - Collection, Credential, CustomCollectionBackend, - CustomParamSchema, - Tool, ToolCredential, + WorkflowToolProviderParameter, WorkflowToolProviderRequest, - WorkflowToolProviderResponse, } from '@/app/components/tools/types' -import { buildProviderQuery } from './_tools_util' +import { + selectApiSchemaParameters, + selectBuiltinProviderTools, + selectCustomCollection, + selectToolCollections, + selectWorkflowToolDetail, +} from '@/app/components/tools/utils/query-select' +// oxlint-disable-next-line no-restricted-imports -- legacy built-in credential flow, see below import { get, post } from './base' +import { consoleClient } from './console' + +const toolProvider = consoleClient.workspaces.current.toolProvider + +type ApiToolSchemaType = 'openai_actions' | 'openai_plugin' | 'openapi' | 'swagger' + +type WorkflowToolParameterForm = 'form' | 'llm' | 'schema' + +// The workflow tool editor keeps parameter forms as select values; the backend validates them. +const toWorkflowToolParameters = (parameters: WorkflowToolProviderParameter[]) => + parameters.map((parameter) => ({ + ...parameter, + form: parameter.form as WorkflowToolParameterForm, + })) + +// The custom tool editor keeps `schema_type` as a free string until a schema is parsed; +// the backend validates the value. +const toSchemaType = (schemaType: string) => schemaType as ApiToolSchemaType export const fetchCollectionList = () => { - return get('/workspaces/current/tool-providers') + return consoleClient.workspaces.current.toolProviders.get({}).then(selectToolCollections) } export const fetchBuiltInToolList = (collectionName: string) => { - return get(`/workspaces/current/tool-provider/builtin/${collectionName}/tools`) + return toolProvider.builtin.byProvider.tools + .get({ params: { provider: collectionName } }) + .then(selectBuiltinProviderTools) } export const fetchCustomToolList = (collectionName: string) => { - const query = buildProviderQuery(collectionName) - return get(`/workspaces/current/tool-provider/api/tools?${query}`) -} - -export const fetchModelToolList = (collectionName: string) => { - const query = buildProviderQuery(collectionName) - return get(`/workspaces/current/tool-provider/model/tools?${query}`) + return toolProvider.api.tools + .get({ query: { provider: collectionName } }) + .then(selectBuiltinProviderTools) } export const fetchWorkflowToolList = (appID: string) => { - return get(`/workspaces/current/tool-provider/workflow/tools?workflow_tool_id=${appID}`) + return toolProvider.workflow.tools + .get({ query: { workflow_tool_id: appID } }) + .then(selectBuiltinProviderTools) } +// Not migrated to the console contract: this legacy built-in credential flow predates +// multi-credential tool auth. The backend now serves the schema at +// `credential/schema/{credential_type}`, returns a credential list from `credentials`, and +// requires `credential_id` for update/delete. Only providers without a plugin reach it, and +// none of the remaining hard-coded built-in providers require credentials. export const fetchBuiltInToolCredentialSchema = (collectionName: string) => { return get( `/workspaces/current/tool-provider/builtin/${collectionName}/credentials_schema`, @@ -44,6 +71,7 @@ export const fetchBuiltInToolCredential = (collectionName: string) => { `/workspaces/current/tool-provider/builtin/${collectionName}/credentials`, ) } + export const updateBuiltInToolCredential = ( collectionName: string, credential: Record, @@ -62,51 +90,38 @@ export const removeBuiltInToolCredential = (collectionName: string) => { } export const parseParamsSchema = (schema: string) => { - return post<{ parameters_schema: CustomParamSchema[]; schema_type: string }>( - '/workspaces/current/tool-provider/api/schema', - { - body: { - schema, - }, - }, - ) + return toolProvider.api.schema.post({ body: { schema } }).then(selectApiSchemaParameters) } export const fetchCustomCollection = (collectionName: string) => { - const query = buildProviderQuery(collectionName) - return get(`/workspaces/current/tool-provider/api/get?${query}`) + return toolProvider.api.get + .get({ query: { provider: collectionName } }) + .then(selectCustomCollection) } export const createCustomCollection = (collection: CustomCollectionBackend) => { - return post('/workspaces/current/tool-provider/api/add', { - body: { - ...collection, - }, + return toolProvider.api.add.post({ + body: { ...collection, schema_type: toSchemaType(collection.schema_type) }, }) } export const updateCustomCollection = (collection: CustomCollectionBackend) => { - return post('/workspaces/current/tool-provider/api/update', { + return toolProvider.api.update.post({ body: { ...collection, + // A collection that was not renamed keeps its current name as the original one. + original_provider: collection.original_provider ?? collection.provider, + schema_type: toSchemaType(collection.schema_type), }, }) } export const removeCustomCollection = (collectionName: string) => { - return post('/workspaces/current/tool-provider/api/delete', { - body: { - provider: collectionName, - }, - }) + return toolProvider.api.delete.post({ body: { provider: collectionName } }) } export const importSchemaFromURL = (url: string) => { - return get<{ schema: string }>('/workspaces/current/tool-provider/api/remote', { - params: { - url, - }, - }) + return toolProvider.api.remote.get({ query: { url } }) } export const testAPIAvailable = (payload: { @@ -117,18 +132,16 @@ export const testAPIAvailable = (payload: { schema: string parameters: Record }) => { - return post('/workspaces/current/tool-provider/api/test/pre', { - body: { - ...payload, - }, + return toolProvider.api.test.pre.post({ + body: { ...payload, schema_type: toSchemaType(payload.schema_type) }, }) } export const createWorkflowToolProvider = ( payload: WorkflowToolProviderRequest & { workflow_app_id: string }, ) => { - return post('/workspaces/current/tool-provider/workflow/create', { - body: { ...payload }, + return toolProvider.workflow.create.post({ + body: { ...payload, parameters: toWorkflowToolParameters(payload.parameters) }, }) } @@ -139,21 +152,23 @@ export const saveWorkflowToolProvider = ( workflow_tool_id: string }>, ) => { - return post('/workspaces/current/tool-provider/workflow/update', { - body: { ...payload }, + const { workflow_tool_id } = payload + if (!workflow_tool_id) throw new Error('workflow_tool_id is required to update a workflow tool') + return toolProvider.workflow.update.post({ + body: { + ...payload, + workflow_tool_id, + parameters: toWorkflowToolParameters(payload.parameters), + }, }) } export const fetchWorkflowToolDetail = (toolID: string) => { - return get( - `/workspaces/current/tool-provider/workflow/get?workflow_tool_id=${toolID}`, - ) + return toolProvider.workflow.get + .get({ query: { workflow_tool_id: toolID } }) + .then(selectWorkflowToolDetail) } export const deleteWorkflowTool = (toolID: string) => { - return post('/workspaces/current/tool-provider/workflow/delete', { - body: { - workflow_tool_id: toolID, - }, - }) + return toolProvider.workflow.delete.post({ body: { workflow_tool_id: toolID } }) } diff --git a/web/service/use-common.ts b/web/service/use-common.ts index 330584efcba97f..b11e6f19d2ebcc 100644 --- a/web/service/use-common.ts +++ b/web/service/use-common.ts @@ -1,152 +1,22 @@ -import type { - ModelParameterRule, - ModelProvider, - ModelTypeEnum, -} from '@/app/components/header/account-setting/model-provider-page/declarations' -import type { AccessControlTemplateLanguage } from '@/i18n/language' -import type { - CodeBasedExtension, - CommonResponse, - FileUploadConfigResponse, - Member, - StructuredOutputRulesRequestBody, - StructuredOutputRulesResponse, -} from '@/models/common' -import { queryOptions, useMutation, useQuery } from '@tanstack/react-query' +import type { SchemaDefinitionItemResponse } from '@dify/contracts/api/console/spec/types.gen' +import type { CommonResponse } from '@/models/common' +import { useMutation, useQuery } from '@tanstack/react-query' import { discardRegistrationSessionState } from '@/app/components/base/amplitude/registration-session-state' import { resetUser } from '@/app/components/base/amplitude/utils' import { clearPageLeaveGuards, confirmPageLeave } from '@/utils/page-leave-guard' import { basePath } from '@/utils/var' // oxlint-disable-next-line no-restricted-imports -import { get, post } from './base' +import { post } from './base' import { consoleQuery } from './console' const NAME_SPACE = 'common' -export const commonQueryKeys = { - fileUploadConfig: [NAME_SPACE, 'file-upload-config'] as const, - members: [NAME_SPACE, 'members'] as const, - filePreview: (fileID: string) => [NAME_SPACE, 'file-preview', fileID] as const, - schemaDefinitions: [NAME_SPACE, 'schema-type-definitions'] as const, - modelProviders: [NAME_SPACE, 'model-providers'] as const, - modelProviderDetails: [NAME_SPACE, 'model-provider-details'] as const, - defaultModel: (type: ModelTypeEnum) => [NAME_SPACE, 'default-model', type] as const, - accountIntegrates: [NAME_SPACE, 'account-integrates'] as const, - notionConnection: [NAME_SPACE, 'notion-connection'] as const, - codeBasedExtensions: (module?: string) => [NAME_SPACE, 'code-based-extensions', module] as const, - invitationCheck: (params?: { workspace_id?: string; email?: string; token?: string }) => - [ - NAME_SPACE, - 'invitation-check', - params?.workspace_id ?? '', - params?.email ?? '', - params?.token ?? '', - ] as const, - notionBinding: (code?: string | null) => [NAME_SPACE, 'notion-binding', code] as const, - modelParameterRules: (provider?: string, model?: string) => - [NAME_SPACE, 'model-parameter-rules', provider, model] as const, - langGeniusVersion: (currentVersion?: string | null) => - [NAME_SPACE, 'langgenius-version', currentVersion] as const, +const commonQueryKeys = { forgotPasswordValidity: (token?: string | null) => [NAME_SPACE, 'forgot-password-validity', token] as const, - dataSourceIntegrates: [NAME_SPACE, 'data-source-integrates'] as const, } -export const useFileUploadConfig = () => { - return useQuery({ - queryKey: commonQueryKeys.fileUploadConfig, - queryFn: () => get('/files/upload'), - }) -} - -export const useGenerateStructuredOutputRules = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'generate-structured-output-rules'], - mutationFn: (body: StructuredOutputRulesRequestBody) => { - return post('/rule-structured-output-generate', { body }) - }, - }) -} - -export type MailSendResponse = { data: string; result: string } -export const useSendMail = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'mail-send'], - mutationFn: (body: { email: string; language: string }) => { - return post('/email-register/send-email', { body }) - }, - }) -} - -export type MailValidityResponse = { is_valid: boolean; token: string } - -export const useMailValidity = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'mail-validity'], - mutationFn: (body: { email: string; code: string; token: string }) => { - return post('/email-register/validity', { body }) - }, - }) -} - -export type MailRegisterResponse = { result: string; data: Record } - -export const useMailRegister = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'mail-register'], - mutationFn: (body: { - token: string - new_password: string - password_confirm: string - language?: string - timezone?: string - }) => { - return post('/email-register', { body }) - }, - }) -} - -type MemberResponse = { - accounts: Member[] | null -} - -export const useMembers = (language?: AccessControlTemplateLanguage) => { - return useQuery({ - queryKey: [...commonQueryKeys.members, language], - queryFn: () => - get('/workspaces/current/members', { - params: { - language, - }, - }), - }) -} - -type FilePreviewResponse = { - content: string -} - -export const useFilePreview = (fileID: string) => { - return useQuery({ - queryKey: commonQueryKeys.filePreview(fileID), - queryFn: () => get(`/files/${fileID}/preview`), - enabled: !!fileID, - }) -} - -export type SchemaTypeDefinition = { - name: string - schema: { - properties: Record - } -} - -export const useSchemaTypeDefinitions = () => { - return useQuery({ - queryKey: commonQueryKeys.schemaDefinitions, - queryFn: () => get('/spec/schema-definitions'), - }) -} +export type SchemaTypeDefinition = SchemaDefinitionItemResponse export const useLogout = ({ redirectTo = '/signin' }: { redirectTo?: string } = {}) => { return useMutation( @@ -179,6 +49,8 @@ export const useLogout = ({ redirectTo = '/signin' }: { redirectTo?: string } = } type ForgotPasswordValidity = CommonResponse & { is_valid: boolean; email: string; token: string } +// Not migrated to the console contract: the endpoint requires `email` and `code`, which the +// token-only reset link flow does not have, so this request cannot succeed as written. export const useVerifyForgotPasswordToken = (token?: string | null) => { return useQuery({ queryKey: commonQueryKeys.forgotPasswordValidity(token), @@ -189,79 +61,3 @@ export const useVerifyForgotPasswordToken = (token?: string | null) => { retry: false, }) } - -type OneMoreStepPayload = { - invitation_code: string - interface_language: string - timezone: string -} -export const useOneMoreStep = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'one-more-step'], - mutationFn: (body: OneMoreStepPayload) => post('/account/init', { body }), - }) -} - -export const modelProviderDetailsQueryOptions = () => - queryOptions({ - queryKey: commonQueryKeys.modelProviderDetails, - queryFn: () => get<{ data: ModelProvider[] }>('/workspaces/current/model-providers'), - }) - -export const useModelProviderDetails = (enabled = true) => { - return useQuery({ - ...modelProviderDetailsQueryOptions(), - enabled, - }) -} - -export const useCodeBasedExtensions = (module: string) => { - return useQuery({ - queryKey: commonQueryKeys.codeBasedExtensions(module), - queryFn: () => get(`/code-based-extension?module=${module}`), - }) -} - -export const useInvitationCheck = ( - params?: { workspace_id?: string; email?: string; token?: string }, - enabled?: boolean, -) => { - return useQuery({ - queryKey: commonQueryKeys.invitationCheck(params), - queryFn: () => - get<{ - is_valid: boolean - data: { - workspace_name: string - email: string - workspace_id: string - account_status?: string - requires_setup?: boolean - } - result: string - }>('/activate/check', { params }), - enabled: enabled ?? !!params?.token, - retry: false, - }) -} - -export const useNotionBinding = (code?: string | null, enabled?: boolean) => { - return useQuery({ - queryKey: commonQueryKeys.notionBinding(code), - queryFn: () => - get<{ result: string }>('/oauth/data-source/binding/notion', { params: { code } }), - enabled: !!code && (enabled ?? true), - }) -} - -export const useModelParameterRules = (provider?: string, model?: string, enabled?: boolean) => { - return useQuery<{ data: ModelParameterRule[] }>({ - queryKey: commonQueryKeys.modelParameterRules(provider, model), - queryFn: () => - get<{ data: ModelParameterRule[] }>( - `/workspaces/current/model-providers/${provider}/models/parameter-rules`, - { params: { model }, silent: true }, - ), - enabled: !!provider && !!model && (enabled ?? true), - }) -} diff --git a/web/service/use-datasource.ts b/web/service/use-datasource.ts deleted file mode 100644 index 2e15fe2e30d785..00000000000000 --- a/web/service/use-datasource.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { - DataSourceAuth, - DataSourceCredential, -} from '@/app/components/header/account-setting/data-source-page-new/types' -import { useMutation, useQuery } from '@tanstack/react-query' -import { get } from './base' -import { useInvalid } from './use-base' - -const NAME_SPACE = 'data-source-auth' - -export const useGetDataSourceListAuth = () => { - return useQuery({ - queryKey: [NAME_SPACE, 'list'], - queryFn: () => get<{ result: DataSourceAuth[] }>('/auth/plugin/datasource/list'), - retry: 0, - }) -} - -export const useInvalidDataSourceListAuth = () => { - return useInvalid([NAME_SPACE, 'list']) -} - -// !This hook is used for fetching the default data source list, which will be legacy and deprecated in the near future. -export const useGetDefaultDataSourceListAuth = () => { - return useQuery({ - queryKey: [NAME_SPACE, 'default-list'], - queryFn: () => get<{ result: DataSourceAuth[] }>('/auth/plugin/datasource/default-list'), - retry: 0, - }) -} - -export const useInvalidDefaultDataSourceListAuth = () => { - return useInvalid([NAME_SPACE, 'default-list']) -} - -export const useGetDataSourceOAuthUrl = (provider: string) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'oauth-url', provider], - mutationFn: (credentialId?: string) => { - return get<{ - authorization_url: string - state: string - context_id: string - }>(`/oauth/plugin/${provider}/datasource/get-authorization-url?credential_id=${credentialId}`) - }, - }) -} - -export const useGetDataSourceAuth = ({ - pluginId, - provider, -}: { - pluginId: string - provider: string -}) => { - return useQuery({ - queryKey: [NAME_SPACE, 'specific-data-source', pluginId, provider], - queryFn: () => - get<{ result: DataSourceCredential[] }>(`/auth/plugin/datasource/${pluginId}/${provider}`), - retry: 0, - }) -} - -export const useInvalidDataSourceAuth = ({ - pluginId, - provider, -}: { - pluginId: string - provider: string -}) => { - return useInvalid([NAME_SPACE, 'specific-data-source', pluginId, provider]) -} diff --git a/web/service/use-log.ts b/web/service/use-log.ts deleted file mode 100644 index 65a201861a8611..00000000000000 --- a/web/service/use-log.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { - ChatConversationFullDetailResponse, - ChatConversationsRequest, - ChatConversationsResponse, - CompletionConversationFullDetailResponse, - CompletionConversationsRequest, - CompletionConversationsResponse, - WorkflowLogsResponse, - WorkflowPausedDetailsResponse, -} from '@/models/log' -import { useQuery } from '@tanstack/react-query' -import { consoleClient } from '@/service/console' -import { get } from './base' - -const NAME_SPACE = 'log' - -// ============ Annotations Count ============ - -export const useAnnotationsCount = (appId: string) => { - return useQuery({ - queryKey: [NAME_SPACE, 'annotations-count', appId], - queryFn: () => - consoleClient.apps.byAppId.annotations.count.get({ - params: { - app_id: appId, - }, - }), - enabled: !!appId, - }) -} - -// ============ Chat Conversations ============ - -type ChatConversationsParams = { - appId: string - params?: Partial -} - -export const useChatConversations = ({ appId, params }: ChatConversationsParams) => { - return useQuery({ - queryKey: [NAME_SPACE, 'chat-conversations', appId, params], - queryFn: () => get(`/apps/${appId}/chat-conversations`, { params }), - enabled: !!appId, - }) -} - -// ============ Completion Conversations ============ - -type CompletionConversationsParams = { - appId: string - params?: Partial -} - -export const useCompletionConversations = ({ appId, params }: CompletionConversationsParams) => { - return useQuery({ - queryKey: [NAME_SPACE, 'completion-conversations', appId, params], - queryFn: () => - get(`/apps/${appId}/completion-conversations`, { params }), - enabled: !!appId, - }) -} - -// ============ Chat Conversation Detail ============ - -export const useChatConversationDetail = (appId?: string, conversationId?: string) => { - return useQuery({ - queryKey: [NAME_SPACE, 'chat-conversation-detail', appId, conversationId], - queryFn: () => - get( - `/apps/${appId}/chat-conversations/${conversationId}`, - ), - enabled: !!appId && !!conversationId, - }) -} - -// ============ Completion Conversation Detail ============ - -export const useCompletionConversationDetail = (appId?: string, conversationId?: string) => { - return useQuery({ - queryKey: [NAME_SPACE, 'completion-conversation-detail', appId, conversationId], - queryFn: () => - get( - `/apps/${appId}/completion-conversations/${conversationId}`, - ), - enabled: !!appId && !!conversationId, - }) -} - -// ============ Workflow Logs ============ - -type WorkflowLogsParams = { - appId: string - params?: Record -} - -export const useWorkflowLogs = ({ appId, params }: WorkflowLogsParams) => { - return useQuery({ - queryKey: [NAME_SPACE, 'workflow-logs', appId, params], - queryFn: () => get(`/apps/${appId}/workflow-app-logs`, { params }), - enabled: !!appId, - }) -} - -// ============ Workflow Pause Details ============ - -type WorkflowPausedDetailsParams = { - workflowRunId: string - enabled?: boolean -} - -export const useWorkflowPausedDetails = ({ - workflowRunId, - enabled = true, -}: WorkflowPausedDetailsParams) => { - return useQuery({ - queryKey: [NAME_SPACE, 'workflow-paused-details', workflowRunId], - queryFn: () => get(`/workflow/${workflowRunId}/pause-details`), - enabled: enabled && !!workflowRunId, - }) -} diff --git a/web/service/use-models.ts b/web/service/use-models.ts deleted file mode 100644 index 97dd930d4dec1d..00000000000000 --- a/web/service/use-models.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { - ModelCredential, - ModelCredentialPayload, - ModelItem, - ModelLoadBalancingConfig, - ModelTypeEnum, - ProviderCredential, -} from '@/app/components/header/account-setting/model-provider-page/declarations' -import { - useMutation, - useQuery, - // useQueryClient, -} from '@tanstack/react-query' -import { del, get, post, put } from './base' - -const NAME_SPACE = 'models' - -export const useModelProviderModelList = (provider: string) => { - return useQuery({ - queryKey: [NAME_SPACE, 'model-list', provider], - queryFn: () => - get<{ data: ModelItem[] }>(`/workspaces/current/model-providers/${provider}/models`), - }) -} - -export const useGetProviderCredential = ( - enabled: boolean, - provider: string, - credentialId?: string, -) => { - return useQuery({ - enabled, - queryKey: [NAME_SPACE, 'model-list', provider, credentialId], - queryFn: () => - get( - `/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`, - ), - staleTime: 0, - gcTime: 0, - }) -} - -export const useAddProviderCredential = (provider: string) => { - return useMutation({ - mutationFn: (data: ProviderCredential) => - post<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, { - body: data, - }), - }) -} - -export const useEditProviderCredential = (provider: string) => { - return useMutation({ - mutationFn: (data: ProviderCredential) => - put<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, { - body: data, - }), - }) -} - -export const useDeleteProviderCredential = (provider: string) => { - return useMutation({ - mutationFn: (data: { credential_id: string }) => - del<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, { - body: data, - }), - }) -} - -export const useActiveProviderCredential = (provider: string) => { - return useMutation({ - mutationFn: (data: { credential_id: string; model?: string; model_type?: ModelTypeEnum }) => - post<{ result: string }>( - `/workspaces/current/model-providers/${provider}/credentials/switch`, - { - body: data, - }, - ), - }) -} - -export const useGetModelCredential = ( - enabled: boolean, - provider: string, - credentialId?: string, - model?: string, - modelType?: string, - configFrom?: string, -) => { - return useQuery({ - enabled, - queryKey: [NAME_SPACE, 'model-list', provider, model, modelType, credentialId, configFrom], - queryFn: () => - get( - `/workspaces/current/model-providers/${provider}/models/credentials?model=${model}&model_type=${modelType}&config_from=${configFrom}${credentialId ? `&credential_id=${credentialId}` : ''}`, - ), - staleTime: 0, - gcTime: 0, - }) -} - -export const useAddModelCredential = (provider: string) => { - return useMutation({ - mutationFn: (data: ModelCredentialPayload) => - post<{ result: string }>( - `/workspaces/current/model-providers/${provider}/models/credentials`, - { - body: data, - }, - ), - }) -} - -export const useEditModelCredential = (provider: string) => { - return useMutation({ - mutationFn: (data: ModelCredentialPayload) => - put<{ result: string }>( - `/workspaces/current/model-providers/${provider}/models/credentials`, - { - body: data, - }, - ), - }) -} - -export const useDeleteModelCredential = (provider: string) => { - return useMutation({ - mutationFn: (data: { credential_id: string; model: string; model_type: ModelTypeEnum }) => - del<{ result: string }>( - `/workspaces/current/model-providers/${provider}/models/credentials`, - { - params: data, - }, - ), - }) -} - -export const useDeleteModel = (provider: string) => { - return useMutation({ - mutationFn: (data: { model: string; model_type: ModelTypeEnum }) => - del<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, { - params: data, - }), - }) -} - -export const useActiveModelCredential = (provider: string) => { - return useMutation({ - mutationFn: (data: { credential_id: string; model: string; model_type: ModelTypeEnum }) => - post<{ result: string }>( - `/workspaces/current/model-providers/${provider}/models/credentials/switch`, - { - body: data, - }, - ), - }) -} - -export const useUpdateModelLoadBalancingConfig = (provider: string) => { - return useMutation({ - mutationFn: (data: { - config_from: string - model: string - model_type: ModelTypeEnum - load_balancing: ModelLoadBalancingConfig - credential_id?: string - }) => - post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, { - body: data, - }), - }) -} diff --git a/web/service/use-pipeline.ts b/web/service/use-pipeline.ts deleted file mode 100644 index 37490c920231fe..00000000000000 --- a/web/service/use-pipeline.ts +++ /dev/null @@ -1,387 +0,0 @@ -import type { MutationOptions } from '@tanstack/react-query' -import type { IconInfo } from '@/models/datasets' -import type { - ConversionResponse, - DatasourceNodeSingleRunRequest, - DatasourceNodeSingleRunResponse, - DeleteTemplateResponse, - ExportTemplateDSLResponse, - ImportPipelineDSLConfirmResponse, - ImportPipelineDSLRequest, - ImportPipelineDSLResponse, - OnlineDocumentPreviewRequest, - OnlineDocumentPreviewResponse, - PipelineCheckDependenciesResponse, - PipelineExecutionLogRequest, - PipelineExecutionLogResponse, - PipelinePreProcessingParamsRequest, - PipelinePreProcessingParamsResponse, - PipelineProcessingParamsRequest, - PipelineProcessingParamsResponse, - PipelineTemplateByIdRequest, - PipelineTemplateByIdResponse, - PipelineTemplateListParams, - PipelineTemplateListResponse, - PublishedPipelineInfoResponse, - PublishedPipelineRunPreviewResponse, - PublishedPipelineRunRequest, - PublishedPipelineRunResponse, - UpdateTemplateInfoRequest, - UpdateTemplateInfoResponse, -} from '@/models/pipeline' -import { useMutation, useQuery } from '@tanstack/react-query' -import { DatasourceType } from '@/models/pipeline' -import { del, get, patch, post } from './base' -import { useInvalid } from './use-base' - -const NAME_SPACE = 'pipeline' - -const PipelineTemplateListQueryKeyPrefix = [NAME_SPACE, 'template-list'] -export const usePipelineTemplateList = (params: PipelineTemplateListParams, enabled = true) => { - return useQuery({ - queryKey: [...PipelineTemplateListQueryKeyPrefix, params], - queryFn: () => { - return get('/rag/pipeline/templates', { params }) - }, - enabled, - }) -} - -export const useInvalidCustomizedTemplateList = () => { - return useInvalid([...PipelineTemplateListQueryKeyPrefix, 'customized']) -} - -export const usePipelineTemplateById = (params: PipelineTemplateByIdRequest, enabled: boolean) => { - const { template_id, type } = params - return useQuery({ - queryKey: [NAME_SPACE, 'template', type, template_id], - queryFn: () => { - return get(`/rag/pipeline/templates/${template_id}`, { - params: { - type, - }, - }) - }, - enabled, - staleTime: 0, - }) -} - -export const useUpdateTemplateInfo = ( - mutationOptions: MutationOptions< - UpdateTemplateInfoResponse, - Error, - UpdateTemplateInfoRequest - > = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'template-update'], - mutationFn: (request: UpdateTemplateInfoRequest) => { - const { template_id, ...rest } = request - return patch( - `/rag/pipeline/customized/templates/${template_id}`, - { - body: rest, - }, - ) - }, - ...mutationOptions, - }) -} - -export const useDeleteTemplate = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'template-delete'], - mutationFn: (templateId: string) => { - return del(`/rag/pipeline/customized/templates/${templateId}`) - }, - ...mutationOptions, - }) -} - -export const useExportTemplateDSL = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'template-dsl-export'], - mutationFn: (templateId: string) => { - return post(`/rag/pipeline/customized/templates/${templateId}`) - }, - ...mutationOptions, - }) -} - -export const useImportPipelineDSL = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'dsl-import'], - mutationFn: (request: ImportPipelineDSLRequest) => { - return post( - '/rag/pipelines/imports', - { body: request }, - { silent: true }, - ) - }, - ...mutationOptions, - }) -} - -export const useImportPipelineDSLConfirm = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'dsl-import-confirm'], - mutationFn: (importId: string) => { - return post( - `/rag/pipelines/imports/${importId}/confirm`, - {}, - { silent: true }, - ) - }, - ...mutationOptions, - }) -} - -export const useCheckPipelineDependencies = ( - mutationOptions: MutationOptions = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'check-dependencies'], - mutationFn: (pipelineId: string) => { - return get( - `/rag/pipelines/imports/${pipelineId}/check-dependencies`, - ) - }, - ...mutationOptions, - }) -} - -export const useDraftPipelineProcessingParams = ( - params: PipelineProcessingParamsRequest, - enabled = true, -) => { - const { pipeline_id, node_id } = params - return useQuery({ - queryKey: [NAME_SPACE, 'draft-pipeline-processing-params', pipeline_id, node_id], - queryFn: () => { - return get( - `/rag/pipelines/${pipeline_id}/workflows/draft/processing/parameters`, - { - params: { - node_id, - }, - }, - ) - }, - staleTime: 0, - enabled, - }) -} - -export const usePublishedPipelineProcessingParams = (params: PipelineProcessingParamsRequest) => { - const { pipeline_id, node_id } = params - return useQuery({ - queryKey: [NAME_SPACE, 'published-pipeline-processing-params', pipeline_id, node_id], - queryFn: () => { - return get( - `/rag/pipelines/${pipeline_id}/workflows/published/processing/parameters`, - { - params: { - node_id, - }, - }, - ) - }, - staleTime: 0, - }) -} - -export const publishedPipelineInfoQueryKeyPrefix = [NAME_SPACE, 'published-pipeline'] - -export const usePublishedPipelineInfo = (pipelineId: string) => { - return useQuery({ - queryKey: [...publishedPipelineInfoQueryKeyPrefix, pipelineId], - queryFn: () => { - return get( - `/rag/pipelines/${pipelineId}/workflows/publish`, - ) - }, - enabled: !!pipelineId, - }) -} - -export const useRunPublishedPipeline = ( - mutationOptions: MutationOptions< - PublishedPipelineRunPreviewResponse | PublishedPipelineRunResponse, - Error, - PublishedPipelineRunRequest - > = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'run-published-pipeline'], - mutationFn: (request: PublishedPipelineRunRequest) => { - const { pipeline_id: pipelineId, is_preview, ...rest } = request - return post( - `/rag/pipelines/${pipelineId}/workflows/published/run`, - { - body: { - ...rest, - is_preview, - response_mode: 'blocking', - }, - }, - ) - }, - ...mutationOptions, - }) -} - -export const useDraftPipelinePreProcessingParams = ( - params: PipelinePreProcessingParamsRequest, - enabled = true, -) => { - const { pipeline_id, node_id } = params - return useQuery({ - queryKey: [NAME_SPACE, 'draft-pipeline-pre-processing-params', pipeline_id, node_id], - queryFn: () => { - return get( - `/rag/pipelines/${pipeline_id}/workflows/draft/pre-processing/parameters`, - { - params: { - node_id, - }, - }, - ) - }, - staleTime: 0, - enabled, - }) -} - -export const usePublishedPipelinePreProcessingParams = ( - params: PipelinePreProcessingParamsRequest, - enabled = true, -) => { - const { pipeline_id, node_id } = params - return useQuery({ - queryKey: [NAME_SPACE, 'published-pipeline-pre-processing-params', pipeline_id, node_id], - queryFn: () => { - return get( - `/rag/pipelines/${pipeline_id}/workflows/published/pre-processing/parameters`, - { - params: { - node_id, - }, - }, - ) - }, - staleTime: 0, - enabled, - }) -} - -export const useExportPipelineDSL = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'export-pipeline-dsl'], - mutationFn: ({ pipelineId, include = false }: { pipelineId: string; include?: boolean }) => { - return get( - `/rag/pipelines/${pipelineId}/exports?include_secret=${include}`, - ) - }, - }) -} - -export const usePublishAsCustomizedPipeline = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'publish-as-customized-pipeline'], - mutationFn: ({ - pipelineId, - name, - icon_info, - description, - }: { - pipelineId: string - name: string - icon_info: IconInfo - description?: string - }) => { - return post(`/rag/pipelines/${pipelineId}/customized/publish`, { - body: { - name, - icon_info, - description, - }, - }) - }, - }) -} - -export const usePipelineExecutionLog = (params: PipelineExecutionLogRequest) => { - const { dataset_id, document_id } = params - return useQuery({ - queryKey: [NAME_SPACE, 'pipeline-execution-log', dataset_id, document_id], - queryFn: () => { - return get( - `/datasets/${dataset_id}/documents/${document_id}/pipeline-execution-log`, - ) - }, - staleTime: 0, - }) -} - -export const usePreviewOnlineDocument = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'preview-online-document'], - mutationFn: (params: OnlineDocumentPreviewRequest) => { - const { pipelineId, datasourceNodeId, workspaceID, pageID, pageType, credentialId } = params - return post( - `/rag/pipelines/${pipelineId}/workflows/published/datasource/nodes/${datasourceNodeId}/preview`, - { - body: { - datasource_type: DatasourceType.onlineDocument, - credential_id: credentialId, - inputs: { - workspace_id: workspaceID, - page_id: pageID, - type: pageType, - }, - }, - }, - ) - }, - }) -} - -export const useConvertDatasetToPipeline = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'convert-dataset-to-pipeline'], - mutationFn: (datasetId: string) => { - return post(`/rag/pipelines/transform/datasets/${datasetId}`) - }, - }) -} - -export const useDatasourceSingleRun = ( - mutationOptions: MutationOptions< - DatasourceNodeSingleRunResponse, - Error, - DatasourceNodeSingleRunRequest - > = {}, -) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'datasource-node-single-run'], - mutationFn: (params: DatasourceNodeSingleRunRequest) => { - const { pipeline_id: pipelineId, ...rest } = params - return post( - `/rag/pipelines/${pipelineId}/workflows/draft/datasource/variables-inspect`, - { - body: rest, - }, - ) - }, - ...mutationOptions, - }) -} diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 2847fbbcea919e..a31725a7007b2d 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -1,10 +1,12 @@ import type { GetWorkspacesCurrentPluginByCategoryListData, + ParserMarketplaceUpgrade, PluginCategoryInstalledPluginResponse, PluginCategoryListResponse, PluginEntity, PluginInstallationItemResponse, PluginListResponse, + PluginParameterOption, } from '@dify/contracts/api/console/workspaces/types.gen' import type { PluginInfoFromMarketPlace, @@ -15,7 +17,6 @@ import type { InfiniteData, MutateOptions, QueryClient, QueryOptions } from '@ta import type { FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { AutoUpdateConfig } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types' import type { - DebugInfo as DebugInfoTypes, Dependency, GitHubItemAndMarketPlaceDependency, InstallPackageResponse, @@ -29,7 +30,6 @@ import type { PluginTask, PluginTaskStart, ReferenceSetting, - uploadGitHubResponse, VersionInfo, VersionListResponse, } from '@/app/components/plugins/types' @@ -48,12 +48,11 @@ import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/ import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' import { PluginCategoryEnum, PluginSource, TaskStatus } from '@/app/components/plugins/types' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { consoleQuery } from '@/service/console' +import { consoleClient, consoleQuery } from '@/service/console' import { uninstallPlugin } from '@/service/plugins' import { hasPermission } from '@/utils/permission' // oxlint-disable-next-line no-restricted-imports -import { get, getMarketplace, post, postMarketplace } from './base' -import { useInvalidateAllBuiltInTools } from './use-tools' +import { getMarketplace, postMarketplace } from './base' const NAME_SPACE = 'plugins' const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList'] @@ -483,11 +482,12 @@ export const normalizeInstalledPluginDetail = (plugin: InstalledPluginResponse): const isUnfinishedPluginTask = (task: PluginTask) => task.status === TaskStatus.pending || task.status === TaskStatus.running +// Each plugin status records its task so a single status can be cleared. const normalizeStartedPluginTask = (task: PluginTaskStart): PluginTask => ({ ...task, plugins: task.plugins.map((plugin) => ({ ...plugin, - taskId: plugin.taskId || task.id, + taskId: task.id, })), }) @@ -759,7 +759,10 @@ export const useRemoveFilteredInstalledPluginPageOnUnmount = ( export const useInvalidateInstalledPluginList = () => { const queryClient = useQueryClient() - const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools() + const invalidateAllBuiltInTools = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }) return (category?: PluginCategoryEnum) => { if (category) { const invalidateCategoryPluginList = queryClient.invalidateQueries({ @@ -809,7 +812,7 @@ export const useInstallPackageFromMarketPlace = ( return useMutation({ ...options, mutationFn: (uniqueIdentifier: string) => { - return post('/workspaces/current/plugin/install/marketplace', { + return consoleClient.workspaces.current.plugin.install.marketplace.post({ body: { plugin_unique_identifiers: [uniqueIdentifier] }, }) }, @@ -821,13 +824,13 @@ export const useInstallPackageFromMarketPlace = ( } export const useUpdatePackageFromMarketPlace = ( - options?: MutateOptions, + options?: MutateOptions, ) => { const queryClient = useQueryClient() return useMutation({ ...options, - mutationFn: (body: object) => { - return post('/workspaces/current/plugin/upgrade/marketplace', { + mutationFn: (body: ParserMarketplaceUpgrade) => { + return consoleClient.workspaces.current.plugin.upgrade.marketplace.post({ body, }) }, @@ -842,8 +845,8 @@ export const usePluginDeclarationFromMarketPlace = (pluginUniqueIdentifier: stri return useQuery({ queryKey: [NAME_SPACE, 'pluginDeclaration', pluginUniqueIdentifier], queryFn: () => - get<{ manifest: PluginDeclaration }>('/workspaces/current/plugin/marketplace/pkg', { - params: { plugin_unique_identifier: pluginUniqueIdentifier }, + consoleClient.workspaces.current.plugin.marketplace.pkg.get({ + query: { plugin_unique_identifier: pluginUniqueIdentifier }, }), enabled: !!pluginUniqueIdentifier, }) @@ -864,7 +867,7 @@ export const useInstallPackageFromLocal = () => { const queryClient = useQueryClient() return useMutation({ mutationFn: (uniqueIdentifier: string) => { - return post('/workspaces/current/plugin/install/pkg', { + return consoleClient.workspaces.current.plugin.install.pkg.post({ body: { plugin_unique_identifiers: [uniqueIdentifier] }, }) }, @@ -888,7 +891,7 @@ export const useInstallPackageFromGitHub = () => { selectedPackage: string uniqueIdentifier: string }) => { - return post('/workspaces/current/plugin/install/github', { + return consoleClient.workspaces.current.plugin.install.github.post({ body: { repo: repoUrl, version: selectedVersion, @@ -907,7 +910,7 @@ export const useUploadGitHub = (payload: { repo: string; version: string; packag return useQuery({ queryKey: [NAME_SPACE, 'uploadGitHub', payload], queryFn: () => - post('/workspaces/current/plugin/upload/github', { + consoleClient.workspaces.current.plugin.upload.github.post({ body: payload, }), retry: 0, @@ -945,16 +948,14 @@ export const useInstallOrUpdate = ({ // From local bundle don't have data.value.github_plugin_unique_identifier uniqueIdentifier = data.value.github_plugin_unique_identifier! if (!uniqueIdentifier) { - const { unique_identifier } = await post( - '/workspaces/current/plugin/upload/github', - { + const { unique_identifier } = + await consoleClient.workspaces.current.plugin.upload.github.post({ body: { repo: data.value.repo!, version: data.value.release! || data.value.version!, package: data.value.packages! || data.value.package!, }, - }, - ) + }) uniqueIdentifier = data.value.github_plugin_unique_identifier! || unique_identifier // has the same version, but not installed if (uniqueIdentifier === installedPayload?.uniqueIdentifier) { @@ -966,17 +967,14 @@ export const useInstallOrUpdate = ({ } } if (!isInstalled) { - const response = await post( - '/workspaces/current/plugin/install/github', - { - body: { - repo: data.value.repo!, - version: data.value.release! || data.value.version!, - package: data.value.packages! || data.value.package!, - plugin_unique_identifier: uniqueIdentifier, - }, + const response = await consoleClient.workspaces.current.plugin.install.github.post({ + body: { + repo: data.value.repo!, + version: data.value.release! || data.value.version!, + package: data.value.packages! || data.value.package!, + plugin_unique_identifier: uniqueIdentifier, }, - ) + }) upsertStartedPluginTask(queryClient, response) const { task_id, all_installed } = response taskId = task_id @@ -997,14 +995,12 @@ export const useInstallOrUpdate = ({ } } if (!isInstalled) { - const response = await post( - '/workspaces/current/plugin/install/marketplace', - { + const response = + await consoleClient.workspaces.current.plugin.install.marketplace.post({ body: { plugin_unique_identifiers: [uniqueIdentifier], }, - }, - ) + }) upsertStartedPluginTask(queryClient, response) const { task_id, all_installed } = response taskId = task_id @@ -1022,14 +1018,11 @@ export const useInstallOrUpdate = ({ } } if (!isInstalled) { - const response = await post( - '/workspaces/current/plugin/install/pkg', - { - body: { - plugin_unique_identifiers: [uniqueIdentifier], - }, + const response = await consoleClient.workspaces.current.plugin.install.pkg.post({ + body: { + plugin_unique_identifiers: [uniqueIdentifier], }, - ) + }) upsertStartedPluginTask(queryClient, response) const { task_id, all_installed } = response taskId = task_id @@ -1039,14 +1032,11 @@ export const useInstallOrUpdate = ({ if (isInstalled) { if (item.type === 'package') { await uninstallPlugin(installedPayload.installedId, { preserveCredentials: true }) - const response = await post( - '/workspaces/current/plugin/install/pkg', - { - body: { - plugin_unique_identifiers: [uniqueIdentifier], - }, + const response = await consoleClient.workspaces.current.plugin.install.pkg.post({ + body: { + plugin_unique_identifiers: [uniqueIdentifier], }, - ) + }) upsertStartedPluginTask(queryClient, response) const { task_id, all_installed } = response taskId = task_id @@ -1087,7 +1077,7 @@ export const useInstallOrUpdate = ({ export const useDebugKey = () => { return useQuery({ queryKey: [NAME_SPACE, 'debugKey'], - queryFn: () => get('/workspaces/current/plugin/debugging-key'), + queryFn: () => consoleClient.workspaces.current.plugin.debuggingKey.get(), }) } @@ -1151,7 +1141,7 @@ const areAutoUpgradeSettingsEqual = ( export const usePluginPermissionSettings = () => { return useQuery({ queryKey: usePluginPermissionSettingsKey, - queryFn: () => get('/workspaces/current/plugin/permission/fetch'), + queryFn: () => consoleClient.workspaces.current.plugin.permission.fetch.get(), }) } @@ -1159,9 +1149,7 @@ export const usePluginAutoUpgradeSettings = (category: PluginCategoryEnum) => { return useQuery({ queryKey: pluginAutoUpgradeSettingsQueryKey(category), queryFn: () => - get('/workspaces/current/plugin/auto-upgrade/fetch', { - params: { category }, - }), + consoleClient.workspaces.current.plugin.autoUpgrade.fetch.get({ query: { category } }), staleTime: 60 * 1000, }) } @@ -1184,7 +1172,7 @@ export const useMutationPluginPermissionSettings = ({ return useMutation({ mutationFn: (payload: Permissions) => { - return post('/workspaces/current/plugin/permission/change', { body: payload }) + return consoleClient.workspaces.current.plugin.permission.change.post({ body: payload }) }, onMutate: async (payload) => { await queryClient.cancelQueries({ queryKey: usePluginPermissionSettingsKey }) @@ -1219,7 +1207,7 @@ export const useMutationPluginAutoUpgradeSettings = ({ return useMutation({ mutationFn: (payload: AutoUpdateConfig) => { - return post('/workspaces/current/plugin/auto-upgrade/change', { + return consoleClient.workspaces.current.plugin.autoUpgrade.change.post({ body: { category, auto_upgrade: payload, @@ -1270,14 +1258,16 @@ export const useMutationReferenceSettings = ({ if (!arePermissionsEqual(payload.permission, currentReferenceSetting?.permission)) mutations.push( - post('/workspaces/current/plugin/permission/change', { body: payload.permission }), + consoleClient.workspaces.current.plugin.permission.change.post({ + body: payload.permission, + }), ) if ( !areAutoUpgradeSettingsEqual(payload.auto_upgrade, currentReferenceSetting?.auto_upgrade) ) { mutations.push( - post('/workspaces/current/plugin/auto-upgrade/change', { + consoleClient.workspaces.current.plugin.autoUpgrade.change.post({ body: { category, auto_upgrade: payload.auto_upgrade, @@ -1358,7 +1348,7 @@ export const useRemoveAutoUpgrade = () => { return useMutation({ mutationFn: (payload: { plugin_id: string; category: PluginCategoryEnum }) => { - return post('/workspaces/current/plugin/auto-upgrade/exclude', { body: payload }) + return consoleClient.workspaces.current.plugin.autoUpgrade.exclude.post({ body: payload }) }, onSuccess: (_data, payload) => { queryClient.invalidateQueries({ @@ -1414,7 +1404,9 @@ export const usePluginTaskList = (category?: PluginCategoryEnum | string) => { enabled: canManagement, queryKey: usePluginTaskListKey, queryFn: () => - get<{ tasks: PluginTask[] }>('/workspaces/current/plugin/tasks?page=1&page_size=100'), + consoleClient.workspaces.current.plugin.tasks + .get({ query: { page: 1, page_size: 100 } }) + .then(({ tasks }) => ({ tasks: tasks.map(normalizeStartedPluginTask) })), structuralSharing: (previousData, nextData) => preserveLocalUnfinishedPluginTasks( previousData as PluginTaskListResponse | undefined, @@ -1478,10 +1470,9 @@ export const usePluginTaskList = (category?: PluginCategoryEnum | string) => { export const useMutationClearTaskPlugin = () => { return useMutation({ mutationFn: ({ taskId, pluginId }: { taskId: string; pluginId: string }) => { - const encodedPluginId = encodeURIComponent(pluginId) - return post<{ success: boolean }>( - `/workspaces/current/plugin/tasks/${taskId}/delete/${encodedPluginId}`, - ) + return consoleClient.workspaces.current.plugin.tasks.byTaskId.delete.byIdentifier.post({ + params: { task_id: taskId, identifier: pluginId }, + }) }, }) } @@ -1498,25 +1489,42 @@ export const usePluginManifestInfo = (pluginUID: string) => { }) } +// Untranslated labels fall back to en_US, matching renderI18nObject. Dynamic options have no +// display conditions. +const toDynamicOption = ({ value, label, icon }: PluginParameterOption): FormOption => ({ + value, + label: { + en_US: label.en_US, + zh_Hans: label.zh_Hans ?? label.en_US, + ...(label.ja_JP != null && { ja_JP: label.ja_JP }), + ...(label.pt_BR != null && { pt_BR: label.pt_BR }), + }, + icon: icon ?? undefined, + show_on: [], +}) + export const useFetchDynamicOptions = ( plugin_id: string, provider: string, action: string, parameter: string, - provider_type?: 'tool' | 'trigger', + // The backend requires a provider type; callers without one render tool options. + provider_type: 'tool' | 'trigger' = 'tool', extra?: Record, ) => { return useMutation({ mutationFn: () => - get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', { - params: { - plugin_id, - provider, - action, - parameter, - provider_type, - ...extra, - }, - }), + consoleClient.workspaces.current.plugin.parameters.dynamicOptions + .get({ + query: { + plugin_id, + provider, + action, + parameter, + provider_type, + ...extra, + }, + }) + .then(({ options }) => ({ options: options.map(toDynamicOption) })), }) } diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts deleted file mode 100644 index 002cbc9b2644c1..00000000000000 --- a/web/service/use-tools.ts +++ /dev/null @@ -1,357 +0,0 @@ -import type { QueryKey, UseQueryOptions } from '@tanstack/react-query' -import type { - Collection, - CollectionProviderType, - MCPServerDetail, - Tool, - WorkflowToolProviderResponse, -} from '@/app/components/tools/types' -import type { RAGRecommendedPlugins, ToolWithProvider } from '@/app/components/workflow/types' -import type { AppIconType } from '@/types/app' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { CollectionType } from '@/app/components/tools/types' -import { consoleClient } from '@/service/console' -import { del, get, post, put } from './base' -import { useInvalid } from './use-base' - -const NAME_SPACE = 'tools' - -const useAllToolProvidersKey = [NAME_SPACE, 'allToolProviders'] -type ToolProviderListType = 'builtin' | 'model' | 'api' | 'workflow' | 'mcp' - -export const useAllToolProviders = (enabled = true, type?: ToolProviderListType) => { - return useQuery({ - queryKey: type ? [...useAllToolProvidersKey, type] : useAllToolProvidersKey, - queryFn: () => - type - ? get('/workspaces/current/tool-providers', { params: { type } }) - : get('/workspaces/current/tool-providers'), - enabled, - }) -} - -export const useInvalidateAllToolProviders = () => { - return useInvalid(useAllToolProvidersKey) -} - -const useAllBuiltInToolsKey = [NAME_SPACE, 'builtIn'] -export const useAllBuiltInTools = (enabled = true) => { - return useQuery({ - queryKey: useAllBuiltInToolsKey, - queryFn: () => get('/workspaces/current/tools/builtin'), - enabled, - }) -} - -export const useInvalidateAllBuiltInTools = () => { - return useInvalid(useAllBuiltInToolsKey) -} - -const useAllCustomToolsKey = [NAME_SPACE, 'customTools'] -export const useAllCustomTools = (enabled = true) => { - return useQuery({ - queryKey: useAllCustomToolsKey, - queryFn: () => get('/workspaces/current/tools/api'), - enabled, - }) -} - -export const useInvalidateAllCustomTools = () => { - return useInvalid(useAllCustomToolsKey) -} - -const useAllWorkflowToolsKey = [NAME_SPACE, 'workflowTools'] -export const useAllWorkflowTools = (enabled = true) => { - return useQuery({ - queryKey: useAllWorkflowToolsKey, - queryFn: () => get('/workspaces/current/tools/workflow'), - enabled, - }) -} - -export const useInvalidateAllWorkflowTools = () => { - return useInvalid(useAllWorkflowToolsKey) -} - -const useAllMCPToolsKey = [NAME_SPACE, 'MCPTools'] -export const useAllMCPTools = (enabled = true) => { - return useQuery({ - queryKey: useAllMCPToolsKey, - queryFn: () => get('/workspaces/current/tools/mcp'), - enabled, - }) -} - -export const useInvalidateAllMCPTools = () => { - return useInvalid(useAllMCPToolsKey) -} - -const useInvalidToolsKeyMap: Partial> = { - [CollectionType.builtIn]: useAllBuiltInToolsKey, - [CollectionType.custom]: useAllCustomToolsKey, - [CollectionType.workflow]: useAllWorkflowToolsKey, - [CollectionType.mcp]: useAllMCPToolsKey, -} -export const useInvalidToolsByType = (type?: CollectionProviderType) => { - const queryKey = type ? useInvalidToolsKeyMap[type] : undefined - return useInvalid(queryKey) -} - -export const useCreateMCP = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'create-mcp'], - mutationFn: (payload: { - name: string - server_url: string - icon_type: AppIconType - icon: string - icon_background?: string | null - timeout?: number - sse_read_timeout?: number - headers?: Record - identity_mode?: 'off' | 'idp_token' - }) => { - return post('workspaces/current/tool-provider/mcp', { - body: { - ...payload, - }, - }) - }, - }) -} - -export const useUpdateMCP = ({ onSuccess }: { onSuccess?: () => void }) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'update-mcp'], - mutationFn: (payload: { - name: string - server_url: string - icon_type: AppIconType - icon: string - icon_background?: string | null - provider_id: string - timeout?: number - sse_read_timeout?: number - headers?: Record - identity_mode?: 'off' | 'idp_token' - }) => { - return put('workspaces/current/tool-provider/mcp', { - body: { - ...payload, - }, - }) - }, - onSuccess, - }) -} - -export const useDeleteMCP = ({ onSuccess }: { onSuccess?: () => void }) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'delete-mcp'], - mutationFn: (id: string) => { - return del('/workspaces/current/tool-provider/mcp', { - body: { - provider_id: id, - }, - }) - }, - onSuccess, - }) -} - -export const useAuthorizeMCP = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'authorize-mcp'], - mutationFn: (payload: { provider_id: string }) => { - return post<{ result?: string; authorization_url?: string }>( - '/workspaces/current/tool-provider/mcp/auth', - { - body: payload, - }, - ) - }, - }) -} - -export const useMCPTools = (providerID: string) => { - return useQuery({ - enabled: !!providerID, - queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID], - queryFn: () => - get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/tools/${providerID}`), - }) -} -export const useInvalidateMCPTools = () => { - const queryClient = useQueryClient() - return (providerID: string) => { - queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID], - }) - } -} - -export const useUpdateMCPTools = () => { - return useMutation({ - mutationFn: (providerID: string) => - get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/update/${providerID}`), - }) -} - -export const useMCPServerDetail = (appID: string, enabled = true) => { - return useQuery({ - queryKey: [NAME_SPACE, 'MCPServerDetail', appID], - queryFn: () => get(`/apps/${appID}/server`), - enabled, - }) -} - -export const useInvalidateMCPServerDetail = () => { - const queryClient = useQueryClient() - return (appID: string) => { - return queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'MCPServerDetail', appID], - }) - } -} - -export const useCreateMCPServer = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'create-mcp-server'], - mutationFn: (payload: { - appID: string - description?: string - parameters?: Record - }) => { - const { appID, ...rest } = payload - return post(`apps/${appID}/server`, { - body: { - ...rest, - }, - }) - }, - }) -} - -export const useUpdateMCPServer = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'update-mcp-server'], - mutationFn: (payload: { - appID: string - id: string - description?: string - status?: string - parameters?: Record - }) => { - const { appID, ...rest } = payload - return put(`apps/${appID}/server`, { - body: { - ...rest, - }, - }) - }, - }) -} - -export const useRefreshMCPServerCode = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'], - mutationFn: (appID: string) => { - return consoleClient.apps.byAppId.server.refresh.post({ - params: { - app_id: appID, - }, - }) - }, - }) -} - -export const useBuiltinTools = (providerName: string) => { - return useQuery({ - enabled: !!providerName, - queryKey: [NAME_SPACE, 'builtin-provider-tools', providerName], - queryFn: () => get(`/workspaces/current/tool-provider/builtin/${providerName}/tools`), - }) -} - -const useRAGRecommendedPluginListKey = [NAME_SPACE, 'rag-recommended-plugins'] - -export const useRAGRecommendedPlugins = (type: 'tool' | 'datasource' | 'all' = 'all') => { - return useQuery({ - queryKey: [...useRAGRecommendedPluginListKey, type], - queryFn: () => - get('/rag/pipelines/recommended-plugins', { - params: { - type, - }, - }), - }) -} - -export const useInvalidateRAGRecommendedPlugins = () => { - const queryClient = useQueryClient() - return (type: 'tool' | 'datasource' | 'all' = 'all') => { - queryClient.invalidateQueries({ - queryKey: [...useRAGRecommendedPluginListKey, type], - }) - } -} - -// App Triggers API hooks -export type AppTrigger = { - id: string - trigger_type: 'trigger-webhook' | 'trigger-schedule' | 'trigger-plugin' - title: string - node_id: string - provider_name: string - icon: string - status: 'enabled' | 'disabled' | 'unauthorized' - created_at: string - updated_at: string -} - -export const useAppTriggers = ( - appId: string | undefined, - options?: Omit, 'queryKey' | 'queryFn'>, -) => { - return useQuery<{ data: AppTrigger[] }>({ - queryKey: [NAME_SPACE, 'app-triggers', appId], - queryFn: () => get<{ data: AppTrigger[] }>(`/apps/${appId}/triggers`), - enabled: !!appId, - ...options, // Merge additional options while maintaining backward compatibility - }) -} - -export const useInvalidateAppTriggers = () => { - const queryClient = useQueryClient() - return (appId: string) => { - return queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'app-triggers', appId], - }) - } -} - -const workflowToolDetailByAppIDKey = (appId: string) => [ - NAME_SPACE, - 'workflowToolDetailByAppID', - appId, -] - -export const useWorkflowToolDetailByAppID = (appId: string, enabled = true) => { - return useQuery({ - queryKey: workflowToolDetailByAppIDKey(appId), - queryFn: () => - get( - `/workspaces/current/tool-provider/workflow/get?workflow_app_id=${appId}`, - ), - enabled: enabled && !!appId, - }) -} - -export const useInvalidateWorkflowToolDetailByAppID = () => { - const queryClient = useQueryClient() - return (appId: string) => { - queryClient.invalidateQueries({ - queryKey: workflowToolDetailByAppIDKey(appId), - }) - } -} diff --git a/web/service/use-workflow.ts b/web/service/use-workflow.ts index 1a3f0645af1cba..539594101d1f67 100644 --- a/web/service/use-workflow.ts +++ b/web/service/use-workflow.ts @@ -11,7 +11,6 @@ import type { } from '@dify/contracts/enterprise-app-deploy/types.gen' import type { InfiniteData, QueryClient, UseQueryOptions } from '@tanstack/react-query' import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' -import type { CommonResponse } from '@/models/common' import type { FetchWorkflowDraftPageParams, FetchWorkflowDraftPageResponse, @@ -28,6 +27,7 @@ import { useQueryClient, } from '@tanstack/react-query' import { consoleClient, consoleQuery } from '@/service/console' +// oxlint-disable-next-line no-restricted-imports -- run history, config, version and publish hooks take prebuilt flow URLs import { del, get, patch, post } from './base' import { appWorkflowQueryOptions, appWorkflowVersionsInfiniteQueryKey } from './workflow-queries' @@ -571,13 +571,10 @@ export const useTestEmailSender = () => { inputs: Record }) => { const { appID, nodeID, deliveryID, inputs } = data - return post( - `/apps/${appID}/workflows/draft/human-input/nodes/${nodeID}/delivery-test`, + return consoleClient.apps.byAppId.workflows.draft.humanInput.nodes.byNodeId.deliveryTest.post( { - body: { - delivery_method_id: deliveryID, - inputs, - }, + params: { app_id: appID, node_id: nodeID }, + body: { delivery_method_id: deliveryID, inputs }, }, ) }, diff --git a/web/service/use-workspace.ts b/web/service/use-workspace.ts deleted file mode 100644 index 996a7a6f3a92f3..00000000000000 --- a/web/service/use-workspace.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { GetWorkspacesCurrentPermissionResponse } from '@dify/contracts/api/console/workspaces/types.gen' -import { useQuery } from '@tanstack/react-query' -import { get } from './base' - -export function useWorkspacePermissions( - workspaceId: GetWorkspacesCurrentPermissionResponse['workspace_id'], - enabled: boolean, -) { - return useQuery({ - queryKey: ['workspace-permissions', workspaceId], - queryFn: () => get('/workspaces/current/permission'), - enabled: enabled && !!workspaceId, - }) -} diff --git a/web/service/workflow.ts b/web/service/workflow.ts index 3f4691487c2062..692abaa0faa7ef 100644 --- a/web/service/workflow.ts +++ b/web/service/workflow.ts @@ -86,15 +86,19 @@ export const stopWorkflowRun = (url: string) => { } export const fetchNodeDefault = (appId: string, blockType: BlockEnum, query = {}) => { - return get(`apps/${appId}/workflows/default-workflow-block-configs/${blockType}`, { - params: { q: JSON.stringify(query) }, + return consoleClient.apps.byAppId.workflows.defaultWorkflowBlockConfigs.byBlockType.get({ + params: { app_id: appId, block_type: blockType }, + query: { q: JSON.stringify(query) }, }) } export const fetchPipelineNodeDefault = (pipelineId: string, blockType: BlockEnum, query = {}) => { - return get(`rag/pipelines/${pipelineId}/workflows/default-workflow-block-configs/${blockType}`, { - params: { q: JSON.stringify(query) }, - }) + return consoleClient.rag.pipelines.byPipelineId.workflows.defaultWorkflowBlockConfigs.byBlockType.get( + { + params: { pipeline_id: pipelineId, block_type: blockType }, + query: { q: JSON.stringify(query) }, + }, + ) } export const fetchCurrentValueOfConversationVariable = ({ @@ -218,7 +222,10 @@ export const submitHumanInputForm = ( action: string }, ) => { - return post(`/form/human_input/${token}`, { body: data }) + return consoleClient.form.humanInput.byFormToken.post({ + params: { form_token: token }, + body: data, + }) } export const fetchHumanInputNodeStepRunForm = ( diff --git a/web/test/console/model-provider.ts b/web/test/console/model-provider.ts new file mode 100644 index 00000000000000..c0d251a448f5ad --- /dev/null +++ b/web/test/console/model-provider.ts @@ -0,0 +1,25 @@ +import type { ConsoleClient } from '@/service/console' + +type ProviderResponse = Awaited< + ReturnType +>['data'][number] + +type ProviderResponseOverrides = Partial> & { + custom_configuration?: Partial +} + +/** A console model provider list item, as `modelProviders.get` returns it. */ +export const createProviderResponseFixture = ({ + custom_configuration, + ...overrides +}: ProviderResponseOverrides = {}): ProviderResponse => ({ + provider: 'openai', + tenant_id: 'tenant-1', + label: { en_US: 'OpenAI' }, + supported_model_types: ['llm'], + configurate_methods: ['predefined-model'], + preferred_provider_type: 'custom', + custom_configuration: { status: 'active', ...custom_configuration }, + system_configuration: { enabled: false, quota_configurations: [] }, + ...overrides, +}) diff --git a/web/test/console/operation-hooks.ts b/web/test/console/operation-hooks.ts new file mode 100644 index 00000000000000..7dba890c5a485a --- /dev/null +++ b/web/test/console/operation-hooks.ts @@ -0,0 +1,65 @@ +import type * as ReactQuery from '@tanstack/react-query' + +type OperationOptions = { queryKey?: unknown; mutationKey?: unknown } +type OperationHandler = (options: never) => unknown +export type OperationRoutes = Partial> + +// Console operation keys start with ['console', ...path]; routes use the dotted path. +const getOperationPath = (key: unknown) => { + if (!Array.isArray(key) || !Array.isArray(key[0])) return undefined + const [prefix, ...path] = key[0] as string[] + return prefix === 'console' ? path.join('.') : undefined +} + +const route = (routes: OperationRoutes, key: unknown, options: OperationOptions) => { + const path = getOperationPath(key) + const handler = path ? routes[path] : undefined + return handler ? { handled: true, value: handler(options as never) } : { handled: false } +} + +/** + * Builds a `@tanstack/react-query` module mock that answers console operations from + * `routes` and leaves every other query to the real hooks. + * + * vi.mock('@tanstack/react-query', async (importOriginal) => + * (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + * await importOriginal(), + * { 'workspaces.current.members.get': () => mockMembersResult() }, + * )) + */ +export const mockConsoleOperationHooks = (actual: typeof ReactQuery, routes: OperationRoutes) => ({ + ...actual, + useQuery: ((options: OperationOptions) => { + const result = route(routes, options.queryKey, options) + return result.handled ? result.value : actual.useQuery(options as never) + }) as typeof actual.useQuery, + useInfiniteQuery: ((options: OperationOptions) => { + const result = route(routes, options.queryKey, options) + return result.handled ? result.value : actual.useInfiniteQuery(options as never) + }) as typeof actual.useInfiniteQuery, + useMutation: ((options: OperationOptions) => { + const result = route(routes, options.mutationKey, options) + return result.handled ? result.value : actual.useMutation(options as never) + }) as typeof actual.useMutation, +}) + +/** + * Stands in for one console operation inside a fully mocked `@/service/console` + * module, producing the same key shape the real utilities use. + */ +export const consoleOperationStub = (path: string) => { + const queryKey = (options?: { input?: unknown }) => + [['console', ...path.split('.')], { type: 'query', input: options?.input }] as const + return { + key: () => [['console', ...path.split('.')]] as const, + queryKey, + queryOptions: (options: { input?: unknown } = {}) => ({ + ...options, + queryKey: queryKey(options), + }), + mutationOptions: (options: object = {}) => ({ + ...options, + mutationKey: [['console', ...path.split('.')], { type: 'mutation' }], + }), + } +} diff --git a/web/test/fixtures/dataset.ts b/web/test/fixtures/dataset.ts new file mode 100644 index 00000000000000..eebd2977168fcb --- /dev/null +++ b/web/test/fixtures/dataset.ts @@ -0,0 +1,54 @@ +import type { DatasetListItemResponse } from '@dify/contracts/api/console/datasets/types.gen' + +/** A general-purpose, high-quality knowledge base as the console dataset list returns it. */ +export const createDatasetFixture = ( + overrides: Partial = {}, +): DatasetListItemResponse => ({ + id: 'dataset-1', + name: 'Test Dataset', + description: 'Test description', + provider: 'vendor', + permission: 'only_me', + data_source_type: 'upload_file', + indexing_technique: 'high_quality', + keyword_number: 10, + app_count: 0, + document_count: 0, + word_count: 0, + created_by: 'user-1', + author_name: 'Test Author', + created_at: 1700000000, + updated_by: 'user-1', + updated_at: 1700000000, + embedding_model: 'text-embedding-ada-002', + embedding_model_provider: 'openai', + embedding_available: true, + retrieval_model_dict: { + search_method: 'semantic_search', + reranking_enable: false, + reranking_model: { reranking_provider_name: '', reranking_model_name: '' }, + top_k: 3, + score_threshold_enabled: false, + score_threshold: 0.5, + }, + summary_index_setting: {}, + tags: [], + doc_form: 'text_model', + external_knowledge_info: {}, + external_retrieval_model: { top_k: 3, score_threshold: 0.5, score_threshold_enabled: false }, + doc_metadata: [], + built_in_field_enabled: false, + pipeline_id: null, + runtime_mode: 'general', + chunk_structure: null, + icon_info: { icon_type: 'emoji', icon: '📙', icon_background: '#FFF4ED', icon_url: '' }, + is_published: false, + total_documents: 0, + total_available_documents: 0, + enable_api: true, + is_multimodal: false, + permission_keys: [], + maintainer: null, + partial_member_list: [], + ...overrides, +}) diff --git a/web/test/fixtures/file-upload-config.ts b/web/test/fixtures/file-upload-config.ts new file mode 100644 index 00000000000000..add846613e982c --- /dev/null +++ b/web/test/fixtures/file-upload-config.ts @@ -0,0 +1,19 @@ +import type { UploadConfig } from '@dify/contracts/api/console/files/types.gen' + +export const createFileUploadConfigFixture = ( + overrides: Partial = {}, +): UploadConfig => ({ + attachment_image_file_size_limit: 2, + audio_file_size_limit: 50, + batch_count_limit: 5, + file_size_limit: 15, + file_upload_limit: 5, + image_file_batch_limit: 10, + image_file_size_limit: 10, + knowledge_file_size_limit: 15, + single_chunk_attachment_limit: 10, + skill_file_size_limit: 50, + video_file_size_limit: 100, + workflow_file_upload_limit: 10, + ...overrides, +}) diff --git a/web/test/fixtures/hit-testing.ts b/web/test/fixtures/hit-testing.ts new file mode 100644 index 00000000000000..581e18aec9f2ca --- /dev/null +++ b/web/test/fixtures/hit-testing.ts @@ -0,0 +1,60 @@ +import type { + HitTestingRecord, + HitTestingSegment, +} from '@dify/contracts/api/console/datasets/types.gen' + +type HitTestingSegmentOverrides = Partial> & { + document?: Partial +} + +const createHitTestingSegmentFixture = ({ + document, + ...overrides +}: HitTestingSegmentOverrides = {}): HitTestingSegment => ({ + id: 'segment-1', + position: 1, + document_id: 'document-1', + content: 'Original content', + sign_content: '', + answer: '', + word_count: 100, + tokens: 20, + keywords: [], + index_node_id: null, + index_node_hash: 'hash-1', + hit_count: 1, + enabled: true, + disabled_at: null, + disabled_by: null, + status: 'completed', + created_by: 'user-1', + created_at: 1710000000, + indexing_at: null, + completed_at: null, + error: null, + stopped_at: null, + ...overrides, + document: { + id: 'document-1', + name: 'file.pdf', + data_source_type: 'upload_file', + doc_type: 'book', + doc_metadata: null, + ...document, + }, +}) + +export const createHitTestingRecordFixture = ({ + segment, + ...overrides +}: Partial> & { + segment?: HitTestingSegmentOverrides +} = {}): HitTestingRecord => ({ + score: 0.85, + tsne_position: null, + child_chunks: [], + files: [], + summary: null, + ...overrides, + segment: createHitTestingSegmentFixture(segment), +}) diff --git a/web/types/app.ts b/web/types/app.ts index b7b891de520a4c..41aaad5fa745f2 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -36,13 +36,11 @@ export type RETRIEVE_METHOD = | 'semantic_search' | 'full_text_search' | 'hybrid_search' - | 'invertedIndex' | 'keyword_search' export const RETRIEVE_METHOD = { semantic: 'semantic_search' as RETRIEVE_METHOD, fullText: 'full_text_search' as RETRIEVE_METHOD, hybrid: 'hybrid_search' as RETRIEVE_METHOD, - invertedIndex: 'invertedIndex' as RETRIEVE_METHOD, keywordSearch: 'keyword_search' as RETRIEVE_METHOD, } as const @@ -353,7 +351,7 @@ export type RetrievalConfig = { } top_k: number score_threshold_enabled: boolean - score_threshold: number + score_threshold?: number reranking_mode?: RerankingModeEnum weights?: { weight_type: WeightedScoreEnum diff --git a/web/types/dataset.ts b/web/types/dataset.ts index 637c9df706a3b2..b063dd138871a2 100644 --- a/web/types/dataset.ts +++ b/web/types/dataset.ts @@ -6,3 +6,6 @@ export const segmentImportStatus = { } as const export type SegmentImportStatus = (typeof segmentImportStatus)[keyof typeof segmentImportStatus] + +export const isSegmentImportStatus = (value: string): value is SegmentImportStatus => + Object.values(segmentImportStatus).includes(value) diff --git a/web/types/pipeline.tsx b/web/types/pipeline.tsx index 9377868db7d592..afa6a63b3af953 100644 --- a/web/types/pipeline.tsx +++ b/web/types/pipeline.tsx @@ -1,4 +1,4 @@ -import type { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth' +import type { DatasourceCredentialResponse } from '@dify/contracts/api/console/auth/types.gen' export type DataSourceNodeProcessingResponse = { event: 'datasource_processing' @@ -31,11 +31,4 @@ export type DataSourceNodeErrorResponse = { error: string } -export type DataSourceCredential = { - avatar_url?: string - credential: Record - id: string - is_default: boolean - name: string - type: CredentialTypeEnum -} +export type DataSourceCredential = DatasourceCredentialResponse diff --git a/web/utils/completion-params.ts b/web/utils/completion-params.ts index 7e15021c31c0f7..86f7ed1ecf40ee 100644 --- a/web/utils/completion-params.ts +++ b/web/utils/completion-params.ts @@ -2,6 +2,7 @@ import type { FormValue, ModelParameterRule, } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { normalizeModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/utils' export const mergeValidCompletionParams = ( oldParams: FormValue | undefined, @@ -82,10 +83,13 @@ export const fetchModelParameterRulesForModel = async ( provider: string, modelId: string, ): Promise => { - const { fetchModelParameterRules } = await import('@/service/common') - const url = `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` - const { data: parameterRules } = await fetchModelParameterRules(url) - return parameterRules ?? [] + const { consoleClient } = await import('@/service/console') + const { data: parameterRules } = + await consoleClient.workspaces.current.modelProviders.byProvider.models.parameterRules.get({ + params: { provider }, + query: { model: modelId }, + }) + return (parameterRules ?? []).map(normalizeModelParameterRule) } export const fetchAndMergeValidCompletionParams = async (