@revenexx/studio-integrations 1.1.0 → 1.2.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +520 -0
  2. package/README.md +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
  7. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
  8. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  9. package/dist/runtime/components/Integrations/ExpressionInput.vue +77 -22
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  11. package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
  12. package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
  13. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
  14. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
  15. package/dist/runtime/components/Integrations/NodeConfigField.vue +23 -0
  16. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  17. package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
  18. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  19. package/dist/runtime/components/Integrations/RunActionsMenu.vue +5 -3
  20. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  21. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  22. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  23. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  24. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  25. package/dist/runtime/components/Integrations/RunsTable.vue +143 -20
  26. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  27. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  28. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  29. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  30. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  31. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  32. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  33. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  34. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  35. package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
  36. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  37. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  38. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  39. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  40. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  41. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  42. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  43. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  44. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  45. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  47. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  48. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  49. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  50. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  51. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  52. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  53. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  54. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  55. package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
  56. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  57. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
  58. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  59. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  60. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  61. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  62. package/dist/runtime/data/expressionEditor.js +11 -5
  63. package/dist/runtime/data/handleId.d.ts +61 -1
  64. package/dist/runtime/data/handleId.js +8 -1
  65. package/dist/runtime/data/payloadForm.js +3 -2
  66. package/dist/runtime/i18n/messages/de.json +30 -11
  67. package/dist/runtime/i18n/messages/en.json +27 -8
  68. package/dist/runtime/pages/integrations/audit.vue +7 -6
  69. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  70. package/dist/runtime/pages/integrations/index.vue +70 -12
  71. package/dist/runtime/pages/integrations/runs.vue +116 -7
  72. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  73. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  74. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  75. package/dist/runtime/pages/integrations/workflows/[id].vue +177 -28
  76. package/dist/runtime/types/integrations.d.ts +5 -0
  77. package/dist/runtime/utils/apiError.d.ts +21 -1
  78. package/dist/runtime/utils/apiError.js +1 -1
  79. package/dist/runtime/utils/clipboard.d.ts +33 -0
  80. package/dist/runtime/utils/clipboard.js +11 -0
  81. package/dist/runtime/utils/deepClone.d.ts +41 -0
  82. package/dist/runtime/utils/deepClone.js +3 -0
  83. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  84. package/dist/runtime/utils/formatDuration.js +13 -0
  85. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  86. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  87. package/dist/runtime/utils/notify.d.ts +1 -1
  88. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  89. package/dist/runtime/utils/runEnvelope.js +12 -0
  90. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  91. package/dist/runtime/utils/runFeedQuery.js +66 -0
  92. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  93. package/dist/runtime/utils/runOutputs.js +10 -0
  94. package/dist/runtime/utils/statusStyle.d.ts +24 -0
  95. package/dist/runtime/utils/statusStyle.js +1 -0
  96. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  97. package/dist/runtime/utils/validationFindings.js +18 -3
  98. package/package.json +1 -1
@@ -12,11 +12,23 @@ export interface InlineNodeJSON {
12
12
  export declare const EXPR_TOKEN_NODE = "exprToken";
13
13
  /** Raw text → inline content: text nodes with one atom pill per token. */
14
14
  export declare function textToInlineContent(text: string): InlineNodeJSON[];
15
- /** Raw text → a full single-paragraph TipTap doc. */
16
- export declare function textToDoc(text: string): InlineNodeJSON;
15
+ /**
16
+ * One paragraph per line, or one paragraph for the whole value.
17
+ *
18
+ * A field the node declared `multiline` keeps its newlines: each line becomes a
19
+ * paragraph, which is the only structure a TipTap doc has for one. Every other
20
+ * field is single-line and stays a single paragraph — the editor swallows Enter
21
+ * there, so nothing can create a second one from inside.
22
+ */
23
+ type LineMode = {
24
+ multiline?: boolean;
25
+ };
26
+ /** Raw text → a full TipTap doc: one paragraph, or one per line when multiline. */
27
+ export declare function textToDoc(text: string, { multiline }?: LineMode): InlineNodeJSON;
17
28
  /** TipTap doc JSON → raw text (pills serialize back to their `${{ … }}` token).
18
- * Multiple paragraphs (pasted newlines) collapse to single spaces — the
19
- * fields are single-line. */
20
- export declare function docToText(doc: InlineNodeJSON | null | undefined): string;
29
+ * Multiple paragraphs are the lines of a multiline field; in a single-line
30
+ * field they can only have arrived by paste, and collapse to single spaces. */
31
+ export declare function docToText(doc: InlineNodeJSON | null | undefined, { multiline }?: LineMode): string;
21
32
  /** True when any TEXT node still carries a complete raw token (needs re-pilling). */
22
33
  export declare function hasRawTokenText(doc: InlineNodeJSON | null | undefined): boolean;
34
+ export {};
@@ -11,11 +11,17 @@ export function textToInlineContent(text) {
11
11
  if (cursor < text.length) out.push({ type: "text", text: text.slice(cursor) });
12
12
  return out;
13
13
  }
14
- export function textToDoc(text) {
15
- const content = textToInlineContent(text);
16
- return { type: "doc", content: [content.length ? { type: "paragraph", content } : { type: "paragraph" }] };
14
+ export function textToDoc(text, { multiline = false } = {}) {
15
+ const lines = multiline ? text.split("\n") : [text];
16
+ return {
17
+ type: "doc",
18
+ content: lines.map((line) => {
19
+ const content = textToInlineContent(line);
20
+ return content.length ? { type: "paragraph", content } : { type: "paragraph" };
21
+ })
22
+ };
17
23
  }
18
- export function docToText(doc) {
24
+ export function docToText(doc, { multiline = false } = {}) {
19
25
  if (!doc) return "";
20
26
  const inlineText = (p) => {
21
27
  let s = "";
@@ -26,7 +32,7 @@ export function docToText(doc) {
26
32
  }
27
33
  return s;
28
34
  };
29
- return (doc.content ?? []).filter((n) => n.type === "paragraph").map(inlineText).join(" ");
35
+ return (doc.content ?? []).filter((n) => n.type === "paragraph").map(inlineText).join(multiline ? "\n" : " ");
30
36
  }
31
37
  export function hasRawTokenText(doc) {
32
38
  if (!doc) return false;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Reading the logical port name out of a canvas handle id.
2
+ * The shape of a canvas handle id — writing one, and reading the port name back out.
3
3
  *
4
4
  * Each node on the canvas renders four connection points per port, one per
5
5
  * side, so that an edge can leave and arrive without crossing a card body. The
@@ -19,5 +19,65 @@
19
19
  * how to read an id, and because that is what makes it testable: the editor is
20
20
  * an SFC, out of reach of the unit suite, and the defect above shipped once
21
21
  * already without anything going red.
22
+ *
23
+ * **Both directions live here, and that is PO-258.** The reader was here alone while
24
+ * twelve places across six components assembled an id with a template string and five
25
+ * of them declared the side union again — so the two halves of one contract agreed by
26
+ * spelling the same thing the same way rather than by construction. Nothing was wrong
27
+ * on screen; what it cost is that adding a fifth dot position would have had the
28
+ * writers produce `paid__centre` while the reader, which never heard of it, handed
29
+ * `paid__centre` back as a port name. That is not nullish, so the `?? 'default'` behind
30
+ * every call site cannot catch it, and the editor would have persisted an output no
31
+ * node has.
32
+ *
33
+ * **What happens elsewhere when a fifth position is added, and that is PO-396.** This
34
+ * module owns what a side *is*; what the canvas does with one — where the library draws
35
+ * it, what faces it, where along an edge it sits — lives in
36
+ * `components/Integrations/canvasSides.ts`, which is separate for one reason worth
37
+ * keeping: `Position` is Vue Flow's, and this file has no imports, so its round trip can
38
+ * be proved without the canvas library.
39
+ *
40
+ * Those three answers used to be ternary chains inside the components, each ending in an
41
+ * `else`, and two of them were written more than once: the position map in the node card
42
+ * and the trigger card, and the opposite-side map in the editor and in both read-only
43
+ * canvases — where it was called `opposite` rather than `oppositeSide`, which is why a
44
+ * search by name found one of the three. An `else` absorbs a new name
45
+ * silently — a `centre` dot drawn on the left, with `right` called its opposite — and
46
+ * every one of those is a plausible answer for *some* side, so nothing would look wrong.
47
+ * They are now `Record<Side, …>` maps: adding a name to `SIDES` below stops all three
48
+ * compiling until they say what it means. Still more than one edit, and none of them
49
+ * can be forgotten, which is the better half of the trade.
22
50
  */
51
+ /**
52
+ * The side suffix, anchored at the end. The four sides are a closed set, which
53
+ * is what lets the name in front of them be read whole: a port name is not
54
+ * restricted to a safe alphabet: `portsForNode` builds one with `String(v)` over
55
+ * the config values a person types into a switch node's cases, and a
56
+ * server-resolved dynamic output is named whatever the remote system calls its
57
+ * fields — where a double underscore is a naming convention rather than an
58
+ * accident. So a name may well contain `__` itself, and taking the FIRST one as
59
+ * the separator would hand back a truncated name that no node has a port for.
60
+ * Truncated is worse than empty: it is not nullish, so a caller's
61
+ * `?? 'default'` cannot catch it either.
62
+ */
63
+ /** Every side a connection dot is rendered on. The closed set the parse anchors on. */
64
+ export declare const SIDES: readonly ["top", "right", "bottom", "left"];
65
+ export type Side = (typeof SIDES)[number];
23
66
  export declare function portFromHandleId(handleId: string | null | undefined): string | null;
67
+ /**
68
+ * The id of an output dot: the port's own name, then the side.
69
+ *
70
+ * The name is passed through as it is. A port name is not restricted to a safe
71
+ * alphabet — `portsForNode` builds one with `String(v)` over what a person types into a
72
+ * switch node's cases — and sanitising it here would produce an id the reader cannot
73
+ * turn back into the name the blob stores.
74
+ */
75
+ export declare function outputHandleId(port: string, side: Side): string;
76
+ /**
77
+ * The id of the single input dot on a given side.
78
+ *
79
+ * The separator sits at the front with no name before it, which is exactly what
80
+ * `portFromHandleId` reads as "no port": a node has one logical input, so there is no
81
+ * name to carry. A group frame's input is this on the top side.
82
+ */
83
+ export declare function inputHandleId(side: Side): string;
@@ -1,6 +1,13 @@
1
- const SIDE_SUFFIX = /__(?:top|right|bottom|left)$/;
1
+ export const SIDES = ["top", "right", "bottom", "left"];
2
+ const SIDE_SUFFIX = new RegExp(`__(?:${SIDES.join("|")})$`);
2
3
  export function portFromHandleId(handleId) {
3
4
  if (!handleId) return null;
4
5
  const name = handleId.replace(SIDE_SUFFIX, "");
5
6
  return name.startsWith("__") || !name ? null : name;
6
7
  }
8
+ export function outputHandleId(port, side) {
9
+ return `${port}__${side}`;
10
+ }
11
+ export function inputHandleId(side) {
12
+ return `__in__${side}`;
13
+ }
@@ -1,4 +1,5 @@
1
1
  import { useIntegrationsT } from "../composables/useIntegrationsT.js";
2
+ import { deepClone } from "../utils/deepClone.js";
2
3
  const PAYLOAD_REF_RE = /\$\{\{\s*trigger\.payload\.([a-zA-Z0-9_.-]+)/g;
3
4
  const FILE_KEYS = /* @__PURE__ */ new Set(["data", "filename", "contentType", "size"]);
4
5
  function collectRefs(value, out) {
@@ -91,7 +92,7 @@ export function getPath(source, path) {
91
92
  return cursor;
92
93
  }
93
94
  export function buildPayload(fields, textValues, files, extras) {
94
- const payload = JSON.parse(JSON.stringify(extras ?? {}));
95
+ const payload = deepClone(extras ?? {});
95
96
  for (const field of fields) {
96
97
  if (field.kind === "file") {
97
98
  const file = files[field.path];
@@ -111,7 +112,7 @@ export function buildPayload(fields, textValues, files, extras) {
111
112
  return payload;
112
113
  }
113
114
  export function splitPayload(fields, payload) {
114
- const extras = JSON.parse(JSON.stringify(payload ?? {}));
115
+ const extras = deepClone(payload ?? {});
115
116
  const textValues = {};
116
117
  const files = {};
117
118
  const deleteAt = (path) => {
@@ -28,6 +28,7 @@
28
28
  "integration.audit.table.column.actor": "Akteur",
29
29
  "integration.audit.table.column.resource": "Ressource",
30
30
  "integration.audit.table.column.when": "Wann",
31
+ "integration.audit.toast.copyFailed": "Kopieren fehlgeschlagen",
31
32
  "integration.audit.toast.copied": "Kopiert",
32
33
  "integration.canvas.badge.trigger": "Trigger",
33
34
  "integration.canvas.color.amber": "Bernstein",
@@ -229,6 +230,7 @@
229
230
  "integration.credentials.sftpImport.empty": "Keine aktiven Plattform-SFTP-Konten. Zuerst eines unter Integration Studio → SFTP anlegen.",
230
231
  "integration.credentials.sftpImport.error.importFailed": "Import fehlgeschlagen.",
231
232
  "integration.credentials.sftpImport.error.readPassword": "Das Kontopasswort konnte nicht gelesen werden.",
233
+ "integration.credentials.sftpImport.state.loading": "SFTP-Konten werden geladen…",
232
234
  "integration.credentials.sftpImport.title": "Plattform-SFTP-Konto importieren",
233
235
  "integration.credentials.shared.action.testConnection": "Verbindung testen",
234
236
  "integration.credentials.shared.authKind.apiKey": "API-Key",
@@ -263,6 +265,7 @@
263
265
  "integration.dashboard.attention.healthy": "Alle Workflows in Ordnung.",
264
266
  "integration.dashboard.attention.missedRuns": "{n} verpasster Run | {n} verpasste Runs",
265
267
  "integration.dashboard.attention.missedRuns.hint": "Trigger haben ausgelöst, während ihr Workflow nicht laufen konnte — prüfen und erneut ausführen.",
268
+ "integration.dashboard.attention.state.loading": "Es wird geprüft, was Aufmerksamkeit erfordert…",
266
269
  "integration.dashboard.attention.title": "Erfordert Aufmerksamkeit",
267
270
  "integration.dashboard.head.title": "Dashboard · Integration Studio",
268
271
  "integration.dashboard.header.description": "Alles, was die Revenue Cloud mit der Außenwelt verbindet.",
@@ -276,7 +279,9 @@
276
279
  "integration.dashboard.kpi.runningNow.sub": "laufende Runs",
277
280
  "integration.dashboard.kpi.scheduled.label": "Geplant",
278
281
  "integration.dashboard.kpi.scheduled.sub": "aktive Zeitplan-Trigger",
282
+ "integration.dashboard.kpi.state.loading": "Kennzahlen werden geladen…",
279
283
  "integration.dashboard.runs.empty": "Noch keine Runs.",
284
+ "integration.dashboard.runs.state.loading": "Letzte Runs werden geladen…",
280
285
  "integration.dashboard.runs.title": "Letzte Runs",
281
286
  "integration.dashboard.surface.events.hint": "Live-Events der Plattform, die Workflows abonnieren.",
282
287
  "integration.dashboard.surface.events.label": "Event-Stream",
@@ -288,6 +293,7 @@
288
293
  "integration.dashboard.surface.webhooks.hint": "Eingehende HTTP-Endpoints, die Workflows auslösen.",
289
294
  "integration.dashboard.surface.webhooks.label": "Webhooks",
290
295
  "integration.dashboard.templates.empty": "Noch keine Vorlagen registriert.",
296
+ "integration.dashboard.templates.state.loading": "Vorlagen werden geladen…",
291
297
  "integration.dashboard.templates.title": "Mit einer Vorlage starten",
292
298
  "integration.deadLetters.action.discard": "Verwerfen",
293
299
  "integration.deadLetters.action.replay": "Wiederholen",
@@ -300,6 +306,7 @@
300
306
  "integration.deadLetters.panel.title": "Verpasste Runs",
301
307
  "integration.deadLetters.reason.fallback": "Kein Grund erfasst.",
302
308
  "integration.deadLetters.state.empty": "Keine verpassten Runs. Jeder Trigger hat einen Run ausgelöst.",
309
+ "integration.deadLetters.state.loading": "Verpasste Runs werden geladen…",
303
310
  "integration.deadLetters.status.discarded": "verworfen",
304
311
  "integration.deadLetters.status.pending": "wartet",
305
312
  "integration.deadLetters.status.replayed": "wiederholt",
@@ -317,7 +324,8 @@
317
324
  "integration.editor.action.discard.label": "Verwerfen",
318
325
  "integration.editor.action.discard.title": "Nicht gespeicherte Änderungen verwerfen",
319
326
  "integration.editor.action.duplicate.label": "Duplizieren",
320
- "integration.editor.action.duplicate.title": "Diesen Workflow duplizieren",
327
+ "integration.editor.action.more.aria": "Weitere Aktionen für diesen Workflow",
328
+ "integration.editor.action.more.label": "Aktionen",
321
329
  "integration.editor.action.pause.title": "Workflow pausieren",
322
330
  "integration.editor.action.retry": "Erneut versuchen",
323
331
  "integration.editor.action.save.label": "Speichern",
@@ -433,6 +441,7 @@
433
441
  "integration.editor.tab.editor": "Editor",
434
442
  "integration.editor.tab.history": "Verlauf",
435
443
  "integration.editor.tab.runs": "Runs",
444
+ "integration.editor.toast.deleted": "„{name}“ gelöscht",
436
445
  "integration.editor.toast.duplicated": "Dupliziert als „{name}“",
437
446
  "integration.editor.tool.addGroup.label": "Gruppe",
438
447
  "integration.editor.tool.addGroup.tooltip": "Gruppenrahmen hinzufügen — hineingezogene Nodes treten ihr bei",
@@ -472,6 +481,7 @@
472
481
  "integration.history.preview.title.current": "Aktuelle Version · Revision {revision}",
473
482
  "integration.history.preview.title.revision": "Revision {revision} (schreibgeschützt)",
474
483
  "integration.history.state.empty": "Noch keine archivierten Revisionen — speichern Sie eine Änderung, um die erste Momentaufnahme zu erstellen.",
484
+ "integration.history.state.loading": "Versionsverlauf wird geladen…",
475
485
  "integration.history.toast.loadRevisionFailed": "Die Revision konnte nicht geladen werden.",
476
486
  "integration.history.toast.restoreFailed": "Die Revision konnte nicht wiederhergestellt werden.",
477
487
  "integration.history.toast.restored": "Revision {revision} wiederhergestellt",
@@ -520,6 +530,7 @@
520
530
  "integration.nodeConfig.dynamicSchema.resolving": "Felder werden geladen…",
521
531
  "integration.nodeConfig.expression.badge.title": "Akzeptiert ${{ … }}-Ausdrücke",
522
532
  "integration.nodeConfig.expression.insertTitle": "Ausdruck einfügen",
533
+ "integration.nodeConfig.expression.insertAria": "Ausdruck einfügen — verfügbare Daten für diese Einstellung",
523
534
  "integration.nodeConfig.expression.picker.aria": "Verfügbare Daten für diese Einstellung",
524
535
  "integration.nodeConfig.expression.picker.empty": "Nichts entspricht „{query}“.",
525
536
  "integration.nodeConfig.expression.picker.filterAria": "Verfügbare Daten filtern",
@@ -727,6 +738,7 @@
727
738
  "integration.nodeInspector.toast.configCopyFailed": "Konfiguration konnte nicht kopiert werden",
728
739
  "integration.nodeInspector.toast.configPasted": "Konfiguration eingefügt",
729
740
  "integration.nodeInspector.toast.expressionCopied": "Ausdruck kopiert. Klicken Sie zuerst in ein Feld, um ihn direkt einzufügen.",
741
+ "integration.nodeInspector.toast.expressionCopyFailed": "Ausdruck konnte nicht kopiert werden. Klicken Sie in ein Feld, um ihn direkt einzufügen.",
730
742
  "integration.nodeInspector.toast.rotateFailed": "Secret konnte nicht neu erzeugt werden.",
731
743
  "integration.nodeInspector.toast.rotateNoSecret": "Der Endpoint hat kein Secret zurückgegeben.",
732
744
  "integration.nodeInspector.toast.samplePinned": "Beispielausgabe angeheftet",
@@ -829,7 +841,7 @@
829
841
  "integration.runs.action.back": "Zurück",
830
842
  "integration.runs.action.cancel": "Run stoppen",
831
843
  "integration.runs.action.resume": "Fortsetzen",
832
- "integration.runs.action.retry": "Retry",
844
+ "integration.runs.action.retry": "Erneut ausführen",
833
845
  "integration.runs.action.terminate": "Terminieren",
834
846
  "integration.runs.confirm.cancel": "Diesen Run stoppen?",
835
847
  "integration.runs.confirm.resume": "Diesen Run fortsetzen?",
@@ -889,9 +901,9 @@
889
901
  "integration.runs.list.header.title": "Runs",
890
902
  "integration.runs.menu.aria.actions": "Aktionen für Run #{id}",
891
903
  "integration.runs.menu.cancel": "Run stoppen",
892
- "integration.runs.menu.replay": "Wiederholen",
904
+ "integration.runs.menu.replay": "Wiedergabe",
893
905
  "integration.runs.menu.resume": "Fortsetzen",
894
- "integration.runs.menu.retry": "Retry",
906
+ "integration.runs.menu.retry": "Erneut ausführen",
895
907
  "integration.runs.menu.terminate": "Terminieren",
896
908
  "integration.runs.menu.viewDetails": "Details anzeigen",
897
909
  "integration.runs.notice.requested": "{action} angefordert.",
@@ -907,13 +919,15 @@
907
919
  "integration.runs.payload.parseError": "Das JSON lässt sich nicht parsen — beheben Sie den Fehler, bevor Sie zur Formularansicht zurückwechseln.",
908
920
  "integration.runs.response.action.viewRun": "Run ansehen",
909
921
  "integration.runs.response.action.viewRunWithId": "Run #{id} ansehen",
910
- "integration.runs.response.async.started": "Run wurde gestartet und läuft im Hintergrund.",
911
- "integration.runs.response.async.startedWithId": "Run #{id} wurde gestartet und läuft im Hintergrund.",
912
- "integration.runs.response.description.async": "Für Hintergrundausführung eingereiht",
922
+ "integration.runs.response.async.started": "Run wurde gestartet.",
923
+ "integration.runs.response.async.startedWithId": "Run #{id} wurde gestartet.",
924
+ "integration.runs.response.description.async": "Im Hintergrund gestartet",
913
925
  "integration.runs.response.description.sync": "Synchrones Run-Ergebnis",
914
- "integration.runs.response.empty": "Keine Antwort.",
926
+ "integration.runs.response.following": "Warten, bis der Run fertig ist…",
915
927
  "integration.runs.response.loading.async": "Wird gestartet…",
916
928
  "integration.runs.response.loading.sync": "Wird ausgeführt…",
929
+ "integration.runs.response.nothingReturned": "Der Run hat nichts zurückgegeben.",
930
+ "integration.runs.response.runNumber": "Run #{id}",
917
931
  "integration.runs.response.title.async": "Run gestartet",
918
932
  "integration.runs.response.title.sync": "Run-Antwort",
919
933
  "integration.runs.status.cancelled": "abgebrochen",
@@ -928,6 +942,8 @@
928
942
  "integration.runs.table.column.trigger": "Trigger",
929
943
  "integration.runs.table.column.workflow": "Workflow",
930
944
  "integration.runs.table.empty": "Noch keine Runs — starten Sie einen Workflow, um diese Ansicht zu füllen.",
945
+ "integration.runs.table.empty.narrowed": "Keine Runs passen zu dieser Einschränkung.",
946
+ "integration.runs.table.filter.all": "Alle",
931
947
  "integration.runs.table.search.placeholder": "Runs durchsuchen…",
932
948
  "integration.runs.tabs.runs.label": "Runs",
933
949
  "integration.runs.toast.action.requested": "{action} angefordert",
@@ -990,6 +1006,7 @@
990
1006
  "integration.secrets.error.loadFailed": "Secrets konnten nicht geladen werden.",
991
1007
  "integration.secrets.error.valueRequired": "Der Wert ist erforderlich.",
992
1008
  "integration.secrets.form.error.invalidKey": "Der Schlüssel muss ^[A-Z0-9_]+$ entsprechen (Großbuchstaben, Ziffern, Unterstriche).",
1009
+ "integration.secrets.form.error.saveFailed": "Das Secret konnte nicht gespeichert werden.",
993
1010
  "integration.secrets.form.key.label": "Schlüssel",
994
1011
  "integration.secrets.form.key.patternLabel": "Muster:",
995
1012
  "integration.secrets.form.value.hide": "Wert ausblenden",
@@ -1128,7 +1145,12 @@
1128
1145
  "integration.workflows.create.template.noMatch": "Keine Treffer für „{query}“.",
1129
1146
  "integration.workflows.create.template.search.aria": "Vorlagen durchsuchen",
1130
1147
  "integration.workflows.create.template.search.placeholder": "Vorlagen durchsuchen…",
1148
+ "integration.workflows.create.template.state.loading": "Vorlagen werden geladen…",
1131
1149
  "integration.workflows.create.toast.templateError": "Der Workflow konnte nicht aus dieser Vorlage erstellt werden.",
1150
+ "integration.workflows.delete.action.deleting": "Wird gelöscht…",
1151
+ "integration.workflows.delete.dialog.description": "und der gesamte Revisionsverlauf werden endgültig gelöscht.",
1152
+ "integration.workflows.delete.dialog.title": "Workflow löschen?",
1153
+ "integration.workflows.delete.error.generic": "Der Workflow konnte nicht gelöscht werden.",
1132
1154
  "integration.workflows.duplicate.dialog.description": "Erstellt eine pausierte Kopie von „{name}“ mit allen Triggern aus.",
1133
1155
  "integration.workflows.duplicate.dialog.title": "Workflow duplizieren",
1134
1156
  "integration.workflows.duplicate.error.generic": "Der Workflow konnte nicht dupliziert werden.",
@@ -1174,9 +1196,6 @@
1174
1196
  "integration.workflows.table.column.status": "Status",
1175
1197
  "integration.workflows.table.column.triggers": "Trigger",
1176
1198
  "integration.workflows.table.column.updated": "Aktualisiert",
1177
- "integration.workflows.table.deleteDialog.action.deleting": "Wird gelöscht…",
1178
- "integration.workflows.table.deleteDialog.description": "und der gesamte Revisionsverlauf werden endgültig gelöscht.",
1179
- "integration.workflows.table.deleteDialog.title": "Workflow löschen?",
1180
1199
  "integration.workflows.table.group.noTags": "Keine Tags",
1181
1200
  "integration.workflows.table.lastRun.aria.never": "{name}, letzter Run: Nie ausgeführt",
1182
1201
  "integration.workflows.table.menu.action.activate": "Aktivieren",
@@ -28,6 +28,7 @@
28
28
  "integration.audit.table.column.actor": "Actor",
29
29
  "integration.audit.table.column.resource": "Resource",
30
30
  "integration.audit.table.column.when": "When",
31
+ "integration.audit.toast.copyFailed": "Could not copy",
31
32
  "integration.audit.toast.copied": "Copied",
32
33
  "integration.canvas.badge.trigger": "Trigger",
33
34
  "integration.canvas.color.amber": "Amber",
@@ -229,6 +230,7 @@
229
230
  "integration.credentials.sftpImport.empty": "No active platform SFTP accounts. Create one under Integration Studio → SFTP first.",
230
231
  "integration.credentials.sftpImport.error.importFailed": "Import failed.",
231
232
  "integration.credentials.sftpImport.error.readPassword": "Could not read the account password.",
233
+ "integration.credentials.sftpImport.state.loading": "Loading the SFTP accounts…",
232
234
  "integration.credentials.sftpImport.title": "Import platform SFTP account",
233
235
  "integration.credentials.shared.action.testConnection": "Connect",
234
236
  "integration.credentials.shared.authKind.apiKey": "API Key",
@@ -263,6 +265,7 @@
263
265
  "integration.dashboard.attention.healthy": "All workflows healthy.",
264
266
  "integration.dashboard.attention.missedRuns": "{n} missed run | {n} missed runs",
265
267
  "integration.dashboard.attention.missedRuns.hint": "Triggers fired while their workflow couldn't run — review & replay.",
268
+ "integration.dashboard.attention.state.loading": "Checking what needs attention…",
266
269
  "integration.dashboard.attention.title": "Needs attention",
267
270
  "integration.dashboard.head.title": "Dashboard · Integration Studio",
268
271
  "integration.dashboard.header.description": "Everything that connects Revenue Cloud to the outside world.",
@@ -276,7 +279,9 @@
276
279
  "integration.dashboard.kpi.runningNow.sub": "live executions in flight",
277
280
  "integration.dashboard.kpi.scheduled.label": "Scheduled",
278
281
  "integration.dashboard.kpi.scheduled.sub": "scheduled triggers active",
282
+ "integration.dashboard.kpi.state.loading": "Loading the key figures…",
279
283
  "integration.dashboard.runs.empty": "No runs yet.",
284
+ "integration.dashboard.runs.state.loading": "Loading the latest executions…",
280
285
  "integration.dashboard.runs.title": "Latest executions",
281
286
  "integration.dashboard.surface.events.hint": "Live platform events that workflows subscribe to.",
282
287
  "integration.dashboard.surface.events.label": "Event Stream",
@@ -288,6 +293,7 @@
288
293
  "integration.dashboard.surface.webhooks.hint": "Inbound HTTP endpoints that trigger workflows.",
289
294
  "integration.dashboard.surface.webhooks.label": "Webhooks",
290
295
  "integration.dashboard.templates.empty": "No templates registered yet.",
296
+ "integration.dashboard.templates.state.loading": "Loading templates…",
291
297
  "integration.dashboard.templates.title": "Start from a template",
292
298
  "integration.deadLetters.action.discard": "Discard",
293
299
  "integration.deadLetters.action.replay": "Replay",
@@ -300,6 +306,7 @@
300
306
  "integration.deadLetters.panel.title": "Missed runs",
301
307
  "integration.deadLetters.reason.fallback": "No reason recorded.",
302
308
  "integration.deadLetters.state.empty": "No missed runs. Every trigger firing made it into a run.",
309
+ "integration.deadLetters.state.loading": "Loading the missed runs…",
303
310
  "integration.deadLetters.status.discarded": "discarded",
304
311
  "integration.deadLetters.status.pending": "waiting",
305
312
  "integration.deadLetters.status.replayed": "replayed",
@@ -317,7 +324,8 @@
317
324
  "integration.editor.action.discard.label": "Cancel",
318
325
  "integration.editor.action.discard.title": "Discard unsaved changes",
319
326
  "integration.editor.action.duplicate.label": "Duplicate",
320
- "integration.editor.action.duplicate.title": "Duplicate this workflow",
327
+ "integration.editor.action.more.aria": "More actions for this workflow",
328
+ "integration.editor.action.more.label": "Actions",
321
329
  "integration.editor.action.pause.title": "Pause this workflow",
322
330
  "integration.editor.action.retry": "Retry",
323
331
  "integration.editor.action.save.label": "Save",
@@ -433,6 +441,7 @@
433
441
  "integration.editor.tab.editor": "Editor",
434
442
  "integration.editor.tab.history": "History",
435
443
  "integration.editor.tab.runs": "Runs",
444
+ "integration.editor.toast.deleted": "Deleted “{name}”",
436
445
  "integration.editor.toast.duplicated": "Duplicated as “{name}”",
437
446
  "integration.editor.tool.addGroup.label": "Group",
438
447
  "integration.editor.tool.addGroup.tooltip": "Add group frame — nodes dropped inside join it",
@@ -472,6 +481,7 @@
472
481
  "integration.history.preview.title.current": "Current version · revision {revision}",
473
482
  "integration.history.preview.title.revision": "Revision {revision} (read-only)",
474
483
  "integration.history.state.empty": "No archived revisions yet — save a change to create the first snapshot.",
484
+ "integration.history.state.loading": "Loading the version history…",
475
485
  "integration.history.toast.loadRevisionFailed": "Could not load the revision.",
476
486
  "integration.history.toast.restoreFailed": "Could not restore the revision.",
477
487
  "integration.history.toast.restored": "Revision {revision} restored",
@@ -520,6 +530,7 @@
520
530
  "integration.nodeConfig.dynamicSchema.resolving": "Resolving fields…",
521
531
  "integration.nodeConfig.expression.badge.title": "Accepts ${{ … }} expressions",
522
532
  "integration.nodeConfig.expression.insertTitle": "Insert expression",
533
+ "integration.nodeConfig.expression.insertAria": "Insert expression — available data for this setting",
523
534
  "integration.nodeConfig.expression.picker.aria": "Available data for this setting",
524
535
  "integration.nodeConfig.expression.picker.empty": "Nothing matches “{query}”.",
525
536
  "integration.nodeConfig.expression.picker.filterAria": "Filter available data",
@@ -727,6 +738,7 @@
727
738
  "integration.nodeInspector.toast.configCopyFailed": "Could not copy config",
728
739
  "integration.nodeInspector.toast.configPasted": "Config pasted",
729
740
  "integration.nodeInspector.toast.expressionCopied": "Expression copied. Click into a field first to insert it directly.",
741
+ "integration.nodeInspector.toast.expressionCopyFailed": "Could not copy the expression. Click into a field to insert it directly.",
730
742
  "integration.nodeInspector.toast.rotateFailed": "Could not rotate the secret.",
731
743
  "integration.nodeInspector.toast.rotateNoSecret": "The endpoint did not return a secret.",
732
744
  "integration.nodeInspector.toast.samplePinned": "Sample output pinned",
@@ -907,13 +919,15 @@
907
919
  "integration.runs.payload.parseError": "The JSON does not parse — fix it before switching back to the form.",
908
920
  "integration.runs.response.action.viewRun": "View run",
909
921
  "integration.runs.response.action.viewRunWithId": "View run #{id}",
910
- "integration.runs.response.async.started": "Run started and is executing in the background.",
911
- "integration.runs.response.async.startedWithId": "Run #{id} started and is executing in the background.",
912
- "integration.runs.response.description.async": "Queued for background execution",
922
+ "integration.runs.response.async.started": "Run started.",
923
+ "integration.runs.response.async.startedWithId": "Run #{id} started.",
924
+ "integration.runs.response.description.async": "Started in the background",
913
925
  "integration.runs.response.description.sync": "Synchronous run result",
914
- "integration.runs.response.empty": "No response.",
926
+ "integration.runs.response.following": "Waiting for the run to finish…",
915
927
  "integration.runs.response.loading.async": "Starting…",
916
928
  "integration.runs.response.loading.sync": "Running…",
929
+ "integration.runs.response.nothingReturned": "The run returned nothing.",
930
+ "integration.runs.response.runNumber": "Run #{id}",
917
931
  "integration.runs.response.title.async": "Run started",
918
932
  "integration.runs.response.title.sync": "Run response",
919
933
  "integration.runs.status.cancelled": "cancelled",
@@ -928,6 +942,8 @@
928
942
  "integration.runs.table.column.trigger": "Trigger",
929
943
  "integration.runs.table.column.workflow": "Workflow",
930
944
  "integration.runs.table.empty": "No runs yet — kick off a workflow to populate this view.",
945
+ "integration.runs.table.empty.narrowed": "No runs match this narrowing.",
946
+ "integration.runs.table.filter.all": "All",
931
947
  "integration.runs.table.search.placeholder": "Search runs…",
932
948
  "integration.runs.tabs.runs.label": "Runs",
933
949
  "integration.runs.toast.action.requested": "{action} requested",
@@ -990,6 +1006,7 @@
990
1006
  "integration.secrets.error.loadFailed": "Could not load secrets.",
991
1007
  "integration.secrets.error.valueRequired": "Value is required.",
992
1008
  "integration.secrets.form.error.invalidKey": "Key must match ^[A-Z0-9_]+$ (uppercase letters, digits, underscores).",
1009
+ "integration.secrets.form.error.saveFailed": "Could not save the secret.",
993
1010
  "integration.secrets.form.key.label": "Key",
994
1011
  "integration.secrets.form.key.patternLabel": "Pattern:",
995
1012
  "integration.secrets.form.value.hide": "Hide value",
@@ -1128,7 +1145,12 @@
1128
1145
  "integration.workflows.create.template.noMatch": "Nothing matches “{query}”.",
1129
1146
  "integration.workflows.create.template.search.aria": "Search templates",
1130
1147
  "integration.workflows.create.template.search.placeholder": "Search templates…",
1148
+ "integration.workflows.create.template.state.loading": "Loading templates…",
1131
1149
  "integration.workflows.create.toast.templateError": "Could not create the workflow from this template.",
1150
+ "integration.workflows.delete.action.deleting": "Deleting…",
1151
+ "integration.workflows.delete.dialog.description": "and all its revision history will be permanently deleted.",
1152
+ "integration.workflows.delete.dialog.title": "Delete workflow?",
1153
+ "integration.workflows.delete.error.generic": "The workflow could not be deleted.",
1132
1154
  "integration.workflows.duplicate.dialog.description": "Creates a paused copy of “{name}” with all triggers off.",
1133
1155
  "integration.workflows.duplicate.dialog.title": "Duplicate workflow",
1134
1156
  "integration.workflows.duplicate.error.generic": "Could not duplicate the workflow.",
@@ -1174,9 +1196,6 @@
1174
1196
  "integration.workflows.table.column.status": "Status",
1175
1197
  "integration.workflows.table.column.triggers": "Triggers",
1176
1198
  "integration.workflows.table.column.updated": "Updated",
1177
- "integration.workflows.table.deleteDialog.action.deleting": "Deleting…",
1178
- "integration.workflows.table.deleteDialog.description": "and all its revision history will be permanently deleted.",
1179
- "integration.workflows.table.deleteDialog.title": "Delete workflow?",
1180
1199
  "integration.workflows.table.group.noTags": "No tags",
1181
1200
  "integration.workflows.table.lastRun.aria.never": "{name} last run: Never run",
1182
1201
  "integration.workflows.table.menu.action.activate": "Activate",
@@ -10,7 +10,7 @@ import {
10
10
  Play as Run
11
11
  } from "#solar-icons";
12
12
  import { Badge, Button } from "@revenexx/studio";
13
- import { toast } from "vue-sonner";
13
+ import { copyToClipboard } from "../../utils/clipboard";
14
14
  const t = useIntegrationsT();
15
15
  useHead({ title: () => t("integration.audit.head.title") });
16
16
  const { entries, loading, error, hasMore, load, loadMore } = useIntegrationsAudit();
@@ -76,11 +76,12 @@ const columns = computed(() => [
76
76
  { key: "when", label: t("integration.audit.table.column.when"), width: "180px", sortable: true, sortGetter: (e) => e.created_at }
77
77
  ]);
78
78
  const selected = ref(null);
79
- function copy(t2) {
80
- if (t2 && navigator.clipboard) {
81
- navigator.clipboard.writeText(t2);
82
- toast.success(t("integration.audit.toast.copied"));
83
- }
79
+ function copy(text) {
80
+ if (!text) return;
81
+ void copyToClipboard(text, {
82
+ copied: t("integration.audit.toast.copied"),
83
+ failed: t("integration.audit.toast.copyFailed")
84
+ });
84
85
  }
85
86
  const panelEl = ref(null);
86
87
  let opener = null;
@@ -31,7 +31,7 @@ import {
31
31
  SelectValue
32
32
  } from "@revenexx/studio";
33
33
  import { toast } from "../../utils/notify";
34
- import { apiFieldErrors, refusalReason } from "../../utils/apiError";
34
+ import { apiFieldErrors, errorReason } from "../../utils/apiError";
35
35
  import { CREDENTIAL_STATES, credentialState, credentialStateLabel } from "../../utils/credentialState";
36
36
  import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
37
37
  import { resolveLocalized } from "../../utils/localize";
@@ -177,35 +177,23 @@ async function submit() {
177
177
  }
178
178
  } catch (e) {
179
179
  fieldErrors.value = apiFieldErrors(e);
180
- saveError.value = refusalReason(e, t("integration.credentials.error.save"));
180
+ saveError.value = errorReason(e, t("integration.credentials.error.save"));
181
181
  toast.error(saveError.value);
182
182
  } finally {
183
183
  saving.value = false;
184
184
  }
185
185
  }
186
186
  const connecting = ref(null);
187
+ const { open: openAuthorisation } = useOAuthConnect(async (outcome) => {
188
+ await load(true);
189
+ if (outcome === "completed") toast.success(t("integration.credentials.toast.connected"));
190
+ });
187
191
  async function connect(c) {
188
192
  connecting.value = c.id;
189
193
  try {
190
- const authorizeUrl = await oauthAuthorizeUrl(c.id);
191
- const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
192
- const onMessage = async (ev) => {
193
- if (ev.data?.type === "integrations-oauth-complete") {
194
- window.removeEventListener("message", onMessage);
195
- await load(true);
196
- toast.success(t("integration.credentials.toast.connected"));
197
- }
198
- };
199
- window.addEventListener("message", onMessage);
200
- const poll = window.setInterval(async () => {
201
- if (popup?.closed) {
202
- window.clearInterval(poll);
203
- window.removeEventListener("message", onMessage);
204
- await load(true);
205
- }
206
- }, 1e3);
194
+ openAuthorisation(await oauthAuthorizeUrl(c.id));
207
195
  } catch (e) {
208
- toast.error(e?.message ?? t("integration.credentials.error.oauthConnect"));
196
+ toast.error(errorReason(e, t("integration.credentials.error.oauthConnect")));
209
197
  } finally {
210
198
  connecting.value = null;
211
199
  }
@@ -221,7 +209,7 @@ async function confirmDelete() {
221
209
  toast.success(t("integration.credentials.toast.deleted"));
222
210
  pendingDelete.value = null;
223
211
  } catch (e) {
224
- toast.error(e?.message ?? t("integration.credentials.error.delete"));
212
+ toast.error(errorReason(e, t("integration.credentials.error.delete")));
225
213
  } finally {
226
214
  deleting.value = false;
227
215
  }