import { Edge, EdgeChange, Node, NodeChange, OnNodesChange, OnEdgesChange, applyNodeChanges, applyEdgeChanges, XYPosition, } from '@xyflow/react'; import { nanoid } from 'nanoid'; import { create } from 'zustand'; import { HistoryData, HistoryState, NodeLayout, NodeRelationType } from './types'; import { getLayoutedElements } from './layout'; const createHistoryState = (initialPresent: HistoryData): HistoryState => ({ past: [], present: initialPresent, future: [], }); const initialNodes: Node[] = [{ id: 'root', type: 'mindmap', data: { label: 'React Flow Mind Map' }, position: { x: 0, y: 0 }, }]; export type RFState = { nodes: Node[]; edges: Edge[]; onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; history: HistoryState; addChildNode: (nodeId: string, position?: XYPosition) => void; updateNodeLabel: (nodeId: string, label: string) => void addSiblingNode: (nodeId: string, position?: XYPosition) => void selectedNodeId: string | null; setSelectedNodeId: (nodeId: string | null) => void; editingNodeId: string | null; setEditingNodeId: (nodeId: string | null) => void; isEditing: boolean; undo: () => void; redo: () => void; canUndo: boolean; canRedo: boolean; }; const useMindMapStore = create((set, get) => { const updateHistory = (newState: Partial) => { const currentState = get().history.present; return { past: [...get().history.past, currentState], present: { ...currentState, ...newState }, future: [], }; }; const createNewNode = (label: string = 'New Node'): Node => ({ id: nanoid(), type: 'mindmap', data: { label }, position: { x: 0, y: 0 } }); const addNode = ( parentId: string, relationType: NodeRelationType ) => { const { nodes, edges, editingNodeId } = get(); const parentNode = nodes.find(node => node.id === parentId); if (!parentNode) return; const newNode = createNewNode(); const newEdge = { id: nanoid(), source: relationType === 'child' ? parentId : edges.find(e => e.target === parentId)?.source ?? parentId, target: newNode.id, type: 'smoothstep', }; const newNodes = [...nodes, newNode]; const newEdges = [...edges, newEdge]; const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(newNodes, newEdges); set({ nodes: layoutedNodes, edges: layoutedEdges, selectedNodeId: newNode.id, history: updateHistory({ nodes: layoutedNodes, edges: layoutedEdges, selectedNodeId: newNode.id, editingNodeId, }), }); }; return { nodes: initialNodes, edges: [], isEditing: false, history: createHistoryState({ nodes: initialNodes, edges: [], selectedNodeId: null, editingNodeId: null }), editingNodeId: null, setEditingNodeId: (nodeId: string | null) => { const { nodes, edges, selectedNodeId } = get(); set({ editingNodeId: nodeId, isEditing: Boolean(nodeId), history: { past: [...get().history.past, get().history.present], present: { nodes, edges, selectedNodeId, editingNodeId: nodeId }, future: [], }, }); }, selectedNodeId: null, setSelectedNodeId: (nodeId: string | null) => { const { nodes, edges, editingNodeId } = get(); set({ selectedNodeId: nodeId, history: { past: [...get().history.past, get().history.present], present: { nodes, edges, selectedNodeId: nodeId, editingNodeId }, future: [], }, }); }, updateNodeLabel: (nodeId: string, label: string) => { const { nodes, edges, selectedNodeId, editingNodeId } = get(); const newNodes = nodes.map((node) => { if (node.id === nodeId) { return { ...node, data: { ...node.data, label } }; } return node; }); set({ nodes: newNodes, edges, selectedNodeId, history: { past: [...get().history.past, get().history.present], present: { nodes: newNodes, edges, selectedNodeId, editingNodeId }, future: [], }, }); }, onNodesChange: (changes: NodeChange[]) => { console.log('on node change', changes) const { nodes, edges, selectedNodeId } = get(); const newNodes = applyNodeChanges(changes, nodes); set({ nodes: newNodes }); }, onEdgesChange: (changes: EdgeChange[]) => { const { nodes, edges, selectedNodeId } = get(); const newEdges = applyEdgeChanges(changes, edges); set({ edges: newEdges }); }, addChildNode: (nodeId: string) => addNode(nodeId, 'child'), addSiblingNode: (nodeId: string) => addNode(nodeId, 'sibling'), undo: () => { const { history } = get(); console.log('[Undo] Starting undo operation'); if (history.past.length === 0) { console.log('[Undo] No past states available, undo skipped'); return; } const previous = history.past[history.past.length - 1]; const newPast = history.past.slice(0, -1); const newPresent = { ...history.present }; console.log('[Undo] Previous state:', previous); console.log('[Undo] New past length:', newPast.length); set({ nodes: previous.nodes, edges: previous.edges, selectedNodeId: previous.selectedNodeId, editingNodeId: previous.editingNodeId, history: { past: newPast, present: previous, future: [newPresent, ...history.future], }, }); console.log('[Undo] Operation completed'); }, redo: () => { const { history } = get(); console.log('[Redo] Starting redo operation'); if (history.future.length === 0) { console.log('[Redo] No future states available, redo skipped'); return; } const next = history.future[0]; const newFuture = history.future.slice(1); console.log('[Redo] Next state:', next); console.log('[Redo] New future length:', newFuture.length); set({ nodes: next.nodes, edges: next.edges, selectedNodeId: next.selectedNodeId, editingNodeId: next.editingNodeId, history: { past: [...history.past, history.present], present: next, future: newFuture, }, }); console.log('[Redo] Operation completed'); }, get canUndo() { return get().history.past.length > 0; }, get canRedo() { return get().history.future.length > 0; }, } }); export default useMindMapStore;