@revenexx/studio-integrations 2.8.0 → 2.9.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 +28 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/ExpressionInput.vue +31 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +44 -11
- package/dist/runtime/components/Integrations/NodeConfigField.vue +5 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +72 -64
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +11 -5
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +2 -2
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +147 -27
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +36 -12
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +162 -13
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +34 -12
- package/dist/runtime/components/Integrations/WorkflowTriggerView.d.vue.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +3 -1
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue.d.ts +8 -1
- package/dist/runtime/components/Integrations/canvasSides.d.ts +27 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +4 -1
- package/dist/runtime/composables/useIntegrationsCredentials.js +8 -3
- package/dist/runtime/data/canvasShapes.d.ts +139 -0
- package/dist/runtime/data/canvasShapes.js +130 -0
- package/dist/runtime/data/connectionRules.d.ts +110 -0
- package/dist/runtime/data/connectionRules.js +110 -0
- package/dist/runtime/data/graphShapes.d.ts +175 -0
- package/dist/runtime/data/graphShapes.js +359 -0
- package/dist/runtime/data/handleId.d.ts +12 -0
- package/dist/runtime/data/handleId.js +3 -0
- package/dist/runtime/data/refusalMessage.d.ts +32 -0
- package/dist/runtime/data/refusalMessage.js +42 -0
- package/dist/runtime/data/saveRefusal.d.ts +31 -0
- package/dist/runtime/data/saveRefusal.js +25 -0
- package/dist/runtime/i18n/messages/de.json +36 -1
- package/dist/runtime/i18n/messages/en.json +36 -1
- package/dist/runtime/pages/integrations/schedules.vue +1 -1
- package/dist/runtime/pages/integrations/workflows/[id].vue +38 -3
- package/dist/runtime/types/integrations.d.ts +13 -4
- package/dist/runtime/utils/skippedStep.d.ts +39 -0
- package/dist/runtime/utils/skippedStep.js +22 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 2.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f849488: The `{ }` mapping list has a RUN section: the run's ID, the link to the run's page, its workflow's ID and its workflow's name, so a ticket or message a workflow sends can lead back to the run it came from. A value mapped from one of them reads as what it is ("Run ID", "Run link", "Workflow name") rather than as its expression (PO-565).
|
|
8
|
+
- 4df13c7: A step skipped because a branch delivered nothing now says so in run replay and in the run's step list: "Skipped: dead end in branch 2". On the replay canvas the step is drawn as skipped rather than as one that ran, and no taken path leads into it. Any other skip reason reads as a plain "Skipped" (PO-750, with PO-502 reporting the reason).
|
|
9
|
+
- 4df13c7: The canvas shows where a workflow splits and where its branches meet (PO-750). An output with more than one connection leaving it is drawn as a box — a splitter, every branch runs — and an input that gathers the branches of one splitter is a box too — a merger, it waits for every branch. A split inside a split that meets again in one step shows each level without a step of its own as a small box in front of that step's input; it is drawn only, never saved and never run. The editor, the run replay and the version history all draw the same. While a connection is drawn, the editor now refuses the two that could never become valid — one crossing a loop's boundary, and one leading back to a step it comes from — and says why; everything else is still left to the save.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- d5898c2: A workflow name cut short on the Schedules page shows itself whole on hover (PO-614).
|
|
14
|
+
- 7ead0a0: A node's credential picker reads the tenant's credentials again each time it opens, and the template install dialog each time it opens. Before, the list was read once per page, so a credential created in another tab stayed missing until the page was reloaded. A re-read that fails keeps the list that was there and reports nothing (PO-653).
|
|
15
|
+
- ece71b7: A multi-line setting that maps values takes a click anywhere in its box. Before, only its first line did: a click on the empty lines below focused nothing, so what was typed went nowhere and a value picked from the data panel landed in the field used before (PO-689).
|
|
16
|
+
- ece00b1: A palette search lists the node named exactly what was typed first. Before, hits came in folder order only, so searching "E-Mail senden" put two Brevo nodes above the node of that name (PO-693).
|
|
17
|
+
- 4df13c7: A change to a workflow's connections that gives it a shape the save will refuse — drawing one, moving an end, inserting a node into one, duplicating or pasting steps, deleting a connection or a step — is still made, but is now said at once: a warning names the step the save will refuse and, where one branch of a split crosses into another, the connection from which output of which step into which step. That step's input is drawn red with the same words as its title, and the crossing connection red and dashed, until the shape is put right (PO-746).
|
|
18
|
+
- 4df13c7: A save refused for how a workflow's branches come back together no longer offers to keep the work as paused — the shape is checked whatever the workflow's state, so that save failed as well. A dialog says the workflow is invalid and cannot be saved like this, and lists each error in plain words in the studio's language — which connection or step, by its name, and what to do about it — instead of the service's English text with step ids. A paused workflow gets the same dialog instead of the raw text in the toolbar (PO-746). The warning on the canvas — said as a connection is drawn, and on the refused input — now uses the same plain words.
|
|
19
|
+
- 4df13c7: The canvas's branch analysis now knows that a step which ends the run, such as Stop and Error, is never the end of a loop's body — the same judgement the save check makes (PO-749). Nothing drawn changes today.
|
|
20
|
+
- 4df13c7: Hovering a merger, a converging input or a level drawn in front of one now names every branch arriving there on a line of its own — the step it ends in, the output it leaves by, and the expression the receiving step reads it with: `${{ in.<step> }}` for a branch gathered with others, `${{ in }}` for the one alternative that arrived (PO-750).
|
|
21
|
+
- 4df13c7: A connection the editor refuses while it is drawn is now explained in a toast with a title and a detail that names the steps involved — "Connection not possible", then which step already runs after which, or which step is inside which loop and which of the loop's outputs may lead in or out (PO-750). Copies that came without some of their connections say so the same way.
|
|
22
|
+
- 4df13c7: Review fixes on the branch rules in the editor (PO-750): a connection drawn from a loop's own Item output to a step reached from outside the loop is explained as such; a new cycle among steps that already sit on another cycle is refused while drawing; a skipped step is drawn as skipped in the replay whatever case the platform writes its status in, and its badge reads "skipped" / "übersprungen" rather than the raw status; checking a connection while it is dragged no longer re-analyses the whole workflow on every pointer move.
|
|
23
|
+
- 560ac24: A node card's output names no longer print over each other. Where the names of a node's outputs would collide evenly spaced — three outputs, or long names, as on the OpenPLZ node in German — the dots are spaced by the names' widths, each under its own name, and the card widens only where even that does not fit. Cards whose names already fit are unchanged. A group's frame follows a card that widened, in the editor, the version history preview and the run replay.
|
|
24
|
+
|
|
25
|
+
## 2.8.1
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- 46507f8: Escape in an open option list closes the list and stops there. In a node's dialog, the searchable list of a setting whose options come from the platform let Escape through to the dialog, which took it as Cancel and threw away every setting entered in it (PO-655). Escape with no list open still cancels the dialog.
|
|
30
|
+
|
|
3
31
|
## 2.8.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
package/dist/module.json
CHANGED
|
@@ -30,6 +30,12 @@ function tok(inner) {
|
|
|
30
30
|
}
|
|
31
31
|
const triggerLabel = "{ }";
|
|
32
32
|
const nodeNames = inject("integrationsNodeNames", null);
|
|
33
|
+
const RUN_IDENTITY = [
|
|
34
|
+
{ expr: "run.id", pill: () => t("integration.nodeConfig.expression.pill.runId"), suggestion: () => t("integration.nodeConfig.expression.suggestion.runId"), kind: "number" },
|
|
35
|
+
{ expr: "run.url", pill: () => t("integration.nodeConfig.expression.pill.runUrl"), suggestion: () => t("integration.nodeConfig.expression.suggestion.runUrl"), kind: "string" },
|
|
36
|
+
{ expr: "workflow.id", pill: () => t("integration.nodeConfig.expression.pill.workflowId"), suggestion: () => t("integration.nodeConfig.expression.suggestion.workflowId"), kind: "number" },
|
|
37
|
+
{ expr: "workflow.name", pill: () => t("integration.nodeConfig.expression.pill.workflowName"), suggestion: () => t("integration.nodeConfig.expression.suggestion.workflowName"), kind: "string" }
|
|
38
|
+
];
|
|
33
39
|
function pillLabel(expr) {
|
|
34
40
|
const path = expr.split(".");
|
|
35
41
|
if (path[0] === "nodes" && path[2] === "outputs") {
|
|
@@ -42,6 +48,8 @@ function pillLabel(expr) {
|
|
|
42
48
|
return rest ? t("integration.nodeConfig.expression.pill.triggerPath", { rest }) : t("integration.nodeConfig.expression.pill.triggerPayload");
|
|
43
49
|
}
|
|
44
50
|
if (path[0] === "vars") return t("integration.nodeConfig.expression.pill.variable", { path: path.slice(1).join(".") });
|
|
51
|
+
const identity = RUN_IDENTITY.find((r) => r.expr === expr);
|
|
52
|
+
if (identity) return identity.pill();
|
|
45
53
|
if (path[0] === "in") {
|
|
46
54
|
const rest = path.slice(1).join(".");
|
|
47
55
|
return rest ? t("integration.nodeConfig.expression.pill.inputPath", { rest }) : t("integration.nodeConfig.expression.pill.inputPayload");
|
|
@@ -255,6 +263,10 @@ const sections = computed(() => {
|
|
|
255
263
|
inItems.push({ label: "input." + f, expr: tok("in." + f), kind: primary?.fieldTypes?.[f] ?? null });
|
|
256
264
|
}
|
|
257
265
|
out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.input"), items: inItems });
|
|
266
|
+
out.push({
|
|
267
|
+
heading: t("integration.nodeConfig.expression.suggestion.heading.run"),
|
|
268
|
+
items: RUN_IDENTITY.map((r) => ({ label: r.suggestion(), expr: tok(r.expr), kind: r.kind }))
|
|
269
|
+
});
|
|
258
270
|
return out;
|
|
259
271
|
});
|
|
260
272
|
const hasSuggestions = computed(() => sections.value.some((s) => s.items.length > 0));
|
|
@@ -285,6 +297,24 @@ function toggle() {
|
|
|
285
297
|
function onTriggerClick(e) {
|
|
286
298
|
if (e.detail === 0) toggle();
|
|
287
299
|
}
|
|
300
|
+
function onBoxMousedown(e) {
|
|
301
|
+
if (!props.multiline || e.button !== 0 || e.shiftKey) return;
|
|
302
|
+
if (e.target.closest(".ProseMirror")) return;
|
|
303
|
+
const box = e.currentTarget;
|
|
304
|
+
const outer = box.getBoundingClientRect();
|
|
305
|
+
if (e.clientX - outer.left - box.clientLeft >= box.clientWidth || e.clientY - outer.top - box.clientTop >= box.clientHeight) return;
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
const region = editor.view.dom.getBoundingClientRect();
|
|
308
|
+
if (e.clientY >= region.bottom) {
|
|
309
|
+
editor.commands.focus("end", { scrollIntoView: false });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const hit = editor.view.posAtCoords({
|
|
313
|
+
left: Math.min(Math.max(e.clientX, region.left + 1), region.right - 1),
|
|
314
|
+
top: Math.min(Math.max(e.clientY, region.top + 1), region.bottom - 1)
|
|
315
|
+
});
|
|
316
|
+
editor.commands.focus(hit ? hit.pos : "end", { scrollIntoView: false });
|
|
317
|
+
}
|
|
288
318
|
function onFitClick(e) {
|
|
289
319
|
if (e.detail === 0 && singleFit.value) insertToken(singleFit.value.expr);
|
|
290
320
|
}
|
|
@@ -333,6 +363,7 @@ function insert(expr) {
|
|
|
333
363
|
multiline ? 'expr-multiline' : 'h-8 overflow-hidden',
|
|
334
364
|
invalid ? 'border-rose-400' : 'border-black/[0.08]'
|
|
335
365
|
]"
|
|
366
|
+
@mousedown="onBoxMousedown"
|
|
336
367
|
>
|
|
337
368
|
<span
|
|
338
369
|
v-if="!text"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, ref } from "vue";
|
|
2
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
3
3
|
import { isVersionExecutable } from "../../data/integrationNodeCatalog";
|
|
4
4
|
import { ChevronDown } from "lucide-vue-next";
|
|
5
5
|
import { Input } from "@revenexx/studio";
|
|
@@ -33,14 +33,44 @@ const options = computed(() => q.data.value?.options ?? []);
|
|
|
33
33
|
const isMulti = computed(() => props.field.type === "multiselect");
|
|
34
34
|
const disabledControlClass = "flex h-8 items-center rounded-md border border-black/[0.08] bg-white px-2 text-sm text-muted-foreground opacity-50";
|
|
35
35
|
const currentValue = computed(() => String(props.modelValue ?? ""));
|
|
36
|
+
const seenLabels = ref(/* @__PURE__ */ new Map());
|
|
37
|
+
watch(options, (list) => {
|
|
38
|
+
for (const o of list) seenLabels.value.set(String(o.value), resolveLocalized(o.label) || String(o.value));
|
|
39
|
+
}, { immediate: true });
|
|
36
40
|
const currentKnown = computed(
|
|
37
|
-
() => options.value.some((o) => String(o.value) === currentValue.value)
|
|
41
|
+
() => options.value.some((o) => String(o.value) === currentValue.value) || seenLabels.value.has(currentValue.value)
|
|
38
42
|
);
|
|
39
|
-
const currentLabel = computed(() =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const currentLabel = computed(() => seenLabels.value.get(currentValue.value) ?? currentValue.value);
|
|
44
|
+
const listDismissed = ref(false);
|
|
45
|
+
const listOpen = computed(() => focused.value && !q.gated.value && !listDismissed.value);
|
|
46
|
+
const escapeHeld = ref(false);
|
|
47
|
+
function onComboEscape(event) {
|
|
48
|
+
if (event.isComposing) return;
|
|
49
|
+
const claim = listOpen.value || escapeHeld.value && event.repeat;
|
|
50
|
+
if (!claim) return;
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
if (!listOpen.value) return;
|
|
54
|
+
escapeHeld.value = true;
|
|
55
|
+
listDismissed.value = true;
|
|
56
|
+
const input = event.target;
|
|
57
|
+
void nextTick(() => input.select());
|
|
58
|
+
}
|
|
59
|
+
function onComboEscapeUp() {
|
|
60
|
+
escapeHeld.value = false;
|
|
61
|
+
}
|
|
62
|
+
function onComboArrowDown(event) {
|
|
63
|
+
if (!listDismissed.value) return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
listDismissed.value = false;
|
|
66
|
+
}
|
|
67
|
+
function onComboSearch(v) {
|
|
68
|
+
search.value = String(v);
|
|
69
|
+
listDismissed.value = false;
|
|
70
|
+
}
|
|
71
|
+
function onComboClick() {
|
|
72
|
+
listDismissed.value = false;
|
|
73
|
+
}
|
|
44
74
|
const visibleOptions = computed(() => {
|
|
45
75
|
const term = search.value.trim().toLowerCase();
|
|
46
76
|
if (!focused.value || !term) return options.value;
|
|
@@ -51,6 +81,7 @@ const visibleOptions = computed(() => {
|
|
|
51
81
|
});
|
|
52
82
|
function onComboFocus() {
|
|
53
83
|
search.value = "";
|
|
84
|
+
listDismissed.value = false;
|
|
54
85
|
focused.value = true;
|
|
55
86
|
}
|
|
56
87
|
function onComboBlur() {
|
|
@@ -155,14 +186,16 @@ function toggleMulti(optValue) {
|
|
|
155
186
|
<template v-else>
|
|
156
187
|
<div class="relative">
|
|
157
188
|
<Input
|
|
158
|
-
:model-value="
|
|
189
|
+
:model-value="listOpen ? search : currentLabel"
|
|
159
190
|
:aria-label="resolveLocalized(field.label)"
|
|
160
191
|
:placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')"
|
|
161
192
|
class="h-8 bg-white pr-7 text-sm"
|
|
162
|
-
@update:model-value="
|
|
163
|
-
search = String(v);
|
|
164
|
-
}"
|
|
193
|
+
@update:model-value="onComboSearch"
|
|
165
194
|
@focus="onComboFocus"
|
|
195
|
+
@click="onComboClick"
|
|
196
|
+
@keydown.esc="onComboEscape"
|
|
197
|
+
@keyup.esc="onComboEscapeUp"
|
|
198
|
+
@keydown.down="onComboArrowDown"
|
|
166
199
|
@blur="onComboBlur"
|
|
167
200
|
/>
|
|
168
201
|
<ChevronDown :size="14" class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
@@ -106,10 +106,13 @@ const constraintHint = computed(() => {
|
|
|
106
106
|
}
|
|
107
107
|
return "";
|
|
108
108
|
});
|
|
109
|
-
const { credentials, loaded: credentialsLoaded, load: loadCredentials } = useIntegrationsCredentials();
|
|
109
|
+
const { credentials, loaded: credentialsLoaded, load: loadCredentials, refresh: refreshCredentials } = useIntegrationsCredentials();
|
|
110
110
|
onMounted(() => {
|
|
111
111
|
void loadCredentials();
|
|
112
112
|
});
|
|
113
|
+
function onCredentialPickerOpen(open) {
|
|
114
|
+
if (open) void refreshCredentials();
|
|
115
|
+
}
|
|
113
116
|
function credsOfType(slug) {
|
|
114
117
|
if (!slug) return credentials.value;
|
|
115
118
|
const slugs = (Array.isArray(slug) ? slug : [slug]).map((s) => s.trim()).filter(Boolean);
|
|
@@ -626,6 +629,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
626
629
|
<Select
|
|
627
630
|
:model-value="value ?? ''"
|
|
628
631
|
@update:model-value="(v) => onCredentialSelect(v)"
|
|
632
|
+
@update:open="onCredentialPickerOpen"
|
|
629
633
|
>
|
|
630
634
|
<SelectTrigger :aria-label="credTriggerLabel" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
631
635
|
<!-- The trigger renders the bound credential itself instead of letting
|
|
@@ -211,18 +211,22 @@ function popLevel() {
|
|
|
211
211
|
search.value = "";
|
|
212
212
|
}
|
|
213
213
|
const levelKey = computed(() => stack.value.length);
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
const normalised = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
215
|
+
const SEARCH_CAP = 50;
|
|
216
|
+
const searchResult = computed(() => {
|
|
217
|
+
const q = normalised(search.value);
|
|
218
|
+
const exact = [];
|
|
219
|
+
const partial = [];
|
|
220
|
+
if (!q || triggersLevel.value) return { exact, partial };
|
|
217
221
|
const scope = currentGroup.value ? inBrowseOrder(currentGroup.value) : rootBrowseOrder.value;
|
|
218
|
-
const hits = [];
|
|
219
222
|
for (const m of scope) {
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
222
|
-
if (
|
|
223
|
+
const name = normalised(resolveLocalized(m.name));
|
|
224
|
+
if (name === q) exact.push({ manifest: m, path: groupPath(m) });
|
|
225
|
+
else if (exact.length + partial.length < SEARCH_CAP && `${name} ${m.slug} ${resolveLocalized(m.description).toLowerCase()}`.includes(q)) partial.push({ manifest: m, path: groupPath(m) });
|
|
223
226
|
}
|
|
224
|
-
return
|
|
227
|
+
return { exact, partial: partial.slice(0, Math.max(0, SEARCH_CAP - exact.length)) };
|
|
225
228
|
});
|
|
229
|
+
const searchHits = computed(() => [...searchResult.value.exact, ...searchResult.value.partial]);
|
|
226
230
|
const matchedTriggers = computed(() => {
|
|
227
231
|
const q = search.value.trim().toLowerCase();
|
|
228
232
|
if (!q || currentGroup.value) return [];
|
|
@@ -353,67 +357,71 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
353
357
|
<div :key="`${levelKey}-${searching}`" class="absolute inset-0 overflow-y-auto overscroll-contain p-2 [scrollbar-color:rgb(0_0_0/0.22)_transparent] [scrollbar-width:thin]">
|
|
354
358
|
<!-- ── Search results (flat, path as subtitle) ─────────────────── -->
|
|
355
359
|
<template v-if="searching">
|
|
356
|
-
<div v-if="matchedTriggers.length" class="mb-1">
|
|
357
|
-
<div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.triggers") }}</div>
|
|
358
|
-
<div
|
|
359
|
-
v-for="tpl in matchedTriggers"
|
|
360
|
-
:key="tpl.id"
|
|
361
|
-
class="group flex cursor-grab items-center gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
|
|
362
|
-
draggable="true"
|
|
363
|
-
@dragstart="onTriggerDragStart(tpl, $event)"
|
|
364
|
-
@dragend="onDragEnd"
|
|
365
|
-
@click="insertTrigger(tpl)"
|
|
366
|
-
>
|
|
367
|
-
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="triggerChipClass">
|
|
368
|
-
<component :is="triggerGlyph(tpl.type)" :size="13" />
|
|
369
|
-
</span>
|
|
370
|
-
<div class="min-w-0">
|
|
371
|
-
<div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
|
|
372
|
-
<div class="text-xs leading-snug text-muted-foreground">{{ t("integration.canvas.badge.trigger") }}</div>
|
|
373
|
-
</div>
|
|
374
|
-
</div>
|
|
375
|
-
</div>
|
|
376
|
-
|
|
377
360
|
<p v-if="!searchHits.length && !matchedTriggers.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
|
|
378
361
|
{{ t("integration.canvas.palette.search.empty", { query: search }) }}
|
|
379
362
|
</p>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
<
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
:aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(hit.manifest.name) })"
|
|
401
|
-
@click.stop
|
|
402
|
-
@mousedown.stop
|
|
403
|
-
@change.stop="setSelectedVersion(hit.manifest.slug, $event.target.value)"
|
|
404
|
-
@mouseenter="paletteDragEnabled = false"
|
|
405
|
-
@mouseleave="onVersionSelectLeave"
|
|
406
|
-
@focus="paletteDragEnabled = false"
|
|
407
|
-
@blur="paletteDragEnabled = true"
|
|
408
|
-
>
|
|
409
|
-
<option v-for="v in versionsOf(hit.manifest.slug)" :key="v.version" :value="v.version">v{{ v.version }}</option>
|
|
410
|
-
</select>
|
|
411
|
-
<span v-else class="shrink-0 text-xs tabular-nums text-muted-foreground/60">v{{ hit.manifest.version }}</span>
|
|
363
|
+
<!-- A node named exactly what was typed stands above the triggers too (AC-13). -->
|
|
364
|
+
<template v-for="section in ['exact', 'triggers', 'partial']" :key="section">
|
|
365
|
+
<div v-if="section === 'triggers' && matchedTriggers.length" class="mb-1">
|
|
366
|
+
<div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.triggers") }}</div>
|
|
367
|
+
<div
|
|
368
|
+
v-for="tpl in matchedTriggers"
|
|
369
|
+
:key="tpl.id"
|
|
370
|
+
class="group flex cursor-grab items-center gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
|
|
371
|
+
draggable="true"
|
|
372
|
+
@dragstart="onTriggerDragStart(tpl, $event)"
|
|
373
|
+
@dragend="onDragEnd"
|
|
374
|
+
@click="insertTrigger(tpl)"
|
|
375
|
+
>
|
|
376
|
+
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="triggerChipClass">
|
|
377
|
+
<component :is="triggerGlyph(tpl.type)" :size="13" />
|
|
378
|
+
</span>
|
|
379
|
+
<div class="min-w-0">
|
|
380
|
+
<div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
|
|
381
|
+
<div class="text-xs leading-snug text-muted-foreground">{{ t("integration.canvas.badge.trigger") }}</div>
|
|
382
|
+
</div>
|
|
412
383
|
</div>
|
|
413
|
-
<div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(hit.manifest.description) }}</div>
|
|
414
|
-
<div class="truncate text-xs text-muted-foreground/70">{{ hit.path.join(" \u203A ") }}</div>
|
|
415
384
|
</div>
|
|
416
|
-
|
|
385
|
+
<template v-if="section !== 'triggers'">
|
|
386
|
+
<div
|
|
387
|
+
v-for="hit in section === 'exact' ? searchResult.exact : searchResult.partial"
|
|
388
|
+
:key="`${hit.manifest.slug}@${hit.manifest.version}`"
|
|
389
|
+
class="group flex cursor-grab items-start gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
|
|
390
|
+
:draggable="paletteDragEnabled"
|
|
391
|
+
:title="resolveLocalized(hit.manifest.name)"
|
|
392
|
+
@dragstart="onManifestDragStart(hit.manifest, $event)"
|
|
393
|
+
@dragend="onDragEnd"
|
|
394
|
+
@click="insertManifest(hit.manifest)"
|
|
395
|
+
>
|
|
396
|
+
<span class="flex size-8 shrink-0 items-center justify-center rounded-md" :class="nodeCategoryColor(hit.manifest)">
|
|
397
|
+
<IntegrationsNodeMark :manifest="hit.manifest" :size="18" :badge="false" />
|
|
398
|
+
</span>
|
|
399
|
+
<div class="min-w-0 flex-1">
|
|
400
|
+
<div class="flex items-center gap-1.5">
|
|
401
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ resolveLocalized(hit.manifest.name) }}</span>
|
|
402
|
+
<select
|
|
403
|
+
v-if="versionsOf(hit.manifest.slug).length > 1"
|
|
404
|
+
:value="effVersion(hit.manifest)"
|
|
405
|
+
class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
|
|
406
|
+
:aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(hit.manifest.name) })"
|
|
407
|
+
@click.stop
|
|
408
|
+
@mousedown.stop
|
|
409
|
+
@change.stop="setSelectedVersion(hit.manifest.slug, $event.target.value)"
|
|
410
|
+
@mouseenter="paletteDragEnabled = false"
|
|
411
|
+
@mouseleave="onVersionSelectLeave"
|
|
412
|
+
@focus="paletteDragEnabled = false"
|
|
413
|
+
@blur="paletteDragEnabled = true"
|
|
414
|
+
>
|
|
415
|
+
<option v-for="v in versionsOf(hit.manifest.slug)" :key="v.version" :value="v.version">v{{ v.version }}</option>
|
|
416
|
+
</select>
|
|
417
|
+
<span v-else class="shrink-0 text-xs tabular-nums text-muted-foreground/60">v{{ hit.manifest.version }}</span>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(hit.manifest.description) }}</div>
|
|
420
|
+
<div class="truncate text-xs text-muted-foreground/70">{{ hit.path.join(" \u203A ") }}</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</template>
|
|
424
|
+
</template>
|
|
417
425
|
</template>
|
|
418
426
|
|
|
419
427
|
<!-- ── Triggers level ──────────────────────────────────────────── -->
|
|
@@ -3,6 +3,7 @@ import { computed, onUnmounted, ref, watch } from "vue";
|
|
|
3
3
|
import { Download as DownloadIcon, RefreshCircle as RefreshIcon } from "#solar-icons";
|
|
4
4
|
import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
5
5
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
6
|
+
import { isSkippedStep, skipReasonMessage, stepStatusLabel } from "../../utils/skippedStep";
|
|
6
7
|
import { IntegrationsApiError, errorReason } from "../../utils/apiError";
|
|
7
8
|
import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
8
9
|
import { formatDuration } from "../../utils/formatDuration";
|
|
@@ -41,6 +42,10 @@ const details = ref(null);
|
|
|
41
42
|
const detailsLoading = ref(false);
|
|
42
43
|
const detailsError = ref(null);
|
|
43
44
|
const steps = ref([]);
|
|
45
|
+
const skipNotes = computed(() => steps.value.map((st) => {
|
|
46
|
+
const m = skipReasonMessage(st);
|
|
47
|
+
return m ? t(m.key, m.params) : "";
|
|
48
|
+
}));
|
|
44
49
|
const stepsLoading = ref(false);
|
|
45
50
|
const stepsError = ref(null);
|
|
46
51
|
const expandedStep = ref(null);
|
|
@@ -313,7 +318,7 @@ function stepKey(st, i) {
|
|
|
313
318
|
return `${st.node_id}-${st.scheduled_event_id ?? i}`;
|
|
314
319
|
}
|
|
315
320
|
function cachedPayload(st) {
|
|
316
|
-
return st.scheduled_event_id
|
|
321
|
+
return st.scheduled_event_id == null ? void 0 : payloadCache.value.get(st.scheduled_event_id);
|
|
317
322
|
}
|
|
318
323
|
function hasPayload(st) {
|
|
319
324
|
return cachedPayload(st) !== void 0;
|
|
@@ -362,7 +367,7 @@ async function loadPayloads(opened) {
|
|
|
362
367
|
if (props.run.id !== runId) return;
|
|
363
368
|
const next = new Map(payloadCache.value);
|
|
364
369
|
for (const row of res?.steps ?? []) {
|
|
365
|
-
if (row.scheduled_event_id
|
|
370
|
+
if (row.scheduled_event_id == null) continue;
|
|
366
371
|
next.set(row.scheduled_event_id, {
|
|
367
372
|
status: row.status,
|
|
368
373
|
input: row.input,
|
|
@@ -382,6 +387,7 @@ async function loadPayloads(opened) {
|
|
|
382
387
|
if (expandedStep.value !== readFor) void loadPayloads(true);
|
|
383
388
|
}
|
|
384
389
|
function toggleStep(st, i) {
|
|
390
|
+
if (isSkippedStep(st)) return;
|
|
385
391
|
const key = stepKey(st, i);
|
|
386
392
|
expandedStep.value = expandedStep.value === key ? null : key;
|
|
387
393
|
if (expandedStep.value !== null) void loadPayloads(true);
|
|
@@ -748,15 +754,15 @@ async function confirmAction() {
|
|
|
748
754
|
class="w-full flex items-center gap-3 px-3 py-2 text-left"
|
|
749
755
|
@click="toggleStep(st, i)"
|
|
750
756
|
>
|
|
751
|
-
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', runStatusClass(st.status)]">{{
|
|
757
|
+
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', runStatusClass(st.status)]">{{ stepStatusLabel(st.status) }}</Badge>
|
|
752
758
|
<div class="min-w-0 flex-1">
|
|
753
759
|
<div class="font-mono text-xs truncate">{{ st.node_slug || st.node_id }}</div>
|
|
754
760
|
<div class="text-[10px] text-muted-foreground truncate">
|
|
755
|
-
{{ st.node_id }}<template v-if="st.branch"> · {{ t("integration.runs.detail.steps.branchLabel") }} {{ st.branch }}</template><template v-if="st.attempt"> · {{ t("integration.runs.detail.steps.attemptLabel") }} {{ st.attempt + 1 }}</template>
|
|
761
|
+
{{ st.node_id }}<template v-if="st.branch"> · {{ t("integration.runs.detail.steps.branchLabel") }} {{ st.branch }}</template><template v-if="skipNotes[i]"> · {{ skipNotes[i] }}</template><template v-if="st.attempt"> · {{ t("integration.runs.detail.steps.attemptLabel") }} {{ st.attempt + 1 }}</template>
|
|
756
762
|
</div>
|
|
757
763
|
</div>
|
|
758
764
|
<span class="text-xs text-muted-foreground tabular-nums shrink-0">{{ fmtMs(st.duration_ms) }}</span>
|
|
759
|
-
<span class="text-[10px] text-muted-foreground shrink-0">{{ expandedStep === stepKey(st, i) ? "\u25BC" : "\u25B6" }}</span>
|
|
765
|
+
<span v-if="!isSkippedStep(st)" class="text-[10px] text-muted-foreground shrink-0">{{ expandedStep === stepKey(st, i) ? "\u25BC" : "\u25B6" }}</span>
|
|
760
766
|
</button>
|
|
761
767
|
<div v-if="st.error" class="px-3 pb-2 -mt-1">
|
|
762
768
|
<div class="rounded-md border border-rose-500/20 bg-rose-500/5 px-2 py-1 text-xs text-rose-700 break-words">{{ st.error }}</div>
|
|
@@ -22,7 +22,7 @@ const emit = defineEmits(["installed", "update:loading", "update:installing"]);
|
|
|
22
22
|
const t = useIntegrationsT();
|
|
23
23
|
const { get, load, fetchDetail, requirements, instantiate } = useIntegrationsTemplates();
|
|
24
24
|
const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
|
|
25
|
-
const { ofType,
|
|
25
|
+
const { ofType, refresh: refreshCredentials } = useIntegrationsCredentials();
|
|
26
26
|
const reqs = ref(null);
|
|
27
27
|
const loading = ref(true);
|
|
28
28
|
const installing = ref(false);
|
|
@@ -40,7 +40,7 @@ watch(() => props.slug, async (slug) => {
|
|
|
40
40
|
await load();
|
|
41
41
|
if (mine !== run) return;
|
|
42
42
|
if (name.value === "") name.value = get(slug)?.name ?? "";
|
|
43
|
-
await Promise.all([fetchDetail(slug),
|
|
43
|
+
await Promise.all([fetchDetail(slug), refreshCredentials(), loadSecrets()]);
|
|
44
44
|
if (mine !== run) return;
|
|
45
45
|
const answer = await requirements(slug);
|
|
46
46
|
if (mine !== run) return;
|
|
@@ -42,6 +42,12 @@ interface Data {
|
|
|
42
42
|
replay?: {
|
|
43
43
|
ahead: boolean;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Where one branch of a split crosses into another, so the save will refuse
|
|
47
|
+
* the step the branches meet at (decided 19 Oct 2026): drawn red and dashed
|
|
48
|
+
* as the connection to change. The replay's colour wins over it.
|
|
49
|
+
*/
|
|
50
|
+
crossing?: boolean;
|
|
45
51
|
}
|
|
46
52
|
type __VLS_Props = EdgeProps<Data>;
|
|
47
53
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -67,6 +67,7 @@ const path = computed(() => getSmoothStepPath({
|
|
|
67
67
|
centerY: centerY.value
|
|
68
68
|
}));
|
|
69
69
|
const NEUTRAL = { stroke: "#94a3b8", label: "#475569", bg: "#f1f5f9" };
|
|
70
|
+
const CROSSING_STROKE = "#e11d48";
|
|
70
71
|
const MUTED = { stroke: "#cbd5e1", label: "#94a3b8", bg: "#f8fafc" };
|
|
71
72
|
const palette = {
|
|
72
73
|
default: NEUTRAL,
|
|
@@ -116,8 +117,9 @@ function spliceNode() {
|
|
|
116
117
|
:path="path[0]"
|
|
117
118
|
:marker-end="props.markerEnd"
|
|
118
119
|
:style="{
|
|
119
|
-
stroke: props.data?.replay ? '#3b82f6' : color.stroke,
|
|
120
|
-
strokeWidth: props.data?.replay ? 2.5 : isSelected || hovered ? 2.5 : 1.75,
|
|
120
|
+
stroke: props.data?.replay ? '#3b82f6' : props.data?.crossing ? CROSSING_STROKE : color.stroke,
|
|
121
|
+
strokeWidth: props.data?.replay ? 2.5 : isSelected || hovered || props.data?.crossing ? 2.5 : 1.75,
|
|
122
|
+
...props.data?.crossing && !props.data?.replay ? { strokeDasharray: '6 4' } : {},
|
|
121
123
|
transition: 'stroke-width 120ms ease-out'
|
|
122
124
|
}"
|
|
123
125
|
/>
|
|
@@ -42,6 +42,12 @@ interface Data {
|
|
|
42
42
|
replay?: {
|
|
43
43
|
ahead: boolean;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Where one branch of a split crosses into another, so the save will refuse
|
|
47
|
+
* the step the branches meet at (decided 19 Oct 2026): drawn red and dashed
|
|
48
|
+
* as the connection to change. The replay's colour wins over it.
|
|
49
|
+
*/
|
|
50
|
+
crossing?: boolean;
|
|
45
51
|
}
|
|
46
52
|
type __VLS_Props = EdgeProps<Data>;
|
|
47
53
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|