@revenexx/studio-integrations 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +43 -6
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +8 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +7 -1
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue +27 -4
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +8 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +14 -11
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +26 -9
- package/dist/runtime/composables/useIntegrationsNodes.js +3 -2
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +3 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +4 -3
- package/dist/runtime/data/integrationNodeCatalog.js +2 -2
- package/dist/runtime/data/nodeIcons.d.ts +7 -1
- package/dist/runtime/data/nodeIcons.js +6 -3
- package/dist/runtime/data/nodeInputData.d.ts +6 -0
- package/dist/runtime/data/nodeInputData.js +14 -5
- package/dist/runtime/i18n/messages/de.json +13 -0
- package/dist/runtime/i18n/messages/en.json +13 -0
- package/dist/runtime/pages/integrations/workflows/[id].vue +99 -39
- package/dist/runtime/types/integrations.d.ts +10 -1
- package/dist/runtime/utils/folderMark.d.ts +20 -0
- package/dist/runtime/utils/folderMark.js +12 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +9 -1
- package/dist/runtime/utils/runFeedQuery.js +14 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 2.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e589a22: A workflow's own Runs tab can be ordered again by the Started and Duration columns, across the workflow's whole history rather than the page in hand, and a new order starts at the first page. The list is no longer re-sorted by start in the browser after it arrives (PO-385).
|
|
8
|
+
- 6ced93a: The `{ }` mapping list says what each earlier output is (list, number, …). A setting that declares `accepts` gets the outputs of that kind first and marked, and an empty one is offered the single fitting output in one click. A required setting the last save refused is named by its label ("Elemente fehlt noch.") instead of the platform's raw sentence (PO-594).
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 962f50d: Saving a workflow says "Saved" in the header once the platform has taken it. The chip beside Save that reports the build waits three seconds before it appears, so a build that finishes in normal time is no longer announced step by step and then dropped without a word (PO-597).
|
|
13
|
+
- 14dfdf1: A run the platform refuses is headed "Run not started" instead of "Run started — Started in the background" above the reason it did not start, and that reason is what the dialog announces as its description. A refusal is a 4xx answer with no run in it; an answer that carries a run keeps its heading and its way into the run. A run that does not wait and ends in a server error or no answer at all is headed "Start not confirmed", since a run may or may not exist. While the platform has not answered, the dialog is headed "Starting run" or "Run in progress" rather than as if it had. A run that started and then failed or timed out keeps its heading. The dialog's subtitle is set in the ordinary typeface rather than monospace (PO-613).
|
|
14
|
+
- 5c7c2bd: The trigger choice in "New workflow" addresses the author as "Sie", like the rest of the studio: "Starten Sie den Workflow selbst …" and "… nach Ihrem Zeitplan …". The German-copy test now also reads the `de:` texts written straight into the source, where these two had slipped past it (PO-616).
|
|
15
|
+
|
|
16
|
+
## 2.5.1
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 2ce4664: "Use revenexx Mail Servers" copies the login the mail server accepts — the sender's name and key — instead of an empty one, and a sender without a secret copies nothing, clears what an earlier pick copied, and says so. A sender created from the panel is copied the same way, and one created without a secret is no longer also reported as created (PO-602, PO-603).
|
|
21
|
+
|
|
3
22
|
## 2.5.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/dist/module.json
CHANGED
|
@@ -4,6 +4,12 @@ type UpstreamOutput = {
|
|
|
4
4
|
nodeName?: string;
|
|
5
5
|
output: string;
|
|
6
6
|
fields?: string[];
|
|
7
|
+
/** The port's declared data type, and each field's — what an entry IS (PO-594). */
|
|
8
|
+
dataType?: string | null;
|
|
9
|
+
fieldTypes?: Record<string, string | null>;
|
|
10
|
+
/** What the port and each field are called, resolved; the key stands in where absent. */
|
|
11
|
+
portLabel?: string | null;
|
|
12
|
+
fieldLabels?: Record<string, string | null>;
|
|
7
13
|
};
|
|
8
14
|
type __VLS_Props = {
|
|
9
15
|
modelValue: unknown;
|
|
@@ -12,6 +18,12 @@ type __VLS_Props = {
|
|
|
12
18
|
upstreamOutputs?: UpstreamOutput[];
|
|
13
19
|
/** Draw the editor as at fault — the field above owns the reason. */
|
|
14
20
|
invalid?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* What a mapped value has to be, as the setting declares it (`array` for a list).
|
|
23
|
+
* The outputs of that kind are listed first and marked, and an empty setting with
|
|
24
|
+
* exactly one of them below it offers that one in a click (PO-594).
|
|
25
|
+
*/
|
|
26
|
+
accepts?: string;
|
|
15
27
|
/** The setting's label, so the editable region has an accessible name. */
|
|
16
28
|
label?: string;
|
|
17
29
|
/** The setting takes several lines: Enter opens one instead of being
|
|
@@ -16,6 +16,7 @@ const props = defineProps({
|
|
|
16
16
|
variables: { type: Object, required: false },
|
|
17
17
|
upstreamOutputs: { type: Array, required: false },
|
|
18
18
|
invalid: { type: Boolean, required: false },
|
|
19
|
+
accepts: { type: String, required: false },
|
|
19
20
|
label: { type: String, required: false },
|
|
20
21
|
multiline: { type: Boolean, required: false }
|
|
21
22
|
});
|
|
@@ -221,6 +222,7 @@ const preview = computed(() => {
|
|
|
221
222
|
if (!sampleContext) return null;
|
|
222
223
|
return resolveExpressionText(text.value, sampleContext.value);
|
|
223
224
|
});
|
|
225
|
+
const wantedKind = computed(() => props.accepts && props.accepts !== "any" ? props.accepts : null);
|
|
224
226
|
const sections = computed(() => {
|
|
225
227
|
const out = [];
|
|
226
228
|
const vars = props.variables ?? {};
|
|
@@ -230,27 +232,41 @@ const sections = computed(() => {
|
|
|
230
232
|
}));
|
|
231
233
|
if (varItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.variables"), items: varItems });
|
|
232
234
|
const upstream = props.upstreamOutputs ?? [];
|
|
235
|
+
const fits = (kind) => !!wantedKind.value && kind === wantedKind.value;
|
|
233
236
|
const nodeItems = upstream.flatMap((u) => {
|
|
234
237
|
const base = "nodes." + u.nodeId + ".outputs." + u.output;
|
|
235
238
|
const rows = [{
|
|
236
|
-
label: (u.nodeName || u.nodeId) + " \u2192 " + u.output,
|
|
237
|
-
expr: tok(base)
|
|
239
|
+
label: (u.nodeName || u.nodeId) + " \u2192 " + (u.portLabel || u.output),
|
|
240
|
+
expr: tok(base),
|
|
241
|
+
kind: u.dataType ?? null,
|
|
242
|
+
fits: fits(u.dataType)
|
|
238
243
|
}];
|
|
239
244
|
for (const f of u.fields ?? []) {
|
|
240
|
-
|
|
245
|
+
const kind = u.fieldTypes?.[f] ?? null;
|
|
246
|
+
rows.push({ label: (u.nodeName || u.nodeId) + " \u2192 " + (u.portLabel || u.output) + " \u2192 " + (u.fieldLabels?.[f] || f), expr: tok(base + "." + f), kind, fits: fits(kind) });
|
|
241
247
|
}
|
|
242
248
|
return rows;
|
|
243
249
|
});
|
|
250
|
+
if (wantedKind.value) nodeItems.sort((a, b) => Number(!!b.fits) - Number(!!a.fits));
|
|
244
251
|
if (nodeItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.nodeOutputs"), items: nodeItems });
|
|
245
252
|
const primary = upstream[0];
|
|
246
253
|
const inItems = [{ label: t("integration.nodeConfig.expression.suggestion.inputPayload"), expr: tok("in") }];
|
|
247
254
|
for (const f of primary?.fields ?? []) {
|
|
248
|
-
inItems.push({ label: "input." + f, expr: tok("in." + f) });
|
|
255
|
+
inItems.push({ label: "input." + f, expr: tok("in." + f), kind: primary?.fieldTypes?.[f] ?? null });
|
|
249
256
|
}
|
|
250
257
|
out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.input"), items: inItems });
|
|
251
258
|
return out;
|
|
252
259
|
});
|
|
253
260
|
const hasSuggestions = computed(() => sections.value.some((s) => s.items.length > 0));
|
|
261
|
+
function kindLabel(kind) {
|
|
262
|
+
return kind && ["array", "object", "string", "number", "boolean"].includes(kind) ? t("integration.nodeConfig.expression.kind." + kind) : null;
|
|
263
|
+
}
|
|
264
|
+
const singleFit = computed(() => {
|
|
265
|
+
if (!wantedKind.value) return null;
|
|
266
|
+
const nodes = sections.value.find((s) => s.heading === t("integration.nodeConfig.expression.suggestion.heading.nodeOutputs"));
|
|
267
|
+
const fitting = (nodes?.items ?? []).filter((i) => i.fits);
|
|
268
|
+
return fitting.length === 1 ? fitting[0] : null;
|
|
269
|
+
});
|
|
254
270
|
const query = ref("");
|
|
255
271
|
const filteredSections = computed(() => {
|
|
256
272
|
const q = query.value.trim().toLowerCase();
|
|
@@ -269,6 +285,9 @@ function toggle() {
|
|
|
269
285
|
function onTriggerClick(e) {
|
|
270
286
|
if (e.detail === 0) toggle();
|
|
271
287
|
}
|
|
288
|
+
function onFitClick(e) {
|
|
289
|
+
if (e.detail === 0 && singleFit.value) insertToken(singleFit.value.expr);
|
|
290
|
+
}
|
|
272
291
|
function onFocusOut(e) {
|
|
273
292
|
if (wrapRef.value && e.relatedTarget instanceof globalThis.Node && wrapRef.value.contains(e.relatedTarget)) return;
|
|
274
293
|
open.value = false;
|
|
@@ -343,6 +362,17 @@ function insert(expr) {
|
|
|
343
362
|
</button>
|
|
344
363
|
</div>
|
|
345
364
|
|
|
365
|
+
<button
|
|
366
|
+
v-if="singleFit && !text"
|
|
367
|
+
type="button"
|
|
368
|
+
class="mt-1 inline-flex max-w-full items-center gap-1 rounded-md border border-emerald-600/30 bg-emerald-50 px-2 py-1 text-left text-xs text-emerald-800 transition hover:bg-emerald-100"
|
|
369
|
+
@mousedown.prevent="insertToken(singleFit.expr)"
|
|
370
|
+
@click="onFitClick"
|
|
371
|
+
>
|
|
372
|
+
<span class="shrink-0 font-medium">{{ t("integration.nodeConfig.expression.useFit") }}</span>
|
|
373
|
+
<span class="min-w-0 break-words">{{ singleFit.label }}</span>
|
|
374
|
+
</button>
|
|
375
|
+
|
|
346
376
|
<p
|
|
347
377
|
v-if="preview"
|
|
348
378
|
class="mt-1 flex min-w-0 items-baseline gap-1 text-[11px]"
|
|
@@ -388,10 +418,17 @@ function insert(expr) {
|
|
|
388
418
|
:key="item.expr"
|
|
389
419
|
type="button"
|
|
390
420
|
class="block w-full px-2 py-1 text-left transition hover:bg-muted/50"
|
|
421
|
+
:class="wantedKind && !item.fits && section.heading === t('integration.nodeConfig.expression.suggestion.heading.nodeOutputs') ? 'opacity-60' : ''"
|
|
391
422
|
@mousedown.prevent="insert(item.expr)"
|
|
392
423
|
>
|
|
393
|
-
<div class="
|
|
394
|
-
{{ item.label }}
|
|
424
|
+
<div class="flex flex-wrap items-baseline gap-x-1.5 text-muted-foreground">
|
|
425
|
+
<span class="min-w-0 break-words">{{ item.label }}</span>
|
|
426
|
+
<span
|
|
427
|
+
v-if="kindLabel(item.kind)"
|
|
428
|
+
class="rounded px-1 text-[10px] font-medium uppercase tracking-wide"
|
|
429
|
+
:class="item.fits ? 'bg-emerald-100 text-emerald-800' : 'bg-muted text-muted-foreground'"
|
|
430
|
+
>{{ kindLabel(item.kind) }}</span>
|
|
431
|
+
<span v-if="item.fits" class="text-[10px] font-medium text-emerald-700">{{ t("integration.nodeConfig.expression.fits") }}</span>
|
|
395
432
|
</div>
|
|
396
433
|
<div class="break-all font-mono text-emerald-700">
|
|
397
434
|
{{ item.expr }}
|
|
@@ -4,6 +4,12 @@ type UpstreamOutput = {
|
|
|
4
4
|
nodeName?: string;
|
|
5
5
|
output: string;
|
|
6
6
|
fields?: string[];
|
|
7
|
+
/** The port's declared data type, and each field's — what an entry IS (PO-594). */
|
|
8
|
+
dataType?: string | null;
|
|
9
|
+
fieldTypes?: Record<string, string | null>;
|
|
10
|
+
/** What the port and each field are called, resolved; the key stands in where absent. */
|
|
11
|
+
portLabel?: string | null;
|
|
12
|
+
fieldLabels?: Record<string, string | null>;
|
|
7
13
|
};
|
|
8
14
|
type __VLS_Props = {
|
|
9
15
|
modelValue: unknown;
|
|
@@ -12,6 +18,12 @@ type __VLS_Props = {
|
|
|
12
18
|
upstreamOutputs?: UpstreamOutput[];
|
|
13
19
|
/** Draw the editor as at fault — the field above owns the reason. */
|
|
14
20
|
invalid?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* What a mapped value has to be, as the setting declares it (`array` for a list).
|
|
23
|
+
* The outputs of that kind are listed first and marked, and an empty setting with
|
|
24
|
+
* exactly one of them below it offers that one in a click (PO-594).
|
|
25
|
+
*/
|
|
26
|
+
accepts?: string;
|
|
15
27
|
/** The setting's label, so the editable region has an accessible name. */
|
|
16
28
|
label?: string;
|
|
17
29
|
/** The setting takes several lines: Enter opens one instead of being
|
|
@@ -18,6 +18,10 @@ type __VLS_Props = {
|
|
|
18
18
|
nodeName?: string;
|
|
19
19
|
output: string;
|
|
20
20
|
fields?: string[];
|
|
21
|
+
dataType?: string | null;
|
|
22
|
+
fieldTypes?: Record<string, string | null>;
|
|
23
|
+
portLabel?: string | null;
|
|
24
|
+
fieldLabels?: Record<string, string | null>;
|
|
21
25
|
}>;
|
|
22
26
|
/** Why the last save refused this setting, if it did. */
|
|
23
27
|
refused?: string;
|
|
@@ -85,7 +85,12 @@ const formatError = computed(() => {
|
|
|
85
85
|
}
|
|
86
86
|
return null;
|
|
87
87
|
});
|
|
88
|
-
const fieldError = computed(() => formatError.value ??
|
|
88
|
+
const fieldError = computed(() => formatError.value ?? refusedSentence.value ?? null);
|
|
89
|
+
const refusedSentence = computed(() => {
|
|
90
|
+
const r = props.refused;
|
|
91
|
+
if (!r) return null;
|
|
92
|
+
return /^Required config field '[^']+' is missing\.$/.test(r) ? t("integration.nodeConfig.required.missing", { label: resolveLocalized(props.field.label) || props.field.key }) : r;
|
|
93
|
+
});
|
|
89
94
|
const invalid = computed(() => !!fieldError.value);
|
|
90
95
|
const constraintHint = computed(() => {
|
|
91
96
|
const val = props.field.validation;
|
|
@@ -452,6 +457,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
452
457
|
:placeholder="resolveLocalized(field.placeholder)"
|
|
453
458
|
:variables="variables"
|
|
454
459
|
:upstream-outputs="upstreamOutputs"
|
|
460
|
+
:accepts="field.accepts"
|
|
455
461
|
:invalid="invalid"
|
|
456
462
|
:label="controlName"
|
|
457
463
|
:multiline="isMultiline"
|
|
@@ -489,6 +495,7 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
489
495
|
:placeholder="resolveLocalized(field.placeholder)"
|
|
490
496
|
:variables="variables"
|
|
491
497
|
:upstream-outputs="upstreamOutputs"
|
|
498
|
+
:accepts="field.accepts"
|
|
492
499
|
:invalid="invalid"
|
|
493
500
|
:label="controlName"
|
|
494
501
|
:multiline="isMultiline"
|
|
@@ -18,6 +18,10 @@ type __VLS_Props = {
|
|
|
18
18
|
nodeName?: string;
|
|
19
19
|
output: string;
|
|
20
20
|
fields?: string[];
|
|
21
|
+
dataType?: string | null;
|
|
22
|
+
fieldTypes?: Record<string, string | null>;
|
|
23
|
+
portLabel?: string | null;
|
|
24
|
+
fieldLabels?: Record<string, string | null>;
|
|
21
25
|
}>;
|
|
22
26
|
/** Why the last save refused this setting, if it did. */
|
|
23
27
|
refused?: string;
|
|
@@ -11,6 +11,10 @@ type __VLS_Props = {
|
|
|
11
11
|
nodeName?: string;
|
|
12
12
|
output: string;
|
|
13
13
|
fields?: string[];
|
|
14
|
+
dataType?: string | null;
|
|
15
|
+
fieldTypes?: Record<string, string | null>;
|
|
16
|
+
portLabel?: string | null;
|
|
17
|
+
fieldLabels?: Record<string, string | null>;
|
|
14
18
|
}>;
|
|
15
19
|
/** Rename intent (context menu / F2): focus + select the name field on open. */
|
|
16
20
|
autofocusName?: boolean;
|
|
@@ -11,6 +11,10 @@ type __VLS_Props = {
|
|
|
11
11
|
nodeName?: string;
|
|
12
12
|
output: string;
|
|
13
13
|
fields?: string[];
|
|
14
|
+
dataType?: string | null;
|
|
15
|
+
fieldTypes?: Record<string, string | null>;
|
|
16
|
+
portLabel?: string | null;
|
|
17
|
+
fieldLabels?: Record<string, string | null>;
|
|
14
18
|
}>;
|
|
15
19
|
/** Rename intent (context menu / F2): focus + select the name field on open. */
|
|
16
20
|
autofocusName?: boolean;
|
|
@@ -6,6 +6,8 @@ import * as Lucide from "lucide-vue-next";
|
|
|
6
6
|
import { INTEGRATION_NODE_CATALOG, isOffered, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
7
7
|
import { resolveLocalized, studioLocale } from "../../utils/localize";
|
|
8
8
|
import { orderFolderLeaves, orderFolders } from "../../utils/paletteOrder";
|
|
9
|
+
import { declaredFolderIcon } from "../../utils/folderMark";
|
|
10
|
+
import { nodeIcon } from "../../data/nodeIcons";
|
|
9
11
|
const { load: loadNodes } = useIntegrationsNodes();
|
|
10
12
|
onMounted(() => {
|
|
11
13
|
void loadNodes();
|
|
@@ -179,6 +181,9 @@ const currentGroup = computed(() => {
|
|
|
179
181
|
}
|
|
180
182
|
return cursor ?? null;
|
|
181
183
|
});
|
|
184
|
+
const appRowIcons = computed(() => new Map(
|
|
185
|
+
appRows.value.map(([label, g]) => [label, declaredFolderIcon(subtreeManifests(g))])
|
|
186
|
+
));
|
|
182
187
|
const levelTitle = computed(() => {
|
|
183
188
|
if (triggersLevel.value) return t("integration.canvas.palette.group.triggers");
|
|
184
189
|
return currentGroup.value?.label ?? t("integration.canvas.palette.title.addNode");
|
|
@@ -469,7 +474,8 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
469
474
|
@click="pushPath(['__apps', label])"
|
|
470
475
|
>
|
|
471
476
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-white ring-1 ring-black/[0.06]">
|
|
472
|
-
<
|
|
477
|
+
<component :is="nodeIcon(appRowIcons.get(label))" v-if="appRowIcons.get(label)" :size="13" class="text-violet-600" />
|
|
478
|
+
<IntegrationsNodeMark v-else-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" logo-only />
|
|
473
479
|
<Lucide.Package v-else :size="13" class="text-violet-600" />
|
|
474
480
|
</span>
|
|
475
481
|
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ label }}</span>
|
|
@@ -38,6 +38,12 @@ type __VLS_Props = {
|
|
|
38
38
|
error?: string | null;
|
|
39
39
|
/** The run behind this result (sync failure / async start) for deep-linking. */
|
|
40
40
|
runId?: string | null;
|
|
41
|
+
/**
|
|
42
|
+
* The platform answered the press with a refusal, so no run exists — as opposed
|
|
43
|
+
* to a run that started and then failed or outran the wait. Decided by the page,
|
|
44
|
+
* which has the status code; `error` alone cannot tell the two apart.
|
|
45
|
+
*/
|
|
46
|
+
refused?: boolean;
|
|
41
47
|
};
|
|
42
48
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
43
49
|
"update:open": (args_0: boolean) => any;
|
|
@@ -21,7 +21,8 @@ const props = defineProps({
|
|
|
21
21
|
result: { type: null, required: false },
|
|
22
22
|
following: { type: Boolean, required: false },
|
|
23
23
|
error: { type: [String, null], required: false },
|
|
24
|
-
runId: { type: [String, null], required: false }
|
|
24
|
+
runId: { type: [String, null], required: false },
|
|
25
|
+
refused: { type: Boolean, required: false }
|
|
25
26
|
});
|
|
26
27
|
const emit = defineEmits(["update:open", "view-run"]);
|
|
27
28
|
const t = useIntegrationsT();
|
|
@@ -40,6 +41,17 @@ const ended = computed(() => {
|
|
|
40
41
|
return isTerminalRun(props.run);
|
|
41
42
|
});
|
|
42
43
|
const returnedNothing = computed(() => props.result == null || typeof props.result === "object" && !Array.isArray(props.result) && !Object.keys(props.result).length);
|
|
44
|
+
const refused = computed(() => !props.loading && !!props.refused);
|
|
45
|
+
const unconfirmed = computed(() => !props.loading && !refused.value && props.mode === "async" && !!props.error && !runNumber.value);
|
|
46
|
+
const title = computed(() => {
|
|
47
|
+
if (props.loading) {
|
|
48
|
+
return props.mode === "async" ? t("integration.runs.response.title.starting") : t("integration.runs.response.title.running");
|
|
49
|
+
}
|
|
50
|
+
if (refused.value) return t("integration.runs.response.title.refused");
|
|
51
|
+
if (unconfirmed.value) return t("integration.runs.response.title.unconfirmed");
|
|
52
|
+
return props.mode === "async" ? t("integration.runs.response.title.async") : t("integration.runs.response.title.sync");
|
|
53
|
+
});
|
|
54
|
+
const withoutRun = computed(() => refused.value || unconfirmed.value);
|
|
43
55
|
function pretty(value) {
|
|
44
56
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
45
57
|
}
|
|
@@ -55,8 +67,12 @@ function pretty(value) {
|
|
|
55
67
|
of the screen at once and took the buttons below it out of reach (PO-402). -->
|
|
56
68
|
<DialogContent class="max-w-[94vw] overflow-hidden sm:max-w-4xl">
|
|
57
69
|
<DialogHeader>
|
|
58
|
-
<DialogTitle>{{
|
|
59
|
-
|
|
70
|
+
<DialogTitle>{{ title }}</DialogTitle>
|
|
71
|
+
<!-- No subtitle where no run is known to exist: "started in the background"
|
|
72
|
+
is exactly what may not have happened. The reason in the body is then
|
|
73
|
+
the dialog's description instead, so it is announced with the heading
|
|
74
|
+
without standing in the page twice. -->
|
|
75
|
+
<DialogDescription v-if="!withoutRun" class="text-sm">
|
|
60
76
|
{{ mode === "async" ? t("integration.runs.response.description.async") : t("integration.runs.response.description.sync") }}
|
|
61
77
|
</DialogDescription>
|
|
62
78
|
</DialogHeader>
|
|
@@ -71,7 +87,14 @@ function pretty(value) {
|
|
|
71
87
|
</div>
|
|
72
88
|
|
|
73
89
|
<template v-else-if="error">
|
|
74
|
-
<
|
|
90
|
+
<DialogDescription
|
|
91
|
+
v-if="withoutRun"
|
|
92
|
+
as="div"
|
|
93
|
+
class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-rose-700"
|
|
94
|
+
>
|
|
95
|
+
{{ error }}
|
|
96
|
+
</DialogDescription>
|
|
97
|
+
<div v-else class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-rose-700">
|
|
75
98
|
{{ error }}
|
|
76
99
|
</div>
|
|
77
100
|
<div v-if="runId" class="mt-3 flex justify-end">
|
|
@@ -38,6 +38,12 @@ type __VLS_Props = {
|
|
|
38
38
|
error?: string | null;
|
|
39
39
|
/** The run behind this result (sync failure / async start) for deep-linking. */
|
|
40
40
|
runId?: string | null;
|
|
41
|
+
/**
|
|
42
|
+
* The platform answered the press with a refusal, so no run exists — as opposed
|
|
43
|
+
* to a run that started and then failed or outran the wait. Decided by the page,
|
|
44
|
+
* which has the status code; `error` alone cannot tell the two apart.
|
|
45
|
+
*/
|
|
46
|
+
refused?: boolean;
|
|
41
47
|
};
|
|
42
48
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
43
49
|
"update:open": (args_0: boolean) => any;
|
|
@@ -30,7 +30,13 @@ type __VLS_Props = {
|
|
|
30
30
|
/** The narrowing in force. Owned by the page, drawn here. */
|
|
31
31
|
status?: RunStatus[];
|
|
32
32
|
triggerType?: TriggerType | '';
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The owning list asks the platform for its order even though the table is not
|
|
35
|
+
* in server mode — the workflow's own Runs tab, which pages and orders on the
|
|
36
|
+
* server but keeps its own pager and search (PO-385). Server mode implies it.
|
|
37
|
+
*/
|
|
38
|
+
orderable?: boolean;
|
|
39
|
+
/** The order the platform is reading the list in. Server mode or orderable only. */
|
|
34
40
|
sort?: RunSort;
|
|
35
41
|
direction?: RunSortDirection;
|
|
36
42
|
};
|
|
@@ -74,5 +80,6 @@ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {},
|
|
|
74
80
|
pagination: boolean;
|
|
75
81
|
showWorkflow: boolean;
|
|
76
82
|
serverMode: boolean;
|
|
83
|
+
orderable: boolean;
|
|
77
84
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
78
85
|
export default _default;
|
|
@@ -25,6 +25,7 @@ const props = defineProps({
|
|
|
25
25
|
perPage: { type: Number, required: false, default: 25 },
|
|
26
26
|
status: { type: Array, required: false },
|
|
27
27
|
triggerType: { type: String, required: false, default: "" },
|
|
28
|
+
orderable: { type: Boolean, required: false, default: false },
|
|
28
29
|
sort: { type: null, required: false, default: DEFAULT_RUN_SORT },
|
|
29
30
|
direction: { type: String, required: false, default: DEFAULT_RUN_DIRECTION }
|
|
30
31
|
});
|
|
@@ -91,22 +92,24 @@ const SERVER_SORTS = {
|
|
|
91
92
|
started: "started_at",
|
|
92
93
|
duration: "duration"
|
|
93
94
|
};
|
|
95
|
+
const ordersOnServer = computed(() => props.serverMode || props.orderable);
|
|
94
96
|
function sortableColumn(key) {
|
|
95
|
-
return
|
|
97
|
+
return ordersOnServer.value && key in SERVER_SORTS;
|
|
96
98
|
}
|
|
97
99
|
const sortedColumn = computed(() => {
|
|
98
100
|
const entry = Object.entries(SERVER_SORTS).find(([, value]) => value === props.sort);
|
|
99
101
|
return entry ? entry[0] : null;
|
|
100
102
|
});
|
|
101
|
-
const serverModels = computed(() =>
|
|
102
|
-
sort: tableSort.value,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} : {}
|
|
103
|
+
const serverModels = computed(() => ({
|
|
104
|
+
...ordersOnServer.value ? { sort: tableSort.value, "onUpdate:sort": onTableSort } : {},
|
|
105
|
+
...props.serverMode ? {
|
|
106
|
+
// 1-based here, as the platform pages; the shared table counts from zero.
|
|
107
|
+
page: page.value - 1,
|
|
108
|
+
"onUpdate:page": (p) => {
|
|
109
|
+
page.value = p + 1;
|
|
110
|
+
}
|
|
111
|
+
} : {}
|
|
112
|
+
}));
|
|
110
113
|
const columns = computed(() => [
|
|
111
114
|
// Sorted and searched by the word on screen, not by the raw field: a column
|
|
112
115
|
// with no `searchGetter` is searched through its `sortGetter`, so a German
|
|
@@ -125,7 +128,7 @@ const columns = computed(() => [
|
|
|
125
128
|
// what a search has to match.
|
|
126
129
|
{ key: "trigger", label: t("integration.runs.table.column.trigger"), width: "120px", sortable: sortableColumn("trigger"), sortGetter: (r) => triggerTypeLabel(r.triggerType) },
|
|
127
130
|
{ key: "started", label: t("integration.runs.table.column.started"), width: "120px", sortable: sortableColumn("started"), sortGetter: (r) => r.startedAt },
|
|
128
|
-
{ key: "duration", label: t("integration.runs.table.column.duration"), width: "100px", sortable: sortableColumn("duration"), sortGetter: (r) => r.durationMs ??
|
|
131
|
+
{ key: "duration", label: t("integration.runs.table.column.duration"), width: "100px", sortable: sortableColumn("duration"), sortGetter: (r) => r.durationMs ?? null },
|
|
129
132
|
{ key: "actions", label: "", width: "52px" }
|
|
130
133
|
]);
|
|
131
134
|
</script>
|
|
@@ -30,7 +30,13 @@ type __VLS_Props = {
|
|
|
30
30
|
/** The narrowing in force. Owned by the page, drawn here. */
|
|
31
31
|
status?: RunStatus[];
|
|
32
32
|
triggerType?: TriggerType | '';
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The owning list asks the platform for its order even though the table is not
|
|
35
|
+
* in server mode — the workflow's own Runs tab, which pages and orders on the
|
|
36
|
+
* server but keeps its own pager and search (PO-385). Server mode implies it.
|
|
37
|
+
*/
|
|
38
|
+
orderable?: boolean;
|
|
39
|
+
/** The order the platform is reading the list in. Server mode or orderable only. */
|
|
34
40
|
sort?: RunSort;
|
|
35
41
|
direction?: RunSortDirection;
|
|
36
42
|
};
|
|
@@ -74,5 +80,6 @@ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {},
|
|
|
74
80
|
pagination: boolean;
|
|
75
81
|
showWorkflow: boolean;
|
|
76
82
|
serverMode: boolean;
|
|
83
|
+
orderable: boolean;
|
|
77
84
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
78
85
|
export default _default;
|
|
@@ -50,7 +50,7 @@ import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
|
50
50
|
import { layoutWorkflowNodes } from "./workflowLayout";
|
|
51
51
|
import { defaultsFor } from "../../utils/configDefaults";
|
|
52
52
|
import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
|
|
53
|
-
import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
|
|
53
|
+
import { collectUpstreamNodes, isTerminalManifest, portsForNode } from "../../data/nodeInputData";
|
|
54
54
|
import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
55
55
|
import { inputHandleId, outputHandleId, portFromHandleId } from "../../data/handleId";
|
|
56
56
|
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
@@ -868,17 +868,34 @@ const selectedUpstreamOutputs = computed(() => {
|
|
|
868
868
|
if (!id) return [];
|
|
869
869
|
const isNode = blob.value.nodes.some((n) => n.id === id);
|
|
870
870
|
if (!isNode) return [];
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
871
|
+
const entries = [];
|
|
872
|
+
const seen = /* @__PURE__ */ new Set();
|
|
873
|
+
const add = (source, output) => {
|
|
874
|
+
if (!source || seen.has(source.id + "\0" + output)) return;
|
|
875
|
+
seen.add(source.id + "\0" + output);
|
|
876
|
+
entries.push({ source, output });
|
|
877
|
+
};
|
|
878
|
+
for (const e of blob.value.edges.filter((e2) => e2.to.nodeId === id)) add(blob.value.nodes.find((n) => n.id === e.from.nodeId), e.from.output);
|
|
879
|
+
for (const { node } of collectUpstreamNodes(id, blob.value.nodes, blob.value.edges)) {
|
|
880
|
+
for (const e of blob.value.edges.filter((e2) => e2.from.nodeId === node.id)) add(node, e.from.output);
|
|
881
|
+
}
|
|
882
|
+
return entries.map(({ source, output }) => {
|
|
883
|
+
const port = portsForNode(source, findManifest(source.nodeSlug, source.nodeVersion), dynamicOutputs.value[source.id]).find((p) => p.name === output);
|
|
884
|
+
return {
|
|
876
885
|
nodeId: source.id,
|
|
877
886
|
// Called what its card calls it, not by its slug (PO-601).
|
|
878
887
|
nodeName: nodeDisplayName(source),
|
|
879
|
-
output
|
|
880
|
-
fields: port?.fields.map((f) => f.name)
|
|
881
|
-
|
|
888
|
+
output,
|
|
889
|
+
fields: port?.fields.map((f) => f.name),
|
|
890
|
+
// What each one is, so the `{ }` list can say "list" and point a setting
|
|
891
|
+
// that wants one at it (PO-594). The names alone made five entries alike.
|
|
892
|
+
dataType: port?.dataType ?? null,
|
|
893
|
+
fieldTypes: Object.fromEntries((port?.fields ?? []).map((f) => [f.name, f.dataType])),
|
|
894
|
+
// What the port and its fields are called, so the list reads "Rechnungen"
|
|
895
|
+
// where the key is `invoices` (PO-594). Absent where the manifest names none.
|
|
896
|
+
portLabel: resolveLocalized(port?.label) || null,
|
|
897
|
+
fieldLabels: Object.fromEntries((port?.fields ?? []).map((f) => [f.name, resolveLocalized(f.label) || null]))
|
|
898
|
+
};
|
|
882
899
|
});
|
|
883
900
|
});
|
|
884
901
|
function applyInspectorUpdate(next) {
|
|
@@ -25,8 +25,9 @@ export function useIntegrationsNodes() {
|
|
|
25
25
|
slug: n.manifest.slug ?? n.slug,
|
|
26
26
|
version: n.manifest.version ?? n.version,
|
|
27
27
|
name: n.manifest.name ?? n.name,
|
|
28
|
-
// Keep the source package — the palette groups app nodes by it
|
|
29
|
-
|
|
28
|
+
// Keep the source package — the palette groups app nodes by it, and
|
|
29
|
+
// marks a package's folder with the icon it declares (PO-640).
|
|
30
|
+
__package: n.package ? { name: n.package.name, label: n.package.label ?? null, icon: n.package.icon ?? null } : void 0,
|
|
30
31
|
// Registered and built are two different things: a version whose code
|
|
31
32
|
// was never built can be drawn but not executed, so the palette must
|
|
32
33
|
// be able to leave it out (PO-338). Missing field = assume built.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RunStatus, RunStepRow, WorkflowRun } from '../types/integrations.js';
|
|
2
|
-
import type { RunFeedQuery } from '../utils/runFeedQuery.js';
|
|
2
|
+
import type { RunFeedQuery, RunSort, RunSortDirection } from '../utils/runFeedQuery.js';
|
|
3
3
|
export type { RunFeedQuery };
|
|
4
4
|
interface RunSummary {
|
|
5
5
|
total: number;
|
|
@@ -25,6 +25,8 @@ export declare function useIntegrationsRuns(): {
|
|
|
25
25
|
reconcile?: boolean;
|
|
26
26
|
page?: number;
|
|
27
27
|
perPage?: number;
|
|
28
|
+
sort?: RunSort;
|
|
29
|
+
direction?: RunSortDirection;
|
|
28
30
|
}) => Promise<{
|
|
29
31
|
runs: WorkflowRun[];
|
|
30
32
|
total: number;
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
mapRunStatus,
|
|
12
12
|
runFeedNarrowingString,
|
|
13
13
|
runFeedQueryString,
|
|
14
|
-
withRunFeedPatch
|
|
14
|
+
withRunFeedPatch,
|
|
15
|
+
workflowRunsQueryString
|
|
15
16
|
} from "../utils/runFeedQuery.js";
|
|
16
17
|
const EMPTY_SUMMARY = { total: 0, in_flight: 0, by_status: {} };
|
|
17
18
|
export function useIntegrationsRuns() {
|
|
@@ -120,9 +121,9 @@ export function useIntegrationsRuns() {
|
|
|
120
121
|
const page = opts.page ?? 1;
|
|
121
122
|
const perPage = opts.perPage ?? 25;
|
|
122
123
|
const res = await api.get(
|
|
123
|
-
`/workflows/${workflowId}/runs
|
|
124
|
+
`/workflows/${workflowId}/runs?${workflowRunsQueryString(page, perPage, opts.sort, opts.direction)}`
|
|
124
125
|
);
|
|
125
|
-
let list2 = (res?.data ?? []).map((r) => mapRun(r, workflowName))
|
|
126
|
+
let list2 = (res?.data ?? []).map((r) => mapRun(r, workflowName));
|
|
126
127
|
if (opts.reconcile) {
|
|
127
128
|
const active = list2.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
|
|
128
129
|
await Promise.allSettled(active.map(async (r) => {
|
|
@@ -59,7 +59,7 @@ export const TRIGGER_TEMPLATES = [
|
|
|
59
59
|
name: { en: "Manual", de: "Manuell" },
|
|
60
60
|
description: {
|
|
61
61
|
en: "Start the workflow yourself with the Run button or an API call.",
|
|
62
|
-
de: "
|
|
62
|
+
de: "Starten Sie den Workflow selbst \xFCber den Run-Button oder einen API-Aufruf."
|
|
63
63
|
},
|
|
64
64
|
defaultConfig: {}
|
|
65
65
|
},
|
|
@@ -69,7 +69,7 @@ export const TRIGGER_TEMPLATES = [
|
|
|
69
69
|
name: { en: "Schedule", de: "Zeitplan" },
|
|
70
70
|
description: {
|
|
71
71
|
en: "Runs automatically on a schedule you define, for example every night.",
|
|
72
|
-
de: "L\xE4uft automatisch nach
|
|
72
|
+
de: "L\xE4uft automatisch nach Ihrem Zeitplan, zum Beispiel jede Nacht."
|
|
73
73
|
},
|
|
74
74
|
defaultConfig: { cron: "0 * * * *", timezone: "Europe/Berlin" }
|
|
75
75
|
},
|
|
@@ -15,5 +15,11 @@
|
|
|
15
15
|
* unquoted key the day the linter unquoted them.
|
|
16
16
|
*/
|
|
17
17
|
export declare const MDI_TO_LUCIDE: Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a manifest icon name to a Lucide component, or `null` when nothing
|
|
20
|
+
* by that name exists — for a caller that must tell an unknown name from a
|
|
21
|
+
* declared `lucide:box` (`declaredFolderIcon`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveNodeIcon(name?: string | null): unknown;
|
|
18
24
|
/** Resolve a manifest icon name to a Lucide component (Box as last resort). */
|
|
19
|
-
export declare function nodeIcon(name?: string | null):
|
|
25
|
+
export declare function nodeIcon(name?: string | null): {};
|
|
@@ -108,13 +108,16 @@ export const MDI_TO_LUCIDE = {
|
|
|
108
108
|
whatsapp: "MessageCircle",
|
|
109
109
|
xml: "CodeXml"
|
|
110
110
|
};
|
|
111
|
-
export function
|
|
112
|
-
if (!name) return
|
|
111
|
+
export function resolveNodeIcon(name) {
|
|
112
|
+
if (!name) return null;
|
|
113
113
|
const bare = name.includes(":") ? name.split(":").pop() : name;
|
|
114
114
|
const mapped = name.startsWith("lucide:") ? void 0 : MDI_TO_LUCIDE[bare];
|
|
115
115
|
if (mapped && Lucide[mapped]) {
|
|
116
116
|
return Lucide[mapped];
|
|
117
117
|
}
|
|
118
118
|
const pascal = bare.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
119
|
-
return Lucide[pascal] ||
|
|
119
|
+
return Lucide[pascal] || null;
|
|
120
|
+
}
|
|
121
|
+
export function nodeIcon(name) {
|
|
122
|
+
return resolveNodeIcon(name) || Lucide.Box;
|
|
120
123
|
}
|
|
@@ -26,12 +26,18 @@ export interface FieldEntry {
|
|
|
26
26
|
* on {@link PortEntry.description}.
|
|
27
27
|
*/
|
|
28
28
|
description?: LocalizedString;
|
|
29
|
+
/** What the field is called, unresolved, where the manifest names it (PO-594). */
|
|
30
|
+
label?: LocalizedString;
|
|
29
31
|
}
|
|
30
32
|
export interface PortEntry {
|
|
31
33
|
name: string;
|
|
32
34
|
kind: OutputKind;
|
|
33
35
|
/** Declared payload fields of this port (schema; may be empty). */
|
|
34
36
|
fields: FieldEntry[];
|
|
37
|
+
/** The port's own declared data type; absent where the manifest gives none. */
|
|
38
|
+
dataType?: string;
|
|
39
|
+
/** What the port is called, unresolved, where the manifest names it (PO-594). */
|
|
40
|
+
label?: LocalizedString;
|
|
35
41
|
/**
|
|
36
42
|
* What the publisher says flows through this port, **unresolved** — the
|
|
37
43
|
* localised map as the manifest sent it, not a string.
|
|
@@ -42,19 +42,28 @@ function fieldEntries(raw) {
|
|
|
42
42
|
return {
|
|
43
43
|
name,
|
|
44
44
|
dataType: typeof s?.dataType === "string" ? s.dataType : null,
|
|
45
|
-
...describedBy(s?.description)
|
|
45
|
+
...describedBy(s?.description),
|
|
46
|
+
...labelledBy(s?.label)
|
|
46
47
|
};
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
return [];
|
|
50
51
|
}
|
|
51
52
|
function describedBy(raw) {
|
|
52
|
-
|
|
53
|
+
const v = cleanLocalized(raw);
|
|
54
|
+
return v === void 0 ? {} : { description: v };
|
|
55
|
+
}
|
|
56
|
+
function labelledBy(raw) {
|
|
57
|
+
const v = cleanLocalized(raw);
|
|
58
|
+
return v === void 0 ? {} : { label: v };
|
|
59
|
+
}
|
|
60
|
+
function cleanLocalized(raw) {
|
|
61
|
+
if (typeof raw === "string") return raw.trim() ? raw.trim() : void 0;
|
|
53
62
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
54
63
|
const entries = Object.entries(raw).filter((e) => typeof e[1] === "string" && e[1].trim() !== "").map(([k, v]) => [k, v.trim()]);
|
|
55
|
-
return entries.length ?
|
|
64
|
+
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
56
65
|
}
|
|
57
|
-
return
|
|
66
|
+
return void 0;
|
|
58
67
|
}
|
|
59
68
|
export function inputPortsForManifest(manifest) {
|
|
60
69
|
const raw = manifest?.inputs;
|
|
@@ -78,7 +87,7 @@ export function portsForNode(node, manifest, resolvedOutputs) {
|
|
|
78
87
|
const ports = [];
|
|
79
88
|
for (const o of list) {
|
|
80
89
|
if (o.name) {
|
|
81
|
-
ports.push({ name: o.name, kind: o.kind, fields: fieldEntries(o.fields), ...describedBy(o.description) });
|
|
90
|
+
ports.push({ name: o.name, kind: o.kind, fields: fieldEntries(o.fields), ...typeof o.dataType === "string" ? { dataType: o.dataType } : {}, ...labelledBy(o.label), ...describedBy(o.description) });
|
|
82
91
|
} else if (o.sourceFromConfig) {
|
|
83
92
|
const raw = node.config?.[o.sourceFromConfig];
|
|
84
93
|
const values = Array.isArray(raw) ? raw : [];
|
|
@@ -467,6 +467,7 @@
|
|
|
467
467
|
"integration.editor.splice.error.terminal": "{name} beendet den Run und kann nicht in eine Verbindung eingefügt werden.",
|
|
468
468
|
"integration.editor.splice.toast": "Node aus der Palette auswählen — er wird in die Verbindung eingefügt.",
|
|
469
469
|
"integration.editor.state.loading": "Wird geladen…",
|
|
470
|
+
"integration.editor.status.saved": "Gespeichert",
|
|
470
471
|
"integration.editor.status.unsavedChanges": "Nicht gespeicherte Änderungen",
|
|
471
472
|
"integration.editor.sticky.starterContent": "## Ich bin eine Notiz\n**Doppelklicken**, um mich zu bearbeiten. **Fett**, *kursiv*, `Code`, [Links](https://revenexx.com) und\n- Listen funktionieren",
|
|
472
473
|
"integration.editor.tab.deadLetters": "Verpasste Runs",
|
|
@@ -563,8 +564,14 @@
|
|
|
563
564
|
"integration.nodeConfig.dynamicSchema.reloading": "Felder werden neu geladen…",
|
|
564
565
|
"integration.nodeConfig.dynamicSchema.resolving": "Felder werden geladen…",
|
|
565
566
|
"integration.nodeConfig.expression.badge.title": "Akzeptiert ${{ … }}-Ausdrücke",
|
|
567
|
+
"integration.nodeConfig.expression.fits": "passt",
|
|
566
568
|
"integration.nodeConfig.expression.insertAria": "Ausdruck einfügen — verfügbare Daten für diese Einstellung",
|
|
567
569
|
"integration.nodeConfig.expression.insertTitle": "Ausdruck einfügen",
|
|
570
|
+
"integration.nodeConfig.expression.kind.array": "Liste",
|
|
571
|
+
"integration.nodeConfig.expression.kind.boolean": "Ja/Nein",
|
|
572
|
+
"integration.nodeConfig.expression.kind.number": "Zahl",
|
|
573
|
+
"integration.nodeConfig.expression.kind.object": "Objekt",
|
|
574
|
+
"integration.nodeConfig.expression.kind.string": "Text",
|
|
568
575
|
"integration.nodeConfig.expression.picker.aria": "Verfügbare Daten für diese Einstellung",
|
|
569
576
|
"integration.nodeConfig.expression.picker.empty": "Nichts entspricht „{query}“.",
|
|
570
577
|
"integration.nodeConfig.expression.picker.filterAria": "Verfügbare Daten filtern",
|
|
@@ -581,6 +588,7 @@
|
|
|
581
588
|
"integration.nodeConfig.expression.suggestion.inputPayload": "Eingabe-Payload",
|
|
582
589
|
"integration.nodeConfig.expression.toggle.off": "Zurück zum normalen Feld (löscht den Ausdruck)",
|
|
583
590
|
"integration.nodeConfig.expression.toggle.on": "Diesen Wert stattdessen aus einem vorherigen Schritt zuordnen",
|
|
591
|
+
"integration.nodeConfig.expression.useFit": "Übernehmen:",
|
|
584
592
|
"integration.nodeConfig.field.requiredMark": "Erforderlich",
|
|
585
593
|
"integration.nodeConfig.hint.charMax": "bis zu {max} Zeichen",
|
|
586
594
|
"integration.nodeConfig.hint.charMin": "mindestens {min} Zeichen",
|
|
@@ -625,6 +633,7 @@
|
|
|
625
633
|
"integration.nodeConfig.queryBuilder.row.valueAria": "Filter {n} — Wert",
|
|
626
634
|
"integration.nodeConfig.queryBuilder.row.valueListPlaceholder": "Wert, Wert, …",
|
|
627
635
|
"integration.nodeConfig.queryBuilder.row.valuePlaceholder": "Wert",
|
|
636
|
+
"integration.nodeConfig.required.missing": "{label} fehlt noch.",
|
|
628
637
|
"integration.nodeConfig.resolve.action.retry": "RETRY",
|
|
629
638
|
"integration.nodeConfig.resolve.error.building": "Node-Bundle wird noch erstellt.",
|
|
630
639
|
"integration.nodeConfig.resolve.error.generic": "Auflösung fehlgeschlagen",
|
|
@@ -973,7 +982,11 @@
|
|
|
973
982
|
"integration.runs.response.nothingReturned": "Der Run hat nichts zurückgegeben.",
|
|
974
983
|
"integration.runs.response.runNumber": "Run #{id}",
|
|
975
984
|
"integration.runs.response.title.async": "Run gestartet",
|
|
985
|
+
"integration.runs.response.title.refused": "Run nicht gestartet",
|
|
986
|
+
"integration.runs.response.title.running": "Run läuft",
|
|
987
|
+
"integration.runs.response.title.starting": "Run wird gestartet",
|
|
976
988
|
"integration.runs.response.title.sync": "Run-Antwort",
|
|
989
|
+
"integration.runs.response.title.unconfirmed": "Start nicht bestätigt",
|
|
977
990
|
"integration.runs.status.cancelled": "abgebrochen",
|
|
978
991
|
"integration.runs.status.failed": "fehlgeschlagen",
|
|
979
992
|
"integration.runs.status.queued": "wartet",
|
|
@@ -467,6 +467,7 @@
|
|
|
467
467
|
"integration.editor.splice.error.terminal": "{name} ends the run and cannot be inserted into a connection.",
|
|
468
468
|
"integration.editor.splice.toast": "Pick a node from the palette — it will be inserted into the connection.",
|
|
469
469
|
"integration.editor.state.loading": "Loading…",
|
|
470
|
+
"integration.editor.status.saved": "Saved",
|
|
470
471
|
"integration.editor.status.unsavedChanges": "Unsaved changes",
|
|
471
472
|
"integration.editor.sticky.starterContent": "## I'm a note\n**Double-click** to edit me. **Bold**, *italic*, `code`, [links](https://revenexx.com) and\n- lists work",
|
|
472
473
|
"integration.editor.tab.deadLetters": "Missed runs",
|
|
@@ -563,8 +564,14 @@
|
|
|
563
564
|
"integration.nodeConfig.dynamicSchema.reloading": "reloading fields…",
|
|
564
565
|
"integration.nodeConfig.dynamicSchema.resolving": "Resolving fields…",
|
|
565
566
|
"integration.nodeConfig.expression.badge.title": "Accepts ${{ … }} expressions",
|
|
567
|
+
"integration.nodeConfig.expression.fits": "fits",
|
|
566
568
|
"integration.nodeConfig.expression.insertAria": "Insert expression — available data for this setting",
|
|
567
569
|
"integration.nodeConfig.expression.insertTitle": "Insert expression",
|
|
570
|
+
"integration.nodeConfig.expression.kind.array": "List",
|
|
571
|
+
"integration.nodeConfig.expression.kind.boolean": "Yes/no",
|
|
572
|
+
"integration.nodeConfig.expression.kind.number": "Number",
|
|
573
|
+
"integration.nodeConfig.expression.kind.object": "Object",
|
|
574
|
+
"integration.nodeConfig.expression.kind.string": "Text",
|
|
568
575
|
"integration.nodeConfig.expression.picker.aria": "Available data for this setting",
|
|
569
576
|
"integration.nodeConfig.expression.picker.empty": "Nothing matches “{query}”.",
|
|
570
577
|
"integration.nodeConfig.expression.picker.filterAria": "Filter available data",
|
|
@@ -581,6 +588,7 @@
|
|
|
581
588
|
"integration.nodeConfig.expression.suggestion.inputPayload": "input payload",
|
|
582
589
|
"integration.nodeConfig.expression.toggle.off": "Back to the regular control (clears the expression)",
|
|
583
590
|
"integration.nodeConfig.expression.toggle.on": "Map this value from a previous step instead",
|
|
591
|
+
"integration.nodeConfig.expression.useFit": "Use:",
|
|
584
592
|
"integration.nodeConfig.field.requiredMark": "Required",
|
|
585
593
|
"integration.nodeConfig.hint.charMax": "up to {max} characters",
|
|
586
594
|
"integration.nodeConfig.hint.charMin": "at least {min} characters",
|
|
@@ -625,6 +633,7 @@
|
|
|
625
633
|
"integration.nodeConfig.queryBuilder.row.valueAria": "Filter {n} — value",
|
|
626
634
|
"integration.nodeConfig.queryBuilder.row.valueListPlaceholder": "value, value, …",
|
|
627
635
|
"integration.nodeConfig.queryBuilder.row.valuePlaceholder": "value",
|
|
636
|
+
"integration.nodeConfig.required.missing": "{label} is still missing.",
|
|
628
637
|
"integration.nodeConfig.resolve.action.retry": "RETRY",
|
|
629
638
|
"integration.nodeConfig.resolve.error.building": "Node bundle still building.",
|
|
630
639
|
"integration.nodeConfig.resolve.error.generic": "Resolve failed",
|
|
@@ -973,7 +982,11 @@
|
|
|
973
982
|
"integration.runs.response.nothingReturned": "The run returned nothing.",
|
|
974
983
|
"integration.runs.response.runNumber": "Run #{id}",
|
|
975
984
|
"integration.runs.response.title.async": "Run started",
|
|
985
|
+
"integration.runs.response.title.refused": "Run not started",
|
|
986
|
+
"integration.runs.response.title.running": "Run in progress",
|
|
987
|
+
"integration.runs.response.title.starting": "Starting run",
|
|
976
988
|
"integration.runs.response.title.sync": "Run response",
|
|
989
|
+
"integration.runs.response.title.unconfirmed": "Start not confirmed",
|
|
977
990
|
"integration.runs.status.cancelled": "cancelled",
|
|
978
991
|
"integration.runs.status.failed": "failed",
|
|
979
992
|
"integration.runs.status.queued": "queued",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
|
-
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, RefreshCircle as Loader2, MenuDots as MoreHorizontal, Play, Refresh as RetryIcon, Diskette as Save, SidebarMinimalistic as RailIcon } from "#solar-icons";
|
|
3
|
+
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, CheckCircle, RefreshCircle as Loader2, MenuDots as MoreHorizontal, Play, Refresh as RetryIcon, Diskette as Save, SidebarMinimalistic as RailIcon } from "#solar-icons";
|
|
4
4
|
import {
|
|
5
5
|
AlertDialog,
|
|
6
6
|
AlertDialogCancel,
|
|
@@ -45,6 +45,7 @@ import { isTerminalRun, unwrapRun } from "../../../utils/runEnvelope";
|
|
|
45
45
|
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
46
46
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
47
47
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
48
|
+
import { DEFAULT_RUN_DIRECTION, DEFAULT_RUN_SORT } from "../../../utils/runFeedQuery";
|
|
48
49
|
const t = useIntegrationsT();
|
|
49
50
|
const route = useRoute();
|
|
50
51
|
const router = useRouter();
|
|
@@ -120,6 +121,27 @@ const reauthCredentials = computed(() => {
|
|
|
120
121
|
return needsReauth.value.filter((c) => blob.includes(c.id));
|
|
121
122
|
});
|
|
122
123
|
const buildWorking = computed(() => workflow.value?.buildStatus === "building" || workflow.value?.buildStatus === "pending");
|
|
124
|
+
const BUILD_CHIP_DELAY_MS = 3e3;
|
|
125
|
+
const buildSlow = ref(false);
|
|
126
|
+
let buildSlowTimer;
|
|
127
|
+
watch(buildWorking, (working) => {
|
|
128
|
+
clearTimeout(buildSlowTimer);
|
|
129
|
+
buildSlow.value = false;
|
|
130
|
+
if (working) buildSlowTimer = setTimeout(() => {
|
|
131
|
+
buildSlow.value = true;
|
|
132
|
+
}, BUILD_CHIP_DELAY_MS);
|
|
133
|
+
}, { immediate: true });
|
|
134
|
+
const buildChipShown = computed(() => buildWorking.value && buildSlow.value);
|
|
135
|
+
const SAVED_SHOWN_MS = 4e3;
|
|
136
|
+
const justSaved = ref(false);
|
|
137
|
+
let justSavedTimer;
|
|
138
|
+
function announceSaved() {
|
|
139
|
+
clearTimeout(justSavedTimer);
|
|
140
|
+
justSaved.value = true;
|
|
141
|
+
justSavedTimer = setTimeout(() => {
|
|
142
|
+
justSaved.value = false;
|
|
143
|
+
}, SAVED_SHOWN_MS);
|
|
144
|
+
}
|
|
123
145
|
const buildFailed = computed(() => workflow.value?.buildStatus === "failed");
|
|
124
146
|
const waitingForBuild = ref(false);
|
|
125
147
|
const BUILD_WAIT_POLL_MS = BUILD_SETTLE_POLL_MS;
|
|
@@ -153,7 +175,11 @@ function settleBuildStatus() {
|
|
|
153
175
|
read: () => get(workflow.value.id)
|
|
154
176
|
});
|
|
155
177
|
}
|
|
156
|
-
onUnmounted(() =>
|
|
178
|
+
onUnmounted(() => {
|
|
179
|
+
buildSettle.stop();
|
|
180
|
+
clearTimeout(buildSlowTimer);
|
|
181
|
+
clearTimeout(justSavedTimer);
|
|
182
|
+
});
|
|
157
183
|
const buildNotice = computed(() => {
|
|
158
184
|
switch (workflow.value?.buildStatus) {
|
|
159
185
|
case "building":
|
|
@@ -295,6 +321,7 @@ const syncError = ref(null);
|
|
|
295
321
|
const runObject = ref(null);
|
|
296
322
|
const followingRun = ref(false);
|
|
297
323
|
const runModalMode = ref("sync");
|
|
324
|
+
const runRefused = ref(false);
|
|
298
325
|
const runModalRunId = ref(null);
|
|
299
326
|
function extractRunId(res) {
|
|
300
327
|
if (res && typeof res === "object" && "id" in res) {
|
|
@@ -314,6 +341,11 @@ function viewRunFromModal(id2) {
|
|
|
314
341
|
syncOpen.value = false;
|
|
315
342
|
void router.push({ query: { ...route.query, run: id2 } });
|
|
316
343
|
}
|
|
344
|
+
function isRefusal(e) {
|
|
345
|
+
if (!(e instanceof IntegrationsApiError)) return false;
|
|
346
|
+
if (e.status < 400 || e.status >= 500 || e.status === 408) return false;
|
|
347
|
+
return extractRunId(e.body?.run) === null;
|
|
348
|
+
}
|
|
317
349
|
function syncErrorMessage(e) {
|
|
318
350
|
if (e instanceof IntegrationsApiError) {
|
|
319
351
|
if (e.status === 504) return t("integration.editor.run.error.syncTimeout");
|
|
@@ -336,6 +368,7 @@ function refusedForSettings(e) {
|
|
|
336
368
|
return /[.!?]$/.test(line) ? line : `${line}.`;
|
|
337
369
|
}
|
|
338
370
|
async function reportRunFailure(e, fallback) {
|
|
371
|
+
runRefused.value = isRefusal(e);
|
|
339
372
|
const settings = refusedForSettings(e);
|
|
340
373
|
if (!settings) {
|
|
341
374
|
syncError.value = fallback(e);
|
|
@@ -368,6 +401,7 @@ async function run(mode) {
|
|
|
368
401
|
syncLoading.value = true;
|
|
369
402
|
syncResponse.value = null;
|
|
370
403
|
syncError.value = null;
|
|
404
|
+
runRefused.value = false;
|
|
371
405
|
if (mode === "sync") {
|
|
372
406
|
try {
|
|
373
407
|
syncResponse.value = await triggerRun(workflow.value.id, payload, "sync");
|
|
@@ -480,6 +514,8 @@ const runsPage = ref(1);
|
|
|
480
514
|
const runsTotal = ref(0);
|
|
481
515
|
const runsLastPage = ref(1);
|
|
482
516
|
const runsWanted = ref(1);
|
|
517
|
+
const runsSort = ref(DEFAULT_RUN_SORT);
|
|
518
|
+
const runsDirection = ref(DEFAULT_RUN_DIRECTION);
|
|
483
519
|
let runsSeq = 0;
|
|
484
520
|
let runsLoadingSeq = 0;
|
|
485
521
|
const RUNS_POLL_MS = 3e3;
|
|
@@ -580,9 +616,11 @@ async function loadRuns(page = runsWanted.value, opts = {}) {
|
|
|
580
616
|
runsError.value = null;
|
|
581
617
|
try {
|
|
582
618
|
const res = await loadForWorkflow(workflow.value.id, workflow.value.name, {
|
|
583
|
-
reconcile:
|
|
619
|
+
reconcile: true,
|
|
584
620
|
page,
|
|
585
|
-
perPage: RUNS_PER_PAGE
|
|
621
|
+
perPage: RUNS_PER_PAGE,
|
|
622
|
+
sort: runsSort.value,
|
|
623
|
+
direction: runsDirection.value
|
|
586
624
|
});
|
|
587
625
|
if (seq !== runsSeq) return;
|
|
588
626
|
wfRuns.value = res.runs;
|
|
@@ -598,6 +636,11 @@ async function loadRuns(page = runsWanted.value, opts = {}) {
|
|
|
598
636
|
if (!opts.silent && seq === runsLoadingSeq) runsLoading.value = false;
|
|
599
637
|
}
|
|
600
638
|
}
|
|
639
|
+
function reorderRuns(order) {
|
|
640
|
+
runsSort.value = order.sort;
|
|
641
|
+
runsDirection.value = order.direction;
|
|
642
|
+
void loadRuns(1);
|
|
643
|
+
}
|
|
601
644
|
function goRunsPage(p) {
|
|
602
645
|
const next = Math.min(Math.max(1, p), runsLastPage.value);
|
|
603
646
|
if (next !== runsWanted.value) void loadRuns(next);
|
|
@@ -747,6 +790,7 @@ async function save() {
|
|
|
747
790
|
executionMode: d.executionMode
|
|
748
791
|
});
|
|
749
792
|
draft.value = null;
|
|
793
|
+
announceSaved();
|
|
750
794
|
settleBuildStatus();
|
|
751
795
|
} catch (e) {
|
|
752
796
|
const issues = apiValidationIssues(e);
|
|
@@ -880,7 +924,7 @@ async function confirmDeactivate() {
|
|
|
880
924
|
</template>
|
|
881
925
|
<template v-else>
|
|
882
926
|
<span
|
|
883
|
-
v-if="
|
|
927
|
+
v-if="buildChipShown"
|
|
884
928
|
class="inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-sky-500/25 bg-sky-500/10 px-2.5 text-sm text-sky-700"
|
|
885
929
|
:title="buildNotice?.text"
|
|
886
930
|
>
|
|
@@ -888,40 +932,15 @@ async function confirmDeactivate() {
|
|
|
888
932
|
{{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
|
|
889
933
|
</span>
|
|
890
934
|
<span v-if="isDirty" class="shrink-0 whitespace-nowrap text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
<DropdownMenu>
|
|
901
|
-
<DropdownMenuTrigger as-child>
|
|
902
|
-
<Button variant="outline" size="icon" class="size-7" :disabled="saving" :aria-label="t('integration.editor.action.more.aria')">
|
|
903
|
-
<MoreHorizontal :size="12" />
|
|
904
|
-
</Button>
|
|
905
|
-
</DropdownMenuTrigger>
|
|
906
|
-
<DropdownMenuContent align="end">
|
|
907
|
-
<DropdownMenuLabel>{{ t("integration.editor.action.more.label") }}</DropdownMenuLabel>
|
|
908
|
-
<DropdownMenuSeparator />
|
|
909
|
-
<DropdownMenuItem @select="duplicateOpen = true">
|
|
910
|
-
{{ t("integration.editor.action.duplicate.label") }}
|
|
911
|
-
</DropdownMenuItem>
|
|
912
|
-
<DropdownMenuItem @select="exporter?.start(workflow.id)">
|
|
913
|
-
{{ t("integration.workflowFile.action.export") }}
|
|
914
|
-
</DropdownMenuItem>
|
|
915
|
-
<DropdownMenuItem @select="importInput?.click()">
|
|
916
|
-
{{ t("integration.workflowFile.action.import") }}
|
|
917
|
-
</DropdownMenuItem>
|
|
918
|
-
<DropdownMenuSeparator />
|
|
919
|
-
<DropdownMenuItem class="text-destructive focus:text-destructive" @select="askDelete">
|
|
920
|
-
{{ t("integration.common.action.delete") }}
|
|
921
|
-
</DropdownMenuItem>
|
|
922
|
-
</DropdownMenuContent>
|
|
923
|
-
</DropdownMenu>
|
|
924
|
-
<Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" :title="t('integration.editor.action.discard.title')" @click="discardChanges">
|
|
935
|
+
<span v-else-if="justSaved" role="status" class="inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-sm text-emerald-700">
|
|
936
|
+
<CheckCircle :size="12" />
|
|
937
|
+
{{ t("integration.editor.status.saved") }}
|
|
938
|
+
</span>
|
|
939
|
+
<!-- Only while there is something to discard (PO-597). Save stays
|
|
940
|
+
put when disabled, so the row keeps its anchor; a Discard that can
|
|
941
|
+
never do anything was only noise beside it. The row is right-aligned,
|
|
942
|
+
so appearing here moves nothing the operator is about to press. -->
|
|
943
|
+
<Button v-if="isDirty" variant="outline" class="h-7 text-sm" :disabled="saving" :title="t('integration.editor.action.discard.title')" @click="discardChanges">
|
|
925
944
|
{{ t("integration.editor.action.discard.label") }}
|
|
926
945
|
</Button>
|
|
927
946
|
<Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
|
|
@@ -975,6 +994,42 @@ async function confirmDeactivate() {
|
|
|
975
994
|
{{ t("integration.editor.action.activate.title.invalid", validationWarnings?.length ?? 0) }}
|
|
976
995
|
</span>
|
|
977
996
|
</div>
|
|
997
|
+
<!-- What is done to the WORKFLOW, as opposed to the draft in front
|
|
998
|
+
of it: the buttons beside this menu discard and save an
|
|
999
|
+
afternoon's work, and these two make and unmake the thing that
|
|
1000
|
+
work belongs to. Two reasons they are a menu rather than two more
|
|
1001
|
+
buttons in a row of five. Deleting cannot be undone and has no
|
|
1002
|
+
business a mis-click away from Save; and a lone Delete under a
|
|
1003
|
+
Duplicate that stayed a button reads as an oversight rather than
|
|
1004
|
+
as a grouping — the menu only holds together while it holds both.
|
|
1005
|
+
Icon-only, so it carries a name of its own. Last in the row, where
|
|
1006
|
+
a menu of further actions is looked for: between the status text
|
|
1007
|
+
and Discard it split the row's two halves and read as a stray
|
|
1008
|
+
button (PO-597). -->
|
|
1009
|
+
<DropdownMenu>
|
|
1010
|
+
<DropdownMenuTrigger as-child>
|
|
1011
|
+
<Button variant="outline" size="icon" class="size-7" :disabled="saving" :aria-label="t('integration.editor.action.more.aria')">
|
|
1012
|
+
<MoreHorizontal :size="12" />
|
|
1013
|
+
</Button>
|
|
1014
|
+
</DropdownMenuTrigger>
|
|
1015
|
+
<DropdownMenuContent align="end">
|
|
1016
|
+
<DropdownMenuLabel>{{ t("integration.editor.action.more.label") }}</DropdownMenuLabel>
|
|
1017
|
+
<DropdownMenuSeparator />
|
|
1018
|
+
<DropdownMenuItem @select="duplicateOpen = true">
|
|
1019
|
+
{{ t("integration.editor.action.duplicate.label") }}
|
|
1020
|
+
</DropdownMenuItem>
|
|
1021
|
+
<DropdownMenuItem @select="exporter?.start(workflow.id)">
|
|
1022
|
+
{{ t("integration.workflowFile.action.export") }}
|
|
1023
|
+
</DropdownMenuItem>
|
|
1024
|
+
<DropdownMenuItem @select="importInput?.click()">
|
|
1025
|
+
{{ t("integration.workflowFile.action.import") }}
|
|
1026
|
+
</DropdownMenuItem>
|
|
1027
|
+
<DropdownMenuSeparator />
|
|
1028
|
+
<DropdownMenuItem class="text-destructive focus:text-destructive" @select="askDelete">
|
|
1029
|
+
{{ t("integration.common.action.delete") }}
|
|
1030
|
+
</DropdownMenuItem>
|
|
1031
|
+
</DropdownMenuContent>
|
|
1032
|
+
</DropdownMenu>
|
|
978
1033
|
</template>
|
|
979
1034
|
</template>
|
|
980
1035
|
|
|
@@ -1103,8 +1158,12 @@ async function confirmDeactivate() {
|
|
|
1103
1158
|
:loading="runsLoading"
|
|
1104
1159
|
:pagination="false"
|
|
1105
1160
|
:show-workflow="false"
|
|
1161
|
+
orderable
|
|
1162
|
+
:sort="runsSort"
|
|
1163
|
+
:direction="runsDirection"
|
|
1106
1164
|
:empty-text="t('integration.editor.runs.empty')"
|
|
1107
1165
|
@changed="() => loadRuns()"
|
|
1166
|
+
@reorder="reorderRuns"
|
|
1108
1167
|
/>
|
|
1109
1168
|
|
|
1110
1169
|
<!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
|
|
@@ -1226,6 +1285,7 @@ async function confirmDeactivate() {
|
|
|
1226
1285
|
:following="followingRun"
|
|
1227
1286
|
:error="syncError"
|
|
1228
1287
|
:run-id="runModalRunId"
|
|
1288
|
+
:refused="runRefused"
|
|
1229
1289
|
@update:open="(v) => syncOpen = v"
|
|
1230
1290
|
@view-run="viewRunFromModal"
|
|
1231
1291
|
/>
|
|
@@ -292,6 +292,10 @@ export interface ConfigField {
|
|
|
292
292
|
default?: unknown;
|
|
293
293
|
placeholder?: LocalizedString;
|
|
294
294
|
expressionAllowed?: boolean;
|
|
295
|
+
/** What a value mapped into this setting has to be (`array` for a list from an
|
|
296
|
+
* earlier step). The `{ }` list puts the outputs of that kind first and an
|
|
297
|
+
* empty setting offers a single one in a click (PO-594). */
|
|
298
|
+
accepts?: string;
|
|
295
299
|
/** The setting takes several lines — a mail body, a list of instructions one
|
|
296
300
|
* per line — so it is drawn with room for them. Read for `string` and
|
|
297
301
|
* `expression` fields; every other type ignores it (a dropdown has no
|
|
@@ -407,10 +411,15 @@ export interface NodeManifest {
|
|
|
407
411
|
*/
|
|
408
412
|
iteration?: boolean;
|
|
409
413
|
config?: ConfigField[];
|
|
410
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* Source package (editor-side enrichment from GET /nodes; not manifest data).
|
|
416
|
+
* `icon` is the glyph the package declares for its palette folder
|
|
417
|
+
* (`revenexx.icon`, a `lucide:<name>`; PO-640), null when it declares none.
|
|
418
|
+
*/
|
|
411
419
|
__package?: {
|
|
412
420
|
name: string;
|
|
413
421
|
label: string | null;
|
|
422
|
+
icon?: string | null;
|
|
414
423
|
};
|
|
415
424
|
/**
|
|
416
425
|
* Where this node stands in its package's own list of nodes, 0 first
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NodeManifest } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* The icon a palette folder directly under Integrations is marked by, when its
|
|
4
|
+
* package declares one (PO-640, `specs/node-marks.md` AC-4): `revenexx.icon` in
|
|
5
|
+
* the package's `package.json`, a `lucide:<name>`, served as `package.icon`.
|
|
6
|
+
*
|
|
7
|
+
* Declaring is optional. `null` means the folder keeps the mark it always had —
|
|
8
|
+
* the first logo among its nodes, else their first image (`groupLogoManifest`).
|
|
9
|
+
* So does a declaration the folder cannot use:
|
|
10
|
+
*
|
|
11
|
+
* - a folder several packages share counts only when every one of them declares
|
|
12
|
+
* the same icon — one that declares nothing, or another icon, leaves the folder
|
|
13
|
+
* its logo, so a package filing a node under a vendor's folder cannot take it over;
|
|
14
|
+
* - a name no Lucide glyph resolves — it would draw the generic `Box`, which says
|
|
15
|
+
* less than the logo it would replace.
|
|
16
|
+
*
|
|
17
|
+
* Within one package any node may carry the declaration: a node a newer release
|
|
18
|
+
* dropped is still served from the older release, whose package declared nothing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function declaredFolderIcon(manifests: NodeManifest[]): string | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolveNodeIcon } from "../data/nodeIcons.js";
|
|
2
|
+
export function declaredFolderIcon(manifests) {
|
|
3
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
4
|
+
for (const m of manifests) {
|
|
5
|
+
const name = m.__package?.name ?? "";
|
|
6
|
+
byPackage.set(name, byPackage.get(name) || m.__package?.icon || null);
|
|
7
|
+
}
|
|
8
|
+
const declared = new Set(byPackage.values());
|
|
9
|
+
if (declared.size !== 1) return null;
|
|
10
|
+
const [icon] = declared;
|
|
11
|
+
return icon && resolveNodeIcon(icon) ? icon : null;
|
|
12
|
+
}
|
|
@@ -72,7 +72,8 @@ export declare function acceptsResume(apiStatus: string | null | undefined, stat
|
|
|
72
72
|
*/
|
|
73
73
|
export declare function apiStatusesFor(words: readonly RunStatus[]): string[];
|
|
74
74
|
/**
|
|
75
|
-
* The orders this studio can ask `GET /runs` for
|
|
75
|
+
* The orders this studio can ask `GET /runs` for — and a workflow's own
|
|
76
|
+
* `GET /workflows/{id}/runs`, which takes the same names (PO-385).
|
|
76
77
|
*
|
|
77
78
|
* Three of the four `RunSort` names the service accepts. Two absences are the
|
|
78
79
|
* platform's: ordering by the workflow's name would need a join on the paging
|
|
@@ -121,6 +122,13 @@ export interface RunFeedQuery {
|
|
|
121
122
|
* empty `status` as a value and answers 422.
|
|
122
123
|
*/
|
|
123
124
|
export declare function runFeedQueryString(query: RunFeedQuery): string;
|
|
125
|
+
/**
|
|
126
|
+
* The query string for a workflow's own `GET /workflows/{id}/runs`: a page, and
|
|
127
|
+
* the order it is read in. That endpoint narrows by nothing yet (PO-319), so this
|
|
128
|
+
* is the paging and the order alone — the order sent by the same rule as the
|
|
129
|
+
* tenant feed's, so the two lists can never ask for one order two ways.
|
|
130
|
+
*/
|
|
131
|
+
export declare function workflowRunsQueryString(page: number, perPage: number, sort?: RunSort, direction?: RunSortDirection): string;
|
|
124
132
|
/**
|
|
125
133
|
* The narrowing alone, without the paging — what `GET /runs/summary` is read
|
|
126
134
|
* with so the pager's total describes the same rows as the page beside it.
|
|
@@ -26,12 +26,22 @@ export function runFeedQueryString(query) {
|
|
|
26
26
|
params.set("page", String(query.page));
|
|
27
27
|
params.set("per_page", String(query.perPage));
|
|
28
28
|
for (const [key, value] of narrowingParams(query)) params.set(key, value);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
setOrder(params, query.sort, query.direction);
|
|
30
|
+
return params.toString();
|
|
31
|
+
}
|
|
32
|
+
export function workflowRunsQueryString(page, perPage, sort = DEFAULT_RUN_SORT, direction = DEFAULT_RUN_DIRECTION) {
|
|
33
|
+
const params = new URLSearchParams();
|
|
34
|
+
params.set("page", String(page));
|
|
35
|
+
params.set("per_page", String(perPage));
|
|
36
|
+
setOrder(params, sort, direction);
|
|
33
37
|
return params.toString();
|
|
34
38
|
}
|
|
39
|
+
function setOrder(params, sort, direction) {
|
|
40
|
+
if (sort !== DEFAULT_RUN_SORT || direction !== DEFAULT_RUN_DIRECTION) {
|
|
41
|
+
params.set("sort", sort);
|
|
42
|
+
params.set("direction", direction);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
35
45
|
export function runFeedNarrowingString(query) {
|
|
36
46
|
return narrowingParams(query).toString();
|
|
37
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-integrations",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
|
|
6
6
|
"repository": {
|