diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java b/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java index a445d171..c3ee340f 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java @@ -57,13 +57,16 @@ public class WorkflowController { return R.ok(workflowService.listByWorkspace(workspaceId)); } - @Operation(summary = "Get a workflow by id (includes inline draft).") + @Operation(summary = "Get a workflow by id (includes inline draft + latest published graph).") @GetMapping("/{id}") @RequireWorkspaceRole("admin") public R get(@PathVariable long id, @RequestHeader("X-Workspace-Id") long workspaceId) { WorkflowEntity row = workflowService.get(id, workspaceId); if (row == null) return R.fail("workflow not found: " + id); + // Surface the published revision's graph so the editor can render a + // published workflow whose inline draft was cleared at publish time. + workflowService.attachPublishedGraph(row); return R.ok(row); } diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java index 07300ec8..8313bcc7 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java @@ -47,6 +47,23 @@ public class WorkflowEntity { @TableField(value = "latest_revision_id", updateStrategy = FieldStrategy.ALWAYS) private Long latestRevisionId; + /** + * Latest published revision's graph JSON. Not a persisted column — it is + * populated on the editor-facing read so a published workflow (whose inline + * draft is cleared at publish time) still has a graph for the editor to + * render instead of an empty canvas. + */ + @TableField(exist = false) + private String publishedGraphJson; + + /** + * Human-facing version number of the latest published revision (1, 2, 3…). + * Not a persisted column — populated on read so the UI shows "v3" instead + * of leaking the latestRevisionId snowflake. Null when never published. + */ + @TableField(exist = false) + private Integer latestRevisionNumber; + private Long createdBy; @TableField(fill = FieldFill.INSERT) diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/service/WorkflowService.java b/mateclaw-server/src/main/java/vip/mate/workflow/service/WorkflowService.java index 8fa5592e..b0a2e9cd 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/service/WorkflowService.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/service/WorkflowService.java @@ -14,6 +14,9 @@ import vip.mate.workflow.repository.WorkflowRevisionMapper; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; /** * Workflow CRUD + draft / publish lifecycle. Drafts live inline on the @@ -32,9 +35,34 @@ public class WorkflowService { private final WorkflowAclPort aclPort; public List listByWorkspace(long workspaceId) { - return workflowMapper.selectList(new LambdaQueryWrapper() + List rows = workflowMapper.selectList(new LambdaQueryWrapper() .eq(WorkflowEntity::getWorkspaceId, workspaceId) .orderByDesc(WorkflowEntity::getUpdateTime)); + attachRevisionNumbers(rows); + return rows; + } + + /** + * Batch-populate {@link WorkflowEntity#getLatestRevisionNumber()} so the + * workflow list shows a human version ("v3") instead of the latestRevisionId + * snowflake. One query for the whole page; no-op when nothing is published. + */ + private void attachRevisionNumbers(List workflows) { + List ids = workflows.stream() + .map(WorkflowEntity::getLatestRevisionId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (ids.isEmpty()) { + return; + } + Map numberById = revisionMapper.selectBatchIds(ids).stream() + .collect(Collectors.toMap(WorkflowRevisionEntity::getId, WorkflowRevisionEntity::getRevision)); + for (WorkflowEntity wf : workflows) { + if (wf.getLatestRevisionId() != null) { + wf.setLatestRevisionNumber(numberById.get(wf.getLatestRevisionId())); + } + } } /** @@ -51,6 +79,24 @@ public class WorkflowService { return row; } + /** + * Populate {@link WorkflowEntity#getPublishedGraphJson()} from the latest + * revision. The editor needs this because {@link #publish} clears the inline + * draft on publish — without the published graph the canvas would render + * empty for any published workflow. No-op when the workflow was never + * published or the revision row is missing. + */ + public void attachPublishedGraph(WorkflowEntity workflow) { + if (workflow == null || workflow.getLatestRevisionId() == null) { + return; + } + WorkflowRevisionEntity revision = revisionMapper.selectById(workflow.getLatestRevisionId()); + if (revision != null) { + workflow.setPublishedGraphJson(revision.getGraphJson()); + workflow.setLatestRevisionNumber(revision.getRevision()); + } + } + /** * Same as {@link #get(long, long)} but throws when the row is missing. * Used by mutation paths that can fail loudly instead of returning null. diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index 7c21f813..adf87487 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -929,6 +929,12 @@ export interface WorkflowSummary { draftJson?: string draftUpdatedAt?: string latestRevisionId?: number + /** Human version number of the latest published revision (1, 2, 3…) — shown + * as "v3" instead of the latestRevisionId snowflake. Null when unpublished. */ + latestRevisionNumber?: number + /** Latest published revision's graph JSON — populated by GET /workflows/{id} + * so the editor can render a published workflow whose draft was cleared. */ + publishedGraphJson?: string createTime: string updateTime: string } diff --git a/mateclaw-ui/src/views/Workflows.vue b/mateclaw-ui/src/views/Workflows.vue index d0fafc85..aaaaaf8e 100644 --- a/mateclaw-ui/src/views/Workflows.vue +++ b/mateclaw-ui/src/views/Workflows.vue @@ -31,7 +31,7 @@ >
{{ wf.name || t('workflows.unnamed') }} - {{ t('workflows.publishedBadge', { rev: wf.latestRevisionId }) }} + {{ t('workflows.publishedBadge', { rev: wf.latestRevisionNumber ?? '?' }) }} {{ t('workflows.draftBadge') }}
{{ wf.description || '-' }}
@@ -46,6 +46,11 @@
+ + {{ selected.latestRevisionId + ? t('workflows.publishedBadge', { rev: selected.latestRevisionNumber ?? '?' }) + : t('workflows.draftBadge') }} + @@ -566,7 +571,10 @@ async function select(id: number) { try { const res = await workflowApi.get(id) selected.value = res.data as unknown as WorkflowSummary - draftJson.value = selected.value?.draftJson ?? '' + // Fall back to the latest published graph when the inline draft is empty — + // publishing clears draftJson on the backend, so without this the canvas + // and JSON editor would render empty for an already-published workflow. + draftJson.value = selected.value?.draftJson || selected.value?.publishedGraphJson || '' compileErrors.value = [] lastStatus.value = '' await reloadRuns() @@ -766,8 +774,13 @@ async function onPublishSubmit(payload: { note: string }) { await workflowApi.saveDraft(selected.value.id, draftJson.value) await workflowApi.publish(selected.value.id, payload.note || undefined) publishDialogOpen.value = false - setStatus(t('workflows.status.published'), 'ok') await reload() + // Re-fetch the just-published workflow so the canvas rebinds to the + // published graph — publish cleared draftJson, and select() now falls + // back to publishedGraphJson. setStatus runs last because select() + // resets lastStatus. + await select(selected.value.id) + setStatus(t('workflows.status.published'), 'ok') } catch (e) { handleCompileError(e) // Close the publish dialog on failure so the operator sees the @@ -993,6 +1006,11 @@ watch(workspaceId, async () => { gap: 6px; flex-wrap: wrap; } +/* Persistent published/draft state — sits at the row's left, actions to its right. */ +.editor-state { + align-self: center; + margin-right: auto; +} .editor-toolbar { display: flex; align-items: center;