mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(workflow): hard-delete + blank-draft compile + canvas blank pane
This commit is contained in:
parent
f8499eefa7
commit
844a64ad37
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@ -66,6 +65,6 @@ public class TriggerEntity {
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
// Hard-delete only (project convention); column kept for schema compat.
|
||||
private Integer deleted;
|
||||
}
|
||||
|
||||
@ -94,12 +94,26 @@ public class WorkflowController {
|
||||
if (row == null) {
|
||||
return ResponseEntity.badRequest().body(R.fail("workflow not found: " + id));
|
||||
}
|
||||
if (row.getDraftJson() == null) {
|
||||
// The parser throws WorkflowParseException for null/blank/whitespace
|
||||
// input, which would otherwise bubble up to the global handler as a
|
||||
// 500. A blank draft is a normal user state ("just created, nothing
|
||||
// typed yet"), so we surface a friendly 400 here.
|
||||
if (row.getDraftJson() == null || row.getDraftJson().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(R.fail("workflow has no draft to compile: " + id));
|
||||
}
|
||||
WorkflowCompiler.Result result = compiler.compile(row.getDraftJson(),
|
||||
new PublishContext(0L, row.getWorkspaceId()), aclPort);
|
||||
WorkflowCompiler.Result result;
|
||||
try {
|
||||
result = compiler.compile(row.getDraftJson(),
|
||||
new PublishContext(0L, row.getWorkspaceId()), aclPort);
|
||||
} catch (vip.mate.workflow.compiler.WorkflowParseException e) {
|
||||
// Malformed JSON / structurally invalid graph → render as a
|
||||
// single-error compile failure so the UI's existing errors
|
||||
// panel handles it without a stack trace dialog.
|
||||
return ResponseEntity.unprocessableEntity().body(buildCompileFailure(List.of(
|
||||
new vip.mate.workflow.compiler.CompileError(
|
||||
"graph.parse_failed", "/", e.getMessage()))));
|
||||
}
|
||||
if (!result.ok()) {
|
||||
return ResponseEntity.unprocessableEntity()
|
||||
.body(buildCompileFailure(result.errors()));
|
||||
@ -119,6 +133,12 @@ public class WorkflowController {
|
||||
return ResponseEntity.ok(R.ok(outcome));
|
||||
} catch (WorkflowCompileFailedException e) {
|
||||
return ResponseEntity.unprocessableEntity().body(buildCompileFailure(e.errors()));
|
||||
} catch (vip.mate.workflow.compiler.WorkflowParseException e) {
|
||||
// Same surface as a compile error so the UI errors panel
|
||||
// handles a malformed / blank draft without a 500 dialog.
|
||||
return ResponseEntity.unprocessableEntity().body(buildCompileFailure(List.of(
|
||||
new vip.mate.workflow.compiler.CompileError(
|
||||
"graph.parse_failed", "/", e.getMessage()))));
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.fail(e.getMessage()));
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@ -56,6 +55,10 @@ public class WorkflowEntity {
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
// The `deleted` column stays on the table for schema compatibility but
|
||||
// is no longer logical-deleted — see contributing.md, the project moved
|
||||
// to hard-delete project-wide. deleteById() now performs a real DELETE,
|
||||
// and the unique key on (workspace_id, name, deleted) no longer collides
|
||||
// when a name is recreated and re-deleted.
|
||||
private Integer deleted;
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@ -56,6 +55,6 @@ public class WorkflowRunEntity {
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableLogic
|
||||
// Hard-delete only (project convention); column kept for schema compat.
|
||||
private Integer deleted;
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
-- The workflow / trigger entities originally shipped with @TableLogic, which
|
||||
-- caused deleteById() to soft-update `deleted=1`. The project convention is
|
||||
-- hard-delete everywhere (see contributing.md), and the soft-delete path
|
||||
-- collided with the (workspace_id, name, deleted) unique key whenever a name
|
||||
-- was recreated and re-deleted: the second update tried to write a tombstone
|
||||
-- that already existed.
|
||||
--
|
||||
-- The entity annotations are removed in this same change set so deleteById()
|
||||
-- now performs a real DELETE. This migration purges any tombstones that the
|
||||
-- old soft-delete path may have written, because the annotation-driven query
|
||||
-- filter is no longer applied — a stale `deleted=1` row would otherwise show
|
||||
-- up in list endpoints.
|
||||
|
||||
DELETE FROM mate_workflow WHERE deleted <> 0;
|
||||
DELETE FROM mate_workflow_run WHERE deleted <> 0;
|
||||
DELETE FROM mate_trigger WHERE deleted <> 0;
|
||||
@ -0,0 +1,9 @@
|
||||
-- See the matching H2 file for context. The workflow / trigger entities
|
||||
-- moved off @TableLogic to align with the project's hard-delete convention;
|
||||
-- this migration drops any tombstones the old soft-delete path persisted so
|
||||
-- list endpoints don't expose them after the annotation-driven filter is
|
||||
-- removed.
|
||||
|
||||
DELETE FROM mate_workflow WHERE deleted <> 0;
|
||||
DELETE FROM mate_workflow_run WHERE deleted <> 0;
|
||||
DELETE FROM mate_trigger WHERE deleted <> 0;
|
||||
@ -27,12 +27,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!graph.nodes.length && !graph.parseError" class="canvas-empty">
|
||||
{{ t('workflows.canvas.empty') }}
|
||||
<div v-if="graph.parseError" class="canvas-error">
|
||||
<div class="canvas-error-icon">⚠</div>
|
||||
<div class="canvas-error-msg">{{ t('workflows.canvas.parseError', { msg: graph.parseError }) }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!graph.nodes.length" class="canvas-empty">
|
||||
<div class="canvas-empty-illustration">
|
||||
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1.5"/>
|
||||
<rect x="14" y="3" width="7" height="7" rx="1.5"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1.5"/>
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5"/>
|
||||
<path d="M10 6.5h4"/>
|
||||
<path d="M10 17.5h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="canvas-empty-text">{{ t('workflows.canvas.empty') }}</div>
|
||||
</div>
|
||||
|
||||
<VueFlow
|
||||
v-else-if="!graph.parseError"
|
||||
v-else
|
||||
:id="canvasId"
|
||||
class="canvas-flow"
|
||||
:nodes="graph.nodes"
|
||||
@ -235,11 +250,46 @@ onBeforeUnmount(() => {
|
||||
.canvas-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
padding: 24px;
|
||||
padding: 32px 24px;
|
||||
color: var(--mc-text-secondary, #666);
|
||||
background: var(--mc-bg-sunken, transparent);
|
||||
min-height: 280px;
|
||||
}
|
||||
.canvas-empty-illustration {
|
||||
color: var(--mc-text-tertiary, #999);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.canvas-empty-text {
|
||||
text-align: center;
|
||||
max-width: 320px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.canvas-error {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 32px 24px;
|
||||
background: var(--mc-danger-bg, rgba(255, 80, 80, 0.08));
|
||||
color: var(--mc-danger, #c0392b);
|
||||
min-height: 280px;
|
||||
}
|
||||
.canvas-error-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
.canvas-error-msg {
|
||||
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.canvas-flow {
|
||||
flex: 1;
|
||||
|
||||
@ -384,7 +384,30 @@ async function onCreateSubmit(payload: { name: string; description: string }) {
|
||||
const created = res.data as unknown as WorkflowSummary
|
||||
createDialogOpen.value = false
|
||||
await reload()
|
||||
if (created?.id) await select(created.id)
|
||||
if (created?.id) {
|
||||
await select(created.id)
|
||||
// Seed a starter step so the canvas renders something the user
|
||||
// can immediately edit, instead of opening on a blank slate.
|
||||
// The compile button also has something real to validate.
|
||||
if (!draftJson.value || !draftJson.value.trim()) {
|
||||
draftJson.value = JSON.stringify({
|
||||
steps: [
|
||||
{
|
||||
name: 'first-step',
|
||||
agentName: 'agent-name',
|
||||
mode: { type: 'sequential' },
|
||||
promptTemplate: 'Hello {{ inputs.payload }}',
|
||||
},
|
||||
],
|
||||
}, null, 2)
|
||||
try {
|
||||
await workflowApi.saveDraft(created.id, draftJson.value)
|
||||
} catch (e) {
|
||||
// Non-fatal: the user can still hit Save Draft manually.
|
||||
console.warn('seed draft save failed', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus(t('workflows.status.createFailed', { msg: (e as Error).message }), 'err')
|
||||
} finally {
|
||||
@ -838,10 +861,21 @@ button:disabled {
|
||||
}
|
||||
.canvas-pane {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 240px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
min-height: 360px;
|
||||
min-height: 420px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.canvas-pane > .workflow-canvas {
|
||||
/* fill available width when no inspector is visible — earlier the
|
||||
pane reserved 240px for an inspector via grid-template-columns
|
||||
even when it was v-if hidden, leaving the canvas confined. */
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.canvas-pane > .canvas-inspector {
|
||||
flex: 0 0 240px;
|
||||
}
|
||||
.canvas-inspector {
|
||||
background: var(--mc-bg-elevated, rgba(0, 0, 0, 0.02));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user