@revenexx/studio-integrations 0.6.0 → 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 (110) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +31 -0
  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 +29 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +16 -15
  13. package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
  14. package/dist/runtime/components/Integrations/Layout.vue +28 -3
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +15 -14
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
  18. package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
  19. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
  20. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
  21. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
  22. package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
  23. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  24. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  25. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  26. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  27. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  28. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  29. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  30. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  31. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  32. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  33. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  34. package/dist/runtime/components/Integrations/StickyNoteView.vue +37 -23
  35. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  36. package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
  37. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
  38. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  39. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
  40. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  41. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  42. package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
  43. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
  44. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  45. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
  46. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
  47. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
  48. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  49. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
  50. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
  51. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  52. package/dist/runtime/composables/integrationsApi.js +4 -2
  53. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  54. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
  55. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  56. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  57. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  58. package/dist/runtime/composables/useIntegrationsNodes.js +3 -1
  59. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  60. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  61. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  62. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  63. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  64. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  65. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  66. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  67. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  68. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  69. package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
  70. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
  71. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  72. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  73. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  74. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  75. package/dist/runtime/data/integrationTemplates.js +7 -1
  76. package/dist/runtime/data/payloadForm.js +2 -1
  77. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  78. package/dist/runtime/i18n/messages/de.json +1207 -0
  79. package/dist/runtime/i18n/messages/en.json +1207 -0
  80. package/dist/runtime/i18n/translate.d.ts +83 -0
  81. package/dist/runtime/i18n/translate.js +41 -0
  82. package/dist/runtime/pages/integrations/audit.vue +52 -34
  83. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  84. package/dist/runtime/pages/integrations/credentials.vue +70 -52
  85. package/dist/runtime/pages/integrations/index.vue +50 -56
  86. package/dist/runtime/pages/integrations/runs.vue +33 -21
  87. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  88. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
  90. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
  92. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  93. package/dist/runtime/plugins/locale.d.ts +10 -0
  94. package/dist/runtime/plugins/locale.js +6 -0
  95. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  96. package/dist/runtime/plugins/realtime.client.js +4 -0
  97. package/dist/runtime/utils/credentialState.d.ts +24 -1
  98. package/dist/runtime/utils/credentialState.js +26 -11
  99. package/dist/runtime/utils/localize.d.ts +18 -5
  100. package/dist/runtime/utils/localize.js +32 -9
  101. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  102. package/dist/runtime/utils/pollBudget.js +17 -0
  103. package/dist/runtime/utils/schedule.d.ts +0 -25
  104. package/dist/runtime/utils/schedule.js +37 -25
  105. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  106. package/dist/runtime/utils/statusStyle.js +9 -1
  107. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  108. package/dist/runtime/utils/validationFindings.js +4 -3
  109. package/dist/types.d.mts +2 -6
  110. package/package.json +3 -2
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, onMounted, ref, watch } from "vue";
3
- import { AddCircle as Plus, TrashBinMinimalistic as Trash2 } from "#solar-icons";
3
+ import { AddCircle as Plus, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import {
5
5
  Button,
6
6
  Input,
@@ -29,6 +29,7 @@ const props = defineProps({
29
29
  refused: { type: String, required: false }
30
30
  });
31
31
  const emit = defineEmits(["set"]);
32
+ const t = useIntegrationsT();
32
33
  const value = computed(() => props.config[props.field.key]);
33
34
  function write(v) {
34
35
  emit("set", props.field.key, v);
@@ -44,18 +45,18 @@ const formatError = computed(() => {
44
45
  const val = props.field.validation;
45
46
  if (!val) return null;
46
47
  if (typeof v === "string") {
47
- if (val.minLength != null && v.length < val.minLength) return `Must be at least ${val.minLength} characters`;
48
- if (val.maxLength != null && v.length > val.maxLength) return `Must be at most ${val.maxLength} characters`;
48
+ if (val.minLength != null && v.length < val.minLength) return t("integration.nodeConfig.validation.minLength", { min: val.minLength });
49
+ if (val.maxLength != null && v.length > val.maxLength) return t("integration.nodeConfig.validation.maxLength", { max: val.maxLength });
49
50
  if (val.pattern) {
50
51
  try {
51
- if (!new RegExp(val.pattern).test(v)) return "Does not match the required format";
52
+ if (!new RegExp(val.pattern).test(v)) return t("integration.nodeConfig.validation.pattern");
52
53
  } catch {
53
54
  }
54
55
  }
55
56
  }
56
57
  if (typeof v === "number") {
57
- if (val.min != null && v < val.min) return `Must be \u2265 ${val.min}`;
58
- if (val.max != null && v > val.max) return `Must be \u2264 ${val.max}`;
58
+ if (val.min != null && v < val.min) return t("integration.nodeConfig.validation.min", { min: val.min });
59
+ if (val.max != null && v > val.max) return t("integration.nodeConfig.validation.max", { max: val.max });
59
60
  }
60
61
  return null;
61
62
  });
@@ -69,9 +70,9 @@ const constraintHint = computed(() => {
69
70
  if (val.min != null) return `\u2265 ${val.min}`;
70
71
  if (val.max != null) return `\u2264 ${val.max}`;
71
72
  } else {
72
- if (val.minLength != null && val.maxLength != null) return `${val.minLength}\u2013${val.maxLength} characters`;
73
- if (val.minLength != null) return `at least ${val.minLength} characters`;
74
- if (val.maxLength != null) return `up to ${val.maxLength} characters`;
73
+ if (val.minLength != null && val.maxLength != null) return t("integration.nodeConfig.hint.charRange", { min: val.minLength, max: val.maxLength });
74
+ if (val.minLength != null) return t("integration.nodeConfig.hint.charMin", { min: val.minLength });
75
+ if (val.maxLength != null) return t("integration.nodeConfig.hint.charMax", { max: val.maxLength });
75
76
  }
76
77
  return "";
77
78
  });
@@ -103,11 +104,11 @@ const bound = computed(() => {
103
104
  const credOptions = computed(
104
105
  () => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c) }))
105
106
  );
106
- const UNKNOWN_CREDENTIAL = "Unknown credential \u2014 pick again";
107
+ const UNKNOWN_CREDENTIAL = computed(() => t("integration.nodeConfig.credential.unknown"));
107
108
  function credTriggerLabel(field) {
108
109
  const label = resolveLocalized(field.label);
109
- if (bound.value) return `${label}: ${bound.value.credential.name}, ${bound.value.state.label}`;
110
- if (value.value && credentialsLoaded.value) return `${label}: ${UNKNOWN_CREDENTIAL}`;
110
+ if (bound.value) return t("integration.nodeConfig.credential.trigger.bound", { label, name: bound.value.credential.name, state: bound.value.state.label });
111
+ if (value.value && credentialsLoaded.value) return t("integration.nodeConfig.credential.trigger.unknown", { label, unknown: UNKNOWN_CREDENTIAL.value });
111
112
  return label;
112
113
  }
113
114
  const credCreateOpen = ref(false);
@@ -126,8 +127,8 @@ function onCredentialSelect(v) {
126
127
  write(v);
127
128
  }
128
129
  function acceptsSftp(field) {
129
- const t = field.credentialType;
130
- return t === "revenexx:sftp" || Array.isArray(t) && t.includes("revenexx:sftp");
130
+ const t2 = field.credentialType;
131
+ return t2 === "revenexx:sftp" || Array.isArray(t2) && t2.includes("revenexx:sftp");
131
132
  }
132
133
  const EXPR_TOKEN = "${{";
133
134
  const canToggleExpression = computed(
@@ -231,7 +232,7 @@ function writeJson(text) {
231
232
  jsonError.value = null;
232
233
  emit("set", props.field.key, parsed);
233
234
  } catch {
234
- jsonError.value = "Not valid JSON yet \u2014 the value is kept once it parses.";
235
+ jsonError.value = t("integration.nodeConfig.json.invalid");
235
236
  }
236
237
  }
237
238
  function readArray() {
@@ -266,28 +267,28 @@ function setArrayItemField(index, fieldKey, v) {
266
267
  without the required marker, which is a state and not part of a
267
268
  name. -->
268
269
  <Label class="text-mono" :class="invalid ? 'text-rose-600' : 'text-muted-foreground'">
269
- {{ resolveLocalized(field.label) }}<span v-if="field.required" class="text-rose-500" title="Required"> *</span>
270
+ {{ resolveLocalized(field.label) }}<span v-if="field.required" class="text-rose-500" :title="t('integration.nodeConfig.field.requiredMark')"> *</span>
270
271
  </Label>
271
272
  <div class="flex shrink-0 items-center gap-1.5">
272
273
  <div v-if="isObjectField && !showExpression" class="flex items-center rounded border border-black/[0.08] bg-white p-px">
273
274
  <button
274
275
  type="button"
275
276
  :disabled="!flatCapable"
276
- title="Edit as key/value rows"
277
+ :title="t('integration.nodeConfig.object.toggle.fieldsTitle')"
277
278
  class="rounded px-1 py-px text-[10px] font-medium transition disabled:pointer-events-none disabled:opacity-40"
278
279
  :class="objectMode === 'form' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
279
280
  @click="objectMode = 'form'"
280
281
  >
281
- Fields
282
+ {{ t("integration.nodeConfig.object.toggle.fields") }}
282
283
  </button>
283
284
  <button
284
285
  type="button"
285
- title="Edit as raw JSON"
286
+ :title="t('integration.nodeConfig.object.toggle.jsonTitle')"
286
287
  class="rounded px-1 py-px text-[10px] font-medium transition"
287
288
  :class="objectMode === 'json' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
288
289
  @click="objectMode = 'json'"
289
290
  >
290
- JSON
291
+ {{ t("integration.nodeConfig.object.toggle.json") }}
291
292
  </button>
292
293
  </div>
293
294
  <button
@@ -295,7 +296,7 @@ function setArrayItemField(index, fieldKey, v) {
295
296
  type="button"
296
297
  class="rounded border px-1 py-0.5 font-mono text-[10px] transition"
297
298
  :class="showExpression ? 'border-emerald-500/40 bg-emerald-500/15 text-emerald-700' : 'border-black/[0.08] bg-white text-muted-foreground hover:text-foreground'"
298
- :title="showExpression ? 'Back to the regular control (clears the expression)' : 'Map this value from a previous step instead'"
299
+ :title="showExpression ? t('integration.nodeConfig.expression.toggle.off') : t('integration.nodeConfig.expression.toggle.on')"
299
300
  @click="toggleExpression"
300
301
  >
301
302
  {{ exprHint }}
@@ -303,7 +304,7 @@ function setArrayItemField(index, fieldKey, v) {
303
304
  <span
304
305
  v-else-if="field.expressionAllowed"
305
306
  class="rounded border border-emerald-500/25 bg-emerald-500/10 px-1 py-0.5 font-mono text-[10px] text-emerald-600"
306
- title="Accepts ${{ … }} expressions"
307
+ :title="t('integration.nodeConfig.expression.badge.title')"
307
308
  >
308
309
  {{ exprHint }}
309
310
  </span>
@@ -338,26 +339,26 @@ function setArrayItemField(index, fieldKey, v) {
338
339
  <!-- A repeating row has no room for visible labels, so each control
339
340
  names itself and says which row of which field it belongs to. -->
340
341
  <Input
341
- :aria-label="`${resolveLocalized(field.label)} \u2014 key of row ${idx + 1}`"
342
+ :aria-label="t('integration.nodeConfig.object.row.keyAria', { label: resolveLocalized(field.label), n: idx + 1 })"
342
343
  :model-value="row.key"
343
- placeholder="key"
344
+ :placeholder="t('integration.nodeConfig.object.row.keyPlaceholder')"
344
345
  class="h-7 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
345
346
  @update:model-value="(v) => setKvRow(idx, { key: String(v) })"
346
347
  />
347
348
  <Input
348
- :aria-label="`${resolveLocalized(field.label)} \u2014 value of row ${idx + 1}`"
349
+ :aria-label="t('integration.nodeConfig.object.row.valueAria', { label: resolveLocalized(field.label), n: idx + 1 })"
349
350
  :model-value="row.value"
350
- placeholder="value"
351
+ :placeholder="t('integration.nodeConfig.object.row.valuePlaceholder')"
351
352
  class="h-7 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
352
353
  @update:model-value="(v) => setKvRow(idx, { value: String(v) })"
353
354
  />
354
355
  <button
355
356
  type="button"
356
- :aria-label="`Remove row ${idx + 1} of ${resolveLocalized(field.label)}`"
357
+ :aria-label="t('integration.nodeConfig.object.row.removeAria', { label: resolveLocalized(field.label), n: idx + 1 })"
357
358
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
358
359
  @click="removeKvRow(idx)"
359
360
  >
360
- <Trash2 :size="11" />
361
+ <TrashBin :size="11" />
361
362
  </button>
362
363
  </div>
363
364
  <Button
@@ -368,7 +369,7 @@ function setArrayItemField(index, fieldKey, v) {
368
369
  @click="addKvRow"
369
370
  >
370
371
  <Plus :size="11" />
371
- Add row
372
+ {{ t("integration.nodeConfig.action.addRow") }}
372
373
  </Button>
373
374
  </div>
374
375
 
@@ -464,7 +465,7 @@ function setArrayItemField(index, fieldKey, v) {
464
465
  @update:model-value="(v) => write(v)"
465
466
  >
466
467
  <SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
467
- <SelectValue :placeholder="resolveLocalized(field.placeholder) || 'Pick one'" />
468
+ <SelectValue :placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')" />
468
469
  </SelectTrigger>
469
470
  <SelectContent>
470
471
  <SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
@@ -498,7 +499,7 @@ function setArrayItemField(index, fieldKey, v) {
498
499
  @update:model-value="(v) => write(v)"
499
500
  >
500
501
  <SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
501
- <SelectValue placeholder="Pick a secret" />
502
+ <SelectValue :placeholder="t('integration.nodeConfig.secretRef.placeholder')" />
502
503
  </SelectTrigger>
503
504
  <SelectContent>
504
505
  <SelectItem v-for="k in secretKeys" :key="k" :value="k">{{ k }}</SelectItem>
@@ -520,7 +521,7 @@ function setArrayItemField(index, fieldKey, v) {
520
521
  "Revenexx APIConnected". Rendering it here keeps the pill a pill, and
521
522
  the placeholder has to be drawn by hand because a slot replaces the
522
523
  fallback that would otherwise show it. -->
523
- <SelectValue placeholder="Pick a credential">
524
+ <SelectValue :placeholder="t('integration.nodeConfig.credential.placeholder')">
524
525
  <span v-if="bound" class="flex min-w-0 items-center gap-1.5">
525
526
  <span class="min-w-0 truncate">{{ bound.credential.name }}</span>
526
527
  <!-- No `title` on this badge: `SelectValue` renders itself with
@@ -539,7 +540,7 @@ function setArrayItemField(index, fieldKey, v) {
539
540
  while the workflow still runs into the dead reference. `loaded`
540
541
  keeps the wait before the first response out of it. -->
541
542
  <span v-else-if="value && credentialsLoaded" class="text-rose-600">{{ UNKNOWN_CREDENTIAL }}</span>
542
- <span v-else class="text-muted-foreground">Pick a credential</span>
543
+ <span v-else class="text-muted-foreground">{{ t("integration.nodeConfig.credential.placeholder") }}</span>
543
544
  </SelectValue>
544
545
  </SelectTrigger>
545
546
  <SelectContent>
@@ -562,24 +563,24 @@ function setArrayItemField(index, fieldKey, v) {
562
563
  </span>
563
564
  </SelectItem>
564
565
  <div v-if="!credOptions.length" class="px-2 py-1.5 text-sm text-muted-foreground">
565
- No credentials yet.
566
+ {{ t("integration.nodeConfig.credential.empty") }}
566
567
  </div>
567
568
  <SelectSeparator />
568
569
  <SelectItem :value="CRED_ACTION_NEW" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
569
- New credential
570
+ {{ t("integration.nodeConfig.credential.action.new") }}
570
571
  </SelectItem>
571
572
  <SelectItem v-if="acceptsSftp(field)" :value="CRED_ACTION_SFTP" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
572
- Import existing Revenue Cloud SFTP account
573
+ {{ t("integration.nodeConfig.credential.action.importSftp") }}
573
574
  </SelectItem>
574
575
  </SelectContent>
575
576
  </Select>
576
577
  <!-- What the state has to add, as text rather than as a tooltip — the shape
577
578
  the detail dialog uses beneath its own badge. -->
578
579
  <p v-if="bound?.state.key === 'needs_reauth'" class="text-sm text-amber-600">
579
- This credential needs reconnect.
580
+ {{ t("integration.nodeConfig.credential.state.needsReauth") }}
580
581
  </p>
581
582
  <p v-else-if="bound?.state.key === 'failed' && bound.state.message" class="text-sm break-words text-rose-600">
582
- Last test failed: {{ bound.state.message }}
583
+ {{ t("integration.nodeConfig.credential.state.failed", { message: bound.state.message }) }}
583
584
  </p>
584
585
  <IntegrationsCredentialCreateDialog
585
586
  v-model:open="credCreateOpen"
@@ -606,11 +607,11 @@ function setArrayItemField(index, fieldKey, v) {
606
607
  </span>
607
608
  <button
608
609
  type="button"
609
- :aria-label="`Remove item ${idx + 1} of ${resolveLocalized(field.label)}`"
610
+ :aria-label="t('integration.nodeConfig.array.removeItemAria', { label: resolveLocalized(field.label), n: idx + 1 })"
610
611
  class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
611
612
  @click="removeArrayItem(idx)"
612
613
  >
613
- <Trash2 :size="11" />
614
+ <TrashBin :size="11" />
614
615
  </button>
615
616
  </div>
616
617
  <div
@@ -624,7 +625,7 @@ function setArrayItemField(index, fieldKey, v) {
624
625
  <!-- The label above repeats per item, so it cannot own an id; the
625
626
  input carries its own name including the item number. -->
626
627
  <Input
627
- :aria-label="`${resolveLocalized(sub.label)} of item ${idx + 1}`"
628
+ :aria-label="t('integration.nodeConfig.array.subFieldAria', { label: resolveLocalized(sub.label), n: idx + 1 })"
628
629
  :model-value="item[sub.key] ?? ''"
629
630
  :placeholder="resolveLocalized(sub.placeholder)"
630
631
  class="h-7 text-sm bg-white"
@@ -641,7 +642,7 @@ function setArrayItemField(index, fieldKey, v) {
641
642
  @click="pushArrayItem(Object.fromEntries((field.items?.properties ?? []).map((p) => [p.key, ''])))"
642
643
  >
643
644
  <Plus :size="11" />
644
- Add row
645
+ {{ t("integration.nodeConfig.action.addRow") }}
645
646
  </Button>
646
647
  </div>
647
648