@stonecrop/node-editor 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/assets/index.css +430 -0
  2. package/dist/node-editor.js +821 -644
  3. package/dist/node-editor.js.map +1 -1
  4. package/dist/src/components/EditableEdge.vue.d.ts +11 -0
  5. package/dist/src/components/EditableEdge.vue.d.ts.map +1 -0
  6. package/dist/src/components/EditableNode.vue.d.ts +10 -0
  7. package/dist/src/components/EditableNode.vue.d.ts.map +1 -0
  8. package/dist/src/components/NodeEditor.vue.d.ts +14 -0
  9. package/dist/src/components/NodeEditor.vue.d.ts.map +1 -0
  10. package/dist/src/components/SelfLoopEdge.vue.d.ts +11 -0
  11. package/dist/src/components/SelfLoopEdge.vue.d.ts.map +1 -0
  12. package/dist/src/components/StateEditor.vue.d.ts +69 -0
  13. package/dist/src/components/StateEditor.vue.d.ts.map +1 -0
  14. package/dist/{tsdoc-metadata.json → src/tsdoc-metadata.json} +1 -1
  15. package/package.json +35 -28
  16. package/dist/node-editor.css +0 -1
  17. package/dist/node-editor.d.ts +0 -60
  18. package/dist/node_editor.tsbuildinfo +0 -1
  19. package/dist/src/index.js +0 -16
  20. package/dist/src/types/index.js +0 -0
  21. package/dist/src/utils/autoLayout.js +0 -50
  22. package/dist/src/utils/stateTransforms.js +0 -148
  23. package/src/components/EditableEdge.vue +0 -92
  24. package/src/components/EditableNode.vue +0 -65
  25. package/src/components/NodeEditor.vue +0 -397
  26. package/src/components/SelfLoopEdge.vue +0 -88
  27. package/src/components/StateEditor.vue +0 -46
  28. package/src/index.ts +0 -22
  29. package/src/shims-vue.d.ts +0 -5
  30. package/src/types/index.ts +0 -33
  31. package/src/utils/autoLayout.ts +0 -67
  32. package/src/utils/stateTransforms.ts +0 -164
@@ -1,60 +0,0 @@
1
- import { App } from 'vue';
2
- import { Element as Element_2 } from '@vue-flow/core';
3
- import { Elements } from '@vue-flow/core';
4
- import NodeEditor from './components/NodeEditor.vue';
5
- import { Position } from '@vue-flow/core';
6
- import StateEditor from './components/StateEditor.vue';
7
- import { XYPosition } from '@vue-flow/core';
8
-
9
- /**
10
- * Flow element
11
- * @public
12
- */
13
- export declare type FlowElement = Element_2<{
14
- hasInput?: boolean;
15
- hasOutput?: boolean;
16
- }, {
17
- hasInput?: boolean;
18
- hasOutput?: boolean;
19
- actionKey?: string;
20
- }>;
21
-
22
- /**
23
- * Flow elements
24
- * @public
25
- */
26
- export declare type FlowElements = Elements<{
27
- hasInput?: boolean;
28
- hasOutput?: boolean;
29
- }, {
30
- hasInput?: boolean;
31
- hasOutput?: boolean;
32
- actionKey?: string;
33
- }>;
34
-
35
- /**
36
- * Install all Node Editor components
37
- * @param app - Vue app instance
38
- * @public
39
- */
40
- export declare function install(app: App): void;
41
-
42
- /**
43
- * Node layout
44
- * @public
45
- */
46
- export declare type Layout = {
47
- [key: string]: {
48
- position?: XYPosition;
49
- targetPosition?: Position;
50
- sourcePosition?: Position;
51
- };
52
- };
53
-
54
- export { NodeEditor }
55
-
56
- export { Position }
57
-
58
- export { StateEditor }
59
-
60
- export { }
@@ -1 +0,0 @@
1
- {"version":"6.0.3"}
package/dist/src/index.js DELETED
@@ -1,16 +0,0 @@
1
- import NodeEditor from './components/NodeEditor.vue';
2
- import StateEditor from './components/StateEditor.vue';
3
- // `Layout` types its handle placement as this enum, so a consumer cannot fill that field without
4
- // naming it. It is an enum rather than a string union, which makes the obvious `'left'` a type
5
- // error, and @vue-flow/core is a transitive dependency a consumer should not have to add.
6
- export { Position } from '@vue-flow/core';
7
- /**
8
- * Install all Node Editor components
9
- * @param app - Vue app instance
10
- * @public
11
- */
12
- function install(app) {
13
- app.component('NodeEditor', NodeEditor);
14
- app.component('StateEditor', StateEditor);
15
- }
16
- export { install, NodeEditor, StateEditor };
File without changes
@@ -1,50 +0,0 @@
1
- import { Graph, layout } from '@dagrejs/dagre';
2
- /**
3
- * Compute node positions for a workflow graph with dagre's layered (Sugiyama) algorithm.
4
- *
5
- * Pure and synchronous: takes VueFlow elements (nodes + edges) and returns a new elements array with
6
- * each node's `position` replaced by a laid-out coordinate; edges and every other node field are
7
- * passed through untouched. Used two ways — as the seed for an un-arranged workflow (fixed default
8
- * dimensions, no DOM) and behind the toolbar button (measured `dimensions`).
9
- *
10
- * Self-loop edges (`source === target`) are skipped: dagre doesn't rank them, and they're reserved
11
- * for the separate mutate-in-place self-transition feature.
12
- * @public
13
- */
14
- export function autoLayout(elements, options = {}) {
15
- const { rankdir = 'LR', nodeWidth = 160, nodeHeight = 40, dimensions } = options;
16
- const sizeOf = (id) => dimensions?.[id] ?? { width: nodeWidth, height: nodeHeight };
17
- // Nodes are the elements without a `source` (the same discriminator flowElementsToStates uses).
18
- const nodeIds = [];
19
- for (const el of elements) {
20
- if (!('source' in el))
21
- nodeIds.push(el.id);
22
- }
23
- if (nodeIds.length === 0)
24
- return [...elements];
25
- const graph = new Graph();
26
- graph.setGraph({ rankdir, nodesep: 40, ranksep: 80, edgesep: 20 });
27
- graph.setDefaultEdgeLabel(() => ({}));
28
- for (const id of nodeIds) {
29
- const { width, height } = sizeOf(id);
30
- graph.setNode(id, { width, height });
31
- }
32
- for (const el of elements) {
33
- if (!('source' in el))
34
- continue;
35
- if (el.source === el.target)
36
- continue;
37
- graph.setEdge(el.source, el.target);
38
- }
39
- layout(graph);
40
- // dagre reports each node by its CENTER; VueFlow positions nodes by their TOP-LEFT corner.
41
- return elements.map(el => {
42
- if ('source' in el)
43
- return el;
44
- const center = graph.node(el.id);
45
- if (!center)
46
- return el;
47
- const { width, height } = sizeOf(el.id);
48
- return { ...el, position: { x: center.x - width / 2, y: center.y - height / 2 } };
49
- });
50
- }
@@ -1,148 +0,0 @@
1
- import { Position } from '@vue-flow/core';
2
- export function statesToFlowElements(workflow, layout) {
3
- const { states = [], actions = {} } = workflow;
4
- const edges = [];
5
- const nodes = [];
6
- for (const [actionKey, actionDef] of Object.entries(actions)) {
7
- // Stateless commands (print/email) have no graph presence; anything graph-owned needs states.
8
- if (actionDef.stateless || !actionDef.allowedStates?.length)
9
- continue;
10
- // A self-transition (mutate-in-place `save`) renders as a self-loop on each allowed state:
11
- // `source === target`, tagged `selfloop` so NodeEditor routes it to the arc renderer.
12
- if (actionDef.selfTransition) {
13
- for (const source of actionDef.allowedStates) {
14
- edges.push({
15
- id: `${actionKey}-${source}`,
16
- source,
17
- target: source,
18
- label: actionDef.label ?? actionKey,
19
- data: { actionKey },
20
- animated: true,
21
- type: 'selfloop',
22
- interactionWidth: 40,
23
- });
24
- }
25
- continue;
26
- }
27
- // A cross-state transition needs a target; a malformed action with neither is skipped.
28
- if (!actionDef.nextState)
29
- continue;
30
- for (const source of actionDef.allowedStates) {
31
- edges.push({
32
- id: `${actionKey}-${source}`,
33
- source,
34
- target: actionDef.nextState,
35
- // The edge paints the human display label; its identity (the action key) rides in
36
- // `data.actionKey`, so relabeling the edge renames the action without re-keying it.
37
- label: actionDef.label ?? actionKey,
38
- data: { actionKey },
39
- animated: true,
40
- type: 'smoothstep',
41
- interactionWidth: 40,
42
- });
43
- }
44
- }
45
- for (let index = 0; index < states.length; index++) {
46
- const state = states[index];
47
- const node = {
48
- id: state,
49
- label: state,
50
- position: layout?.[state]?.position ?? { x: 200 * index, y: 100 },
51
- targetPosition: layout?.[state]?.targetPosition ?? Position.Left,
52
- sourcePosition: layout?.[state]?.sourcePosition ?? Position.Right,
53
- };
54
- // Every state renders identically — both handles, no start-state styling — so any transition
55
- // (including a returning `reject`/`reopen` into the initial state) is authorable and a
56
- // self-loop can anchor both ends. Marking an entry state is deferred (YAGNI) until needed.
57
- nodes.push(node);
58
- }
59
- return [...edges, ...nodes];
60
- }
61
- export function flowElementsToStates(nextElements, existingWorkflow) {
62
- const idToLabel = {};
63
- const nextLayout = {};
64
- const stateNames = [];
65
- for (const el of nextElements) {
66
- if ('source' in el)
67
- continue;
68
- const label = typeof el.label === 'string' ? el.label : el.id;
69
- idToLabel[el.id] = label;
70
- stateNames.push(label);
71
- if (el.position) {
72
- nextLayout[label] = {
73
- position: el.position,
74
- ...(el.targetPosition !== undefined && { targetPosition: el.targetPosition }),
75
- ...(el.sourcePosition !== undefined && { sourcePosition: el.sourcePosition }),
76
- };
77
- }
78
- }
79
- // Group directed edges by their stable action key. A round-tripped edge carries the key in
80
- // `data.actionKey`; a freshly-drawn edge (no data yet) falls back to its label as the key — the
81
- // one moment label and key coincide, when the action is first born. The edge's label is captured
82
- // separately as the display name, so a later relabel renames the action without re-keying it.
83
- const transitionGroups = {};
84
- for (const el of nextElements) {
85
- if (!('source' in el))
86
- continue;
87
- const edgeLabel = typeof el.label === 'string' ? el.label : el.id;
88
- const actionKey = el.data?.actionKey ?? edgeLabel;
89
- const sourceLabel = idToLabel[el.source] || el.source;
90
- const targetLabel = idToLabel[el.target] || el.target;
91
- // Classify by topology, not by edge `type`: a self-loop (source === target) round-trips to a
92
- // self-transition regardless of how it was authored (drawn node→itself, or re-rendered from a
93
- // `selfTransition` action). A group's kind is set by its first edge; a multi-state self-loop
94
- // (e.g. `save` on Draft AND Pending) accumulates each source into allowedStates.
95
- const isSelf = el.source === el.target;
96
- if (!transitionGroups[actionKey]) {
97
- transitionGroups[actionKey] = {
98
- allowedStates: [sourceLabel],
99
- label: edgeLabel,
100
- selfLoop: isSelf,
101
- ...(isSelf ? {} : { nextState: targetLabel }),
102
- };
103
- }
104
- else {
105
- transitionGroups[actionKey].allowedStates.push(sourceLabel);
106
- }
107
- }
108
- const nextActions = {};
109
- // Transitions (and self-transitions) derived from graph edges
110
- for (const [actionKey, group] of Object.entries(transitionGroups)) {
111
- const existing = existingWorkflow?.actions?.[actionKey];
112
- nextActions[actionKey] = {
113
- // Spread existing first so every field the graph does NOT own — clientHandler,
114
- // requiredFields and any field added to ActionDefinition later — survives the
115
- // round-trip. The graph owns topology (allowedStates/nextState/selfTransition) and the
116
- // display label. (Enumerating named fields here previously dropped clientHandler.)
117
- ...existing,
118
- // The edge's label is the display name (decoupled from the key). Fall back to the
119
- // existing label, then the key, if an edge ever arrives label-less.
120
- label: group.label || existing?.label || actionKey,
121
- // The graph owns topology only. A self-transition (self-loop) stays in place: mark
122
- // `selfTransition`, carry NO `nextState`. A cross-state transition carries `nextState`
123
- // and clears any stale self flag (a self-loop redrawn as a normal edge). Explicit
124
- // `undefined` on the unused field is dropped by JSON.stringify, so no format churn.
125
- allowedStates: group.allowedStates,
126
- nextState: group.selfLoop ? undefined : group.nextState,
127
- selfTransition: group.selfLoop ? true : undefined,
128
- };
129
- }
130
- // Pass through Verbs (stateless: true) and global Workflow actions (no allowedStates) verbatim
131
- for (const [actionKey, actionDef] of Object.entries(existingWorkflow?.actions ?? {})) {
132
- if (actionKey in transitionGroups)
133
- continue;
134
- if (actionDef.stateless || !actionDef.allowedStates?.length) {
135
- nextActions[actionKey] = actionDef;
136
- }
137
- // Workflow action with allowedStates not in graph = user deleted its edges → remove
138
- }
139
- return {
140
- // Spread existingWorkflow first so every top-level key the graph does NOT own — the
141
- // sibling `triggers` map (field-validation triggers), and any WorkflowMeta key added
142
- // later — survives the round-trip. The graph owns topology only: states + actions,
143
- // overridden below. (Same principle the per-action spread applies above; enumerating
144
- // only states+actions here previously dropped `triggers` on every graph edit.)
145
- workflow: { ...existingWorkflow, states: stateNames, actions: nextActions },
146
- layout: nextLayout,
147
- };
148
- }
@@ -1,92 +0,0 @@
1
- <template>
2
- <!-- transparent ghost path gives a ~20px click zone around the 2px visible stroke -->
3
- <path :d="path[0]" fill="none" stroke="transparent" stroke-width="20" class="vue-flow__edge-interaction" />
4
- <path :id="id" :style="style" class="vue-flow__edge-path" :d="path[0]" :marker-end="markerEnd" />
5
-
6
- <EdgeLabelRenderer>
7
- <div
8
- :style="{
9
- pointerEvents: 'all',
10
- position: 'absolute',
11
- transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,
12
- }"
13
- class="nodrag nopan editable-edge-label"
14
- @click="labelOnClick()"
15
- @contextmenu.prevent="$emit('remove', id)">
16
- <div class="vue-flow__edge-label">{{ label }}</div>
17
- <div v-if="showInput" class="label-input-wrapper">
18
- <input
19
- ref="labelInput"
20
- v-model="newLabel"
21
- class="label-input"
22
- @blur="showInput = false"
23
- @keypress.enter="submitNewLabel" />
24
- </div>
25
- </div>
26
- </EdgeLabelRenderer>
27
- </template>
28
-
29
- <script setup lang="ts">
30
- import { type EdgeProps, EdgeLabelRenderer, getBezierPath /* useVueFlow */ } from '@vue-flow/core'
31
- import { computed, ref, nextTick, useTemplateRef } from 'vue'
32
-
33
- const props = defineProps<EdgeProps>()
34
- const emit = defineEmits(['change', 'remove'])
35
-
36
- const inputRef = useTemplateRef<HTMLInputElement>('labelInput')
37
- const newLabel = ref<EdgeProps['label']>('')
38
- const showInput = ref(false)
39
- let lastClick = 0
40
-
41
- const labelOnClick = async () => {
42
- let now = Date.now()
43
- if (now - lastClick < 500 && !showInput.value) {
44
- await showLabelInput()
45
- }
46
- lastClick = now
47
- }
48
-
49
- const showLabelInput = async () => {
50
- newLabel.value = props.label
51
- showInput.value = true
52
- await nextTick()
53
- inputRef.value?.focus()
54
- }
55
-
56
- const submitNewLabel = () => {
57
- showInput.value = false
58
- emit('change', newLabel.value)
59
- }
60
-
61
- const path = computed(() => getBezierPath(props))
62
- </script>
63
-
64
- <script lang="ts">
65
- export default {
66
- inheritAttrs: false,
67
- }
68
- </script>
69
-
70
- <style>
71
- .editable-edge-label {
72
- background-color: white;
73
- position: relative;
74
- font-size: 12px;
75
- }
76
-
77
- .label-input-wrapper {
78
- position: absolute;
79
- top: 0;
80
- left: 0;
81
- right: 0;
82
- bottom: 0;
83
- display: flex;
84
- align-items: center;
85
- justify-content: center;
86
- }
87
-
88
- .label-input {
89
- text-align: center;
90
- width: 40ch;
91
- }
92
- </style>
@@ -1,65 +0,0 @@
1
- <template>
2
- <div @click="nodeOnClick()">
3
- <div>{{ label }}</div>
4
- <div v-if="showInput" class="label-input-wrapper">
5
- <input
6
- ref="labelInput"
7
- v-model="newLabel"
8
- class="label-input"
9
- @blur="showInput = false"
10
- @keypress.enter="submitNewLabel" />
11
- </div>
12
- <Handle v-if="data.hasInput" id="a" type="target" :position="targetPosition" />
13
- <Handle v-if="data.hasOutput" id="b" type="source" :position="sourcePosition" />
14
- </div>
15
- </template>
16
-
17
- <script setup lang="ts">
18
- import { NodeProps, Handle } from '@vue-flow/core'
19
- import { ref, nextTick, useTemplateRef } from 'vue'
20
-
21
- const props = defineProps<NodeProps>()
22
- const emit = defineEmits(['change'])
23
-
24
- const inputRef = useTemplateRef<HTMLInputElement>('labelInput')
25
- const newLabel = ref<NodeProps['label']>('')
26
- const showInput = ref(false)
27
- let lastClick = 0
28
-
29
- const nodeOnClick = async () => {
30
- let now = Date.now()
31
- if (now - lastClick < 500 && !showInput.value) {
32
- await showLabelInput()
33
- }
34
- lastClick = now
35
- }
36
-
37
- const showLabelInput = async () => {
38
- newLabel.value = props.label
39
- showInput.value = true
40
- await nextTick()
41
- inputRef.value?.focus()
42
- }
43
-
44
- const submitNewLabel = () => {
45
- showInput.value = false
46
- emit('change', newLabel.value)
47
- }
48
- </script>
49
-
50
- <style>
51
- .label-input-wrapper {
52
- position: absolute;
53
- top: 0;
54
- left: 0;
55
- right: 0;
56
- bottom: 0;
57
- display: flex;
58
- align-items: center;
59
- justify-content: center;
60
- }
61
-
62
- .label-input {
63
- text-align: center;
64
- }
65
- </style>