@revenexx/studio-integrations 1.3.0 → 1.4.1

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 (62) hide show
  1. package/CHANGELOG.md +457 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
  4. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
  5. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
  6. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +61 -29
  7. package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
  8. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +10 -0
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue +94 -49
  10. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +10 -0
  11. package/dist/runtime/components/Integrations/NodeInspector.vue +37 -60
  12. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  13. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  14. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  15. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  16. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  17. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  18. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  19. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  20. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  21. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  22. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  23. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  24. package/dist/runtime/components/Integrations/WorkflowEditor.vue +165 -56
  25. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  26. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  27. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  28. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  29. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  30. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  31. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  32. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  33. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  34. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  35. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  36. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  37. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -8
  38. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  39. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -7
  40. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  41. package/dist/runtime/data/canvasShortcuts.js +68 -0
  42. package/dist/runtime/i18n/messages/de.json +30 -16
  43. package/dist/runtime/i18n/messages/en.json +30 -16
  44. package/dist/runtime/pages/integrations/credentials.vue +40 -18
  45. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  46. package/dist/runtime/pages/integrations/workflows/[id].vue +128 -33
  47. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  48. package/dist/runtime/utils/apiEnvelope.js +20 -0
  49. package/dist/runtime/utils/apiError.d.ts +35 -8
  50. package/dist/runtime/utils/apiError.js +12 -12
  51. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  52. package/dist/runtime/utils/configDefaults.js +7 -0
  53. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  54. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  55. package/dist/runtime/utils/notify.d.ts +2 -2
  56. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  57. package/dist/runtime/utils/runEnvelope.js +2 -1
  58. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  59. package/dist/runtime/utils/selectOptions.js +19 -0
  60. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  61. package/dist/runtime/utils/shortcutLabel.js +48 -0
  62. package/package.json +1 -1
@@ -14,7 +14,6 @@ import {
14
14
  RefreshCircle as Loader2
15
15
  } from "#solar-icons";
16
16
  import { Badge, Button } from "@revenexx/studio";
17
- import { toast } from "vue-sonner";
18
17
  import "@vue-flow/core/dist/style.css";
19
18
  import "@vue-flow/core/dist/theme-default.css";
20
19
  import WorkflowNodeView from "./WorkflowNodeView.vue";
@@ -27,7 +26,6 @@ import { findManifest } from "../../data/integrationNodeCatalog";
27
26
  import { OPPOSITE_SIDE } from "./canvasSides";
28
27
  import { inputHandleId, outputHandleId } from "../../data/handleId";
29
28
  import { nodeLoopData } from "../../data/loopBody";
30
- import { triggerGlyph } from "../../data/triggerGlyphs";
31
29
  import { formatDuration } from "../../utils/formatDuration";
32
30
  import { SAFETY_POLL_MS } from "../../utils/pollBudget";
33
31
  import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
@@ -37,7 +35,7 @@ const props = defineProps({
37
35
  initialRunId: { type: [String, null], required: false },
38
36
  single: { type: Boolean, required: false }
39
37
  });
40
- const { loadForWorkflow, getSteps } = useIntegrationsRuns();
38
+ const { getSteps } = useIntegrationsRuns();
41
39
  const t = useIntegrationsT();
42
40
  const TERMINAL = /* @__PURE__ */ new Set(["completed", "succeeded", "failed", "cancelled", "terminated"]);
43
41
  const STEPS_POLL_MS = 4e3;
@@ -46,33 +44,7 @@ const { delivering: realtimeDelivering, runTick, lastRunEvent } = useIntegration
46
44
  function stepsPollDelay() {
47
45
  return realtimeDelivering.value ? SAFETY_POLL_MS : STEPS_POLL_MS;
48
46
  }
49
- const runs = ref([]);
50
- const runsPage = ref(1);
51
- const runsLastPage = ref(1);
52
- const runsLoading = ref(false);
53
47
  const selectedRunId = ref(null);
54
- async function loadRuns(reset = false) {
55
- if (runsLoading.value) return;
56
- runsLoading.value = true;
57
- try {
58
- const res = await loadForWorkflow(props.workflow.id, props.workflow.name, {
59
- page: reset ? 1 : runsPage.value,
60
- reconcile: true
61
- });
62
- runs.value = reset ? res.runs : [...runs.value, ...res.runs];
63
- runsPage.value = res.page;
64
- runsLastPage.value = res.lastPage;
65
- } catch (e) {
66
- toast.error(errorReason(e, t("integration.replay.toast.loadRunsFailed")));
67
- } finally {
68
- runsLoading.value = false;
69
- }
70
- }
71
- function loadMoreRuns() {
72
- if (runsPage.value >= runsLastPage.value) return;
73
- runsPage.value += 1;
74
- void loadRuns();
75
- }
76
48
  const steps = ref([]);
77
49
  const stepsSource = ref(null);
78
50
  const runStatus = ref(null);
@@ -382,14 +354,6 @@ const edgeTypes = {
382
354
  preview.onNodeClick((evt) => {
383
355
  if (evt.node.type === "workflow") onNodeClick(evt.node.id);
384
356
  });
385
- function formatDateTime(iso) {
386
- if (!iso) return "\u2014";
387
- try {
388
- return new Date(iso).toLocaleString();
389
- } catch {
390
- return String(iso);
391
- }
392
- }
393
357
  function formatTime(iso) {
394
358
  if (!iso) return "\u2014";
395
359
  try {
@@ -405,30 +369,16 @@ function pretty(v) {
405
369
  return String(v);
406
370
  }
407
371
  }
408
- function triggerIcon(type) {
409
- return triggerGlyph(type);
410
- }
411
372
  const activeStepTitle = computed(() => {
412
373
  const s = activeStep.value;
413
374
  if (!s) return "";
414
375
  return `${s.node_slug || s.node_id}${s.attempt > 0 ? ` (#${s.attempt + 1})` : ""}`;
415
376
  });
416
- onMounted(async () => {
417
- if (props.single && props.initialRunId) {
418
- selectRun(props.initialRunId);
419
- return;
420
- }
421
- await loadRuns(true);
422
- const initial = props.initialRunId && runs.value.some((r) => r.id === props.initialRunId) ? props.initialRunId : runs.value[0]?.id;
423
- if (initial) selectRun(initial);
377
+ onMounted(() => {
378
+ if (props.initialRunId) selectRun(props.initialRunId);
424
379
  });
425
380
  watch(() => props.initialRunId, (id) => {
426
- if (!id) return;
427
- if (props.single) {
428
- selectRun(id);
429
- return;
430
- }
431
- if (runs.value.some((r) => r.id === id)) selectRun(id);
381
+ if (id) selectRun(id);
432
382
  });
433
383
  </script>
434
384
 
@@ -589,58 +539,6 @@ watch(() => props.initialRunId, (id) => {
589
539
  </p>
590
540
  </aside>
591
541
 
592
- <!-- Run list — hidden in single mode (the run detail replays ONE run). -->
593
- <aside v-if="!single" class="flex w-80 shrink-0 flex-col border-l border-black/[0.06] bg-white">
594
- <div class="border-b border-black/[0.06] px-3 py-2">
595
- <div class="text-sm font-semibold">{{ t("integration.replay.runsList.heading") }}</div>
596
- <p class="text-sm text-muted-foreground">
597
- {{ t("integration.replay.runsList.description") }}
598
- </p>
599
- </div>
600
-
601
- <div class="flex-1 space-y-1 overflow-y-auto p-2">
602
- <button
603
- v-for="r in runs"
604
- :key="r.id"
605
- type="button"
606
- class="w-full rounded-lg border p-2.5 text-left transition"
607
- :class="selectedRunId === r.id ? 'border-primary/40 bg-primary/[0.04]' : 'border-transparent hover:border-black/[0.08] hover:bg-muted/30'"
608
- @click="selectRun(r.id)"
609
- >
610
- <div class="flex items-center justify-between gap-2">
611
- <span class="flex min-w-0 items-center gap-1.5 text-sm font-medium">
612
- <component :is="triggerIcon(r.triggerType)" :size="12" class="shrink-0 text-muted-foreground" />
613
- <span class="truncate">#{{ r.id }}</span>
614
- </span>
615
- <Badge variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="runStatusClass(r.status)">
616
- {{ runStatusLabel(r.status) }}
617
- </Badge>
618
- </div>
619
- <div class="mt-0.5 flex items-center justify-between gap-2 text-sm text-muted-foreground">
620
- <span class="tabular-nums">{{ formatDateTime(r.startedAt) }}</span>
621
- <span class="tabular-nums">{{ formatDuration(r.durationMs) }}</span>
622
- </div>
623
- </button>
624
-
625
- <p v-if="!runs.length && !runsLoading" class="px-2 py-6 text-center text-sm text-muted-foreground">
626
- {{ t("integration.replay.runsList.empty") }}
627
- </p>
628
-
629
- <div v-if="runsLoading" class="flex justify-center py-3">
630
- <Loader2 :size="15" class="animate-spin text-muted-foreground" />
631
- </div>
632
- <Button
633
- v-else-if="runsPage < runsLastPage"
634
- variant="ghost"
635
- size="sm"
636
- class="w-full text-sm"
637
- @click="loadMoreRuns"
638
- >
639
- {{ t("integration.replay.runsList.loadMore") }}
640
- </Button>
641
- </div>
642
- </aside>
643
-
644
542
  </div>
645
543
  </template>
646
544
 
@@ -3,9 +3,13 @@ import '@vue-flow/core/dist/theme-default.css';
3
3
  import type { Workflow } from '../../types/integrations.js';
4
4
  type __VLS_Props = {
5
5
  workflow: Workflow;
6
- /** Preselect a run (deep-link from the Runs table). */
6
+ /** The run to replay (deep-link from the Runs table). */
7
7
  initialRunId?: string | null;
8
- /** Replay ONLY the initial run: no run list, no browsing (run detail view). */
8
+ /**
9
+ * @deprecated Accepted and ignored since PO-350 — the panel has no other mode
10
+ * to switch out of. Kept so a host passing it keeps working; drop it at the
11
+ * next major.
12
+ */
9
13
  single?: boolean;
10
14
  };
11
15
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -14,7 +14,9 @@ import {
14
14
  DropdownMenuItem,
15
15
  DropdownMenuLabel,
16
16
  DropdownMenuSeparator,
17
- DropdownMenuTrigger
17
+ DropdownMenuTrigger,
18
+ Label,
19
+ Switch
18
20
  } from "@revenexx/studio";
19
21
  defineProps({
20
22
  workflows: { type: Array, required: true },
@@ -247,16 +249,25 @@ const columns = computed(() => [
247
249
  @row-click="(wf2) => router.push(`/integrations/workflows/${wf2.id}`)"
248
250
  >
249
251
  <template #toolbar-left>
250
- <Button
251
- variant="outline"
252
- size="sm"
253
- class="h-7 gap-1.5 text-sm"
254
- :class="groupByTags ? 'border-primary/30 text-primary' : ''"
255
- @click="groupByTags = !groupByTags"
256
- >
257
- <Layers :size="13" />
258
- {{ t("integration.workflows.table.action.groupByTags") }}
259
- </Button>
252
+ <!-- A switch rather than a button: the control is a state, not an action.
253
+ A button labelled "Group by tags" ungroups once grouping is on, and
254
+ said so neither to a reader nor to a screen reader — the state lived
255
+ in a colour class alone. The Label is what names the switch. -->
256
+ <div class="flex h-7 items-center gap-2">
257
+ <Switch
258
+ id="wf-group-by-tags"
259
+ :model-value="groupByTags"
260
+ @update:model-value="(on) => groupByTags = on"
261
+ />
262
+ <Label
263
+ for="wf-group-by-tags"
264
+ class="flex cursor-pointer items-center gap-1.5 text-sm font-normal"
265
+ :class="groupByTags ? 'text-primary' : 'text-muted-foreground'"
266
+ >
267
+ <Layers :size="13" />
268
+ {{ t("integration.workflows.table.action.groupByTags") }}
269
+ </Label>
270
+ </div>
260
271
  </template>
261
272
 
262
273
  <!-- Name (icon + title + description) -->
@@ -32,6 +32,40 @@ export type ShortcutHandler = ((event: KeyboardEvent) => void) | {
32
32
  run: (event: KeyboardEvent) => void;
33
33
  };
34
34
  export type ShortcutMap = Record<string, ShortcutHandler>;
35
+ /**
36
+ * A map key in its comparable form.
37
+ *
38
+ * Exported for the unit suite: the whole layout question below is decided here
39
+ * and in {@link shortcutCandidates}, and both are cheaper to prove against
40
+ * hand-built events than against a browser per keyboard layout.
41
+ */
42
+ export declare function normalizeShortcut(shortcut: string): string;
43
+ /** Everything a match reads from a keydown — so the rule needs no DOM to prove. */
44
+ export interface ShortcutKeyEvent {
45
+ key: string;
46
+ code?: string;
47
+ shiftKey?: boolean;
48
+ altKey?: boolean;
49
+ metaKey?: boolean;
50
+ ctrlKey?: boolean;
51
+ }
52
+ /**
53
+ * The two forms a keydown is looked up under.
54
+ *
55
+ * `byKey` is what the layout PRODUCED and `byCode` is which key was pressed. Both
56
+ * are needed and neither is enough: a Dvorak or AZERTY layout moves the letters,
57
+ * so a letter binding is only found by key — while a character a layout reaches
58
+ * through a modifier, or a dead key that produces no useful character at all
59
+ * (⌥⇧T is `ˇ`), is only found by code.
60
+ *
61
+ * Shift is dropped from `byKey` alone, where it is part of the character. `byCode`
62
+ * stays strict: there the physical key says nothing about what was produced, so
63
+ * Shift is the only thing separating `shift+s` from `s`.
64
+ */
65
+ export declare function shortcutCandidates(event: ShortcutKeyEvent, isMac?: boolean): {
66
+ byKey: string;
67
+ byCode: string;
68
+ };
35
69
  export declare function useCanvasShortcuts(keymap: MaybeRefOrGetter<ShortcutMap>, options?: {
36
70
  disabled?: MaybeRefOrGetter<boolean>;
37
71
  }): void;
@@ -1,26 +1,29 @@
1
1
  import { computed, onBeforeUnmount, onMounted, toValue } from "vue";
2
- const IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
2
+ import { IS_MAC, shortcutTokens } from "../utils/shortcutLabel.js";
3
3
  function canonical(parts) {
4
4
  return parts.map((p) => p.toLowerCase()).sort((a, b) => a.localeCompare(b)).join("+");
5
5
  }
6
- function normalizeShortcut(shortcut) {
7
- if (shortcut.length === 1) return shortcut.toLowerCase();
8
- return canonical(shortcut.split("+"));
6
+ export function normalizeShortcut(shortcut) {
7
+ return canonical(shortcutTokens(shortcut));
9
8
  }
10
9
  function codeToKey(code) {
11
10
  if (code.startsWith("Key")) return code.slice(3).toLowerCase();
12
11
  if (code.startsWith("Digit")) return code.slice(5);
13
12
  return code.toLowerCase();
14
13
  }
15
- function eventShortcuts(event) {
14
+ function shiftMadeTheCharacter(loweredKey) {
15
+ return loweredKey.length === 1 && !/^[\p{L}\p{N}]$/u.test(loweredKey);
16
+ }
17
+ export function shortcutCandidates(event, isMac = IS_MAC) {
16
18
  const mods = [];
17
19
  if (event.shiftKey) mods.push("shift");
18
20
  if (event.altKey) mods.push("alt");
19
- if (IS_MAC ? event.metaKey : event.ctrlKey) mods.push("mod");
21
+ if ((isMac ? event.metaKey : event.ctrlKey) ?? false) mods.push("mod");
20
22
  const key = event.key === " " ? "space" : event.key.toLowerCase();
21
23
  const code = codeToKey(event.code ?? "");
24
+ const keyMods = shiftMadeTheCharacter(key) ? mods.filter((m) => m !== "shift") : mods;
22
25
  return {
23
- byKey: canonical([...mods, key]),
26
+ byKey: canonical([...keyMods, key]),
24
27
  byCode: canonical([...mods, code])
25
28
  };
26
29
  }
@@ -48,7 +51,7 @@ export function useCanvasShortcuts(keymap, options) {
48
51
  if (toValue(options?.disabled)) return;
49
52
  if (event.defaultPrevented) return;
50
53
  if (isEditingContext(event)) return;
51
- const { byKey, byCode } = eventShortcuts(event);
54
+ const { byKey, byCode } = shortcutCandidates(event);
52
55
  const handler = normalized.value[byKey] ?? normalized.value[byCode];
53
56
  if (!handler) return;
54
57
  const run = typeof handler === "function" ? handler : handler.disabled() ? void 0 : handler.run;
@@ -1,6 +1,7 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
2
  import { useIntegrationsT } from "./useIntegrationsT.js";
3
3
  import { errorReason } from "../utils/apiError.js";
4
+ import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
4
5
  import { useLoadGate } from "./useLoadGate.js";
5
6
  export function useCredentialTypes() {
6
7
  const types = useState("integrations.credentialTypes", () => []);
@@ -10,17 +11,11 @@ export function useCredentialTypes() {
10
11
  const gate = useLoadGate("integrations.credentialTypes", { loading, loaded });
11
12
  const api = useIntegrationsApi();
12
13
  const t = useIntegrationsT();
13
- function normalize(res) {
14
- const obj = res;
15
- if (Array.isArray(obj)) return obj;
16
- if (Array.isArray(obj?.data)) return obj.data;
17
- return [];
18
- }
19
14
  async function load(force = false) {
20
15
  if (!await gate.claim(force)) return types.value;
21
16
  error.value = null;
22
17
  try {
23
- types.value = normalize(await api.get("/credential-types"));
18
+ types.value = unwrapList(await api.get("/credential-types"));
24
19
  loaded.value = true;
25
20
  } catch (e) {
26
21
  error.value = errorReason(e, t("integration.credentials.error.loadTypes"));
@@ -36,8 +31,7 @@ export function useCredentialTypes() {
36
31
  const cached = bySlug(slug);
37
32
  if (cached) return cached;
38
33
  try {
39
- const res = await api.get(`/credential-types/${encodeURIComponent(slug)}`);
40
- return res?.data ?? res ?? null;
34
+ return unwrapRecord(await api.get(`/credential-types/${encodeURIComponent(slug)}`));
41
35
  } catch {
42
36
  return null;
43
37
  }
@@ -2,6 +2,7 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
5
+ import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
5
6
  import { useLoadGate } from "./useLoadGate.js";
6
7
  export function useIntegrationsCredentials() {
7
8
  const credentials = useState("integrations.credentials", () => []);
@@ -11,20 +12,11 @@ export function useIntegrationsCredentials() {
11
12
  const gate = useLoadGate("integrations.credentials", { loading, loaded });
12
13
  const api = useIntegrationsApi();
13
14
  const t = useIntegrationsT();
14
- function normalize(res) {
15
- const obj = res;
16
- if (Array.isArray(obj)) return obj;
17
- if (Array.isArray(obj?.data)) return obj.data;
18
- return [];
19
- }
20
- function unwrap(res) {
21
- return res?.data ?? res;
22
- }
23
15
  async function load(force = false) {
24
16
  if (!await gate.claim(force)) return credentials.value;
25
17
  error.value = null;
26
18
  try {
27
- credentials.value = normalize(await api.get("/credentials"));
19
+ credentials.value = unwrapList(await api.get("/credentials"));
28
20
  loaded.value = true;
29
21
  } catch (e) {
30
22
  error.value = errorReason(e, t("integration.credentials.error.loadFailed"));
@@ -37,15 +29,15 @@ export function useIntegrationsCredentials() {
37
29
  return credentials.value.filter((c) => c.credential_type_slug === slug);
38
30
  }
39
31
  async function get(id) {
40
- return unwrap(await api.get(`/credentials/${encodeURIComponent(id)}`));
32
+ return unwrapRecord(await api.get(`/credentials/${encodeURIComponent(id)}`));
41
33
  }
42
34
  async function create(input) {
43
- const created = unwrap(await api.post("/credentials", input));
35
+ const created = unwrapRecord(await api.post("/credentials", input));
44
36
  await load(true);
45
37
  return created;
46
38
  }
47
39
  async function update(id, input) {
48
- const updated = unwrap(await api.patch(`/credentials/${encodeURIComponent(id)}`, input));
40
+ const updated = unwrapRecord(await api.patch(`/credentials/${encodeURIComponent(id)}`, input));
49
41
  await load(true);
50
42
  return updated;
51
43
  }
@@ -71,7 +63,7 @@ export function useIntegrationsCredentials() {
71
63
  }
72
64
  async function oauthAuthorizeUrl(id) {
73
65
  const res = await api.post(`/credentials/${encodeURIComponent(id)}/oauth/authorize-url`);
74
- const authorizeUrl = res?.authorize_url ?? res?.data?.authorize_url;
66
+ const authorizeUrl = unwrapRecord(res).authorize_url;
75
67
  if (!authorizeUrl) throw new IntegrationsApiError(0, res, t("integration.credentials.error.noAuthorizeUrl"));
76
68
  return authorizeUrl;
77
69
  }
@@ -3,6 +3,7 @@ import { resolveLocalized } from "../utils/localize.js";
3
3
  import { useIntegrationsApi } from "./integrationsApi.js";
4
4
  import { useIntegrationsT } from "./useIntegrationsT.js";
5
5
  import { errorReason } from "../utils/apiError.js";
6
+ import { unwrapList } from "../utils/apiEnvelope.js";
6
7
  import { useLoadGate } from "./useLoadGate.js";
7
8
  const RETRY_COOLDOWN_MS = 3e4;
8
9
  export function useIntegrationsNodes() {
@@ -18,8 +19,7 @@ export function useIntegrationsNodes() {
18
19
  if (!await gate.claim(force)) return;
19
20
  error.value = null;
20
21
  try {
21
- const res = await api.get("/nodes");
22
- const list = Array.isArray(res) ? res : res?.data ?? [];
22
+ const list = unwrapList(await api.get("/nodes"));
23
23
  const manifests = list.filter((n) => n.manifest).map((n) => ({
24
24
  ...n.manifest,
25
25
  slug: n.manifest.slug ?? n.slug,
@@ -2,6 +2,8 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { errorReason } from "../utils/apiError.js";
5
+ import { unwrapRecord } from "../utils/apiEnvelope.js";
6
+ import { RUN_OWN_FIELDS } from "../utils/runEnvelope.js";
5
7
  import { useLoadGate } from "./useLoadGate.js";
6
8
  import {
7
9
  DEFAULT_RUN_DIRECTION,
@@ -93,9 +95,7 @@ export function useIntegrationsRuns() {
93
95
  const active = runs.value.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
94
96
  await Promise.allSettled(active.map(async (r) => {
95
97
  try {
96
- const raw = await api.get(`/workflows/${r.workflowId}/runs/${r.id}`);
97
- const apiRun = raw?.data ?? raw;
98
- const fresh = mapRun(apiRun, r.workflowName);
98
+ const fresh = await fetchRun(r.workflowId, r.id, r.workflowName);
99
99
  runs.value = runs.value.map((x) => x.id === fresh.id ? fresh : x);
100
100
  } catch {
101
101
  }
@@ -124,9 +124,7 @@ export function useIntegrationsRuns() {
124
124
  const active = list2.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
125
125
  await Promise.allSettled(active.map(async (r) => {
126
126
  try {
127
- const raw = await api.get(`/workflows/${workflowId}/runs/${r.id}`);
128
- const apiRun = raw?.data ?? raw;
129
- const fresh = mapRun(apiRun, workflowName);
127
+ const fresh = await fetchRun(workflowId, r.id, workflowName);
130
128
  list2 = list2.map((x) => x.id === fresh.id ? fresh : x);
131
129
  } catch {
132
130
  }
@@ -185,13 +183,11 @@ export function useIntegrationsRuns() {
185
183
  async function trigger(workflowId, payload = {}, mode) {
186
184
  const body = { trigger_payload: payload };
187
185
  if (mode) body.mode = mode;
188
- const res = await api.post(`/workflows/${workflowId}/runs`, body);
189
- return res?.data ?? res;
186
+ return unwrapRecord(await api.post(`/workflows/${workflowId}/runs`, body));
190
187
  }
191
188
  async function runAction(workflowId, runId, action, reason) {
192
189
  const body = reason ? { reason } : {};
193
- const res = await api.post(`/workflows/${workflowId}/runs/${runId}/${action}`, body);
194
- return res?.data ?? res;
190
+ return unwrapRecord(await api.post(`/workflows/${workflowId}/runs/${runId}/${action}`, body));
195
191
  }
196
192
  const cancelRun = (workflowId, runId, reason) => runAction(workflowId, runId, "cancel", reason);
197
193
  const terminateRun = (workflowId, runId, reason) => runAction(workflowId, runId, "terminate", reason);
@@ -199,13 +195,12 @@ export function useIntegrationsRuns() {
199
195
  const resumeRun = (workflowId, runId, reason) => runAction(workflowId, runId, "resume", reason);
200
196
  const getRun = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}`);
201
197
  async function fetchRun(workflowId, runId, workflowName) {
202
- const raw = await getRun(workflowId, runId);
203
- const apiRun = raw?.data ?? raw;
198
+ const apiRun = unwrapRecord(await getRun(workflowId, runId), RUN_OWN_FIELDS);
204
199
  return mapRun(apiRun, workflowName);
205
200
  }
206
201
  const getDetails = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/details`);
207
202
  const getSteps = (workflowId, runId, includePayloads = false) => api.get(
208
- `/workflows/${workflowId}/runs/${runId}/steps${includePayloads ? "?include_payloads=1" : ""}`
203
+ `/workflows/${workflowId}/runs/${runId}/steps?include_payloads=${includePayloads ? "1" : "0"}`
209
204
  );
210
205
  const getHistory = (workflowId, runId, params = {}) => {
211
206
  const qs = new URLSearchParams();
@@ -2,6 +2,7 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { errorReason } from "../utils/apiError.js";
5
+ import { unwrapList } from "../utils/apiEnvelope.js";
5
6
  import { useLoadGate } from "./useLoadGate.js";
6
7
  const KEY_PATTERN = /^[A-Z0-9_]+$/;
7
8
  export function useIntegrationsSecrets() {
@@ -17,11 +18,10 @@ export function useIntegrationsSecrets() {
17
18
  }
18
19
  function normalize(res) {
19
20
  const obj = res;
20
- if (Array.isArray(obj?.data)) return obj.data.map(toRecord);
21
- if (Array.isArray(obj?.secrets)) return obj.secrets.map(toRecord);
22
- if (Array.isArray(obj?.keys)) return obj.keys.map((key) => ({ key, created_at: "", updated_at: "" }));
23
- if (Array.isArray(res)) return res.map(toRecord);
24
- return [];
21
+ if (obj?.data === void 0 && Array.isArray(obj?.keys)) {
22
+ return obj.keys.map((key) => ({ key, created_at: "", updated_at: "" }));
23
+ }
24
+ return unwrapList(res).map(toRecord);
25
25
  }
26
26
  function toRecord(r) {
27
27
  return { key: r.key ?? "", created_at: r.created_at ?? "", updated_at: r.updated_at ?? "" };
@@ -1,6 +1,7 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { errorReason } from "../utils/apiError.js";
4
+ import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
4
5
  import { resolveLocalized } from "../utils/localize.js";
5
6
  import { useLoadGate } from "./useLoadGate.js";
6
7
  const BLOB_VERSION = "v0-draft";
@@ -102,14 +103,11 @@ export function useIntegrationsTemplates() {
102
103
  const error = useState("integrations.templates.error", () => null);
103
104
  const gate = useLoadGate("integrations.templates", { loading, loaded });
104
105
  const api = useIntegrationsApi();
105
- function unwrap(res) {
106
- return res?.data ?? res;
107
- }
108
106
  async function load(force = false) {
109
107
  if (!await gate.claim(force)) return templates.value;
110
108
  error.value = null;
111
109
  try {
112
- const list2 = unwrap(await api.get("/templates")) ?? [];
110
+ const list2 = unwrapList(await api.get("/templates"));
113
111
  rawList.value = list2;
114
112
  rawDetails.value = {};
115
113
  loaded.value = true;
@@ -133,7 +131,7 @@ export function useIntegrationsTemplates() {
133
131
  const base = get(slug);
134
132
  if (!base?.fromApi) return base;
135
133
  try {
136
- const detail = unwrap(await api.get(`/templates/${slug}`));
134
+ const detail = unwrapRecord(await api.get(`/templates/${slug}`));
137
135
  rawDetails.value = { ...rawDetails.value, [slug]: detail };
138
136
  return get(slug);
139
137
  } catch {
@@ -152,8 +150,7 @@ export function useIntegrationsTemplates() {
152
150
  }
153
151
  async function requirements(slug) {
154
152
  try {
155
- const raw = unwrap(await api.get(`/templates/${slug}/requirements`));
156
- if (!raw) return null;
153
+ const raw = unwrapRecord(await api.get(`/templates/${slug}/requirements`));
157
154
  return {
158
155
  ...raw,
159
156
  credentialTypes: (raw.credentialTypes ?? []).map((c) => ({
@@ -172,7 +169,7 @@ export function useIntegrationsTemplates() {
172
169
  Object.entries(opts.credentialBindings ?? {}).filter(([, v]) => v)
173
170
  );
174
171
  if (Object.keys(bindings).length) body.credential_bindings = bindings;
175
- const created = unwrap(await api.post(`/templates/${slug}/instantiate`, body));
172
+ const created = unwrapRecord(await api.post(`/templates/${slug}/instantiate`, body));
176
173
  return Number(created.id);
177
174
  }
178
175
  async function install(slug) {
@@ -1,6 +1,7 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
2
  import { useIntegrationsT } from "./useIntegrationsT.js";
3
3
  import { errorReason } from "../utils/apiError.js";
4
+ import { unwrapList } from "../utils/apiEnvelope.js";
4
5
  import { useLoadGate } from "./useLoadGate.js";
5
6
  export function tenantTriggers(workflows, byWorkflow) {
6
7
  return workflows.flatMap((wf) => (byWorkflow[wf.id] ?? []).flatMap((t) => {
@@ -24,10 +25,6 @@ export function useIntegrationsTriggers() {
24
25
  const t = useIntegrationsT();
25
26
  const { workflows, apiTriggers, load: loadWorkflows, loaded: workflowsLoaded, error: workflowsError } = useIntegrationsWorkflows();
26
27
  const gate = useLoadGate("integrations.triggers", { loading, loaded });
27
- function unwrapList(res) {
28
- if (Array.isArray(res)) return res;
29
- return res?.data ?? [];
30
- }
31
28
  async function list(workflowId) {
32
29
  return unwrapList(await api.get(`/workflows/${workflowId}/triggers`));
33
30
  }
@@ -3,6 +3,7 @@ import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { deepClone } from "../utils/deepClone.js";
5
5
  import { errorReason } from "../utils/apiError.js";
6
+ import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
6
7
  import { useLoadGate } from "./useLoadGate.js";
7
8
  const BLOB_VERSION = "v0-draft";
8
9
  function toCockpit(api) {
@@ -93,9 +94,6 @@ export function useIntegrationsWorkflows() {
93
94
  const gate = useLoadGate("integrations.workflows", { loading, loaded });
94
95
  const api = useIntegrationsApi();
95
96
  const t = useIntegrationsT();
96
- function unwrap(res) {
97
- return res?.data ?? res;
98
- }
99
97
  function remember(api2, into) {
100
98
  if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
101
99
  }
@@ -122,7 +120,7 @@ export function useIntegrationsWorkflows() {
122
120
  if (!await gate.claim(force)) return workflows.value;
123
121
  error.value = null;
124
122
  try {
125
- const list2 = unwrap(await api.get("/workflows")) ?? [];
123
+ const list2 = unwrapList(await api.get("/workflows"));
126
124
  workflows.value = ingestList(list2);
127
125
  loaded.value = true;
128
126
  } catch (e) {
@@ -139,7 +137,7 @@ export function useIntegrationsWorkflows() {
139
137
  }
140
138
  async function get(id) {
141
139
  try {
142
- const wf = ingest(unwrap(await api.get(`/workflows/${id}`)));
140
+ const wf = ingest(unwrapRecord(await api.get(`/workflows/${id}`)));
143
141
  upsertCache(wf);
144
142
  return wf;
145
143
  } catch {
@@ -147,7 +145,7 @@ export function useIntegrationsWorkflows() {
147
145
  }
148
146
  }
149
147
  async function create(input) {
150
- const wf = ingest(unwrap(await api.post("/workflows", toPayload({
148
+ const wf = ingest(unwrapRecord(await api.post("/workflows", toPayload({
151
149
  name: input.name,
152
150
  description: input.description,
153
151
  blob: input.blob,
@@ -198,7 +196,7 @@ export function useIntegrationsWorkflows() {
198
196
  throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
199
197
  }
200
198
  async function update(id, patch) {
201
- const wf = ingest(unwrap(await api.put(`/workflows/${id}`, toPayload({
199
+ const wf = ingest(unwrapRecord(await api.put(`/workflows/${id}`, toPayload({
202
200
  name: patch.name,
203
201
  description: patch.description,
204
202
  blob: patch.blob,