diff --git a/assets/js/hooks/Mapper/components/map/Map.tsx b/assets/js/hooks/Mapper/components/map/Map.tsx index e6065b31..5a5f0ffb 100644 --- a/assets/js/hooks/Mapper/components/map/Map.tsx +++ b/assets/js/hooks/Mapper/components/map/Map.tsx @@ -12,8 +12,6 @@ import ReactFlow, { OnSelectionChangeFunc, SelectionDragHandler, SelectionMode, - useEdgesState, - useNodesState, useReactFlow, } from 'reactflow'; import 'reactflow/dist/style.css'; @@ -21,7 +19,7 @@ import classes from './Map.module.scss'; import './styles/neon-theme.scss'; import './styles/eve-common.scss'; import { MapProvider, useMapState } from './MapProvider'; -import { useMapHandlers, useUpdateNodes } from './hooks'; +import { useNodesState, useEdgesState, useMapHandlers, useUpdateNodes } from './hooks'; import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts'; import { ContextMenuConnection, @@ -115,8 +113,8 @@ const MapComp = ({ isThickConnections, }: MapCompProps) => { const { getNode } = useReactFlow(); - const [nodes, , onNodesChange] = useNodesState(initialNodes); - const [edges, , onEdgesChange] = useEdgesState[]>(initialEdges); + const [nodes, , onNodesChange] = useNodesState>(initialNodes); + const [edges, , onEdgesChange] = useEdgesState>(initialEdges); useMapHandlers(refn, onSelectionChange); useUpdateNodes(nodes); diff --git a/assets/js/hooks/Mapper/components/map/hooks/index.ts b/assets/js/hooks/Mapper/components/map/hooks/index.ts index 184578ab..708fcae9 100644 --- a/assets/js/hooks/Mapper/components/map/hooks/index.ts +++ b/assets/js/hooks/Mapper/components/map/hooks/index.ts @@ -1,2 +1,3 @@ export * from './useMapHandlers'; export * from './useUpdateNodes'; +export * from './useNodesEdgesState'; diff --git a/assets/js/hooks/Mapper/components/map/hooks/useNodesEdgesState.ts b/assets/js/hooks/Mapper/components/map/hooks/useNodesEdgesState.ts new file mode 100644 index 00000000..7d9f4602 --- /dev/null +++ b/assets/js/hooks/Mapper/components/map/hooks/useNodesEdgesState.ts @@ -0,0 +1,36 @@ +import { useState, useCallback, type Dispatch, type SetStateAction } from 'react'; + +import { applyNodeChanges, applyEdgeChanges } from '../utils/changes'; +import { OnNodesChange, Edge, OnEdgesChange, Node } from 'reactflow'; + +/** + * Hook for managing the state of nodes - should only be used for prototyping / simple use cases. + * + * @public + * @param initialNodes + * @returns an array [nodes, setNodes, onNodesChange] + */ +export function useNodesState( + initialNodes: NodeType[], +): [NodeType[], Dispatch>, OnNodesChange] { + const [nodes, setNodes] = useState(initialNodes); + const onNodesChange: OnNodesChange = useCallback(changes => setNodes(nds => applyNodeChanges(changes, nds)), []); + + return [nodes, setNodes, onNodesChange]; +} + +/** + * Hook for managing the state of edges - should only be used for prototyping / simple use cases. + * + * @public + * @param initialEdges + * @returns an array [edges, setEdges, onEdgesChange] + */ +export function useEdgesState( + initialEdges: EdgeType[], +): [EdgeType[], Dispatch>, OnEdgesChange] { + const [edges, setEdges] = useState(initialEdges); + const onEdgesChange: OnEdgesChange = useCallback(changes => setEdges(eds => applyEdgeChanges(changes, eds)), []); + + return [edges, setEdges, onEdgesChange]; +} diff --git a/assets/js/hooks/Mapper/components/map/utils/changes.ts b/assets/js/hooks/Mapper/components/map/utils/changes.ts new file mode 100644 index 00000000..aafb0146 --- /dev/null +++ b/assets/js/hooks/Mapper/components/map/utils/changes.ts @@ -0,0 +1,174 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { EdgeChange, NodeChange, Node, Edge } from 'reactflow'; + +// This function applies changes to nodes or edges that are triggered by React Flow internally. +// When you drag a node for example, React Flow will send a position change update. +// This function then applies the changes and returns the updated elements. +function applyChanges(changes: any[], elements: any[]): any[] { + // we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows + if (changes.some(c => c.type === 'reset')) { + return changes.filter(c => c.type === 'reset').map(c => c.item); + } + + const updatedElements: any[] = []; + // By storing a map of changes for each element, we can a quick lookup as we + // iterate over the elements array! + const changesMap = new Map(); + const addItemChanges: any[] = []; + + for (const change of changes) { + if (change.type === 'add') { + addItemChanges.push(change); + continue; + } else if (change.type === 'remove' || change.type === 'replace') { + // For a 'remove' change we can safely ignore any other changes queued for + // the same element, it's going to be removed anyway! + changesMap.set(change.id, [change]); + } else { + const elementChanges = changesMap.get(change.id); + + if (elementChanges) { + // If we have some changes queued already, we can do a mutable update of + // that array and save ourselves some copying. + elementChanges.push(change); + } else { + changesMap.set(change.id, [change]); + } + } + } + + for (const element of elements) { + const changes = changesMap.get(element.id); + + // When there are no changes for an element we can just push it unmodified, + // no need to copy it. + if (!changes) { + updatedElements.push(element); + continue; + } + + // If we have a 'remove' change queued, it'll be the only change in the array + if (changes[0].type === 'remove') { + continue; + } + + if (changes[0].type === 'replace') { + updatedElements.push({ ...changes[0].item }); + continue; + } + + // For other types of changes, we want to start with a shallow copy of the + // object so React knows this element has changed. Sequential changes will + /// each _mutate_ this object, so there's only ever one copy. + const updatedElement = { ...element }; + + for (const change of changes) { + applyChange(change, updatedElement); + } + + updatedElements.push(updatedElement); + } + + // we need to wait for all changes to be applied before adding new items + // to be able to add them at the correct index + if (addItemChanges.length) { + addItemChanges.forEach(change => { + if (change.index !== undefined) { + updatedElements.splice(change.index, 0, { ...change.item }); + } else { + updatedElements.push({ ...change.item }); + } + }); + } + + return updatedElements; +} + +// Applies a single change to an element. This is a *mutable* update. +function applyChange(change: any, element: any): any { + switch (change.type) { + case 'select': { + element.selected = change.selected; + break; + } + + case 'position': { + if (typeof change.position !== 'undefined') { + element.position = change.position; + } + + if (typeof change.dragging !== 'undefined') { + element.dragging = change.dragging; + } + + break; + } + + case 'dimensions': { + if (typeof change.dimensions !== 'undefined') { + element.measured ??= {}; + element.measured.width = change.dimensions.width; + element.measured.height = change.dimensions.height; + + if (change.setAttributes) { + element.width = change.dimensions.width; + element.height = change.dimensions.height; + } + } + + if (typeof change.resizing === 'boolean') { + element.resizing = change.resizing; + } + + break; + } + } +} + +/** + * Drop in function that applies node changes to an array of nodes. + * @public + * @remarks Various events on the component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way. + If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. + * @param changes - Array of changes to apply + * @param nodes - Array of nodes to apply the changes to + * @returns Array of updated nodes + * @example + * const onNodesChange = useCallback( + (changes) => { + setNodes((oldNodes) => applyNodeChanges(changes, oldNodes)); + }, + [setNodes], + ); + + return ( + + ); + */ +export function applyNodeChanges(changes: NodeChange[], nodes: NodeType[]): NodeType[] { + return applyChanges(changes, nodes) as NodeType[]; +} + +/** + * Drop in function that applies edge changes to an array of edges. + * @public + * @remarks Various events on the component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way. + If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. + * @param changes - Array of changes to apply + * @param edges - Array of edge to apply the changes to + * @returns Array of updated edges + * @example + * const onEdgesChange = useCallback( + (changes) => { + setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges)); + }, + [setEdges], + ); + + return ( + + ); + */ +export function applyEdgeChanges(changes: EdgeChange[], edges: EdgeType[]): EdgeType[] { + return applyChanges(changes, edges) as EdgeType[]; +} diff --git a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useCommandsSystems.ts b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useCommandsSystems.ts index 98439fd8..b730751b 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useCommandsSystems.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useCommandsSystems.ts @@ -11,44 +11,50 @@ export const useCommandsSystems = () => { const { addSystemStatic } = useLoadSystemStatic({ systems: [] }); - const ref = useRef({ systems, update }); - ref.current = { systems, update }; + const ref = useRef({ systems, update, addSystemStatic }); + ref.current = { systems, update, addSystemStatic }; - const addSystems = useCallback( - (addSystems: CommandAddSystems) => { - addSystems.forEach(sys => { + const addSystems = useCallback((systemsToAdd: CommandAddSystems) => { + const { update, addSystemStatic, systems } = ref.current; + + systemsToAdd.forEach(sys => { + if (sys.system_static_info) { addSystemStatic(sys.system_static_info); - }); + } + }); - update({ - systems: [...ref.current.systems.filter(sys => !addSystems.some(x => sys.id === x.id)), ...addSystems], - }); - }, - [addSystemStatic, update], - ); + update( + { + systems: [...systems.filter(sys => !systemsToAdd.some(x => sys.id === x.id)), ...systemsToAdd], + }, + true, + ); + }, []); const removeSystems = useCallback((toRemove: CommandRemoveSystems) => { const { update, systems } = ref.current; - update({ - systems: systems.filter(x => !toRemove.includes(parseInt(x.id))), - }); + update( + { + systems: systems.filter(x => !toRemove.includes(parseInt(x.id))), + }, + true, + ); }, []); - const updateSystems = useCallback( - (systems: CommandUpdateSystems) => { - const out = ref.current.systems.map(current => { - const newSystem = systems.find(x => current.id === x.id); - if (!newSystem) { - return current; - } + const updateSystems = useCallback((updatedSystems: CommandUpdateSystems) => { + const { update, systems } = ref.current; - return newSystem; - }); + const out = systems.map(current => { + const newSystem = updatedSystems.find(x => current.id === x.id); + if (!newSystem) { + return current; + } - update({ systems: out }); - }, - [update], - ); + return newSystem; + }); + + update({ systems: out }, true); + }, []); return { addSystems, removeSystems, updateSystems }; };