From 88f92f5c27eb580cec552112b34bdabe26a6348b Mon Sep 17 00:00:00 2001 From: Quang Tran <16215255+trmquang93@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:40:44 +0700 Subject: [PATCH] feat: conditional hotspot drag-to-connect and per-branch actions Enable conditional branching via drag from a hotspot to multiple screens, and allow each condition branch to have its own action type (navigate, back, modal, api, custom) with full follow-up configuration. --- mcp-server/src/state.js | 14 +- mcp-server/src/tools/hotspot-tools.js | 18 ++- src/Drawd.jsx | 2 +- src/components/HotspotModal.jsx | 219 +++++++++++++++++++------- src/hooks/useInteractionCallbacks.js | 40 ++++- src/hooks/useScreenManager.js | 57 +++++-- src/pages/docs/userGuide.md | 12 +- src/utils/importFlow.js | 17 ++ src/utils/instructionRenderers.js | 25 ++- 9 files changed, 325 insertions(+), 79 deletions(-) diff --git a/mcp-server/src/state.js b/mcp-server/src/state.js index 9bba339..3461d7c 100644 --- a/mcp-server/src/state.js +++ b/mcp-server/src/state.js @@ -294,8 +294,18 @@ export class FlowState { // Auto-create connections for conditional branches if (hs.action === "conditional" && Array.isArray(hs.conditions)) { hs.conditions.forEach((cond, i) => { - if (cond.targetScreenId) { - this._addHotspotConnection(screenId, cond.targetScreenId, hs.id, "navigate", `condition-${i}`); + const branchAction = cond.action || "navigate"; + if ((branchAction === "navigate" || branchAction === "modal") && cond.targetScreenId) { + this._addHotspotConnection(screenId, cond.targetScreenId, hs.id, branchAction, `condition-${i}`); + } + // API success/error follow-up connections + if (branchAction === "api") { + if (cond.onSuccessTargetId && (cond.onSuccessAction === "navigate" || cond.onSuccessAction === "modal")) { + this._addHotspotConnection(screenId, cond.onSuccessTargetId, hs.id, cond.onSuccessAction, `condition-${i}-api-success`); + } + if (cond.onErrorTargetId && (cond.onErrorAction === "navigate" || cond.onErrorAction === "modal")) { + this._addHotspotConnection(screenId, cond.onErrorTargetId, hs.id, cond.onErrorAction, `condition-${i}-api-error`); + } } }); } diff --git a/mcp-server/src/tools/hotspot-tools.js b/mcp-server/src/tools/hotspot-tools.js index d5dfa29..4269be1 100644 --- a/mcp-server/src/tools/hotspot-tools.js +++ b/mcp-server/src/tools/hotspot-tools.js @@ -53,11 +53,23 @@ export const hotspotTools = [ items: { type: "object", properties: { - label: { type: "string" }, - targetScreenId: { type: "string" }, + label: { type: "string", description: "Condition description" }, + action: { + type: "string", + enum: ["navigate", "back", "modal", "api", "custom"], + description: "Action for this branch (default: navigate)", + }, + targetScreenId: { type: "string", description: "Target screen for navigate/modal" }, + customDescription: { type: "string", description: "Description for custom action" }, + apiEndpoint: { type: "string", description: "API endpoint for api action" }, + apiMethod: { type: "string", enum: ["GET", "POST", "PUT", "DELETE", "PATCH"] }, + onSuccessAction: { type: "string", enum: ["navigate", "back", "modal", "custom", ""] }, + onSuccessTargetId: { type: "string" }, + onErrorAction: { type: "string", enum: ["navigate", "back", "modal", "custom", ""] }, + onErrorTargetId: { type: "string" }, }, }, - description: "Condition branches for 'conditional' action", + description: "Condition branches for 'conditional' action. Each branch can have its own action type.", }, onSuccessAction: { type: "string", enum: ["navigate", "back", "modal", "custom", ""] }, onSuccessTargetId: { type: "string" }, diff --git a/src/Drawd.jsx b/src/Drawd.jsx index c0d27bb..821d244 100644 --- a/src/Drawd.jsx +++ b/src/Drawd.jsx @@ -275,7 +275,7 @@ export default function Drawd({ initialRoomCode }) { setConditionalPrompt, setEditingConditionGroup, setConnectionTypePrompt, setHotspotModal, setConnectionEditModal, - quickConnectHotspot, addConnection, addToConditionalGroup, + quickConnectHotspot, addConnection, addToConditionalGroup, convertToConditionalGroup, onStartConnect, activeTool, captureDragSnapshot, handleDragStart, handleMultiDragStart, diff --git a/src/components/HotspotModal.jsx b/src/components/HotspotModal.jsx index 7760e52..9d18578 100644 --- a/src/components/HotspotModal.jsx +++ b/src/components/HotspotModal.jsx @@ -123,9 +123,15 @@ export function HotspotModal({ screen, hotspot, connection, screens, documents = const [conditions, setConditions] = useState( hotspot?.conditions?.length > 0 ? hotspot.conditions - : [{ id: generateId(), label: "", targetScreenId: "" }] + : [{ id: generateId(), label: "", targetScreenId: "", action: "navigate", dataFlow: [] }] ); + const updateCondition = (index, patch) => { + const updated = [...conditions]; + updated[index] = { ...updated[index], ...patch }; + setConditions(updated); + }; + // Transition (read from associated connection when opened via double-click) const [transitionType, setTransitionType] = useState(connection?.transitionType || ""); const [transitionLabel, setTransitionLabel] = useState(connection?.transitionLabel || ""); @@ -308,7 +314,15 @@ export function HotspotModal({ screen, hotspot, connection, screens, documents = dataFlow: (action === "navigate" || action === "modal") ? dataFlow : [], onSuccessDataFlow: action === "api" ? onSuccessDataFlow : [], onErrorDataFlow: action === "api" ? onErrorDataFlow : [], - conditions: action === "conditional" ? conditions : [], + conditions: action === "conditional" ? conditions.map((cond) => ({ + ...cond, + action: cond.action || "navigate", + targetScreenId: (cond.action === "navigate" || cond.action === "modal" || !cond.action) + ? (cond.targetScreenId || "") : "", + customDescription: cond.action === "custom" ? (cond.customDescription || "") : "", + dataFlow: (cond.action === "navigate" || cond.action === "modal" || !cond.action) + ? (cond.dataFlow || []) : [], + })) : [], x, y, w, h, transitionType, transitionLabel: transitionType === "custom" ? transitionLabel : "", @@ -403,76 +417,171 @@ export function HotspotModal({ screen, hotspot, connection, screens, documents = {conditions.map((cond, i) => ( -
-
+
+
- {conditions.length > 1 && ( - + style={{ background: "none", border: "none", color: COLORS.danger, cursor: "pointer", fontSize: 16, padding: "6px", marginBottom: 6 }} + >✕ )}
-
- { - const updated = [...conditions]; - updated[i] = { ...updated[i], dataFlow: newDataFlow }; - setConditions(updated); - }} - /> -
+ + + + {(cond.action === "navigate" || cond.action === "modal" || !cond.action) && ( + <> + +
+ updateCondition(i, { dataFlow: val })} + /> +
+ + )} + + {cond.action === "custom" && ( +