feat: live lifecycle warning analysis with one-click Fix / Fix all

Lifecycle warnings now recompute client-side from the live graph for
editable workflows, so the banner reflects edits immediately instead of
waiting for a save round-trip. The two deterministically fixable codes gain
one-click fixes in the banner (all view modes):
- missing-merge-region inserts a Merge boundary in front of end;
- missing-completion-summary inserts the canonical completion-summary node
  (config from @fusion/core's completionSummaryNode) upstream of the merge
  region when one exists, else in front of end.
"Fix all" on the collapsed summary line applies both in order, producing
start → summary → merge → end on a fresh workflow in one click. The other
three codes are structural judgment calls and stay manual.

analyzeWorkflowLifecycle + completionSummaryNode are pure and now re-export
through core's browser-safe types.ts alias entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-11 17:45:05 -07:00
parent 72a98aed18
commit f6806f06cb
14 changed files with 429 additions and 26 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@@ -7946,3 +7946,19 @@ export {
export type { ResolvedModelSelection } from "./model-resolution.js";
export { resolveResearchSettings } from "./research-settings.js";
export type { ResolvedResearchSettings } from "./research-settings.js";
/*
FNXC:WorkflowLifecycleAutofix 2026-07-12-13:00:
The workflow editor recomputes lifecycle warnings client-side as the graph is
edited (so the banner clears without a save round-trip) and offers one-click
fixes that insert the canonical completion-summary node. Both helpers are
pure (types + string constants only), so they are safe to re-export through
this browser-safe alias entry.
*/
export { analyzeWorkflowLifecycle } from "./workflow-lifecycle-validation.js";
export type { WorkflowLifecycleWarning, WorkflowLifecycleWarningCode } from "./workflow-lifecycle-validation.js";
export {
completionSummaryNode,
isCompletionSummaryNode,
COMPLETION_SUMMARY_NODE_ID,
} from "./builtin-completion-summary-node.js";

View File

@@ -1683,6 +1683,41 @@ native marker and rotate the chevron on open.
margin-top: var(--space-xs);
}
/*
FNXC:WorkflowLifecycleAutofix 2026-07-12-13:00:
One-click fix affordances: a quiet pill per fixable warning row plus a
"Fix all" pill inline in the collapsed summary line.
*/
.wf-lifecycle-fix {
display: inline-flex;
align-items: center;
padding: 1px 9px;
border: 1px solid color-mix(in srgb, var(--ws-warning) 55%, var(--border));
border-radius: var(--radius-pill);
background: transparent;
color: var(--ws-warning);
font: inherit;
font-size: 0.7rem;
font-weight: 600;
cursor: pointer;
transition:
background var(--transition-fast),
color var(--transition-fast);
}
.wf-lifecycle-fix:hover {
background: color-mix(in srgb, var(--ws-warning) 15%, transparent);
}
.wf-lifecycle-fix:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.wf-lifecycle-fix--all {
margin-left: var(--space-xs);
}
.wf-lifecycle-warnings ul {
display: grid;
gap: 4px;

View File

@@ -20,7 +20,8 @@ import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { X, Plus, Trash2, Save, MessageSquare, Terminal, Shield, GitMerge, Loader2, HelpCircle, PauseCircle, Split, Merge, Repeat, ToggleRight, ClipboardCheck, ListChecks, Code2, Bell, LayoutGrid, Workflow, Download, Upload, ChevronDown, ChevronRight, ChevronLeft, Library, Sparkles, Maximize2, Minimize2, DoorOpen } from "lucide-react";
import type { WorkflowDefinition, WorkflowIrColumn, TraitViolation, WorkflowStepTemplate, WorkflowIrNodeKind } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
import { getErrorMessage, analyzeWorkflowLifecycle } from "@fusion/core";
import type { WorkflowLifecycleWarning, WorkflowLifecycleWarningCode } from "@fusion/core";
import {
fetchWorkflows,
createWorkflow,
@@ -92,6 +93,12 @@ import {
} from "./workflow-flow-mapping";
import { autoLayout, applyAutoLayout } from "./workflow-auto-layout";
import { insertNodeOnEdge, findAppendEdgeId, spliceInsertedSubgraphOnEdge } from "./workflow-simple-layout";
import {
LIFECYCLE_AUTOFIXABLE_CODES,
lifecycleFixNodeSpec,
applyLifecycleWarningFix,
applyAllLifecycleWarningFixes,
} from "./workflow-lifecycle-autofix";
import { WorkflowSimpleCanvas } from "./WorkflowSimpleCanvas";
import { WorkflowAddStepModal, type AddStepPaletteEntry } from "./WorkflowAddStepModal";
import { fetchTraits, fetchStepParsers, type TraitCatalogEntry } from "../api";
@@ -1035,7 +1042,25 @@ function InnerEditor({
const activeWorkflow = useMemo(() => workflows.find((w) => w.id === activeId), [workflows, activeId]);
const isBuiltin = !!activeWorkflow && isBuiltinWorkflowId(activeWorkflow.id);
const lifecycleWarnings = activeWorkflow?.lifecycleWarnings ?? [];
/*
FNXC:WorkflowLifecycleAutofix 2026-07-12-13:00:
Lifecycle warnings recompute client-side from the LIVE graph for editable
workflows, so the banner reflects edits (including one-click fixes)
immediately instead of waiting for the save round-trip. Built-ins are
read-only, and an unmaterialized canvas has nothing to analyze — both keep
the server-computed warnings.
*/
const lifecycleWarnings: WorkflowLifecycleWarning[] = useMemo(() => {
if (!activeWorkflow) return [];
const serverWarnings = activeWorkflow.lifecycleWarnings ?? [];
if (isBuiltin || nodes.length === 0) return serverWarnings;
try {
const { ir } = flowToIr(name || activeWorkflow.name, nodes, edges, columns, fields, settings);
return analyzeWorkflowLifecycle(ir, { kind: activeWorkflow.kind });
} catch {
return serverWarnings;
}
}, [activeWorkflow, isBuiltin, nodes, edges, columns, fields, settings, name]);
// Live mirror of the active workflow id, readable inside async callbacks that
// captured an earlier value before an await (e.g. the AI-design round-trip).
@@ -1760,6 +1785,45 @@ function InnerEditor({
[insertFromAddStep],
);
/*
FNXC:WorkflowLifecycleAutofix 2026-07-12-13:00:
One-click lifecycle fixes: splice the canonical node into the unambiguous
wiring point when one exists, else fall back to a free-floating node with
the same config (advanced-canvas users can wire it manually). The live
warning recompute clears the banner row immediately; Save persists it.
*/
const handleLifecycleFix = useCallback(
(code: WorkflowLifecycleWarningCode) => {
if (isBuiltin) return;
const result = applyLifecycleWarningFix(nodes, edges, code);
if (result) {
setNodes(result.nodes);
setEdges(result.edges);
setSelectedNodeId(result.newNodeId);
setSelectedEdgeId(null);
return;
}
const spec = lifecycleFixNodeSpec(code);
if (spec) addNode(spec.kind, spec.label, spec.presetConfig);
},
[isBuiltin, nodes, edges, setNodes, setEdges, addNode],
);
const fixableLifecycleCodes = useMemo(
() => lifecycleWarnings.map((w) => w.code).filter((code) => LIFECYCLE_AUTOFIXABLE_CODES.has(code)),
[lifecycleWarnings],
);
const handleLifecycleFixAll = useCallback(() => {
if (isBuiltin || fixableLifecycleCodes.length === 0) return;
const result = applyAllLifecycleWarningFixes(nodes, edges, fixableLifecycleCodes);
if (!result) return;
setNodes(result.nodes);
setEdges(result.edges);
setSelectedNodeId(result.newNodeId);
setSelectedEdgeId(null);
}, [isBuiltin, fixableLifecycleCodes, nodes, edges, setNodes, setEdges]);
// Auto-layout: one-click left-to-right tidy (U5, R8). Recomputes positions
// only; bands and foreach template children are left in place. Marks the
// editor dirty automatically via the layout serialization in isDirty.
@@ -3104,6 +3168,24 @@ function InnerEditor({
})}
</span>
<ChevronDown size={12} className="wf-lifecycle-warnings-chevron" aria-hidden />
{/* FNXC:WorkflowLifecycleAutofix 2026-07-12-13:00: one
click inserts every deterministically fixable node
(merge boundary, then completion summary upstream of
it) without expanding the disclosure. */}
{!isBuiltin && fixableLifecycleCodes.length > 0 && (
<button
type="button"
className="wf-lifecycle-fix wf-lifecycle-fix--all"
data-testid="wf-lifecycle-fix-all"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleLifecycleFixAll();
}}
>
{t("workflows.lifecycleFixAll", "Fix all")}
</button>
)}
</summary>
<ul>
{lifecycleWarnings.map((warning, index) => (
@@ -3111,6 +3193,16 @@ function InnerEditor({
<span className="wf-lifecycle-warning-code">{warning.code}</span>
{warning.nodeId && <span className="wf-lifecycle-warning-node">{warning.nodeId}</span>}
<span>{warning.message}</span>
{!isBuiltin && LIFECYCLE_AUTOFIXABLE_CODES.has(warning.code) && (
<button
type="button"
className="wf-lifecycle-fix"
data-testid={`wf-lifecycle-fix-${warning.code}`}
onClick={() => handleLifecycleFix(warning.code)}
>
{t("workflows.lifecycleFix", "Fix")}
</button>
)}
</li>
))}
</ul>

View File

@@ -550,38 +550,71 @@ describe("WorkflowNodeEditor", () => {
expect(screen.getAllByRole("button", { name: "QA" })[0]).toHaveClass("active");
});
it("renders workflow lifecycle warnings returned by the store", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([
{
...v2Def(),
lifecycleWarnings: [
{
code: "missing-merge-region",
message: "Full task workflows should include a merge region so done is backed by merge proof.",
},
{
code: "optional-group-after-execution",
nodeId: "plan-review",
message: "Plan Review should be ordered before parse/execution.",
},
],
},
]);
it("analyzes lifecycle warnings live and fixes a missing completion summary in one click", async () => {
// FNXC:WorkflowLifecycleAutofix 2026-07-12-13:00: warnings recompute from
// the LIVE graph (not the server snapshot) for editable workflows, and
// deterministically fixable codes carry a one-click Fix button.
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
// FNXC:WorkflowEditor 2026-07-12-10:30: banner is a collapsed-by-default
// disclosure: a count summary line, with details revealed on expand.
// def() has a merge seam but no completion-summary node → exactly one
// live warning, collapsed to a count line by default.
const banner = await screen.findByTestId("wf-lifecycle-warnings");
expect(banner).toHaveTextContent("2 lifecycle warnings");
expect(banner).toHaveTextContent("1 lifecycle warning");
expect(banner).not.toHaveAttribute("open");
fireEvent.click(screen.getByTestId("wf-lifecycle-warnings-toggle"));
expect(banner).toHaveAttribute("open");
expect(banner).toHaveTextContent("missing-merge-region");
expect(banner).toHaveTextContent("optional-group-after-execution");
expect(banner).toHaveTextContent("plan-review");
expect(banner).toHaveTextContent("missing-completion-summary");
fireEvent.click(screen.getByTestId("wf-lifecycle-fix-missing-completion-summary"));
// The canonical summary node is inserted (selected → inspector opens) and
// the live re-analysis clears the banner without a save round-trip.
await waitFor(() => expect(screen.queryByTestId("wf-lifecycle-warnings")).not.toBeInTheDocument());
expect(screen.getAllByText("Completion summary").length).toBeGreaterThan(0);
});
it("fixes all lifecycle warnings on a fresh start→end graph and wires summary→merge→end", async () => {
const blank: WorkflowDefinition = {
...def(),
id: "WF-BLANK",
name: "Blank",
ir: {
version: "v1",
name: "Blank",
nodes: [
{ id: "start", kind: "start" },
{ id: "end", kind: "end" },
],
edges: [{ from: "start", to: "end", condition: "success" }],
},
layout: { start: { x: 0, y: 0 }, end: { x: 360, y: 0 } },
};
vi.mocked(fetchWorkflows).mockResolvedValue([blank]);
vi.mocked(updateWorkflow).mockImplementation(async (_id, updates) => ({ ...blank, ...(updates as object) }));
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
const banner = await screen.findByTestId("wf-lifecycle-warnings");
expect(banner).toHaveTextContent("2 lifecycle warnings");
// "Fix all" sits on the collapsed summary line — no expand needed.
fireEvent.click(screen.getByTestId("wf-lifecycle-fix-all"));
await waitFor(() => expect(screen.queryByTestId("wf-lifecycle-warnings")).not.toBeInTheDocument());
fireEvent.click(screen.getByText("Save").closest("button")!);
await waitFor(() => expect(updateWorkflow).toHaveBeenCalledTimes(1));
const [, updates] = vi.mocked(updateWorkflow).mock.calls[0];
const ir = (updates as { ir: { nodes: Array<{ id: string; kind: string; config?: Record<string, unknown> }>; edges: Array<{ from: string; to: string }> } }).ir;
const summary = ir.nodes.find((n) => n.config?.summaryTarget === "task");
const merge = ir.nodes.find((n) => n.config?.seam === "merge");
expect(summary).toBeDefined();
expect(merge).toBeDefined();
expect(ir.edges.some((e) => e.from === "start" && e.to === summary!.id)).toBe(true);
expect(ir.edges.some((e) => e.from === summary!.id && e.to === merge!.id)).toBe(true);
expect(ir.edges.some((e) => e.from === merge!.id && e.to === "end")).toBe(true);
expect(ir.edges.some((e) => e.from === "start" && e.to === "end")).toBe(false);
});
it("lets desktop users collapse and restore the workflow sidebar", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);

View File

@@ -0,0 +1,93 @@
import { describe, it, expect } from "vitest";
import type { Node as FlowNode, Edge as FlowEdge } from "@xyflow/react";
import type { WorkflowFlowNodeData } from "../nodes/WorkflowNodeTypes";
import {
LIFECYCLE_AUTOFIXABLE_CODES,
lifecycleFixNodeSpec,
lifecycleFixTargetEdgeId,
applyLifecycleWarningFix,
applyAllLifecycleWarningFixes,
} from "../workflow-lifecycle-autofix";
/*
FNXC:WorkflowLifecycleAutofix 2026-07-12-13:00:
Unit coverage for the deterministic lifecycle fixes: canonical node specs,
wiring-point selection (summary lands upstream of an existing merge region),
and the fix-all composition (merge first, then summary before it).
*/
type N = FlowNode<WorkflowFlowNodeData>;
function node(id: string, kind: WorkflowFlowNodeData["kind"], x = 0, y = 0, extra: Partial<N> = {}): N {
return { id, type: kind, position: { x, y }, data: { kind, label: id, config: {} }, ...extra };
}
function edge(id: string, source: string, target: string, condition = "success"): FlowEdge {
return { id, source, target, data: { condition } };
}
describe("lifecycleFixNodeSpec", () => {
it("produces the canonical completion-summary config", () => {
const spec = lifecycleFixNodeSpec("missing-completion-summary")!;
expect(spec.kind).toBe("prompt");
expect(spec.presetConfig?.summaryTarget).toBe("task");
expect(spec.presetConfig?.toolMode).toBe("readonly");
expect(typeof spec.presetConfig?.prompt).toBe("string");
});
it("produces a merge boundary for missing-merge-region and null otherwise", () => {
expect(lifecycleFixNodeSpec("missing-merge-region")?.kind).toBe("merge");
expect(lifecycleFixNodeSpec("unsafe-terminal-before-merge")).toBeNull();
expect(LIFECYCLE_AUTOFIXABLE_CODES.has("optional-group-after-execution")).toBe(false);
});
});
describe("lifecycleFixTargetEdgeId", () => {
it("targets the edge into an existing merge node for the summary fix", () => {
const nodes = [node("start", "start"), node("a", "prompt", 300, 0), node("m", "merge", 600, 0), node("end", "end", 900, 0)];
const edges = [edge("e1", "start", "a"), edge("e2", "a", "m"), edge("e3", "m", "end")];
expect(lifecycleFixTargetEdgeId(nodes, edges, "missing-completion-summary")).toBe("e2");
expect(lifecycleFixTargetEdgeId(nodes, edges, "missing-merge-region")).toBe("e3");
});
it("falls back to the edge into end when no merge node exists", () => {
const nodes = [node("start", "start"), node("end", "end", 360, 0)];
const edges = [edge("e1", "start", "end")];
expect(lifecycleFixTargetEdgeId(nodes, edges, "missing-completion-summary")).toBe("e1");
});
});
describe("applyAllLifecycleWarningFixes", () => {
it("wires start→summary→merge→end on a fresh graph", () => {
const nodes = [node("start", "start"), node("end", "end", 360, 0)];
const edges = [edge("e1", "start", "end")];
const result = applyAllLifecycleWarningFixes(nodes, edges, [
"missing-completion-summary",
"missing-merge-region",
]);
expect(result).not.toBeNull();
const summary = result!.nodes.find((n) => n.data.config?.summaryTarget === "task");
const merge = result!.nodes.find((n) => n.data.kind === "merge");
expect(summary).toBeDefined();
expect(merge).toBeDefined();
const has = (from: string, to: string) => result!.edges.some((e) => e.source === from && e.target === to);
expect(has("start", summary!.id)).toBe(true);
expect(has(summary!.id, merge!.id)).toBe(true);
expect(has(merge!.id, "end")).toBe(true);
expect(has("start", "end")).toBe(false);
});
it("returns null when no wiring point exists and no fix applies", () => {
// Two edges into end → ambiguous; no merge node to anchor on.
const nodes = [node("start", "start"), node("a", "prompt", 300, 0), node("end", "end", 900, 0)];
const edges = [edge("e1", "start", "end"), edge("e2", "a", "end")];
expect(applyAllLifecycleWarningFixes(nodes, edges, ["missing-merge-region"])).toBeNull();
});
});
describe("applyLifecycleWarningFix", () => {
it("returns null for non-fixable codes", () => {
const nodes = [node("start", "start"), node("end", "end", 360, 0)];
expect(applyLifecycleWarningFix(nodes, [edge("e1", "start", "end")], "review-gate-without-failure-route")).toBeNull();
});
});

View File

@@ -0,0 +1,122 @@
import type { Node as FlowNode, Edge as FlowEdge } from "@xyflow/react";
import { completionSummaryNode } from "@fusion/core";
import type { WorkflowLifecycleWarningCode } from "@fusion/core";
import type { WorkflowFlowNodeData } from "./nodes/WorkflowNodeTypes";
import {
insertNodeOnEdge,
findAppendEdgeId,
edgeSupportsSimpleInsert,
type SimpleInsertSpec,
type SimpleInsertResult,
} from "./workflow-simple-layout";
/*
FNXC:WorkflowLifecycleAutofix 2026-07-12-13:00:
Two of the five lifecycle warning codes have a deterministic remedy, so the
editor's warning banner offers one-click fixes for them (all view modes; the
simplified view is the motivating surface since its users cannot drag nodes
into place):
- missing-completion-summary → insert the CANONICAL completion-summary
prompt node (config from @fusion/core's completionSummaryNode, keyed by
summaryTarget:"task") in front of the merge region when one exists,
otherwise in front of `end`.
- missing-merge-region → insert a Merge boundary node (serializes to
prompt + seam:"merge") in front of `end`.
The remaining codes (unsafe-terminal-before-merge, optional-group-after-
execution, review-gate-without-failure-route) are structural judgment calls
and stay manual.
*/
type LayoutNode = FlowNode<WorkflowFlowNodeData>;
export const LIFECYCLE_AUTOFIXABLE_CODES: ReadonlySet<WorkflowLifecycleWarningCode> = new Set([
"missing-completion-summary",
"missing-merge-region",
] as WorkflowLifecycleWarningCode[]);
/** The node the fix inserts, shared by the edge-splice path and the
* free-floating fallback so both produce identical configs. */
export function lifecycleFixNodeSpec(code: WorkflowLifecycleWarningCode): SimpleInsertSpec | null {
if (code === "missing-merge-region") {
return { kind: "merge", label: "Merge boundary" };
}
if (code === "missing-completion-summary") {
// Canonical config (name/prompt/toolMode/summaryTarget) from core; the
// column argument only stamps the IR node's column, which the editor
// derives from placement instead.
const canonical = completionSummaryNode("");
return {
kind: "prompt",
label: "Completion summary",
presetConfig: { ...(canonical.config ?? {}) },
};
}
return null;
}
function singleInboundEdgeId(nodes: LayoutNode[], edges: FlowEdge[], targetId: string): string | null {
const inbound = edges.filter(
(e) => e.target === targetId && edgeSupportsSimpleInsert(e) && nodes.some((n) => n.id === e.source),
);
return inbound.length === 1 ? inbound[0].id : null;
}
/** The edge the fix should splice into, or null when no unambiguous wiring
* point exists (caller falls back to a free-floating node). */
export function lifecycleFixTargetEdgeId(
nodes: LayoutNode[],
edges: FlowEdge[],
code: WorkflowLifecycleWarningCode,
): string | null {
if (code === "missing-merge-region") {
return findAppendEdgeId(nodes, edges);
}
if (code === "missing-completion-summary") {
// Prefer directly upstream of the merge region so the summary runs
// before review/merge/done, matching the built-in workflows' shape.
const mergeNode = nodes.find((n) => !n.parentId && n.data.kind === "merge");
if (mergeNode) {
const beforeMerge = singleInboundEdgeId(nodes, edges, mergeNode.id);
if (beforeMerge) return beforeMerge;
}
return findAppendEdgeId(nodes, edges);
}
return null;
}
/**
* Apply one lifecycle fix. Returns the updated graph (and the inserted node
* id) or null when the code is not auto-fixable or no unambiguous wiring
* point exists.
*/
export function applyLifecycleWarningFix(
nodes: LayoutNode[],
edges: FlowEdge[],
code: WorkflowLifecycleWarningCode,
): SimpleInsertResult | null {
const spec = lifecycleFixNodeSpec(code);
if (!spec) return null;
const edgeId = lifecycleFixTargetEdgeId(nodes, edges, code);
if (!edgeId) return null;
return insertNodeOnEdge(nodes, edges, edgeId, spec);
}
/**
* Apply every auto-fixable warning in one pass. Merge region first, then the
* completion summary — with the merge boundary in place, the summary lands
* directly upstream of it. Returns null when nothing could be applied.
*/
export function applyAllLifecycleWarningFixes(
nodes: LayoutNode[],
edges: FlowEdge[],
codes: readonly WorkflowLifecycleWarningCode[],
): SimpleInsertResult | null {
const wanted = new Set(codes.filter((code) => LIFECYCLE_AUTOFIXABLE_CODES.has(code)));
let current: SimpleInsertResult | null = null;
for (const code of ["missing-merge-region", "missing-completion-summary"] as WorkflowLifecycleWarningCode[]) {
if (!wanted.has(code)) continue;
const next = applyLifecycleWarningFix(current?.nodes ?? nodes, current?.edges ?? edges, code);
if (next) current = next;
}
return current;
}

View File

@@ -8806,6 +8806,8 @@
"importInvalidJson": "That file isn't valid JSON.",
"importStripped": "Auto-approval flags were removed from imported nodes",
"importTooltip": "Import a workflow from a JSON file",
"lifecycleFix": "Fix",
"lifecycleFixAll": "Fix all",
"lifecycleWarningsCount_one": "{{count}} lifecycle warning",
"lifecycleWarningsCount_other": "{{count}} lifecycle warnings",
"loadFailed": "Failed to load workflows",

View File

@@ -8795,6 +8795,8 @@
"importInvalidJson": "Ese archivo no es JSON válido.",
"importStripped": "Se eliminaron los indicadores de aprobación automática de los nodos importados",
"importTooltip": "Importar un flujo de trabajo desde un archivo JSON",
"lifecycleFix": "",
"lifecycleFixAll": "",
"lifecycleWarningsCount_one": "",
"lifecycleWarningsCount_other": "",
"loadFailed": "",

View File

@@ -8796,6 +8796,8 @@
"importInvalidJson": "Ce fichier n'est pas un JSON valide.",
"importStripped": "Les indicateurs d'approbation automatique ont été supprimés des nœuds importés",
"importTooltip": "Importer un workflow depuis un fichier JSON",
"lifecycleFix": "",
"lifecycleFixAll": "",
"lifecycleWarningsCount_one": "",
"lifecycleWarningsCount_other": "",
"loadFailed": "",

View File

@@ -8795,6 +8795,8 @@
"importInvalidJson": "이 파일은 유효한 JSON이 아닙니다.",
"importStripped": "가져온 노드에서 자동 승인 플래그가 제거되었습니다",
"importTooltip": "JSON 파일에서 워크플로 가져오기",
"lifecycleFix": "",
"lifecycleFixAll": "",
"lifecycleWarningsCount_one": "",
"lifecycleWarningsCount_other": "",
"loadFailed": "",

View File

@@ -8795,6 +8795,8 @@
"importInvalidJson": "该文件不是有效的 JSON。",
"importStripped": "已从导入的节点中移除自动审批标志",
"importTooltip": "从 JSON 文件导入工作流",
"lifecycleFix": "",
"lifecycleFixAll": "",
"lifecycleWarningsCount_one": "",
"lifecycleWarningsCount_other": "",
"loadFailed": "",

View File

@@ -8795,6 +8795,8 @@
"importInvalidJson": "該檔案不是有效的 JSON。",
"importStripped": "已從匯入的節點移除自動核准旗標",
"importTooltip": "從 JSON 檔案匯入工作流程",
"lifecycleFix": "",
"lifecycleFixAll": "",
"lifecycleWarningsCount_one": "",
"lifecycleWarningsCount_other": "",
"loadFailed": "",