@revenexx/studio-integrations 0.5.1 → 0.7.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 (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, reactive, ref, watch } from "vue";
3
- import { AddCircle as Plus, Copy, TrashBinMinimalistic as Trash2 } from "#solar-icons";
3
+ import { AddCircle as Plus, Copy, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import {
5
5
  Button,
6
6
  Dialog,
@@ -18,11 +18,13 @@ import {
18
18
  SelectValue
19
19
  } from "@revenexx/studio";
20
20
  import { toast } from "vue-sonner";
21
+ import { resolveDisplayName } from "../../utils/localize";
21
22
  const props = defineProps({
22
23
  open: { type: Boolean, required: true },
23
24
  variables: { type: Object, required: true }
24
25
  });
25
26
  const emit = defineEmits(["update:open", "update:variables"]);
27
+ const t = useIntegrationsT();
26
28
  const KEY_PATTERN = /^[a-z_][a-z0-9_]*$/i;
27
29
  const TYPES = ["string", "number", "boolean", "object", "array"];
28
30
  const jsonDrafts = reactive({});
@@ -79,19 +81,17 @@ function applyJsonDraft(key, type) {
79
81
  const parsed = JSON.parse(raw);
80
82
  const ok = type === "array" ? Array.isArray(parsed) : parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
81
83
  if (!ok) {
82
- jsonErrors[key] = type === "array" ? "Must be a JSON array." : "Must be a JSON object.";
84
+ jsonErrors[key] = type === "array" ? t("integration.workflows.variables.error.mustBeArray") : t("integration.workflows.variables.error.mustBeObject");
83
85
  return;
84
86
  }
85
87
  jsonErrors[key] = null;
86
88
  patchVar(key, { default: parsed });
87
89
  } catch {
88
- jsonErrors[key] = "Not valid JSON.";
90
+ jsonErrors[key] = t("integration.workflows.variables.error.invalidJson");
89
91
  }
90
92
  }
91
93
  function descriptionText(v) {
92
- const d = v.description;
93
- if (!d) return "";
94
- return typeof d === "string" ? d : d.en ?? Object.values(d)[0] ?? "";
94
+ return resolveDisplayName(v.description);
95
95
  }
96
96
  function writeDescription(key, text) {
97
97
  const cur = props.variables[key];
@@ -108,8 +108,8 @@ const newKey = ref("");
108
108
  const newKeyError = computed(() => {
109
109
  const k = newKey.value.trim();
110
110
  if (!k) return null;
111
- if (!KEY_PATTERN.test(k)) return "Letters, digits and _ only; must not start with a digit.";
112
- if (props.variables[k]) return "Key already exists.";
111
+ if (!KEY_PATTERN.test(k)) return t("integration.workflows.variables.error.invalidKey");
112
+ if (props.variables[k]) return t("integration.workflows.variables.error.keyExists");
113
113
  return null;
114
114
  });
115
115
  function addVariable() {
@@ -121,9 +121,9 @@ function addVariable() {
121
121
  async function copyUsage(key) {
122
122
  try {
123
123
  await navigator.clipboard.writeText(`\${{ vars.${key} }}`);
124
- toast.success("Expression copied");
124
+ toast.success(t("integration.workflows.variables.toast.copied"));
125
125
  } catch {
126
- toast.error("Could not copy");
126
+ toast.error(t("integration.workflows.variables.toast.copyFailed"));
127
127
  }
128
128
  }
129
129
  </script>
@@ -132,22 +132,24 @@ async function copyUsage(key) {
132
132
  <Dialog :open="open" @update:open="(v2) => emit('update:open', v2)">
133
133
  <DialogContent class="max-w-[94vw] sm:max-w-3xl">
134
134
  <DialogHeader>
135
- <DialogTitle>Workflow variables</DialogTitle>
135
+ <DialogTitle>{{ t("integration.workflows.variables.dialog.title") }}</DialogTitle>
136
136
  <DialogDescription class="text-sm">
137
- Reusable values for this workflow — reference them in any config field as
137
+ {{ t("integration.workflows.variables.dialog.description.before") }}
138
138
  <code v-pre class="rounded bg-black/[0.05] px-1 py-0.5 font-mono text-xs">${{ vars.key }}</code>.
139
- Changes persist with the workflow Save.
139
+ {{ t("integration.workflows.variables.dialog.description.after") }}
140
140
  </DialogDescription>
141
141
  </DialogHeader>
142
142
 
143
143
  <div class="max-h-[62vh] space-y-3 overflow-y-auto pr-1">
144
144
  <p v-if="!entries.length" class="rounded-lg border border-dashed border-black/[0.12] px-4 py-6 text-center text-sm text-muted-foreground">
145
- No variables yet — add the first one below.
145
+ {{ t("integration.workflows.variables.state.empty") }}
146
146
  </p>
147
147
 
148
148
  <div
149
149
  v-for="[key, v] in entries"
150
150
  :key="key"
151
+ role="group"
152
+ :aria-label="t('integration.workflows.variables.row.aria', { key })"
151
153
  class="rounded-lg border border-black/[0.06] bg-white p-3"
152
154
  >
153
155
  <div class="flex items-center gap-2">
@@ -155,34 +157,32 @@ async function copyUsage(key) {
155
157
  <button
156
158
  type="button"
157
159
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
158
- :title="`Copy \${{ vars.${key} }}`"
160
+ :title="t('integration.workflows.variables.action.copyUsage.title', { key })"
159
161
  @click="copyUsage(key)"
160
162
  >
161
163
  <Copy :size="12" />
162
164
  </button>
163
165
  <div class="flex-1" />
164
- <label class="flex items-center gap-1.5 text-sm text-muted-foreground">
165
- Required
166
- <Switch
167
- :model-value="v.required === true"
168
- @update:model-value="(val) => patchVar(key, { required: val || void 0 })"
169
- />
170
- </label>
171
166
  <button
172
167
  type="button"
173
168
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
174
- title="Delete variable"
169
+ :title="t('integration.workflows.variables.action.delete')"
175
170
  @click="removeVar(key)"
176
171
  >
177
- <Trash2 :size="13" />
172
+ <TrashBin :size="13" />
178
173
  </button>
179
174
  </div>
180
175
 
181
176
  <div class="mt-2 grid gap-2 sm:grid-cols-[8rem_1fr]">
177
+ <!-- One row per variable, so every id carries its key — otherwise
178
+ the labels of the second row point at the first row's fields. -->
182
179
  <div class="space-y-1">
183
- <Label class="text-mono text-muted-foreground">Type</Label>
184
- <Select :model-value="v.type" @update:model-value="(t) => changeType(key, t)">
185
- <SelectTrigger class="h-8 bg-white text-sm">
180
+ <Label :for="`var-${key}-type`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.type.label") }}</Label>
181
+ <Select :model-value="v.type" @update:model-value="(vt) => changeType(key, vt)">
182
+ <!-- Both: the `id` lets the visible Label's `for` resolve, and the
183
+ `aria-label` names the trigger without relying on an engine
184
+ naming a button from a <label> — see the sweep's docblock. -->
185
+ <SelectTrigger :id="`var-${key}-type`" :aria-label="t('integration.workflows.variables.field.type.label')" class="h-8 bg-white text-sm">
186
186
  <SelectValue />
187
187
  </SelectTrigger>
188
188
  <SelectContent>
@@ -192,37 +192,49 @@ async function copyUsage(key) {
192
192
  </div>
193
193
 
194
194
  <div class="space-y-1">
195
- <Label class="text-mono text-muted-foreground">
196
- Default{{ v.required ? " (ignored while required)" : "" }}
197
- </Label>
195
+ <!-- "Value", not "Default": PO-206. The `for`/`id` still name the
196
+ blob field, which is called `default` and stays that way. -->
197
+ <Label :for="`var-${key}-default`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.value.label") }}</Label>
198
198
 
199
+ <!-- Only one of these renders, so they share the one `id` the Label
200
+ points at. The labelable ones take their name from that Label —
201
+ one string, and renaming it turns the tests red instead of
202
+ leaving them green against changed copy. -->
199
203
  <Input
200
204
  v-if="v.type === 'string'"
205
+ :id="`var-${key}-default`"
201
206
  :model-value="typeof v.default === 'string' ? v.default : ''"
202
207
  class="h-8 bg-white text-sm"
203
208
  @update:model-value="(val) => patchVar(key, { default: String(val) })"
204
209
  />
205
210
  <Input
206
211
  v-else-if="v.type === 'number'"
212
+ :id="`var-${key}-default`"
207
213
  type="number"
208
214
  :model-value="typeof v.default === 'number' ? v.default : ''"
209
215
  class="h-8 bg-white text-sm"
210
216
  @update:model-value="(val) => patchVar(key, { default: val === '' ? void 0 : Number(val) })"
211
217
  />
212
218
  <div v-else-if="v.type === 'boolean'" class="flex h-8 items-center">
219
+ <!-- Named out loud: a Switch is a `button role="switch"`, and
220
+ HTML-AAM does not name a button from a <label> — Chromium
221
+ does, other engines need not. -->
213
222
  <Switch
223
+ :id="`var-${key}-default`"
214
224
  :model-value="v.default === true"
225
+ :aria-label="t('integration.workflows.variables.field.value.label')"
215
226
  @update:model-value="(val) => patchVar(key, { default: val })"
216
227
  />
217
228
  </div>
218
229
  <div v-else>
219
230
  <textarea
231
+ :id="`var-${key}-default`"
220
232
  :value="jsonDrafts[key] ?? ''"
221
233
  rows="3"
222
234
  spellcheck="false"
223
235
  class="w-full rounded-md border bg-white p-2 font-mono text-xs focus:outline-none focus:ring-2 focus:ring-primary/40"
224
236
  :class="jsonErrors[key] ? 'border-rose-400' : 'border-black/[0.08]'"
225
- :placeholder="v.type === 'array' ? 'JSON array, e.g. [1, 2]' : 'JSON object, e.g. { \u2026 }'"
237
+ :placeholder="v.type === 'array' ? t('integration.workflows.variables.field.value.placeholder.array') : t('integration.workflows.variables.field.value.placeholder.object')"
226
238
  @input="(e) => jsonDrafts[key] = e.target.value"
227
239
  @blur="applyJsonDraft(key, v.type)"
228
240
  />
@@ -232,11 +244,12 @@ async function copyUsage(key) {
232
244
  </div>
233
245
 
234
246
  <div class="mt-2 space-y-1">
235
- <Label class="text-mono text-muted-foreground">Description</Label>
247
+ <Label :for="`var-${key}-desc`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.description.label") }}</Label>
236
248
  <Input
249
+ :id="`var-${key}-desc`"
237
250
  :model-value="descriptionText(v)"
238
251
  class="h-8 bg-white text-sm"
239
- placeholder="What this variable is for"
252
+ :placeholder="t('integration.workflows.variables.field.description.placeholder')"
240
253
  @update:model-value="(val) => writeDescription(key, String(val))"
241
254
  />
242
255
  </div>
@@ -244,17 +257,18 @@ async function copyUsage(key) {
244
257
 
245
258
  <!-- Add row -->
246
259
  <div class="rounded-lg border border-dashed border-black/[0.12] p-3">
247
- <Label class="text-mono text-muted-foreground">New variable</Label>
260
+ <Label for="var-new-key" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.newKey.label") }}</Label>
248
261
  <div class="mt-1 flex items-center gap-2">
249
262
  <Input
263
+ id="var-new-key"
250
264
  v-model="newKey"
251
265
  class="h-8 flex-1 bg-white font-mono text-sm"
252
- placeholder="e.g. base_url"
266
+ :placeholder="t('integration.workflows.variables.field.newKey.placeholder')"
253
267
  @keydown.enter.prevent="addVariable"
254
268
  />
255
269
  <Button size="sm" class="h-8" :disabled="!newKey.trim() || !!newKeyError" @click="addVariable">
256
270
  <Plus :size="14" class="mr-1" />
257
- Add
271
+ {{ t("integration.workflows.variables.action.add") }}
258
272
  </Button>
259
273
  </div>
260
274
  <p v-if="newKeyError" class="mt-1 text-xs text-rose-600">{{ newKeyError }}</p>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { RefreshCircle as Loader2, MenuDots as MoreHorizontal } from "#solar-icons";
3
3
  import { toast } from "vue-sonner";
4
+ import { apiValidationIssues } from "../../utils/apiError";
4
5
  import WorkflowDuplicateDialog from "./WorkflowDuplicateDialog.vue";
5
6
  import WorkflowIconView from "./WorkflowIconView.vue";
6
7
  import { Layers } from "#solar-icons";
@@ -24,6 +25,7 @@ defineProps({
24
25
  workflows: { type: Array, required: true },
25
26
  loading: { type: Boolean, required: false, default: false }
26
27
  });
28
+ const t = useIntegrationsT();
27
29
  const router = useRouter();
28
30
  const { remove, toggleActive } = useIntegrationsWorkflows();
29
31
  const duplicateTarget = ref(null);
@@ -33,7 +35,7 @@ function askDuplicate(wf) {
33
35
  duplicateOpen.value = true;
34
36
  }
35
37
  async function onDuplicated(copy) {
36
- toast.success(`Duplicated as \u201C${copy.name}\u201D`);
38
+ toast.success(t("integration.workflows.table.toast.duplicated", { name: copy.name }));
37
39
  await router.push(`/integrations/workflows/${copy.id}`);
38
40
  }
39
41
  const GROUP_BY_TAGS_KEY = "integrations.workflows.groupByTags";
@@ -48,7 +50,7 @@ watch(groupByTags, (on) => {
48
50
  } catch {
49
51
  }
50
52
  });
51
- const { runs, load: loadRuns, list: listRuns } = useIntegrationsRuns();
53
+ const { runs, loaded: runsLoaded, load: loadRuns, list: listRuns } = useIntegrationsRuns();
52
54
  onMounted(() => {
53
55
  void loadRuns();
54
56
  });
@@ -59,15 +61,15 @@ function lastRunFor(id) {
59
61
  function runBadge(status) {
60
62
  switch (status) {
61
63
  case "succeeded":
62
- return { label: "Success", class: "bg-emerald-500/15 text-emerald-700 border-emerald-500/20" };
64
+ return { label: t("integration.workflows.table.status.run.succeeded"), class: "bg-emerald-500/15 text-emerald-700 border-emerald-500/20" };
63
65
  case "failed":
64
- return { label: "Failed", class: "bg-rose-500/15 text-rose-700 border-rose-500/20" };
66
+ return { label: t("integration.workflows.table.status.run.failed"), class: "bg-rose-500/15 text-rose-700 border-rose-500/20" };
65
67
  case "running":
66
- return { label: "Running", class: "bg-sky-500/15 text-sky-700 border-sky-500/20" };
68
+ return { label: t("integration.workflows.table.status.run.running"), class: "bg-sky-500/15 text-sky-700 border-sky-500/20" };
67
69
  case "queued":
68
- return { label: "Queued", class: "bg-amber-500/10 text-amber-700 border-amber-500/20" };
70
+ return { label: t("integration.workflows.table.status.run.queued"), class: "bg-amber-500/10 text-amber-700 border-amber-500/20" };
69
71
  case "cancelled":
70
- return { label: "Cancelled", class: "bg-slate-100 text-slate-700 border-slate-200" };
72
+ return { label: t("integration.workflows.table.status.run.cancelled"), class: "bg-slate-100 text-slate-700 border-slate-200" };
71
73
  }
72
74
  }
73
75
  const pendingDelete = ref(null);
@@ -86,21 +88,44 @@ async function confirmDelete() {
86
88
  deleting.value = false;
87
89
  }
88
90
  }
91
+ async function onToggleActive(wf) {
92
+ try {
93
+ await toggleActive(wf.id);
94
+ } catch (e) {
95
+ const issues = apiValidationIssues(e);
96
+ if (issues.length) {
97
+ toast.error(t("integration.workflows.table.toast.activateBlocked", issues.length, { name: wf.name }));
98
+ return;
99
+ }
100
+ toast.error(e?.message ?? t("integration.workflows.table.toast.statusChangeFailed"));
101
+ }
102
+ }
103
+ function lifecycleOf(wf) {
104
+ if (!wf.active) return "paused";
105
+ return wf.revision === 0 ? "draft" : "active";
106
+ }
107
+ function lifecycleLabel(state) {
108
+ return t(`integration.workflows.table.status.lifecycle.${state}`);
109
+ }
110
+ const LIFECYCLE_TONE = {
111
+ active: "bg-emerald-500/10 text-emerald-700 border-emerald-500/20",
112
+ draft: "bg-amber-500/10 text-amber-700 border-amber-500/20",
113
+ paused: "bg-slate-100 text-slate-700 border-slate-200"
114
+ };
89
115
  function statusBadge(wf) {
90
- if (!wf.active) return { label: "Paused", class: "bg-slate-100 text-slate-700 border-slate-200" };
91
- if (wf.revision === 0) return { label: "Draft", class: "bg-amber-500/10 text-amber-700 border-amber-500/20" };
92
- return { label: "Active", class: "bg-emerald-500/10 text-emerald-700 border-emerald-500/20" };
116
+ const state = lifecycleOf(wf);
117
+ return { label: lifecycleLabel(state), class: LIFECYCLE_TONE[state] };
93
118
  }
94
119
  function buildState(wf) {
95
120
  switch (wf.buildStatus) {
96
121
  case "building":
97
- return { label: "Building\u2026", class: "bg-sky-500/15 text-sky-700 border-sky-500/20" };
122
+ return { label: t("integration.workflows.table.status.build.building"), class: "bg-sky-500/15 text-sky-700 border-sky-500/20" };
98
123
  case "pending":
99
- return { label: "Pending", class: "bg-amber-500/10 text-amber-700 border-amber-500/20" };
124
+ return { label: t("integration.workflows.table.status.build.pending"), class: "bg-amber-500/10 text-amber-700 border-amber-500/20" };
100
125
  case "failed":
101
- return { label: "Failed", class: "bg-rose-500/15 text-rose-700 border-rose-500/20" };
126
+ return { label: t("integration.workflows.table.status.build.failed"), class: "bg-rose-500/15 text-rose-700 border-rose-500/20" };
102
127
  case "ready":
103
- return { label: "Ready", class: "bg-emerald-500/10 text-emerald-700 border-emerald-500/20" };
128
+ return { label: t("integration.workflows.table.status.build.ready"), class: "bg-emerald-500/10 text-emerald-700 border-emerald-500/20" };
104
129
  default:
105
130
  return { label: "\u2014", class: "bg-slate-100 text-slate-700 border-slate-200" };
106
131
  }
@@ -108,11 +133,11 @@ function buildState(wf) {
108
133
  function modeLabel(m) {
109
134
  switch (m) {
110
135
  case "sync_only":
111
- return "sync";
136
+ return t("integration.workflows.table.mode.sync");
112
137
  case "caller_decides":
113
- return "caller";
138
+ return t("integration.workflows.table.mode.caller");
114
139
  case "async_only":
115
- return "async";
140
+ return t("integration.workflows.table.mode.async");
116
141
  default:
117
142
  return "\u2014";
118
143
  }
@@ -130,45 +155,71 @@ function formatDateTime(iso) {
130
155
  return iso;
131
156
  }
132
157
  }
133
- const columns = [
158
+ const LIFECYCLE = ["active", "draft", "paused"];
159
+ const BUILD_STATES = ["pending", "building", "ready", "failed"];
160
+ const MODES = ["async_only", "sync_only", "caller_decides"];
161
+ const filterTabs = computed(() => [
162
+ {
163
+ key: "status",
164
+ label: t("integration.workflows.table.column.status"),
165
+ getter: (wf) => lifecycleOf(wf),
166
+ options: LIFECYCLE.map((v) => ({ value: v, label: lifecycleLabel(v) }))
167
+ },
168
+ {
169
+ key: "build",
170
+ label: t("integration.workflows.table.column.build"),
171
+ getter: (wf) => wf.buildStatus ?? "",
172
+ options: BUILD_STATES.map((v) => ({
173
+ value: v,
174
+ label: t(`integration.workflows.table.status.build.${v}`)
175
+ }))
176
+ },
177
+ {
178
+ key: "mode",
179
+ label: t("integration.workflows.table.column.mode"),
180
+ getter: (wf) => wf.execution_mode ?? "",
181
+ options: MODES.map((v) => ({ value: v, label: modeLabel(v) }))
182
+ }
183
+ ]);
184
+ const columns = computed(() => [
134
185
  {
135
186
  key: "name",
136
- label: "Name",
187
+ label: t("integration.workflows.table.column.name"),
137
188
  sortable: true,
138
189
  sortGetter: (w) => w.name.toLowerCase(),
139
190
  searchGetter: (w) => `${w.name} ${w.description ?? ""} ${(w.blob.tags ?? []).join(" ")}`
140
191
  },
141
192
  {
142
193
  key: "status",
143
- label: "Status",
194
+ label: t("integration.workflows.table.column.status"),
144
195
  width: "100px",
145
196
  sortable: true,
146
- sortGetter: (w) => w.active ? w.revision === 0 ? "draft" : "active" : "paused"
197
+ sortGetter: (w) => lifecycleOf(w)
147
198
  },
148
199
  {
149
200
  key: "build",
150
- label: "Build",
201
+ label: t("integration.workflows.table.column.build"),
151
202
  width: "120px",
152
203
  sortable: true,
153
204
  sortGetter: (w) => w.buildStatus ?? ""
154
205
  },
155
206
  {
156
207
  key: "mode",
157
- label: "Mode",
208
+ label: t("integration.workflows.table.column.mode"),
158
209
  width: "120px",
159
210
  sortable: true,
160
211
  sortGetter: (w) => w.execution_mode ?? ""
161
212
  },
162
213
  {
163
214
  key: "triggers",
164
- label: "Triggers",
215
+ label: t("integration.workflows.table.column.triggers"),
165
216
  width: "140px",
166
217
  sortable: true,
167
- sortGetter: (w) => w.blob.triggers.map((t) => t.type).join(",")
218
+ sortGetter: (w) => w.blob.triggers.map((tr) => tr.type).join(",")
168
219
  },
169
220
  {
170
221
  key: "lastRun",
171
- label: "Last run",
222
+ label: t("integration.workflows.table.column.lastRun"),
172
223
  // Wide enough for badge + full timestamp — the flexible Name column has
173
224
  // the slack to give; a truncated "18. Juli, 1…" helps nobody.
174
225
  width: "210px",
@@ -177,7 +228,7 @@ const columns = [
177
228
  },
178
229
  {
179
230
  key: "updated",
180
- label: "Updated",
231
+ label: t("integration.workflows.table.column.updated"),
181
232
  width: "120px",
182
233
  sortable: true,
183
234
  sortGetter: (w) => w.updated_at ?? ""
@@ -188,21 +239,21 @@ const columns = [
188
239
  width: "40px",
189
240
  toggleable: false
190
241
  }
191
- ];
242
+ ]);
192
243
  </script>
193
244
 
194
245
  <template>
195
246
  <DataTable
196
247
  :data="workflows"
197
248
  :group-by="groupByTags ? (w) => w.blob.tags ?? [] : null"
198
- group-empty-label="No tags"
249
+ :group-empty-label="t('integration.workflows.table.group.noTags')"
199
250
  :columns="columns"
200
251
  :row-key="(wf2) => wf2.id"
201
252
  :default-sort="{ column: 'updated', direction: 'desc' }"
202
- :search="{ placeholder: 'Search workflows\u2026' }"
203
- :filter="true"
253
+ :search="{ placeholder: t('integration.workflows.table.search.placeholder') }"
254
+ :filter-tabs="filterTabs"
204
255
  :loading="loading"
205
- empty-text="No workflows yet."
256
+ :empty-text="t('integration.workflows.table.state.empty')"
206
257
  @row-click="(wf2) => router.push(`/integrations/workflows/${wf2.id}`)"
207
258
  >
208
259
  <template #toolbar-left>
@@ -214,7 +265,7 @@ const columns = [
214
265
  @click="groupByTags = !groupByTags"
215
266
  >
216
267
  <Layers :size="13" />
217
- Group by tags
268
+ {{ t("integration.workflows.table.action.groupByTags") }}
218
269
  </Button>
219
270
  </template>
220
271
 
@@ -238,14 +289,28 @@ const columns = [
238
289
  </div>
239
290
  </template>
240
291
 
292
+ <!-- The badge word alone ("Paused", "Failed") also matches a column header
293
+ and the neighbouring column's own words, so each badge carries its
294
+ workflow's name — the rows are plain divs with nothing else to tie a
295
+ value to the workflow it belongs to. -->
241
296
  <template #cell-status="{ row: wf }">
242
- <Badge variant="outline" :class="['border text-xs font-medium uppercase', statusBadge(wf).class]">
297
+ <Badge
298
+ variant="outline"
299
+ :class="['border text-xs font-medium uppercase', statusBadge(wf).class]"
300
+ :aria-label="`${wf.name}: ${statusBadge(wf).label}`"
301
+ >
243
302
  {{ statusBadge(wf).label }}
244
303
  </Badge>
245
304
  </template>
246
305
 
306
+ <!-- A response without a build state renders `—`; naming that would announce
307
+ "<name> build: —", which claims nothing in more words. No state, no name. -->
247
308
  <template #cell-build="{ row: wf }">
248
- <Badge variant="outline" :class="['border text-xs font-medium uppercase', buildState(wf).class]">
309
+ <Badge
310
+ variant="outline"
311
+ :class="['border text-xs font-medium uppercase', buildState(wf).class]"
312
+ :aria-label="wf.buildStatus ? `${wf.name} build: ${buildState(wf).label}` : void 0"
313
+ >
249
314
  {{ buildState(wf).label }}
250
315
  </Badge>
251
316
  </template>
@@ -273,14 +338,25 @@ const columns = [
273
338
 
274
339
  <template #cell-lastRun="{ row: wf }">
275
340
  <div v-if="lastRunFor(wf.id)" class="flex items-center gap-2 min-w-0">
276
- <Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', runBadge(lastRunFor(wf.id).status).class]">
341
+ <Badge
342
+ variant="outline"
343
+ :class="['border text-xs font-medium uppercase shrink-0', runBadge(lastRunFor(wf.id).status).class]"
344
+ :aria-label="`${wf.name} last run: ${runBadge(lastRunFor(wf.id).status).label}`"
345
+ >
277
346
  {{ runBadge(lastRunFor(wf.id).status).label }}
278
347
  </Badge>
279
348
  <span class="text-sm tabular-nums text-muted-foreground truncate">
280
349
  {{ formatDateTime(lastRunFor(wf.id).startedAt) }}
281
350
  </span>
282
351
  </div>
283
- <span v-else class="text-sm text-muted-foreground">—</span>
352
+ <!-- Runs arrive after the rows do, so until they are in, "never run" is
353
+ not known — the bare `—` says nothing, and that is the honest answer.
354
+ The name is only claimed once there is something to claim it from. -->
355
+ <span
356
+ v-else
357
+ class="text-sm text-muted-foreground"
358
+ :aria-label="runsLoaded ? t('integration.workflows.table.lastRun.aria.never', { name: wf.name }) : void 0"
359
+ >—</span>
284
360
  </template>
285
361
 
286
362
  <template #cell-updated="{ row: wf }">
@@ -293,28 +369,30 @@ const columns = [
293
369
  <div @click.stop>
294
370
  <DropdownMenu>
295
371
  <DropdownMenuTrigger as-child>
296
- <Button variant="ghost" size="icon" class="size-7">
372
+ <!-- Icon-only, so it needs a name of its own — the rows are plain
373
+ divs, which leaves nothing else to address this button by. -->
374
+ <Button variant="ghost" size="icon" class="size-7" :aria-label="t('integration.workflows.table.menu.actions.aria', { name: wf.name })">
297
375
  <MoreHorizontal :size="14" />
298
376
  </Button>
299
377
  </DropdownMenuTrigger>
300
378
  <DropdownMenuContent align="end">
301
- <DropdownMenuLabel>Actions</DropdownMenuLabel>
379
+ <DropdownMenuLabel>{{ t("integration.workflows.table.menu.actions.label") }}</DropdownMenuLabel>
302
380
  <DropdownMenuSeparator />
303
381
  <DropdownMenuItem @select="router.push(`/integrations/workflows/${wf.id}`)">
304
- Open editor
382
+ {{ t("integration.workflows.table.menu.action.openEditor") }}
305
383
  </DropdownMenuItem>
306
384
  <DropdownMenuItem @select="askDuplicate(wf)">
307
- Duplicate
385
+ {{ t("integration.workflows.action.duplicate") }}
308
386
  </DropdownMenuItem>
309
- <DropdownMenuItem @select="toggleActive(wf.id)">
310
- {{ wf.active ? "Pause" : "Activate" }}
387
+ <DropdownMenuItem @select="onToggleActive(wf)">
388
+ {{ wf.active ? t("integration.workflows.table.menu.action.pause") : t("integration.workflows.table.menu.action.activate") }}
311
389
  </DropdownMenuItem>
312
390
  <DropdownMenuSeparator />
313
391
  <DropdownMenuItem
314
392
  class="text-destructive focus:text-destructive"
315
393
  @select="askDelete(wf)"
316
394
  >
317
- Delete
395
+ {{ t("integration.common.action.delete") }}
318
396
  </DropdownMenuItem>
319
397
  </DropdownMenuContent>
320
398
  </DropdownMenu>
@@ -327,21 +405,21 @@ const columns = [
327
405
  }">
328
406
  <AlertDialogContent>
329
407
  <AlertDialogHeader>
330
- <AlertDialogTitle>Delete workflow?</AlertDialogTitle>
408
+ <AlertDialogTitle>{{ t("integration.workflows.table.deleteDialog.title") }}</AlertDialogTitle>
331
409
  <AlertDialogDescription>
332
410
  <span v-if="pendingDelete">
333
411
  <span class="font-medium text-foreground">{{ pendingDelete.name }}</span>
334
- and all its revision history will be permanently deleted.
412
+ {{ t("integration.workflows.table.deleteDialog.description") }}
335
413
  </span>
336
414
  </AlertDialogDescription>
337
415
  </AlertDialogHeader>
338
416
  <AlertDialogFooter>
339
417
  <Button variant="outline" :disabled="deleting" @click="pendingDelete = null">
340
- Cancel
418
+ {{ t("integration.common.action.cancel") }}
341
419
  </Button>
342
420
  <Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="confirmDelete">
343
421
  <Loader2 v-if="deleting" :size="14" class="animate-spin" />
344
- {{ deleting ? "Deleting\u2026" : "Delete" }}
422
+ {{ deleting ? t("integration.workflows.table.deleteDialog.action.deleting") : t("integration.common.action.delete") }}
345
423
  </Button>
346
424
  </AlertDialogFooter>
347
425
  </AlertDialogContent>
@@ -1,15 +1,17 @@
1
1
  import { computed } from "vue";
2
2
  import { IntegrationsApiError } from "../utils/apiError.js";
3
+ import { useIntegrationsT } from "./useIntegrationsT.js";
3
4
  export function useIntegrationsApi() {
4
5
  const config = useRuntimeConfig();
5
6
  const { getToken } = usePlatformAuth();
6
7
  const { slug: onlyTenantSlug, load: loadTenants } = usePlatformTenant();
8
+ const t = useIntegrationsT();
7
9
  const baseUrl = computed(
8
10
  () => config.public.integrationsApi ?? "https://integrations.revenexx.com/api/v1"
9
11
  );
10
12
  async function buildHeaders(extra = {}) {
11
13
  const token = await getToken();
12
- if (!token) throw new IntegrationsApiError(401, null, "Not signed in.");
14
+ if (!token) throw new IntegrationsApiError(401, null, t("integration.common.error.notSignedIn"));
13
15
  if (!onlyTenantSlug.value) await loadTenants();
14
16
  const headers = {
15
17
  Accept: "application/json",
@@ -34,7 +36,7 @@ export function useIntegrationsApi() {
34
36
  } catch (err) {
35
37
  const fe = err;
36
38
  const data = fe?.response?._data;
37
- const detail = data?.message ?? fe?.message ?? "Integrations request failed";
39
+ const detail = data?.message ?? fe?.message ?? t("integration.common.error.requestFailed");
38
40
  throw new IntegrationsApiError(fe?.response?.status ?? 0, data, detail);
39
41
  }
40
42
  }