@antelopejs/dms-automation 0.0.1
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.
- package/LICENSE +190 -0
- package/README.md +95 -0
- package/dist/cluster/bus.d.ts +17 -0
- package/dist/cluster/bus.js +82 -0
- package/dist/cluster/bus.js.map +1 -0
- package/dist/cluster/instance.d.ts +1 -0
- package/dist/cluster/instance.js +6 -0
- package/dist/cluster/instance.js.map +1 -0
- package/dist/cluster/leader.d.ts +7 -0
- package/dist/cluster/leader.js +123 -0
- package/dist/cluster/leader.js.map +1 -0
- package/dist/cluster/mode.d.ts +3 -0
- package/dist/cluster/mode.js +25 -0
- package/dist/cluster/mode.js.map +1 -0
- package/dist/data/procedures-table.d.ts +66 -0
- package/dist/data/procedures-table.js +271 -0
- package/dist/data/procedures-table.js.map +1 -0
- package/dist/data/runs-table.d.ts +23 -0
- package/dist/data/runs-table.js +125 -0
- package/dist/data/runs-table.js.map +1 -0
- package/dist/db/index.d.ts +7 -0
- package/dist/db/index.js +24 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/automation_template.model.d.ts +58 -0
- package/dist/db/models/automation_template.model.js +134 -0
- package/dist/db/models/automation_template.model.js.map +1 -0
- package/dist/db/models/index.d.ts +3 -0
- package/dist/db/models/index.js +20 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/procedure.model.d.ts +25 -0
- package/dist/db/models/procedure.model.js +28 -0
- package/dist/db/models/procedure.model.js.map +1 -0
- package/dist/db/models/procedure_run.model.d.ts +34 -0
- package/dist/db/models/procedure_run.model.js +39 -0
- package/dist/db/models/procedure_run.model.js.map +1 -0
- package/dist/db/models/stats.model.d.ts +73 -0
- package/dist/db/models/stats.model.js +267 -0
- package/dist/db/models/stats.model.js.map +1 -0
- package/dist/db/models/type_config.model.d.ts +47 -0
- package/dist/db/models/type_config.model.js +51 -0
- package/dist/db/models/type_config.model.js.map +1 -0
- package/dist/db/tables/action_type_config.table.d.ts +7 -0
- package/dist/db/tables/action_type_config.table.js +35 -0
- package/dist/db/tables/action_type_config.table.js.map +1 -0
- package/dist/db/tables/automation_template.table.d.ts +11 -0
- package/dist/db/tables/automation_template.table.js +52 -0
- package/dist/db/tables/automation_template.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +5 -0
- package/dist/db/tables/index.js +22 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/procedure.table.d.ts +10 -0
- package/dist/db/tables/procedure.table.js +48 -0
- package/dist/db/tables/procedure.table.js.map +1 -0
- package/dist/db/tables/procedure_run.table.d.ts +13 -0
- package/dist/db/tables/procedure_run.table.js +57 -0
- package/dist/db/tables/procedure_run.table.js.map +1 -0
- package/dist/db/tables/trigger_type_config.table.d.ts +7 -0
- package/dist/db/tables/trigger_type_config.table.js +35 -0
- package/dist/db/tables/trigger_type_config.table.js.map +1 -0
- package/dist/implementations/dms-automation/index.d.ts +19 -0
- package/dist/implementations/dms-automation/index.js +50 -0
- package/dist/implementations/dms-automation/index.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +121 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/builder.d.ts +10 -0
- package/dist/pages/builder.js +28 -0
- package/dist/pages/builder.js.map +1 -0
- package/dist/pages/category.d.ts +1 -0
- package/dist/pages/category.js +10 -0
- package/dist/pages/category.js.map +1 -0
- package/dist/pages/index.d.ts +6 -0
- package/dist/pages/index.js +9 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/library.d.ts +10 -0
- package/dist/pages/library.js +28 -0
- package/dist/pages/library.js.map +1 -0
- package/dist/pages/overview.d.ts +10 -0
- package/dist/pages/overview.js +28 -0
- package/dist/pages/overview.js.map +1 -0
- package/dist/pages/procedures.d.ts +10 -0
- package/dist/pages/procedures.js +72 -0
- package/dist/pages/procedures.js.map +1 -0
- package/dist/pages/runs.d.ts +11 -0
- package/dist/pages/runs.js +61 -0
- package/dist/pages/runs.js.map +1 -0
- package/dist/routes/action-types.d.ts +23 -0
- package/dist/routes/action-types.js +80 -0
- package/dist/routes/action-types.js.map +1 -0
- package/dist/routes/data-node-types.d.ts +6 -0
- package/dist/routes/data-node-types.js +36 -0
- package/dist/routes/data-node-types.js.map +1 -0
- package/dist/routes/graphSchema.d.ts +92 -0
- package/dist/routes/graphSchema.js +60 -0
- package/dist/routes/graphSchema.js.map +1 -0
- package/dist/routes/index.d.ts +8 -0
- package/dist/routes/index.js +11 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/node-kinds.d.ts +9 -0
- package/dist/routes/node-kinds.js +35 -0
- package/dist/routes/node-kinds.js.map +1 -0
- package/dist/routes/procedures.d.ts +23 -0
- package/dist/routes/procedures.js +181 -0
- package/dist/routes/procedures.js.map +1 -0
- package/dist/routes/runs.d.ts +7 -0
- package/dist/routes/runs.js +81 -0
- package/dist/routes/runs.js.map +1 -0
- package/dist/routes/stats.d.ts +6 -0
- package/dist/routes/stats.js +35 -0
- package/dist/routes/stats.js.map +1 -0
- package/dist/routes/templates.d.ts +20 -0
- package/dist/routes/templates.js +243 -0
- package/dist/routes/templates.js.map +1 -0
- package/dist/routes/trigger-types.d.ts +24 -0
- package/dist/routes/trigger-types.js +80 -0
- package/dist/routes/trigger-types.js.map +1 -0
- package/dist/routes/typeConfigShared.d.ts +22 -0
- package/dist/routes/typeConfigShared.js +37 -0
- package/dist/routes/typeConfigShared.js.map +1 -0
- package/dist/runtime/builtins/actions/http.d.ts +13 -0
- package/dist/runtime/builtins/actions/http.js +47 -0
- package/dist/runtime/builtins/actions/http.js.map +1 -0
- package/dist/runtime/builtins/actions/index.d.ts +2 -0
- package/dist/runtime/builtins/actions/index.js +7 -0
- package/dist/runtime/builtins/actions/index.js.map +1 -0
- package/dist/runtime/builtins/actions/log.d.ts +13 -0
- package/dist/runtime/builtins/actions/log.js +49 -0
- package/dist/runtime/builtins/actions/log.js.map +1 -0
- package/dist/runtime/builtins/data/boolean.d.ts +2 -0
- package/dist/runtime/builtins/data/boolean.js +39 -0
- package/dist/runtime/builtins/data/boolean.js.map +1 -0
- package/dist/runtime/builtins/data/compare.d.ts +2 -0
- package/dist/runtime/builtins/data/compare.js +29 -0
- package/dist/runtime/builtins/data/compare.js.map +1 -0
- package/dist/runtime/builtins/data/constant.d.ts +2 -0
- package/dist/runtime/builtins/data/constant.js +29 -0
- package/dist/runtime/builtins/data/constant.js.map +1 -0
- package/dist/runtime/builtins/data/index.d.ts +2 -0
- package/dist/runtime/builtins/data/index.js +20 -0
- package/dist/runtime/builtins/data/index.js.map +1 -0
- package/dist/runtime/builtins/data/json.d.ts +2 -0
- package/dist/runtime/builtins/data/json.js +46 -0
- package/dist/runtime/builtins/data/json.js.map +1 -0
- package/dist/runtime/builtins/data/math.d.ts +2 -0
- package/dist/runtime/builtins/data/math.js +53 -0
- package/dist/runtime/builtins/data/math.js.map +1 -0
- package/dist/runtime/builtins/data/object.d.ts +2 -0
- package/dist/runtime/builtins/data/object.js +121 -0
- package/dist/runtime/builtins/data/object.js.map +1 -0
- package/dist/runtime/builtins/data/string.d.ts +2 -0
- package/dist/runtime/builtins/data/string.js +143 -0
- package/dist/runtime/builtins/data/string.js.map +1 -0
- package/dist/runtime/builtins/index.d.ts +17 -0
- package/dist/runtime/builtins/index.js +24 -0
- package/dist/runtime/builtins/index.js.map +1 -0
- package/dist/runtime/builtins/nodes/action.d.ts +1 -0
- package/dist/runtime/builtins/nodes/action.js +36 -0
- package/dist/runtime/builtins/nodes/action.js.map +1 -0
- package/dist/runtime/builtins/nodes/data.d.ts +1 -0
- package/dist/runtime/builtins/nodes/data.js +28 -0
- package/dist/runtime/builtins/nodes/data.js.map +1 -0
- package/dist/runtime/builtins/nodes/delay.d.ts +1 -0
- package/dist/runtime/builtins/nodes/delay.js +41 -0
- package/dist/runtime/builtins/nodes/delay.js.map +1 -0
- package/dist/runtime/builtins/nodes/forRange.d.ts +1 -0
- package/dist/runtime/builtins/nodes/forRange.js +52 -0
- package/dist/runtime/builtins/nodes/forRange.js.map +1 -0
- package/dist/runtime/builtins/nodes/foreach.d.ts +1 -0
- package/dist/runtime/builtins/nodes/foreach.js +31 -0
- package/dist/runtime/builtins/nodes/foreach.js.map +1 -0
- package/dist/runtime/builtins/nodes/groupInput.d.ts +1 -0
- package/dist/runtime/builtins/nodes/groupInput.js +11 -0
- package/dist/runtime/builtins/nodes/groupInput.js.map +1 -0
- package/dist/runtime/builtins/nodes/groupOutput.d.ts +1 -0
- package/dist/runtime/builtins/nodes/groupOutput.js +11 -0
- package/dist/runtime/builtins/nodes/groupOutput.js.map +1 -0
- package/dist/runtime/builtins/nodes/if.d.ts +1 -0
- package/dist/runtime/builtins/nodes/if.js +23 -0
- package/dist/runtime/builtins/nodes/if.js.map +1 -0
- package/dist/runtime/builtins/nodes/parallel.d.ts +1 -0
- package/dist/runtime/builtins/nodes/parallel.js +34 -0
- package/dist/runtime/builtins/nodes/parallel.js.map +1 -0
- package/dist/runtime/builtins/nodes/retry.d.ts +1 -0
- package/dist/runtime/builtins/nodes/retry.js +46 -0
- package/dist/runtime/builtins/nodes/retry.js.map +1 -0
- package/dist/runtime/builtins/nodes/setVariable.d.ts +1 -0
- package/dist/runtime/builtins/nodes/setVariable.js +31 -0
- package/dist/runtime/builtins/nodes/setVariable.js.map +1 -0
- package/dist/runtime/builtins/nodes/switch.d.ts +1 -0
- package/dist/runtime/builtins/nodes/switch.js +33 -0
- package/dist/runtime/builtins/nodes/switch.js.map +1 -0
- package/dist/runtime/builtins/nodes/trigger.d.ts +1 -0
- package/dist/runtime/builtins/nodes/trigger.js +25 -0
- package/dist/runtime/builtins/nodes/trigger.js.map +1 -0
- package/dist/runtime/builtins/nodes/tryCatch.d.ts +1 -0
- package/dist/runtime/builtins/nodes/tryCatch.js +35 -0
- package/dist/runtime/builtins/nodes/tryCatch.js.map +1 -0
- package/dist/runtime/builtins/triggers/cron.d.ts +9 -0
- package/dist/runtime/builtins/triggers/cron.js +52 -0
- package/dist/runtime/builtins/triggers/cron.js.map +1 -0
- package/dist/runtime/builtins/triggers/index.d.ts +2 -0
- package/dist/runtime/builtins/triggers/index.js +12 -0
- package/dist/runtime/builtins/triggers/index.js.map +1 -0
- package/dist/runtime/builtins/triggers/manual.d.ts +3 -0
- package/dist/runtime/builtins/triggers/manual.js +20 -0
- package/dist/runtime/builtins/triggers/manual.js.map +1 -0
- package/dist/runtime/builtins/triggers/webhook.d.ts +14 -0
- package/dist/runtime/builtins/triggers/webhook.js +90 -0
- package/dist/runtime/builtins/triggers/webhook.js.map +1 -0
- package/dist/runtime/executionResult.d.ts +6 -0
- package/dist/runtime/executionResult.js +3 -0
- package/dist/runtime/executionResult.js.map +1 -0
- package/dist/runtime/executor.d.ts +9 -0
- package/dist/runtime/executor.js +300 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/flatten.d.ts +3 -0
- package/dist/runtime/flatten.js +188 -0
- package/dist/runtime/flatten.js.map +1 -0
- package/dist/runtime/nodeKinds.d.ts +64 -0
- package/dist/runtime/nodeKinds.js +43 -0
- package/dist/runtime/nodeKinds.js.map +1 -0
- package/dist/runtime/reconcileDiff.d.ts +9 -0
- package/dist/runtime/reconcileDiff.js +26 -0
- package/dist/runtime/reconcileDiff.js.map +1 -0
- package/dist/runtime/registry.d.ts +19 -0
- package/dist/runtime/registry.js +47 -0
- package/dist/runtime/registry.js.map +1 -0
- package/dist/runtime/resolveInputs.d.ts +14 -0
- package/dist/runtime/resolveInputs.js +64 -0
- package/dist/runtime/resolveInputs.js.map +1 -0
- package/dist/runtime/resolveSubgraph.d.ts +7 -0
- package/dist/runtime/resolveSubgraph.js +15 -0
- package/dist/runtime/resolveSubgraph.js.map +1 -0
- package/dist/runtime/subscriptions.d.ts +16 -0
- package/dist/runtime/subscriptions.js +419 -0
- package/dist/runtime/subscriptions.js.map +1 -0
- package/dist/runtime/subwalk.d.ts +17 -0
- package/dist/runtime/subwalk.js +74 -0
- package/dist/runtime/subwalk.js.map +1 -0
- package/dist/runtime/topo.d.ts +3 -0
- package/dist/runtime/topo.js +75 -0
- package/dist/runtime/topo.js.map +1 -0
- package/dist/runtime/typeOrigin.d.ts +7 -0
- package/dist/runtime/typeOrigin.js +24 -0
- package/dist/runtime/typeOrigin.js.map +1 -0
- package/dist/runtime/validate.d.ts +10 -0
- package/dist/runtime/validate.js +185 -0
- package/dist/runtime/validate.js.map +1 -0
- package/dist/runtime/validateTemplate.d.ts +3 -0
- package/dist/runtime/validateTemplate.js +54 -0
- package/dist/runtime/validateTemplate.js.map +1 -0
- package/dist/types/constants.d.ts +6 -0
- package/dist/types/constants.js +10 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/graph.d.ts +51 -0
- package/dist/types/graph.js +23 -0
- package/dist/types/graph.js.map +1 -0
- package/dist/types/pagination.d.ts +8 -0
- package/dist/types/pagination.js +3 -0
- package/dist/types/pagination.js.map +1 -0
- package/dist/types/runLog.d.ts +26 -0
- package/dist/types/runLog.js +3 -0
- package/dist/types/runLog.js.map +1 -0
- package/frontend-vue/.prettierignore +4 -0
- package/frontend-vue/app/components/Actions.vue +141 -0
- package/frontend-vue/app/components/Catalog/TypeConfigForm.vue +166 -0
- package/frontend-vue/app/components/Catalog/TypeList.vue +198 -0
- package/frontend-vue/app/components/DataNodes.vue +259 -0
- package/frontend-vue/app/components/Editor/BuilderPalette.vue +234 -0
- package/frontend-vue/app/components/Editor/GraphCanvas.vue +994 -0
- package/frontend-vue/app/components/Editor/GroupPortEditor.vue +289 -0
- package/frontend-vue/app/components/Editor/JsonField.vue +63 -0
- package/frontend-vue/app/components/Editor/NodeInspector.vue +356 -0
- package/frontend-vue/app/components/Editor/ProcedureList.vue +156 -0
- package/frontend-vue/app/components/Editor/RunsDrawer.vue +312 -0
- package/frontend-vue/app/components/Editor/WiredField.vue +42 -0
- package/frontend-vue/app/components/Editor/nodes/GenericNode.vue +161 -0
- package/frontend-vue/app/components/Editor/nodes/GroupInputNode.vue +19 -0
- package/frontend-vue/app/components/Editor/nodes/GroupNode.vue +200 -0
- package/frontend-vue/app/components/Editor/nodes/GroupOutputNode.vue +19 -0
- package/frontend-vue/app/components/Editor/nodes/GroupSentinel.vue +161 -0
- package/frontend-vue/app/components/Editor/nodes/styles.ts +53 -0
- package/frontend-vue/app/components/Editor.vue +2504 -0
- package/frontend-vue/app/components/Library.vue +67 -0
- package/frontend-vue/app/components/Overview.vue +503 -0
- package/frontend-vue/app/components/RunDetailDrawer.vue +165 -0
- package/frontend-vue/app/components/RunFireTree.vue +170 -0
- package/frontend-vue/app/components/RunTraceDrawer.vue +59 -0
- package/frontend-vue/app/components/RunsStats.vue +126 -0
- package/frontend-vue/app/components/RunsTimelineDisplay.vue +287 -0
- package/frontend-vue/app/components/Templates.vue +663 -0
- package/frontend-vue/app/components/Triggers.vue +137 -0
- package/frontend-vue/app/composables/useAutomationNodeKinds.ts +48 -0
- package/frontend-vue/app/composables/useAutomationRuns.ts +75 -0
- package/frontend-vue/app/composables/useGraphConnect.ts +542 -0
- package/frontend-vue/app/plugins/runs-timeline-display.client.ts +16 -0
- package/frontend-vue/app/utils/automation.ts +51 -0
- package/frontend-vue/dms.frontend.ts +26 -0
- package/frontend-vue/eslint.config.mjs +24 -0
- package/frontend-vue/i18n/locales/dms-automation-en-GB.json +369 -0
- package/frontend-vue/i18n/locales/dms-automation-fr-FR.json +369 -0
- package/frontend-vue/package.json +33 -0
- package/frontend-vue/pnpm-lock.yaml +6483 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/package.json +81 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, markRaw, nextTick, ref, watch } from 'vue'
|
|
3
|
+
import { newId } from '../../utils/automation'
|
|
4
|
+
// The canvas chrome (dotted DMS background, zoom controls, themed
|
|
5
|
+
// node/handle/edge styling, pan/zoom + fit-view) now comes from the shared
|
|
6
|
+
// DmsFlowCanvas wrapper (dms ≥0.0.25). We only keep the @vue-flow/core bits
|
|
7
|
+
// our business renderers need: `Position` for node source/target anchors and
|
|
8
|
+
// the connection / change / edge types for our wiring handlers. The imperative
|
|
9
|
+
// flow controller (zoom/fit/updateNodeInternals/viewport) is reached through
|
|
10
|
+
// DmsFlowCanvas's exposed store via a template ref (see `flowRef`).
|
|
11
|
+
import { Position } from '@vue-flow/core'
|
|
12
|
+
import type {
|
|
13
|
+
Connection,
|
|
14
|
+
NodeChange,
|
|
15
|
+
EdgeChange,
|
|
16
|
+
Edge as VfEdge,
|
|
17
|
+
} from '@vue-flow/core'
|
|
18
|
+
// Most node kinds share a single port-spec-driven renderer. Group and the
|
|
19
|
+
// two group sentinels stay custom because they have cross-graph port
|
|
20
|
+
// declarations and special handle naming.
|
|
21
|
+
import GenericNode from './nodes/GenericNode.vue'
|
|
22
|
+
import GroupNode from './nodes/GroupNode.vue'
|
|
23
|
+
import GroupInputNode from './nodes/GroupInputNode.vue'
|
|
24
|
+
import GroupOutputNode from './nodes/GroupOutputNode.vue'
|
|
25
|
+
import {
|
|
26
|
+
canConnect,
|
|
27
|
+
connectionKind,
|
|
28
|
+
parseSourceBranch,
|
|
29
|
+
parseTargetBranch,
|
|
30
|
+
parseDataPort,
|
|
31
|
+
parseDataField,
|
|
32
|
+
} from '../../composables/useGraphConnect'
|
|
33
|
+
import type {
|
|
34
|
+
ProcedureGraph,
|
|
35
|
+
GraphNode as AppGraphNode,
|
|
36
|
+
TriggerEdge,
|
|
37
|
+
DataEdge,
|
|
38
|
+
NodeKind,
|
|
39
|
+
TriggerType,
|
|
40
|
+
ActionType,
|
|
41
|
+
DataNodeType,
|
|
42
|
+
JsonSchema,
|
|
43
|
+
GroupPort,
|
|
44
|
+
} from '../../composables/useGraphConnect'
|
|
45
|
+
import type {
|
|
46
|
+
NodeKindEntry,
|
|
47
|
+
NodeKindUI,
|
|
48
|
+
} from '../../composables/useAutomationNodeKinds'
|
|
49
|
+
|
|
50
|
+
// Minimal shape of a workspace template surfaced in the palette. Mirrors the
|
|
51
|
+
// `TemplateRow` interface in Templates.vue but only the fields the palette
|
|
52
|
+
// actually needs (id, name, icon, ports). `ports` is deep-cloned (JSON
|
|
53
|
+
// round-trip) onto each new template-instance group node so future template
|
|
54
|
+
// edits don't retroactively mutate the instance's port list (the resolved
|
|
55
|
+
// subgraph still updates live — that's the live-link model).
|
|
56
|
+
interface TemplateSummary {
|
|
57
|
+
_id: string
|
|
58
|
+
name: string
|
|
59
|
+
icon?: string
|
|
60
|
+
ports: GroupPort[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const props = defineProps<{
|
|
64
|
+
graph: ProcedureGraph
|
|
65
|
+
triggerTypes: TriggerType[]
|
|
66
|
+
actionTypes: ActionType[]
|
|
67
|
+
dataNodeTypes: DataNodeType[]
|
|
68
|
+
/**
|
|
69
|
+
* Per-NodeKind metadata served by `/api/automation/node-kinds`. The
|
|
70
|
+
* `ui` block on each entry tells `dataFor` how to render the node —
|
|
71
|
+
* label, icon, port lists, type-registry / dynamic-config hooks. The
|
|
72
|
+
* frontend has zero kind-specific code beyond group + sentinels.
|
|
73
|
+
*/
|
|
74
|
+
nodeKinds: NodeKindEntry[]
|
|
75
|
+
canUndo?: boolean
|
|
76
|
+
canRedo?: boolean
|
|
77
|
+
isDirty?: boolean
|
|
78
|
+
applying?: boolean
|
|
79
|
+
// When the canvas is showing a group's inner subgraph, these are the
|
|
80
|
+
// parent group's declared ports — the sentinel nodes (groupInput /
|
|
81
|
+
// groupOutput) render handles derived from this list rather than
|
|
82
|
+
// reading from their own node data. (Sentinel nodes themselves don't
|
|
83
|
+
// carry ports; their parent group does.) Undefined at the procedure
|
|
84
|
+
// root.
|
|
85
|
+
parentGroupPorts?: GroupPort[]
|
|
86
|
+
// When true, the canvas suppresses graph mutations from user input —
|
|
87
|
+
// used for the read-only view of template-instance inner subgraphs.
|
|
88
|
+
// User can still pan / zoom / select / inspect, just not connect,
|
|
89
|
+
// move, delete, or edit. The "Apply / Cancel" buttons are hidden too.
|
|
90
|
+
readOnly?: boolean
|
|
91
|
+
}>()
|
|
92
|
+
|
|
93
|
+
const emit = defineEmits<{
|
|
94
|
+
(e: 'update:graph', g: ProcedureGraph): void
|
|
95
|
+
(e: 'select:node', id: string | null): void
|
|
96
|
+
// Fires whenever the selection set changes (single or multi). `ids` is
|
|
97
|
+
// the full set of currently selected node ids in this canvas — used by
|
|
98
|
+
// Editor.vue to drive the "Group selection" toolbar action. Sentinel
|
|
99
|
+
// nodes (groupInput / groupOutput) are filtered out before emitting.
|
|
100
|
+
(e: 'selection-change', payload: { ids: string[] }): void
|
|
101
|
+
(e: 'undo'): void
|
|
102
|
+
(e: 'redo'): void
|
|
103
|
+
(e: 'cancel'): void
|
|
104
|
+
(e: 'apply'): void
|
|
105
|
+
// Bubbles up GroupNode's @dblclick — Editor.vue uses this to push the
|
|
106
|
+
// group onto the subgraph navigation path. Filtered by kind here so
|
|
107
|
+
// the parent doesn't need to look the node up itself.
|
|
108
|
+
(e: 'group:enter', payload: { id: string }): void
|
|
109
|
+
}>()
|
|
110
|
+
|
|
111
|
+
const toast = useToast()
|
|
112
|
+
|
|
113
|
+
const nodeTypes = markRaw({
|
|
114
|
+
genericNode: GenericNode,
|
|
115
|
+
group: GroupNode,
|
|
116
|
+
groupInput: GroupInputNode,
|
|
117
|
+
groupOutput: GroupOutputNode,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Sentinel node kinds — only valid inside a group's subgraph and never
|
|
121
|
+
// user-deletable. We filter them out of keyboard/right-click delete in
|
|
122
|
+
// onNodesChange below.
|
|
123
|
+
const SENTINEL_KINDS: ReadonlySet<NodeKind> = new Set<NodeKind>([
|
|
124
|
+
'groupInput',
|
|
125
|
+
'groupOutput',
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
function vfTypeFor(kind: NodeKind): string {
|
|
129
|
+
if (kind === 'group') return 'group'
|
|
130
|
+
if (kind === 'groupInput') return 'groupInput'
|
|
131
|
+
if (kind === 'groupOutput') return 'groupOutput'
|
|
132
|
+
return 'genericNode'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function propsOf(schema: JsonSchema | undefined) {
|
|
136
|
+
if (!schema?.properties) return []
|
|
137
|
+
return Object.entries(schema.properties).map(([name, p]) => ({
|
|
138
|
+
name,
|
|
139
|
+
type: Array.isArray(p?.type) ? p?.type[0] : p?.type,
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Index node-kind metadata by kind for O(1) lookup.
|
|
144
|
+
const nodeKindUiByKind = computed<Map<string, NodeKindUI>>(() => {
|
|
145
|
+
const m = new Map<string, NodeKindUI>()
|
|
146
|
+
for (const entry of props.nodeKinds ?? []) {
|
|
147
|
+
if (entry.meta?.ui) m.set(entry.kind, entry.meta.ui)
|
|
148
|
+
}
|
|
149
|
+
return m
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Pull a registered TriggerType / ActionType / DataNodeType from the
|
|
153
|
+
// type-registry prop indicated by a NodeKindUI.typeRegistry hint.
|
|
154
|
+
function lookupTypeBySpec(spec: NodeKindUI, typeId: string | undefined) {
|
|
155
|
+
if (!spec.typeRegistry || !typeId) return null
|
|
156
|
+
if (spec.typeRegistry === 'triggers')
|
|
157
|
+
return props.triggerTypes.find((t) => t.id === typeId) ?? null
|
|
158
|
+
if (spec.typeRegistry === 'actions')
|
|
159
|
+
return props.actionTypes.find((a) => a.id === typeId) ?? null
|
|
160
|
+
if (spec.typeRegistry === 'dataNodes')
|
|
161
|
+
return props.dataNodeTypes.find((d) => d.id === typeId) ?? null
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Build the GenericNode `data` payload from a graph node. For every kind
|
|
166
|
+
// with `ui` metadata (every kind except group + sentinels), one
|
|
167
|
+
// data-driven resolver applies: combine `spec` from the backend with the
|
|
168
|
+
// node's `typeId` (for type-registered kinds) and `config` (for dynamic
|
|
169
|
+
// trigger-out ports).
|
|
170
|
+
function dataFor(node: AppGraphNode) {
|
|
171
|
+
const spec = nodeKindUiByKind.value.get(node.kind)
|
|
172
|
+
if (spec) {
|
|
173
|
+
let label = spec.label
|
|
174
|
+
let icon = spec.icon
|
|
175
|
+
let category: string | undefined
|
|
176
|
+
let dataIns = [...spec.staticDataIns]
|
|
177
|
+
let dataOuts = [...spec.staticDataOuts]
|
|
178
|
+
|
|
179
|
+
const type = lookupTypeBySpec(spec, node.typeId) as
|
|
180
|
+
| {
|
|
181
|
+
name?: string
|
|
182
|
+
icon?: string
|
|
183
|
+
category?: string
|
|
184
|
+
inputSchema?: JsonSchema
|
|
185
|
+
outputSchema?: JsonSchema
|
|
186
|
+
configSchema?: JsonSchema
|
|
187
|
+
}
|
|
188
|
+
| null
|
|
189
|
+
if (type) {
|
|
190
|
+
if (spec.labelFromType) label = type.name ?? label
|
|
191
|
+
if (spec.iconFromType) icon = type.icon ?? icon
|
|
192
|
+
if (spec.categoryFromType) category = type.category
|
|
193
|
+
if (spec.dataInsFromTypeSchema)
|
|
194
|
+
dataIns = propsOf(type[spec.dataInsFromTypeSchema])
|
|
195
|
+
if (spec.dataOutsFromTypeSchema)
|
|
196
|
+
dataOuts = propsOf(type[spec.dataOutsFromTypeSchema])
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const dynPorts = spec.dynamicTriggerOutsFromConfig
|
|
200
|
+
? ((node.config?.[spec.dynamicTriggerOutsFromConfig] as
|
|
201
|
+
| string[]
|
|
202
|
+
| undefined) ?? [])
|
|
203
|
+
: []
|
|
204
|
+
const triggerOuts = [...dynPorts, ...spec.staticTriggerOuts]
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
label,
|
|
208
|
+
icon,
|
|
209
|
+
category,
|
|
210
|
+
typeId: node.typeId,
|
|
211
|
+
config: node.config,
|
|
212
|
+
hasTriggerIn: spec.hasTriggerIn,
|
|
213
|
+
hasMainTriggerOut: spec.hasMainTriggerOut,
|
|
214
|
+
triggerOuts,
|
|
215
|
+
dataIns,
|
|
216
|
+
dataOuts,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (node.kind === 'group') {
|
|
221
|
+
// Group node carries its own port list + (optional) inline subgraph
|
|
222
|
+
// or templateId. The fields live on the node itself (per backend
|
|
223
|
+
// GroupNode interface) — surface them so the renderer can display
|
|
224
|
+
// handles, linked badges, etc.
|
|
225
|
+
const g = node as AppGraphNode & {
|
|
226
|
+
ports?: unknown
|
|
227
|
+
templateId?: string
|
|
228
|
+
subgraph?: unknown
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
label: node.config?.name ?? 'Group',
|
|
232
|
+
ports: Array.isArray(g.ports) ? g.ports : [],
|
|
233
|
+
templateId: g.templateId,
|
|
234
|
+
subgraph: g.subgraph,
|
|
235
|
+
config: node.config,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (node.kind === 'groupInput' || node.kind === 'groupOutput') {
|
|
239
|
+
// Sentinels render the parent group's ports. In subgraph-editor
|
|
240
|
+
// mode (C5.3) Editor.vue passes `parentGroupPorts` so both
|
|
241
|
+
// sentinels share one source of truth — the outer group node.
|
|
242
|
+
// At the procedure-graph root there's no parent group, so we
|
|
243
|
+
// fall back to whatever ports the sentinel carries (which
|
|
244
|
+
// should normally be none — a stray sentinel renders empty).
|
|
245
|
+
if (Array.isArray(props.parentGroupPorts)) {
|
|
246
|
+
return { ports: props.parentGroupPorts }
|
|
247
|
+
}
|
|
248
|
+
const s = node as AppGraphNode & { ports?: unknown }
|
|
249
|
+
return { ports: Array.isArray(s.ports) ? s.ports : [] }
|
|
250
|
+
}
|
|
251
|
+
return {}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const vfNodes = computed(() =>
|
|
255
|
+
props.graph.nodes.map((n) => ({
|
|
256
|
+
id: n.id,
|
|
257
|
+
type: vfTypeFor(n.kind),
|
|
258
|
+
position: n.position,
|
|
259
|
+
data: dataFor(n),
|
|
260
|
+
sourcePosition: Position.Bottom,
|
|
261
|
+
targetPosition: Position.Top,
|
|
262
|
+
})),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
// Look up the kind of an edge endpoint by node id — needed for the group
|
|
266
|
+
// handle-id derivation below. Sentinels and groups have richer handle
|
|
267
|
+
// naming than regular nodes (`group-trigger-in-<port>` vs `trigger-in`),
|
|
268
|
+
// so the edge mapping needs to know which target/source it's wiring to.
|
|
269
|
+
const nodeKindById = computed<Map<string, NodeKind>>(() => {
|
|
270
|
+
const m = new Map<string, NodeKind>()
|
|
271
|
+
for (const n of props.graph.nodes) m.set(n.id, n.kind)
|
|
272
|
+
return m
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// Trigger edges into a group node or groupOutput sentinel target a specific
|
|
276
|
+
// trigger-in port — the port name lives on `e.to.branch`. Ordinary target
|
|
277
|
+
// nodes have a single fixed `trigger-in` handle.
|
|
278
|
+
function triggerTargetHandle(e: TriggerEdge, targetKind: NodeKind | undefined): string {
|
|
279
|
+
if (targetKind === 'group') {
|
|
280
|
+
return `group-trigger-in-${e.to.branch}`
|
|
281
|
+
}
|
|
282
|
+
if (targetKind === 'groupOutput') {
|
|
283
|
+
// Trigger edge from an inner node into the groupOutput sentinel —
|
|
284
|
+
// the sentinel exposes a `groupOutput-trigger-in-<port>` handle per
|
|
285
|
+
// declared trigger-OUT port (since trigger-OUT on the OUTER group is
|
|
286
|
+
// trigger-IN on the sentinel side).
|
|
287
|
+
return `groupOutput-trigger-in-${e.to.branch}`
|
|
288
|
+
}
|
|
289
|
+
return 'trigger-in'
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function triggerSourceHandle(e: TriggerEdge, sourceKind: NodeKind | undefined): string {
|
|
293
|
+
if (sourceKind === 'group') {
|
|
294
|
+
// Source is the OUTER group node firing into an outer downstream
|
|
295
|
+
// node — the branch name IS the port name (see planNewGroup's
|
|
296
|
+
// outerTriggerEdges where `from.branch = portName`).
|
|
297
|
+
return `group-trigger-out-${e.from.branch ?? 'main'}`
|
|
298
|
+
}
|
|
299
|
+
if (sourceKind === 'groupInput') {
|
|
300
|
+
// Inner edge out of groupInput into an inner node — handle id is
|
|
301
|
+
// `groupInput-trigger-out-<port>` and the port name lives on
|
|
302
|
+
// `from.branch` (per planNewGroup's innerTriggerEdges).
|
|
303
|
+
return `groupInput-trigger-out-${e.from.branch ?? 'main'}`
|
|
304
|
+
}
|
|
305
|
+
return `trigger-out-${e.from.branch ?? 'main'}`
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function dataSourceHandle(e: DataEdge, sourceKind: NodeKind | undefined): string {
|
|
309
|
+
if (sourceKind === 'group') return `group-data-out-${e.from.port}`
|
|
310
|
+
if (sourceKind === 'groupInput') return `groupInput-data-out-${e.from.port}`
|
|
311
|
+
return `data-out-${e.from.port}`
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function dataTargetHandle(e: DataEdge, targetKind: NodeKind | undefined): string {
|
|
315
|
+
if (targetKind === 'group') return `group-data-in-${e.to.field}`
|
|
316
|
+
if (targetKind === 'groupOutput') return `groupOutput-data-in-${e.to.field}`
|
|
317
|
+
return `data-in-${e.to.field}`
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Edge styling lives in <style> (.edge-trigger / .edge-data) so the
|
|
321
|
+
// `.selected` highlight can win without `!important`. Inline `style`
|
|
322
|
+
// on a vue-flow edge defeats the .selected rule — documented gotcha.
|
|
323
|
+
const vfEdges = computed(() => {
|
|
324
|
+
const kinds = nodeKindById.value
|
|
325
|
+
const triggerEdges = props.graph.triggerEdges.map((e) => ({
|
|
326
|
+
id: e.id,
|
|
327
|
+
source: e.from.node,
|
|
328
|
+
target: e.to.node,
|
|
329
|
+
sourceHandle: triggerSourceHandle(e, kinds.get(e.from.node)),
|
|
330
|
+
targetHandle: triggerTargetHandle(e, kinds.get(e.to.node)),
|
|
331
|
+
data: { kind: 'trigger' as const },
|
|
332
|
+
class: 'edge-trigger',
|
|
333
|
+
}))
|
|
334
|
+
const dataEdges = props.graph.dataEdges.map((e) => ({
|
|
335
|
+
id: e.id,
|
|
336
|
+
source: e.from.node,
|
|
337
|
+
target: e.to.node,
|
|
338
|
+
sourceHandle: dataSourceHandle(e, kinds.get(e.from.node)),
|
|
339
|
+
targetHandle: dataTargetHandle(e, kinds.get(e.to.node)),
|
|
340
|
+
data: { kind: 'data' as const },
|
|
341
|
+
class: 'edge-data',
|
|
342
|
+
}))
|
|
343
|
+
return [...triggerEdges, ...dataEdges]
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
function emitGraph(g: ProcedureGraph) {
|
|
348
|
+
emit('update:graph', g)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function defaultPos() {
|
|
352
|
+
// Stagger new nodes a bit so they don't all stack.
|
|
353
|
+
const n = props.graph.nodes.length
|
|
354
|
+
return { x: 120 + (n % 5) * 40, y: 120 + n * 40 }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function defaultsFromSchema(schema: JsonSchema | undefined): Record<string, unknown> {
|
|
358
|
+
if (!schema?.properties) return {}
|
|
359
|
+
const out: Record<string, unknown> = {}
|
|
360
|
+
for (const [name, prop] of Object.entries(schema.properties)) {
|
|
361
|
+
if (prop && prop.default !== undefined) out[name] = prop.default
|
|
362
|
+
}
|
|
363
|
+
return out
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function defaultConfigFor(kind: NodeKind, typeId?: string): Record<string, unknown> {
|
|
367
|
+
const ui = nodeKindUiByKind.value.get(kind)
|
|
368
|
+
if (ui?.typeRegistry === 'triggers') {
|
|
369
|
+
const t = props.triggerTypes.find((tt) => tt.id === typeId)
|
|
370
|
+
return defaultsFromSchema(t?.configSchema)
|
|
371
|
+
}
|
|
372
|
+
if (ui?.typeRegistry === 'actions') {
|
|
373
|
+
const a = props.actionTypes.find((aa) => aa.id === typeId)
|
|
374
|
+
return defaultsFromSchema(a?.inputSchema)
|
|
375
|
+
}
|
|
376
|
+
if (ui?.typeRegistry === 'dataNodes') {
|
|
377
|
+
const d = props.dataNodeTypes.find((dd) => dd.id === typeId)
|
|
378
|
+
// Constant nodes seed from `configSchema` (their value lives there);
|
|
379
|
+
// wired-port data nodes seed any `inputSchema` field defaults.
|
|
380
|
+
return { ...defaultsFromSchema(d?.inputSchema), ...defaultsFromSchema(d?.configSchema) }
|
|
381
|
+
}
|
|
382
|
+
// Static-config kinds carry their seed config in metadata; deep-clone
|
|
383
|
+
// so each instance gets its own mutable copy. JSON round-trip strips
|
|
384
|
+
// the Vue reactive proxy that wraps `props.nodeKinds` — structuredClone
|
|
385
|
+
// would throw on the proxy.
|
|
386
|
+
return ui?.defaultConfig
|
|
387
|
+
? (JSON.parse(JSON.stringify(ui.defaultConfig)) as Record<string, unknown>)
|
|
388
|
+
: {}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function addNode(kind: NodeKind, typeId?: string) {
|
|
392
|
+
const node: AppGraphNode = {
|
|
393
|
+
id: newId(),
|
|
394
|
+
kind,
|
|
395
|
+
typeId,
|
|
396
|
+
config: defaultConfigFor(kind, typeId),
|
|
397
|
+
position: defaultPos(),
|
|
398
|
+
}
|
|
399
|
+
emitGraph({
|
|
400
|
+
...props.graph,
|
|
401
|
+
nodes: [...props.graph.nodes, node],
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Drop a template-instance group node into the current graph. The instance
|
|
406
|
+
// carries `templateId` and a deep clone (JSON round-trip) of the template's
|
|
407
|
+
// port list;
|
|
408
|
+
// it has NO inline `subgraph` — the executor resolves the live subgraph
|
|
409
|
+
// from the template cache at run time (C3.2 / C4.4). Editing the source
|
|
410
|
+
// template later will retroactively change what this instance executes
|
|
411
|
+
// (the "live link" model); the port list is cloned so a port rename on
|
|
412
|
+
// the template doesn't silently invalidate the outer wiring on this
|
|
413
|
+
// instance (the user can manually re-sync by forking + re-saving).
|
|
414
|
+
function addTemplateInstance(template: TemplateSummary) {
|
|
415
|
+
const node: AppGraphNode & {
|
|
416
|
+
ports: GroupPort[]
|
|
417
|
+
templateId: string
|
|
418
|
+
} = {
|
|
419
|
+
id: newId(),
|
|
420
|
+
kind: 'group',
|
|
421
|
+
config: { name: template.name },
|
|
422
|
+
position: defaultPos(),
|
|
423
|
+
templateId: template._id,
|
|
424
|
+
// JSON round-trip strips Vue's reactive proxy that wraps the template
|
|
425
|
+
// passed from the palette; structuredClone throws on proxies.
|
|
426
|
+
ports: JSON.parse(JSON.stringify(template.ports ?? [])) as GroupPort[],
|
|
427
|
+
}
|
|
428
|
+
emitGraph({
|
|
429
|
+
...props.graph,
|
|
430
|
+
nodes: [...props.graph.nodes, node],
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Node creation is driven from the left BuilderPalette (in Editor.vue) — expose
|
|
435
|
+
// the two add helpers so the palette's events can reach the graph mutation.
|
|
436
|
+
defineExpose({ addNode, addTemplateInstance })
|
|
437
|
+
|
|
438
|
+
// DmsFlowCanvas owns the VueFlow store (it calls `useVueFlow(flowId)` with a
|
|
439
|
+
// private id and `defineExpose`s the resulting controller). We grab that
|
|
440
|
+
// controller through a template ref instead of calling `useVueFlow()` here —
|
|
441
|
+
// a bare `useVueFlow()` would resolve to the DEFAULT store, which is NOT the
|
|
442
|
+
// one DmsFlowCanvas drives, so zoom/fit/updateNodeInternals would silently
|
|
443
|
+
// no-op. Only the handful of imperative methods we actually use are typed.
|
|
444
|
+
interface FlowController {
|
|
445
|
+
zoomIn: () => void
|
|
446
|
+
zoomOut: () => void
|
|
447
|
+
fitView: () => void
|
|
448
|
+
zoomTo: (zoom: number) => void
|
|
449
|
+
updateNodeInternals: (ids?: string | string[]) => void
|
|
450
|
+
// DmsFlowCanvas `defineExpose`s the VueFlow controller; Vue's expose proxy
|
|
451
|
+
// unwraps the `viewport` ref, so consumers read `.viewport.zoom` directly
|
|
452
|
+
// (NOT `.viewport.value.zoom` — that extra `.value` is undefined and pinned
|
|
453
|
+
// the zoom readout at 100%).
|
|
454
|
+
viewport: { zoom: number }
|
|
455
|
+
}
|
|
456
|
+
const flowRef = ref<FlowController | null>(null)
|
|
457
|
+
|
|
458
|
+
// Vue Flow caches each node's handle bounding boxes in its internal store and
|
|
459
|
+
// only re-measures when `updateNodeInternals(id)` is called. When a port is
|
|
460
|
+
// renamed or moved between the title bar and body (e.g. renaming a port to
|
|
461
|
+
// "main" moves the handle from a body row into the header), the Handle DOM
|
|
462
|
+
// element ends up in a different on-screen position but the cached bbox stays
|
|
463
|
+
// stale — connected edges keep drawing to the OLD coordinates until the next
|
|
464
|
+
// re-measure.
|
|
465
|
+
//
|
|
466
|
+
// To keep edges glued to handles, watch a signature derived from the per-node
|
|
467
|
+
// port lists / kinds and force a re-measure of every node after the DOM has
|
|
468
|
+
// applied the new template. Watching the full `props.graph` would fire on
|
|
469
|
+
// every drag (each emitGraph returns a fresh object) — costly and pointless,
|
|
470
|
+
// since node positions trigger their own internal updates already. The
|
|
471
|
+
// signature only changes when port shape changes, which is exactly when we
|
|
472
|
+
// need to re-measure.
|
|
473
|
+
const portsSignature = computed(() => {
|
|
474
|
+
const own = props.graph.nodes
|
|
475
|
+
.map((n) => {
|
|
476
|
+
const ports = (n as AppGraphNode & { ports?: unknown }).ports
|
|
477
|
+
const portsKey = Array.isArray(ports) ? JSON.stringify(ports) : ''
|
|
478
|
+
return `${n.id}:${n.kind}:${portsKey}`
|
|
479
|
+
})
|
|
480
|
+
.join('|')
|
|
481
|
+
// Sentinel nodes (groupInput / groupOutput) inside this canvas render
|
|
482
|
+
// handles derived from `parentGroupPorts`, not from their own node data.
|
|
483
|
+
// A rename on the outer group's port list re-renders the sentinel but
|
|
484
|
+
// doesn't show up in `props.graph.nodes` above, so we must also fold
|
|
485
|
+
// the parent ports into the signature.
|
|
486
|
+
const parent = Array.isArray(props.parentGroupPorts)
|
|
487
|
+
? JSON.stringify(props.parentGroupPorts)
|
|
488
|
+
: ''
|
|
489
|
+
return `${own}#${parent}`
|
|
490
|
+
})
|
|
491
|
+
watch(portsSignature, () => {
|
|
492
|
+
nextTick(() => {
|
|
493
|
+
for (const node of props.graph.nodes) {
|
|
494
|
+
flowRef.value?.updateNodeInternals(node.id)
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
const zoomPercent = computed(() => {
|
|
500
|
+
const zoom = flowRef.value?.viewport.zoom ?? 1
|
|
501
|
+
return `${Math.round(zoom * 100)}%`
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
function resetZoom() {
|
|
505
|
+
flowRef.value?.zoomTo(1)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function onConnect(conn: Connection) {
|
|
509
|
+
if (props.readOnly) return
|
|
510
|
+
const kind = connectionKind(conn.sourceHandle)
|
|
511
|
+
const result = canConnect(
|
|
512
|
+
props.graph,
|
|
513
|
+
props.nodeKinds,
|
|
514
|
+
props.triggerTypes,
|
|
515
|
+
props.actionTypes,
|
|
516
|
+
props.dataNodeTypes,
|
|
517
|
+
conn,
|
|
518
|
+
{ parentGroupPorts: props.parentGroupPorts },
|
|
519
|
+
)
|
|
520
|
+
if (!result.ok) {
|
|
521
|
+
toast.add({
|
|
522
|
+
title: 'Cannot connect',
|
|
523
|
+
description: result.reason,
|
|
524
|
+
color: 'warning',
|
|
525
|
+
icon: 'i-ph-warning',
|
|
526
|
+
})
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
if (kind === 'trigger') {
|
|
530
|
+
const branch = parseSourceBranch(conn.sourceHandle)
|
|
531
|
+
const toBranch = parseTargetBranch(conn.targetHandle)
|
|
532
|
+
const newEdge: TriggerEdge = {
|
|
533
|
+
id: newId(),
|
|
534
|
+
from: { node: conn.source, branch },
|
|
535
|
+
to: toBranch !== undefined
|
|
536
|
+
? { node: conn.target, branch: toBranch }
|
|
537
|
+
: { node: conn.target },
|
|
538
|
+
}
|
|
539
|
+
// Each source/branch carries exactly one outgoing trigger edge.
|
|
540
|
+
// Connecting again replaces any existing edge from that source/branch.
|
|
541
|
+
// (Branching across if/switch named branches still works because each
|
|
542
|
+
// branch has its own `from.branch` and only conflicts with itself.)
|
|
543
|
+
const remaining = props.graph.triggerEdges.filter(
|
|
544
|
+
(e) =>
|
|
545
|
+
!(
|
|
546
|
+
e.from.node === conn.source &&
|
|
547
|
+
(e.from.branch ?? 'main') === branch
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
emitGraph({
|
|
551
|
+
...props.graph,
|
|
552
|
+
triggerEdges: [...remaining, newEdge],
|
|
553
|
+
})
|
|
554
|
+
} else if (kind === 'data') {
|
|
555
|
+
const port = parseDataPort(conn.sourceHandle)
|
|
556
|
+
const field = parseDataField(conn.targetHandle)
|
|
557
|
+
if (!port || !field) return
|
|
558
|
+
const newEdge: DataEdge = {
|
|
559
|
+
id: newId(),
|
|
560
|
+
from: { node: conn.source, port },
|
|
561
|
+
to: { node: conn.target, field },
|
|
562
|
+
}
|
|
563
|
+
// Overwrite: if the target field is already wired, drop the existing
|
|
564
|
+
// data edge before adding the new one.
|
|
565
|
+
const remaining = props.graph.dataEdges.filter(
|
|
566
|
+
(e) => !(e.to.node === conn.target && e.to.field === field),
|
|
567
|
+
)
|
|
568
|
+
emitGraph({
|
|
569
|
+
...props.graph,
|
|
570
|
+
dataEdges: [...remaining, newEdge],
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function isValidConnection(conn: Connection): boolean {
|
|
576
|
+
return canConnect(
|
|
577
|
+
props.graph,
|
|
578
|
+
props.nodeKinds,
|
|
579
|
+
props.triggerTypes,
|
|
580
|
+
props.actionTypes,
|
|
581
|
+
props.dataNodeTypes,
|
|
582
|
+
conn,
|
|
583
|
+
{ parentGroupPorts: props.parentGroupPorts },
|
|
584
|
+
).ok
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Multi-selection tracking — Vue Flow emits one NodeSelectionChange per
|
|
588
|
+
// affected node when the user box-selects or shift-clicks. We accumulate
|
|
589
|
+
// the current set here and emit a single `selection-change` event with
|
|
590
|
+
// the full list (sentinels filtered out — the user shouldn't be able to
|
|
591
|
+
// fold a sentinel into a new group). The single-node `select:node` event
|
|
592
|
+
// is preserved for the inspector (it expects exactly one node id).
|
|
593
|
+
const selectedNodeIds = new Set<string>()
|
|
594
|
+
|
|
595
|
+
// Drop any tracked ids that no longer exist in the current graph (e.g.
|
|
596
|
+
// after navigating to a different procedure / subgraph, or after the
|
|
597
|
+
// "Group selection" action removes the selected nodes). Re-emit if the
|
|
598
|
+
// set shrank. We can't `watch(() => props.graph, ...)` because that
|
|
599
|
+
// fires on every node drag (each emitGraph returns a fresh object).
|
|
600
|
+
watch(
|
|
601
|
+
() => props.graph.nodes,
|
|
602
|
+
(nodes) => {
|
|
603
|
+
if (selectedNodeIds.size === 0) return
|
|
604
|
+
const valid = new Set(nodes.map((n) => n.id))
|
|
605
|
+
let pruned = false
|
|
606
|
+
for (const id of [...selectedNodeIds]) {
|
|
607
|
+
if (!valid.has(id)) {
|
|
608
|
+
selectedNodeIds.delete(id)
|
|
609
|
+
pruned = true
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (pruned) {
|
|
613
|
+
const ids = [...selectedNodeIds].filter((id) => {
|
|
614
|
+
const n = nodes.find((nn) => nn.id === id)
|
|
615
|
+
return n ? !SENTINEL_KINDS.has(n.kind) : false
|
|
616
|
+
})
|
|
617
|
+
emit('selection-change', { ids })
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
function onNodesChange(changes: NodeChange[]) {
|
|
623
|
+
let nextNodes = props.graph.nodes
|
|
624
|
+
let nextTrigger = props.graph.triggerEdges
|
|
625
|
+
let nextData = props.graph.dataEdges
|
|
626
|
+
let changed = false
|
|
627
|
+
let selectionChanged = false
|
|
628
|
+
|
|
629
|
+
for (const c of changes) {
|
|
630
|
+
// In read-only mode, still surface selection so the inspector
|
|
631
|
+
// can show node details; suppress position drag and delete.
|
|
632
|
+
if (props.readOnly && c.type !== 'select') continue
|
|
633
|
+
if (c.type === 'position' && c.position) {
|
|
634
|
+
const pos = c.position
|
|
635
|
+
nextNodes = nextNodes.map((n) =>
|
|
636
|
+
n.id === c.id ? { ...n, position: { x: pos.x, y: pos.y } } : n,
|
|
637
|
+
)
|
|
638
|
+
changed = true
|
|
639
|
+
} else if (c.type === 'remove') {
|
|
640
|
+
// Sentinel nodes (groupInput / groupOutput) are auto-managed by
|
|
641
|
+
// the editor and never user-deletable. Drop the change silently
|
|
642
|
+
// so keyboard delete + right-click delete both no-op on them.
|
|
643
|
+
const victim = nextNodes.find((n) => n.id === c.id)
|
|
644
|
+
if (victim && SENTINEL_KINDS.has(victim.kind)) continue
|
|
645
|
+
nextNodes = nextNodes.filter((n) => n.id !== c.id)
|
|
646
|
+
nextTrigger = nextTrigger.filter(
|
|
647
|
+
(e) => e.from.node !== c.id && e.to.node !== c.id,
|
|
648
|
+
)
|
|
649
|
+
nextData = nextData.filter(
|
|
650
|
+
(e) => e.from.node !== c.id && e.to.node !== c.id,
|
|
651
|
+
)
|
|
652
|
+
if (selectedNodeIds.delete(c.id)) selectionChanged = true
|
|
653
|
+
changed = true
|
|
654
|
+
} else if (c.type === 'select') {
|
|
655
|
+
if (c.selected) {
|
|
656
|
+
if (!selectedNodeIds.has(c.id)) {
|
|
657
|
+
selectedNodeIds.add(c.id)
|
|
658
|
+
selectionChanged = true
|
|
659
|
+
}
|
|
660
|
+
emit('select:node', c.id)
|
|
661
|
+
} else {
|
|
662
|
+
if (selectedNodeIds.delete(c.id)) selectionChanged = true
|
|
663
|
+
// Re-point the single-node inspector on deselect: fall back to a
|
|
664
|
+
// still-selected node (most-recent), or clear it once the
|
|
665
|
+
// selection is empty. A click-swap fires `select: true` above in
|
|
666
|
+
// the same batch, which then overrides this.
|
|
667
|
+
const remaining = [...selectedNodeIds]
|
|
668
|
+
emit('select:node', remaining[remaining.length - 1] ?? null)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (selectionChanged) {
|
|
674
|
+
// Filter out sentinels — folding them into a new group makes no
|
|
675
|
+
// sense and the toolbar gating depends on a clean set.
|
|
676
|
+
const ids = [...selectedNodeIds].filter((id) => {
|
|
677
|
+
const n = props.graph.nodes.find((nn) => nn.id === id)
|
|
678
|
+
return n ? !SENTINEL_KINDS.has(n.kind) : false
|
|
679
|
+
})
|
|
680
|
+
emit('selection-change', { ids })
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (changed) {
|
|
684
|
+
emitGraph({
|
|
685
|
+
...props.graph,
|
|
686
|
+
nodes: nextNodes,
|
|
687
|
+
triggerEdges: nextTrigger,
|
|
688
|
+
dataEdges: nextData,
|
|
689
|
+
})
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// DmsFlowCanvas's delete affordance (trash badge on the selected node + the
|
|
694
|
+
// Delete/Backspace key path it now owns) is emit-only: it hands us the ids and
|
|
695
|
+
// we perform the mutation. Route them through onNodesChange's `remove` path so
|
|
696
|
+
// sentinel filtering, edge cleanup, selection sync and emitGraph stay in one
|
|
697
|
+
// place.
|
|
698
|
+
function onDeleteNodes(ids: string[]) {
|
|
699
|
+
if (props.readOnly) return
|
|
700
|
+
onNodesChange(ids.map((id) => ({ type: 'remove', id }) as NodeChange))
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function onEdgesChange(changes: EdgeChange[]) {
|
|
704
|
+
if (props.readOnly) return
|
|
705
|
+
let nextTrigger = props.graph.triggerEdges
|
|
706
|
+
let nextData = props.graph.dataEdges
|
|
707
|
+
let changed = false
|
|
708
|
+
|
|
709
|
+
for (const c of changes) {
|
|
710
|
+
if (c.type === 'remove') {
|
|
711
|
+
const beforeT = nextTrigger.length
|
|
712
|
+
nextTrigger = nextTrigger.filter((e) => e.id !== c.id)
|
|
713
|
+
if (nextTrigger.length !== beforeT) {
|
|
714
|
+
changed = true
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
const beforeD = nextData.length
|
|
718
|
+
nextData = nextData.filter((e) => e.id !== c.id)
|
|
719
|
+
if (nextData.length !== beforeD) changed = true
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (changed) {
|
|
724
|
+
emitGraph({
|
|
725
|
+
...props.graph,
|
|
726
|
+
triggerEdges: nextTrigger,
|
|
727
|
+
dataEdges: nextData,
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Edge-update flow (drag an existing edge's endpoint to reattach or delete):
|
|
733
|
+
// 1. @edge-update-start clears the success flag.
|
|
734
|
+
// 2. @edge-update fires only if the user drops on a valid target — mark
|
|
735
|
+
// successful and rewrite the edge with the new connection.
|
|
736
|
+
// 3. @edge-update-end fires unconditionally; if no successful update
|
|
737
|
+
// happened, the user dropped in empty space → delete the edge.
|
|
738
|
+
let edgeUpdateSuccessful = false
|
|
739
|
+
|
|
740
|
+
function onEdgeUpdateStart() {
|
|
741
|
+
edgeUpdateSuccessful = false
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function onEdgeUpdate(payload: {
|
|
745
|
+
event: MouseEvent
|
|
746
|
+
edge: VfEdge
|
|
747
|
+
connection: Connection
|
|
748
|
+
}) {
|
|
749
|
+
edgeUpdateSuccessful = true
|
|
750
|
+
const { edge, connection } = payload
|
|
751
|
+
const kind = (edge.data as { kind?: 'trigger' | 'data' } | undefined)?.kind
|
|
752
|
+
if (kind === 'trigger') {
|
|
753
|
+
const branch = parseSourceBranch(connection.sourceHandle)
|
|
754
|
+
const toBranch = parseTargetBranch(connection.targetHandle)
|
|
755
|
+
const nextTrigger = props.graph.triggerEdges.map((e) =>
|
|
756
|
+
e.id === edge.id
|
|
757
|
+
? {
|
|
758
|
+
...e,
|
|
759
|
+
from: { node: connection.source, branch },
|
|
760
|
+
to: toBranch !== undefined
|
|
761
|
+
? { node: connection.target, branch: toBranch }
|
|
762
|
+
: { node: connection.target },
|
|
763
|
+
}
|
|
764
|
+
: e,
|
|
765
|
+
)
|
|
766
|
+
emitGraph({ ...props.graph, triggerEdges: nextTrigger })
|
|
767
|
+
} else if (kind === 'data') {
|
|
768
|
+
const port = parseDataPort(connection.sourceHandle)
|
|
769
|
+
const field = parseDataField(connection.targetHandle)
|
|
770
|
+
if (!port || !field) return
|
|
771
|
+
const nextData = props.graph.dataEdges.map((e) =>
|
|
772
|
+
e.id === edge.id
|
|
773
|
+
? {
|
|
774
|
+
...e,
|
|
775
|
+
from: { node: connection.source, port },
|
|
776
|
+
to: { node: connection.target, field },
|
|
777
|
+
}
|
|
778
|
+
: e,
|
|
779
|
+
)
|
|
780
|
+
emitGraph({ ...props.graph, dataEdges: nextData })
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Vue Flow fires @node-double-click with a single `{ event, node }` payload.
|
|
785
|
+
// Filter to group nodes so Editor.vue receives a clean group:enter signal —
|
|
786
|
+
// double-clicking any other node kind is reserved for future inline edit
|
|
787
|
+
// affordances.
|
|
788
|
+
function onNodeDoubleClick(payload: {
|
|
789
|
+
event: MouseEvent
|
|
790
|
+
node: { id: string; type?: string }
|
|
791
|
+
}) {
|
|
792
|
+
if (payload.node?.type !== 'group') return
|
|
793
|
+
emit('group:enter', { id: payload.node.id })
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function onEdgeUpdateEnd(payload: { event: MouseEvent; edge: VfEdge }) {
|
|
797
|
+
if (edgeUpdateSuccessful) return
|
|
798
|
+
const { edge } = payload
|
|
799
|
+
if (!edge) return
|
|
800
|
+
const nextTrigger = props.graph.triggerEdges.filter((e) => e.id !== edge.id)
|
|
801
|
+
const nextData = props.graph.dataEdges.filter((e) => e.id !== edge.id)
|
|
802
|
+
if (
|
|
803
|
+
nextTrigger.length === props.graph.triggerEdges.length &&
|
|
804
|
+
nextData.length === props.graph.dataEdges.length
|
|
805
|
+
) {
|
|
806
|
+
return
|
|
807
|
+
}
|
|
808
|
+
emitGraph({
|
|
809
|
+
...props.graph,
|
|
810
|
+
triggerEdges: nextTrigger,
|
|
811
|
+
dataEdges: nextData,
|
|
812
|
+
})
|
|
813
|
+
}
|
|
814
|
+
</script>
|
|
815
|
+
|
|
816
|
+
<template>
|
|
817
|
+
<div
|
|
818
|
+
class="graph-canvas-wrap relative w-full h-full border border-default rounded-lg overflow-hidden"
|
|
819
|
+
>
|
|
820
|
+
<DmsFlowCanvas
|
|
821
|
+
ref="flowRef"
|
|
822
|
+
:nodes="vfNodes"
|
|
823
|
+
:edges="vfEdges"
|
|
824
|
+
:node-types="nodeTypes"
|
|
825
|
+
:controls="false"
|
|
826
|
+
:is-valid-connection="isValidConnection"
|
|
827
|
+
:nodes-draggable="!readOnly"
|
|
828
|
+
:nodes-connectable="!readOnly"
|
|
829
|
+
:elements-selectable="true"
|
|
830
|
+
:edges-updatable="!readOnly"
|
|
831
|
+
:deletable-nodes="!readOnly"
|
|
832
|
+
@connect="onConnect"
|
|
833
|
+
@nodes-change="onNodesChange"
|
|
834
|
+
@delete-nodes="onDeleteNodes"
|
|
835
|
+
@edges-change="onEdgesChange"
|
|
836
|
+
@edge-update-start="onEdgeUpdateStart"
|
|
837
|
+
@edge-update="onEdgeUpdate"
|
|
838
|
+
@edge-update-end="onEdgeUpdateEnd"
|
|
839
|
+
@node-double-click="onNodeDoubleClick"
|
|
840
|
+
>
|
|
841
|
+
<!-- Toolbar. While editable: Add Node / Undo / Redo / group
|
|
842
|
+
actions / Cancel / Apply. Read-only template-instance view:
|
|
843
|
+
only the group actions (Fork) survive, in a bare card. The two
|
|
844
|
+
branches share DmsFlowCanvas's single `top-right` Panel slot. -->
|
|
845
|
+
<template v-if="!readOnly || $slots.groupActions" #top-right>
|
|
846
|
+
<div
|
|
847
|
+
v-if="!readOnly"
|
|
848
|
+
class="flex items-center gap-1 rounded-lg border border-default bg-elevated p-1 shadow-md flex-wrap"
|
|
849
|
+
>
|
|
850
|
+
<UButton
|
|
851
|
+
size="sm"
|
|
852
|
+
variant="ghost"
|
|
853
|
+
color="neutral"
|
|
854
|
+
icon="i-ph-arrow-counter-clockwise"
|
|
855
|
+
:aria-label="$t('dms_automation.editor.canvas.undo')"
|
|
856
|
+
:disabled="!canUndo"
|
|
857
|
+
@click="emit('undo')"
|
|
858
|
+
/>
|
|
859
|
+
<UButton
|
|
860
|
+
size="sm"
|
|
861
|
+
variant="ghost"
|
|
862
|
+
color="neutral"
|
|
863
|
+
icon="i-ph-arrow-clockwise"
|
|
864
|
+
:aria-label="$t('dms_automation.editor.canvas.redo')"
|
|
865
|
+
:disabled="!canRedo"
|
|
866
|
+
@click="emit('redo')"
|
|
867
|
+
/>
|
|
868
|
+
<!-- Group-related actions slotted in by Editor.vue: Group
|
|
869
|
+
selection, Save as template, Fork to local group, Enter
|
|
870
|
+
group. Kept inside the canvas toolbar so they sit next
|
|
871
|
+
to Add Node / Undo / Redo instead of in a separate card. -->
|
|
872
|
+
<template v-if="$slots.groupActions">
|
|
873
|
+
<div class="w-px h-5 bg-default mx-1" />
|
|
874
|
+
<slot name="groupActions" />
|
|
875
|
+
</template>
|
|
876
|
+
<div class="mx-1 h-5 w-px bg-default" />
|
|
877
|
+
<UButton
|
|
878
|
+
size="sm"
|
|
879
|
+
variant="ghost"
|
|
880
|
+
color="neutral"
|
|
881
|
+
:label="$t('dms_automation.editor.canvas.cancel')"
|
|
882
|
+
:disabled="!isDirty || applying"
|
|
883
|
+
@click="emit('cancel')"
|
|
884
|
+
/>
|
|
885
|
+
<UButton
|
|
886
|
+
size="sm"
|
|
887
|
+
color="primary"
|
|
888
|
+
:label="$t('dms_automation.editor.canvas.apply')"
|
|
889
|
+
:disabled="!isDirty"
|
|
890
|
+
:loading="applying"
|
|
891
|
+
@click="emit('apply')"
|
|
892
|
+
/>
|
|
893
|
+
</div>
|
|
894
|
+
<!-- Read-only fallback: Fork stays reachable from the
|
|
895
|
+
template-instance inner-subgraph view. -->
|
|
896
|
+
<div
|
|
897
|
+
v-else-if="$slots.groupActions"
|
|
898
|
+
class="flex items-center gap-1 rounded-lg border border-default bg-elevated p-1 shadow-md flex-wrap"
|
|
899
|
+
>
|
|
900
|
+
<slot name="groupActions" />
|
|
901
|
+
</div>
|
|
902
|
+
</template>
|
|
903
|
+
|
|
904
|
+
<!-- The #top-left slot is provided UNCONDITIONALLY (inner v-if gates
|
|
905
|
+
the content). DmsFlowCanvas computes its active panel positions
|
|
906
|
+
once from a non-reactive useSlots() snapshot, so a panel slot
|
|
907
|
+
added later (e.g. when a node is first selected) would never
|
|
908
|
+
render. Providing it from first render keeps the inspector panel
|
|
909
|
+
alive; it just shows nothing until a node is selected. The real
|
|
910
|
+
fix belongs upstream in DmsFlowCanvas (make panel detection
|
|
911
|
+
reactive); this is a consumer workaround. -->
|
|
912
|
+
<template #top-left>
|
|
913
|
+
<div
|
|
914
|
+
v-if="$slots.inspector"
|
|
915
|
+
class="w-[320px] max-h-[calc(100vh-16rem)] overflow-y-auto shadow-md rounded-lg"
|
|
916
|
+
>
|
|
917
|
+
<slot name="inspector" />
|
|
918
|
+
</div>
|
|
919
|
+
</template>
|
|
920
|
+
|
|
921
|
+
<template #bottom-center>
|
|
922
|
+
<div
|
|
923
|
+
class="flex items-center gap-1 rounded-lg border border-default bg-elevated p-1 shadow-md"
|
|
924
|
+
>
|
|
925
|
+
<UButton
|
|
926
|
+
size="sm"
|
|
927
|
+
variant="ghost"
|
|
928
|
+
color="neutral"
|
|
929
|
+
icon="i-ph-magnifying-glass-minus"
|
|
930
|
+
:aria-label="$t('dms_automation.editor.canvas.zoomOut')"
|
|
931
|
+
@click="flowRef?.zoomOut()"
|
|
932
|
+
/>
|
|
933
|
+
<button
|
|
934
|
+
type="button"
|
|
935
|
+
class="px-2 text-sm tabular-nums text-muted transition-colors hover:text-highlighted"
|
|
936
|
+
:aria-label="$t('dms_automation.editor.canvas.resetZoom')"
|
|
937
|
+
@click="resetZoom"
|
|
938
|
+
>
|
|
939
|
+
{{ zoomPercent }}
|
|
940
|
+
</button>
|
|
941
|
+
<UButton
|
|
942
|
+
size="sm"
|
|
943
|
+
variant="ghost"
|
|
944
|
+
color="neutral"
|
|
945
|
+
icon="i-ph-magnifying-glass-plus"
|
|
946
|
+
:aria-label="$t('dms_automation.editor.canvas.zoomIn')"
|
|
947
|
+
@click="flowRef?.zoomIn()"
|
|
948
|
+
/>
|
|
949
|
+
<div class="mx-1 h-5 w-px bg-default" />
|
|
950
|
+
<UButton
|
|
951
|
+
size="sm"
|
|
952
|
+
variant="ghost"
|
|
953
|
+
color="neutral"
|
|
954
|
+
icon="i-ph-arrows-out"
|
|
955
|
+
:aria-label="$t('dms_automation.editor.canvas.fitView')"
|
|
956
|
+
@click="flowRef?.fitView()"
|
|
957
|
+
/>
|
|
958
|
+
</div>
|
|
959
|
+
</template>
|
|
960
|
+
</DmsFlowCanvas>
|
|
961
|
+
</div>
|
|
962
|
+
</template>
|
|
963
|
+
|
|
964
|
+
<style>
|
|
965
|
+
/* DmsFlowCanvas imports the @vue-flow core/theme/controls/minimap CSS itself,
|
|
966
|
+
so we only ship the editor's business edge palette here: trigger edges
|
|
967
|
+
(control flow) read cyan, data edges (data flow) read green-dashed. The
|
|
968
|
+
`.edge-*` class out-specifies DmsFlowCanvas's generic `.vue-flow__edge-path`
|
|
969
|
+
rule, so these win without `!important`. */
|
|
970
|
+
.vue-flow__edge.edge-trigger .vue-flow__edge-path {
|
|
971
|
+
stroke: #2dc1cf;
|
|
972
|
+
stroke-width: 2;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.vue-flow__edge.edge-data .vue-flow__edge-path {
|
|
976
|
+
stroke: #10b981;
|
|
977
|
+
stroke-width: 2;
|
|
978
|
+
stroke-dasharray: 4 4;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/* Selected / focused edge highlight (amber). The doubled `.edge-*` class lifts
|
|
982
|
+
specificity above DmsFlowCanvas's own `.dms-flow-canvas .vue-flow__edge.selected`
|
|
983
|
+
rule, which would otherwise repaint the edge in the DMS accent and erase the
|
|
984
|
+
trigger-vs-data distinction. */
|
|
985
|
+
.vue-flow__edge.edge-trigger.selected .vue-flow__edge-path,
|
|
986
|
+
.vue-flow__edge.edge-data.selected .vue-flow__edge-path,
|
|
987
|
+
.vue-flow__edge.edge-trigger:focus .vue-flow__edge-path,
|
|
988
|
+
.vue-flow__edge.edge-data:focus .vue-flow__edge-path,
|
|
989
|
+
.vue-flow__edge.edge-trigger:focus-visible .vue-flow__edge-path,
|
|
990
|
+
.vue-flow__edge.edge-data:focus-visible .vue-flow__edge-path {
|
|
991
|
+
stroke: #f59e0b;
|
|
992
|
+
stroke-width: 3;
|
|
993
|
+
}
|
|
994
|
+
</style>
|