@revenexx/studio-integrations 0.5.0 → 0.6.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.
- package/CHANGELOG.md +201 -0
- package/README.md +17 -4
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
- package/dist/runtime/components/Integrations/Layout.vue +21 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
- package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
- package/dist/runtime/composables/useCredentialTypes.js +4 -4
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
- package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
- package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationTemplates.d.ts +39 -0
- package/dist/runtime/data/integrationTemplates.js +15 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +27 -3
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -5
- package/dist/runtime/pages/integrations/audit.vue +91 -6
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
- package/dist/runtime/pages/integrations/credentials.vue +119 -21
- package/dist/runtime/pages/integrations/index.vue +49 -44
- package/dist/runtime/pages/integrations/runs.vue +1 -1
- package/dist/runtime/pages/integrations/schedules.vue +51 -7
- package/dist/runtime/pages/integrations/secrets.vue +29 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
- package/dist/runtime/pages/integrations/templates/index.vue +57 -86
- package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
- package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +29 -0
- package/dist/runtime/utils/localize.js +4 -0
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +141 -0
- package/dist/runtime/utils/schedule.js +207 -0
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +53 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/package.json +8 -2
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { computed, markRaw, nextTick, provide, ref, watch } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
ConnectionLineType,
|
|
5
|
+
ConnectionMode,
|
|
5
6
|
MarkerType,
|
|
6
|
-
PanOnScrollMode,
|
|
7
7
|
SelectionMode,
|
|
8
8
|
VueFlow,
|
|
9
9
|
useVueFlow
|
|
@@ -48,9 +48,17 @@ import { toast } from "vue-sonner";
|
|
|
48
48
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
49
49
|
import { layoutWorkflowNodes } from "./workflowLayout";
|
|
50
50
|
import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
|
|
51
|
+
import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
|
|
52
|
+
import { portFromHandleId } from "../../data/handleId";
|
|
53
|
+
import { groupFindingsByNode } from "../../utils/validationFindings";
|
|
54
|
+
import { reorderBranchSubtrees } from "../../data/branchReorder";
|
|
55
|
+
import { findingsByNodeField } from "../../utils/validationFindings";
|
|
56
|
+
import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
51
57
|
const props = defineProps({
|
|
52
58
|
workflow: { type: Object, required: true },
|
|
53
|
-
runBusy: { type: Boolean, required: false }
|
|
59
|
+
runBusy: { type: Boolean, required: false },
|
|
60
|
+
runWaiting: { type: Boolean, required: false },
|
|
61
|
+
overlayOpen: { type: Boolean, required: false }
|
|
54
62
|
});
|
|
55
63
|
const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
|
|
56
64
|
const { keys: secretKeys } = useIntegrationsSecrets();
|
|
@@ -65,7 +73,20 @@ function clone(v) {
|
|
|
65
73
|
const vfNodes = ref([]);
|
|
66
74
|
const vfEdges = ref([]);
|
|
67
75
|
const flow = useVueFlow();
|
|
76
|
+
const invalidNodeMessages = computed(() => findingsByNodeField(props.workflow.warnings ?? [], props.workflow.blob.nodes));
|
|
68
77
|
const dynamicOutputs = ref({});
|
|
78
|
+
const invalidNodeFields = computed(() => {
|
|
79
|
+
const out = {};
|
|
80
|
+
for (const g of groupFindingsByNode(props.workflow.warnings ?? [], props.workflow.blob.nodes, () => "")) {
|
|
81
|
+
if (g.nodeId) out[g.nodeId] = g.fields;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
});
|
|
85
|
+
let dragInFlight = false;
|
|
86
|
+
watch(() => JSON.stringify(invalidNodeFields.value), () => {
|
|
87
|
+
if (dragInFlight) return;
|
|
88
|
+
syncFromBlob();
|
|
89
|
+
});
|
|
69
90
|
const dynamicOutputNodes = computed(() => blob.value.nodes.filter((n) => {
|
|
70
91
|
const m = findManifest(n.nodeSlug, n.nodeVersion);
|
|
71
92
|
return (m?.outputs ?? []).some((o) => o.resolveOutputs === true);
|
|
@@ -78,6 +99,7 @@ function edgeKindFromPort(sourceNodeId, output) {
|
|
|
78
99
|
if (output === "true") return "branch-true";
|
|
79
100
|
if (output === "false") return "branch-false";
|
|
80
101
|
if (output === "error") return "error";
|
|
102
|
+
if (output === "done") return "success";
|
|
81
103
|
const portDef = (manifest.outputs ?? []).find((o) => o.name === output);
|
|
82
104
|
if (portDef) {
|
|
83
105
|
if (portDef.kind === "error") return "error";
|
|
@@ -92,10 +114,10 @@ function edgeKindFromPort(sourceNodeId, output) {
|
|
|
92
114
|
}
|
|
93
115
|
return "default";
|
|
94
116
|
}
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return
|
|
117
|
+
function orderedOutputPorts(nodeId) {
|
|
118
|
+
const wn = blob.value.nodes.find((n) => n.id === nodeId);
|
|
119
|
+
if (!wn) return ["default"];
|
|
120
|
+
return portsForNode(wn, findManifest(wn.nodeSlug, wn.nodeVersion), dynamicOutputs.value[nodeId]).map((p) => p.name);
|
|
99
121
|
}
|
|
100
122
|
function triggerPosition(id, index) {
|
|
101
123
|
const saved = blob.value.triggerPositions?.[id];
|
|
@@ -120,10 +142,6 @@ function stickyArea(s) {
|
|
|
120
142
|
}
|
|
121
143
|
const GROUP_PAD = 24;
|
|
122
144
|
const GROUP_LABEL_ROOM = 26;
|
|
123
|
-
function locText(v) {
|
|
124
|
-
if (!v) return "";
|
|
125
|
-
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
126
|
-
}
|
|
127
145
|
function memberDims(id) {
|
|
128
146
|
const d = flow.findNode(id)?.dimensions;
|
|
129
147
|
return d && d.width > 0 ? d : { width: 220, height: 64 };
|
|
@@ -261,7 +279,11 @@ function syncFromBlob() {
|
|
|
261
279
|
type: "workflow",
|
|
262
280
|
position: n.position ?? { x: 240, y: 80 },
|
|
263
281
|
class: bornClass(n.id),
|
|
264
|
-
data: {
|
|
282
|
+
data: {
|
|
283
|
+
...n,
|
|
284
|
+
__resolvedOutputs: dynamicOutputs.value[n.id],
|
|
285
|
+
__invalidFields: invalidNodeFields.value[n.id]
|
|
286
|
+
}
|
|
265
287
|
}))
|
|
266
288
|
];
|
|
267
289
|
const seenEdge = /* @__PURE__ */ new Set();
|
|
@@ -284,6 +306,18 @@ function syncFromBlob() {
|
|
|
284
306
|
targetHandle: tgtGroup ? "__in__top" : vf.targetHandle
|
|
285
307
|
});
|
|
286
308
|
}
|
|
309
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
310
|
+
for (const e of mapped) {
|
|
311
|
+
const list = bySource.get(e.source) ?? [];
|
|
312
|
+
list.push(e);
|
|
313
|
+
bySource.set(e.source, list);
|
|
314
|
+
}
|
|
315
|
+
for (const list of bySource.values()) {
|
|
316
|
+
list.sort((a, b) => (a.data?.portIndex ?? 0) - (b.data?.portIndex ?? 0));
|
|
317
|
+
list.forEach((e, i) => {
|
|
318
|
+
e.data = { ...e.data, laneIndex: i, laneCount: list.length };
|
|
319
|
+
});
|
|
320
|
+
}
|
|
287
321
|
vfEdges.value = mapped;
|
|
288
322
|
void nextTick(() => flow.updateNodeInternals(vfNodes.value.map((n) => n.id)));
|
|
289
323
|
}
|
|
@@ -291,9 +325,11 @@ function makeVfEdge(id, sourceId, targetId, output) {
|
|
|
291
325
|
const kind = edgeKindFromPort(sourceId, output);
|
|
292
326
|
const wn = blob.value.nodes.find((n) => n.id === sourceId);
|
|
293
327
|
const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
294
|
-
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"
|
|
328
|
+
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(output);
|
|
295
329
|
const srcSide = pickSourceSide(sourceId, targetId);
|
|
296
330
|
const tgtSide = oppositeSide(srcSide);
|
|
331
|
+
const order = orderedOutputPorts(sourceId);
|
|
332
|
+
const portIndex = order.indexOf(output);
|
|
297
333
|
return {
|
|
298
334
|
id,
|
|
299
335
|
source: sourceId,
|
|
@@ -303,7 +339,8 @@ function makeVfEdge(id, sourceId, targetId, output) {
|
|
|
303
339
|
type: "flow",
|
|
304
340
|
data: {
|
|
305
341
|
kind,
|
|
306
|
-
label: isBranchPort ? output : void 0
|
|
342
|
+
label: isBranchPort ? output : void 0,
|
|
343
|
+
portIndex: portIndex < 0 ? 0 : portIndex
|
|
307
344
|
},
|
|
308
345
|
markerEnd: {
|
|
309
346
|
type: MarkerType.ArrowClosed,
|
|
@@ -314,24 +351,21 @@ function makeVfEdge(id, sourceId, targetId, output) {
|
|
|
314
351
|
};
|
|
315
352
|
}
|
|
316
353
|
function edgeColorByKind(kind) {
|
|
317
|
-
|
|
318
|
-
case "success":
|
|
319
|
-
return "#10b981";
|
|
320
|
-
case "error":
|
|
321
|
-
return "#f43f5e";
|
|
322
|
-
case "branch-true":
|
|
323
|
-
return "#10b981";
|
|
324
|
-
case "branch-false":
|
|
325
|
-
return "#f59e0b";
|
|
326
|
-
case "branch":
|
|
327
|
-
return "#8b5cf6";
|
|
328
|
-
default:
|
|
329
|
-
return "#94a3b8";
|
|
330
|
-
}
|
|
354
|
+
return kind === "error" ? "#cbd5e1" : "#94a3b8";
|
|
331
355
|
}
|
|
332
356
|
syncFromBlob();
|
|
333
|
-
|
|
357
|
+
const paneReady = ref(false);
|
|
358
|
+
const pendingReveal = ref(null);
|
|
359
|
+
flow.onPaneReady(() => {
|
|
360
|
+
flow.fitView({ padding: 0.2, maxZoom: 1 });
|
|
361
|
+
paneReady.value = true;
|
|
362
|
+
if (pendingReveal.value) {
|
|
363
|
+
centerOnNode(pendingReveal.value);
|
|
364
|
+
pendingReveal.value = null;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
334
367
|
flow.onNodeDragStart(({ node }) => {
|
|
368
|
+
dragInFlight = true;
|
|
335
369
|
if (node.type !== "group") return;
|
|
336
370
|
const g = blob.value.groups?.find((x) => x.id === node.id);
|
|
337
371
|
if (!g) return;
|
|
@@ -349,6 +383,7 @@ flow.onNodeDrag(({ node }) => {
|
|
|
349
383
|
}
|
|
350
384
|
});
|
|
351
385
|
function persistDraggedNodes(evt) {
|
|
386
|
+
dragInFlight = false;
|
|
352
387
|
const moved = evt.nodes?.length ? evt.nodes : [evt.node];
|
|
353
388
|
for (const n of moved) {
|
|
354
389
|
const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
|
|
@@ -394,7 +429,7 @@ flow.onConnect((conn) => {
|
|
|
394
429
|
connectCompleted = true;
|
|
395
430
|
if (!conn.source || !conn.target) return;
|
|
396
431
|
const id = `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
397
|
-
const output =
|
|
432
|
+
const output = portFromHandleId(conn.sourceHandle) ?? "default";
|
|
398
433
|
blob.value.edges.push({
|
|
399
434
|
id,
|
|
400
435
|
from: { nodeId: conn.source, output },
|
|
@@ -408,7 +443,7 @@ let connectStart = null;
|
|
|
408
443
|
let connectCompleted = false;
|
|
409
444
|
flow.onConnectStart(({ nodeId, handleId, handleType }) => {
|
|
410
445
|
connectCompleted = false;
|
|
411
|
-
connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output:
|
|
446
|
+
connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output: portFromHandleId(handleId) ?? "default" } : null;
|
|
412
447
|
});
|
|
413
448
|
flow.onConnectEnd((event) => {
|
|
414
449
|
const start = connectStart;
|
|
@@ -428,7 +463,7 @@ flow.onEdgeUpdate(({ edge, connection }) => {
|
|
|
428
463
|
if (connection.source === connection.target) return;
|
|
429
464
|
const be = blob.value.edges.find((e) => e.id === edge.id);
|
|
430
465
|
if (!be) return;
|
|
431
|
-
be.from = { nodeId: connection.source, output:
|
|
466
|
+
be.from = { nodeId: connection.source, output: portFromHandleId(connection.sourceHandle) ?? be.from.output };
|
|
432
467
|
be.to = { nodeId: connection.target, input: be.to.input };
|
|
433
468
|
syncFromBlob();
|
|
434
469
|
push();
|
|
@@ -547,9 +582,18 @@ function duplicateBlobNodes(ids) {
|
|
|
547
582
|
});
|
|
548
583
|
}
|
|
549
584
|
provide("workflow-node-actions", {
|
|
585
|
+
configure: (id) => {
|
|
586
|
+
renameIntent.value = false;
|
|
587
|
+
selectedId.value = id;
|
|
588
|
+
},
|
|
550
589
|
duplicate: (id) => duplicateBlobNodes([id]),
|
|
551
590
|
remove: (id) => removeBlobNode(id)
|
|
552
591
|
});
|
|
592
|
+
const controlBar = ref(null);
|
|
593
|
+
provide(
|
|
594
|
+
"workflow-canvas-bottom-inset",
|
|
595
|
+
() => controlBar.value ? controlBar.value.offsetHeight + 12 : 0
|
|
596
|
+
);
|
|
553
597
|
const CLIP_KIND = "revenexx.workflow.nodes";
|
|
554
598
|
async function copyNodes(ids) {
|
|
555
599
|
const nodes = blob.value.nodes.filter((n) => ids.includes(n.id));
|
|
@@ -697,17 +741,33 @@ const selection = computed(() => {
|
|
|
697
741
|
if (!selectedId.value) return null;
|
|
698
742
|
return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t) => t.id === selectedId.value) ?? null;
|
|
699
743
|
});
|
|
700
|
-
|
|
744
|
+
const selectionIsTrigger = computed(() => !!selectedId.value && blob.value.triggers.some((t) => t.id === selectedId.value));
|
|
745
|
+
flow.onNodeDoubleClick((evt) => {
|
|
701
746
|
if (evt.node.type === "sticky" || evt.node.type === "group") return;
|
|
702
747
|
selectedId.value = evt.node.id;
|
|
703
748
|
});
|
|
704
749
|
flow.onPaneClick(() => {
|
|
705
750
|
selectedId.value = null;
|
|
706
751
|
});
|
|
707
|
-
function
|
|
708
|
-
|
|
709
|
-
|
|
752
|
+
function centerOnNode(id) {
|
|
753
|
+
const vf = vfNodes.value.find((n) => n.id === id);
|
|
754
|
+
if (!vf) return;
|
|
755
|
+
const size = flow.findNode(id)?.dimensions;
|
|
756
|
+
void flow.setCenter(
|
|
757
|
+
vf.position.x + (size?.width ?? 220) / 2,
|
|
758
|
+
vf.position.y + (size?.height ?? 76) / 2,
|
|
759
|
+
{ zoom: Math.max(flow.viewport.value?.zoom ?? 1, 0.75), duration: 300 }
|
|
760
|
+
);
|
|
710
761
|
}
|
|
762
|
+
function revealNode(id) {
|
|
763
|
+
if (paneReady.value) centerOnNode(id);
|
|
764
|
+
else pendingReveal.value = id;
|
|
765
|
+
if (blob.value.nodes.some((n) => n.id === id) || blob.value.triggers.some((t) => t.id === id)) {
|
|
766
|
+
renameIntent.value = false;
|
|
767
|
+
selectedId.value = id;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
defineExpose({ revealNode });
|
|
711
771
|
const selectedUpstreamOutputs = computed(() => {
|
|
712
772
|
const id = selectedId.value;
|
|
713
773
|
if (!id) return [];
|
|
@@ -719,7 +779,9 @@ const selectedUpstreamOutputs = computed(() => {
|
|
|
719
779
|
const portDef = (findManifest(source.nodeSlug, source.nodeVersion)?.outputs ?? []).find((o) => o.name === e.from.output);
|
|
720
780
|
return [{
|
|
721
781
|
nodeId: source.id,
|
|
722
|
-
|
|
782
|
+
// A node's own name is text the user typed — never trimmed, never read
|
|
783
|
+
// as JSON. The manifest strings below it get the normalising resolver.
|
|
784
|
+
nodeName: resolveDisplayName(source.name) || source.nodeSlug,
|
|
723
785
|
output: e.from.output,
|
|
724
786
|
fields: portDef?.fields
|
|
725
787
|
}];
|
|
@@ -818,7 +880,6 @@ function addFromPayload(payload, pos) {
|
|
|
818
880
|
adoptIntoGroupAt(id2, at);
|
|
819
881
|
markBorn(id2);
|
|
820
882
|
syncFromBlob();
|
|
821
|
-
selectedId.value = id2;
|
|
822
883
|
push();
|
|
823
884
|
return;
|
|
824
885
|
}
|
|
@@ -835,7 +896,6 @@ function addFromPayload(payload, pos) {
|
|
|
835
896
|
adoptIntoGroupAt(id, at);
|
|
836
897
|
markBorn(id);
|
|
837
898
|
syncFromBlob();
|
|
838
|
-
selectedId.value = id;
|
|
839
899
|
push();
|
|
840
900
|
}
|
|
841
901
|
function onDrop(ev) {
|
|
@@ -870,7 +930,6 @@ function onPaletteInsert(payload) {
|
|
|
870
930
|
});
|
|
871
931
|
markBorn(id);
|
|
872
932
|
syncFromBlob();
|
|
873
|
-
selectedId.value = id;
|
|
874
933
|
push();
|
|
875
934
|
return;
|
|
876
935
|
}
|
|
@@ -902,6 +961,10 @@ function spliceIntoEdge(edgeId, payload) {
|
|
|
902
961
|
const edge = blob.value.edges.find((e) => e.id === edgeId);
|
|
903
962
|
const manifest = findManifest(payload.slug, payload.version);
|
|
904
963
|
if (!edge || !manifest) return;
|
|
964
|
+
if (isTerminalManifest(manifest)) {
|
|
965
|
+
toast.error(`${resolveLocalized(manifest.name) || manifest.slug} ends the run and cannot be inserted into a connection.`);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
905
968
|
const a = nodePosition(edge.from.nodeId);
|
|
906
969
|
const b = nodePosition(edge.to.nodeId);
|
|
907
970
|
const id = `n-${Date.now()}`;
|
|
@@ -920,7 +983,6 @@ function spliceIntoEdge(edgeId, payload) {
|
|
|
920
983
|
);
|
|
921
984
|
markBorn(id);
|
|
922
985
|
syncFromBlob();
|
|
923
|
-
selectedId.value = id;
|
|
924
986
|
push();
|
|
925
987
|
}
|
|
926
988
|
function defaultConfigFor(fields = []) {
|
|
@@ -934,6 +996,34 @@ function nodeDims(id) {
|
|
|
934
996
|
const d = flow.findNode(id)?.dimensions;
|
|
935
997
|
return d && d.width > 0 ? d : { width: 220, height: 64 };
|
|
936
998
|
}
|
|
999
|
+
function reorderBranchesByHandle(laid, memberToGroup) {
|
|
1000
|
+
const posById = new Map(laid.map((n) => [n.id, n]));
|
|
1001
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1002
|
+
for (const n of laid) {
|
|
1003
|
+
if (!memberToGroup.has(n.id)) nodes.set(n.id, { x: n.position.x });
|
|
1004
|
+
}
|
|
1005
|
+
const portOrderCache = /* @__PURE__ */ new Map();
|
|
1006
|
+
const portsFor = (id) => {
|
|
1007
|
+
let o = portOrderCache.get(id);
|
|
1008
|
+
if (!o) {
|
|
1009
|
+
o = orderedOutputPorts(id);
|
|
1010
|
+
portOrderCache.set(id, o);
|
|
1011
|
+
}
|
|
1012
|
+
return o;
|
|
1013
|
+
};
|
|
1014
|
+
const edges = [];
|
|
1015
|
+
for (const e of vfEdges.value) {
|
|
1016
|
+
if (!nodes.has(e.source) || !nodes.has(e.target)) continue;
|
|
1017
|
+
const port = portFromHandleId(e.sourceHandle) ?? "default";
|
|
1018
|
+
const order = portsFor(e.source);
|
|
1019
|
+
const idx = order.indexOf(port);
|
|
1020
|
+
edges.push({ source: e.source, target: e.target, portIndex: idx < 0 ? order.length : idx });
|
|
1021
|
+
}
|
|
1022
|
+
for (const [id, x] of reorderBranchSubtrees(nodes, edges)) {
|
|
1023
|
+
const n = posById.get(id);
|
|
1024
|
+
n.position = { x, y: n.position.y };
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
937
1027
|
async function tidyUp() {
|
|
938
1028
|
const containment = /* @__PURE__ */ new Map();
|
|
939
1029
|
for (const s of blob.value.stickies ?? []) {
|
|
@@ -1013,6 +1103,7 @@ async function tidyUp() {
|
|
|
1013
1103
|
if (!inner) return n;
|
|
1014
1104
|
return { ...n, position: { x: anchor.x + GROUP_PAD + inner.position.x, y: anchor.y + GROUP_PAD + GROUP_LABEL_ROOM + inner.position.y } };
|
|
1015
1105
|
});
|
|
1106
|
+
reorderBranchesByHandle(laid, memberToGroup);
|
|
1016
1107
|
for (const n of laid) {
|
|
1017
1108
|
const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
|
|
1018
1109
|
if (n.type === "workflow") {
|
|
@@ -1119,6 +1210,7 @@ async function focusPalette() {
|
|
|
1119
1210
|
paletteRef.value?.focusSearch();
|
|
1120
1211
|
}
|
|
1121
1212
|
const lightboxOpen = computed(() => !!selection.value);
|
|
1213
|
+
const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
|
|
1122
1214
|
const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
|
|
1123
1215
|
useCanvasShortcuts(() => ({
|
|
1124
1216
|
"enter": {
|
|
@@ -1180,7 +1272,7 @@ useCanvasShortcuts(() => ({
|
|
|
1180
1272
|
pendingConnect.value = null;
|
|
1181
1273
|
flow.removeSelectedElements();
|
|
1182
1274
|
}
|
|
1183
|
-
}), { disabled:
|
|
1275
|
+
}), { disabled: () => !canvasHasKeyboard.value });
|
|
1184
1276
|
const nodeTypes = {
|
|
1185
1277
|
workflow: markRaw(WorkflowNodeView),
|
|
1186
1278
|
trigger: markRaw(WorkflowTriggerView),
|
|
@@ -1218,15 +1310,15 @@ const edgeTypes = {
|
|
|
1218
1310
|
:default-edge-options="{ type: 'flow' }"
|
|
1219
1311
|
:connection-line-type="ConnectionLineType.Step"
|
|
1220
1312
|
:connection-line-style="{ stroke: '#94a3b8', strokeWidth: 1.75, strokeDasharray: '4 4' }"
|
|
1313
|
+
:connection-mode="ConnectionMode.Strict"
|
|
1221
1314
|
:is-valid-connection="isValidConnection"
|
|
1222
1315
|
:snap-to-grid="true"
|
|
1223
1316
|
:snap-grid="[20, 20]"
|
|
1224
1317
|
:selection-mode="SelectionMode.Partial"
|
|
1225
1318
|
:selection-key-code="pointerMode === 'select' ? true : 'Shift'"
|
|
1226
1319
|
:pan-on-drag="pointerMode === 'hand' ? true : [1]"
|
|
1227
|
-
:
|
|
1228
|
-
:
|
|
1229
|
-
:delete-key-code="['Backspace', 'Delete']"
|
|
1320
|
+
:zoom-on-double-click="false"
|
|
1321
|
+
:delete-key-code="canvasHasKeyboard ? ['Backspace', 'Delete'] : null"
|
|
1230
1322
|
:edges-updatable="true"
|
|
1231
1323
|
:min-zoom="0.25"
|
|
1232
1324
|
:max-zoom="4"
|
|
@@ -1465,14 +1557,14 @@ const edgeTypes = {
|
|
|
1465
1557
|
</Transition>
|
|
1466
1558
|
|
|
1467
1559
|
<!-- Floating control pane — bottom center. -->
|
|
1468
|
-
<div class="absolute bottom-3 left-1/2 z-10 -translate-x-1/2">
|
|
1560
|
+
<div ref="controlBar" class="absolute bottom-3 left-1/2 z-10 -translate-x-1/2">
|
|
1469
1561
|
<TooltipProvider :delay-duration="250">
|
|
1470
1562
|
<div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
|
|
1471
1563
|
<div class="flex items-center gap-0.5 whitespace-nowrap border border-white bg-white rounded-lg bg-gradient-to-br from-white via-violet-50/40 to-white px-2 py-1">
|
|
1472
1564
|
<!-- Smart Run: the page decides what "run" means — workflows that
|
|
1473
1565
|
consume trigger.payload open the payload dialog, payload-free
|
|
1474
1566
|
ones fire immediately. One button either way. -->
|
|
1475
|
-
<IntegrationsEditorTip label="Run workflow" shortcut="mod+enter">
|
|
1567
|
+
<IntegrationsEditorTip :label="runWaiting ? 'The bundle is still building \u2014 the run starts when it is ready' : 'Run workflow'" shortcut="mod+enter">
|
|
1476
1568
|
<button
|
|
1477
1569
|
type="button"
|
|
1478
1570
|
class="relative flex h-8 items-center gap-1.5 rounded-lg bg-gradient-to-b from-emerald-500 to-emerald-600 px-3.5 text-sm font-medium text-white transition after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_1px_0_rgba(255,255,255,0.3),inset_0_-2px_0_rgba(0,0,0,0.25)] hover:from-emerald-600 hover:to-emerald-600 active:translate-y-px disabled:opacity-60"
|
|
@@ -1481,16 +1573,20 @@ const edgeTypes = {
|
|
|
1481
1573
|
>
|
|
1482
1574
|
<Loader2 v-if="runBusy" :size="14" class="animate-spin" />
|
|
1483
1575
|
<Play v-else :size="14" />
|
|
1484
|
-
Run
|
|
1576
|
+
{{ runWaiting ? "Waiting for build\u2026" : "Run" }}
|
|
1485
1577
|
</button>
|
|
1486
1578
|
</IntegrationsEditorTip>
|
|
1487
1579
|
|
|
1488
1580
|
<div class="mx-1 h-5 w-px bg-black/10" />
|
|
1489
1581
|
|
|
1490
1582
|
<div class="flex items-center gap-0.5 rounded-lg bg-black/[0.05] p-0.5 shadow-[inset_0_1px_2px_rgba(15,23,42,0.12)]">
|
|
1583
|
+
<!-- Icon-only, and the tooltip is not an accessible name: each one
|
|
1584
|
+
says what it does itself, and whether it is the active mode. -->
|
|
1491
1585
|
<IntegrationsEditorTip label="Select" shortcut="v">
|
|
1492
1586
|
<button
|
|
1493
1587
|
type="button"
|
|
1588
|
+
aria-label="Select"
|
|
1589
|
+
:aria-pressed="pointerMode === 'select'"
|
|
1494
1590
|
class="flex size-7 items-center justify-center rounded-lg transition"
|
|
1495
1591
|
:class="pointerMode === 'select' ? 'bg-gradient-to-b from-white to-gray-50 text-foreground shadow-[0_1px_2px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.9)]' : 'text-muted-foreground hover:text-foreground'"
|
|
1496
1592
|
@click="setPointerMode('select')"
|
|
@@ -1501,6 +1597,8 @@ const edgeTypes = {
|
|
|
1501
1597
|
<IntegrationsEditorTip label="Hand (pan)" shortcut="h">
|
|
1502
1598
|
<button
|
|
1503
1599
|
type="button"
|
|
1600
|
+
aria-label="Hand (pan)"
|
|
1601
|
+
:aria-pressed="pointerMode === 'hand'"
|
|
1504
1602
|
class="flex size-7 items-center justify-center rounded-lg transition"
|
|
1505
1603
|
:class="pointerMode === 'hand' ? 'bg-gradient-to-b from-white to-gray-50 text-foreground shadow-[0_1px_2px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.9)]' : 'text-muted-foreground hover:text-foreground'"
|
|
1506
1604
|
@click="setPointerMode('hand')"
|
|
@@ -1577,6 +1675,7 @@ const edgeTypes = {
|
|
|
1577
1675
|
<IntegrationsEditorTip label="Zoom out" shortcut="-">
|
|
1578
1676
|
<button
|
|
1579
1677
|
type="button"
|
|
1678
|
+
aria-label="Zoom out"
|
|
1580
1679
|
class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
|
|
1581
1680
|
@click="flow.zoomOut({ duration: 150 })"
|
|
1582
1681
|
>
|
|
@@ -1590,8 +1689,13 @@ const edgeTypes = {
|
|
|
1590
1689
|
<span class="inline-flex">
|
|
1591
1690
|
<DropdownMenu v-model:open="zoomMenuOpen">
|
|
1592
1691
|
<DropdownMenuTrigger as-child>
|
|
1692
|
+
<!-- The label carries the percentage because it replaces the
|
|
1693
|
+
button's text as its accessible name: a bare "Zoom level"
|
|
1694
|
+
would leave screen reader users with the one thing sighted
|
|
1695
|
+
users do not need told and without the value they do. -->
|
|
1593
1696
|
<button
|
|
1594
1697
|
type="button"
|
|
1698
|
+
:aria-label="`Zoom level ${zoomPct}`"
|
|
1595
1699
|
class="w-12 rounded-lg py-1 text-center text-sm tabular-nums text-muted-foreground transition hover:bg-black/[0.04] hover:text-foreground"
|
|
1596
1700
|
>
|
|
1597
1701
|
{{ zoomPct }}
|
|
@@ -1603,6 +1707,7 @@ const edgeTypes = {
|
|
|
1603
1707
|
v-model="zoomInputText"
|
|
1604
1708
|
type="text"
|
|
1605
1709
|
inputmode="numeric"
|
|
1710
|
+
aria-label="Zoom level in percent"
|
|
1606
1711
|
class="h-6 w-full rounded-md border border-black/[0.08] bg-white px-1 text-center text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-primary/40"
|
|
1607
1712
|
placeholder="%"
|
|
1608
1713
|
@keydown.stop
|
|
@@ -1625,6 +1730,7 @@ const edgeTypes = {
|
|
|
1625
1730
|
<IntegrationsEditorTip label="Zoom in" shortcut="=">
|
|
1626
1731
|
<button
|
|
1627
1732
|
type="button"
|
|
1733
|
+
aria-label="Zoom in"
|
|
1628
1734
|
class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
|
|
1629
1735
|
@click="flow.zoomIn({ duration: 150 })"
|
|
1630
1736
|
>
|
|
@@ -1668,7 +1774,22 @@ const edgeTypes = {
|
|
|
1668
1774
|
renameIntent = false;
|
|
1669
1775
|
}
|
|
1670
1776
|
}">
|
|
1671
|
-
|
|
1777
|
+
<!-- Sized for the form, not for three columns. 1100px leaves the 1024px
|
|
1778
|
+
reading width a small gutter instead of floating it in the middle,
|
|
1779
|
+
and still gives the form ~770px with the Input panel open. Fixed
|
|
1780
|
+
rather than fitting the content: the tabs hold very different
|
|
1781
|
+
amounts, and a dialog that resizes under the pointer as you switch
|
|
1782
|
+
between them is worse than one with some room to spare.
|
|
1783
|
+
|
|
1784
|
+
A trigger is the exception, and gets a box its own size. It has one
|
|
1785
|
+
tab, no output to test and a handful of controls — a schedule is a
|
|
1786
|
+
frequency, a time and its days — so the node's width would leave the
|
|
1787
|
+
form as a strip down the left of an empty workbench. -->
|
|
1788
|
+
<DialogContent
|
|
1789
|
+
:show-close-button="false"
|
|
1790
|
+
class="flex max-w-none flex-col gap-0 overflow-hidden rounded-lg p-0 sm:max-w-none"
|
|
1791
|
+
:class="selectionIsTrigger ? 'h-[min(520px,86vh)] w-[min(600px,94vw)]' : 'h-[min(680px,86vh)] w-[min(1100px,94vw)]'"
|
|
1792
|
+
>
|
|
1672
1793
|
<NodeInspector
|
|
1673
1794
|
v-if="selection"
|
|
1674
1795
|
:selection="selection"
|
|
@@ -1681,6 +1802,7 @@ const edgeTypes = {
|
|
|
1681
1802
|
:triggers="blob.triggers"
|
|
1682
1803
|
:dynamic-outputs="dynamicOutputs"
|
|
1683
1804
|
:workflow-id="workflow.id"
|
|
1805
|
+
:refused-fields="selectedId ? invalidNodeMessages[selectedId] : void 0"
|
|
1684
1806
|
@update="applyInspectorUpdate"
|
|
1685
1807
|
@close="selectedId = null"
|
|
1686
1808
|
@remove="selectedId && removeBlobNode(selectedId)"
|
|
@@ -6,8 +6,29 @@ type __VLS_Props = {
|
|
|
6
6
|
workflow: Workflow;
|
|
7
7
|
/** Busy flag for the control-pane Play button (a run is being triggered). */
|
|
8
8
|
runBusy?: boolean;
|
|
9
|
+
/** The run is queued behind a bundle build — busy, but worth saying why. */
|
|
10
|
+
runWaiting?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* One of the page's own lightboxes is open above the editor (settings,
|
|
13
|
+
* duplicate, run payload, the sync response, the deactivation prompt). The
|
|
14
|
+
* canvas stands its keyboard shortcuts down while one is up — see
|
|
15
|
+
* `canvasHasKeyboard` below. A new page-level dialog belongs in here too.
|
|
16
|
+
*/
|
|
17
|
+
overlayOpen?: boolean;
|
|
9
18
|
};
|
|
10
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Show a node the user was sent to from outside the canvas — the validation
|
|
21
|
+
* banner's per-node line (PO-209). Centres the card and opens its settings,
|
|
22
|
+
* which is where the fields at fault are.
|
|
23
|
+
*
|
|
24
|
+
* A node inside a collapsed group has no card on the canvas: centring would pan
|
|
25
|
+
* to empty space, so it only opens the settings. The reverse can't happen — an
|
|
26
|
+
* id that is on the canvas is in the blob by construction.
|
|
27
|
+
*/
|
|
28
|
+
declare function revealNode(id: string): void;
|
|
29
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {
|
|
30
|
+
revealNode: typeof revealNode;
|
|
31
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
32
|
change: (blob: WorkflowBlob) => any;
|
|
12
33
|
run: () => any;
|
|
13
34
|
toggleActive: () => any;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { computed, markRaw, nextTick, onMounted, provide, ref, watch } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
MarkerType,
|
|
5
|
-
PanOnScrollMode,
|
|
6
5
|
VueFlow,
|
|
7
6
|
useVueFlow
|
|
8
7
|
} from "@vue-flow/core";
|
|
@@ -65,24 +64,9 @@ function onResolvedOutputs(nodeId, outputs) {
|
|
|
65
64
|
const previewNodes = ref([]);
|
|
66
65
|
const previewEdges = ref([]);
|
|
67
66
|
const preview = useVueFlow("workflow-history-preview");
|
|
68
|
-
provide("workflow-node-actions",
|
|
69
|
-
}, remove: () => {
|
|
70
|
-
} });
|
|
67
|
+
provide("workflow-node-actions", null);
|
|
71
68
|
function edgeColorByKind(kind) {
|
|
72
|
-
|
|
73
|
-
case "success":
|
|
74
|
-
return "#10b981";
|
|
75
|
-
case "error":
|
|
76
|
-
return "#f43f5e";
|
|
77
|
-
case "branch-true":
|
|
78
|
-
return "#10b981";
|
|
79
|
-
case "branch-false":
|
|
80
|
-
return "#f59e0b";
|
|
81
|
-
case "branch":
|
|
82
|
-
return "#8b5cf6";
|
|
83
|
-
default:
|
|
84
|
-
return "#94a3b8";
|
|
85
|
-
}
|
|
69
|
+
return kind === "error" ? "#cbd5e1" : "#94a3b8";
|
|
86
70
|
}
|
|
87
71
|
const GROUP_PAD = 24;
|
|
88
72
|
const GROUP_LABEL_ROOM = 26;
|
|
@@ -140,6 +124,7 @@ function buildPreview(blob) {
|
|
|
140
124
|
if (output === "true") return "branch-true";
|
|
141
125
|
if (output === "false") return "branch-false";
|
|
142
126
|
if (output === "error") return "error";
|
|
127
|
+
if (output === "done") return "success";
|
|
143
128
|
const outs = [...manifest.outputs ?? [], ...dynamicOutputs.value[sourceNodeId] ?? []];
|
|
144
129
|
const portDef = outs.find((o) => o.name === output);
|
|
145
130
|
if (portDef) {
|
|
@@ -187,7 +172,7 @@ function buildPreview(blob) {
|
|
|
187
172
|
const kind = edgeKind(e.from.nodeId, e.from.output);
|
|
188
173
|
const wn = blob.nodes.find((n) => n.id === e.from.nodeId);
|
|
189
174
|
const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
190
|
-
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"
|
|
175
|
+
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(e.from.output);
|
|
191
176
|
const srcSide = pickSourceSide(e.from.nodeId, e.to.nodeId);
|
|
192
177
|
return {
|
|
193
178
|
id: e.id,
|
|
@@ -296,7 +281,6 @@ onMounted(() => {
|
|
|
296
281
|
:nodes-connectable="false"
|
|
297
282
|
:elements-selectable="false"
|
|
298
283
|
:edges-updatable="false"
|
|
299
|
-
:pan-on-scroll-mode="PanOnScrollMode.Free"
|
|
300
284
|
:delete-key-code="null"
|
|
301
285
|
:min-zoom="0.25"
|
|
302
286
|
:max-zoom="2"
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import type { WorkflowIcon } from '../../types/integrations.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The caller names the picker by pointing at its own `<Label>`'s id — not with
|
|
4
|
+
* `<Label for>`, and not with an `id` of ours on the trigger.
|
|
5
|
+
*
|
|
6
|
+
* An `id` on the trigger would be a quiet regression: `PopoverTrigger` puts
|
|
7
|
+
* reka-ui's generated `triggerId` there, `PopoverContentImpl` points the opened
|
|
8
|
+
* dialog's `aria-labelledby` at exactly that id, and reka's `Slot` merges with
|
|
9
|
+
* the *child's* props winning — so ours would replace it and leave the popover
|
|
10
|
+
* naming an element that no longer exists.
|
|
11
|
+
*
|
|
12
|
+
* `aria-labelledby` also lists the id of the span holding the current choice, so
|
|
13
|
+
* the name stays "Icon Business Central" rather than dropping the value the way
|
|
14
|
+
* a plain label would. Attribute fallthrough is no help here either: the root
|
|
15
|
+
* this component renders is the popover, not the button.
|
|
16
|
+
*/
|
|
17
|
+
type __VLS_Props = {
|
|
18
|
+
triggerLabelledby?: string;
|
|
19
|
+
};
|
|
20
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
3
21
|
modelValue?: WorkflowIcon | null;
|
|
4
22
|
};
|
|
5
23
|
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, ref } from "vue";
|
|
2
|
+
import { computed, ref, useId } from "vue";
|
|
3
3
|
import { AltArrowDown as ChevronDown } from "#solar-icons";
|
|
4
4
|
import { Popover, PopoverContent, PopoverTrigger } from "@revenexx/studio";
|
|
5
5
|
import { WORKFLOW_SOLAR_ICONS, imageIdFromSrc, nodeLogoOptions } from "../../data/workflowIconCatalog";
|
|
6
6
|
import WorkflowIconView from "./WorkflowIconView.vue";
|
|
7
7
|
const model = defineModel({ type: [Object, null], ...{ default: null } });
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
triggerLabelledby: { type: String, required: false }
|
|
10
|
+
});
|
|
11
|
+
const valueId = useId();
|
|
12
|
+
const triggerLabelledby = computed(
|
|
13
|
+
() => props.triggerLabelledby ? `${props.triggerLabelledby} ${valueId}` : void 0
|
|
14
|
+
);
|
|
8
15
|
const open = ref(false);
|
|
9
16
|
const logos = computed(() => nodeLogoOptions());
|
|
10
17
|
function pick(icon) {
|
|
@@ -30,13 +37,14 @@ const selectedLabel = computed(() => {
|
|
|
30
37
|
<Popover v-model:open="open">
|
|
31
38
|
<PopoverTrigger as-child>
|
|
32
39
|
<button
|
|
40
|
+
:aria-labelledby="triggerLabelledby"
|
|
33
41
|
type="button"
|
|
34
42
|
class="flex h-9 w-full items-center gap-2 rounded-md border border-black/[0.08] bg-white px-2.5 text-left text-sm transition hover:border-black/20"
|
|
35
43
|
>
|
|
36
44
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
37
45
|
<WorkflowIconView :icon="model" :size="14" />
|
|
38
46
|
</span>
|
|
39
|
-
<span class="min-w-0 flex-1 truncate" :class="model ? '' : 'text-muted-foreground'">
|
|
47
|
+
<span :id="valueId" class="min-w-0 flex-1 truncate" :class="model ? '' : 'text-muted-foreground'">
|
|
40
48
|
{{ selectedLabel }}
|
|
41
49
|
</span>
|
|
42
50
|
<ChevronDown :size="14" class="shrink-0 text-muted-foreground" />
|
|
@@ -50,6 +58,7 @@ const selectedLabel = computed(() => {
|
|
|
50
58
|
class="flex size-8 items-center justify-center rounded-md border text-sm text-muted-foreground transition hover:bg-muted/50"
|
|
51
59
|
:class="!model ? 'border-emerald-500/40 bg-emerald-500/10' : 'border-transparent'"
|
|
52
60
|
title="No icon (default glyph)"
|
|
61
|
+
aria-label="No icon (default glyph)"
|
|
53
62
|
@click="pick(null)"
|
|
54
63
|
>
|
|
55
64
|
—
|