mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +08:00
fix(workflow): keep the editor canvas and status correct after publish
This commit is contained in:
parent
8cfc78e7b4
commit
4cf991851b
@ -57,13 +57,16 @@ public class WorkflowController {
|
|||||||
return R.ok(workflowService.listByWorkspace(workspaceId));
|
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}")
|
@GetMapping("/{id}")
|
||||||
@RequireWorkspaceRole("admin")
|
@RequireWorkspaceRole("admin")
|
||||||
public R<WorkflowEntity> get(@PathVariable long id,
|
public R<WorkflowEntity> get(@PathVariable long id,
|
||||||
@RequestHeader("X-Workspace-Id") long workspaceId) {
|
@RequestHeader("X-Workspace-Id") long workspaceId) {
|
||||||
WorkflowEntity row = workflowService.get(id, workspaceId);
|
WorkflowEntity row = workflowService.get(id, workspaceId);
|
||||||
if (row == null) return R.fail("workflow not found: " + id);
|
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);
|
return R.ok(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,23 @@ public class WorkflowEntity {
|
|||||||
@TableField(value = "latest_revision_id", updateStrategy = FieldStrategy.ALWAYS)
|
@TableField(value = "latest_revision_id", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private Long latestRevisionId;
|
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;
|
private Long createdBy;
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
|||||||
@ -14,6 +14,9 @@ import vip.mate.workflow.repository.WorkflowRevisionMapper;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
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
|
* Workflow CRUD + draft / publish lifecycle. Drafts live inline on the
|
||||||
@ -32,9 +35,34 @@ public class WorkflowService {
|
|||||||
private final WorkflowAclPort aclPort;
|
private final WorkflowAclPort aclPort;
|
||||||
|
|
||||||
public List<WorkflowEntity> listByWorkspace(long workspaceId) {
|
public List<WorkflowEntity> listByWorkspace(long workspaceId) {
|
||||||
return workflowMapper.selectList(new LambdaQueryWrapper<WorkflowEntity>()
|
List<WorkflowEntity> rows = workflowMapper.selectList(new LambdaQueryWrapper<WorkflowEntity>()
|
||||||
.eq(WorkflowEntity::getWorkspaceId, workspaceId)
|
.eq(WorkflowEntity::getWorkspaceId, workspaceId)
|
||||||
.orderByDesc(WorkflowEntity::getUpdateTime));
|
.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;
|
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.
|
* 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.
|
* Used by mutation paths that can fail loudly instead of returning null.
|
||||||
|
|||||||
@ -929,6 +929,12 @@ export interface WorkflowSummary {
|
|||||||
draftJson?: string
|
draftJson?: string
|
||||||
draftUpdatedAt?: string
|
draftUpdatedAt?: string
|
||||||
latestRevisionId?: number
|
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
|
createTime: string
|
||||||
updateTime: string
|
updateTime: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
>
|
>
|
||||||
<div class="list-row-name">
|
<div class="list-row-name">
|
||||||
{{ wf.name || t('workflows.unnamed') }}
|
{{ 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>
|
<span v-else class="badge draft">{{ t('workflows.draftBadge') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-row-desc">{{ wf.description || '-' }}</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.name" class="editor-name" :placeholder="t('workflows.namePlaceholder')" />
|
||||||
<input v-model="selected.description" class="editor-desc" :placeholder="t('workflows.descPlaceholder')" />
|
<input v-model="selected.description" class="editor-desc" :placeholder="t('workflows.descPlaceholder')" />
|
||||||
<div class="editor-actions">
|
<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="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="saveDraft">{{ t('workflows.actions.saveDraft') }}</button>
|
||||||
<button class="btn-ghost" :disabled="busy" @click="compile">{{ t('workflows.actions.compile') }}</button>
|
<button class="btn-ghost" :disabled="busy" @click="compile">{{ t('workflows.actions.compile') }}</button>
|
||||||
@ -566,7 +571,10 @@ async function select(id: number) {
|
|||||||
try {
|
try {
|
||||||
const res = await workflowApi.get(id)
|
const res = await workflowApi.get(id)
|
||||||
selected.value = res.data as unknown as WorkflowSummary
|
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 = []
|
compileErrors.value = []
|
||||||
lastStatus.value = ''
|
lastStatus.value = ''
|
||||||
await reloadRuns()
|
await reloadRuns()
|
||||||
@ -766,8 +774,13 @@ async function onPublishSubmit(payload: { note: string }) {
|
|||||||
await workflowApi.saveDraft(selected.value.id, draftJson.value)
|
await workflowApi.saveDraft(selected.value.id, draftJson.value)
|
||||||
await workflowApi.publish(selected.value.id, payload.note || undefined)
|
await workflowApi.publish(selected.value.id, payload.note || undefined)
|
||||||
publishDialogOpen.value = false
|
publishDialogOpen.value = false
|
||||||
setStatus(t('workflows.status.published'), 'ok')
|
|
||||||
await reload()
|
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) {
|
} catch (e) {
|
||||||
handleCompileError(e)
|
handleCompileError(e)
|
||||||
// Close the publish dialog on failure so the operator sees the
|
// Close the publish dialog on failure so the operator sees the
|
||||||
@ -993,6 +1006,11 @@ watch(workspaceId, async () => {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
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 {
|
.editor-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user