@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.
- package/README.md +51 -0
- package/dist/module.d.mts +6 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +45 -0
- package/dist/runtime/components/Integrations/Layout.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/Layout.vue +20 -0
- package/dist/runtime/components/Integrations/Layout.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +450 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +140 -0
- package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/RunDetailDialog.d.vue.ts +11 -0
- package/dist/runtime/components/Integrations/RunDetailDialog.vue +169 -0
- package/dist/runtime/components/Integrations/RunDetailDialog.vue.d.ts +11 -0
- package/dist/runtime/components/Integrations/Sidebar.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/Sidebar.vue +32 -0
- package/dist/runtime/components/Integrations/Sidebar.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +23 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +86 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +23 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +419 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +11 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +200 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +11 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.d.vue.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +82 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue.d.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +268 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/workflowLayout.d.ts +17 -0
- package/dist/runtime/components/Integrations/workflowLayout.js +29 -0
- package/dist/runtime/composables/integrationsApi.d.ts +13 -0
- package/dist/runtime/composables/integrationsApi.js +55 -0
- package/dist/runtime/composables/useCredentialTypes.d.ts +19 -0
- package/dist/runtime/composables/useCredentialTypes.js +47 -0
- package/dist/runtime/composables/useIntegrationsAudit.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsAudit.js +39 -0
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +43 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +98 -0
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +16 -0
- package/dist/runtime/composables/useIntegrationsFocus.js +17 -0
- package/dist/runtime/composables/useIntegrationsNodes.d.ts +6 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +32 -0
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +31 -0
- package/dist/runtime/composables/useIntegrationsRuns.js +109 -0
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +14 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +32 -0
- package/dist/runtime/composables/useIntegrationsSecrets.d.ts +27 -0
- package/dist/runtime/composables/useIntegrationsSecrets.js +76 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +19 -0
- package/dist/runtime/composables/useIntegrationsTemplates.js +141 -0
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +39 -0
- package/dist/runtime/composables/useIntegrationsTriggers.js +39 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +28 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.js +152 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +67 -0
- package/dist/runtime/data/integrationRuns.d.ts +7 -0
- package/dist/runtime/data/integrationRuns.js +109 -0
- package/dist/runtime/data/integrationTemplates.d.ts +11 -0
- package/dist/runtime/data/integrationTemplates.js +9 -0
- package/dist/runtime/data/integrationWorkflowSamples.d.ts +18 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +318 -0
- package/dist/runtime/pages/integrations/audit.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/audit.vue +156 -0
- package/dist/runtime/pages/integrations/audit.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/credentials/oauth-complete.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +28 -0
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/credentials.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/credentials.vue +403 -0
- package/dist/runtime/pages/integrations/credentials.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/index.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/index.vue +226 -0
- package/dist/runtime/pages/integrations/index.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/runs.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/runs.vue +100 -0
- package/dist/runtime/pages/integrations/runs.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/schedules.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/schedules.vue +111 -0
- package/dist/runtime/pages/integrations/schedules.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/secrets.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/secrets.vue +248 -0
- package/dist/runtime/pages/integrations/secrets.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/[id].d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/[id].vue +326 -0
- package/dist/runtime/pages/integrations/workflows/[id].vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/index.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/index.vue +31 -0
- package/dist/runtime/pages/integrations/workflows/index.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/new.vue +153 -0
- package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +2 -0
- package/dist/runtime/types/integrations.d.ts +427 -0
- package/dist/runtime/types/integrations.js +0 -0
- package/dist/runtime/utils/localize.d.ts +9 -0
- package/dist/runtime/utils/localize.js +21 -0
- package/dist/runtime/utils/statusStyle.d.ts +15 -0
- package/dist/runtime/utils/statusStyle.js +78 -0
- package/dist/types.d.mts +9 -0
- 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;
|