@revenexx/studio-integrations 1.1.1 → 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.
Files changed (119) hide show
  1. package/CHANGELOG.md +638 -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 +45 -19
  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/NodeConfigDynamicSchema.vue +10 -2
  16. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +88 -1
  18. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
  19. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +7 -3
  20. package/dist/runtime/components/Integrations/NodeInspector.vue +125 -54
  21. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +7 -3
  22. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  23. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  24. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  25. package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
  26. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  27. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  28. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  29. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  30. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  31. package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
  32. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  33. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  34. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  35. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  36. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  38. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  39. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  40. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  41. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  42. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  43. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  44. package/dist/runtime/components/Integrations/WorkflowEditor.vue +78 -30
  45. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  46. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  47. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  48. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  49. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  50. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  51. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  52. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  53. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  54. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  55. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  56. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  57. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  58. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  59. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  60. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  61. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  62. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  63. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  64. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  65. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  66. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  67. package/dist/runtime/composables/useIntegrationsTemplates.js +7 -5
  68. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  69. package/dist/runtime/composables/useIntegrationsWorkflows.js +10 -7
  70. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  71. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  72. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  73. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  74. package/dist/runtime/data/expressionEditor.js +11 -5
  75. package/dist/runtime/data/handleId.d.ts +61 -1
  76. package/dist/runtime/data/handleId.js +8 -1
  77. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  78. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  79. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  80. package/dist/runtime/data/nodeVersionMove.js +65 -0
  81. package/dist/runtime/data/payloadForm.js +3 -2
  82. package/dist/runtime/i18n/messages/de.json +79 -12
  83. package/dist/runtime/i18n/messages/en.json +79 -12
  84. package/dist/runtime/pages/integrations/audit.vue +7 -6
  85. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  86. package/dist/runtime/pages/integrations/index.vue +55 -8
  87. package/dist/runtime/pages/integrations/runs.vue +116 -7
  88. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  90. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +169 -24
  92. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  93. package/dist/runtime/types/integrations.d.ts +49 -1
  94. package/dist/runtime/utils/apiError.d.ts +21 -1
  95. package/dist/runtime/utils/apiError.js +1 -1
  96. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  97. package/dist/runtime/utils/buildSettle.js +29 -0
  98. package/dist/runtime/utils/clipboard.d.ts +33 -0
  99. package/dist/runtime/utils/clipboard.js +11 -0
  100. package/dist/runtime/utils/deepClone.d.ts +41 -0
  101. package/dist/runtime/utils/deepClone.js +3 -0
  102. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  103. package/dist/runtime/utils/formatDuration.js +13 -0
  104. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  105. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  106. package/dist/runtime/utils/notify.d.ts +1 -1
  107. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  108. package/dist/runtime/utils/runEnvelope.js +12 -0
  109. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  110. package/dist/runtime/utils/runFeedQuery.js +66 -0
  111. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  112. package/dist/runtime/utils/runOutputs.js +10 -0
  113. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  114. package/dist/runtime/utils/settingCondition.js +84 -0
  115. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  116. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  117. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  118. package/dist/runtime/utils/validationFindings.js +18 -3
  119. package/package.json +1 -1
@@ -0,0 +1,216 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import { AddCircle as Plus, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
+ import {
5
+ Button,
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ Input,
13
+ Label,
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue
19
+ } from "@revenexx/studio";
20
+ import { deepClone } from "../../utils/deepClone";
21
+ import { stateNamespaceErrorKey } from "../../utils/stateNamespaceName";
22
+ const props = defineProps({
23
+ open: { type: Boolean, required: true },
24
+ state: { type: Array, required: true }
25
+ });
26
+ const emit = defineEmits(["update:open", "update:state"]);
27
+ const t = useIntegrationsT();
28
+ const ROLES = ["mapping", "cursor", "dedupe", "digest"];
29
+ const working = ref([]);
30
+ watch(() => props.open, (isOpen) => {
31
+ if (!isOpen) return;
32
+ working.value = deepClone(props.state ?? []);
33
+ newName.value = "";
34
+ newRole.value = "mapping";
35
+ newVisibility.value = "private";
36
+ });
37
+ const changed = computed(() => JSON.stringify(working.value) !== JSON.stringify(props.state ?? []));
38
+ function applyAndClose() {
39
+ if (changed.value) emit("update:state", working.value);
40
+ emit("update:open", false);
41
+ }
42
+ function setVisibility(index, visibility) {
43
+ const next = [...working.value];
44
+ const row = next[index];
45
+ if (!row) return;
46
+ next[index] = { ...row, visibility };
47
+ working.value = next;
48
+ }
49
+ function removeNamespace(index) {
50
+ working.value = working.value.filter((_, i) => i !== index);
51
+ }
52
+ const newName = ref("");
53
+ const newRole = ref("mapping");
54
+ const newVisibility = ref("private");
55
+ const newNameError = computed(() => {
56
+ const key = stateNamespaceErrorKey(newName.value, working.value.map((n) => n.namespace));
57
+ return key ? t(key) : null;
58
+ });
59
+ function addNamespace() {
60
+ const namespace = newName.value.trim();
61
+ if (!namespace || newNameError.value) return;
62
+ working.value = [...working.value, { namespace, role: newRole.value, visibility: newVisibility.value }];
63
+ newName.value = "";
64
+ }
65
+ </script>
66
+
67
+ <template>
68
+ <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
69
+ <DialogContent class="max-w-[94vw] sm:max-w-3xl">
70
+ <DialogHeader>
71
+ <DialogTitle>{{ t("integration.workflows.state.dialog.title") }}</DialogTitle>
72
+ <DialogDescription class="text-sm">
73
+ {{ t("integration.workflows.state.dialog.description") }}
74
+ </DialogDescription>
75
+ </DialogHeader>
76
+
77
+ <div class="max-h-[62vh] space-y-3 overflow-y-auto pr-1">
78
+ <p v-if="!working.length" class="rounded-lg border border-dashed border-black/[0.12] px-4 py-6 text-center text-sm text-muted-foreground">
79
+ {{ t("integration.workflows.state.empty") }}
80
+ </p>
81
+
82
+ <div
83
+ v-for="(row, index) in working"
84
+ :key="row.namespace"
85
+ role="group"
86
+ :aria-label="t('integration.workflows.state.row.aria', { namespace: row.namespace })"
87
+ class="rounded-lg border border-black/[0.06] bg-white p-3"
88
+ >
89
+ <div class="flex items-center gap-2">
90
+ <code class="min-w-0 truncate font-mono text-sm font-semibold">{{ row.namespace }}</code>
91
+ <!-- The role is stated, not offered: entries written under one
92
+ meaning cannot be re-read under another (specs AC-3). -->
93
+ <span class="shrink-0 rounded-lg bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
94
+ {{ t(`integration.workflows.state.role.${row.role}.label`) }}
95
+ </span>
96
+ <div class="flex-1" />
97
+ <button
98
+ type="button"
99
+ class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
100
+ :title="t('integration.workflows.state.action.remove')"
101
+ @click="removeNamespace(index)"
102
+ >
103
+ <TrashBin :size="13" />
104
+ </button>
105
+ </div>
106
+
107
+ <p class="mt-1 text-xs text-muted-foreground">
108
+ {{ t(`integration.workflows.state.role.${row.role}.hint`) }}
109
+ {{ t("integration.workflows.state.role.fixed") }}
110
+ </p>
111
+
112
+ <div class="mt-2 grid gap-2 sm:grid-cols-[10rem_1fr]">
113
+ <div class="space-y-1">
114
+ <Label :for="`state-${row.namespace}-visibility`" class="text-mono text-muted-foreground">
115
+ {{ t("integration.workflows.state.field.visibility.label") }}
116
+ </Label>
117
+ <Select
118
+ :model-value="row.visibility ?? 'private'"
119
+ @update:model-value="(v) => setVisibility(index, String(v))"
120
+ >
121
+ <SelectTrigger
122
+ :id="`state-${row.namespace}-visibility`"
123
+ :aria-label="t('integration.workflows.state.field.visibility.label')"
124
+ class="h-8 bg-white text-sm"
125
+ >
126
+ <SelectValue />
127
+ </SelectTrigger>
128
+ <SelectContent>
129
+ <SelectItem value="private">
130
+ {{ t("integration.workflows.state.visibility.private.label") }}
131
+ </SelectItem>
132
+ <SelectItem value="shared">
133
+ {{ t("integration.workflows.state.visibility.shared.label") }}
134
+ </SelectItem>
135
+ </SelectContent>
136
+ </Select>
137
+ </div>
138
+ <!-- What the choice costs, beside the choice — a shared namespace is
139
+ written by every workflow in the tenant and outlives this one
140
+ (specs AC-4). -->
141
+ <p class="self-end pb-1 text-xs text-muted-foreground">
142
+ {{ t(`integration.workflows.state.visibility.${row.visibility ?? "private"}.hint`) }}
143
+ </p>
144
+ </div>
145
+
146
+ <p class="mt-2 text-xs text-muted-foreground">
147
+ {{ t("integration.workflows.state.action.remove.hint") }}
148
+ </p>
149
+ </div>
150
+
151
+ <div class="rounded-lg border border-black/[0.06] bg-muted/30 p-3">
152
+ <Label for="state-new-name" class="text-mono text-muted-foreground">
153
+ {{ t("integration.workflows.state.field.newName.label") }}
154
+ </Label>
155
+ <div class="mt-1 grid gap-2 sm:grid-cols-[1fr_10rem_10rem_auto]">
156
+ <Input
157
+ id="state-new-name"
158
+ v-model="newName"
159
+ class="h-8 bg-white font-mono text-sm"
160
+ :placeholder="t('integration.workflows.state.field.newName.placeholder')"
161
+ @keydown.enter.prevent="addNamespace"
162
+ />
163
+ <!-- The role IS chosen here, and only here — the positive half of
164
+ AC-3, without which "no role control on a row" proves nothing. -->
165
+ <Select v-model="newRole">
166
+ <SelectTrigger
167
+ id="state-new-role"
168
+ :aria-label="t('integration.workflows.state.field.newRole.label')"
169
+ class="h-8 bg-white text-sm"
170
+ >
171
+ <SelectValue />
172
+ </SelectTrigger>
173
+ <SelectContent>
174
+ <SelectItem v-for="role in ROLES" :key="role" :value="role">
175
+ {{ t(`integration.workflows.state.role.${role}.label`) }}
176
+ </SelectItem>
177
+ </SelectContent>
178
+ </Select>
179
+ <Select v-model="newVisibility">
180
+ <SelectTrigger
181
+ id="state-new-visibility"
182
+ :aria-label="t('integration.workflows.state.field.newVisibility.label')"
183
+ class="h-8 bg-white text-sm"
184
+ >
185
+ <SelectValue />
186
+ </SelectTrigger>
187
+ <SelectContent>
188
+ <SelectItem value="private">
189
+ {{ t("integration.workflows.state.visibility.private.label") }}
190
+ </SelectItem>
191
+ <SelectItem value="shared">
192
+ {{ t("integration.workflows.state.visibility.shared.label") }}
193
+ </SelectItem>
194
+ </SelectContent>
195
+ </Select>
196
+ <Button size="sm" class="h-8" :disabled="!newName.trim() || !!newNameError" @click="addNamespace">
197
+ <Plus :size="14" class="mr-1" />
198
+ {{ t("integration.workflows.state.action.add") }}
199
+ </Button>
200
+ </div>
201
+ <p v-if="newNameError" class="mt-1 text-xs text-rose-600">{{ newNameError }}</p>
202
+ <p class="mt-1 text-xs text-muted-foreground">{{ t(`integration.workflows.state.role.${newRole}.hint`) }}</p>
203
+ </div>
204
+ </div>
205
+
206
+ <DialogFooter>
207
+ <Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
208
+ {{ t("integration.common.action.cancel") }}
209
+ </Button>
210
+ <Button size="sm" class="h-8" @click="applyAndClose">
211
+ {{ t("integration.workflows.state.action.done") }}
212
+ </Button>
213
+ </DialogFooter>
214
+ </DialogContent>
215
+ </Dialog>
216
+ </template>
@@ -0,0 +1,13 @@
1
+ import type { WorkflowStateNamespace } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ open: boolean;
4
+ state: WorkflowStateNamespace[];
5
+ };
6
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ "update:open": (args_0: boolean) => any;
8
+ "update:state": (next: WorkflowStateNamespace[]) => any;
9
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
11
+ "onUpdate:state"?: ((next: WorkflowStateNamespace[]) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ export default _default;
@@ -3,6 +3,8 @@ import { computed, ref } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
5
  import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
6
+ import { SIDE_POSITION } from "./canvasSides";
7
+ import { outputHandleId } from "../../data/handleId";
6
8
  import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
7
9
  import { resolveDisplayName } from "../../utils/localize";
8
10
  import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
@@ -45,21 +47,18 @@ function remove() {
45
47
  flow.removeNodes([props.id]);
46
48
  }
47
49
  const OUT_SIDES = ["bottom"];
48
- function sideToPosition(s) {
49
- return s === "top" ? Position.Top : s === "right" ? Position.Right : s === "bottom" ? Position.Bottom : Position.Left;
50
- }
51
50
  </script>
52
51
 
53
52
  <template>
54
53
  <NodeToolbar :is-visible="hovered || selected" :position="Position.Top" :offset="8" class="flex items-center gap-1 rounded-md border border-black/[0.08] bg-white px-1 py-1 shadow-md" @mouseenter="onHoverEnter" @mouseleave="onHoverLeave">
55
54
  <button
56
55
  type="button"
57
- class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
56
+ class="flex size-6 items-center justify-center rounded text-sm text-rose-600 hover:bg-rose-50"
58
57
  :title="t('integration.common.action.delete')"
59
58
  :aria-label="t('integration.canvas.trigger.action.deleteAria')"
60
59
  @click="remove"
61
60
  >
62
- <TrashBin :size="11" />
61
+ <TrashBin :size="13" />
63
62
  </button>
64
63
  </NodeToolbar>
65
64
 
@@ -97,10 +96,10 @@ function sideToPosition(s) {
97
96
  "default"; the side suffix is stripped on persist. -->
98
97
  <Handle
99
98
  v-for="side in OUT_SIDES"
100
- :id="`default__${side}`"
99
+ :id="outputHandleId('default', side)"
101
100
  :key="side"
102
101
  type="source"
103
- :position="sideToPosition(side)"
102
+ :position="SIDE_POSITION[side]"
104
103
  class="!size-3 !rounded-full !border-2 !border-white !bg-emerald-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-emerald-600"
105
104
  />
106
105
  </template>
@@ -19,6 +19,8 @@ import {
19
19
  SelectValue
20
20
  } from "@revenexx/studio";
21
21
  import { toast } from "vue-sonner";
22
+ import { deepClone } from "../../utils/deepClone";
23
+ import { copyToClipboard } from "../../utils/clipboard";
22
24
  import { languageKeyRead, languageKeyToWrite, resolveDisplayName } from "../../utils/localize";
23
25
  const props = defineProps({
24
26
  open: { type: Boolean, required: true },
@@ -34,7 +36,7 @@ const jsonErrors = reactive({});
34
36
  const descDrafts = reactive({});
35
37
  watch(() => props.open, (o) => {
36
38
  if (!o) return;
37
- working.value = JSON.parse(JSON.stringify(props.variables));
39
+ working.value = deepClone(props.variables);
38
40
  for (const k of Object.keys(jsonDrafts)) delete jsonDrafts[k];
39
41
  for (const k of Object.keys(jsonErrors)) delete jsonErrors[k];
40
42
  for (const k of Object.keys(descDrafts)) delete descDrafts[k];
@@ -146,12 +148,10 @@ function addVariable() {
146
148
  newKey.value = "";
147
149
  }
148
150
  async function copyUsage(key) {
149
- try {
150
- await navigator.clipboard.writeText(`\${{ vars.${key} }}`);
151
- toast.success(t("integration.workflows.variables.toast.copied"));
152
- } catch {
153
- toast.error(t("integration.workflows.variables.toast.copyFailed"));
154
- }
151
+ await copyToClipboard(`\${{ vars.${key} }}`, {
152
+ copied: t("integration.workflows.variables.toast.copied"),
153
+ failed: t("integration.workflows.variables.toast.copyFailed")
154
+ });
155
155
  }
156
156
  </script>
157
157
 
@@ -1,17 +1,12 @@
1
1
  <script setup>
2
- import { RefreshCircle as Loader2, MenuDots as MoreHorizontal } from "#solar-icons";
2
+ import { MenuDots as MoreHorizontal } from "#solar-icons";
3
3
  import { toast } from "vue-sonner";
4
- import { apiValidationIssues } from "../../utils/apiError";
4
+ import { apiValidationIssues, errorReason } from "../../utils/apiError";
5
+ import WorkflowDeleteDialog from "./WorkflowDeleteDialog.vue";
5
6
  import WorkflowDuplicateDialog from "./WorkflowDuplicateDialog.vue";
6
7
  import WorkflowIconView from "./WorkflowIconView.vue";
7
8
  import { Layers } from "#solar-icons";
8
9
  import {
9
- AlertDialog,
10
- AlertDialogContent,
11
- AlertDialogDescription,
12
- AlertDialogFooter,
13
- AlertDialogHeader,
14
- AlertDialogTitle,
15
10
  Badge,
16
11
  Button,
17
12
  DropdownMenu,
@@ -27,7 +22,7 @@ defineProps({
27
22
  });
28
23
  const t = useIntegrationsT();
29
24
  const router = useRouter();
30
- const { remove, toggleActive } = useIntegrationsWorkflows();
25
+ const { toggleActive } = useIntegrationsWorkflows();
31
26
  const duplicateTarget = ref(null);
32
27
  const duplicateOpen = ref(false);
33
28
  function askDuplicate(wf) {
@@ -77,21 +72,11 @@ function runBadge(status) {
77
72
  return { label: t("integration.workflows.table.status.run.cancelled"), class: "bg-slate-100 text-slate-700 border-slate-200" };
78
73
  }
79
74
  }
80
- const pendingDelete = ref(null);
81
- const deleting = ref(false);
75
+ const deleteOpen = ref(false);
76
+ const deleteTarget = ref(null);
82
77
  function askDelete(wf) {
83
- pendingDelete.value = wf;
84
- }
85
- async function confirmDelete() {
86
- const wf = pendingDelete.value;
87
- if (!wf) return;
88
- deleting.value = true;
89
- try {
90
- await remove(wf.id);
91
- pendingDelete.value = null;
92
- } finally {
93
- deleting.value = false;
94
- }
78
+ deleteTarget.value = { id: wf.id, name: wf.name };
79
+ deleteOpen.value = true;
95
80
  }
96
81
  async function onToggleActive(wf) {
97
82
  try {
@@ -102,7 +87,7 @@ async function onToggleActive(wf) {
102
87
  toast.error(t("integration.workflows.table.toast.activateBlocked", issues.length, { name: wf.name }));
103
88
  return;
104
89
  }
105
- toast.error(e?.message ?? t("integration.workflows.table.toast.statusChangeFailed"));
90
+ toast.error(errorReason(e, t("integration.workflows.table.toast.statusChangeFailed")));
106
91
  }
107
92
  }
108
93
  function lifecycleOf(wf) {
@@ -405,30 +390,7 @@ const columns = computed(() => [
405
390
  </template>
406
391
  </DataTable>
407
392
 
408
- <AlertDialog :open="!!pendingDelete" @update:open="(v) => {
409
- if (!v) pendingDelete = null;
410
- }">
411
- <AlertDialogContent>
412
- <AlertDialogHeader>
413
- <AlertDialogTitle>{{ t("integration.workflows.table.deleteDialog.title") }}</AlertDialogTitle>
414
- <AlertDialogDescription>
415
- <span v-if="pendingDelete">
416
- <span class="font-medium text-foreground">{{ pendingDelete.name }}</span>
417
- {{ t("integration.workflows.table.deleteDialog.description") }}
418
- </span>
419
- </AlertDialogDescription>
420
- </AlertDialogHeader>
421
- <AlertDialogFooter>
422
- <Button variant="outline" :disabled="deleting" @click="pendingDelete = null">
423
- {{ t("integration.common.action.cancel") }}
424
- </Button>
425
- <Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="confirmDelete">
426
- <Loader2 v-if="deleting" :size="14" class="animate-spin" />
427
- {{ deleting ? t("integration.workflows.table.deleteDialog.action.deleting") : t("integration.common.action.delete") }}
428
- </Button>
429
- </AlertDialogFooter>
430
- </AlertDialogContent>
431
- </AlertDialog>
393
+ <WorkflowDeleteDialog v-model:open="deleteOpen" :workflow="deleteTarget" />
432
394
 
433
395
  <WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
434
396
  </template>
@@ -0,0 +1,48 @@
1
+ import { Position } from '@vue-flow/core';
2
+ import type { Side } from '../../data/handleId.js';
3
+ /**
4
+ * What a connection dot's side means to the canvas: where the library draws it, what
5
+ * faces it, and where along the edge it sits.
6
+ *
7
+ * `data/handleId.ts` owns what a side *is* — the closed set of four, and the id a dot
8
+ * carries. This owns what the canvas does with one. The split is the import: that
9
+ * module has none, so its round trip can be proved without the canvas library, and
10
+ * `Position` is Vue Flow's. Keeping the two apart is what stops a string-shape contract
11
+ * from depending on a rendering library.
12
+ *
13
+ * **Every map here is a `Record<Side, …>`, and that is the point of the module.** Each
14
+ * was a four-branch ternary ending in an `else`, in a component, and two components had
15
+ * their own copy of the first one. An `else` absorbs a fifth side silently: a `centre`
16
+ * dot would have been drawn on the **left**, called `right` its opposite, and spread
17
+ * along the wrong axis — with no type error and nothing on screen to notice, because
18
+ * every one of those is a plausible answer for *some* side.
19
+ *
20
+ * A `Record<Side, …>` cannot do that. Add a name to `SIDES` and all three of these stop
21
+ * compiling until they say what it means. That is weaker than PO-258's "one edit" and
22
+ * better than it: it is still more than one edit, but none of them can be forgotten.
23
+ */
24
+ /**
25
+ * The side a dot is on → the position Vue Flow draws it at.
26
+ *
27
+ * The two vocabularies happen to be the same four strings today — `Position.Top` is
28
+ * `'top'` — so this could be a cast, and is not. A cast would tie us to that
29
+ * coincidence holding in a library we do not own, and it would say nothing when it
30
+ * stopped: the studio's sides are its own, and this is where they are translated.
31
+ */
32
+ export declare const SIDE_POSITION: Record<Side, Position>;
33
+ /**
34
+ * The side facing a given one, which is where an edge arriving from it should land.
35
+ *
36
+ * "Opposite" is only meaningful because the four sides are two axes. A fifth position
37
+ * that is not on an axis — a centre dot — has no opposite at all, and the honest answer
38
+ * then is to change this type rather than to invent one; the compiler will ask.
39
+ */
40
+ export declare const OPPOSITE_SIDE: Record<Side, Side>;
41
+ /**
42
+ * Place the `i`th of `total` output dots evenly along a side.
43
+ *
44
+ * Evenly *between* the corners rather than across them — `(i + 1) / (total + 1)` — so
45
+ * the first and last dot sit inside the card's edge instead of on it, and a single
46
+ * output lands in the middle.
47
+ */
48
+ export declare function distributedStyle(side: Side, i: number, total: number): Record<string, string>;
@@ -0,0 +1,22 @@
1
+ import { Position } from "@vue-flow/core";
2
+ export const SIDE_POSITION = {
3
+ top: Position.Top,
4
+ right: Position.Right,
5
+ bottom: Position.Bottom,
6
+ left: Position.Left
7
+ };
8
+ export const OPPOSITE_SIDE = {
9
+ top: "bottom",
10
+ right: "left",
11
+ bottom: "top",
12
+ left: "right"
13
+ };
14
+ const SIDE_SPREAD_AXIS = {
15
+ top: "left",
16
+ right: "top",
17
+ bottom: "left",
18
+ left: "top"
19
+ };
20
+ export function distributedStyle(side, i, total) {
21
+ return { [SIDE_SPREAD_AXIS[side]]: `${(i + 1) / (total + 1) * 100}%` };
22
+ }
@@ -1,6 +1,6 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
2
  import { useIntegrationsT } from "./useIntegrationsT.js";
3
- import { IntegrationsApiError } from "../utils/apiError.js";
3
+ import { errorReason } from "../utils/apiError.js";
4
4
  import { useLoadGate } from "./useLoadGate.js";
5
5
  export function useCredentialTypes() {
6
6
  const types = useState("integrations.credentialTypes", () => []);
@@ -16,10 +16,6 @@ export function useCredentialTypes() {
16
16
  if (Array.isArray(obj?.data)) return obj.data;
17
17
  return [];
18
18
  }
19
- function captureError(e, fallback) {
20
- if (e instanceof IntegrationsApiError) return e.message;
21
- return e?.message ?? fallback;
22
- }
23
19
  async function load(force = false) {
24
20
  if (!await gate.claim(force)) return types.value;
25
21
  error.value = null;
@@ -27,7 +23,7 @@ export function useCredentialTypes() {
27
23
  types.value = normalize(await api.get("/credential-types"));
28
24
  loaded.value = true;
29
25
  } catch (e) {
30
- error.value = captureError(e, t("integration.credentials.error.loadTypes"));
26
+ error.value = errorReason(e, t("integration.credentials.error.loadTypes"));
31
27
  } finally {
32
28
  gate.release();
33
29
  }
@@ -1,5 +1,5 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
- import { IntegrationsApiError } from "../utils/apiError.js";
2
+ import { errorReason } from "../utils/apiError.js";
3
3
  import { useLoadGate } from "./useLoadGate.js";
4
4
  export function useIntegrationsAudit() {
5
5
  const api = useIntegrationsApi();
@@ -28,7 +28,7 @@ export function useIntegrationsAudit() {
28
28
  entries.value = append ? [...entries.value, ...data] : data;
29
29
  nextCursor.value = res.meta?.next_cursor ?? null;
30
30
  } catch (e) {
31
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? "Could not load the audit log.";
31
+ error.value = errorReason(e, "Could not load the audit log.");
32
32
  } finally {
33
33
  gate.release();
34
34
  }
@@ -1,7 +1,7 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
- import { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
5
5
  import { useLoadGate } from "./useLoadGate.js";
6
6
  export function useIntegrationsCredentials() {
7
7
  const credentials = useState("integrations.credentials", () => []);
@@ -20,10 +20,6 @@ export function useIntegrationsCredentials() {
20
20
  function unwrap(res) {
21
21
  return res?.data ?? res;
22
22
  }
23
- function captureError(e, fallback) {
24
- if (e instanceof IntegrationsApiError) return e.message;
25
- return e?.message ?? fallback;
26
- }
27
23
  async function load(force = false) {
28
24
  if (!await gate.claim(force)) return credentials.value;
29
25
  error.value = null;
@@ -31,7 +27,7 @@ export function useIntegrationsCredentials() {
31
27
  credentials.value = normalize(await api.get("/credentials"));
32
28
  loaded.value = true;
33
29
  } catch (e) {
34
- error.value = captureError(e, t("integration.credentials.error.loadFailed"));
30
+ error.value = errorReason(e, t("integration.credentials.error.loadFailed"));
35
31
  } finally {
36
32
  gate.release();
37
33
  }
@@ -62,7 +58,7 @@ export function useIntegrationsCredentials() {
62
58
  const res = await api.post("/credentials/test", { credential_type_slug: credentialTypeSlug, config });
63
59
  return res?.ok === false ? res : { ok: true, message: res?.message };
64
60
  } catch (e) {
65
- return { ok: false, error: captureError(e, t("integration.credentials.error.testFailed")) };
61
+ return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")) };
66
62
  }
67
63
  }
68
64
  async function test(id) {
@@ -70,7 +66,7 @@ export function useIntegrationsCredentials() {
70
66
  const res = await api.post(`/credentials/${encodeURIComponent(id)}/test`);
71
67
  return res?.ok === false ? res : { ok: true, message: res?.message };
72
68
  } catch (e) {
73
- return { ok: false, error: captureError(e, t("integration.credentials.error.testFailed")) };
69
+ return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")) };
74
70
  }
75
71
  }
76
72
  async function oauthAuthorizeUrl(id) {
@@ -24,6 +24,10 @@ export declare function useIntegrationsDeadLetters(): {
24
24
  globalLoading: any;
25
25
  globalLoaded: any;
26
26
  globalHasMore: any;
27
+ globalPage: any;
28
+ globalLastPage: import("vue").ComputedRef<number>;
29
+ goGlobalPage: (page: number) => Promise<GlobalDeadLetterRow[]>;
30
+ resetGlobalPage: () => void;
27
31
  summary: any;
28
32
  pendingCount: import("vue").ComputedRef<any>;
29
33
  };
@@ -1,6 +1,7 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useLoadGate } from "./useLoadGate.js";
4
+ import { runFeedLastPage } from "../utils/runFeedQuery.js";
4
5
  const EMPTY_SUMMARY = { total: 0, by_status: {} };
5
6
  export function useIntegrationsDeadLetters() {
6
7
  const api = useIntegrationsApi();
@@ -25,12 +26,15 @@ export function useIntegrationsDeadLetters() {
25
26
  const gate = useLoadGate("integrations.deadletters.global", { loading: globalLoading, loaded: globalLoaded });
26
27
  const summary = useState("integrations.deadletters.summary", () => ({ ...EMPTY_SUMMARY }));
27
28
  const globalHasMore = useState("integrations.deadletters.global.hasMore", () => false);
29
+ const GLOBAL_PER_PAGE = 25;
30
+ const globalPage = useState("integrations.deadletters.global.page", () => 1);
31
+ const globalLastPage = computed(() => runFeedLastPage(summary.value.total, GLOBAL_PER_PAGE));
28
32
  const pendingCount = computed(() => summary.value.by_status.pending ?? 0);
29
33
  async function loadGlobal(force = false) {
30
34
  if (!await gate.claim(force)) return globalItems.value;
31
35
  try {
32
36
  const [page, counts] = await Promise.all([
33
- api.get("/dead-letters?page=1&per_page=100"),
37
+ api.get(`/dead-letters?page=${globalPage.value}&per_page=${GLOBAL_PER_PAGE}`),
34
38
  api.get("/dead-letters/summary")
35
39
  ]);
36
40
  globalItems.value = page?.data ?? [];
@@ -45,6 +49,13 @@ export function useIntegrationsDeadLetters() {
45
49
  }
46
50
  return globalItems.value;
47
51
  }
52
+ async function goGlobalPage(page) {
53
+ globalPage.value = Math.min(Math.max(1, page), globalLastPage.value);
54
+ return loadGlobal(true);
55
+ }
56
+ function resetGlobalPage() {
57
+ globalPage.value = 1;
58
+ }
48
59
  return {
49
60
  list,
50
61
  replay,
@@ -54,6 +65,10 @@ export function useIntegrationsDeadLetters() {
54
65
  globalLoading,
55
66
  globalLoaded,
56
67
  globalHasMore,
68
+ globalPage,
69
+ globalLastPage,
70
+ goGlobalPage,
71
+ resetGlobalPage,
57
72
  summary,
58
73
  pendingCount
59
74
  };
@@ -2,7 +2,7 @@ import { applyNodeCatalog, findManifest } from "../data/integrationNodeCatalog.j
2
2
  import { resolveLocalized } from "../utils/localize.js";
3
3
  import { useIntegrationsApi } from "./integrationsApi.js";
4
4
  import { useIntegrationsT } from "./useIntegrationsT.js";
5
- import { IntegrationsApiError } from "../utils/apiError.js";
5
+ import { errorReason } from "../utils/apiError.js";
6
6
  import { useLoadGate } from "./useLoadGate.js";
7
7
  const RETRY_COOLDOWN_MS = 3e4;
8
8
  export function useIntegrationsNodes() {
@@ -39,7 +39,7 @@ export function useIntegrationsNodes() {
39
39
  applyNodeCatalog([]);
40
40
  loaded.value = false;
41
41
  failedAt.value = Date.now();
42
- error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.nodes.error.loadFailed");
42
+ error.value = errorReason(e, t("integration.nodes.error.loadFailed"));
43
43
  } finally {
44
44
  gate.release();
45
45
  }