import { type NodeProps, Position, type Node } from '@xyflow/react'; import { Toolbar } from '../components/NodeComponents'; import styles from '../../VisProg.module.css'; import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx"; import {allowOnlyConnectionsFromType, noSelfConnections} from "../HandleRules.ts"; import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry'; import useFlowStore from '../VisProgStores'; import { TextField } from '../../../../components/TextField'; /** * The default data dot a phase node * @param label: the label of this phase * @param droppable: whether this node is droppable from the drop bar (initialized as true) * @param children: ID's of children of this node * @param hasReduce: whether this node has reducing functionality (true by default) * @param nextPhaseId: */ export type PhaseNodeData = { label: string; droppable: boolean; children: string[]; hasReduce: boolean; nextPhaseId: string | "end" | null; isFirstPhase: boolean; }; export type PhaseNode = Node /** * Defines how a phase node should be rendered * @param props NodeProps, like id, label, children * @returns React.JSX.Element */ export default function PhaseNode(props: NodeProps) { const data = props.data; const {updateNodeData} = useFlowStore(); const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value}); const label_input_id = `phase_${props.id}_label_input`; return ( <>
); }; /** * Reduces each phase, including its children down into its relevant data. * @param node the node which is being reduced * @param nodes all the nodes currently in the flow. * @returns A collection of all reduced nodes in this phase, starting with this phases' reduced data. */ export function PhaseReduce(node: Node, nodes: Node[]) { const thisNode = node as PhaseNode; const data = thisNode.data as PhaseNodeData; // node typings that are not in phase const nodesNotInPhase: string[] = Object.entries(NodesInPhase) .filter(([, f]) => !f()) .map(([t]) => t); // node typings that then are in phase const nodesInPhase: string[] = Object.entries(NodeTypes) .filter(([t]) => !nodesNotInPhase.includes(t)) .map(([t]) => t); // children nodes - make sure to check for empty arrays let childrenNodes: Node[] = []; if (data.children) childrenNodes = nodes.filter((node) => data.children.includes(node.id)); // Build the result object const result: Record = { id: thisNode.id, label: data.label, }; nodesInPhase.forEach((type) => { const typedChildren = childrenNodes.filter((child) => child.type == type); const reducer = NodeReduces[type as keyof typeof NodeReduces]; if (!reducer) { console.warn(`No reducer found for node type ${type}`); result[type + "s"] = []; } else { result[type + "s"] = typedChildren.map((child) => reducer(child, nodes)); } }); return result; } /** * This function is called whenever a connection is made with this node type as the target (phase) * @param _thisNode the node of this node type which function is called * @param _sourceNodeId the source of the received connection */ export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) { const data = _thisNode.data as PhaseNodeData const nodes = useFlowStore.getState().nodes; const sourceNode = nodes.find((node) => node.id === _sourceNodeId)! switch (sourceNode.type) { case "phase": break; case "start": data.isFirstPhase = true; break; // we only add none phase or start nodes to the children // endNodes cannot be the source of an outgoing connection // so we don't need to cover them with a special case // before handling the default behavior default: data.children.push(_sourceNodeId); break; } } /** * This function is called whenever a connection is made with this node type as the source (phase) * @param _thisNode the node of this node type which function is called * @param _targetNodeId the target of the created connection */ export function PhaseConnectionSource(_thisNode: Node, _targetNodeId: string) { const data = _thisNode.data as PhaseNodeData const nodes = useFlowStore.getState().nodes; const targetNode = nodes.find((node) => node.id === _targetNodeId) if (!targetNode) {throw new Error("Source node not found")} // we set the nextPhaseId to the next target's id if the target is a phaseNode, // or "end" if the target node is the end node switch (targetNode.type) { case 'phase': data.nextPhaseId = _targetNodeId; break; case 'end': data.nextPhaseId = "end"; break; default: break; } } /** * This function is called whenever a connection is disconnected with this node type as the target (phase) * @param _thisNode the node of this node type which function is called * @param _sourceNodeId the source of the disconnected connection */ export function PhaseDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) { const data = _thisNode.data as PhaseNodeData const nodes = useFlowStore.getState().nodes; const sourceNode = nodes.find((node) => node.id === _sourceNodeId) const sourceType = sourceNode ? sourceNode.type : "deleted"; switch (sourceType) { case "phase": break; case "start": data.isFirstPhase = false; break; // we only add none phase or start nodes to the children // endNodes cannot be the source of an outgoing connection // so we don't need to cover them with a special case // before handling the default behavior default: data.children = data.children.filter((child) => { if (child != _sourceNodeId) return child; }); break; } } /** * This function is called whenever a connection is disconnected with this node type as the source (phase) * @param _thisNode the node of this node type which function is called * @param _targetNodeId the target of the diconnected connection */ export function PhaseDisconnectionSource(_thisNode: Node, _targetNodeId: string) { const data = _thisNode.data as PhaseNodeData const nodes = useFlowStore.getState().nodes; // if the target is a phase or end node set the nextPhaseId to null, // as we are no longer connected to a subsequent phaseNode or to the endNode if (nodes.some((node) => node.id === _targetNodeId && ['phase', 'end'].includes(node.type!))){ data.nextPhaseId = null; } }