fix: workflow editor Phase 5/6 sign-off — save guard, conflict detection, dispatch pre-check
- Save button disabled for non-admins (canSave prop threaded through WorkflowEditor → WorkflowCanvas → WorkflowCanvasToolbar); tooltip explains why when hovered - Optimistic concurrency: migration 072 adds updated_at to workflow_definitions; PUT /workflows/:id returns 409 when client sends a stale updated_at; frontend shows a specific reload-prompt toast - _legacy_dispatch now routes through dispatch_order_line_render instead of calling render_order_line_task directly, so cancelled/rejected order lines are skipped before queueing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -705,7 +705,7 @@ def dispatch_render_with_workflow(order_line_id: str) -> dict:
|
||||
|
||||
|
||||
def _legacy_dispatch(order_line_id: str) -> dict:
|
||||
"""Queue render_order_line_task (the working Celery render implementation)."""
|
||||
from app.tasks.step_tasks import render_order_line_task
|
||||
render_order_line_task.delay(order_line_id)
|
||||
"""Queue via dispatch_order_line_render so cancelled/rejected pre-checks apply."""
|
||||
from app.tasks.step_tasks import dispatch_order_line_render
|
||||
dispatch_order_line_render.delay(order_line_id)
|
||||
return {"backend": "celery", "queued": True}
|
||||
|
||||
@@ -166,6 +166,7 @@ class WorkflowDefinition(Base):
|
||||
config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
|
||||
runs: Mapped[list["WorkflowRun"]] = relationship(
|
||||
"WorkflowRun", back_populates="workflow_def", lazy="noload", cascade="all, delete-orphan"
|
||||
|
||||
@@ -193,6 +193,7 @@ class WorkflowDefinitionUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
config: dict | None = None
|
||||
is_active: bool | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class WorkflowDefinitionOut(BaseModel):
|
||||
@@ -207,6 +208,7 @@ class WorkflowDefinitionOut(BaseModel):
|
||||
)
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ async def _workflow_to_out(db: AsyncSession, wf: WorkflowDefinition) -> Workflow
|
||||
),
|
||||
is_active=wf.is_active,
|
||||
created_at=wf.created_at,
|
||||
updated_at=wf.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@@ -938,6 +939,15 @@ async def update_workflow(
|
||||
if not wf:
|
||||
raise HTTPException(status_code=404, detail="Workflow definition not found")
|
||||
|
||||
if body.updated_at is not None:
|
||||
stored_ts = wf.updated_at.replace(tzinfo=None) if wf.updated_at.tzinfo else wf.updated_at
|
||||
client_ts = body.updated_at.replace(tzinfo=None) if body.updated_at.tzinfo else body.updated_at
|
||||
if abs((stored_ts - client_ts).total_seconds()) > 1:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Workflow was modified by someone else. Reload and try again.",
|
||||
)
|
||||
|
||||
if body.name is not None:
|
||||
wf.name = body.name
|
||||
if body.config is not None:
|
||||
|
||||
Reference in New Issue
Block a user