fix(workflow): keep the editor canvas and status correct after publish

This commit is contained in:
matevip 2026-05-18 17:34:53 +08:00
parent 8cfc78e7b4
commit 4cf991851b
5 changed files with 95 additions and 5 deletions

View File

@ -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<WorkflowEntity> 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);
}

View File

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

View File

@ -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<WorkflowEntity> listByWorkspace(long workspaceId) {
return workflowMapper.selectList(new LambdaQueryWrapper<WorkflowEntity>()
List<WorkflowEntity> rows = workflowMapper.selectList(new LambdaQueryWrapper<WorkflowEntity>()
.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<WorkflowEntity> workflows) {
List<Long> ids = workflows.stream()
.map(WorkflowEntity::getLatestRevisionId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (ids.isEmpty()) {
return;
}
Map<Long, Integer> 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.

View File

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

View File

@ -31,7 +31,7 @@
>
<div class="list-row-name">
{{ wf.name || t('workflows.unnamed') }}
<span v-if="wf.latestRevisionId" class="badge published">{{ t('workflows.publishedBadge', { rev: wf.latestRevisionId }) }}</span>
<span v-if="wf.latestRevisionId" class="badge published">{{ t('workflows.publishedBadge', { rev: wf.latestRevisionNumber ?? '?' }) }}</span>
<span v-else class="badge draft">{{ t('workflows.draftBadge') }}</span>
</div>
<div class="list-row-desc">{{ wf.description || '-' }}</div>
@ -46,6 +46,11 @@
<input v-model="selected.name" class="editor-name" :placeholder="t('workflows.namePlaceholder')" />
<input v-model="selected.description" class="editor-desc" :placeholder="t('workflows.descPlaceholder')" />
<div class="editor-actions">
<span class="badge editor-state" :class="selected.latestRevisionId ? 'published' : 'draft'">
{{ selected.latestRevisionId
? t('workflows.publishedBadge', { rev: selected.latestRevisionNumber ?? '?' })
: t('workflows.draftBadge') }}
</span>
<button class="btn-ghost" :disabled="busy" @click="saveMeta">{{ t('workflows.actions.saveMeta') }}</button>
<button class="btn-ghost" :disabled="busy" @click="saveDraft">{{ t('workflows.actions.saveDraft') }}</button>
<button class="btn-ghost" :disabled="busy" @click="compile">{{ t('workflows.actions.compile') }}</button>
@ -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;