Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions mcp-server/src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export class FlowState {
// ── Connection Operations ───────────────────

addConnection(options) {
const { fromScreenId, toScreenId, label, action, hotspotId, connectionPath, condition, conditionGroupId, transitionType, transitionLabel } = options;
const { fromScreenId, toScreenId, label, action, hotspotId, connectionPath, condition, conditionGroupId, transitionType, transitionLabel, dataFlow } = options;

if (!this.getScreen(fromScreenId)) throw new Error(`Source screen not found: ${fromScreenId}`);
if (!this.getScreen(toScreenId)) throw new Error(`Target screen not found: ${toScreenId}`);
Expand All @@ -355,6 +355,7 @@ export class FlowState {
conditionGroupId: conditionGroupId || null,
transitionType: transitionType || null,
transitionLabel: transitionLabel || "",
dataFlow: dataFlow || [],
};

this.connections.push(conn);
Expand All @@ -368,7 +369,7 @@ export class FlowState {
const allowed = [
"label", "action", "fromScreenId", "toScreenId",
"connectionPath", "condition", "conditionGroupId",
"transitionType", "transitionLabel",
"transitionType", "transitionLabel", "dataFlow",
];
for (const key of allowed) {
if (updates[key] !== undefined) {
Expand Down
45 changes: 42 additions & 3 deletions mcp-server/src/tools/connection-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ export const connectionTools = [
condition: { type: "string", description: "Condition text for conditional connections" },
conditionGroupId: { type: "string", description: "Group ID for conditional branch connections" },
transitionType: { type: "string", description: "Transition animation type" },
data_flow: {
type: "array",
description: "Data items passed along this connection",
items: {
type: "object",
properties: {
name: { type: "string", description: "Parameter name (e.g. 'productId')" },
type: { type: "string", description: "Data type (String, Int, Bool, Object, Array, Date, ID, or custom)" },
description: { type: "string", description: "What this parameter represents" },
},
required: ["name"],
},
},
},
required: ["fromScreenId", "toScreenId"],
},
Expand All @@ -28,6 +41,19 @@ export const connectionTools = [
condition: { type: "string" },
transitionType: { type: "string" },
transitionLabel: { type: "string" },
data_flow: {
type: "array",
description: "Data items passed along this connection",
items: {
type: "object",
properties: {
name: { type: "string", description: "Parameter name" },
type: { type: "string", description: "Data type" },
description: { type: "string", description: "What this parameter represents" },
},
required: ["name"],
},
},
},
required: ["connectionId"],
},
Expand All @@ -53,16 +79,29 @@ export const connectionTools = [
},
];

function convertDataFlow(dataFlowSnake) {
if (!Array.isArray(dataFlowSnake)) return undefined;
return dataFlowSnake.map(item => ({
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: item.name || "",
type: item.type || "String",
description: item.description || "",
}));
}

export function handleConnectionTool(name, args, state) {
switch (name) {
case "create_connection": {
const conn = state.addConnection(args);
const { data_flow, ...rest } = args;
const dataFlow = convertDataFlow(data_flow);
const conn = state.addConnection({ ...rest, ...(dataFlow ? { dataFlow } : {}) });
return { connectionId: conn.id, fromScreenId: conn.fromScreenId, toScreenId: conn.toScreenId };
}

case "update_connection": {
const { connectionId, ...updates } = args;
const conn = state.updateConnection(connectionId, updates);
const { connectionId, data_flow, ...updates } = args;
const dataFlow = convertDataFlow(data_flow);
const conn = state.updateConnection(connectionId, { ...updates, ...(dataFlow ? { dataFlow } : {}) });
return { success: true, connectionId: conn.id };
}

Expand Down
119 changes: 68 additions & 51 deletions src/components/ConnectionEditModal.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from "react";
import { COLORS, styles } from "../styles/theme";
import { generateId } from "../utils/generateId";
import { DataFlowEditor } from "./DataFlowEditor";

export function ConnectionEditModal({ connection, groupConnections, screens, fromScreen, onSave, onDelete, onClose }) {
const isConditional = groupConnections.length > 1 || !!connection.conditionGroupId;
Expand All @@ -10,16 +11,18 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
const [targetId, setTargetId] = useState(connection.toScreenId || "");
const [transitionType, setTransitionType] = useState(connection.transitionType || "");
const [transitionLabel, setTransitionLabel] = useState(connection.transitionLabel || "");
const [dataFlow, setDataFlow] = useState(connection.dataFlow || []);

const [conditions, setConditions] = useState(() => {
if (isConditional) {
return groupConnections.map((c) => ({
id: c.id,
label: c.condition || c.label || "",
targetScreenId: c.toScreenId || "",
dataFlow: c.dataFlow || [],
}));
}
return [{ id: generateId(), label: "", targetScreenId: connection.toScreenId || "" }];
return [{ id: generateId(), label: "", targetScreenId: connection.toScreenId || "", dataFlow: [] }];
});

const otherScreens = screens.filter((s) => s.id !== fromScreen.id);
Expand All @@ -35,6 +38,7 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
conditionGroupId: connection.conditionGroupId || null,
transitionType: transitionType || null,
transitionLabel: transitionType === "custom" ? transitionLabel : "",
dataFlow: mode === "navigate" ? dataFlow : [],
});
};

Expand Down Expand Up @@ -103,6 +107,8 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
))}
</select>
</label>

<DataFlowEditor items={dataFlow} onChange={setDataFlow} />
</>
)}

Expand All @@ -126,65 +132,76 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
</div>

{conditions.map((cond, i) => (
<div key={cond.id} style={{
display: "flex",
gap: 8,
marginBottom: 8,
alignItems: "flex-end",
}}>
<label style={{ ...styles.monoLabel, flex: 1 }}>
{i === 0 ? "CONDITION" : ""}
<input
value={cond.label}
onChange={(e) => {
<div key={cond.id} style={{ marginBottom: 8 }}>
<div style={{
display: "flex",
gap: 8,
alignItems: "flex-end",
}}>
<label style={{ ...styles.monoLabel, flex: 1 }}>
{i === 0 ? "CONDITION" : ""}
<input
value={cond.label}
onChange={(e) => {
const updated = [...conditions];
updated[i] = { ...updated[i], label: e.target.value };
setConditions(updated);
}}
placeholder={i === conditions.length - 1 ? "e.g. otherwise" : "e.g. user is subscriber"}
style={styles.input}
/>
</label>
<label style={{ ...styles.monoLabel, flex: 1 }}>
{i === 0 ? "TARGET SCREEN" : ""}
<select
value={cond.targetScreenId || ""}
onChange={(e) => {
const updated = [...conditions];
updated[i] = { ...updated[i], targetScreenId: e.target.value || "" };
setConditions(updated);
}}
style={styles.select}
>
<option value="">-- Select --</option>
{otherScreens.map((s) => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</label>
{conditions.length > 1 && (
<button
type="button"
onClick={() => setConditions(conditions.filter((_, j) => j !== i))}
style={{
background: "none",
border: "none",
color: COLORS.danger,
cursor: "pointer",
fontSize: 16,
padding: "6px",
marginBottom: 6,
}}
>
&#10005;
</button>
)}
</div>
<div style={{ marginTop: 6, marginLeft: 8 }}>
<DataFlowEditor
items={cond.dataFlow || []}
onChange={(newDataFlow) => {
const updated = [...conditions];
updated[i] = { ...updated[i], label: e.target.value };
updated[i] = { ...updated[i], dataFlow: newDataFlow };
setConditions(updated);
}}
placeholder={i === conditions.length - 1 ? "e.g. otherwise" : "e.g. user is subscriber"}
style={styles.input}
/>
</label>
<label style={{ ...styles.monoLabel, flex: 1 }}>
{i === 0 ? "TARGET SCREEN" : ""}
<select
value={cond.targetScreenId || ""}
onChange={(e) => {
const updated = [...conditions];
updated[i] = { ...updated[i], targetScreenId: e.target.value || "" };
setConditions(updated);
}}
style={styles.select}
>
<option value="">-- Select --</option>
{otherScreens.map((s) => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</label>
{conditions.length > 1 && (
<button
type="button"
onClick={() => setConditions(conditions.filter((_, j) => j !== i))}
style={{
background: "none",
border: "none",
color: COLORS.danger,
cursor: "pointer",
fontSize: 16,
padding: "6px",
marginBottom: 6,
}}
>
&#10005;
</button>
)}
</div>
</div>
))}

<button
type="button"
onClick={() => setConditions([...conditions, { id: generateId(), label: "", targetScreenId: "" }])}
onClick={() => setConditions([...conditions, { id: generateId(), label: "", targetScreenId: "", dataFlow: [] }])}
style={{
width: "100%",
padding: "6px 0",
Expand Down
35 changes: 35 additions & 0 deletions src/components/ConnectionLines.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,41 @@ export function ConnectionLines({
</g>
);
})()}
{/* Data flow badge */}
{conn.dataFlow?.length > 0 && (() => {
const mx = (fromX + toX) / 2;
const hasLabel = conn.label || conn.condition;
const hasTransition = conn.transitionType;
let badgeY = (fromY + toY) / 2 + (hasLabel ? 12 : 0) + (hasTransition ? 16 : 0);
const dataLabel = conn.dataFlow.length === 1
? conn.dataFlow[0].name || "1 param"
: `${conn.dataFlow.length} params`;
const dataBadgeW = dataLabel.length * 5.5 + 10;
return (
<g>
<rect
x={mx - dataBadgeW / 2}
y={badgeY - 8}
width={dataBadgeW}
height={14}
rx={4}
fill="rgba(0,210,211,0.18)"
stroke="rgba(0,210,211,0.35)"
strokeWidth={1}
/>
<text
x={mx}
y={badgeY + 2}
fill="#00d2d3"
fontSize={9}
fontFamily={FONTS.mono}
textAnchor="middle"
>
{dataLabel}
</text>
</g>
);
})()}
{/* Endpoint handles when selected */}
{isSelected && (
<>
Expand Down
Loading
Loading