FN-6523: add mobile workflow connection picker

Adds a touch-friendly path for creating workflow edges from the mobile editor.

- Add mobile node Connect controls with a target picker that delegates to the existing edge validation path.\n- Surface duplicate-edge feedback and select newly created mobile edges for editing.\n- Document the mobile connection flow, add regression coverage, register locale strings, and add the required changeset.\n\nFiles changed:\n .changeset/fn-6523-mobile-workflow-connect.md      |   5 +\n docs/workflow-editor.md                            |   2 +-\n .../app/components/MobileWorkflowGraphView.css     |  67 ++++++++++--\n .../app/components/MobileWorkflowGraphView.tsx     |  76 ++++++++++++--\n .../app/components/WorkflowNodeEditor.tsx          |  69 +++++++++++--\n .../__tests__/WorkflowNodeEditor.test.tsx          | 114 +++++++++++++++++++++\n .../app/components/workflow-mobile-graph.ts        |   7 ++\n packages/i18n/locales/en/app.json                  |   4 +\n packages/i18n/locales/es/app.json                  |   4 +\n packages/i18n/locales/fr/app.json                  |   4 +\n packages/i18n/locales/ko/app.json                  |   4 +\n packages/i18n/locales/zh-CN/app.json               |   4 +\n packages/i18n/locales/zh-TW/app.json               |   4 +\n 13 files changed, 342 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-6523

Fusion-Task-Lineage: 23b32826-278f-4230-a49c-22755ee700cc
This commit is contained in:
gsxdsm
2026-06-17 02:06:32 -07:00
parent 448ac6ab52
commit a998f63242
13 changed files with 344 additions and 24 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Enable creating workflow node connections from the mobile workflow editor.

View File

@@ -61,7 +61,7 @@ Some nodes expose specialized inspector fields: for example prompt execution det
## Edges, conditions, and rework ## Edges, conditions, and rework
Create edges by connecting node handles on the graph. A new connection defaults to a **success** edge. The edge inspector lets you edit routing details when the source node supports it: Create edges by connecting node handles on the graph. In the mobile and compact simple graph, use a node's **Connect** action and target picker to create the same edge without dragging on the canvas; built-in workflows hide this mutation control because they are read-only. A new connection defaults to a **success** edge. The edge inspector lets you edit routing details when the source node supports it:
- **Success / failure conditions:** prompt, script, gate, code, and for-each style sources can route on `success` or `failure`. - **Success / failure conditions:** prompt, script, gate, code, and for-each style sources can route on `success` or `failure`.
- **Outcome conditions:** review-style nodes route verdicts as `outcome:<verdict>` values. The shipped verdict list is `approve`, `revise`, `rethink`, and `unavailable`. - **Outcome conditions:** review-style nodes route verdicts as `outcome:<verdict>` values. The shipped verdict list is `approve`, `revise`, `rethink`, and `unavailable`.

View File

@@ -19,13 +19,24 @@
padding-left: calc(var(--mobile-wf-depth, 0) * var(--space-md)); padding-left: calc(var(--mobile-wf-depth, 0) * var(--space-md));
} }
.mobile-wf-node-actions {
display: inline-flex;
align-items: stretch;
gap: var(--space-xs);
}
.mobile-wf-node-actions--connect {
justify-content: flex-end;
padding-left: calc((var(--mobile-wf-depth, 0) * var(--space-md)) + var(--space-xs));
}
.mobile-wf-node-main { .mobile-wf-node-main {
display: grid; display: grid;
grid-template-columns: auto minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: var(--space-sm); gap: var(--space-sm);
min-width: 0; min-width: 0;
min-height: var(--wf-editor-touch-target, 44px); min-height: var(--wf-editor-touch-target);
padding: var(--space-sm); padding: var(--space-sm);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
@@ -94,12 +105,12 @@
font-size: 0.78rem; font-size: 0.78rem;
} }
.mobile-wf-node-expand { .mobile-wf-node-expand,
.mobile-wf-connect-button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: var(--wf-editor-touch-target, 44px); min-height: var(--wf-editor-touch-target);
min-height: var(--wf-editor-touch-target, 44px);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--bg-secondary); background: var(--bg-secondary);
@@ -111,19 +122,61 @@
box-shadow var(--transition-fast); box-shadow var(--transition-fast);
} }
.mobile-wf-node-expand:hover { .mobile-wf-node-expand {
width: var(--wf-editor-touch-target);
}
.mobile-wf-connect-button {
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
}
.mobile-wf-node-expand:hover,
.mobile-wf-connect-button:hover {
background: var(--bg-tertiary); background: var(--bg-tertiary);
} }
.mobile-wf-node-expand:focus-visible { .mobile-wf-node-expand:focus-visible,
.mobile-wf-connect-button:focus-visible,
.mobile-wf-connect-select:focus-visible {
outline: none; outline: none;
box-shadow: var(--focus-ring-strong); box-shadow: var(--focus-ring-strong);
} }
.mobile-wf-node-expand:active { .mobile-wf-node-expand:active,
.mobile-wf-connect-button:active {
transform: scale(0.97); transform: scale(0.97);
} }
.mobile-wf-connect-picker {
display: grid;
gap: var(--space-xs);
padding-left: calc((var(--mobile-wf-depth, 0) * var(--space-md)) + var(--space-xs));
}
.mobile-wf-connect-label {
color: var(--text-muted);
font-size: var(--font-size-sm);
}
.mobile-wf-connect-select {
width: 100%;
}
@media (max-width: 768px) {
.mobile-wf-node-row {
grid-template-columns: minmax(0, 1fr);
}
.mobile-wf-node-actions {
justify-content: flex-end;
}
.mobile-wf-connect-button {
flex: 1;
}
}
.mobile-wf-node-meta { .mobile-wf-node-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -10,6 +10,7 @@ interface MobileWorkflowGraphViewProps {
selectedEdgeId?: string | null; selectedEdgeId?: string | null;
onSelectNode: (id: string) => void; onSelectNode: (id: string) => void;
onSelectEdge: (id: string) => void; onSelectEdge: (id: string) => void;
onCreateConnection?: (source: string, target: string) => void;
} }
function NodeRow({ function NodeRow({
@@ -19,6 +20,7 @@ function NodeRow({
selectedEdgeId, selectedEdgeId,
onSelectNode, onSelectNode,
onSelectEdge, onSelectEdge,
onCreateConnection,
}: { }: {
row: MobileWorkflowNodeSummary; row: MobileWorkflowNodeSummary;
depth: number; depth: number;
@@ -26,11 +28,15 @@ function NodeRow({
selectedEdgeId?: string | null; selectedEdgeId?: string | null;
onSelectNode: (id: string) => void; onSelectNode: (id: string) => void;
onSelectEdge: (id: string) => void; onSelectEdge: (id: string) => void;
onCreateConnection?: (source: string, target: string) => void;
}) { }) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const hasChildren = row.children.length > 0; const hasChildren = row.children.length > 0;
const [expanded, setExpanded] = useState(depth === 0); const [expanded, setExpanded] = useState(depth === 0);
const [connectPickerOpen, setConnectPickerOpen] = useState(false);
const selected = selectedNodeId === row.id; const selected = selectedNodeId === row.id;
const connectionTargets = row.connectionTargets ?? [];
const canCreateConnection = !!onCreateConnection && row.editable && connectionTargets.length > 0;
return ( return (
<div className="mobile-wf-node-group"> <div className="mobile-wf-node-group">
@@ -53,17 +59,70 @@ function NodeRow({
{row.editable ? <Pencil size={14} aria-hidden /> : null} {row.editable ? <Pencil size={14} aria-hidden /> : null}
</button> </button>
{hasChildren ? ( {hasChildren ? (
<button <div className="mobile-wf-node-actions">
type="button" <button
className="mobile-wf-node-expand" type="button"
aria-expanded={expanded} className="mobile-wf-node-expand"
aria-label={expanded ? t("common.collapse", "Collapse") : t("common.expand", "Expand")} aria-expanded={expanded}
onClick={() => setExpanded((value) => !value)} aria-label={expanded ? t("common.collapse", "Collapse") : t("common.expand", "Expand")}
> onClick={() => setExpanded((value) => !value)}
{expanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />} >
</button> {expanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
</div>
) : null} ) : null}
</div> </div>
{canCreateConnection ? (
<div
className="mobile-wf-node-actions mobile-wf-node-actions--connect"
style={{ ["--mobile-wf-depth" as string]: String(depth) }}
>
<button
type="button"
className="mobile-wf-connect-button"
data-testid={`mobile-wf-connect-${row.id}`}
aria-expanded={connectPickerOpen}
onClick={() => setConnectPickerOpen((value) => !value)}
>
<GitBranch size={14} aria-hidden />
<span>{t("workflowNodes.mobileConnect", "Connect")}</span>
</button>
</div>
) : null}
{/*
FNXC:WorkflowEditor 2026-06-16-23:45:
Mobile and compact simple editing do not render the React Flow canvas, so drag-to-connect handles are unavailable. This picker gives touch users a non-canvas path while the editor still owns edge validation and construction.
*/}
{canCreateConnection && connectPickerOpen ? (
<div
className="mobile-wf-connect-picker"
style={{ ["--mobile-wf-depth" as string]: String(depth) }}
>
<label className="mobile-wf-connect-label" htmlFor={`mobile-wf-connect-target-${row.id}`}>
{t("workflowNodes.mobileConnectTarget", "Target node")}
</label>
<select
id={`mobile-wf-connect-target-${row.id}`}
className="input mobile-wf-connect-select"
data-testid={`mobile-wf-connect-target-${row.id}`}
defaultValue=""
onChange={(event) => {
const target = event.currentTarget.value;
if (!target) return;
onCreateConnection?.(row.id, target);
event.currentTarget.value = "";
setConnectPickerOpen(false);
}}
>
<option value="">{t("workflowNodes.mobileConnectChooseTarget", "Choose a target…")}</option>
{connectionTargets.map((target) => (
<option key={target.id} value={target.id}>
{target.label} ({target.kind})
</option>
))}
</select>
</div>
) : null}
{(row.columnName || row.outgoing.length > 0) && ( {(row.columnName || row.outgoing.length > 0) && (
<div <div
className="mobile-wf-node-meta" className="mobile-wf-node-meta"
@@ -96,6 +155,7 @@ function NodeRow({
selectedEdgeId={selectedEdgeId} selectedEdgeId={selectedEdgeId}
onSelectNode={onSelectNode} onSelectNode={onSelectNode}
onSelectEdge={onSelectEdge} onSelectEdge={onSelectEdge}
onCreateConnection={onCreateConnection}
/> />
))} ))}
</div> </div>
@@ -110,6 +170,7 @@ export function MobileWorkflowGraphView({
selectedEdgeId, selectedEdgeId,
onSelectNode, onSelectNode,
onSelectEdge, onSelectEdge,
onCreateConnection,
}: MobileWorkflowGraphViewProps) { }: MobileWorkflowGraphViewProps) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
if (rows.length === 0) { if (rows.length === 0) {
@@ -131,6 +192,7 @@ export function MobileWorkflowGraphView({
selectedEdgeId={selectedEdgeId} selectedEdgeId={selectedEdgeId}
onSelectNode={onSelectNode} onSelectNode={onSelectNode}
onSelectEdge={onSelectEdge} onSelectEdge={onSelectEdge}
onCreateConnection={onCreateConnection}
/> />
))} ))}
</div> </div>

View File

@@ -85,7 +85,7 @@ import { WorkflowSettingsPanel } from "./WorkflowSettingsPanel";
import type { WorkflowFieldDefinition, WorkflowSettingDefinition } from "../api"; import type { WorkflowFieldDefinition, WorkflowSettingDefinition } from "../api";
import { CustomModelDropdown } from "./CustomModelDropdown"; import { CustomModelDropdown } from "./CustomModelDropdown";
import { MobileWorkflowGraphView } from "./MobileWorkflowGraphView"; import { MobileWorkflowGraphView } from "./MobileWorkflowGraphView";
import { buildMobileWorkflowGraph } from "./workflow-mobile-graph"; import { buildMobileWorkflowGraph, type MobileWorkflowConnectionTarget } from "./workflow-mobile-graph";
type ExecutorKind = "model" | "agent" | "skill" | "cli" | "cli-agent"; type ExecutorKind = "model" | "agent" | "skill" | "cli" | "cli-agent";
type MobileWorkflowPanel = "graph" | "add" | "settings" | "fields" | "columns" | "actions"; type MobileWorkflowPanel = "graph" | "add" | "settings" | "fields" | "columns" | "actions";
@@ -1234,8 +1234,8 @@ function InnerEditor({
// which dedupes on source/target/handles and would block parallel // which dedupes on source/target/handles and would block parallel
// success+failure edges between the same pair (KTD-3). buildConnectionEdge // success+failure edges between the same pair (KTD-3). buildConnectionEdge
// reimplements addEdge's sanity guards plus the author-time cycle guard (KTD-9). // reimplements addEdge's sanity guards plus the author-time cycle guard (KTD-9).
const onConnect = useCallback( const createConnectionEdge = useCallback(
(connection: Connection) => { (connection: Connection, options: { selectCreatedEdge?: boolean } = {}) => {
const result = buildConnectionEdge(connection, edges, nodes); const result = buildConnectionEdge(connection, edges, nodes);
if ("error" in result) { if ("error" in result) {
if (result.error === "cycle") { if (result.error === "cycle") {
@@ -1246,14 +1246,38 @@ function InnerEditor({
), ),
"warning", "warning",
); );
} else if (result.error === "duplicate") {
addToast(
t("workflowNodes.duplicateBlocked", "That connection already exists"),
"warning",
);
} }
return; return;
} }
setEdges((eds) => [...eds, result.edge]); setEdges((eds) => [...eds, result.edge]);
if (options.selectCreatedEdge) {
setSelectedEdgeId(result.edge.id);
setSelectedNodeId(null);
setInspectorCollapsed(false);
}
}, },
[edges, nodes, setEdges, addToast, t], [edges, nodes, setEdges, addToast, t],
); );
const onConnect = useCallback(
(connection: Connection) => {
createConnectionEdge(connection);
},
[createConnectionEdge],
);
const onCreateSimpleConnection = useCallback(
(source: string, target: string) => {
createConnectionEdge({ source, target, sourceHandle: null, targetHandle: null }, { selectCreatedEdge: true });
},
[createConnectionEdge],
);
// Dragging a step node into a column band sets node.column (position-based // Dragging a step node into a column band sets node.column (position-based
// hit testing against the ordered bands — see workflow-flow-mapping). // hit testing against the ordered bands — see workflow-flow-mapping).
const onNodeDragStop = useCallback( const onNodeDragStop = useCallback(
@@ -1974,10 +1998,40 @@ function InnerEditor({
}), }),
[models, agents, skills], [models, agents, skills],
); );
const mobileGraphRows = useMemo( const mobileConnectionTargetsBySource = useMemo(() => {
() => buildMobileWorkflowGraph(nodesForRender, edges, columns, catalogs, t), const targetNodes = nodesForRender
[nodesForRender, edges, columns, catalogs, t], .filter((node) => !isColumnBandNode(node.id) && node.data.kind !== "start")
); .map((node): MobileWorkflowConnectionTarget => ({
id: node.id,
label: node.data.label || node.id,
kind: node.data.kind,
}));
const targetsBySource = new Map<string, MobileWorkflowConnectionTarget[]>();
for (const source of nodesForRender) {
if (
isColumnBandNode(source.id)
|| source.data.kind === "start"
|| source.data.kind === "end"
) {
continue;
}
const targets = targetNodes.filter((target) => target.id !== source.id);
if (targets.length > 0) targetsBySource.set(source.id, targets);
}
return targetsBySource;
}, [nodesForRender]);
const mobileGraphRows = useMemo(() => {
const attachConnectionTargets = (rows: ReturnType<typeof buildMobileWorkflowGraph>): ReturnType<typeof buildMobileWorkflowGraph> =>
rows.map((row) => ({
...row,
connectionTargets: isBuiltin ? [] : mobileConnectionTargetsBySource.get(row.id) ?? [],
children: attachConnectionTargets(row.children),
}));
return attachConnectionTargets(buildMobileWorkflowGraph(nodesForRender, edges, columns, catalogs, t));
}, [nodesForRender, edges, columns, catalogs, t, isBuiltin, mobileConnectionTargetsBySource]);
const currentExecutor = (selectedNode?.data.config?.executor as ExecutorKind | undefined) ?? "model"; const currentExecutor = (selectedNode?.data.config?.executor as ExecutorKind | undefined) ?? "model";
@@ -2520,6 +2574,7 @@ function InnerEditor({
setSelectedEdgeId(id); setSelectedEdgeId(id);
setSelectedNodeId(null); setSelectedNodeId(null);
}} }}
onCreateConnection={isBuiltin ? undefined : onCreateSimpleConnection}
/> />
)} )}

View File

@@ -290,6 +290,41 @@ function scriptDef(): WorkflowDefinition {
}; };
} }
function plainConnectDef(): WorkflowDefinition {
return {
id: "WF-PLAIN-CONNECT",
kind: "workflow",
name: "Plain connect",
description: "",
ir: {
version: "v2",
name: "Plain connect",
columns: [
{ id: "triage", name: "Triage", traits: [{ trait: "intake" }] },
{ id: "done", name: "Done", traits: [{ trait: "complete" }] },
],
nodes: [
{ id: "start", kind: "start", column: "triage" },
{ id: "draft", kind: "step-review", column: "triage", config: { type: "code" } },
{ id: "review", kind: "prompt", column: "triage", config: { prompt: "review" } },
{ id: "end", kind: "end", column: "done" },
],
edges: [
{ from: "start", to: "draft", condition: "success" },
{ from: "review", to: "end", condition: "success" },
],
},
layout: {
start: { x: 0, y: 20 },
draft: { x: 120, y: 60 },
review: { x: 240, y: 120 },
end: { x: 360, y: 240 },
},
createdAt: "2026-06-03T00:00:00.000Z",
updatedAt: "2026-06-03T00:00:00.000Z",
};
}
describe("workflow-flow-mapping", () => { describe("workflow-flow-mapping", () => {
it("round-trips IR through flow and back, preserving structure and layout", () => { it("round-trips IR through flow and back, preserving structure and layout", () => {
const original = def(); const original = def();
@@ -436,6 +471,85 @@ describe("WorkflowNodeEditor", () => {
expect(screen.getByTestId("wf-mobile-add-gate-gate")).toBeInTheDocument(); expect(screen.getByTestId("wf-mobile-add-gate-gate")).toBeInTheDocument();
}); });
it("creates a condition-capable edge from the mobile simple graph without the canvas", async () => {
mockWorkflowEditorViewport("mobile");
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
fireEvent.click(await screen.findByRole("button", { name: "QA" }));
await screen.findByText("Save");
const shell = await screen.findByTestId("wf-mobile-shell");
expect(within(shell).queryByTestId("rf__wrapper")).not.toBeInTheDocument();
expect(screen.queryByTestId("mobile-wf-connect-start")).not.toBeInTheDocument();
expect(screen.queryByTestId("mobile-wf-connect-end")).not.toBeInTheDocument();
fireEvent.click(await screen.findByTestId("mobile-wf-connect-lint"));
fireEvent.change(screen.getByTestId("mobile-wf-connect-target-lint"), { target: { value: "end" } });
const inspector = await screen.findByTestId("wf-edge-inspector");
expect(within(inspector).getByTestId("wf-edge-condition")).toHaveValue("success");
expect(screen.getAllByText("end").length).toBeGreaterThan(1);
});
it("creates a verdict-source edge in the desktop compact simple graph", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([plainConnectDef()]);
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
expect(await screen.findByTestId("wf-workflow-name")).toHaveTextContent("Plain connect");
fireEvent.click(screen.getByTestId("wf-layout-toggle"));
await screen.findByTestId("wf-mobile-shell");
fireEvent.click(await screen.findByTestId("mobile-wf-connect-draft"));
fireEvent.change(screen.getByTestId("mobile-wf-connect-target-draft"), { target: { value: "review" } });
const inspector = await screen.findByTestId("wf-edge-inspector");
expect(within(inspector).queryByTestId("wf-edge-condition")).not.toBeInTheDocument();
expect(within(inspector).getByTestId("wf-edge-verdict")).toHaveValue("");
});
it("hides simple-graph connection controls for built-in read-only workflows", async () => {
mockWorkflowEditorViewport("mobile");
vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef()]);
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
fireEvent.click(await screen.findByRole("button", { name: "Default coding workflow" }));
await screen.findByTestId("wf-mobile-shell");
expect(screen.queryByTestId(/mobile-wf-connect-/)).not.toBeInTheDocument();
});
it("rejects cyclic simple-graph connections with a toast", async () => {
mockWorkflowEditorViewport("mobile");
const addToast = vi.fn();
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={addToast} />);
fireEvent.click(await screen.findByRole("button", { name: "QA" }));
await screen.findByTestId("wf-mobile-shell");
fireEvent.click(await screen.findByTestId("mobile-wf-connect-merge"));
fireEvent.change(screen.getByTestId("mobile-wf-connect-target-merge"), { target: { value: "lint" } });
await waitFor(() => expect(addToast).toHaveBeenCalledWith(
"That connection would create a cycle — only rework edges inside a for-each template may loop back",
"warning",
));
expect(screen.queryByTestId("wf-edge-inspector")).not.toBeInTheDocument();
});
it("offers connection controls for editable foreach template children in the simple graph", async () => {
mockWorkflowEditorViewport("mobile");
vi.mocked(fetchWorkflows).mockResolvedValue([stepwiseDef()]);
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
fireEvent.click(await screen.findByRole("button", { name: "Stepwise" }));
await screen.findByTestId("wf-mobile-shell");
expect(await screen.findByTestId(`mobile-wf-connect-${foreachChildFlowId("loop", "exec")}`)).toBeInTheDocument();
});
it("surfaces built-in simple-editor actions at desktop width", async () => { it("surfaces built-in simple-editor actions at desktop width", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef()]); vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef()]);

View File

@@ -17,6 +17,12 @@ export interface MobileWorkflowEdgeSummary {
kind?: string; kind?: string;
} }
export interface MobileWorkflowConnectionTarget {
id: string;
label: string;
kind: WorkflowFlowNodeData["kind"];
}
export interface MobileWorkflowNodeSummary { export interface MobileWorkflowNodeSummary {
id: string; id: string;
label: string; label: string;
@@ -27,6 +33,7 @@ export interface MobileWorkflowNodeSummary {
parentId?: string; parentId?: string;
templateLocalId?: string; templateLocalId?: string;
outgoing: MobileWorkflowEdgeSummary[]; outgoing: MobileWorkflowEdgeSummary[];
connectionTargets?: MobileWorkflowConnectionTarget[];
children: MobileWorkflowNodeSummary[]; children: MobileWorkflowNodeSummary[];
} }

View File

@@ -6905,6 +6905,10 @@
"gateBlocks": "Gate (blocks)", "gateBlocks": "Gate (blocks)",
"gateMode": "Gate mode", "gateMode": "Gate mode",
"insertTemplate": "Insert template {{name}}", "insertTemplate": "Insert template {{name}}",
"mobileConnect": "Connect",
"mobileConnectTarget": "Target node",
"mobileConnectChooseTarget": "Choose a target…",
"duplicateBlocked": "That connection already exists",
"interpreterOnly": "This workflow branches, so it runs on the graph interpreter — it can't compile to the linear step engine, but it will still run.", "interpreterOnly": "This workflow branches, so it runs on the graph interpreter — it can't compile to the linear step engine, but it will still run.",
"joinAll": "All branches", "joinAll": "All branches",
"joinAny": "Any branch", "joinAny": "Any branch",

View File

@@ -6792,6 +6792,10 @@
"summaryReviewType": "Revisión {{type}}", "summaryReviewType": "Revisión {{type}}",
"trivialGraphHint": "Este flujo de trabajo solo ejecuta inicio → fin. Añade pasos desde la paleta superior para construirlo.", "trivialGraphHint": "Este flujo de trabajo solo ejecuta inicio → fin. Añade pasos desde la paleta superior para construirlo.",
"insertTemplate": "Insertar plantilla {{name}}", "insertTemplate": "Insertar plantilla {{name}}",
"mobileConnect": "Conectar",
"mobileConnectTarget": "Nodo de destino",
"mobileConnectChooseTarget": "Elige un destino…",
"duplicateBlocked": "Esa conexión ya existe",
"templateFilterLabel": "Filtrar plantillas", "templateFilterLabel": "Filtrar plantillas",
"templateFilterPlaceholder": "Filtrar plantillas", "templateFilterPlaceholder": "Filtrar plantillas",
"templateSeamConflict": "Este fragmento duplica la unión \"{{seam}}\" que ya está en el lienzo, por lo que no se puede insertar.", "templateSeamConflict": "Este fragmento duplica la unión \"{{seam}}\" que ya está en el lienzo, por lo que no se puede insertar.",

View File

@@ -6792,6 +6792,10 @@
"summaryReviewType": "Examen {{type}}", "summaryReviewType": "Examen {{type}}",
"trivialGraphHint": "Ce workflow ne fait qu'exécuter début → fin. Ajoutez des étapes depuis la palette ci-dessus pour le développer.", "trivialGraphHint": "Ce workflow ne fait qu'exécuter début → fin. Ajoutez des étapes depuis la palette ci-dessus pour le développer.",
"insertTemplate": "Insérer le modèle {{name}}", "insertTemplate": "Insérer le modèle {{name}}",
"mobileConnect": "Connecter",
"mobileConnectTarget": "Nœud cible",
"mobileConnectChooseTarget": "Choisissez une cible…",
"duplicateBlocked": "Cette connexion existe déjà",
"templateFilterLabel": "Filtrer les modèles", "templateFilterLabel": "Filtrer les modèles",
"templateFilterPlaceholder": "Filtrer les modèles", "templateFilterPlaceholder": "Filtrer les modèles",
"templateSeamConflict": "Ce fragment duplique la jointure « {{seam}} » déjà présente sur le canevas, il ne peut donc pas être inséré.", "templateSeamConflict": "Ce fragment duplique la jointure « {{seam}} » déjà présente sur le canevas, il ne peut donc pas être inséré.",

View File

@@ -6792,6 +6792,10 @@
"summaryReviewType": "{{type}} 검토", "summaryReviewType": "{{type}} 검토",
"trivialGraphHint": "이 워크플로는 시작 → 끝만 실행합니다. 위 팔레트에서 단계를 추가하여 구성하세요.", "trivialGraphHint": "이 워크플로는 시작 → 끝만 실행합니다. 위 팔레트에서 단계를 추가하여 구성하세요.",
"insertTemplate": "{{name}} 템플릿 삽입", "insertTemplate": "{{name}} 템플릿 삽입",
"mobileConnect": "연결",
"mobileConnectTarget": "대상 노드",
"mobileConnectChooseTarget": "대상을 선택하세요…",
"duplicateBlocked": "해당 연결이 이미 있습니다",
"templateFilterLabel": "템플릿 필터", "templateFilterLabel": "템플릿 필터",
"templateFilterPlaceholder": "템플릿 필터", "templateFilterPlaceholder": "템플릿 필터",
"templateSeamConflict": "이 조각은 이미 캔버스에 있는 \"{{seam}}\" 이음새와 중복되므로 삽입할 수 없습니다.", "templateSeamConflict": "이 조각은 이미 캔버스에 있는 \"{{seam}}\" 이음새와 중복되므로 삽입할 수 없습니다.",

View File

@@ -6792,6 +6792,10 @@
"summaryReviewType": "{{type}} 审查", "summaryReviewType": "{{type}} 审查",
"trivialGraphHint": "此工作流仅运行开始→结束。从上方面板添加步骤以构建工作流。", "trivialGraphHint": "此工作流仅运行开始→结束。从上方面板添加步骤以构建工作流。",
"insertTemplate": "插入模板 {{name}}", "insertTemplate": "插入模板 {{name}}",
"mobileConnect": "连接",
"mobileConnectTarget": "目标节点",
"mobileConnectChooseTarget": "选择目标…",
"duplicateBlocked": "该连接已存在",
"templateFilterLabel": "筛选模板", "templateFilterLabel": "筛选模板",
"templateFilterPlaceholder": "筛选模板", "templateFilterPlaceholder": "筛选模板",
"templateSeamConflict": "此片段与画布上已存在的“{{seam}}”接缝重复,无法插入。", "templateSeamConflict": "此片段与画布上已存在的“{{seam}}”接缝重复,无法插入。",

View File

@@ -6792,6 +6792,10 @@
"summaryReviewType": "{{type}} 審查", "summaryReviewType": "{{type}} 審查",
"trivialGraphHint": "這個工作流程只會執行 start → end。請從上方的選盤新增步驟來建構它。", "trivialGraphHint": "這個工作流程只會執行 start → end。請從上方的選盤新增步驟來建構它。",
"insertTemplate": "插入範本 {{name}}", "insertTemplate": "插入範本 {{name}}",
"mobileConnect": "連接",
"mobileConnectTarget": "目標節點",
"mobileConnectChooseTarget": "選擇目標…",
"duplicateBlocked": "該連接已存在",
"templateFilterLabel": "篩選範本", "templateFilterLabel": "篩選範本",
"templateFilterPlaceholder": "篩選範本", "templateFilterPlaceholder": "篩選範本",
"templateSeamConflict": "這個片段與畫布上已有的「{{seam}}」接縫重複,因此無法插入。", "templateSeamConflict": "這個片段與畫布上已有的「{{seam}}」接縫重複,因此無法插入。",