@revenexx/studio-integrations 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +51 -0
  2. package/dist/module.d.mts +6 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +45 -0
  5. package/dist/runtime/components/Integrations/Layout.d.vue.ts +12 -0
  6. package/dist/runtime/components/Integrations/Layout.vue +20 -0
  7. package/dist/runtime/components/Integrations/Layout.vue.d.ts +12 -0
  8. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +16 -0
  9. package/dist/runtime/components/Integrations/NodeInspector.vue +450 -0
  10. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +16 -0
  11. package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +2 -0
  12. package/dist/runtime/components/Integrations/NodePalette.vue +140 -0
  13. package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +2 -0
  14. package/dist/runtime/components/Integrations/RunDetailDialog.d.vue.ts +11 -0
  15. package/dist/runtime/components/Integrations/RunDetailDialog.vue +169 -0
  16. package/dist/runtime/components/Integrations/RunDetailDialog.vue.d.ts +11 -0
  17. package/dist/runtime/components/Integrations/Sidebar.d.vue.ts +2 -0
  18. package/dist/runtime/components/Integrations/Sidebar.vue +32 -0
  19. package/dist/runtime/components/Integrations/Sidebar.vue.d.ts +2 -0
  20. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +23 -0
  21. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +86 -0
  22. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +23 -0
  23. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +14 -0
  24. package/dist/runtime/components/Integrations/WorkflowEditor.vue +419 -0
  25. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +14 -0
  26. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +11 -0
  27. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +200 -0
  28. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +11 -0
  29. package/dist/runtime/components/Integrations/WorkflowTriggerView.d.vue.ts +8 -0
  30. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +82 -0
  31. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue.d.ts +8 -0
  32. package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +6 -0
  33. package/dist/runtime/components/Integrations/WorkflowsTable.vue +268 -0
  34. package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +6 -0
  35. package/dist/runtime/components/Integrations/workflowLayout.d.ts +17 -0
  36. package/dist/runtime/components/Integrations/workflowLayout.js +29 -0
  37. package/dist/runtime/composables/integrationsApi.d.ts +13 -0
  38. package/dist/runtime/composables/integrationsApi.js +55 -0
  39. package/dist/runtime/composables/useCredentialTypes.d.ts +19 -0
  40. package/dist/runtime/composables/useCredentialTypes.js +47 -0
  41. package/dist/runtime/composables/useIntegrationsAudit.d.ts +26 -0
  42. package/dist/runtime/composables/useIntegrationsAudit.js +39 -0
  43. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +43 -0
  44. package/dist/runtime/composables/useIntegrationsCredentials.js +98 -0
  45. package/dist/runtime/composables/useIntegrationsFocus.d.ts +16 -0
  46. package/dist/runtime/composables/useIntegrationsFocus.js +17 -0
  47. package/dist/runtime/composables/useIntegrationsNodes.d.ts +6 -0
  48. package/dist/runtime/composables/useIntegrationsNodes.js +32 -0
  49. package/dist/runtime/composables/useIntegrationsRuns.d.ts +31 -0
  50. package/dist/runtime/composables/useIntegrationsRuns.js +109 -0
  51. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +14 -0
  52. package/dist/runtime/composables/useIntegrationsSchedules.js +32 -0
  53. package/dist/runtime/composables/useIntegrationsSecrets.d.ts +27 -0
  54. package/dist/runtime/composables/useIntegrationsSecrets.js +76 -0
  55. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +19 -0
  56. package/dist/runtime/composables/useIntegrationsTemplates.js +141 -0
  57. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +39 -0
  58. package/dist/runtime/composables/useIntegrationsTriggers.js +39 -0
  59. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +28 -0
  60. package/dist/runtime/composables/useIntegrationsWorkflows.js +152 -0
  61. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  62. package/dist/runtime/data/integrationNodeCatalog.js +67 -0
  63. package/dist/runtime/data/integrationRuns.d.ts +7 -0
  64. package/dist/runtime/data/integrationRuns.js +109 -0
  65. package/dist/runtime/data/integrationTemplates.d.ts +11 -0
  66. package/dist/runtime/data/integrationTemplates.js +9 -0
  67. package/dist/runtime/data/integrationWorkflowSamples.d.ts +18 -0
  68. package/dist/runtime/data/integrationWorkflowSamples.js +318 -0
  69. package/dist/runtime/pages/integrations/audit.d.vue.ts +2 -0
  70. package/dist/runtime/pages/integrations/audit.vue +156 -0
  71. package/dist/runtime/pages/integrations/audit.vue.d.ts +2 -0
  72. package/dist/runtime/pages/integrations/credentials/oauth-complete.d.vue.ts +2 -0
  73. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +28 -0
  74. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue.d.ts +2 -0
  75. package/dist/runtime/pages/integrations/credentials.d.vue.ts +2 -0
  76. package/dist/runtime/pages/integrations/credentials.vue +403 -0
  77. package/dist/runtime/pages/integrations/credentials.vue.d.ts +2 -0
  78. package/dist/runtime/pages/integrations/index.d.vue.ts +2 -0
  79. package/dist/runtime/pages/integrations/index.vue +226 -0
  80. package/dist/runtime/pages/integrations/index.vue.d.ts +2 -0
  81. package/dist/runtime/pages/integrations/runs.d.vue.ts +2 -0
  82. package/dist/runtime/pages/integrations/runs.vue +100 -0
  83. package/dist/runtime/pages/integrations/runs.vue.d.ts +2 -0
  84. package/dist/runtime/pages/integrations/schedules.d.vue.ts +2 -0
  85. package/dist/runtime/pages/integrations/schedules.vue +111 -0
  86. package/dist/runtime/pages/integrations/schedules.vue.d.ts +2 -0
  87. package/dist/runtime/pages/integrations/secrets.d.vue.ts +2 -0
  88. package/dist/runtime/pages/integrations/secrets.vue +248 -0
  89. package/dist/runtime/pages/integrations/secrets.vue.d.ts +2 -0
  90. package/dist/runtime/pages/integrations/workflows/[id].d.vue.ts +2 -0
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +326 -0
  92. package/dist/runtime/pages/integrations/workflows/[id].vue.d.ts +2 -0
  93. package/dist/runtime/pages/integrations/workflows/index.d.vue.ts +2 -0
  94. package/dist/runtime/pages/integrations/workflows/index.vue +31 -0
  95. package/dist/runtime/pages/integrations/workflows/index.vue.d.ts +2 -0
  96. package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +2 -0
  97. package/dist/runtime/pages/integrations/workflows/new.vue +153 -0
  98. package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +2 -0
  99. package/dist/runtime/types/integrations.d.ts +427 -0
  100. package/dist/runtime/types/integrations.js +0 -0
  101. package/dist/runtime/utils/localize.d.ts +9 -0
  102. package/dist/runtime/utils/localize.js +21 -0
  103. package/dist/runtime/utils/statusStyle.d.ts +15 -0
  104. package/dist/runtime/utils/statusStyle.js +78 -0
  105. package/dist/types.d.mts +9 -0
  106. package/package.json +46 -0
@@ -0,0 +1,419 @@
1
+ <script setup>
2
+ import { computed, markRaw, nextTick, ref, watch } from "vue";
3
+ import {
4
+ ConnectionLineType,
5
+ MarkerType,
6
+ PanOnScrollMode,
7
+ SelectionMode,
8
+ VueFlow,
9
+ useVueFlow
10
+ } from "@vue-flow/core";
11
+ import { Background } from "@vue-flow/background";
12
+ import { Controls } from "@vue-flow/controls";
13
+ import { MiniMap } from "@vue-flow/minimap";
14
+ import { Widget as LayoutGrid, AddCircle as Plus, MagicStick as Sparkles } from "#solar-icons";
15
+ import "@vue-flow/core/dist/style.css";
16
+ import "@vue-flow/core/dist/theme-default.css";
17
+ import "@vue-flow/controls/dist/style.css";
18
+ import "@vue-flow/minimap/dist/style.css";
19
+ import NodePalette from "./NodePalette.vue";
20
+ import NodeInspector from "./NodeInspector.vue";
21
+ import WorkflowNodeView from "./WorkflowNodeView.vue";
22
+ import WorkflowTriggerView from "./WorkflowTriggerView.vue";
23
+ import WorkflowEdgeView from "./WorkflowEdgeView.vue";
24
+ import { layoutWorkflowNodes } from "./workflowLayout";
25
+ import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
26
+ const props = defineProps({
27
+ workflow: { type: Object, required: true }
28
+ });
29
+ const emit = defineEmits(["change"]);
30
+ const { keys: secretKeys } = useIntegrationsSecrets();
31
+ const blob = ref(clone(props.workflow.blob));
32
+ watch(() => props.workflow.id, () => {
33
+ blob.value = clone(props.workflow.blob);
34
+ syncFromBlob();
35
+ });
36
+ function clone(v) {
37
+ return JSON.parse(JSON.stringify(v));
38
+ }
39
+ const vfNodes = ref([]);
40
+ const vfEdges = ref([]);
41
+ function edgeKindFromPort(sourceNodeId, output) {
42
+ const wn = blob.value.nodes.find((n) => n.id === sourceNodeId);
43
+ if (!wn) return "default";
44
+ const manifest = findManifest(wn.nodeSlug, wn.nodeVersion);
45
+ if (!manifest) return "default";
46
+ if (output === "true") return "branch-true";
47
+ if (output === "false") return "branch-false";
48
+ if (output === "error") return "error";
49
+ const portDef = (manifest.outputs ?? []).find((o) => o.name === output);
50
+ if (portDef) {
51
+ if (portDef.kind === "error") return "error";
52
+ if (portDef.kind === "branch") return "branch";
53
+ return "success";
54
+ }
55
+ for (const o of manifest.outputs ?? []) {
56
+ if (o.sourceFromConfig) {
57
+ if (o.kind === "error") return "error";
58
+ if (o.kind === "branch") return "branch";
59
+ }
60
+ }
61
+ return "default";
62
+ }
63
+ function stripSide(handleId) {
64
+ if (!handleId) return null;
65
+ const i = handleId.indexOf("__");
66
+ return i >= 0 ? handleId.slice(0, i) : handleId;
67
+ }
68
+ function nodePosition(id) {
69
+ const wn = blob.value.nodes.find((n) => n.id === id);
70
+ if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
71
+ const ti = blob.value.triggers.findIndex((t) => t.id === id);
72
+ if (ti >= 0) return { x: -40, y: 60 + ti * 140 };
73
+ return { x: 0, y: 0 };
74
+ }
75
+ function pickSourceSide(sourceId, targetId) {
76
+ const a = nodePosition(sourceId);
77
+ const b = nodePosition(targetId);
78
+ const dx = b.x - a.x;
79
+ const dy = b.y - a.y;
80
+ if (Math.abs(dx) * 1.1 > Math.abs(dy)) return dx >= 0 ? "right" : "left";
81
+ return dy >= 0 ? "bottom" : "top";
82
+ }
83
+ function oppositeSide(s) {
84
+ return s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
85
+ }
86
+ function syncFromBlob() {
87
+ vfNodes.value = [
88
+ ...blob.value.triggers.map((t, i) => ({
89
+ id: t.id,
90
+ type: "trigger",
91
+ position: { x: -40, y: 60 + i * 140 },
92
+ data: t
93
+ })),
94
+ ...blob.value.nodes.map((n) => ({
95
+ id: n.id,
96
+ type: "workflow",
97
+ position: n.position ?? { x: 240, y: 80 },
98
+ data: n
99
+ }))
100
+ ];
101
+ vfEdges.value = blob.value.edges.map((e) => makeVfEdge(e.id, e.from.nodeId, e.to.nodeId, e.from.output));
102
+ }
103
+ function makeVfEdge(id, sourceId, targetId, output) {
104
+ const kind = edgeKindFromPort(sourceId, output);
105
+ const wn = blob.value.nodes.find((n) => n.id === sourceId);
106
+ const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
107
+ const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false", "error"].includes(output);
108
+ const srcSide = pickSourceSide(sourceId, targetId);
109
+ const tgtSide = oppositeSide(srcSide);
110
+ return {
111
+ id,
112
+ source: sourceId,
113
+ target: targetId,
114
+ sourceHandle: `${output}__${srcSide}`,
115
+ targetHandle: `__in__${tgtSide}`,
116
+ type: "flow",
117
+ data: {
118
+ kind,
119
+ label: isBranchPort ? output : void 0
120
+ },
121
+ markerEnd: {
122
+ type: MarkerType.ArrowClosed,
123
+ width: 16,
124
+ height: 16,
125
+ color: edgeColorByKind(kind)
126
+ }
127
+ };
128
+ }
129
+ function edgeColorByKind(kind) {
130
+ switch (kind) {
131
+ case "success":
132
+ return "#10b981";
133
+ case "error":
134
+ return "#f43f5e";
135
+ case "branch-true":
136
+ return "#10b981";
137
+ case "branch-false":
138
+ return "#f59e0b";
139
+ case "branch":
140
+ return "#8b5cf6";
141
+ default:
142
+ return "#94a3b8";
143
+ }
144
+ }
145
+ syncFromBlob();
146
+ const flow = useVueFlow();
147
+ flow.onNodeDragStop((evt) => {
148
+ const id = evt.node.id;
149
+ if (blob.value.triggers.some((t) => t.id === id)) return;
150
+ const wn = blob.value.nodes.find((n) => n.id === id);
151
+ if (!wn) return;
152
+ wn.position = { x: Math.round(evt.node.position.x), y: Math.round(evt.node.position.y) };
153
+ syncFromBlob();
154
+ push();
155
+ });
156
+ flow.onConnect((conn) => {
157
+ if (!conn.source || !conn.target) return;
158
+ const id = `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
159
+ const output = stripSide(conn.sourceHandle) ?? "default";
160
+ blob.value.edges.push({
161
+ id,
162
+ from: { nodeId: conn.source, output },
163
+ to: { nodeId: conn.target, input: void 0 }
164
+ });
165
+ syncFromBlob();
166
+ push();
167
+ });
168
+ flow.onEdgesChange((changes) => {
169
+ let mutated = false;
170
+ for (const c of changes) {
171
+ if (c.type === "remove") {
172
+ blob.value.edges = blob.value.edges.filter((e) => e.id !== c.id);
173
+ mutated = true;
174
+ }
175
+ }
176
+ if (mutated) push();
177
+ });
178
+ flow.onNodesChange((changes) => {
179
+ for (const c of changes) {
180
+ if (c.type === "remove") removeBlobNode(c.id);
181
+ }
182
+ });
183
+ function removeBlobNode(id) {
184
+ blob.value.nodes = blob.value.nodes.filter((n) => n.id !== id);
185
+ blob.value.triggers = blob.value.triggers.filter((t) => t.id !== id);
186
+ blob.value.edges = blob.value.edges.filter((e) => e.from.nodeId !== id && e.to.nodeId !== id);
187
+ if (selectedId.value === id) selectedId.value = null;
188
+ syncFromBlob();
189
+ push();
190
+ }
191
+ function push() {
192
+ emit("change", clone(blob.value));
193
+ }
194
+ function isValidConnection(conn) {
195
+ if (!conn.source || !conn.target) return false;
196
+ if (conn.source === conn.target) return false;
197
+ return true;
198
+ }
199
+ const selectedId = ref(null);
200
+ const selection = computed(() => {
201
+ if (!selectedId.value) return null;
202
+ return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t) => t.id === selectedId.value) ?? null;
203
+ });
204
+ flow.onNodeClick((evt) => {
205
+ selectedId.value = evt.node.id;
206
+ });
207
+ flow.onPaneClick(() => {
208
+ selectedId.value = null;
209
+ });
210
+ const paletteOpen = ref(false);
211
+ watch(selectedId, (next) => {
212
+ if (next) paletteOpen.value = false;
213
+ });
214
+ watch(
215
+ () => blob.value.nodes.length + blob.value.triggers.length,
216
+ (count, prev) => {
217
+ if (prev === void 0 && count === 0) paletteOpen.value = true;
218
+ },
219
+ { immediate: true }
220
+ );
221
+ function applyInspectorUpdate(next) {
222
+ const id = next.id;
223
+ const triggerIdx = blob.value.triggers.findIndex((t) => t.id === id);
224
+ if (triggerIdx >= 0) {
225
+ blob.value.triggers[triggerIdx] = next;
226
+ } else {
227
+ const nodeIdx = blob.value.nodes.findIndex((n) => n.id === id);
228
+ if (nodeIdx >= 0) blob.value.nodes[nodeIdx] = next;
229
+ }
230
+ syncFromBlob();
231
+ push();
232
+ }
233
+ function onDragOver(ev) {
234
+ ev.preventDefault();
235
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = "move";
236
+ }
237
+ function onDrop(ev) {
238
+ ev.preventDefault();
239
+ const raw = ev.dataTransfer?.getData("application/integration-node");
240
+ if (!raw) return;
241
+ const payload = JSON.parse(raw);
242
+ const pos = flow.screenToFlowCoordinate({ x: ev.clientX, y: ev.clientY });
243
+ if (payload.kind === "trigger") {
244
+ const tpl = TRIGGER_TEMPLATES.find((t) => t.type === payload.type);
245
+ if (!tpl) return;
246
+ const id2 = `trg-${Date.now()}`;
247
+ blob.value.triggers.push({ id: id2, type: tpl.type, config: { ...tpl.defaultConfig } });
248
+ syncFromBlob();
249
+ selectedId.value = id2;
250
+ push();
251
+ return;
252
+ }
253
+ const manifest = findManifest(payload.slug, payload.version);
254
+ if (!manifest) return;
255
+ const id = `n-${Date.now()}`;
256
+ blob.value.nodes.push({
257
+ id,
258
+ nodeSlug: manifest.slug,
259
+ nodeVersion: manifest.version,
260
+ config: defaultConfigFor(manifest.config ?? []),
261
+ position: { x: Math.round(pos.x), y: Math.round(pos.y) }
262
+ });
263
+ syncFromBlob();
264
+ selectedId.value = id;
265
+ push();
266
+ }
267
+ function defaultConfigFor(fields = []) {
268
+ const out = {};
269
+ for (const f of fields ?? []) {
270
+ if (f.default !== void 0) out[f.key] = f.default;
271
+ }
272
+ return out;
273
+ }
274
+ async function tidyUp() {
275
+ const laid = layoutWorkflowNodes(vfNodes.value, vfEdges.value, { direction: "LR" });
276
+ vfNodes.value = laid;
277
+ for (const n of laid) {
278
+ if (n.type === "workflow") {
279
+ const wn = blob.value.nodes.find((b) => b.id === n.id);
280
+ if (wn) wn.position = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
281
+ }
282
+ }
283
+ push();
284
+ await nextTick();
285
+ flow.fitView({ padding: 0.2, duration: 400 });
286
+ }
287
+ const nodeTypes = {
288
+ workflow: markRaw(WorkflowNodeView),
289
+ trigger: markRaw(WorkflowTriggerView)
290
+ };
291
+ const edgeTypes = {
292
+ flow: markRaw(WorkflowEdgeView)
293
+ };
294
+ </script>
295
+
296
+ <template>
297
+ <div class="flex h-full min-h-0 flex-1 bg-[#f7f6fb] relative">
298
+ <!-- Slide-in node palette: collapses to 0-width unless `paletteOpen`. -->
299
+ <div
300
+ class="shrink-0 overflow-hidden transition-[width] duration-200 ease-out"
301
+ :class="paletteOpen ? 'w-60' : 'w-0'"
302
+ >
303
+ <NodePalette />
304
+ </div>
305
+
306
+ <div
307
+ class="relative flex-1 min-w-0"
308
+ @drop="onDrop"
309
+ @dragover="onDragOver"
310
+ >
311
+ <VueFlow
312
+ v-model:nodes="vfNodes"
313
+ v-model:edges="vfEdges"
314
+ :node-types="nodeTypes"
315
+ :edge-types="edgeTypes"
316
+ :default-edge-options="{ type: 'flow' }"
317
+ :connection-line-type="ConnectionLineType.SmoothStep"
318
+ :connection-line-style="{ stroke: '#94a3b8', strokeWidth: 1.75, strokeDasharray: '4 4' }"
319
+ :is-valid-connection="isValidConnection"
320
+ :snap-to-grid="true"
321
+ :snap-grid="[20, 20]"
322
+ :selection-mode="SelectionMode.Partial"
323
+ :pan-on-scroll-mode="PanOnScrollMode.Free"
324
+ :delete-key-code="['Backspace', 'Delete']"
325
+ :min-zoom="0.25"
326
+ :max-zoom="2"
327
+ fit-view-on-init
328
+ >
329
+ <Background pattern-color="#cbd5e1" :gap="20" :size="1.2" />
330
+ <Controls position="bottom-right" :show-interactive="false" />
331
+ <MiniMap
332
+ position="bottom-left"
333
+ pannable
334
+ zoomable
335
+ :node-stroke-width="3"
336
+ :node-color="(n) => n.type === 'trigger' ? '#10b981' : '#94a3b8'"
337
+ :mask-color="'rgba(15, 23, 42, 0.06)'"
338
+ />
339
+ </VueFlow>
340
+
341
+ <!-- Top-left action cluster -->
342
+ <div class="absolute top-3 left-3 z-10 flex items-center gap-1.5">
343
+ <button
344
+ type="button"
345
+ class="flex items-center gap-1.5 rounded-md border border-black/[0.08] bg-white px-2 py-1 text-sm font-medium text-foreground shadow-sm transition hover:bg-muted/30"
346
+ :class="paletteOpen ? 'ring-2 ring-emerald-500/40' : ''"
347
+ :title="paletteOpen ? 'Hide palette' : 'Add node'"
348
+ @click="paletteOpen = !paletteOpen"
349
+ >
350
+ <Plus :size="14" :class="paletteOpen ? 'rotate-45' : ''" class="transition-transform" />
351
+ {{ paletteOpen ? "Close" : "Add node" }}
352
+ </button>
353
+ <button
354
+ type="button"
355
+ class="flex items-center gap-1.5 rounded-md border border-black/[0.08] bg-white px-2 py-1 text-sm font-medium text-foreground shadow-sm transition hover:bg-muted/30"
356
+ title="Tidy up layout"
357
+ @click="tidyUp"
358
+ >
359
+ <LayoutGrid :size="13" />
360
+ Tidy up
361
+ </button>
362
+ <button
363
+ type="button"
364
+ class="flex items-center gap-1.5 rounded-md border border-black/[0.08] bg-white px-2 py-1 text-sm font-medium text-foreground shadow-sm transition hover:bg-muted/30"
365
+ title="Re-center"
366
+ @click="flow.fitView({ padding: 0.2, duration: 400 })"
367
+ >
368
+ <Sparkles :size="13" />
369
+ Fit
370
+ </button>
371
+ </div>
372
+
373
+ <!-- Empty-state -->
374
+ <div
375
+ v-if="!vfNodes.length"
376
+ class="pointer-events-none absolute inset-0 flex items-center justify-center"
377
+ >
378
+ <div class="pointer-events-auto rounded-lg border border-dashed border-black/[0.12] bg-white/80 px-5 py-4 text-center text-sm text-muted-foreground shadow-sm backdrop-blur-sm">
379
+ <div class="font-medium text-foreground mb-1">
380
+ Empty canvas
381
+ </div>
382
+ <p class="mb-3">
383
+ Drop a trigger and an action to build your first workflow.
384
+ </p>
385
+ <button
386
+ type="button"
387
+ class="inline-flex items-center gap-1 rounded-md bg-emerald-500 px-2 py-1 text-sm font-medium text-white shadow-sm transition hover:bg-emerald-600"
388
+ @click="paletteOpen = true"
389
+ >
390
+ <Plus :size="11" />
391
+ Open palette
392
+ </button>
393
+ </div>
394
+ </div>
395
+ </div>
396
+
397
+ <Transition
398
+ enter-active-class="transition-transform duration-200 ease-out"
399
+ enter-from-class="translate-x-full"
400
+ enter-to-class="translate-x-0"
401
+ leave-active-class="transition-transform duration-150 ease-in"
402
+ leave-from-class="translate-x-0"
403
+ leave-to-class="translate-x-full"
404
+ >
405
+ <NodeInspector
406
+ v-if="selection"
407
+ :selection="selection"
408
+ :secret-keys="secretKeys"
409
+ @update="applyInspectorUpdate"
410
+ @close="selectedId = null"
411
+ @remove="selectedId && removeBlobNode(selectedId)"
412
+ />
413
+ </Transition>
414
+ </div>
415
+ </template>
416
+
417
+ <style>
418
+ .vue-flow__attribution{display:none}.vue-flow__connection-path{stroke:#94a3b8;stroke-dasharray:4 4;stroke-width:1.75}.vue-flow__nodesselection-rect,.vue-flow__selection{background:rgba(16,185,129,.06);border:1px dashed rgba(16,185,129,.45)}.vue-flow__handle{z-index:5}.vue-flow__edge.selected .vue-flow__edge-path{filter:drop-shadow(0 0 4px rgba(99,102,241,.5))}.vue-flow__controls{border:1px solid rgba(15,23,42,.08);border-radius:8px;box-shadow:0 1px 2px rgba(15,23,42,.06);overflow:hidden}.vue-flow__controls-button{background:#fff;border-bottom:1px solid rgba(15,23,42,.04);height:28px;width:28px}.vue-flow__controls-button svg{fill:#475569}.vue-flow__minimap{background:hsla(0,0%,100%,.9)!important;border:1px solid rgba(15,23,42,.08);border-radius:8px;box-shadow:0 1px 2px rgba(15,23,42,.06);overflow:hidden}
419
+ </style>
@@ -0,0 +1,14 @@
1
+ import '@vue-flow/core/dist/style.css';
2
+ import '@vue-flow/core/dist/theme-default.css';
3
+ import '@vue-flow/controls/dist/style.css';
4
+ import '@vue-flow/minimap/dist/style.css';
5
+ import type { Workflow, WorkflowBlob } from '../../types/integrations.js';
6
+ type __VLS_Props = {
7
+ workflow: Workflow;
8
+ };
9
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ change: (blob: WorkflowBlob) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onChange?: ((blob: WorkflowBlob) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import type { WorkflowNode } from '../../types/integrations.js';
2
+ interface NodeData extends WorkflowNode {
3
+ __selected?: boolean;
4
+ }
5
+ type __VLS_Props = {
6
+ id: string;
7
+ data: NodeData;
8
+ selected?: boolean;
9
+ };
10
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ export default _default;
@@ -0,0 +1,200 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
+ import { NodeToolbar } from "@vue-flow/node-toolbar";
5
+ import * as Lucide from "lucide-vue-next";
6
+ import { NODE_CATEGORIES, findManifest } from "../../data/integrationNodeCatalog";
7
+ const props = defineProps({
8
+ id: { type: String, required: true },
9
+ data: { type: Object, required: true },
10
+ selected: { type: Boolean, required: false }
11
+ });
12
+ defineOptions({ inheritAttrs: false });
13
+ defineEmits(["updateNodeInternals"]);
14
+ const manifest = computed(() => findManifest(props.data.nodeSlug, props.data.nodeVersion));
15
+ const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.value?.category));
16
+ function iconFor(name) {
17
+ if (!name) return Lucide.Box;
18
+ const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
19
+ return Lucide[cleaned] || Lucide.Box;
20
+ }
21
+ function labelText(value) {
22
+ if (!value) return "";
23
+ return typeof value === "string" ? value : Object.values(value)[0] ?? "";
24
+ }
25
+ const title = computed(
26
+ () => labelText(props.data.name) || labelText(manifest.value?.name) || props.data.nodeSlug
27
+ );
28
+ const description = computed(() => labelText(manifest.value?.description));
29
+ const inputRequired = computed(() => manifest.value?.input?.required !== false);
30
+ const outputs = computed(() => {
31
+ const m = manifest.value;
32
+ if (!m) return [{ name: "default", kind: "default", label: "out" }];
33
+ const ports = [];
34
+ for (const o of m.outputs ?? []) {
35
+ if (o.name) {
36
+ ports.push({ name: o.name, kind: o.kind, label: labelText(o.label) || o.name });
37
+ } else if (o.sourceFromConfig) {
38
+ const raw = props.data.config?.[o.sourceFromConfig];
39
+ const values = Array.isArray(raw) ? raw : [];
40
+ for (const v of values) {
41
+ const name = String(v);
42
+ ports.push({ name, kind: o.kind, label: name });
43
+ }
44
+ if (o.fallback) {
45
+ ports.push({
46
+ name: o.fallback.name,
47
+ kind: o.kind,
48
+ label: labelText(o.fallback.label) || o.fallback.name
49
+ });
50
+ }
51
+ }
52
+ }
53
+ if (!ports.length) ports.push({ name: "default", kind: "default", label: "out" });
54
+ return ports;
55
+ });
56
+ function handleColorClass(kind) {
57
+ if (kind === "error") return "!bg-rose-400";
58
+ if (kind === "branch") return "!bg-violet-400";
59
+ return "!bg-slate-400";
60
+ }
61
+ function legendDotClass(kind) {
62
+ if (kind === "error") return "bg-rose-400";
63
+ if (kind === "branch") return "bg-violet-400";
64
+ return "bg-slate-400";
65
+ }
66
+ const SIDES = ["top", "right", "bottom", "left"];
67
+ function sideToPosition(s) {
68
+ return s === "top" ? Position.Top : s === "right" ? Position.Right : s === "bottom" ? Position.Bottom : Position.Left;
69
+ }
70
+ function distributedStyle(side, i, total) {
71
+ const pct = `${(i + 1) / (total + 1) * 100}%`;
72
+ if (side === "top" || side === "bottom") return { left: pct };
73
+ return { top: pct };
74
+ }
75
+ const flow = useVueFlow();
76
+ function duplicate() {
77
+ const offset = 80;
78
+ const newId = `n-${Date.now()}`;
79
+ flow.addNodes([{
80
+ id: newId,
81
+ type: "workflow",
82
+ position: {
83
+ x: (props.data.position?.x ?? 0) + offset,
84
+ y: (props.data.position?.y ?? 0) + offset
85
+ },
86
+ data: {
87
+ ...props.data,
88
+ id: newId,
89
+ position: {
90
+ x: (props.data.position?.x ?? 0) + offset,
91
+ y: (props.data.position?.y ?? 0) + offset
92
+ }
93
+ }
94
+ }]);
95
+ }
96
+ function remove() {
97
+ flow.removeNodes([props.id]);
98
+ }
99
+ </script>
100
+
101
+ <template>
102
+ <NodeToolbar :position="Position.Top" :offset="36" class="flex items-center gap-1 rounded-md border border-black/[0.08] bg-white px-1 py-1 shadow-md">
103
+ <button
104
+ type="button"
105
+ class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-foreground/80 hover:bg-muted/50"
106
+ title="Duplicate"
107
+ @click="duplicate"
108
+ >
109
+ <Lucide.Copy :size="11" />
110
+ </button>
111
+ <div class="h-4 w-px bg-black/10" />
112
+ <button
113
+ type="button"
114
+ class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
115
+ title="Delete"
116
+ @click="remove"
117
+ >
118
+ <Lucide.Trash2 :size="11" />
119
+ </button>
120
+ </NodeToolbar>
121
+
122
+ <!-- Input handles: one per side, centered on each edge. -->
123
+ <template v-if="inputRequired">
124
+ <Handle
125
+ v-for="side in SIDES"
126
+ :id="`__in__${side}`"
127
+ :key="`in-${side}`"
128
+ type="target"
129
+ :position="sideToPosition(side)"
130
+ class="!size-2 !rounded-full !border-2 !border-white !bg-slate-400 opacity-70 transition-all hover:!size-2.5 hover:!bg-slate-600 hover:opacity-100"
131
+ />
132
+ </template>
133
+
134
+ <!-- Output handles: each port exposed on all 4 sides, distributed evenly
135
+ per side so multi-output nodes (success/error, switch cases) don't
136
+ overlap. -->
137
+ <template v-for="side in SIDES" :key="`side-${side}`">
138
+ <Handle
139
+ v-for="(p, i) in outputs"
140
+ :id="`${p.name}__${side}`"
141
+ :key="`${p.name}-${side}`"
142
+ type="source"
143
+ :position="sideToPosition(side)"
144
+ :title="p.label"
145
+ :style="distributedStyle(side, i, outputs.length)"
146
+ class="!size-2 !rounded-full !border-2 !border-white opacity-70 transition-all hover:!size-2.5 hover:opacity-100"
147
+ :class="handleColorClass(p.kind)"
148
+ />
149
+ </template>
150
+
151
+ <div
152
+ class="w-[240px] rounded-lg border bg-white transition-all duration-150"
153
+ :class="selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]'"
154
+ >
155
+ <div class="flex items-center gap-2 px-3 pt-2.5 pb-1.5">
156
+ <span class="flex size-7 shrink-0 items-center justify-center rounded-md" :class="category?.color ?? 'bg-muted text-foreground'">
157
+ <component :is="iconFor(manifest?.icon)" :size="14" :stroke-width="2.25" />
158
+ </span>
159
+ <div class="min-w-0 flex-1">
160
+ <div class="text-sm font-semibold leading-tight text-foreground truncate">
161
+ {{ title }}
162
+ </div>
163
+ <div class="text-sm text-muted-foreground truncate font-mono">
164
+ {{ data.nodeSlug }}<span class="opacity-60">@{{ data.nodeVersion }}</span>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <p v-if="description" class="px-3 pb-2 text-sm text-muted-foreground line-clamp-2">
170
+ {{ description }}
171
+ </p>
172
+
173
+ <div
174
+ v-if="outputs.length"
175
+ class="border-t border-black/[0.04] bg-muted/20 px-3 py-1.5 rounded-b-lg"
176
+ >
177
+ <div
178
+ v-for="p in outputs"
179
+ :key="p.name"
180
+ class="flex items-center gap-1.5 py-0.5 text-sm"
181
+ >
182
+ <span class="size-1.5 rounded-full shrink-0" :class="legendDotClass(p.kind)" />
183
+ <span
184
+ class="font-mono truncate"
185
+ :class="p.kind === 'error' ? 'text-rose-600' : p.kind === 'branch' ? 'text-violet-600' : 'text-foreground/70'"
186
+ >
187
+ {{ p.label }}
188
+ </span>
189
+ </div>
190
+ </div>
191
+
192
+ <div
193
+ v-if="data.retry"
194
+ class="flex items-center gap-1.5 border-t border-black/[0.04] px-3 py-1 text-sm text-muted-foreground"
195
+ >
196
+ <Lucide.RotateCcw :size="10" />
197
+ <span>Retry × {{ data.retry.maxAttempts }} · {{ data.retry.strategy ?? "linear" }}</span>
198
+ </div>
199
+ </div>
200
+ </template>
@@ -0,0 +1,11 @@
1
+ import type { WorkflowNode } from '../../types/integrations.js';
2
+ interface NodeData extends WorkflowNode {
3
+ __selected?: boolean;
4
+ }
5
+ type __VLS_Props = {
6
+ id: string;
7
+ data: NodeData;
8
+ selected?: boolean;
9
+ };
10
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import type { WorkflowTrigger } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ id: string;
4
+ data: WorkflowTrigger;
5
+ selected?: boolean;
6
+ };
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ export default _default;