mirror of
https://github.com/langgenius/dify.git
synced 2026-06-25 22:31:10 +08:00
feat: refine snippet siderbar (#37928)
This commit is contained in:
parent
a8d8589e18
commit
ac2de006b5
@ -8,6 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
|
||||
|
||||
from controllers.common.controller_schemas import WorkflowUpdatePayload
|
||||
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
@ -96,6 +97,7 @@ register_schema_models(
|
||||
SnippetLoopNodeRunPayload,
|
||||
SnippetWorkflowListQuery,
|
||||
WorkflowRunQuery,
|
||||
WorkflowUpdatePayload,
|
||||
PublishWorkflowPayload,
|
||||
)
|
||||
register_response_schema_models(
|
||||
@ -411,6 +413,49 @@ class SnippetDraftWorkflowRestoreApi(Resource):
|
||||
}
|
||||
|
||||
|
||||
@console_ns.route("/snippets/<uuid:snippet_id>/workflows/<string:workflow_id>")
|
||||
class SnippetWorkflowByIdApi(Resource):
|
||||
@console_ns.doc("update_snippet_workflow_by_id")
|
||||
@console_ns.doc(description="Update published snippet workflow attributes")
|
||||
@console_ns.doc(params={"snippet_id": "Snippet ID", "workflow_id": "Workflow ID"})
|
||||
@console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__])
|
||||
@console_ns.response(200, "Workflow updated successfully", console_ns.models[SnippetWorkflowResponse.__name__])
|
||||
@console_ns.response(400, "No valid fields to update")
|
||||
@console_ns.response(404, "Workflow not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@get_snippet
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
|
||||
)
|
||||
def patch(self, current_user: Account, snippet: CustomizedSnippet, workflow_id: str):
|
||||
"""Update a published snippet workflow version's display metadata."""
|
||||
payload = WorkflowUpdatePayload.model_validate(console_ns.payload or {})
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
|
||||
if not update_data:
|
||||
return {"message": "No valid fields to update"}, 400
|
||||
|
||||
snippet_service = _snippet_service()
|
||||
with _snippet_session_maker().begin() as session:
|
||||
workflow = snippet_service.update_workflow(
|
||||
session=session,
|
||||
snippet=snippet,
|
||||
workflow_id=workflow_id,
|
||||
account=current_user,
|
||||
data=update_data,
|
||||
)
|
||||
if not workflow:
|
||||
raise NotFound("Workflow not found")
|
||||
|
||||
response = SnippetWorkflowResponse.model_validate(workflow, from_attributes=True).model_dump(mode="json")
|
||||
response["input_fields"] = snippet.input_fields_list
|
||||
return response
|
||||
|
||||
|
||||
@console_ns.route("/snippets/<uuid:snippet_id>/workflow-runs")
|
||||
class SnippetWorkflowRunsApi(Resource):
|
||||
@console_ns.doc("list_snippet_workflow_runs")
|
||||
|
||||
@ -680,6 +680,46 @@ class SnippetService:
|
||||
|
||||
return workflows, has_more
|
||||
|
||||
def update_workflow(
|
||||
self,
|
||||
*,
|
||||
session: Session,
|
||||
snippet: CustomizedSnippet,
|
||||
workflow_id: str,
|
||||
account: Account,
|
||||
data: dict[str, Any],
|
||||
) -> Workflow | None:
|
||||
"""
|
||||
Update a published snippet workflow version's display metadata.
|
||||
|
||||
:param session: Database session
|
||||
:param snippet: CustomizedSnippet instance
|
||||
:param workflow_id: Workflow ID
|
||||
:param account: Account making the change
|
||||
:param data: Dictionary containing fields to update
|
||||
:return: Updated workflow or None if not found
|
||||
"""
|
||||
stmt = select(Workflow).where(
|
||||
Workflow.id == workflow_id,
|
||||
Workflow.tenant_id == snippet.tenant_id,
|
||||
Workflow.app_id == snippet.id,
|
||||
self._snippet_kind_filter(),
|
||||
Workflow.version != Workflow.VERSION_DRAFT,
|
||||
)
|
||||
workflow = session.scalar(stmt)
|
||||
if not workflow:
|
||||
return None
|
||||
|
||||
allowed_fields = {"marked_name", "marked_comment"}
|
||||
for field, value in data.items():
|
||||
if field in allowed_fields:
|
||||
setattr(workflow, field, value)
|
||||
|
||||
workflow.updated_by = account.id
|
||||
workflow.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
session.add(workflow)
|
||||
return workflow
|
||||
|
||||
# --- Default Block Configs ---
|
||||
|
||||
def get_default_block_configs(self) -> list[dict]:
|
||||
|
||||
@ -361,6 +361,119 @@ def test_restore_published_snippet_workflow_to_draft_returns_400_for_invalid_gra
|
||||
assert exc.value.description == "invalid snippet workflow graph"
|
||||
|
||||
|
||||
def test_update_published_snippet_workflow_returns_updated_workflow(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
workflow = SimpleNamespace(
|
||||
id="workflow-1",
|
||||
graph_dict={"nodes": [], "edges": []},
|
||||
features_dict={},
|
||||
unique_hash="hash-1",
|
||||
version="2024-01-01 00:00:00",
|
||||
marked_name="v1",
|
||||
marked_comment="first version",
|
||||
created_by_account=None,
|
||||
created_at=datetime(2024, 1, 1),
|
||||
updated_by_account=None,
|
||||
updated_at=datetime(2024, 1, 1),
|
||||
tool_published=False,
|
||||
environment_variables=[],
|
||||
conversation_variables=[],
|
||||
rag_pipeline_variables=[],
|
||||
)
|
||||
user = _account("account-1")
|
||||
input_fields = [{"variable": "query", "type": "text"}]
|
||||
snippet = _snippet(input_fields=json.dumps(input_fields))
|
||||
session = SimpleNamespace()
|
||||
update_workflow = Mock(return_value=workflow)
|
||||
|
||||
class TransactionContext:
|
||||
def __enter__(self):
|
||||
return session
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
class SessionMaker:
|
||||
def begin(self):
|
||||
return TransactionContext()
|
||||
|
||||
monkeypatch.setattr(snippet_workflow_module, "_snippet_session_maker", Mock(return_value=SessionMaker()))
|
||||
monkeypatch.setattr(
|
||||
snippet_workflow_module,
|
||||
"SnippetService",
|
||||
lambda: SimpleNamespace(update_workflow=update_workflow),
|
||||
)
|
||||
|
||||
api = snippet_workflow_module.SnippetWorkflowByIdApi()
|
||||
handler = unwrap(api.patch)
|
||||
|
||||
with app.test_request_context(
|
||||
"/snippets/snippet-1/workflows/workflow-1",
|
||||
method="PATCH",
|
||||
json={"marked_name": "v1", "marked_comment": "first version"},
|
||||
):
|
||||
response = handler(api, user, snippet, workflow_id="workflow-1")
|
||||
|
||||
update_workflow.assert_called_once_with(
|
||||
session=session,
|
||||
snippet=snippet,
|
||||
workflow_id="workflow-1",
|
||||
account=user,
|
||||
data={"marked_name": "v1", "marked_comment": "first version"},
|
||||
)
|
||||
assert response["marked_name"] == "v1"
|
||||
assert response["marked_comment"] == "first version"
|
||||
assert response["input_fields"] == input_fields
|
||||
|
||||
|
||||
def test_update_published_snippet_workflow_returns_400_when_no_fields(app: Flask) -> None:
|
||||
api = snippet_workflow_module.SnippetWorkflowByIdApi()
|
||||
handler = unwrap(api.patch)
|
||||
|
||||
with app.test_request_context("/snippets/snippet-1/workflows/workflow-1", method="PATCH", json={}):
|
||||
response, status_code = handler(api, _account("account-1"), _snippet(), workflow_id="workflow-1")
|
||||
|
||||
assert status_code == 400
|
||||
assert response == {"message": "No valid fields to update"}
|
||||
|
||||
|
||||
def test_update_published_snippet_workflow_raises_not_found(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
user = _account("account-1")
|
||||
snippet = _snippet()
|
||||
|
||||
class TransactionContext:
|
||||
def __enter__(self):
|
||||
return SimpleNamespace()
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
class SessionMaker:
|
||||
def begin(self):
|
||||
return TransactionContext()
|
||||
|
||||
monkeypatch.setattr(snippet_workflow_module, "_snippet_session_maker", Mock(return_value=SessionMaker()))
|
||||
monkeypatch.setattr(
|
||||
snippet_workflow_module,
|
||||
"SnippetService",
|
||||
lambda: SimpleNamespace(update_workflow=Mock(return_value=None)),
|
||||
)
|
||||
|
||||
api = snippet_workflow_module.SnippetWorkflowByIdApi()
|
||||
handler = unwrap(api.patch)
|
||||
|
||||
with app.test_request_context(
|
||||
"/snippets/snippet-1/workflows/missing-workflow",
|
||||
method="PATCH",
|
||||
json={"marked_name": "v1"},
|
||||
):
|
||||
with pytest.raises(NotFound, match="Workflow not found"):
|
||||
handler(api, user, snippet, workflow_id="missing-workflow")
|
||||
|
||||
|
||||
def test_workflow_run_detail_raises_not_found_when_run_missing(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
snippet = _snippet()
|
||||
monkeypatch.setattr(
|
||||
|
||||
@ -273,6 +273,45 @@ def test_sync_draft_workflow_updates_existing_draft_and_clears_variables(monkeyp
|
||||
session.commit.assert_called_once()
|
||||
|
||||
|
||||
def test_update_workflow_updates_marked_fields() -> None:
|
||||
service = SnippetService.__new__(SnippetService)
|
||||
workflow = SimpleNamespace(marked_name="", marked_comment="", updated_by=None, updated_at=None)
|
||||
session = SimpleNamespace(scalar=Mock(return_value=workflow), add=Mock())
|
||||
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1")
|
||||
account = SimpleNamespace(id="account-1")
|
||||
|
||||
result = service.update_workflow(
|
||||
session=session,
|
||||
snippet=snippet,
|
||||
workflow_id="workflow-1",
|
||||
account=account,
|
||||
data={"marked_name": "v1", "marked_comment": "first version", "ignored": "value"},
|
||||
)
|
||||
|
||||
assert result is workflow
|
||||
assert workflow.marked_name == "v1"
|
||||
assert workflow.marked_comment == "first version"
|
||||
assert workflow.updated_by == "account-1"
|
||||
session.scalar.assert_called_once()
|
||||
session.add.assert_called_once_with(workflow)
|
||||
|
||||
|
||||
def test_update_workflow_returns_none_when_missing() -> None:
|
||||
service = SnippetService.__new__(SnippetService)
|
||||
session = SimpleNamespace(scalar=Mock(return_value=None), add=Mock())
|
||||
|
||||
result = service.update_workflow(
|
||||
session=session,
|
||||
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
|
||||
workflow_id="missing-workflow",
|
||||
account=SimpleNamespace(id="account-1"),
|
||||
data={"marked_name": "v1"},
|
||||
)
|
||||
|
||||
assert result is None
|
||||
session.add.assert_not_called()
|
||||
|
||||
|
||||
def test_get_default_block_configs_skips_empty_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
node_with_default = SimpleNamespace(get_default_config=Mock(return_value={"type": "llm"}))
|
||||
node_without_default = SimpleNamespace(get_default_config=Mock(return_value=None))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user