Browse Source

feat(45-03): extend dimming system for Conditional Switch non-matching outputs

- Import ConditionalSwitchNodeData type in dimmingUtils.ts
- Add conditionalSwitch block to collect potentially dimmed nodes
  - Non-matching rules (isMatched=false) trigger downstream dimming
  - Default output dims when ANY rule matches (inactive when no match)
- Extend smart cascade blockedTypes collection
  - Check conditionalSwitch rules for match status
  - Add inactive outputs to blockedTypes
- Extend hasReplacementInput check to skip non-matching outputs
- Guard conditionalSwitch nodes from dimming in WorkflowCanvas
  - Updated guard: if (node.type === "switch" || node.type === "conditionalSwitch")

Phase 44's dimming system now handles both Switch and Conditional Switch.
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
ecb8bb0ea7
  1. 4
      src/components/WorkflowCanvas.tsx
  2. 94
      src/store/utils/dimmingUtils.ts

4
src/components/WorkflowCanvas.tsx

@ -294,8 +294,8 @@ export function WorkflowCanvas() {
// Apply dimming className to nodes downstream of disabled Switch outputs // Apply dimming className to nodes downstream of disabled Switch outputs
const allNodes = useMemo(() => { const allNodes = useMemo(() => {
return nodes.map((node) => { return nodes.map((node) => {
// Never dim Switch nodes themselves // Never dim Switch or ConditionalSwitch nodes themselves
if (node.type === "switch") return node; if (node.type === "switch" || node.type === "conditionalSwitch") return node;
const isDimmed = dimmedNodeIds.has(node.id); const isDimmed = dimmedNodeIds.has(node.id);
const dimClass = isDimmed ? "switch-dimmed" : ""; const dimClass = isDimmed ? "switch-dimmed" : "";

94
src/store/utils/dimmingUtils.ts

@ -1,4 +1,4 @@
import type { WorkflowNode, WorkflowEdge, SwitchNodeData } from "@/types"; import type { WorkflowNode, WorkflowEdge, SwitchNodeData, ConditionalSwitchNodeData } from "@/types";
/** /**
* Compute set of node IDs that should be visually dimmed. * Compute set of node IDs that should be visually dimmed.
@ -13,23 +13,53 @@ export function computeDimmedNodes(
const potentiallyDimmed = new Set<string>(); const potentiallyDimmed = new Set<string>();
nodes.forEach(node => { nodes.forEach(node => {
if (node.type !== "switch") return; if (node.type === "switch") {
const switchData = node.data as SwitchNodeData; const switchData = node.data as SwitchNodeData;
if (!switchData.switches) return; if (!switchData.switches) return;
switchData.switches.forEach(sw => { switchData.switches.forEach(sw => {
if (sw.enabled) return; // Only process disabled switches if (sw.enabled) return; // Only process disabled switches
// Find edges from this disabled output handle // Find edges from this disabled output handle
const disabledEdges = edges.filter( const disabledEdges = edges.filter(
e => e.source === node.id && e.sourceHandle === sw.id e => e.source === node.id && e.sourceHandle === sw.id
); );
// DFS traverse downstream from each disabled edge target // DFS traverse downstream from each disabled edge target
disabledEdges.forEach(edge => { disabledEdges.forEach(edge => {
traverseDownstream(edge.target, edges, potentiallyDimmed); traverseDownstream(edge.target, edges, potentiallyDimmed);
});
}); });
}); }
if (node.type === "conditionalSwitch") {
const condData = node.data as ConditionalSwitchNodeData;
if (!condData.rules) return;
// Non-matching rules: their downstream should be dimmed
condData.rules.forEach(rule => {
if (rule.isMatched) return; // Only process non-matching rules
const disabledEdges = edges.filter(
e => e.source === node.id && e.sourceHandle === rule.id
);
disabledEdges.forEach(edge => {
traverseDownstream(edge.target, edges, potentiallyDimmed);
});
});
// Default output: dimmed when ANY rule matches (because default only active when NO rules match)
const anyRuleMatches = condData.rules.some(r => r.isMatched);
if (anyRuleMatches) {
const defaultEdges = edges.filter(
e => e.source === node.id && e.sourceHandle === "default"
);
defaultEdges.forEach(edge => {
traverseDownstream(edge.target, edges, potentiallyDimmed);
});
}
}
}); });
// Step 2: Type-aware smart cascade — only un-dim if an active input replaces // Step 2: Type-aware smart cascade — only un-dim if an active input replaces
@ -41,7 +71,7 @@ export function computeDimmedNodes(
const incomingEdges = edges.filter(e => e.target === nodeId); const incomingEdges = edges.filter(e => e.target === nodeId);
// Collect which handle types are blocked on this node // Collect which handle types are blocked on this node
// (from disabled Switch outputs or from transitively dimmed sources) // (from disabled Switch outputs, non-matching ConditionalSwitch outputs, or from transitively dimmed sources)
const blockedTypes = new Set<string>(); const blockedTypes = new Set<string>();
incomingEdges.forEach(edge => { incomingEdges.forEach(edge => {
const sourceNode = nodes.find(n => n.id === edge.source); const sourceNode = nodes.find(n => n.id === edge.source);
@ -51,6 +81,23 @@ export function computeDimmedNodes(
if (switchEntry && !switchEntry.enabled && edge.targetHandle) { if (switchEntry && !switchEntry.enabled && edge.targetHandle) {
blockedTypes.add(edge.targetHandle); blockedTypes.add(edge.targetHandle);
} }
} else if (sourceNode?.type === "conditionalSwitch") {
const condData = sourceNode.data as ConditionalSwitchNodeData;
const rule = condData.rules?.find(r => r.id === edge.sourceHandle);
const isDefaultHandle = edge.sourceHandle === "default";
// Check if this output is inactive
let isInactive = false;
if (rule) {
isInactive = !rule.isMatched;
} else if (isDefaultHandle) {
// Default is inactive when any rule matches
isInactive = condData.rules?.some(r => r.isMatched) ?? false;
}
if (isInactive && edge.targetHandle) {
blockedTypes.add(edge.targetHandle);
}
} else if (potentiallyDimmed.has(edge.source) && edge.targetHandle) { } else if (potentiallyDimmed.has(edge.source) && edge.targetHandle) {
blockedTypes.add(edge.targetHandle); blockedTypes.add(edge.targetHandle);
} }
@ -67,6 +114,23 @@ export function computeDimmedNodes(
const switchEntry = switchData.switches?.find(s => s.id === edge.sourceHandle); const switchEntry = switchData.switches?.find(s => s.id === edge.sourceHandle);
if (switchEntry && !switchEntry.enabled) return false; if (switchEntry && !switchEntry.enabled) return false;
} }
// Skip non-matching ConditionalSwitch outputs
if (sourceNode?.type === "conditionalSwitch") {
const condData = sourceNode.data as ConditionalSwitchNodeData;
const rule = condData.rules?.find(r => r.id === edge.sourceHandle);
const isDefaultHandle = edge.sourceHandle === "default";
// Check if this output is inactive
let isInactive = false;
if (rule) {
isInactive = !rule.isMatched;
} else if (isDefaultHandle) {
// Default is inactive when any rule matches
isInactive = condData.rules?.some(r => r.isMatched) ?? false;
}
if (isInactive) return false;
}
// Active input — only counts if it provides a blocked type // Active input — only counts if it provides a blocked type
return edge.targetHandle ? blockedTypes.has(edge.targetHandle) : false; return edge.targetHandle ? blockedTypes.has(edge.targetHandle) : false;
}); });

Loading…
Cancel
Save