diff --git a/docs/assets/simplified-workflow-view/lifecycle-warnings-fix-all.png b/docs/assets/simplified-workflow-view/lifecycle-warnings-fix-all.png new file mode 100644 index 0000000000..0af09b2401 Binary files /dev/null and b/docs/assets/simplified-workflow-view/lifecycle-warnings-fix-all.png differ diff --git a/docs/assets/simplified-workflow-view/lifecycle-warnings-fixed.png b/docs/assets/simplified-workflow-view/lifecycle-warnings-fixed.png new file mode 100644 index 0000000000..51b54f69cc Binary files /dev/null and b/docs/assets/simplified-workflow-view/lifecycle-warnings-fixed.png differ diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ebff481491..b07db8ce24 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -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"; diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.css b/packages/dashboard/app/components/WorkflowNodeEditor.css index 61999843e8..013e987a44 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.css +++ b/packages/dashboard/app/components/WorkflowNodeEditor.css @@ -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; diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.tsx b/packages/dashboard/app/components/WorkflowNodeEditor.tsx index 208c269648..21794bf31f 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.tsx +++ b/packages/dashboard/app/components/WorkflowNodeEditor.tsx @@ -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({ })} + {/* 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 && ( + + )} diff --git a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx index f3df7675e6..44d6492878 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx @@ -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( {}} 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( {}} 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 }>; 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()]); diff --git a/packages/dashboard/app/components/__tests__/workflow-lifecycle-autofix.test.ts b/packages/dashboard/app/components/__tests__/workflow-lifecycle-autofix.test.ts new file mode 100644 index 0000000000..f00ae115dd --- /dev/null +++ b/packages/dashboard/app/components/__tests__/workflow-lifecycle-autofix.test.ts @@ -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; + +function node(id: string, kind: WorkflowFlowNodeData["kind"], x = 0, y = 0, extra: Partial = {}): 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(); + }); +}); diff --git a/packages/dashboard/app/components/workflow-lifecycle-autofix.ts b/packages/dashboard/app/components/workflow-lifecycle-autofix.ts new file mode 100644 index 0000000000..74696ab4eb --- /dev/null +++ b/packages/dashboard/app/components/workflow-lifecycle-autofix.ts @@ -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; + +export const LIFECYCLE_AUTOFIXABLE_CODES: ReadonlySet = 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; +} diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 8728b524f9..ac3fc7d386 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -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", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index 3226b43905..cfaaab43a3 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -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": "", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 262fb6e1f0..ca2d79af5e 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -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": "", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index 4d94cf400b..8d896e9dc9 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -8795,6 +8795,8 @@ "importInvalidJson": "이 파일은 유효한 JSON이 아닙니다.", "importStripped": "가져온 노드에서 자동 승인 플래그가 제거되었습니다", "importTooltip": "JSON 파일에서 워크플로 가져오기", + "lifecycleFix": "", + "lifecycleFixAll": "", "lifecycleWarningsCount_one": "", "lifecycleWarningsCount_other": "", "loadFailed": "", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index 675d4bda76..d16563e0ea 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -8795,6 +8795,8 @@ "importInvalidJson": "该文件不是有效的 JSON。", "importStripped": "已从导入的节点中移除自动审批标志", "importTooltip": "从 JSON 文件导入工作流", + "lifecycleFix": "", + "lifecycleFixAll": "", "lifecycleWarningsCount_one": "", "lifecycleWarningsCount_other": "", "loadFailed": "", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index 53229b3011..eb25fee3de 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -8795,6 +8795,8 @@ "importInvalidJson": "該檔案不是有效的 JSON。", "importStripped": "已從匯入的節點移除自動核准旗標", "importTooltip": "從 JSON 檔案匯入工作流程", + "lifecycleFix": "", + "lifecycleFixAll": "", "lifecycleWarningsCount_one": "", "lifecycleWarningsCount_other": "", "loadFailed": "",