@revenexx/studio-integrations 0.5.1 → 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 +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +167 -46
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -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 +196 -8
- 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,10 +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
59
|
runBusy: { type: Boolean, required: false },
|
|
54
|
-
runWaiting: { type: Boolean, required: false }
|
|
60
|
+
runWaiting: { type: Boolean, required: false },
|
|
61
|
+
overlayOpen: { type: Boolean, required: false }
|
|
55
62
|
});
|
|
56
63
|
const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
|
|
57
64
|
const { keys: secretKeys } = useIntegrationsSecrets();
|
|
@@ -66,7 +73,20 @@ function clone(v) {
|
|
|
66
73
|
const vfNodes = ref([]);
|
|
67
74
|
const vfEdges = ref([]);
|
|
68
75
|
const flow = useVueFlow();
|
|
76
|
+
const invalidNodeMessages = computed(() => findingsByNodeField(props.workflow.warnings ?? [], props.workflow.blob.nodes));
|
|
69
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
|
+
});
|
|
70
90
|
const dynamicOutputNodes = computed(() => blob.value.nodes.filter((n) => {
|
|
71
91
|
const m = findManifest(n.nodeSlug, n.nodeVersion);
|
|
72
92
|
return (m?.outputs ?? []).some((o) => o.resolveOutputs === true);
|
|
@@ -79,6 +99,7 @@ function edgeKindFromPort(sourceNodeId, output) {
|
|
|
79
99
|
if (output === "true") return "branch-true";
|
|
80
100
|
if (output === "false") return "branch-false";
|
|
81
101
|
if (output === "error") return "error";
|
|
102
|
+
if (output === "done") return "success";
|
|
82
103
|
const portDef = (manifest.outputs ?? []).find((o) => o.name === output);
|
|
83
104
|
if (portDef) {
|
|
84
105
|
if (portDef.kind === "error") return "error";
|
|
@@ -93,10 +114,10 @@ function edgeKindFromPort(sourceNodeId, output) {
|
|
|
93
114
|
}
|
|
94
115
|
return "default";
|
|
95
116
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
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);
|
|
100
121
|
}
|
|
101
122
|
function triggerPosition(id, index) {
|
|
102
123
|
const saved = blob.value.triggerPositions?.[id];
|
|
@@ -121,10 +142,6 @@ function stickyArea(s) {
|
|
|
121
142
|
}
|
|
122
143
|
const GROUP_PAD = 24;
|
|
123
144
|
const GROUP_LABEL_ROOM = 26;
|
|
124
|
-
function locText(v) {
|
|
125
|
-
if (!v) return "";
|
|
126
|
-
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
127
|
-
}
|
|
128
145
|
function memberDims(id) {
|
|
129
146
|
const d = flow.findNode(id)?.dimensions;
|
|
130
147
|
return d && d.width > 0 ? d : { width: 220, height: 64 };
|
|
@@ -262,7 +279,11 @@ function syncFromBlob() {
|
|
|
262
279
|
type: "workflow",
|
|
263
280
|
position: n.position ?? { x: 240, y: 80 },
|
|
264
281
|
class: bornClass(n.id),
|
|
265
|
-
data: {
|
|
282
|
+
data: {
|
|
283
|
+
...n,
|
|
284
|
+
__resolvedOutputs: dynamicOutputs.value[n.id],
|
|
285
|
+
__invalidFields: invalidNodeFields.value[n.id]
|
|
286
|
+
}
|
|
266
287
|
}))
|
|
267
288
|
];
|
|
268
289
|
const seenEdge = /* @__PURE__ */ new Set();
|
|
@@ -285,6 +306,18 @@ function syncFromBlob() {
|
|
|
285
306
|
targetHandle: tgtGroup ? "__in__top" : vf.targetHandle
|
|
286
307
|
});
|
|
287
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
|
+
}
|
|
288
321
|
vfEdges.value = mapped;
|
|
289
322
|
void nextTick(() => flow.updateNodeInternals(vfNodes.value.map((n) => n.id)));
|
|
290
323
|
}
|
|
@@ -292,9 +325,11 @@ function makeVfEdge(id, sourceId, targetId, output) {
|
|
|
292
325
|
const kind = edgeKindFromPort(sourceId, output);
|
|
293
326
|
const wn = blob.value.nodes.find((n) => n.id === sourceId);
|
|
294
327
|
const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
295
|
-
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"
|
|
328
|
+
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(output);
|
|
296
329
|
const srcSide = pickSourceSide(sourceId, targetId);
|
|
297
330
|
const tgtSide = oppositeSide(srcSide);
|
|
331
|
+
const order = orderedOutputPorts(sourceId);
|
|
332
|
+
const portIndex = order.indexOf(output);
|
|
298
333
|
return {
|
|
299
334
|
id,
|
|
300
335
|
source: sourceId,
|
|
@@ -304,7 +339,8 @@ function makeVfEdge(id, sourceId, targetId, output) {
|
|
|
304
339
|
type: "flow",
|
|
305
340
|
data: {
|
|
306
341
|
kind,
|
|
307
|
-
label: isBranchPort ? output : void 0
|
|
342
|
+
label: isBranchPort ? output : void 0,
|
|
343
|
+
portIndex: portIndex < 0 ? 0 : portIndex
|
|
308
344
|
},
|
|
309
345
|
markerEnd: {
|
|
310
346
|
type: MarkerType.ArrowClosed,
|
|
@@ -315,24 +351,21 @@ function makeVfEdge(id, sourceId, targetId, output) {
|
|
|
315
351
|
};
|
|
316
352
|
}
|
|
317
353
|
function edgeColorByKind(kind) {
|
|
318
|
-
|
|
319
|
-
case "success":
|
|
320
|
-
return "#10b981";
|
|
321
|
-
case "error":
|
|
322
|
-
return "#f43f5e";
|
|
323
|
-
case "branch-true":
|
|
324
|
-
return "#10b981";
|
|
325
|
-
case "branch-false":
|
|
326
|
-
return "#f59e0b";
|
|
327
|
-
case "branch":
|
|
328
|
-
return "#8b5cf6";
|
|
329
|
-
default:
|
|
330
|
-
return "#94a3b8";
|
|
331
|
-
}
|
|
354
|
+
return kind === "error" ? "#cbd5e1" : "#94a3b8";
|
|
332
355
|
}
|
|
333
356
|
syncFromBlob();
|
|
334
|
-
|
|
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
|
+
});
|
|
335
367
|
flow.onNodeDragStart(({ node }) => {
|
|
368
|
+
dragInFlight = true;
|
|
336
369
|
if (node.type !== "group") return;
|
|
337
370
|
const g = blob.value.groups?.find((x) => x.id === node.id);
|
|
338
371
|
if (!g) return;
|
|
@@ -350,6 +383,7 @@ flow.onNodeDrag(({ node }) => {
|
|
|
350
383
|
}
|
|
351
384
|
});
|
|
352
385
|
function persistDraggedNodes(evt) {
|
|
386
|
+
dragInFlight = false;
|
|
353
387
|
const moved = evt.nodes?.length ? evt.nodes : [evt.node];
|
|
354
388
|
for (const n of moved) {
|
|
355
389
|
const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
|
|
@@ -395,7 +429,7 @@ flow.onConnect((conn) => {
|
|
|
395
429
|
connectCompleted = true;
|
|
396
430
|
if (!conn.source || !conn.target) return;
|
|
397
431
|
const id = `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
398
|
-
const output =
|
|
432
|
+
const output = portFromHandleId(conn.sourceHandle) ?? "default";
|
|
399
433
|
blob.value.edges.push({
|
|
400
434
|
id,
|
|
401
435
|
from: { nodeId: conn.source, output },
|
|
@@ -409,7 +443,7 @@ let connectStart = null;
|
|
|
409
443
|
let connectCompleted = false;
|
|
410
444
|
flow.onConnectStart(({ nodeId, handleId, handleType }) => {
|
|
411
445
|
connectCompleted = false;
|
|
412
|
-
connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output:
|
|
446
|
+
connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output: portFromHandleId(handleId) ?? "default" } : null;
|
|
413
447
|
});
|
|
414
448
|
flow.onConnectEnd((event) => {
|
|
415
449
|
const start = connectStart;
|
|
@@ -429,7 +463,7 @@ flow.onEdgeUpdate(({ edge, connection }) => {
|
|
|
429
463
|
if (connection.source === connection.target) return;
|
|
430
464
|
const be = blob.value.edges.find((e) => e.id === edge.id);
|
|
431
465
|
if (!be) return;
|
|
432
|
-
be.from = { nodeId: connection.source, output:
|
|
466
|
+
be.from = { nodeId: connection.source, output: portFromHandleId(connection.sourceHandle) ?? be.from.output };
|
|
433
467
|
be.to = { nodeId: connection.target, input: be.to.input };
|
|
434
468
|
syncFromBlob();
|
|
435
469
|
push();
|
|
@@ -548,9 +582,18 @@ function duplicateBlobNodes(ids) {
|
|
|
548
582
|
});
|
|
549
583
|
}
|
|
550
584
|
provide("workflow-node-actions", {
|
|
585
|
+
configure: (id) => {
|
|
586
|
+
renameIntent.value = false;
|
|
587
|
+
selectedId.value = id;
|
|
588
|
+
},
|
|
551
589
|
duplicate: (id) => duplicateBlobNodes([id]),
|
|
552
590
|
remove: (id) => removeBlobNode(id)
|
|
553
591
|
});
|
|
592
|
+
const controlBar = ref(null);
|
|
593
|
+
provide(
|
|
594
|
+
"workflow-canvas-bottom-inset",
|
|
595
|
+
() => controlBar.value ? controlBar.value.offsetHeight + 12 : 0
|
|
596
|
+
);
|
|
554
597
|
const CLIP_KIND = "revenexx.workflow.nodes";
|
|
555
598
|
async function copyNodes(ids) {
|
|
556
599
|
const nodes = blob.value.nodes.filter((n) => ids.includes(n.id));
|
|
@@ -698,17 +741,33 @@ const selection = computed(() => {
|
|
|
698
741
|
if (!selectedId.value) return null;
|
|
699
742
|
return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t) => t.id === selectedId.value) ?? null;
|
|
700
743
|
});
|
|
701
|
-
|
|
744
|
+
const selectionIsTrigger = computed(() => !!selectedId.value && blob.value.triggers.some((t) => t.id === selectedId.value));
|
|
745
|
+
flow.onNodeDoubleClick((evt) => {
|
|
702
746
|
if (evt.node.type === "sticky" || evt.node.type === "group") return;
|
|
703
747
|
selectedId.value = evt.node.id;
|
|
704
748
|
});
|
|
705
749
|
flow.onPaneClick(() => {
|
|
706
750
|
selectedId.value = null;
|
|
707
751
|
});
|
|
708
|
-
function
|
|
709
|
-
|
|
710
|
-
|
|
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
|
+
);
|
|
711
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 });
|
|
712
771
|
const selectedUpstreamOutputs = computed(() => {
|
|
713
772
|
const id = selectedId.value;
|
|
714
773
|
if (!id) return [];
|
|
@@ -720,7 +779,9 @@ const selectedUpstreamOutputs = computed(() => {
|
|
|
720
779
|
const portDef = (findManifest(source.nodeSlug, source.nodeVersion)?.outputs ?? []).find((o) => o.name === e.from.output);
|
|
721
780
|
return [{
|
|
722
781
|
nodeId: source.id,
|
|
723
|
-
|
|
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,
|
|
724
785
|
output: e.from.output,
|
|
725
786
|
fields: portDef?.fields
|
|
726
787
|
}];
|
|
@@ -819,7 +880,6 @@ function addFromPayload(payload, pos) {
|
|
|
819
880
|
adoptIntoGroupAt(id2, at);
|
|
820
881
|
markBorn(id2);
|
|
821
882
|
syncFromBlob();
|
|
822
|
-
selectedId.value = id2;
|
|
823
883
|
push();
|
|
824
884
|
return;
|
|
825
885
|
}
|
|
@@ -836,7 +896,6 @@ function addFromPayload(payload, pos) {
|
|
|
836
896
|
adoptIntoGroupAt(id, at);
|
|
837
897
|
markBorn(id);
|
|
838
898
|
syncFromBlob();
|
|
839
|
-
selectedId.value = id;
|
|
840
899
|
push();
|
|
841
900
|
}
|
|
842
901
|
function onDrop(ev) {
|
|
@@ -871,7 +930,6 @@ function onPaletteInsert(payload) {
|
|
|
871
930
|
});
|
|
872
931
|
markBorn(id);
|
|
873
932
|
syncFromBlob();
|
|
874
|
-
selectedId.value = id;
|
|
875
933
|
push();
|
|
876
934
|
return;
|
|
877
935
|
}
|
|
@@ -903,6 +961,10 @@ function spliceIntoEdge(edgeId, payload) {
|
|
|
903
961
|
const edge = blob.value.edges.find((e) => e.id === edgeId);
|
|
904
962
|
const manifest = findManifest(payload.slug, payload.version);
|
|
905
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
|
+
}
|
|
906
968
|
const a = nodePosition(edge.from.nodeId);
|
|
907
969
|
const b = nodePosition(edge.to.nodeId);
|
|
908
970
|
const id = `n-${Date.now()}`;
|
|
@@ -921,7 +983,6 @@ function spliceIntoEdge(edgeId, payload) {
|
|
|
921
983
|
);
|
|
922
984
|
markBorn(id);
|
|
923
985
|
syncFromBlob();
|
|
924
|
-
selectedId.value = id;
|
|
925
986
|
push();
|
|
926
987
|
}
|
|
927
988
|
function defaultConfigFor(fields = []) {
|
|
@@ -935,6 +996,34 @@ function nodeDims(id) {
|
|
|
935
996
|
const d = flow.findNode(id)?.dimensions;
|
|
936
997
|
return d && d.width > 0 ? d : { width: 220, height: 64 };
|
|
937
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
|
+
}
|
|
938
1027
|
async function tidyUp() {
|
|
939
1028
|
const containment = /* @__PURE__ */ new Map();
|
|
940
1029
|
for (const s of blob.value.stickies ?? []) {
|
|
@@ -1014,6 +1103,7 @@ async function tidyUp() {
|
|
|
1014
1103
|
if (!inner) return n;
|
|
1015
1104
|
return { ...n, position: { x: anchor.x + GROUP_PAD + inner.position.x, y: anchor.y + GROUP_PAD + GROUP_LABEL_ROOM + inner.position.y } };
|
|
1016
1105
|
});
|
|
1106
|
+
reorderBranchesByHandle(laid, memberToGroup);
|
|
1017
1107
|
for (const n of laid) {
|
|
1018
1108
|
const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
|
|
1019
1109
|
if (n.type === "workflow") {
|
|
@@ -1120,6 +1210,7 @@ async function focusPalette() {
|
|
|
1120
1210
|
paletteRef.value?.focusSearch();
|
|
1121
1211
|
}
|
|
1122
1212
|
const lightboxOpen = computed(() => !!selection.value);
|
|
1213
|
+
const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
|
|
1123
1214
|
const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
|
|
1124
1215
|
useCanvasShortcuts(() => ({
|
|
1125
1216
|
"enter": {
|
|
@@ -1181,7 +1272,7 @@ useCanvasShortcuts(() => ({
|
|
|
1181
1272
|
pendingConnect.value = null;
|
|
1182
1273
|
flow.removeSelectedElements();
|
|
1183
1274
|
}
|
|
1184
|
-
}), { disabled:
|
|
1275
|
+
}), { disabled: () => !canvasHasKeyboard.value });
|
|
1185
1276
|
const nodeTypes = {
|
|
1186
1277
|
workflow: markRaw(WorkflowNodeView),
|
|
1187
1278
|
trigger: markRaw(WorkflowTriggerView),
|
|
@@ -1219,15 +1310,15 @@ const edgeTypes = {
|
|
|
1219
1310
|
:default-edge-options="{ type: 'flow' }"
|
|
1220
1311
|
:connection-line-type="ConnectionLineType.Step"
|
|
1221
1312
|
:connection-line-style="{ stroke: '#94a3b8', strokeWidth: 1.75, strokeDasharray: '4 4' }"
|
|
1313
|
+
:connection-mode="ConnectionMode.Strict"
|
|
1222
1314
|
:is-valid-connection="isValidConnection"
|
|
1223
1315
|
:snap-to-grid="true"
|
|
1224
1316
|
:snap-grid="[20, 20]"
|
|
1225
1317
|
:selection-mode="SelectionMode.Partial"
|
|
1226
1318
|
:selection-key-code="pointerMode === 'select' ? true : 'Shift'"
|
|
1227
1319
|
:pan-on-drag="pointerMode === 'hand' ? true : [1]"
|
|
1228
|
-
:
|
|
1229
|
-
:
|
|
1230
|
-
:delete-key-code="['Backspace', 'Delete']"
|
|
1320
|
+
:zoom-on-double-click="false"
|
|
1321
|
+
:delete-key-code="canvasHasKeyboard ? ['Backspace', 'Delete'] : null"
|
|
1231
1322
|
:edges-updatable="true"
|
|
1232
1323
|
:min-zoom="0.25"
|
|
1233
1324
|
:max-zoom="4"
|
|
@@ -1466,7 +1557,7 @@ const edgeTypes = {
|
|
|
1466
1557
|
</Transition>
|
|
1467
1558
|
|
|
1468
1559
|
<!-- Floating control pane — bottom center. -->
|
|
1469
|
-
<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">
|
|
1470
1561
|
<TooltipProvider :delay-duration="250">
|
|
1471
1562
|
<div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
|
|
1472
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">
|
|
@@ -1489,9 +1580,13 @@ const edgeTypes = {
|
|
|
1489
1580
|
<div class="mx-1 h-5 w-px bg-black/10" />
|
|
1490
1581
|
|
|
1491
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. -->
|
|
1492
1585
|
<IntegrationsEditorTip label="Select" shortcut="v">
|
|
1493
1586
|
<button
|
|
1494
1587
|
type="button"
|
|
1588
|
+
aria-label="Select"
|
|
1589
|
+
:aria-pressed="pointerMode === 'select'"
|
|
1495
1590
|
class="flex size-7 items-center justify-center rounded-lg transition"
|
|
1496
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'"
|
|
1497
1592
|
@click="setPointerMode('select')"
|
|
@@ -1502,6 +1597,8 @@ const edgeTypes = {
|
|
|
1502
1597
|
<IntegrationsEditorTip label="Hand (pan)" shortcut="h">
|
|
1503
1598
|
<button
|
|
1504
1599
|
type="button"
|
|
1600
|
+
aria-label="Hand (pan)"
|
|
1601
|
+
:aria-pressed="pointerMode === 'hand'"
|
|
1505
1602
|
class="flex size-7 items-center justify-center rounded-lg transition"
|
|
1506
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'"
|
|
1507
1604
|
@click="setPointerMode('hand')"
|
|
@@ -1578,6 +1675,7 @@ const edgeTypes = {
|
|
|
1578
1675
|
<IntegrationsEditorTip label="Zoom out" shortcut="-">
|
|
1579
1676
|
<button
|
|
1580
1677
|
type="button"
|
|
1678
|
+
aria-label="Zoom out"
|
|
1581
1679
|
class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
|
|
1582
1680
|
@click="flow.zoomOut({ duration: 150 })"
|
|
1583
1681
|
>
|
|
@@ -1591,8 +1689,13 @@ const edgeTypes = {
|
|
|
1591
1689
|
<span class="inline-flex">
|
|
1592
1690
|
<DropdownMenu v-model:open="zoomMenuOpen">
|
|
1593
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. -->
|
|
1594
1696
|
<button
|
|
1595
1697
|
type="button"
|
|
1698
|
+
:aria-label="`Zoom level ${zoomPct}`"
|
|
1596
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"
|
|
1597
1700
|
>
|
|
1598
1701
|
{{ zoomPct }}
|
|
@@ -1604,6 +1707,7 @@ const edgeTypes = {
|
|
|
1604
1707
|
v-model="zoomInputText"
|
|
1605
1708
|
type="text"
|
|
1606
1709
|
inputmode="numeric"
|
|
1710
|
+
aria-label="Zoom level in percent"
|
|
1607
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"
|
|
1608
1712
|
placeholder="%"
|
|
1609
1713
|
@keydown.stop
|
|
@@ -1626,6 +1730,7 @@ const edgeTypes = {
|
|
|
1626
1730
|
<IntegrationsEditorTip label="Zoom in" shortcut="=">
|
|
1627
1731
|
<button
|
|
1628
1732
|
type="button"
|
|
1733
|
+
aria-label="Zoom in"
|
|
1629
1734
|
class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
|
|
1630
1735
|
@click="flow.zoomIn({ duration: 150 })"
|
|
1631
1736
|
>
|
|
@@ -1669,7 +1774,22 @@ const edgeTypes = {
|
|
|
1669
1774
|
renameIntent = false;
|
|
1670
1775
|
}
|
|
1671
1776
|
}">
|
|
1672
|
-
|
|
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
|
+
>
|
|
1673
1793
|
<NodeInspector
|
|
1674
1794
|
v-if="selection"
|
|
1675
1795
|
:selection="selection"
|
|
@@ -1682,6 +1802,7 @@ const edgeTypes = {
|
|
|
1682
1802
|
:triggers="blob.triggers"
|
|
1683
1803
|
:dynamic-outputs="dynamicOutputs"
|
|
1684
1804
|
:workflow-id="workflow.id"
|
|
1805
|
+
:refused-fields="selectedId ? invalidNodeMessages[selectedId] : void 0"
|
|
1685
1806
|
@update="applyInspectorUpdate"
|
|
1686
1807
|
@close="selectedId = null"
|
|
1687
1808
|
@remove="selectedId && removeBlobNode(selectedId)"
|
|
@@ -8,8 +8,27 @@ type __VLS_Props = {
|
|
|
8
8
|
runBusy?: boolean;
|
|
9
9
|
/** The run is queued behind a bundle build — busy, but worth saying why. */
|
|
10
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;
|
|
11
18
|
};
|
|
12
|
-
|
|
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, {
|
|
13
32
|
change: (blob: WorkflowBlob) => any;
|
|
14
33
|
run: () => any;
|
|
15
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
|
—
|
|
@@ -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, {
|