@revenexx/studio-integrations 1.2.0 → 1.3.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 +155 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue +65 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.vue +103 -25
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -2
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
- package/dist/runtime/composables/useIntegrationsTemplates.js +5 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.js +6 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -0
- package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
- package/dist/runtime/data/nodeVersionMove.js +65 -0
- package/dist/runtime/i18n/messages/de.json +53 -4
- package/dist/runtime/i18n/messages/en.json +53 -4
- package/dist/runtime/pages/integrations/workflows/[id].vue +30 -20
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
- package/dist/runtime/types/integrations.d.ts +44 -1
- package/dist/runtime/utils/buildSettle.d.ts +68 -0
- package/dist/runtime/utils/buildSettle.js +29 -0
- package/dist/runtime/utils/settingCondition.d.ts +48 -0
- package/dist/runtime/utils/settingCondition.js +84 -0
- package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
- package/dist/runtime/utils/stateNamespaceName.js +8 -0
- package/package.json +1 -1
|
@@ -70,6 +70,11 @@ function applyDetail(base, api) {
|
|
|
70
70
|
edges: def.edges ?? [],
|
|
71
71
|
triggerPositions: def.triggerPositions ?? {},
|
|
72
72
|
variables: def.variables,
|
|
73
|
+
// Enumerated key by key like every block above it, so one missing here is
|
|
74
|
+
// one lost in this direction. Harmless today — a template becomes a workflow
|
|
75
|
+
// server-side, so this blob only feeds the preview — but the preview is
|
|
76
|
+
// where an author would look to see what a template remembers.
|
|
77
|
+
state: def.state,
|
|
73
78
|
triggers
|
|
74
79
|
};
|
|
75
80
|
const md = resolveLocalized(api.description ?? "") || base.descriptionMd;
|
|
@@ -20,6 +20,7 @@ function toCockpit(api) {
|
|
|
20
20
|
nodes: apiBlob.nodes ?? [],
|
|
21
21
|
edges: apiBlob.edges ?? [],
|
|
22
22
|
variables: apiBlob.variables ?? void 0,
|
|
23
|
+
state: apiBlob.state ?? void 0,
|
|
23
24
|
triggerPositions: apiBlob.triggerPositions ?? {},
|
|
24
25
|
stickies: apiBlob.stickies ?? [],
|
|
25
26
|
groups: apiBlob.groups ?? [],
|
|
@@ -64,6 +65,11 @@ function toPayload(meta) {
|
|
|
64
65
|
...b.groups?.length ? { groups: b.groups } : {},
|
|
65
66
|
// variables were silently dropped before — the schema always allowed them.
|
|
66
67
|
...b.variables && Object.keys(b.variables).length ? { variables: b.variables } : {},
|
|
68
|
+
// Same shape, and the same trap: both mappers enumerate blob keys, so a
|
|
69
|
+
// block missing from either one is lost in that direction. `state` is the
|
|
70
|
+
// declaration that makes the run grant reachable at all (PO-374) — losing
|
|
71
|
+
// it on save would take the workflow's memory away without a message.
|
|
72
|
+
...b.state?.length ? { state: b.state } : {},
|
|
67
73
|
...b.tags?.length ? { tags: b.tags } : {},
|
|
68
74
|
...b.icon ? { icon: b.icon } : {}
|
|
69
75
|
},
|
|
@@ -81,6 +81,31 @@ export declare function isVersionExecutable(slug: string, version: string): bool
|
|
|
81
81
|
* only what is wrong.
|
|
82
82
|
*/
|
|
83
83
|
export declare function newestExecutableVersion(slug: string): string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Every version of this node the platform can execute, newest first — the
|
|
86
|
+
* choice offered when a step is moved onto another version (PO-339).
|
|
87
|
+
*
|
|
88
|
+
* Order is the catalogue's own: the registry returns each slug's versions
|
|
89
|
+
* semver-descending, which is the assumption `newestExecutableVersion` above and
|
|
90
|
+
* the palette's own list already rest on. So "newer than the pinned one" is
|
|
91
|
+
* "earlier in this array", and nothing here has to parse a semver.
|
|
92
|
+
*
|
|
93
|
+
* A version whose code was never built is left out, for the reason the palette
|
|
94
|
+
* leaves it out: it is not a choice, it is a dead end with a version number
|
|
95
|
+
* (PO-338). The version a step is pinned to is therefore not guaranteed to be in
|
|
96
|
+
* here — which is exactly the state the move exists to get out of.
|
|
97
|
+
*/
|
|
98
|
+
export declare function executableVersionsOf(slug: string): NodeManifest[];
|
|
99
|
+
/**
|
|
100
|
+
* The versions of this node that are newer than the one pinned, and runnable.
|
|
101
|
+
*
|
|
102
|
+
* Empty when the step is already on the newest — which is the common case, and
|
|
103
|
+
* the reason the inspector can stay silent rather than telling most authors that
|
|
104
|
+
* their node is fine. A pinned version the catalogue does not carry counts as
|
|
105
|
+
* "nothing newer": there is no position to be newer *than*, and inventing one
|
|
106
|
+
* would put an upgrade notice on a node whose real problem is different.
|
|
107
|
+
*/
|
|
108
|
+
export declare function newerExecutableVersions(slug: string, version: string): NodeManifest[];
|
|
84
109
|
export declare function findManifest(slug: string, version?: string): NodeManifest | null;
|
|
85
110
|
export declare const TRIGGER_OUTPUT_PORT = "default";
|
|
86
111
|
export interface TriggerTemplate {
|
|
@@ -34,6 +34,14 @@ export function newestExecutableVersion(slug) {
|
|
|
34
34
|
const hit = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && m.__hasBundle !== false);
|
|
35
35
|
return hit?.version ?? null;
|
|
36
36
|
}
|
|
37
|
+
export function executableVersionsOf(slug) {
|
|
38
|
+
return INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug && m.__hasBundle !== false);
|
|
39
|
+
}
|
|
40
|
+
export function newerExecutableVersions(slug, version) {
|
|
41
|
+
const versions = executableVersionsOf(slug);
|
|
42
|
+
const at = versions.findIndex((m) => m.version === version);
|
|
43
|
+
return at < 0 ? [] : versions.slice(0, at);
|
|
44
|
+
}
|
|
37
45
|
export function findManifest(slug, version) {
|
|
38
46
|
const matches = INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug);
|
|
39
47
|
if (!matches.length) return null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ConfigField, NodeManifest } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* What moving a step onto another version of its node costs (PO-339).
|
|
4
|
+
*
|
|
5
|
+
* A version is chosen when a node is inserted and, until this, never again — so
|
|
6
|
+
* the only route onto another one was to delete the step and build it back,
|
|
7
|
+
* losing its settings, its name, its note and its edges. Moving instead is
|
|
8
|
+
* cheap in the blob (one field changes) and expensive in trust: the two
|
|
9
|
+
* versions may not declare the same settings, and an author who is not told
|
|
10
|
+
* beforehand finds out by a value having quietly gone.
|
|
11
|
+
*
|
|
12
|
+
* This module answers "what would the move cost" and nothing else. It builds no
|
|
13
|
+
* words: labels are localised strings on the manifest, so the caller resolves
|
|
14
|
+
* them and this stays testable without an i18n context.
|
|
15
|
+
*
|
|
16
|
+
* **A value that is not there is not a loss.** Only settings the node actually
|
|
17
|
+
* holds are weighed. A field the author never filled has nothing to carry, drop
|
|
18
|
+
* or check, and listing it would bury the two or three that matter.
|
|
19
|
+
*
|
|
20
|
+
* **The comparison is over the config's own top-level keys**, which is what
|
|
21
|
+
* `nextConfig` can act on: the blob stores settings as one flat object, and
|
|
22
|
+
* removing `attachments` is removing one key from it. A version that changed
|
|
23
|
+
* something *inside* an object or an array element is caught as far as it can
|
|
24
|
+
* be — such a field lands in `review` rather than being silently carried — but
|
|
25
|
+
* the stale value inside it is not pruned. `specs/node-configuration.md`
|
|
26
|
+
* records that limit rather than this pretending to close it.
|
|
27
|
+
*/
|
|
28
|
+
/** A setting the move has something to say about, with the field it was read from. */
|
|
29
|
+
export interface MoveEntry {
|
|
30
|
+
key: string;
|
|
31
|
+
/**
|
|
32
|
+
* The declaration to name it by. For a carried or reviewed setting that is the
|
|
33
|
+
* TARGET's field, because that is what the author will be looking at after the
|
|
34
|
+
* move; for a dropped one it is the SOURCE's, which is the only version that
|
|
35
|
+
* still has a word for it. `null` where neither declares it — a key left behind
|
|
36
|
+
* by an earlier move, which has no label anywhere and shows as its own name.
|
|
37
|
+
*/
|
|
38
|
+
field: ConfigField | null;
|
|
39
|
+
/** Why it needs looking at. Only set on `review`. */
|
|
40
|
+
reason?: 'changed' | 'nowRequired';
|
|
41
|
+
}
|
|
42
|
+
export interface VersionMovePlan {
|
|
43
|
+
/** Settings that travel untouched: the target declares the same key, the same way. */
|
|
44
|
+
carried: MoveEntry[];
|
|
45
|
+
/** Settings the target does not declare. Their values are gone from `nextConfig`. */
|
|
46
|
+
dropped: MoveEntry[];
|
|
47
|
+
/** Settings that arrive but need the author's eye — see `reason`. Values are kept. */
|
|
48
|
+
review: MoveEntry[];
|
|
49
|
+
/** The config to save with the new version: the current one minus what was dropped. */
|
|
50
|
+
nextConfig: Record<string, unknown>;
|
|
51
|
+
/** Nothing is dropped and nothing needs checking — the move is a version change and no more. */
|
|
52
|
+
lossless: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the version being left is still in the catalogue, so the two
|
|
55
|
+
* declarations could actually be held against each other. `false` for a
|
|
56
|
+
* withdrawn version: the move still works and nothing here is wrong, but
|
|
57
|
+
* `carried` then means "kept" rather than "checked and unchanged", and the
|
|
58
|
+
* dialog owes the author that sentence.
|
|
59
|
+
*/
|
|
60
|
+
comparable: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* What it would cost to move a step from `from` to `to`, given the settings it
|
|
64
|
+
* holds today.
|
|
65
|
+
*
|
|
66
|
+
* Either manifest may be `null` — a version the catalogue does not carry, which
|
|
67
|
+
* happens to a workflow pinned to something later withdrawn. A missing SOURCE
|
|
68
|
+
* only costs labels; a missing TARGET would drop every setting, so the caller
|
|
69
|
+
* must not offer a move onto a version it cannot find, and `planVersionMove`
|
|
70
|
+
* does not defend against being asked anyway.
|
|
71
|
+
*/
|
|
72
|
+
export declare function planVersionMove(from: NodeManifest | null, to: NodeManifest | null, config: Record<string, unknown> | null | undefined): VersionMovePlan;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function hasValue(config, key) {
|
|
2
|
+
if (!(key in config)) return false;
|
|
3
|
+
const v = config[key];
|
|
4
|
+
return v !== void 0 && v !== null && v !== "";
|
|
5
|
+
}
|
|
6
|
+
function byKey(manifest) {
|
|
7
|
+
const map = /* @__PURE__ */ new Map();
|
|
8
|
+
for (const f of manifest?.config ?? []) {
|
|
9
|
+
if (f && typeof f.key === "string" && f.key) map.set(f.key, f);
|
|
10
|
+
}
|
|
11
|
+
return map;
|
|
12
|
+
}
|
|
13
|
+
function shapeOf(field) {
|
|
14
|
+
if (!field) return "";
|
|
15
|
+
return JSON.stringify([
|
|
16
|
+
field.type,
|
|
17
|
+
(field.options ?? []).map((o) => o.value),
|
|
18
|
+
(field.properties ?? []).map((p) => [p.key, p.type]),
|
|
19
|
+
field.items ? [field.items.key, field.items.type, (field.items.properties ?? []).map((p) => [p.key, p.type])] : null
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
export function planVersionMove(from, to, config) {
|
|
23
|
+
const current = config ?? {};
|
|
24
|
+
const sourceFields = byKey(from);
|
|
25
|
+
const targetFields = byKey(to);
|
|
26
|
+
const targetHasDynamicSchema = (to?.config ?? []).some((f) => f?.type === "dynamic-schema");
|
|
27
|
+
const carried = [];
|
|
28
|
+
const dropped = [];
|
|
29
|
+
const review = [];
|
|
30
|
+
const nextConfig = {};
|
|
31
|
+
for (const key of Object.keys(current)) {
|
|
32
|
+
if (!hasValue(current, key)) {
|
|
33
|
+
nextConfig[key] = current[key];
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const target = targetFields.get(key);
|
|
37
|
+
if (!target) {
|
|
38
|
+
if (targetHasDynamicSchema) {
|
|
39
|
+
nextConfig[key] = current[key];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
dropped.push({ key, field: sourceFields.get(key) ?? null });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
nextConfig[key] = current[key];
|
|
46
|
+
const source = sourceFields.get(key);
|
|
47
|
+
if (!source || shapeOf(source) === shapeOf(target)) carried.push({ key, field: target });
|
|
48
|
+
else review.push({ key, field: target, reason: "changed" });
|
|
49
|
+
}
|
|
50
|
+
for (const [key, field] of targetFields) {
|
|
51
|
+
if (field.default !== void 0 && !(key in current)) nextConfig[key] = field.default;
|
|
52
|
+
if (field.required !== true) continue;
|
|
53
|
+
if (field.default !== void 0) continue;
|
|
54
|
+
if (hasValue(current, key)) continue;
|
|
55
|
+
review.push({ key, field, reason: "nowRequired" });
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
carried,
|
|
59
|
+
dropped,
|
|
60
|
+
review,
|
|
61
|
+
nextConfig,
|
|
62
|
+
lossless: dropped.length === 0 && review.length === 0,
|
|
63
|
+
comparable: from !== null && from !== void 0
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
"integration.canvas.node.action.duplicate": "Duplizieren",
|
|
72
72
|
"integration.canvas.node.action.duplicateAria": "Node duplizieren",
|
|
73
73
|
"integration.canvas.node.badge.invalidConfig": "Konfiguration unvollständig — {n} Einstellung muss noch behoben werden: {fields} | Konfiguration unvollständig — {n} Einstellungen müssen noch behoben werden: {fields}",
|
|
74
|
+
"integration.canvas.node.badge.loopBody": "Schleife — am Ausgang „Element“ hängt noch nichts, es wiederholt sich also nichts | Schleife — wiederholt den einen Schritt am Ausgang „Element“, einmal pro Element | Schleife — wiederholt die {n} Schritte am Ausgang „Element“, einmal pro Element",
|
|
74
75
|
"integration.canvas.node.badge.noBuiltCode": "Version {version} ist nicht einsatzbereit — dieser Schritt lässt sich nicht testen",
|
|
75
76
|
"integration.canvas.node.badge.repeats": "Wiederholt sich — läuft einmal pro Element von {iterators} | Wiederholt sich — läuft einmal pro Element jeder der {n} Schleifen, in denen er liegt: {iterators}",
|
|
76
|
-
"integration.canvas.node.badge.loopBody": "Schleife — am Ausgang „Element“ hängt noch nichts, es wiederholt sich also nichts | Schleife — wiederholt den einen Schritt am Ausgang „Element“, einmal pro Element | Schleife — wiederholt die {n} Schritte am Ausgang „Element“, einmal pro Element",
|
|
77
77
|
"integration.canvas.node.handle.input": "Eingang",
|
|
78
78
|
"integration.canvas.node.meta.versionTitle": "Version {version}",
|
|
79
79
|
"integration.canvas.node.note.heading": "Notiz",
|
|
@@ -455,6 +455,8 @@
|
|
|
455
455
|
"integration.editor.tool.select": "Auswählen",
|
|
456
456
|
"integration.editor.tool.settings.label": "Einstellungen",
|
|
457
457
|
"integration.editor.tool.settings.tooltip": "Workflow-Einstellungen",
|
|
458
|
+
"integration.editor.tool.state.label": "Zustand",
|
|
459
|
+
"integration.editor.tool.state.tooltip": "Was sich dieser Workflow zwischen Läufen merkt",
|
|
458
460
|
"integration.editor.tool.tidyUp": "Aufräumen",
|
|
459
461
|
"integration.editor.tool.variables.label": "Variablen",
|
|
460
462
|
"integration.editor.tool.variables.tooltip": "Workflow-Variablen",
|
|
@@ -529,8 +531,8 @@
|
|
|
529
531
|
"integration.nodeConfig.dynamicSchema.reloading": "Felder werden neu geladen…",
|
|
530
532
|
"integration.nodeConfig.dynamicSchema.resolving": "Felder werden geladen…",
|
|
531
533
|
"integration.nodeConfig.expression.badge.title": "Akzeptiert ${{ … }}-Ausdrücke",
|
|
532
|
-
"integration.nodeConfig.expression.insertTitle": "Ausdruck einfügen",
|
|
533
534
|
"integration.nodeConfig.expression.insertAria": "Ausdruck einfügen — verfügbare Daten für diese Einstellung",
|
|
535
|
+
"integration.nodeConfig.expression.insertTitle": "Ausdruck einfügen",
|
|
534
536
|
"integration.nodeConfig.expression.picker.aria": "Verfügbare Daten für diese Einstellung",
|
|
535
537
|
"integration.nodeConfig.expression.picker.empty": "Nichts entspricht „{query}“.",
|
|
536
538
|
"integration.nodeConfig.expression.picker.filterAria": "Verfügbare Daten filtern",
|
|
@@ -596,6 +598,11 @@
|
|
|
596
598
|
"integration.nodeConfig.resolve.error.generic": "Auflösung fehlgeschlagen",
|
|
597
599
|
"integration.nodeConfig.secretRef.placeholder": "Secret auswählen",
|
|
598
600
|
"integration.nodeConfig.select.placeholder": "Auswählen",
|
|
601
|
+
"integration.nodeConfig.state.action.new": "Neuen Namespace deklarieren…",
|
|
602
|
+
"integration.nodeConfig.state.create.description": "Er wird den Deklarationen dieses Workflows als {role}-Namespace hinzugefügt und hier ausgewählt.",
|
|
603
|
+
"integration.nodeConfig.state.create.title": "Zustands-Namespace deklarieren",
|
|
604
|
+
"integration.nodeConfig.state.empty": "Dieser Workflow deklariert davon noch keinen.",
|
|
605
|
+
"integration.nodeConfig.state.placeholder": "Namespace wählen…",
|
|
599
606
|
"integration.nodeConfig.validation.max": "Muss ≤ {max} sein",
|
|
600
607
|
"integration.nodeConfig.validation.maxLength": "Darf höchstens {max} Zeichen lang sein",
|
|
601
608
|
"integration.nodeConfig.validation.min": "Muss ≥ {min} sein",
|
|
@@ -641,9 +648,9 @@
|
|
|
641
648
|
"integration.nodeInspector.notice.versionNotReady.upgrade": "Version {version} dieses Nodes ist nicht mehr einsatzbereit. Um den Node weiter zu verwenden, ist ein Upgrade auf Version {ready} notwendig.",
|
|
642
649
|
"integration.nodeInspector.output.description": "Was nachgelagerte Nodes von diesem erhalten. Deklariert vom Node-Typ; wird mit echten Werten gefüllt, sobald der Schritt gelaufen ist.",
|
|
643
650
|
"integration.nodeInspector.output.loopBody.heading": "Der Schleifenkörper",
|
|
644
|
-
"integration.nodeInspector.output.loopBody.size": "Am Ausgang „Element“ hängt noch nichts, diese Schleife wiederholt also nichts. | Ein Schritt hängt am Ausgang „Element“ und läuft einmal pro Element. | {n} Schritte hängen am Ausgang „Element“ und laufen jeder einmal pro Element.",
|
|
645
|
-
"integration.nodeInspector.output.loopBody.names": "Diese wiederholen sich: {names}",
|
|
646
651
|
"integration.nodeInspector.output.loopBody.joined": "Einer davon hängt zugleich hinter der Schleife, am Ausgang „Fertig“ — er wiederholt sich also mit dem Schleifenkörper, statt einmal zu laufen: {names} | {n} davon hängen zugleich hinter der Schleife, am Ausgang „Fertig“ — sie wiederholen sich also mit dem Schleifenkörper, statt einmal zu laufen: {names}",
|
|
652
|
+
"integration.nodeInspector.output.loopBody.names": "Diese wiederholen sich: {names}",
|
|
653
|
+
"integration.nodeInspector.output.loopBody.size": "Am Ausgang „Element“ hängt noch nichts, diese Schleife wiederholt also nichts. | Ein Schritt hängt am Ausgang „Element“ und läuft einmal pro Element. | {n} Schritte hängen am Ausgang „Element“ und laufen jeder einmal pro Element.",
|
|
647
654
|
"integration.nodeInspector.output.pin.editAction": "Angeheftetes Beispiel bearbeiten",
|
|
648
655
|
"integration.nodeInspector.output.pin.editorAria": "Angeheftete Beispielausgabe",
|
|
649
656
|
"integration.nodeInspector.output.pin.hint": "Ein angeheftetes Beispiel steht anstelle von Run-Daten, während Sie nachgelagerte Felder zuordnen.",
|
|
@@ -800,6 +807,20 @@
|
|
|
800
807
|
"integration.nodeInspector.trigger.webhook.username.hint": "Optional — legt den Basic-Auth-Benutzernamen fest. Das Passwort ist das Endpoint-Secret.",
|
|
801
808
|
"integration.nodeInspector.trigger.webhook.username.label": "Benutzername",
|
|
802
809
|
"integration.nodeInspector.trigger.webhook.username.placeholder": "Beliebiger Benutzername",
|
|
810
|
+
"integration.nodeInspector.version.available": "Version {version} ist verfügbar.",
|
|
811
|
+
"integration.nodeInspector.version.change": "Version wechseln",
|
|
812
|
+
"integration.nodeInspector.version.move.confirm": "Version wechseln",
|
|
813
|
+
"integration.nodeInspector.version.move.description": "Der Schritt behält seine Verbindungen, seinen Namen und seine Notiz. Es ändert sich die Version — und jede Einstellung, die es in der gewählten Version nicht gibt.",
|
|
814
|
+
"integration.nodeInspector.version.move.dropped": "Wird entfernt",
|
|
815
|
+
"integration.nodeInspector.version.move.lossless": "Alle Einstellungen kommen unverändert mit.",
|
|
816
|
+
"integration.nodeInspector.version.move.notComparable": "Version {version} liegt nicht mehr im Katalog, deshalb lässt sich nicht vergleichen, was sich zwischen beiden unterscheidet. Unten steht, was ankommt — nicht, was geprüft wurde.",
|
|
817
|
+
"integration.nodeInspector.version.move.reason.changed": "hat sich zwischen den Versionen geändert",
|
|
818
|
+
"integration.nodeInspector.version.move.reason.nowRequired": "erforderlich und noch nicht ausgefüllt",
|
|
819
|
+
"integration.nodeInspector.version.move.review": "Danach prüfen",
|
|
820
|
+
"integration.nodeInspector.version.move.same": "Auf dieser Version steht der Schritt bereits.",
|
|
821
|
+
"integration.nodeInspector.version.move.targetAria": "Version, auf die dieser Schritt wechselt",
|
|
822
|
+
"integration.nodeInspector.version.move.title": "Version dieses Schritts wechseln",
|
|
823
|
+
"integration.nodeInspector.version.moveTo": "Auf Version {version} wechseln",
|
|
803
824
|
"integration.nodes.category.action": "Aktionen",
|
|
804
825
|
"integration.nodes.category.control": "Ablaufsteuerung",
|
|
805
826
|
"integration.nodes.category.io": "I/O",
|
|
@@ -1188,6 +1209,34 @@
|
|
|
1188
1209
|
"integration.workflows.settings.section.appearance": "Aussehen",
|
|
1189
1210
|
"integration.workflows.settings.section.execution": "Ausführung",
|
|
1190
1211
|
"integration.workflows.settings.section.metadata": "Metadaten",
|
|
1212
|
+
"integration.workflows.state.action.add": "Deklarieren",
|
|
1213
|
+
"integration.workflows.state.action.done": "Fertig",
|
|
1214
|
+
"integration.workflows.state.action.remove": "Deklaration entfernen",
|
|
1215
|
+
"integration.workflows.state.action.remove.hint": "Das Entfernen nimmt diesem Workflow den Zugriff. Die gespeicherten Einträge bleiben bestehen.",
|
|
1216
|
+
"integration.workflows.state.dialog.description": "Was sich dieser Workflow zwischen Läufen merken darf. Er erreicht ausschließlich die hier deklarierten Namespaces — alles andere wird beim Lauf abgewiesen.",
|
|
1217
|
+
"integration.workflows.state.dialog.title": "Zustands-Namespaces",
|
|
1218
|
+
"integration.workflows.state.error.invalidName": "Kleinbuchstaben, Ziffern und . - _, beginnend mit einem Buchstaben — zum Beispiel article oder crm.customers.",
|
|
1219
|
+
"integration.workflows.state.error.nameExists": "Dieser Workflow deklariert diesen Namespace bereits.",
|
|
1220
|
+
"integration.workflows.state.field.newName.label": "Namespace deklarieren",
|
|
1221
|
+
"integration.workflows.state.field.newName.placeholder": "article",
|
|
1222
|
+
"integration.workflows.state.field.newRole.label": "Rolle",
|
|
1223
|
+
"integration.workflows.state.field.newVisibility.label": "Sichtbar für",
|
|
1224
|
+
"integration.workflows.state.field.visibility.label": "Sichtbar für",
|
|
1225
|
+
"integration.workflows.state.role.cursor.hint": "Merkt sich, wie weit ein inkrementeller Import gelesen hat. Es rückt nur vor, wenn der Lauf sauber endet.",
|
|
1226
|
+
"integration.workflows.state.role.cursor.label": "Lesezeichen",
|
|
1227
|
+
"integration.workflows.state.role.dedupe.hint": "Markiert Verarbeitetes, damit eine wiederholte Zustellung nicht doppelt gebucht wird.",
|
|
1228
|
+
"integration.workflows.state.role.dedupe.label": "Dublettenschutz",
|
|
1229
|
+
"integration.workflows.state.role.digest.hint": "Merkt sich den zuletzt geschriebenen Stand, damit unveränderte Datensätze übersprungen werden.",
|
|
1230
|
+
"integration.workflows.state.role.digest.label": "Prüfsumme",
|
|
1231
|
+
"integration.workflows.state.role.fixed": "Die Rolle steht mit der Deklaration fest: Einträge, die mit einer Bedeutung geschrieben wurden, lassen sich nicht mit einer anderen lesen.",
|
|
1232
|
+
"integration.workflows.state.role.mapping.hint": "Verknüpft je eine ID auf beiden Seiten, damit ein späterer Lauf weiß, ob er anlegen oder aktualisieren muss.",
|
|
1233
|
+
"integration.workflows.state.role.mapping.label": "Zuordnung",
|
|
1234
|
+
"integration.workflows.state.row.aria": "Zustands-Namespace {namespace}",
|
|
1235
|
+
"integration.workflows.state.empty": "Dieser Workflow merkt sich noch nichts zwischen Läufen.",
|
|
1236
|
+
"integration.workflows.state.visibility.private.hint": "Nur dieser Workflow liest und schreibt ihn, und er wird mit dem Workflow gelöscht.",
|
|
1237
|
+
"integration.workflows.state.visibility.private.label": "Diesen Workflow",
|
|
1238
|
+
"integration.workflows.state.visibility.shared.hint": "Jeder Workflow dieses Mandanten liest und schreibt diese Einträge, und der Namespace bleibt bestehen, wenn dieser Workflow gelöscht wird.",
|
|
1239
|
+
"integration.workflows.state.visibility.shared.label": "Alle Workflows",
|
|
1191
1240
|
"integration.workflows.table.action.groupByTags": "Nach Tags gruppieren",
|
|
1192
1241
|
"integration.workflows.table.column.build": "Build",
|
|
1193
1242
|
"integration.workflows.table.column.lastRun": "Letzter Run",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
"integration.canvas.node.action.duplicate": "Duplicate",
|
|
72
72
|
"integration.canvas.node.action.duplicateAria": "Duplicate node",
|
|
73
73
|
"integration.canvas.node.badge.invalidConfig": "Config incomplete — {n} setting still needs fixing: {fields} | Config incomplete — {n} settings still need fixing: {fields}",
|
|
74
|
+
"integration.canvas.node.badge.loopBody": "Loop — nothing is wired to Item yet, so nothing repeats | Loop — repeats the one step wired to Item, once per element | Loop — repeats the {n} steps wired to Item, once per element",
|
|
74
75
|
"integration.canvas.node.badge.noBuiltCode": "Version {version} is not ready to use — this step cannot be tested",
|
|
75
76
|
"integration.canvas.node.badge.repeats": "Repeats — runs once per element of {iterators} | Repeats — runs once per element of each of the {n} loops it sits inside: {iterators}",
|
|
76
|
-
"integration.canvas.node.badge.loopBody": "Loop — nothing is wired to Item yet, so nothing repeats | Loop — repeats the one step wired to Item, once per element | Loop — repeats the {n} steps wired to Item, once per element",
|
|
77
77
|
"integration.canvas.node.handle.input": "Input",
|
|
78
78
|
"integration.canvas.node.meta.versionTitle": "Version {version}",
|
|
79
79
|
"integration.canvas.node.note.heading": "Note",
|
|
@@ -455,6 +455,8 @@
|
|
|
455
455
|
"integration.editor.tool.select": "Select",
|
|
456
456
|
"integration.editor.tool.settings.label": "Settings",
|
|
457
457
|
"integration.editor.tool.settings.tooltip": "Workflow settings",
|
|
458
|
+
"integration.editor.tool.state.label": "State",
|
|
459
|
+
"integration.editor.tool.state.tooltip": "What this workflow remembers between runs",
|
|
458
460
|
"integration.editor.tool.tidyUp": "Tidy up",
|
|
459
461
|
"integration.editor.tool.variables.label": "Variables",
|
|
460
462
|
"integration.editor.tool.variables.tooltip": "Workflow variables",
|
|
@@ -529,8 +531,8 @@
|
|
|
529
531
|
"integration.nodeConfig.dynamicSchema.reloading": "reloading fields…",
|
|
530
532
|
"integration.nodeConfig.dynamicSchema.resolving": "Resolving fields…",
|
|
531
533
|
"integration.nodeConfig.expression.badge.title": "Accepts ${{ … }} expressions",
|
|
532
|
-
"integration.nodeConfig.expression.insertTitle": "Insert expression",
|
|
533
534
|
"integration.nodeConfig.expression.insertAria": "Insert expression — available data for this setting",
|
|
535
|
+
"integration.nodeConfig.expression.insertTitle": "Insert expression",
|
|
534
536
|
"integration.nodeConfig.expression.picker.aria": "Available data for this setting",
|
|
535
537
|
"integration.nodeConfig.expression.picker.empty": "Nothing matches “{query}”.",
|
|
536
538
|
"integration.nodeConfig.expression.picker.filterAria": "Filter available data",
|
|
@@ -596,6 +598,11 @@
|
|
|
596
598
|
"integration.nodeConfig.resolve.error.generic": "Resolve failed",
|
|
597
599
|
"integration.nodeConfig.secretRef.placeholder": "Pick a secret",
|
|
598
600
|
"integration.nodeConfig.select.placeholder": "Pick one",
|
|
601
|
+
"integration.nodeConfig.state.action.new": "Declare a new namespace…",
|
|
602
|
+
"integration.nodeConfig.state.create.description": "It is added to this workflow's declarations as a {role} namespace and selected here.",
|
|
603
|
+
"integration.nodeConfig.state.create.title": "Declare a state namespace",
|
|
604
|
+
"integration.nodeConfig.state.empty": "This workflow declares none of this kind yet.",
|
|
605
|
+
"integration.nodeConfig.state.placeholder": "Choose a namespace…",
|
|
599
606
|
"integration.nodeConfig.validation.max": "Must be ≤ {max}",
|
|
600
607
|
"integration.nodeConfig.validation.maxLength": "Must be at most {max} characters",
|
|
601
608
|
"integration.nodeConfig.validation.min": "Must be ≥ {min}",
|
|
@@ -641,9 +648,9 @@
|
|
|
641
648
|
"integration.nodeInspector.notice.versionNotReady.upgrade": "Version {version} of this node is no longer usable. To keep using this step, it has to be upgraded to version {ready}.",
|
|
642
649
|
"integration.nodeInspector.output.description": "What downstream nodes receive from this one. Declared by the node type; filled with real values once the step has run.",
|
|
643
650
|
"integration.nodeInspector.output.loopBody.heading": "The loop body",
|
|
644
|
-
"integration.nodeInspector.output.loopBody.size": "Nothing is wired to Item yet, so this loop repeats nothing. | One step is wired to Item and runs once per element. | {n} steps are wired to Item and each runs once per element.",
|
|
645
|
-
"integration.nodeInspector.output.loopBody.names": "These repeat: {names}",
|
|
646
651
|
"integration.nodeInspector.output.loopBody.joined": "One of them is wired after the loop as well, on the Done side — so it repeats with the body rather than running once: {names} | {n} of them are wired after the loop as well, on the Done side — so they repeat with the body rather than running once: {names}",
|
|
652
|
+
"integration.nodeInspector.output.loopBody.names": "These repeat: {names}",
|
|
653
|
+
"integration.nodeInspector.output.loopBody.size": "Nothing is wired to Item yet, so this loop repeats nothing. | One step is wired to Item and runs once per element. | {n} steps are wired to Item and each runs once per element.",
|
|
647
654
|
"integration.nodeInspector.output.pin.editAction": "Edit pinned sample",
|
|
648
655
|
"integration.nodeInspector.output.pin.editorAria": "Pinned sample output",
|
|
649
656
|
"integration.nodeInspector.output.pin.hint": "A pinned sample stands in for run data while you map downstream fields.",
|
|
@@ -800,6 +807,20 @@
|
|
|
800
807
|
"integration.nodeInspector.trigger.webhook.username.hint": "Optional — pins the Basic-auth username. The password is the endpoint secret.",
|
|
801
808
|
"integration.nodeInspector.trigger.webhook.username.label": "Username",
|
|
802
809
|
"integration.nodeInspector.trigger.webhook.username.placeholder": "Any username",
|
|
810
|
+
"integration.nodeInspector.version.available": "Version {version} is available.",
|
|
811
|
+
"integration.nodeInspector.version.change": "Change version",
|
|
812
|
+
"integration.nodeInspector.version.move.confirm": "Change version",
|
|
813
|
+
"integration.nodeInspector.version.move.description": "The step keeps its connections, its name and its note. What changes is the version — and any setting the chosen version does not have.",
|
|
814
|
+
"integration.nodeInspector.version.move.dropped": "Will be removed",
|
|
815
|
+
"integration.nodeInspector.version.move.lossless": "Every setting travels unchanged.",
|
|
816
|
+
"integration.nodeInspector.version.move.notComparable": "Version {version} is no longer in the catalogue, so what differs between the two cannot be compared. The list below is what arrives, not what was checked.",
|
|
817
|
+
"integration.nodeInspector.version.move.reason.changed": "changed between the versions",
|
|
818
|
+
"integration.nodeInspector.version.move.reason.nowRequired": "required, and not answered yet",
|
|
819
|
+
"integration.nodeInspector.version.move.review": "Worth checking afterwards",
|
|
820
|
+
"integration.nodeInspector.version.move.same": "This is the version the step is already on.",
|
|
821
|
+
"integration.nodeInspector.version.move.targetAria": "Version to change this step to",
|
|
822
|
+
"integration.nodeInspector.version.move.title": "Change this step's version",
|
|
823
|
+
"integration.nodeInspector.version.moveTo": "Change to version {version}",
|
|
803
824
|
"integration.nodes.category.action": "Actions",
|
|
804
825
|
"integration.nodes.category.control": "Control flow",
|
|
805
826
|
"integration.nodes.category.io": "I/O",
|
|
@@ -1188,6 +1209,34 @@
|
|
|
1188
1209
|
"integration.workflows.settings.section.appearance": "Appearance",
|
|
1189
1210
|
"integration.workflows.settings.section.execution": "Execution",
|
|
1190
1211
|
"integration.workflows.settings.section.metadata": "Metadata",
|
|
1212
|
+
"integration.workflows.state.action.add": "Declare",
|
|
1213
|
+
"integration.workflows.state.action.done": "Done",
|
|
1214
|
+
"integration.workflows.state.action.remove": "Remove declaration",
|
|
1215
|
+
"integration.workflows.state.action.remove.hint": "Removing the declaration takes this workflow's access away. The stored entries stay where they are.",
|
|
1216
|
+
"integration.workflows.state.dialog.description": "What this workflow may remember between runs. It can only reach the namespaces declared here — anything else is refused when the workflow runs.",
|
|
1217
|
+
"integration.workflows.state.dialog.title": "State namespaces",
|
|
1218
|
+
"integration.workflows.state.error.invalidName": "Use lowercase letters, digits and . - _, starting with a letter — for example article or crm.customers.",
|
|
1219
|
+
"integration.workflows.state.error.nameExists": "This workflow already declares that namespace.",
|
|
1220
|
+
"integration.workflows.state.field.newName.label": "Declare a namespace",
|
|
1221
|
+
"integration.workflows.state.field.newName.placeholder": "article",
|
|
1222
|
+
"integration.workflows.state.field.newRole.label": "Role",
|
|
1223
|
+
"integration.workflows.state.field.newVisibility.label": "Visible to",
|
|
1224
|
+
"integration.workflows.state.field.visibility.label": "Visible to",
|
|
1225
|
+
"integration.workflows.state.role.cursor.hint": "Remembers how far an incremental import read. It only moves on when the run finishes cleanly.",
|
|
1226
|
+
"integration.workflows.state.role.cursor.label": "Cursor",
|
|
1227
|
+
"integration.workflows.state.role.dedupe.hint": "Marks something as already processed, so a repeated delivery is not booked twice.",
|
|
1228
|
+
"integration.workflows.state.role.dedupe.label": "Dedupe",
|
|
1229
|
+
"integration.workflows.state.role.digest.hint": "Remembers what was last written, so unchanged records can be skipped.",
|
|
1230
|
+
"integration.workflows.state.role.digest.label": "Digest",
|
|
1231
|
+
"integration.workflows.state.role.fixed": "The role is fixed once the namespace is declared: entries written with one meaning cannot be read back with another.",
|
|
1232
|
+
"integration.workflows.state.role.mapping.hint": "Correlates an id on each side, so a later run knows whether to create or update.",
|
|
1233
|
+
"integration.workflows.state.role.mapping.label": "Mapping",
|
|
1234
|
+
"integration.workflows.state.row.aria": "State namespace {namespace}",
|
|
1235
|
+
"integration.workflows.state.empty": "This workflow remembers nothing between runs yet.",
|
|
1236
|
+
"integration.workflows.state.visibility.private.hint": "Only this workflow reads and writes it, and it is deleted along with the workflow.",
|
|
1237
|
+
"integration.workflows.state.visibility.private.label": "This workflow",
|
|
1238
|
+
"integration.workflows.state.visibility.shared.hint": "Every workflow in this tenant reads and writes these entries, and the namespace stays when this workflow is deleted.",
|
|
1239
|
+
"integration.workflows.state.visibility.shared.label": "Every workflow",
|
|
1191
1240
|
"integration.workflows.table.action.groupByTags": "Group by tags",
|
|
1192
1241
|
"integration.workflows.table.column.build": "Build",
|
|
1193
1242
|
"integration.workflows.table.column.lastRun": "Last run",
|
|
@@ -42,6 +42,7 @@ import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue"
|
|
|
42
42
|
import { toast } from "vue-sonner";
|
|
43
43
|
import { IntegrationsApiError, apiValidationIssues, errorReason } from "../../../utils/apiError";
|
|
44
44
|
import { createPollBudget, SAFETY_POLL_MS } from "../../../utils/pollBudget";
|
|
45
|
+
import { BUILD_SETTLE_POLL_MS, BUILD_SETTLE_TIMEOUT_MS, createBuildSettle, isBuildUnsettled } from "../../../utils/buildSettle";
|
|
45
46
|
import { splitRunOutputs } from "../../../utils/runOutputs";
|
|
46
47
|
import { isTerminalRun, unwrapRun } from "../../../utils/runEnvelope";
|
|
47
48
|
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
@@ -124,8 +125,8 @@ const reauthCredentials = computed(() => {
|
|
|
124
125
|
const buildWorking = computed(() => workflow.value?.buildStatus === "building" || workflow.value?.buildStatus === "pending");
|
|
125
126
|
const buildFailed = computed(() => workflow.value?.buildStatus === "failed");
|
|
126
127
|
const waitingForBuild = ref(false);
|
|
127
|
-
const BUILD_WAIT_POLL_MS =
|
|
128
|
-
const BUILD_WAIT_TIMEOUT_MS =
|
|
128
|
+
const BUILD_WAIT_POLL_MS = BUILD_SETTLE_POLL_MS;
|
|
129
|
+
const BUILD_WAIT_TIMEOUT_MS = BUILD_SETTLE_TIMEOUT_MS;
|
|
129
130
|
async function waitForBuildReady() {
|
|
130
131
|
waitingForBuild.value = true;
|
|
131
132
|
try {
|
|
@@ -148,19 +149,14 @@ async function waitForBuildReady() {
|
|
|
148
149
|
waitingForBuild.value = false;
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const status = workflow.value.buildStatus;
|
|
158
|
-
if (status !== "pending" && status !== "building") return;
|
|
159
|
-
await new Promise((resolve) => setTimeout(resolve, BUILD_WAIT_POLL_MS));
|
|
160
|
-
if (mine !== buildWatch || !workflow.value) return;
|
|
161
|
-
await get(workflow.value.id);
|
|
162
|
-
}
|
|
152
|
+
const buildSettle = createBuildSettle();
|
|
153
|
+
function settleBuildStatus() {
|
|
154
|
+
void buildSettle.settle({
|
|
155
|
+
unsettled: () => !!workflow.value && isBuildUnsettled(workflow.value.buildStatus),
|
|
156
|
+
read: () => get(workflow.value.id)
|
|
157
|
+
});
|
|
163
158
|
}
|
|
159
|
+
onUnmounted(() => buildSettle.stop());
|
|
164
160
|
const buildNotice = computed(() => {
|
|
165
161
|
switch (workflow.value?.buildStatus) {
|
|
166
162
|
case "building":
|
|
@@ -182,7 +178,7 @@ watch(id, async () => {
|
|
|
182
178
|
draft.value = null;
|
|
183
179
|
warningsExpanded.value = false;
|
|
184
180
|
if (tab.value === "runs") void loadRuns(1);
|
|
185
|
-
|
|
181
|
+
settleBuildStatus();
|
|
186
182
|
}, { immediate: true });
|
|
187
183
|
function onChange(blob) {
|
|
188
184
|
const d = draftFrom();
|
|
@@ -252,7 +248,7 @@ async function onRevisionRestored() {
|
|
|
252
248
|
if (!workflow.value) return;
|
|
253
249
|
await get(workflow.value.id);
|
|
254
250
|
draft.value = null;
|
|
255
|
-
|
|
251
|
+
settleBuildStatus();
|
|
256
252
|
}
|
|
257
253
|
const { trigger: triggerRun, loadForWorkflow, fetchRun, getRun, retryRun } = useIntegrationsRuns();
|
|
258
254
|
const running = ref(false);
|
|
@@ -711,7 +707,7 @@ async function save() {
|
|
|
711
707
|
executionMode: d.executionMode
|
|
712
708
|
});
|
|
713
709
|
draft.value = null;
|
|
714
|
-
|
|
710
|
+
settleBuildStatus();
|
|
715
711
|
} catch (e) {
|
|
716
712
|
const issues = apiValidationIssues(e);
|
|
717
713
|
if (issues.length && workflow.value.active) {
|
|
@@ -748,7 +744,7 @@ async function confirmDeactivate() {
|
|
|
748
744
|
draft.value = null;
|
|
749
745
|
deactivationOpen.value = false;
|
|
750
746
|
toast.success(t("integration.editor.deactivate.toast.savedPaused"));
|
|
751
|
-
|
|
747
|
+
settleBuildStatus();
|
|
752
748
|
} catch (e) {
|
|
753
749
|
deactivationOpen.value = false;
|
|
754
750
|
saveError.value = errorReason(e, t("integration.editor.save.error.generic"));
|
|
@@ -1195,7 +1191,18 @@ function formatDateTime(iso) {
|
|
|
1195
1191
|
afternoon this dialog exists to protect.
|
|
1196
1192
|
`as-child` so the studio's own Button is that element rather than a second one
|
|
1197
1193
|
beside it. Escape means the same thing, through `update:open`; a click beside
|
|
1198
|
-
the dialog does nothing at all, which an alert dialog is entitled to.
|
|
1194
|
+
the dialog does nothing at all, which an alert dialog is entitled to.
|
|
1195
|
+
|
|
1196
|
+
The class list on that Button is the price of `as-child`: `AlertDialogCancel`
|
|
1197
|
+
applies the outline variant to whatever it renders, and every dialog in this
|
|
1198
|
+
module draws the answer on the right as a filled one. So the two variants meet
|
|
1199
|
+
on one element and `tailwind-merge` decides between them per group — which it
|
|
1200
|
+
cannot do where the outline variant names a group the default one does not, and
|
|
1201
|
+
those are the five that have to be said here. Left to itself the merge produced
|
|
1202
|
+
a background from the outline variant and a text colour from the default, which
|
|
1203
|
+
resolve to the same colour in this theme: the label was invisible until hovered
|
|
1204
|
+
(PO-415). Nothing marks this answer as the one holding the keyboard otherwise —
|
|
1205
|
+
`focus-visible` does not match after the click that opened the dialog. -->
|
|
1199
1206
|
<AlertDialog :open="leaveOpen" @update:open="(v) => {
|
|
1200
1207
|
if (!v) answerLeave(false);
|
|
1201
1208
|
}">
|
|
@@ -1211,7 +1218,10 @@ function formatDateTime(iso) {
|
|
|
1211
1218
|
{{ t("integration.editor.leave.dialog.discard") }}
|
|
1212
1219
|
</Button>
|
|
1213
1220
|
<AlertDialogCancel as-child>
|
|
1214
|
-
<Button
|
|
1221
|
+
<Button
|
|
1222
|
+
class="border-transparent bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground dark:border-transparent dark:bg-primary dark:hover:bg-primary/90"
|
|
1223
|
+
@click="answerLeave(false)"
|
|
1224
|
+
>
|
|
1215
1225
|
{{ t("integration.editor.leave.dialog.stay") }}
|
|
1216
1226
|
</Button>
|
|
1217
1227
|
</AlertDialogCancel>
|
|
@@ -3,15 +3,30 @@ import { AddCircle as Plus } from "#solar-icons";
|
|
|
3
3
|
import { Button } from "@revenexx/studio";
|
|
4
4
|
import WorkflowsTable from "../../../components/Integrations/WorkflowsTable.vue";
|
|
5
5
|
import WorkflowCreateDialog from "../../../components/Integrations/WorkflowCreateDialog.vue";
|
|
6
|
+
import { createBuildSettle, isBuildUnsettled } from "../../../utils/buildSettle";
|
|
6
7
|
const t = useIntegrationsT();
|
|
7
8
|
useHead({ title: () => t("integration.workflows.list.head.title") });
|
|
8
9
|
const { workflows, loading, error, load } = useIntegrationsWorkflows();
|
|
9
|
-
onMounted(() => {
|
|
10
|
-
|
|
10
|
+
onMounted(async () => {
|
|
11
|
+
await load();
|
|
12
|
+
settleBuildStates();
|
|
13
|
+
});
|
|
14
|
+
let onScreen = true;
|
|
15
|
+
const buildSettle = createBuildSettle();
|
|
16
|
+
function settleBuildStates() {
|
|
17
|
+
if (!onScreen) return;
|
|
18
|
+
void buildSettle.settle({
|
|
19
|
+
unsettled: () => workflows.value.some((wf) => isBuildUnsettled(wf.buildStatus)),
|
|
20
|
+
read: () => load(true)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
onUnmounted(() => {
|
|
24
|
+
onScreen = false;
|
|
25
|
+
buildSettle.stop();
|
|
11
26
|
});
|
|
12
27
|
const { buildTick } = useIntegrationsRealtime();
|
|
13
28
|
watch(buildTick, () => {
|
|
14
|
-
void load(true);
|
|
29
|
+
void load(true).then(() => settleBuildStates());
|
|
15
30
|
});
|
|
16
31
|
const loadFailed = computed(() => !!error && !!error.value && !workflows.value.length);
|
|
17
32
|
const createOpen = ref(false);
|
|
@@ -43,7 +58,11 @@ const createOpen = ref(false);
|
|
|
43
58
|
</Button>
|
|
44
59
|
</template>
|
|
45
60
|
</StateCard>
|
|
46
|
-
|
|
61
|
+
<!-- `loading` is the composable's app-wide flag and goes up for every read, the
|
|
62
|
+
build watch's included — so passing it straight through would flip the table
|
|
63
|
+
between its skeleton and its empty text every few seconds while a build
|
|
64
|
+
runs. What the table's skeleton means is "nothing to show yet". -->
|
|
65
|
+
<WorkflowsTable v-else :workflows="workflows" :loading="loading && !workflows.length" />
|
|
47
66
|
</div>
|
|
48
67
|
|
|
49
68
|
<WorkflowCreateDialog v-model:open="createOpen" />
|