@revenexx/studio-integrations 0.7.0 → 1.0.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 +416 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +4 -1
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +8 -2
- package/dist/runtime/components/Integrations/ExpressionInput.vue +5 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +16 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +1 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +95 -10
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +34 -8
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +12 -8
- package/dist/runtime/components/Integrations/StickyNoteView.vue +45 -11
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -3
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +12 -3
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +52 -11
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +59 -28
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +3 -4
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +37 -26
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -5
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +53 -14
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
- package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +18 -7
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +25 -4
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +22 -1
- package/dist/runtime/data/integrationNodeCatalog.js +8 -4
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/i18n/messages/de.json +20 -3
- package/dist/runtime/i18n/messages/en.json +20 -3
- package/dist/runtime/pages/integrations/credentials.vue +4 -1
- package/dist/runtime/pages/integrations/index.vue +16 -17
- package/dist/runtime/pages/integrations/templates/[slug].vue +13 -5
- package/dist/runtime/pages/integrations/workflows/[id].vue +193 -104
- package/dist/runtime/types/integrations.d.ts +25 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +6 -1
- package/dist/runtime/utils/credentialState.js +1 -1
- package/dist/runtime/utils/localize.d.ts +34 -0
- package/dist/runtime/utils/localize.js +22 -7
- package/dist/runtime/utils/pollBudget.d.ts +1 -1
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, resolveComponent } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2
|
|
3
|
+
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
|
+
import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
|
|
4
5
|
import { templateCategoryLabel, templatePackageLabel } from "../../data/integrationTemplates";
|
|
5
6
|
import TemplateFlowPreview from "./TemplateFlowPreview.vue";
|
|
6
7
|
const props = defineProps({
|
|
@@ -17,7 +18,6 @@ const full = computed(() => props.density === "full");
|
|
|
17
18
|
const banner = computed(
|
|
18
19
|
() => full.value ? props.template.images.find((i) => i.category === "banner")?.src ?? null : null
|
|
19
20
|
);
|
|
20
|
-
const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
|
|
21
21
|
const TRIGGER_LABEL_KEY = {
|
|
22
22
|
schedule: "integration.templates.card.trigger.schedule",
|
|
23
23
|
webhook: "integration.templates.trigger.webhook",
|
|
@@ -104,7 +104,7 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
|
|
|
104
104
|
:key="triggerType"
|
|
105
105
|
class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase"
|
|
106
106
|
>
|
|
107
|
-
<component :is="
|
|
107
|
+
<component :is="glyphForTrigger(triggerType)" :size="10" />
|
|
108
108
|
{{ triggerTypeLabel(triggerType) }}
|
|
109
109
|
</span>
|
|
110
110
|
<span class="flex-1" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, onMounted } from "vue";
|
|
3
3
|
import * as Lucide from "lucide-vue-next";
|
|
4
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
4
5
|
import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
5
6
|
import { resolveLocalized } from "../../utils/localize";
|
|
6
7
|
import { describeCron } from "../../utils/schedule";
|
|
@@ -12,6 +13,10 @@ const props = defineProps({
|
|
|
12
13
|
showErrorBranches: { type: Boolean, required: false, default: false }
|
|
13
14
|
});
|
|
14
15
|
const t = useIntegrationsT();
|
|
16
|
+
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
void loadNodeCatalog();
|
|
19
|
+
});
|
|
15
20
|
const STOP_SLUG = "revenexx:stop-and-error";
|
|
16
21
|
const NODE_W = 220;
|
|
17
22
|
const NODE_H = 58;
|
|
@@ -37,7 +42,11 @@ const boxes = computed(() => {
|
|
|
37
42
|
subtitle,
|
|
38
43
|
type: t2.type,
|
|
39
44
|
manifest: null,
|
|
40
|
-
|
|
45
|
+
// The trigger's glyph from the one map, like every other trigger box. This
|
|
46
|
+
// resolved `TRIGGER_TEMPLATES`' icon name ('Zap', 'Play', …) out of Lucide
|
|
47
|
+
// instead — the same route the palette's tiles took — so the preview drew a
|
|
48
|
+
// Lucide bolt where the card it previews draws Solar's (PO-329).
|
|
49
|
+
icon: triggerGlyph(t2.type)
|
|
41
50
|
});
|
|
42
51
|
});
|
|
43
52
|
blob.nodes.forEach((n, i) => {
|
|
@@ -162,7 +171,7 @@ const bounds = computed(() => {
|
|
|
162
171
|
>
|
|
163
172
|
<div class="flex flex-1 items-center gap-2.5 px-3 py-2">
|
|
164
173
|
<span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-emerald-500 text-white shadow-sm">
|
|
165
|
-
<component :is="b.icon" :size="15"
|
|
174
|
+
<component :is="b.icon" :size="15" />
|
|
166
175
|
</span>
|
|
167
176
|
<div class="min-w-0 flex-1">
|
|
168
177
|
<div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2, MagicStick as Sparkles } from "#solar-icons";
|
|
3
|
+
import { RefreshCircle as Loader2, Magnifier as Search, MagicStick as Sparkles } from "#solar-icons";
|
|
4
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
4
5
|
import * as Lucide from "lucide-vue-next";
|
|
5
6
|
import {
|
|
6
7
|
Badge,
|
|
@@ -179,7 +180,7 @@ async function useTemplate(slug) {
|
|
|
179
180
|
<SelectValue>
|
|
180
181
|
<span class="flex min-w-0 items-center gap-2">
|
|
181
182
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
182
|
-
<component :is="
|
|
183
|
+
<component :is="triggerGlyph(selectedTrigger?.type)" :size="13" />
|
|
183
184
|
</span>
|
|
184
185
|
<span class="shrink-0 text-sm font-medium">{{ resolveLocalized(selectedTrigger?.name) }}</span>
|
|
185
186
|
<span class="truncate text-sm text-muted-foreground">{{ resolveLocalized(selectedTrigger?.description) }}</span>
|
|
@@ -190,7 +191,7 @@ async function useTemplate(slug) {
|
|
|
190
191
|
<SelectItem v-for="t in TRIGGER_TEMPLATES" :key="t.id" :value="t.type">
|
|
191
192
|
<span class="flex items-center gap-2.5 py-0.5">
|
|
192
193
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
|
|
193
|
-
<component :is="
|
|
194
|
+
<component :is="triggerGlyph(t.type)" :size="14" />
|
|
194
195
|
</span>
|
|
195
196
|
<span class="min-w-0">
|
|
196
197
|
<span class="block text-sm font-medium">{{ resolveLocalized(t.name) }}</span>
|
|
@@ -250,7 +251,7 @@ async function useTemplate(slug) {
|
|
|
250
251
|
<!-- ── From template ─────────────────────────────────────────────────── -->
|
|
251
252
|
<div v-else class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1">
|
|
252
253
|
<div class="relative">
|
|
253
|
-
<
|
|
254
|
+
<Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
254
255
|
<input
|
|
255
256
|
v-model="templateSearch"
|
|
256
257
|
type="text"
|
|
@@ -4,6 +4,22 @@ import '@vue-flow/minimap/dist/style.css';
|
|
|
4
4
|
import type { Workflow, WorkflowBlob } from '../../types/integrations.js';
|
|
5
5
|
type __VLS_Props = {
|
|
6
6
|
workflow: Workflow;
|
|
7
|
+
/**
|
|
8
|
+
* Unsaved work the page is still holding for this workflow, read ONCE, at
|
|
9
|
+
* mount (behaviour specs/workflow-save.md).
|
|
10
|
+
*
|
|
11
|
+
* The editor is mounted per tab, so a trip to the Runs tab and back seeded a
|
|
12
|
+
* fresh working copy from the SAVED blob while the page still held the draft:
|
|
13
|
+
* the canvas showed one graph, the header said "Unsaved changes" about
|
|
14
|
+
* another, and Save wrote the one nobody could see. Seeding from the draft is
|
|
15
|
+
* what makes a move inside the workflow keep the work.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately not watched. The page's draft is a copy of what this editor
|
|
18
|
+
* last emitted, so following it would re-seed the canvas — and drop the
|
|
19
|
+
* selection, the undo stack and any open dialog with it — on the user's own
|
|
20
|
+
* keystrokes.
|
|
21
|
+
*/
|
|
22
|
+
draft?: WorkflowBlob | null;
|
|
7
23
|
/** Busy flag for the control-pane Play button (a run is being triggered). */
|
|
8
24
|
runBusy?: boolean;
|
|
9
25
|
/** The run is queued behind a bundle build — busy, but worth saying why. */
|
|
@@ -56,6 +56,7 @@ import { findingsByNodeField } from "../../utils/validationFindings";
|
|
|
56
56
|
import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
57
57
|
const props = defineProps({
|
|
58
58
|
workflow: { type: Object, required: true },
|
|
59
|
+
draft: { type: [Object, null], required: false },
|
|
59
60
|
runBusy: { type: Boolean, required: false },
|
|
60
61
|
runWaiting: { type: Boolean, required: false },
|
|
61
62
|
overlayOpen: { type: Boolean, required: false }
|
|
@@ -63,7 +64,7 @@ const props = defineProps({
|
|
|
63
64
|
const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
|
|
64
65
|
const t = useIntegrationsT();
|
|
65
66
|
const { keys: secretKeys } = useIntegrationsSecrets();
|
|
66
|
-
const blob = ref(clone(props.workflow.blob));
|
|
67
|
+
const blob = ref(clone(props.draft ?? props.workflow.blob));
|
|
67
68
|
watch(() => props.workflow.id, () => {
|
|
68
69
|
blob.value = clone(props.workflow.blob);
|
|
69
70
|
syncFromBlob();
|
|
@@ -469,22 +470,49 @@ flow.onEdgeUpdate(({ edge, connection }) => {
|
|
|
469
470
|
syncFromBlob();
|
|
470
471
|
push();
|
|
471
472
|
});
|
|
472
|
-
|
|
473
|
+
let pendingEdgeRemovals = [];
|
|
474
|
+
let pendingNodeRemovals = [];
|
|
475
|
+
let removalFlushQueued = false;
|
|
476
|
+
function queueRemovalFlush() {
|
|
477
|
+
if (removalFlushQueued) return;
|
|
478
|
+
removalFlushQueued = true;
|
|
479
|
+
void nextTick(flushRemovals);
|
|
480
|
+
}
|
|
481
|
+
function flushRemovals() {
|
|
482
|
+
removalFlushQueued = false;
|
|
483
|
+
const edges = pendingEdgeRemovals;
|
|
484
|
+
const nodes = pendingNodeRemovals;
|
|
485
|
+
pendingEdgeRemovals = [];
|
|
486
|
+
pendingNodeRemovals = [];
|
|
487
|
+
const goneNodes = new Set(nodes);
|
|
473
488
|
let mutated = false;
|
|
489
|
+
for (const c of edges) {
|
|
490
|
+
if (goneNodes.has(c.source) || goneNodes.has(c.target)) continue;
|
|
491
|
+
blob.value.edges = blob.value.edges.filter((e) => e.id !== c.id);
|
|
492
|
+
mutated = true;
|
|
493
|
+
}
|
|
494
|
+
if (nodes.length) {
|
|
495
|
+
dropFromBlob(nodes);
|
|
496
|
+
mutated = true;
|
|
497
|
+
}
|
|
498
|
+
if (mutated) {
|
|
499
|
+
syncFromBlob();
|
|
500
|
+
push();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
flow.onEdgesChange((changes) => {
|
|
474
504
|
for (const c of changes) {
|
|
475
505
|
if (c.type === "remove") {
|
|
476
|
-
|
|
477
|
-
mutated = true;
|
|
506
|
+
pendingEdgeRemovals.push({ id: c.id, source: c.source, target: c.target });
|
|
478
507
|
}
|
|
479
508
|
}
|
|
480
|
-
if (
|
|
509
|
+
if (pendingEdgeRemovals.length) queueRemovalFlush();
|
|
481
510
|
});
|
|
482
511
|
flow.onNodesChange((changes) => {
|
|
483
512
|
const removed = changes.filter((c) => c.type === "remove").map((c) => c.id);
|
|
484
513
|
if (!removed.length) return;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
});
|
|
514
|
+
pendingNodeRemovals.push(...removed);
|
|
515
|
+
queueRemovalFlush();
|
|
488
516
|
});
|
|
489
517
|
function stripFromGroups(drop) {
|
|
490
518
|
if (!blob.value.groups?.length) return;
|
|
@@ -541,7 +569,7 @@ function addSticky(at) {
|
|
|
541
569
|
syncFromBlob();
|
|
542
570
|
push();
|
|
543
571
|
}
|
|
544
|
-
function
|
|
572
|
+
function dropFromBlob(ids) {
|
|
545
573
|
if (!ids.length) return;
|
|
546
574
|
const drop = new Set(ids);
|
|
547
575
|
stripFromGroups(drop);
|
|
@@ -553,6 +581,10 @@ function removeBlobNodes(ids) {
|
|
|
553
581
|
if (blob.value.triggerPositions?.[id]) delete blob.value.triggerPositions[id];
|
|
554
582
|
}
|
|
555
583
|
if (selectedId.value && drop.has(selectedId.value)) selectedId.value = null;
|
|
584
|
+
}
|
|
585
|
+
function removeBlobNodes(ids) {
|
|
586
|
+
if (!ids.length) return;
|
|
587
|
+
dropFromBlob(ids);
|
|
556
588
|
syncFromBlob();
|
|
557
589
|
push();
|
|
558
590
|
}
|
|
@@ -779,14 +811,14 @@ const selectedUpstreamOutputs = computed(() => {
|
|
|
779
811
|
return blob.value.edges.filter((e) => e.to.nodeId === id).flatMap((e) => {
|
|
780
812
|
const source = blob.value.nodes.find((n) => n.id === e.from.nodeId);
|
|
781
813
|
if (!source) return [];
|
|
782
|
-
const
|
|
814
|
+
const port = portsForNode(source, findManifest(source.nodeSlug, source.nodeVersion), dynamicOutputs.value[source.id]).find((p) => p.name === e.from.output);
|
|
783
815
|
return [{
|
|
784
816
|
nodeId: source.id,
|
|
785
817
|
// A node's own name is text the user typed — never trimmed, never read
|
|
786
818
|
// as JSON. The manifest strings below it get the normalising resolver.
|
|
787
819
|
nodeName: resolveDisplayName(source.name) || source.nodeSlug,
|
|
788
820
|
output: e.from.output,
|
|
789
|
-
fields:
|
|
821
|
+
fields: port?.fields.map((f) => f.name)
|
|
790
822
|
}];
|
|
791
823
|
});
|
|
792
824
|
});
|
|
@@ -1213,6 +1245,14 @@ async function focusPalette() {
|
|
|
1213
1245
|
paletteRef.value?.focusSearch();
|
|
1214
1246
|
}
|
|
1215
1247
|
const lightboxOpen = computed(() => !!selection.value);
|
|
1248
|
+
watch(lightboxOpen, (open) => {
|
|
1249
|
+
if (!open) renameIntent.value = false;
|
|
1250
|
+
});
|
|
1251
|
+
function onConfigOpenAutoFocus(event) {
|
|
1252
|
+
if (renameIntent.value) return;
|
|
1253
|
+
event.preventDefault();
|
|
1254
|
+
event.target?.focus();
|
|
1255
|
+
}
|
|
1216
1256
|
const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
|
|
1217
1257
|
const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
|
|
1218
1258
|
useCanvasShortcuts(() => ({
|
|
@@ -1790,6 +1830,7 @@ const edgeTypes = {
|
|
|
1790
1830
|
form as a strip down the left of an empty workbench. -->
|
|
1791
1831
|
<DialogContent
|
|
1792
1832
|
:show-close-button="false"
|
|
1833
|
+
@open-auto-focus="onConfigOpenAutoFocus"
|
|
1793
1834
|
class="flex max-w-none flex-col gap-0 overflow-hidden rounded-lg p-0 sm:max-w-none"
|
|
1794
1835
|
:class="selectionIsTrigger ? 'h-[min(520px,86vh)] w-[min(600px,94vw)]' : 'h-[min(680px,86vh)] w-[min(1100px,94vw)]'"
|
|
1795
1836
|
>
|
|
@@ -4,6 +4,22 @@ import '@vue-flow/minimap/dist/style.css';
|
|
|
4
4
|
import type { Workflow, WorkflowBlob } from '../../types/integrations.js';
|
|
5
5
|
type __VLS_Props = {
|
|
6
6
|
workflow: Workflow;
|
|
7
|
+
/**
|
|
8
|
+
* Unsaved work the page is still holding for this workflow, read ONCE, at
|
|
9
|
+
* mount (behaviour specs/workflow-save.md).
|
|
10
|
+
*
|
|
11
|
+
* The editor is mounted per tab, so a trip to the Runs tab and back seeded a
|
|
12
|
+
* fresh working copy from the SAVED blob while the page still held the draft:
|
|
13
|
+
* the canvas showed one graph, the header said "Unsaved changes" about
|
|
14
|
+
* another, and Save wrote the one nobody could see. Seeding from the draft is
|
|
15
|
+
* what makes a move inside the workflow keep the work.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately not watched. The page's draft is a copy of what this editor
|
|
18
|
+
* last emitted, so following it would re-seed the canvas — and drop the
|
|
19
|
+
* selection, the undo stack and any open dialog with it — on the user's own
|
|
20
|
+
* keystrokes.
|
|
21
|
+
*/
|
|
22
|
+
draft?: WorkflowBlob | null;
|
|
7
23
|
/** Busy flag for the control-pane Play button (a run is being triggered). */
|
|
8
24
|
runBusy?: boolean;
|
|
9
25
|
/** The run is queued behind a bundle build — busy, but worth saying why. */
|
|
@@ -3,6 +3,12 @@ import '@vue-flow/core/dist/theme-default.css';
|
|
|
3
3
|
import type { Workflow } from '../../types/integrations.js';
|
|
4
4
|
type __VLS_Props = {
|
|
5
5
|
workflow: Workflow;
|
|
6
|
+
/**
|
|
7
|
+
* The editor has unsaved work the page is holding. Nothing here shows it — the
|
|
8
|
+
* canvas behind this tab is not mounted — but a restore is what destroys it, so
|
|
9
|
+
* the question this panel asks has to name it (specs/workflow-save.md).
|
|
10
|
+
*/
|
|
11
|
+
draftPending?: boolean;
|
|
6
12
|
};
|
|
7
13
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
14
|
restored: () => any;
|
|
@@ -20,7 +20,8 @@ import NodeDynamicOutputsResolver from "./NodeDynamicOutputsResolver.vue";
|
|
|
20
20
|
import { findManifest } from "../../data/integrationNodeCatalog";
|
|
21
21
|
import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
|
|
22
22
|
const props = defineProps({
|
|
23
|
-
workflow: { type: Object, required: true }
|
|
23
|
+
workflow: { type: Object, required: true },
|
|
24
|
+
draftPending: { type: Boolean, required: false }
|
|
24
25
|
});
|
|
25
26
|
const emit = defineEmits(["restored"]);
|
|
26
27
|
const revisionsApi = useIntegrationsRevisions();
|
|
@@ -225,7 +226,8 @@ async function select(revision) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
async function restoreRevision(rev) {
|
|
228
|
-
|
|
229
|
+
const question = props.draftPending ? t("integration.history.confirm.restoreWithDraft", { revision: rev.revision }) : t("integration.history.confirm.restore", { revision: rev.revision });
|
|
230
|
+
if (!confirm(question)) return;
|
|
229
231
|
restoring.value = rev.revision;
|
|
230
232
|
try {
|
|
231
233
|
await revisionsApi.restore(props.workflow.id, rev.revision);
|
|
@@ -3,6 +3,12 @@ import '@vue-flow/core/dist/theme-default.css';
|
|
|
3
3
|
import type { Workflow } from '../../types/integrations.js';
|
|
4
4
|
type __VLS_Props = {
|
|
5
5
|
workflow: Workflow;
|
|
6
|
+
/**
|
|
7
|
+
* The editor has unsaved work the page is holding. Nothing here shows it — the
|
|
8
|
+
* canvas behind this tab is not mounted — but a restore is what destroys it, so
|
|
9
|
+
* the question this panel asks has to name it (specs/workflow-save.md).
|
|
10
|
+
*/
|
|
11
|
+
draftPending?: boolean;
|
|
6
12
|
};
|
|
7
13
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
14
|
restored: () => any;
|
|
@@ -3,8 +3,8 @@ import { computed, inject, nextTick, ref, watch } from "vue";
|
|
|
3
3
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
5
|
import * as Lucide from "lucide-vue-next";
|
|
6
|
-
import { Copy, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
7
|
-
import { NODE_CATEGORIES, findManifest } from "../../data/integrationNodeCatalog";
|
|
6
|
+
import { Copy, DangerTriangle as AlertTriangle, RefreshCircle as Loader2, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
7
|
+
import { NODE_CATEGORIES, findManifest, isVersionExecutable } from "../../data/integrationNodeCatalog";
|
|
8
8
|
import { isTerminalManifest } from "../../data/nodeInputData";
|
|
9
9
|
import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
|
|
10
10
|
const props = defineProps({
|
|
@@ -20,7 +20,6 @@ const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.v
|
|
|
20
20
|
const title = computed(
|
|
21
21
|
() => resolveDisplayName(props.data.name) || resolveLocalized(manifest.value?.name) || props.data.nodeSlug
|
|
22
22
|
);
|
|
23
|
-
const inputRequired = computed(() => manifest.value?.input?.required !== false);
|
|
24
23
|
const invalidFields = computed(() => props.data.__invalidFields ?? []);
|
|
25
24
|
const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, { fields: invalidFields.value.join(", ") }));
|
|
26
25
|
const notes = computed(() => (props.data.notes ?? "").trim());
|
|
@@ -31,11 +30,15 @@ const notePlacement = ref(Position.Bottom);
|
|
|
31
30
|
const noteMaxHeight = ref(0);
|
|
32
31
|
let noteTimer = null;
|
|
33
32
|
const notePanelId = computed(() => `node-note-${props.id}`);
|
|
33
|
+
function handleTitle(p) {
|
|
34
|
+
return p.hint ? p.label + " \u2014 " + p.hint : p.label;
|
|
35
|
+
}
|
|
34
36
|
function portsFromOutputs(list) {
|
|
35
37
|
const ports = [];
|
|
36
38
|
for (const o of list) {
|
|
37
39
|
if (o.name) {
|
|
38
|
-
|
|
40
|
+
const hint = resolveLocalized(o.description);
|
|
41
|
+
ports.push({ name: o.name, kind: o.kind, label: resolveLocalized(o.label) || o.name, ...hint ? { hint } : {} });
|
|
39
42
|
} else if (o.sourceFromConfig) {
|
|
40
43
|
const raw = props.data.config?.[o.sourceFromConfig];
|
|
41
44
|
const values = Array.isArray(raw) ? raw : [];
|
|
@@ -160,6 +163,8 @@ function releaseNote() {
|
|
|
160
163
|
}, 250);
|
|
161
164
|
}
|
|
162
165
|
const nodeActions = inject("workflow-node-actions", null);
|
|
166
|
+
const unbuildableVersion = computed(() => nodeActions !== null && !isVersionExecutable(props.data.nodeSlug, props.data.nodeVersion));
|
|
167
|
+
const unbuildableLabel = computed(() => t("integration.canvas.node.badge.noBuiltCode", { version: props.data.nodeVersion }));
|
|
163
168
|
const canConfigure = computed(() => typeof nodeActions?.configure === "function");
|
|
164
169
|
function configure() {
|
|
165
170
|
nodeActions?.configure?.(props.id);
|
|
@@ -226,17 +231,15 @@ function remove() {
|
|
|
226
231
|
|
|
227
232
|
<!-- Input: ONE logical input, attachable from the left or the top — both
|
|
228
233
|
dots feed the same `__in` port. Clearly visible, ring + shadow. -->
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
/>
|
|
239
|
-
</template>
|
|
234
|
+
<Handle
|
|
235
|
+
v-for="side in IN_SIDES"
|
|
236
|
+
:id="`__in__${side}`"
|
|
237
|
+
:key="`in-${side}`"
|
|
238
|
+
type="target"
|
|
239
|
+
:position="sideToPosition(side)"
|
|
240
|
+
:title="t('integration.canvas.node.handle.input')"
|
|
241
|
+
class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-slate-700"
|
|
242
|
+
/>
|
|
240
243
|
|
|
241
244
|
<!-- Output handles: right + bottom, distributed per side. Color encodes the
|
|
242
245
|
port semantics (true/false/error/branch/default), matching the edges. -->
|
|
@@ -247,7 +250,7 @@ function remove() {
|
|
|
247
250
|
:key="`${p.name}-${side}`"
|
|
248
251
|
type="source"
|
|
249
252
|
:position="sideToPosition(side)"
|
|
250
|
-
:title="p
|
|
253
|
+
:title="handleTitle(p)"
|
|
251
254
|
:style="distributedStyle(side, i, outputs.length)"
|
|
252
255
|
class="!size-3 !rounded-full !border-2 !border-white !shadow-sm transition-all hover:!size-3.5"
|
|
253
256
|
:class="portTone(p).handle"
|
|
@@ -265,19 +268,43 @@ function remove() {
|
|
|
265
268
|
showPortLabels ? 'pb-3.5' : ''
|
|
266
269
|
]"
|
|
267
270
|
>
|
|
268
|
-
<!--
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
<!-- The card's marks, in one row at the corner the input handles leave free.
|
|
272
|
+
A row rather than two absolutely-placed badges: the amber one's width
|
|
273
|
+
follows its count, so any fixed offset for a second badge overlaps it as
|
|
274
|
+
soon as the count reaches two digits. Hoverable on purpose — a symbol or
|
|
275
|
+
a bare count at a corner is a riddle, so each carries its own title;
|
|
276
|
+
`nodrag` keeps that hover from starting a card drag. -->
|
|
272
277
|
<span
|
|
273
|
-
v-if="invalidFields.length"
|
|
274
|
-
|
|
275
|
-
:aria-label="invalidLabel"
|
|
276
|
-
:title="invalidLabel"
|
|
277
|
-
class="nodrag absolute -left-2 -top-2 z-10 flex cursor-help items-center gap-0.5 rounded-full bg-amber-500 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
|
|
278
|
+
v-if="invalidFields.length || unbuildableVersion"
|
|
279
|
+
class="nodrag absolute -left-2 -top-2 z-10 flex items-center gap-1"
|
|
278
280
|
>
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
+
<span
|
|
282
|
+
v-if="invalidFields.length"
|
|
283
|
+
role="img"
|
|
284
|
+
:aria-label="invalidLabel"
|
|
285
|
+
:title="invalidLabel"
|
|
286
|
+
class="flex cursor-help items-center gap-0.5 rounded-full bg-amber-500 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
|
|
287
|
+
>
|
|
288
|
+
<!-- `weight="Bold"` for the same reason the Lucide draw carried
|
|
289
|
+
`stroke-width="2.5"`: 9px of white on amber. Solar's default
|
|
290
|
+
BoldDuotone washes out at that size against the pill. -->
|
|
291
|
+
<AlertTriangle :size="9" weight="Bold" />
|
|
292
|
+
{{ invalidFields.length }}
|
|
293
|
+
</span>
|
|
294
|
+
<!-- The pinned version cannot be run. Rose rather than amber, and no
|
|
295
|
+
count: amber on this card means unfinished work of the user's own —
|
|
296
|
+
settings still to fill in — while this is the platform missing a piece,
|
|
297
|
+
which the user cannot fix by typing. Hence the stronger colour despite
|
|
298
|
+
being the one that does NOT block activation. -->
|
|
299
|
+
<span
|
|
300
|
+
v-if="unbuildableVersion"
|
|
301
|
+
role="img"
|
|
302
|
+
:aria-label="unbuildableLabel"
|
|
303
|
+
:title="unbuildableLabel"
|
|
304
|
+
class="flex cursor-help items-center rounded-full bg-rose-600 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
|
|
305
|
+
>
|
|
306
|
+
<AlertTriangle :size="9" weight="Bold" />
|
|
307
|
+
</span>
|
|
281
308
|
</span>
|
|
282
309
|
<!-- Always-visible edit affordance: single click opens the config lightbox.
|
|
283
310
|
(Double-click on the whole card works too.) Hidden in read-only hosts
|
|
@@ -330,7 +357,11 @@ function remove() {
|
|
|
330
357
|
v-if="resolvePending"
|
|
331
358
|
class="pointer-events-none absolute bottom-1 left-1/2 flex -translate-x-1/2 select-none items-center gap-1 whitespace-nowrap text-[9px] font-semibold uppercase tracking-wide text-muted-foreground"
|
|
332
359
|
>
|
|
333
|
-
|
|
360
|
+
<!-- `weight="Bold"` for the badge's reason one size down: the default
|
|
361
|
+
BoldDuotone draws a filled disc across the whole viewBox, and a disc
|
|
362
|
+
turning looks like a disc standing still. Bold knocks the arrows out
|
|
363
|
+
of the ring, so 9px of it still reads as spinning. -->
|
|
364
|
+
<Loader2 :size="9" weight="Bold" class="animate-spin" />
|
|
334
365
|
{{ t("integration.canvas.node.port.resolving") }}
|
|
335
366
|
</span>
|
|
336
367
|
<template v-else>
|
|
@@ -11,9 +11,7 @@ import {
|
|
|
11
11
|
AltArrowRight as Next,
|
|
12
12
|
Play,
|
|
13
13
|
Pause,
|
|
14
|
-
RefreshCircle as Loader2
|
|
15
|
-
ClockCircle as Clock,
|
|
16
|
-
LinkRound as Webhook
|
|
14
|
+
RefreshCircle as Loader2
|
|
17
15
|
} from "#solar-icons";
|
|
18
16
|
import { Badge, Button } from "@revenexx/studio";
|
|
19
17
|
import { toast } from "vue-sonner";
|
|
@@ -26,6 +24,7 @@ import StickyNoteView from "./StickyNoteView.vue";
|
|
|
26
24
|
import GroupFrameView from "./GroupFrameView.vue";
|
|
27
25
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
28
26
|
import { findManifest } from "../../data/integrationNodeCatalog";
|
|
27
|
+
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
29
28
|
import { SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
30
29
|
import { runStatusLabel, statusClass } from "../../utils/statusStyle";
|
|
31
30
|
const props = defineProps({
|
|
@@ -408,7 +407,7 @@ function pretty(v) {
|
|
|
408
407
|
}
|
|
409
408
|
}
|
|
410
409
|
function triggerIcon(type) {
|
|
411
|
-
return type
|
|
410
|
+
return triggerGlyph(type);
|
|
412
411
|
}
|
|
413
412
|
const activeStepTitle = computed(() => {
|
|
414
413
|
const s = activeStep.value;
|
|
@@ -4,33 +4,32 @@ export interface ExecutionModeOption {
|
|
|
4
4
|
label: string;
|
|
5
5
|
hint: string;
|
|
6
6
|
}
|
|
7
|
+
/** What this dialog edits — the values as they stand, saved or not. */
|
|
8
|
+
export interface WorkflowSettingsValues {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
tags: string[];
|
|
12
|
+
icon: WorkflowIcon | null;
|
|
13
|
+
executionMode: string;
|
|
14
|
+
}
|
|
7
15
|
type __VLS_Props = {
|
|
8
16
|
open: boolean;
|
|
9
17
|
workflow: Workflow;
|
|
10
18
|
modes: ExecutionModeOption[];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The values to edit: the editor's unsaved ones where it has any, the saved ones
|
|
21
|
+
* otherwise. Reopening the dialog therefore shows what the operator last entered
|
|
22
|
+
* rather than what the platform still holds.
|
|
23
|
+
*/
|
|
24
|
+
current: WorkflowSettingsValues;
|
|
14
25
|
/** Tag suggestions (tags already used across the tenant's workflows). */
|
|
15
26
|
tagSuggestions?: string[];
|
|
16
27
|
};
|
|
17
28
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
29
|
"update:open": (args_0: boolean) => any;
|
|
19
|
-
|
|
20
|
-
"save-meta": (meta: {
|
|
21
|
-
name: string;
|
|
22
|
-
description: string;
|
|
23
|
-
tags: string[];
|
|
24
|
-
icon: WorkflowIcon | null;
|
|
25
|
-
}) => any;
|
|
30
|
+
apply: (values: WorkflowSettingsValues) => any;
|
|
26
31
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
27
32
|
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
28
|
-
|
|
29
|
-
"onSave-meta"?: ((meta: {
|
|
30
|
-
name: string;
|
|
31
|
-
description: string;
|
|
32
|
-
tags: string[];
|
|
33
|
-
icon: WorkflowIcon | null;
|
|
34
|
-
}) => any) | undefined;
|
|
33
|
+
onApply?: ((values: WorkflowSettingsValues) => any) | undefined;
|
|
35
34
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
36
35
|
export default _default;
|