feat: refine snippet siderbar (#37925)

This commit is contained in:
FFXN 2026-06-25 15:27:39 +08:00 committed by GitHub
parent 3206d7d876
commit 81a07f3fad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 239 additions and 0 deletions

View File

@ -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,50 @@ 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 Session(db.engine) 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")
session.commit()
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")

View File

@ -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]:

View File

@ -361,6 +361,120 @@ 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(commit=Mock())
update_workflow = Mock(return_value=workflow)
class SessionContext:
def __init__(self, engine):
self.engine = engine
def __enter__(self):
return session
def __exit__(self, exc_type, exc, tb):
return False
monkeypatch.setattr(snippet_workflow_module, "Session", SessionContext)
monkeypatch.setattr(snippet_workflow_module, "db", SimpleNamespace(engine=object()))
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"},
)
session.commit.assert_called_once()
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 SessionContext:
def __init__(self, engine):
self.engine = engine
def __enter__(self):
return SimpleNamespace(commit=Mock())
def __exit__(self, exc_type, exc, tb):
return False
monkeypatch.setattr(snippet_workflow_module, "Session", SessionContext)
monkeypatch.setattr(snippet_workflow_module, "db", SimpleNamespace(engine=object()))
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(

View File

@ -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))