From 8c3f54ec28873c512f07146f7ef7d4a601acf0f3 Mon Sep 17 00:00:00 2001 From: braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Wed, 22 Mar 2023 21:39:49 +0100 Subject: [PATCH] feat(core): add `onError` hook Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> --- packages/core/src/auto-imports.d.ts | 4 +- .../core/src/components/Nodes/NodeWrapper.ts | 2 +- packages/core/src/composables/useDrag.ts | 2 + packages/core/src/composables/useEdge.ts | 5 +- packages/core/src/composables/useEdgeHooks.ts | 18 +-- packages/core/src/composables/useNode.ts | 8 +- packages/core/src/composables/useNodeHooks.ts | 18 +-- .../src/composables/useUpdateNodePositions.ts | 4 +- packages/core/src/composables/useVueFlow.ts | 5 - .../container/EdgeRenderer/EdgeRenderer.vue | 3 +- .../container/NodeRenderer/NodeRenderer.vue | 2 +- .../core/src/container/Viewport/Viewport.vue | 4 +- packages/core/src/index.ts | 2 +- packages/core/src/store/actions.ts | 45 ++++++-- packages/core/src/store/getters.ts | 2 +- packages/core/src/store/hooks.ts | 105 +++++++++--------- packages/core/src/types/hooks.ts | 65 +---------- .../core/src/utils/createExtendedEventHook.ts | 49 ++++++++ packages/core/src/utils/drag.ts | 14 ++- packages/core/src/utils/errors.ts | 47 ++++++++ packages/core/src/utils/log.ts | 6 - packages/core/src/utils/store.ts | 20 ++-- 22 files changed, 250 insertions(+), 180 deletions(-) create mode 100644 packages/core/src/utils/createExtendedEventHook.ts create mode 100644 packages/core/src/utils/errors.ts diff --git a/packages/core/src/auto-imports.d.ts b/packages/core/src/auto-imports.d.ts index 65b43fe1..2d60918e 100644 --- a/packages/core/src/auto-imports.d.ts +++ b/packages/core/src/auto-imports.d.ts @@ -14,12 +14,13 @@ declare global { const EdgeId: typeof import('./context/index')['EdgeId'] const EdgeRef: typeof import('./context/index')['EdgeRef'] const EffectScope: typeof import('vue')['EffectScope'] + const ErrorCode: typeof import('./utils/errors')['ErrorCode'] const NodeId: typeof import('./context/index')['NodeId'] const NodeRef: typeof import('./context/index')['NodeRef'] const Slots: typeof import('./context/index')['Slots'] const Storage: typeof import('./composables/useVueFlow')['Storage'] const VueFlow: typeof import('./context/index')['VueFlow'] - const VueFlowError: typeof import('./utils/log')['VueFlowError'] + const VueFlowError: typeof import('./utils/errors')['VueFlowError'] const addEdge: typeof import('./utils/graph')['addEdge'] const addEdgeToStore: typeof import('./utils/store')['addEdgeToStore'] const applyChanges: typeof import('./utils/changes')['applyChanges'] @@ -44,6 +45,7 @@ declare global { const createAdditionChange: typeof import('./utils/changes')['createAdditionChange'] const createApp: typeof import('vue')['createApp'] const createEventHook: typeof import('@vueuse/core')['createEventHook'] + const createExtendedEventHook: typeof import('./utils/createExtendedEventHook')['createExtendedEventHook'] const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] const createGraphNodes: typeof import('./utils/store')['createGraphNodes'] const createHooks: typeof import('./store/hooks')['createHooks'] diff --git a/packages/core/src/components/Nodes/NodeWrapper.ts b/packages/core/src/components/Nodes/NodeWrapper.ts index b4da5d3a..d7ece650 100644 --- a/packages/core/src/components/Nodes/NodeWrapper.ts +++ b/packages/core/src/components/Nodes/NodeWrapper.ts @@ -235,7 +235,7 @@ const NodeWrapper = defineComponent({ nextPos.y = snapGrid[1] * Math.round(nextPos.y / snapGrid[1]) } - const { computedPosition, position } = calcNextPosition(node, nextPos, nodeExtent, parentNode) + const { computedPosition, position } = calcNextPosition(node, nextPos, emits.error, nodeExtent, parentNode) // only overwrite positions if there are changes when clamping if (node.computedPosition.x !== computedPosition.x || node.computedPosition.y !== computedPosition.y) { diff --git a/packages/core/src/composables/useDrag.ts b/packages/core/src/composables/useDrag.ts index f313198d..dbfb27e5 100644 --- a/packages/core/src/composables/useDrag.ts +++ b/packages/core/src/composables/useDrag.ts @@ -40,6 +40,7 @@ function useDrag(params: UseDragParams) { removeSelectedElements, addSelectedNodes, updateNodePositions, + emits, } = $(useVueFlow()) const { onStart, onDrag, onStop, el, disabled, id, selectable } = $(params) @@ -83,6 +84,7 @@ function useDrag(params: UseDragParams) { const { computedPosition } = calcNextPosition( n, nextPosition, + emits.error, nodeExtent, n.parentNode ? findNode(n.parentNode) : undefined, ) diff --git a/packages/core/src/composables/useEdge.ts b/packages/core/src/composables/useEdge.ts index 430610ed..db1151fe 100644 --- a/packages/core/src/composables/useEdge.ts +++ b/packages/core/src/composables/useEdge.ts @@ -1,4 +1,5 @@ import type { CustomEvent, ElementData } from '~/types' +import { VueFlowError } from '~/utils/errors' /** * Access an edge @@ -11,12 +12,12 @@ export default function useEdge(edgeId) if (!edge) { - throw new VueFlowError(`Edge with id ${edgeId} not found!`, 'useEdge') + emits.error(new VueFlowError(ErrorCode.EDGE_NOT_FOUND, edgeId)) } return { diff --git a/packages/core/src/composables/useEdgeHooks.ts b/packages/core/src/composables/useEdgeHooks.ts index d00b8b3c..b9735acb 100644 --- a/packages/core/src/composables/useEdgeHooks.ts +++ b/packages/core/src/composables/useEdgeHooks.ts @@ -1,15 +1,15 @@ import type { EdgeEventsEmit, EdgeEventsOn, GraphEdge, VueFlowStore } from '~/types' const createEdgeHooks = () => ({ - doubleClick: createEventHook(), - click: createEventHook(), - mouseEnter: createEventHook(), - mouseMove: createEventHook(), - mouseLeave: createEventHook(), - contextMenu: createEventHook(), - updateStart: createEventHook(), - update: createEventHook(), - updateEnd: createEventHook(), + doubleClick: createExtendedEventHook(), + click: createExtendedEventHook(), + mouseEnter: createExtendedEventHook(), + mouseMove: createExtendedEventHook(), + mouseLeave: createExtendedEventHook(), + contextMenu: createExtendedEventHook(), + updateStart: createExtendedEventHook(), + update: createExtendedEventHook(), + updateEnd: createExtendedEventHook(), }) export default function useEdgeHooks(edge: GraphEdge, emits: VueFlowStore['emits']): { emit: EdgeEventsEmit; on: EdgeEventsOn } { diff --git a/packages/core/src/composables/useNode.ts b/packages/core/src/composables/useNode.ts index 3084753e..e0d84d15 100644 --- a/packages/core/src/composables/useNode.ts +++ b/packages/core/src/composables/useNode.ts @@ -11,19 +11,19 @@ export default function useNode(nodeId) if (!node) { - throw new VueFlowError(`Node with id ${nodeId} not found!`, 'useNode') + emits.error(new VueFlowError(ErrorCode.NODE_NOT_FOUND, nodeId)) } return { id: nodeId, nodeEl, node, - parentNode: computed(() => (node.parentNode ? findNode(node.parentNode) : undefined)), - connectedEdges: computed(() => getConnectedEdges([node], getEdges.value)), + parentNode: computed(() => (node!.parentNode ? findNode(node!.parentNode) : undefined)), + connectedEdges: computed(() => getConnectedEdges([node!], getEdges.value)), } } diff --git a/packages/core/src/composables/useNodeHooks.ts b/packages/core/src/composables/useNodeHooks.ts index dd29add6..a2c1c918 100644 --- a/packages/core/src/composables/useNodeHooks.ts +++ b/packages/core/src/composables/useNodeHooks.ts @@ -1,15 +1,15 @@ import type { GraphNode, NodeEventsEmit, NodeEventsOn, VueFlowStore } from '~/types' const createNodeHooks = () => ({ - doubleClick: createEventHook(), - click: createEventHook(), - mouseEnter: createEventHook(), - mouseMove: createEventHook(), - mouseLeave: createEventHook(), - contextMenu: createEventHook(), - dragStart: createEventHook(), - drag: createEventHook(), - dragStop: createEventHook(), + doubleClick: createExtendedEventHook(), + click: createExtendedEventHook(), + mouseEnter: createExtendedEventHook(), + mouseMove: createExtendedEventHook(), + mouseLeave: createExtendedEventHook(), + contextMenu: createExtendedEventHook(), + dragStart: createExtendedEventHook(), + drag: createExtendedEventHook(), + dragStop: createExtendedEventHook(), }) export default function useNodeHooks(node: GraphNode, emits: VueFlowStore['emits']): { emit: NodeEventsEmit; on: NodeEventsOn } { diff --git a/packages/core/src/composables/useUpdateNodePositions.ts b/packages/core/src/composables/useUpdateNodePositions.ts index f50fb0b4..a7a3a91d 100644 --- a/packages/core/src/composables/useUpdateNodePositions.ts +++ b/packages/core/src/composables/useUpdateNodePositions.ts @@ -1,7 +1,8 @@ import type { NodeDragItem, XYPosition } from '~/types' function useUpdateNodePositions() { - const { getSelectedNodes, nodeExtent, updateNodePositions, findNode, snapGrid, snapToGrid, nodesDraggable } = useVueFlow() + const { getSelectedNodes, nodeExtent, updateNodePositions, findNode, snapGrid, snapToGrid, nodesDraggable, emits } = + useVueFlow() return (positionDiff: XYPosition, isShiftPressed = false) => { // by default a node moves 5px on each key press, or 20px if shift is pressed @@ -21,6 +22,7 @@ function useUpdateNodePositions() { const { computedPosition } = calcNextPosition( n, nextPosition, + emits.error, nodeExtent.value, n.parentNode ? findNode(n.parentNode) : undefined, ) diff --git a/packages/core/src/composables/useVueFlow.ts b/packages/core/src/composables/useVueFlow.ts index 5a68d276..3942e79a 100644 --- a/packages/core/src/composables/useVueFlow.ts +++ b/packages/core/src/composables/useVueFlow.ts @@ -123,11 +123,6 @@ export default (options?: FlowProps): VueFlowStore => { if (options) vueFlow.setState(options) } - /** - * Vue flow wasn't able to find any store instance - we can't proceed - */ - if (!vueFlow) throw new VueFlowError('Store instance not found.', 'useVueFlow') - // always provide a fresh instance into context on call if (scope) { provide(VueFlow, vueFlow) diff --git a/packages/core/src/container/EdgeRenderer/EdgeRenderer.vue b/packages/core/src/container/EdgeRenderer/EdgeRenderer.vue index f367b093..8b7f10c5 100644 --- a/packages/core/src/container/EdgeRenderer/EdgeRenderer.vue +++ b/packages/core/src/container/EdgeRenderer/EdgeRenderer.vue @@ -19,6 +19,7 @@ const { getEdgeTypes, elevateEdgesOnSelect, dimensions, + emits, } = $(useVueFlow()) const sourceNode = controlledComputed( @@ -75,7 +76,7 @@ function getType(type?: string, template?: GraphEdge['template']) { const slot = slots?.[`edge-${name}`] if (!slot) { - warn(`Edge type "${type}" not found and no edge-slot detected. Using fallback type "default".`) + emits.error(new VueFlowError(ErrorCode.EDGE_TYPE_MISSING, edgeType)) return false } diff --git a/packages/core/src/container/NodeRenderer/NodeRenderer.vue b/packages/core/src/container/NodeRenderer/NodeRenderer.vue index 2c28c1d2..6a467b85 100644 --- a/packages/core/src/container/NodeRenderer/NodeRenderer.vue +++ b/packages/core/src/container/NodeRenderer/NodeRenderer.vue @@ -68,7 +68,7 @@ function getType(type?: string, template?: GraphNode['template']) { const slot = slots?.[`node-${name}`] if (!slot) { - warn(`Node type "${type}" not found and no node-slot detected. Using fallback type "default".`) + emits.error(new VueFlowError(ErrorCode.NODE_TYPE_MISSING, nodeType)) return false } diff --git a/packages/core/src/container/Viewport/Viewport.vue b/packages/core/src/container/Viewport/Viewport.vue index ab358bcd..00f0347e 100644 --- a/packages/core/src/container/Viewport/Viewport.vue +++ b/packages/core/src/container/Viewport/Viewport.vue @@ -287,7 +287,9 @@ function setDimensions() { const { width, height } = getDimensions(viewportEl.value) - if (width === 0 || height === 0) warn('The Vue Flow parent container needs a width and a height to render the graph.') + if (width === 0 || height === 0) { + emits.error(new VueFlowError(ErrorCode.MISSING_VIEWPORT_DIMENSIONS)) + } dimensions.width = width || 500 dimensions.height = height || 500 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cdf86de6..31339add 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -71,6 +71,6 @@ export { default as useEdge } from './composables/useEdge' export { useGetPointerPosition } from './composables/useGetPointerPosition' -export { VueFlowError } from './utils/log' +export { VueFlowError, ErrorCode } from './utils/errors' export * from './types' diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index b4481268..530502ec 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -290,7 +290,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions { if (!state.initialized && !nextNodes.length) return - state.nodes = createGraphNodes(nextNodes, findNode, state.nodes) + state.nodes = createGraphNodes(nextNodes, state.nodes, findNode, state.hooks.error.trigger) } const setEdges: Actions['setEdges'] = (edges) => { @@ -315,9 +315,21 @@ export function useActions(state: State, getters: ComputedGetters): Actions { const missingSource = !sourceNode || typeof sourceNode === 'undefined' const missingTarget = !targetNode || typeof targetNode === 'undefined' - if (missingSource) warn(`Couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`) - if (missingTarget) warn(`Couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`) - if (missingSource || missingTarget) return res + if (missingSource && missingTarget) { + state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_TARGET_MISSING, edge.id, edge.source, edge.target)) + } else { + if (missingSource) { + state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_MISSING, edge.id, edge.source)) + } + + if (missingTarget) { + state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_TARGET_MISSING, edge.id, edge.target)) + } + } + + if (missingSource || missingTarget) { + return res + } const storedEdge = getters.getEdge.value(edge.id) @@ -343,7 +355,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions { const addNodes: Actions['addNodes'] = (nodes) => { const nextNodes = nodes instanceof Function ? nodes(state.nodes) : nodes - const graphNodes = createGraphNodes(nextNodes, findNode, state.nodes) + const graphNodes = createGraphNodes(nextNodes, state.nodes, findNode, state.hooks.error.trigger) const changes = graphNodes.map(createAdditionChange) @@ -370,6 +382,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions { ...state.defaultEdgeOptions, }, state.edges, + state.hooks.error.trigger, ) if (edge) { @@ -379,9 +392,21 @@ export function useActions(state: State, getters: ComputedGetters): Actions { const missingSource = !sourceNode || typeof sourceNode === 'undefined' const missingTarget = !targetNode || typeof targetNode === 'undefined' - if (missingSource) warn(`Couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`) - if (missingTarget) warn(`Couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`) - if (missingTarget || missingSource) return acc + if (missingSource && missingTarget) { + state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_TARGET_MISSING, edge.id, edge.source, edge.target)) + } else { + if (missingSource) { + state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_MISSING, edge.id, edge.source)) + } + + if (missingTarget) { + state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_TARGET_MISSING, edge.id, edge.target)) + } + } + + if (missingSource || missingTarget) { + return acc + } acc.push( createAdditionChange({ @@ -445,7 +470,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions { } const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection, shouldReplaceId = true) => - updateEdgeAction(oldEdge, newConnection, state.edges, findEdge, shouldReplaceId) + updateEdgeAction(oldEdge, newConnection, state.edges, findEdge, shouldReplaceId, state.hooks.error.trigger) const applyNodeChanges: Actions['applyNodeChanges'] = (changes) => applyChanges(changes, state.nodes) @@ -468,8 +493,6 @@ export function useActions(state: State, getters: ComputedGetters): Actions { state.connectionPosition = position state.connectionStartHandle.result = result state.connectionStatus = status - } else { - warn('Cannot update connection as it has not been started') } } diff --git a/packages/core/src/store/getters.ts b/packages/core/src/store/getters.ts index 94c7f836..4c2ec445 100644 --- a/packages/core/src/store/getters.ts +++ b/packages/core/src/store/getters.ts @@ -60,7 +60,7 @@ export function useGetters(state: State): ComputedGetters { if (!source || !target) { state.edges = state.edges.filter((edge) => edge.id !== e.id) - warn(`Orphaned edge ${e.id} removed.`) + state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_ORPHANED, e.id)) return } diff --git a/packages/core/src/store/hooks.ts b/packages/core/src/store/hooks.ts index 807afbc8..fd18d093 100644 --- a/packages/core/src/store/hooks.ts +++ b/packages/core/src/store/hooks.ts @@ -1,64 +1,65 @@ import type { Ref } from 'vue' -import type { Emits, FlowHooks } from '~/types' +import type { FlowHooks } from '~/types' // flow event hooks export const createHooks = (): FlowHooks => ({ - edgesChange: createEventHook(), - nodesChange: createEventHook(), - nodeDoubleClick: createEventHook(), - nodeClick: createEventHook(), - nodeMouseEnter: createEventHook(), - nodeMouseMove: createEventHook(), - nodeMouseLeave: createEventHook(), - nodeContextMenu: createEventHook(), - nodeDragStart: createEventHook(), - nodeDrag: createEventHook(), - nodeDragStop: createEventHook(), - nodesInitialized: createEventHook(), - miniMapNodeClick: createEventHook(), - miniMapNodeDoubleClick: createEventHook(), - miniMapNodeMouseEnter: createEventHook(), - miniMapNodeMouseMove: createEventHook(), - miniMapNodeMouseLeave: createEventHook(), - connect: createEventHook(), - connectStart: createEventHook(), - connectEnd: createEventHook(), - paneReady: createEventHook(), - move: createEventHook(), - moveStart: createEventHook(), - moveEnd: createEventHook(), - selectionDragStart: createEventHook(), - selectionDrag: createEventHook(), - selectionDragStop: createEventHook(), - selectionContextMenu: createEventHook(), - selectionStart: createEventHook(), - selectionEnd: createEventHook(), - viewportChangeStart: createEventHook(), - viewportChange: createEventHook(), - viewportChangeEnd: createEventHook(), - paneScroll: createEventHook(), - paneClick: createEventHook(), - paneContextMenu: createEventHook(), - paneMouseEnter: createEventHook(), - paneMouseMove: createEventHook(), - paneMouseLeave: createEventHook(), - edgeContextMenu: createEventHook(), - edgeMouseEnter: createEventHook(), - edgeMouseMove: createEventHook(), - edgeMouseLeave: createEventHook(), - edgeDoubleClick: createEventHook(), - edgeClick: createEventHook(), - edgeUpdateStart: createEventHook(), - edgeUpdate: createEventHook(), - edgeUpdateEnd: createEventHook(), - updateNodeInternals: createEventHook(), + edgesChange: createExtendedEventHook(), + nodesChange: createExtendedEventHook(), + nodeDoubleClick: createExtendedEventHook(), + nodeClick: createExtendedEventHook(), + nodeMouseEnter: createExtendedEventHook(), + nodeMouseMove: createExtendedEventHook(), + nodeMouseLeave: createExtendedEventHook(), + nodeContextMenu: createExtendedEventHook(), + nodeDragStart: createExtendedEventHook(), + nodeDrag: createExtendedEventHook(), + nodeDragStop: createExtendedEventHook(), + nodesInitialized: createExtendedEventHook(), + miniMapNodeClick: createExtendedEventHook(), + miniMapNodeDoubleClick: createExtendedEventHook(), + miniMapNodeMouseEnter: createExtendedEventHook(), + miniMapNodeMouseMove: createExtendedEventHook(), + miniMapNodeMouseLeave: createExtendedEventHook(), + connect: createExtendedEventHook(), + connectStart: createExtendedEventHook(), + connectEnd: createExtendedEventHook(), + paneReady: createExtendedEventHook(), + move: createExtendedEventHook(), + moveStart: createExtendedEventHook(), + moveEnd: createExtendedEventHook(), + selectionDragStart: createExtendedEventHook(), + selectionDrag: createExtendedEventHook(), + selectionDragStop: createExtendedEventHook(), + selectionContextMenu: createExtendedEventHook(), + selectionStart: createExtendedEventHook(), + selectionEnd: createExtendedEventHook(), + viewportChangeStart: createExtendedEventHook(), + viewportChange: createExtendedEventHook(), + viewportChangeEnd: createExtendedEventHook(), + paneScroll: createExtendedEventHook(), + paneClick: createExtendedEventHook(), + paneContextMenu: createExtendedEventHook(), + paneMouseEnter: createExtendedEventHook(), + paneMouseMove: createExtendedEventHook(), + paneMouseLeave: createExtendedEventHook(), + edgeContextMenu: createExtendedEventHook(), + edgeMouseEnter: createExtendedEventHook(), + edgeMouseMove: createExtendedEventHook(), + edgeMouseLeave: createExtendedEventHook(), + edgeDoubleClick: createExtendedEventHook(), + edgeClick: createExtendedEventHook(), + edgeUpdateStart: createExtendedEventHook(), + edgeUpdate: createExtendedEventHook(), + edgeUpdateEnd: createExtendedEventHook(), + updateNodeInternals: createExtendedEventHook(), + error: createExtendedEventHook((err) => warn(err.message)), }) -export function useHooks(emit: Emits, hooks: Ref) { +export function useHooks(emit: (...args: any[]) => void, hooks: Ref) { onBeforeMount(() => { for (const [key, value] of Object.entries(hooks.value)) { const listener = (data: any) => { - emit(key as Emits extends (event: infer Event) => void ? Event : never, data) + emit(key, data) } value.on(listener) diff --git a/packages/core/src/types/hooks.ts b/packages/core/src/types/hooks.ts index af63208c..533bb751 100644 --- a/packages/core/src/types/hooks.ts +++ b/packages/core/src/types/hooks.ts @@ -6,7 +6,7 @@ import type { Connection, OnConnectStartParams } from './connection' import type { ViewportTransform } from './zoom' import type { EdgeChange, NodeChange } from './changes' import type { VueFlowStore } from './store' -import type { FlowElements } from './flow' +import type { VueFlowError } from '~/utils/errors' export type MouseTouchEvent = MouseEvent | TouchEvent @@ -86,68 +86,7 @@ export interface FlowEvents { edgeUpdateStart: EdgeMouseEvent edgeUpdate: EdgeUpdateEvent edgeUpdateEnd: EdgeMouseEvent -} - -export interface Emits { - (event: 'nodesChange', changes: NodeChange[]): void - (event: 'edgesChange', changes: EdgeChange[]): void - (event: 'nodeDoubleClick', nodeMouseEvent: NodeMouseEvent): void - (event: 'nodeClick', nodeMouseEvent: NodeMouseEvent): void - (event: 'nodeMouseEnter', nodeMouseEvent: NodeMouseEvent): void - (event: 'nodeMouseMove', nodeMouseEvent: NodeMouseEvent): void - (event: 'nodeMouseLeave', nodeMouseEvent: NodeMouseEvent): void - (event: 'nodeContextMenu', nodeMouseEvent: NodeMouseEvent): void - (event: 'nodeDragStart', nodeDragEvent: NodeDragEvent): void - (event: 'nodeDrag', nodeDragEvent: NodeDragEvent): void - (event: 'nodeDragStop', nodeDragEvent: NodeDragEvent): void - (event: 'nodesInitialized'): void - (event: 'miniMapNodeClick', nodeMouseEvent: NodeMouseEvent): void - (event: 'miniMapNodeDoubleClick', nodeMouseEvent: NodeMouseEvent): void - (event: 'miniMapNodeMouseEnter', nodeMouseEvent: NodeMouseEvent): void - (event: 'miniMapNodeMouseMove', nodeMouseEvent: NodeMouseEvent): void - (event: 'miniMapNodeMouseLeave', nodeMouseEvent: NodeMouseEvent): void - (event: 'connect', connectionEvent: Connection): void - ( - event: 'connectStart', - connectionEvent: { - event?: MouseEvent - } & OnConnectStartParams, - ): void - (event: 'connectEnd', connectionEvent?: MouseEvent): void - (event: 'moveStart', moveEvent: { event: D3ZoomEvent; flowTransform: ViewportTransform }): void - (event: 'move', moveEvent: { event: D3ZoomEvent; flowTransform: ViewportTransform }): void - (event: 'moveEnd', moveEvent: { event: D3ZoomEvent; flowTransform: ViewportTransform }): void - (event: 'selectionDragStart', selectionEvent: NodeDragEvent): void - (event: 'selectionDrag', selectionEvent: NodeDragEvent): void - (event: 'selectionDragStop', selectionEvent: NodeDragEvent): void - (event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void - (event: 'selectionStart', selectionEvent: MouseEvent): void - (event: 'selectionEnd', selectionEvent: MouseEvent): void - (event: 'viewportChangeStart', viewport: ViewportTransform): void - (event: 'viewportChange', viewport: ViewportTransform): void - (event: 'viewportChangeEnd', viewport: ViewportTransform): void - (event: 'paneReady', paneEvent: VueFlowStore): void - (event: 'paneScroll', paneEvent: WheelEvent | undefined): void - (event: 'paneClick', paneEvent: MouseEvent): void - (event: 'paneContextMenu', paneEvent: MouseEvent): void - (event: 'paneMouseEnter', paneEvent: MouseEvent): void - (event: 'paneMouseMove', paneEvent: MouseEvent): void - (event: 'paneMouseLeave', paneEvent: MouseEvent): void - (event: 'edgeContextMenu', edgeMouseEvent: EdgeMouseEvent): void - (event: 'edgeMouseEnter', edgeMouseEvent: EdgeMouseEvent): void - (event: 'edgeMouseMove', edgeMouseEvent: EdgeMouseEvent): void - (event: 'edgeMouseLeave', edgeMouseEvent: EdgeMouseEvent): void - (event: 'edgeDoubleClick', edgeMouseEvent: EdgeMouseEvent): void - (event: 'edgeClick', edgeMouseEvent: EdgeMouseEvent): void - (event: 'edgeUpdateStart', edgeMouseEvent: EdgeMouseEvent): void - (event: 'edgeUpdate', edgeUpdateEvent: EdgeUpdateEvent): void - (event: 'edgeUpdateEnd', edgeMouseEvent: EdgeMouseEvent): void - (event: 'updateNodeInternals'): void - - /** v-model event definitions */ - (event: 'update:modelValue', value: FlowElements): void - (event: 'update:nodes', value: GraphNode[]): void - (event: 'update:edges', value: GraphEdge[]): void + error: VueFlowError } export type FlowHooks = Readonly<{ diff --git a/packages/core/src/utils/createExtendedEventHook.ts b/packages/core/src/utils/createExtendedEventHook.ts new file mode 100644 index 00000000..f6ab92ab --- /dev/null +++ b/packages/core/src/utils/createExtendedEventHook.ts @@ -0,0 +1,49 @@ +import type { Ref } from 'vue' +import type { EventHook } from '@vueuse/core' + +/** + * Source code taken from https://github.com/vueuse/vueuse/blob/main/packages/shared/createEventHook/index.ts + * + * Modified to be able to check if there are any event listeners + */ +export interface EventHookExtended extends EventHook { + fns: Ref void>> +} + +export function createExtendedEventHook(defaultHandler: (param: T) => void = () => {}): EventHookExtended { + const fns = ref(new Set<(param: T) => void>()) + + if (defaultHandler) { + fns.value.add(defaultHandler) + } + + const off = (fn: (param: T) => void) => { + fns.value.delete(fn) + } + + const on = (fn: (param: T) => void) => { + if (fns.value.has(defaultHandler)) { + fns.value.delete(defaultHandler) + } + + fns.value.add(fn) + const offFn = () => off(fn) + + tryOnScopeDispose(offFn) + + return { + off: offFn, + } + } + + const trigger = (param: T) => { + return Promise.all(Array.from(fns.value).map((fn) => fn(param))) + } + + return { + on, + off, + trigger, + fns, + } +} diff --git a/packages/core/src/utils/drag.ts b/packages/core/src/utils/drag.ts index 9f1b4384..672e2f3b 100644 --- a/packages/core/src/utils/drag.ts +++ b/packages/core/src/utils/drag.ts @@ -1,5 +1,5 @@ import { isNumber } from '@vueuse/shared' -import type { Actions, CoordinateExtent, ExtendedParentExtent, GraphNode, NodeDragItem, XYPosition } from '~/types' +import type { Actions, CoordinateExtent, ExtendedParentExtent, GraphNode, NodeDragItem, State, XYPosition } from '~/types' export function hasSelector(target: Element, selector: string, node: Element): boolean { let current = target @@ -97,7 +97,12 @@ function getParentExtent( return false } -export function getExtent(item: T, extent?: CoordinateExtent, parent?: GraphNode) { +export function getExtent( + item: T, + onError: State['hooks']['error']['trigger'], + extent?: CoordinateExtent, + parent?: GraphNode, +) { let currentExtent = item.extent || extent if (item.extent === 'parent' || (!Array.isArray(item.extent) && item.extent?.range === 'parent')) { @@ -108,7 +113,7 @@ export function getExtent(item: T, extent?: currentExtent = parentExtent } } else { - warn('Only child nodes can use a parent extent.') + onError(new VueFlowError(ErrorCode.NODE_EXTENT_INVALID, item.id)) currentExtent = extent } @@ -127,10 +132,11 @@ export function getExtent(item: T, extent?: export function calcNextPosition( node: GraphNode | NodeDragItem, nextPosition: XYPosition, + onError: State['hooks']['error']['trigger'], nodeExtent?: CoordinateExtent, parentNode?: GraphNode, ) { - const extent = getExtent(node, nodeExtent, parentNode) + const extent = getExtent(node, onError, nodeExtent, parentNode) const clampedPos = clampPosition(nextPosition, extent) diff --git a/packages/core/src/utils/errors.ts b/packages/core/src/utils/errors.ts new file mode 100644 index 00000000..a28b63b0 --- /dev/null +++ b/packages/core/src/utils/errors.ts @@ -0,0 +1,47 @@ +export enum ErrorCode { + MISSING_VIEWPORT_DIMENSIONS = 'MISSING_VIEWPORT_DIMENSIONS', + NODE_NOT_FOUND = 'NODE_NOT_FOUND', + NODE_MISSING_PARENT = 'NODE_MISSING_PARENT', + NODE_TYPE_MISSING = 'NODE_TYPE_MISSING', + NODE_EXTENT_INVALID = 'NODE_EXTENT_INVALID', + EDGE_INVALID = 'EDGE_INVALID', + EDGE_NOT_FOUND = 'EDGE_NOT_FOUND', + EDGE_SOURCE_MISSING = 'EDGE_SOURCE_MISSING', + EDGE_TARGET_MISSING = 'EDGE_TARGET_MISSING', + EDGE_TYPE_MISSING = 'EDGE_TYPE_MISSING', + EDGE_SOURCE_TARGET_SAME = 'EDGE_SOURCE_TARGET_SAME', + EDGE_SOURCE_TARGET_MISSING = 'EDGE_SOURCE_TARGET_MISSING', + EDGE_ORPHANED = 'EDGE_ORPHANED', +} + +const messages = { + [ErrorCode.MISSING_VIEWPORT_DIMENSIONS]: () => 'The Vue Flow parent container needs a width and a height to render the graph', + [ErrorCode.NODE_NOT_FOUND]: (id: string) => `Node not found\nNode: ${id}`, + [ErrorCode.NODE_MISSING_PARENT]: (id: string, parentId: string) => `Node is missing a parent\nNode: ${id}\nParent: ${parentId}`, + [ErrorCode.NODE_TYPE_MISSING]: (type: string) => `Node type is missing\nType: ${type}`, + [ErrorCode.NODE_EXTENT_INVALID]: (id: string) => `Only child nodes can use a parent extent\nNode: ${id}`, + [ErrorCode.EDGE_INVALID]: (id: string) => `An edge needs a source and a target\nEdge: ${id}`, + [ErrorCode.EDGE_SOURCE_MISSING]: (id: string, source: string) => `Edge source is missing\nEdge: ${id} \nSource: ${source}`, + [ErrorCode.EDGE_TARGET_MISSING]: (id: string, target: string) => `Edge target is missing\nEdge: ${id} \nTarget: ${target}`, + [ErrorCode.EDGE_TYPE_MISSING]: (type: string) => `Edge type is missing\nType: ${type}`, + [ErrorCode.EDGE_SOURCE_TARGET_SAME]: (id: string, source: string, target: string) => + `Edge source and target are the same\nEdge: ${id} \nSource: ${source} \nTarget: ${target}`, + [ErrorCode.EDGE_SOURCE_TARGET_MISSING]: (id: string, source: string, target: string) => + `Edge source or target is missing\nEdge: ${id} \nSource: ${source} \nTarget: ${target}`, + [ErrorCode.EDGE_ORPHANED]: (id: string) => + `Edge was orphaned (suddenly missing source or target) and has been removed\nEdge: ${id}`, + [ErrorCode.EDGE_NOT_FOUND]: (id: string) => `Edge not found\nEdge: ${id}`, +} as const + +export class VueFlowError extends Error { + code: T + constructor( + code: T, + ...args: (typeof messages)[T] extends (...args: any[]) => string ? Parameters<(typeof messages)[T]> : never + ) { + // @ts-expect-error - TS doesn't know that the message is a key of messages + super(messages[code]?.(...args)) + + this.code = code + } +} diff --git a/packages/core/src/utils/log.ts b/packages/core/src/utils/log.ts index 412d6dd1..baf150a9 100644 --- a/packages/core/src/utils/log.ts +++ b/packages/core/src/utils/log.ts @@ -1,11 +1,5 @@ const productionEnvs = ['production', 'prod'] -export class VueFlowError extends Error { - constructor(message: string, scope?: string) { - super(`[Vue Flow]: ${scope ? `${scope} - ` : ''}${message}`) - } -} - export function warn(message: string, ...args: any[]) { if (!productionEnvs.includes(__ENV__ || '')) { console.warn(`[Vue Flow]: ${message}`, ...args) diff --git a/packages/core/src/utils/store.ts b/packages/core/src/utils/store.ts index 61ddb759..73368edb 100644 --- a/packages/core/src/utils/store.ts +++ b/packages/core/src/utils/store.ts @@ -1,10 +1,10 @@ -import type { Actions, Connection, Edge, GraphEdge, GraphNode, Node } from '~/types' +import type { Actions, Connection, Edge, GraphEdge, GraphNode, Node, State } from '~/types' export const isDef = (val: T): val is NonNullable => typeof unref(val) !== 'undefined' -export function addEdgeToStore(edgeParams: Edge | Connection, edges: Edge[]) { +export function addEdgeToStore(edgeParams: Edge | Connection, edges: Edge[], onError: State['hooks']['error']['trigger']) { if (!edgeParams.source || !edgeParams.target) { - warn("Can't create edge. An edge needs a source and a target.") + onError(new VueFlowError(ErrorCode.EDGE_INVALID, (edgeParams as Edge).id)) return false } @@ -31,16 +31,17 @@ export function updateEdgeAction( edges: GraphEdge[], findEdge: Actions['findEdge'], shouldReplaceId: boolean, + onError: State['hooks']['error']['trigger'], ) { if (!newConnection.source || !newConnection.target) { - warn("Can't create new edge. An edge needs a source and a target.") + onError(new VueFlowError(ErrorCode.EDGE_INVALID, edge.id)) return false } const foundEdge = findEdge(edge.id) if (!foundEdge) { - warn(`The old edge with id=${edge.id} does not exist.`) + onError(new VueFlowError(ErrorCode.EDGE_NOT_FOUND, edge.id)) return false } @@ -60,7 +61,12 @@ export function updateEdgeAction( return newEdge } -export function createGraphNodes(nodes: Node[], findNode: Actions['findNode'], currGraphNodes: GraphNode[]) { +export function createGraphNodes( + nodes: Node[], + currGraphNodes: GraphNode[], + findNode: Actions['findNode'], + onError: State['hooks']['error']['trigger'], +) { const parentNodes: Record = {} const graphNodes = nodes.map((node) => { @@ -82,7 +88,7 @@ export function createGraphNodes(nodes: Node[], findNode: Actions['findNode'], c const parentNode = nextNodes.find((n) => n.id === node.parentNode) if (node.parentNode && !parentNode) { - warn(`Parent node ${node.parentNode} not found`) + onError(new VueFlowError(ErrorCode.NODE_MISSING_PARENT, node.id, node.parentNode)) } if (node.parentNode || parentNodes[node.id]) {