@revenexx/studio-integrations 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +457 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
  4. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
  5. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
  6. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +61 -29
  7. package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
  8. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +10 -0
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue +94 -49
  10. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +10 -0
  11. package/dist/runtime/components/Integrations/NodeInspector.vue +37 -60
  12. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  13. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  14. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  15. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  16. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  17. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  18. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  19. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  20. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  21. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  22. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  23. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  24. package/dist/runtime/components/Integrations/WorkflowEditor.vue +165 -56
  25. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  26. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  27. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  28. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  29. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  30. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  31. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  32. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  33. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  34. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  35. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  36. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  37. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -8
  38. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  39. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -7
  40. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  41. package/dist/runtime/data/canvasShortcuts.js +68 -0
  42. package/dist/runtime/i18n/messages/de.json +30 -16
  43. package/dist/runtime/i18n/messages/en.json +30 -16
  44. package/dist/runtime/pages/integrations/credentials.vue +40 -18
  45. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  46. package/dist/runtime/pages/integrations/workflows/[id].vue +128 -33
  47. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  48. package/dist/runtime/utils/apiEnvelope.js +20 -0
  49. package/dist/runtime/utils/apiError.d.ts +35 -8
  50. package/dist/runtime/utils/apiError.js +12 -12
  51. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  52. package/dist/runtime/utils/configDefaults.js +7 -0
  53. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  54. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  55. package/dist/runtime/utils/notify.d.ts +2 -2
  56. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  57. package/dist/runtime/utils/runEnvelope.js +2 -1
  58. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  59. package/dist/runtime/utils/selectOptions.js +19 -0
  60. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  61. package/dist/runtime/utils/shortcutLabel.js +48 -0
  62. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, reactive, ref, watch } from "vue";
2
+ import { computed, nextTick, reactive, ref, watch } from "vue";
3
3
  import {
4
4
  CheckCircle as CheckCircle2,
5
5
  CloseCircle as XCircle,
@@ -30,7 +30,7 @@ import {
30
30
  SelectValue
31
31
  } from "@revenexx/studio";
32
32
  import { toast } from "../../utils/notify";
33
- import { apiFieldErrors, errorReason } from "../../utils/apiError";
33
+ import { apiRefusal, errorReason } from "../../utils/apiError";
34
34
  import { credentialState, relativeTime } from "../../utils/credentialState";
35
35
  import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
36
36
  import { resolveLocalized } from "../../utils/localize";
@@ -53,14 +53,31 @@ const saving = ref(false);
53
53
  const deleting = ref(false);
54
54
  const testing = ref(false);
55
55
  const liveTest = ref(null);
56
- const saveError = ref(null);
57
- const fieldErrors = ref({});
56
+ const saveRefusal = ref(null);
57
+ const drawnKeys = computed(() => [
58
+ "name",
59
+ ...sourceActive.value ? [] : fieldsOf(props.type).map((f) => `config.${f.key}`)
60
+ ]);
61
+ const refusal = computed(() => apiRefusal(saveRefusal.value, drawnKeys.value));
62
+ const fieldErrors = computed(() => refusal.value.atFields);
63
+ const summaryReasons = computed(() => {
64
+ if (!saveRefusal.value) return [];
65
+ if (refusal.value.unshown.length) return refusal.value.unshown.map((u) => u.message);
66
+ if (Object.keys(refusal.value.atFields).length) return [];
67
+ return [errorReason(saveRefusal.value, t("integration.credentials.error.save"))];
68
+ });
58
69
  function configFieldAria(key) {
59
70
  return fieldErrors.value[`config.${key}`] ? { "aria-invalid": "true", "aria-describedby": `detail-field-${key}-error` } : {};
60
71
  }
72
+ async function focusFirstRefusedField() {
73
+ await nextTick();
74
+ const key = drawnKeys.value.find((k) => k in fieldErrors.value);
75
+ if (!key) return;
76
+ const id = key === "name" ? "detail-cred-name" : `detail-field-${key.slice("config.".length)}`;
77
+ document.getElementById(id)?.focus();
78
+ }
61
79
  function clearRefusal() {
62
- saveError.value = null;
63
- fieldErrors.value = {};
80
+ saveRefusal.value = null;
64
81
  }
65
82
  const connecting = ref(false);
66
83
  const typeName = computed(() => resolveLocalized(props.type?.name) || props.credential?.credential_type_slug || "");
@@ -103,6 +120,7 @@ async function runTest() {
103
120
  if (!props.credential) return;
104
121
  testing.value = true;
105
122
  liveTest.value = null;
123
+ clearRefusal();
106
124
  try {
107
125
  liveTest.value = await test(props.credential.id);
108
126
  await load(true);
@@ -121,9 +139,9 @@ async function save() {
121
139
  emit("update:open", false);
122
140
  toast.success(t("integration.credentials.toast.updated"));
123
141
  } catch (e) {
124
- fieldErrors.value = apiFieldErrors(e);
125
- saveError.value = errorReason(e, t("integration.credentials.error.save"));
126
- toast.error(saveError.value);
142
+ saveRefusal.value = e;
143
+ void focusFirstRefusedField();
144
+ toast.error(errorReason(e, t("integration.credentials.error.save")));
127
145
  } finally {
128
146
  saving.value = false;
129
147
  }
@@ -182,8 +200,11 @@ async function connect() {
182
200
  <!-- Standard cockpit tab bar (violet underline) -->
183
201
  <TabBar :model-value="tab" :tabs="tabs" @update:model-value="(k) => tab = k" />
184
202
 
185
- <!-- Tab 1: Credentials -->
186
- <div v-if="tab === 'credentials'" class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
203
+ <!-- Tab 1: Credentials. A `template` rather than a wrapping element: the
204
+ scrolling pane and the block below it are siblings, as they were when the
205
+ pane was the whole tab, so nothing about the spacing around them moves. -->
206
+ <template v-if="tab === 'credentials'">
207
+ <div class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
187
208
  <!-- Connection state — the headline answer: are these credentials correct? -->
188
209
  <div class="rounded-lg border p-3" :class="state.toneClass">
189
210
  <div class="flex items-center gap-2">
@@ -243,7 +264,10 @@ async function connect() {
243
264
  <IntegrationsCredentialRevenexxSource
244
265
  v-if="hasRevenexxSource(type?.slug)"
245
266
  :type="type"
246
- @update:active="(v) => sourceActive = v"
267
+ @update:active="(v) => {
268
+ sourceActive = v;
269
+ clearRefusal();
270
+ }"
247
271
  @apply="(c) => Object.assign(form.config, c)"
248
272
  />
249
273
 
@@ -313,27 +337,35 @@ async function connect() {
313
337
  {{ liveTest.ok ? liveTest.message ?? t("integration.credentials.shared.test.ok") : liveTest.error ?? liveTest.message ?? t("integration.credentials.shared.test.failed") }}
314
338
  </div>
315
339
 
316
- <!-- The save was refused. Same rule as the create dialog: stated here, and at
317
- the field where the service keyed it. -->
318
- <div v-if="saveError" role="alert" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
319
- {{ saveError }}
320
- </div>
340
+ </div>
321
341
 
322
- <div class="flex items-center gap-2 pt-1">
323
- <div class="flex-1" />
324
- <Button variant="outline" :disabled="testing || saving" @click="runTest">
325
- <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
326
- <TestIcon v-else :size="14" class="mr-1" />
327
- {{ t("integration.credentials.shared.action.testConnection") }}
328
- </Button>
329
- <Button :disabled="saving || !form.name.trim()" @click="save">
330
- <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
331
- {{ t("integration.common.action.save") }}
332
- </Button>
333
- </div>
334
342
  </div>
335
343
 
344
+ <!-- Outside the scrolling pane on purpose, the way the create dialog has it:
345
+ a refusal puts the operator in the first marked field, which scrolls that
346
+ pane back to the top — and a summary rendered at the end of the form would
347
+ be carried out of sight by the very focus meant to make the refusal
348
+ visible. What is marked at a field is not repeated here; what has no field
349
+ to be marked at would otherwise be silent. -->
350
+ <div v-if="summaryReasons.length" role="alert" class="mt-3 space-y-1 rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
351
+ <!-- Keyed by position: two undrawn keys can be refused with the same
352
+ sentence, and a duplicate key is how one of them goes unrendered. -->
353
+ <p v-for="(reason, i) in summaryReasons" :key="i">{{ reason }}</p>
354
+ </div>
355
+
356
+ <div class="flex items-center gap-2 pt-3">
357
+ <div class="flex-1" />
358
+ <Button variant="outline" :disabled="testing || saving" @click="runTest">
359
+ <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
360
+ <TestIcon v-else :size="14" class="mr-1" />
361
+ {{ t("integration.credentials.shared.action.testConnection") }}
362
+ </Button>
363
+ <Button :disabled="saving || !form.name.trim()" @click="save">
364
+ <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
365
+ {{ t("integration.common.action.save") }}
366
+ </Button>
336
367
  </div>
368
+ </template>
337
369
 
338
370
  <!-- Tab 2: Details -->
339
371
  <div v-else class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
@@ -1,25 +1,12 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
3
  import { Tooltip, TooltipContent, TooltipTrigger } from "@revenexx/studio";
4
+ import { formatShortcut } from "../../utils/shortcutLabel";
4
5
  const props = defineProps({
5
6
  label: { type: String, required: true },
6
7
  shortcut: { type: String, required: false }
7
8
  });
8
- const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
9
- const keys = computed(() => {
10
- if (!props.shortcut) return "";
11
- const out = [];
12
- for (const raw of props.shortcut.split("+")) {
13
- const t = raw.toLowerCase();
14
- if (t === "mod") out.push(isMac ? "\u2318" : "Ctrl");
15
- else if (t === "shift") out.push("\u21E7");
16
- else if (t === "alt") out.push(isMac ? "\u2325" : "Alt");
17
- else if (t === "enter") out.push("\u21B5");
18
- else if (t === "escape") out.push("Esc");
19
- else out.push(t.toUpperCase());
20
- }
21
- return isMac ? out.join("") : out.join("+");
22
- });
9
+ const keys = computed(() => props.shortcut ? formatShortcut(props.shortcut) : "");
23
10
  </script>
24
11
 
25
12
  <template>
@@ -21,6 +21,16 @@ type __VLS_Props = {
21
21
  }>;
22
22
  /** Why the last save refused this setting, if it did. */
23
23
  refused?: string;
24
+ /**
25
+ * The 1-based row this setting sits in, when it is a sub-setting of a list
26
+ * row — set only by the list branch below, rendering itself.
27
+ *
28
+ * It is what keeps a repeating control nameable: the visible label repeats
29
+ * per row, so every row's control would otherwise answer to the same name
30
+ * (specs/accessibility.md AC-1). With it, each says which setting AND which
31
+ * row it belongs to.
32
+ */
33
+ rowIndex?: number;
24
34
  };
25
35
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
26
36
  set: (key: string, value: unknown) => any;
@@ -15,7 +15,10 @@ import {
15
15
  Textarea
16
16
  } from "@revenexx/studio";
17
17
  import { resolveLocalized } from "../../utils/localize";
18
+ import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
18
19
  import { credentialState } from "../../utils/credentialState";
20
+ import { defaultsFor } from "../../utils/configDefaults";
21
+ import { applyingFields, settingApplies, staleKeys } from "../../utils/settingCondition";
19
22
  const props = defineProps({
20
23
  field: { type: Object, required: true },
21
24
  config: { type: Object, required: true },
@@ -28,7 +31,8 @@ const props = defineProps({
28
31
  variables: { type: Object, required: false },
29
32
  stateNamespaces: { type: Array, required: false },
30
33
  upstreamOutputs: { type: Array, required: false },
31
- refused: { type: String, required: false }
34
+ refused: { type: String, required: false },
35
+ rowIndex: { type: Number, required: false }
32
36
  });
33
37
  const emit = defineEmits(["set", "declare-state"]);
34
38
  const t = useIntegrationsT();
@@ -38,6 +42,11 @@ function write(v) {
38
42
  }
39
43
  const exprHint = "${{ \u2026 }}";
40
44
  const descriptionText = computed(() => resolveLocalized(props.field.description));
45
+ const inRow = computed(() => props.rowIndex !== void 0);
46
+ const controlName = computed(() => {
47
+ const label = resolveLocalized(props.field.label);
48
+ return inRow.value ? t("integration.nodeConfig.array.subFieldAria", { label, n: props.rowIndex }) : label;
49
+ });
41
50
  function isEmpty(v) {
42
51
  return v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
43
52
  }
@@ -107,12 +116,12 @@ const credOptions = computed(
107
116
  () => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c) }))
108
117
  );
109
118
  const UNKNOWN_CREDENTIAL = computed(() => t("integration.nodeConfig.credential.unknown"));
110
- function credTriggerLabel(field) {
111
- const label = resolveLocalized(field.label);
119
+ const credTriggerLabel = computed(() => {
120
+ const label = controlName.value;
112
121
  if (bound.value) return t("integration.nodeConfig.credential.trigger.bound", { label, name: bound.value.credential.name, state: bound.value.state.label });
113
122
  if (value.value && credentialsLoaded.value) return t("integration.nodeConfig.credential.trigger.unknown", { label, unknown: UNKNOWN_CREDENTIAL.value });
114
123
  return label;
115
- }
124
+ });
116
125
  const credCreateOpen = ref(false);
117
126
  const sftpImportOpen = ref(false);
118
127
  const CRED_ACTION_NEW = "__action_new_credential__";
@@ -267,17 +276,36 @@ function writeJson(text) {
267
276
  function readArray() {
268
277
  return Array.isArray(value.value) ? value.value : [];
269
278
  }
279
+ const rowKeys = ref([]);
280
+ let nextRowKey = 0;
281
+ watch(() => readArray().length, (n) => {
282
+ while (rowKeys.value.length < n) rowKeys.value.push(nextRowKey++);
283
+ if (rowKeys.value.length > n) rowKeys.value.splice(n);
284
+ }, { immediate: true });
270
285
  function pushArrayItem(defaultItem) {
271
286
  write([...readArray(), defaultItem]);
272
287
  }
288
+ function newRow() {
289
+ const declared = props.field.items?.properties ?? [];
290
+ const row = defaultsFor(declared);
291
+ for (; ; ) {
292
+ const withdrawn = declared.filter((f) => f.key in row && !settingApplies(f, row));
293
+ if (!withdrawn.length) return row;
294
+ for (const f of withdrawn) delete row[f.key];
295
+ }
296
+ }
273
297
  function removeArrayItem(index) {
274
298
  const arr = readArray().slice();
275
299
  arr.splice(index, 1);
300
+ rowKeys.value.splice(index, 1);
276
301
  write(arr);
277
302
  }
278
303
  function setArrayItemField(index, fieldKey, v) {
279
304
  const arr = readArray().slice();
280
- arr[index] = { ...arr[index] ?? {}, [fieldKey]: v };
305
+ const after = { ...arr[index] ?? {}, [fieldKey]: v };
306
+ for (const key of staleKeys(props.field.items?.properties ?? [], fieldKey, after))
307
+ delete after[key];
308
+ arr[index] = after;
281
309
  write(arr);
282
310
  }
283
311
  </script>
@@ -351,7 +379,7 @@ function setArrayItemField(index, fieldKey, v) {
351
379
 
352
380
  <template v-else-if="isJsonField">
353
381
  <Textarea
354
- :aria-label="resolveLocalized(field.label)"
382
+ :aria-label="controlName"
355
383
  :model-value="jsonDraft"
356
384
  :rows="8"
357
385
  spellcheck="false"
@@ -368,14 +396,14 @@ function setArrayItemField(index, fieldKey, v) {
368
396
  <!-- A repeating row has no room for visible labels, so each control
369
397
  names itself and says which row of which field it belongs to. -->
370
398
  <Input
371
- :aria-label="t('integration.nodeConfig.object.row.keyAria', { label: resolveLocalized(field.label), n: idx + 1 })"
399
+ :aria-label="t('integration.nodeConfig.object.row.keyAria', { label: controlName, n: idx + 1 })"
372
400
  :model-value="row.key"
373
401
  :placeholder="t('integration.nodeConfig.object.row.keyPlaceholder')"
374
402
  class="h-7 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
375
403
  @update:model-value="(v) => setKvRow(idx, { key: String(v) })"
376
404
  />
377
405
  <Input
378
- :aria-label="t('integration.nodeConfig.object.row.valueAria', { label: resolveLocalized(field.label), n: idx + 1 })"
406
+ :aria-label="t('integration.nodeConfig.object.row.valueAria', { label: controlName, n: idx + 1 })"
379
407
  :model-value="row.value"
380
408
  :placeholder="t('integration.nodeConfig.object.row.valuePlaceholder')"
381
409
  class="h-7 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
@@ -383,7 +411,7 @@ function setArrayItemField(index, fieldKey, v) {
383
411
  />
384
412
  <button
385
413
  type="button"
386
- :aria-label="t('integration.nodeConfig.object.row.removeAria', { label: resolveLocalized(field.label), n: idx + 1 })"
414
+ :aria-label="t('integration.nodeConfig.object.row.removeAria', { label: controlName, n: idx + 1 })"
387
415
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
388
416
  @click="removeKvRow(idx)"
389
417
  >
@@ -410,16 +438,22 @@ function setArrayItemField(index, fieldKey, v) {
410
438
  :variables="variables"
411
439
  :upstream-outputs="upstreamOutputs"
412
440
  :invalid="invalid"
413
- :label="resolveLocalized(field.label)"
441
+ :label="controlName"
414
442
  :multiline="isMultiline"
415
443
  @update:model-value="(v) => write(v)"
416
444
  />
417
445
 
418
446
  <!-- dynamic options — resolved at author time via config:resolve. Ahead of
419
447
  the generic expression branch so a dynamic select with
420
- expressionAllowed still renders as a dropdown by default. -->
448
+ expressionAllowed still renders as a dropdown by default.
449
+
450
+ Not inside a list row: the resolve request carries the config the
451
+ setting belongs to, and a row is not that config — the platform would
452
+ be asked about a set of values it has never heard of. No published node
453
+ declares one there; a row's setting is drawn from what it declares
454
+ instead (specs/node-configuration.md, ## Gaps). -->
421
455
  <IntegrationsNodeConfigDynamicField
422
- v-else-if="field.dynamic"
456
+ v-else-if="field.dynamic && !inRow"
423
457
  :slug="slug"
424
458
  :version="version"
425
459
  :field="field"
@@ -437,7 +471,7 @@ function setArrayItemField(index, fieldKey, v) {
437
471
  :variables="variables"
438
472
  :upstream-outputs="upstreamOutputs"
439
473
  :invalid="invalid"
440
- :label="resolveLocalized(field.label)"
474
+ :label="controlName"
441
475
  :multiline="isMultiline"
442
476
  @update:model-value="(v) => write(v)"
443
477
  />
@@ -451,7 +485,7 @@ function setArrayItemField(index, fieldKey, v) {
451
485
  shared control sizes itself to its content and ignores it. -->
452
486
  <Textarea
453
487
  v-else-if="field.type === 'string' && isMultiline"
454
- :aria-label="resolveLocalized(field.label)"
488
+ :aria-label="controlName"
455
489
  :model-value="value ?? ''"
456
490
  :placeholder="resolveLocalized(field.placeholder)"
457
491
  :rows="4"
@@ -463,7 +497,7 @@ function setArrayItemField(index, fieldKey, v) {
463
497
  <!-- plain string (no expressions) -->
464
498
  <Input
465
499
  v-else-if="field.type === 'string'"
466
- :aria-label="resolveLocalized(field.label)"
500
+ :aria-label="controlName"
467
501
  :model-value="value ?? ''"
468
502
  :placeholder="resolveLocalized(field.placeholder)"
469
503
  class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
@@ -474,7 +508,7 @@ function setArrayItemField(index, fieldKey, v) {
474
508
  <!-- number -->
475
509
  <Input
476
510
  v-else-if="field.type === 'number'"
477
- :aria-label="resolveLocalized(field.label)"
511
+ :aria-label="controlName"
478
512
  type="number"
479
513
  :model-value="value ?? ''"
480
514
  class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
@@ -485,7 +519,7 @@ function setArrayItemField(index, fieldKey, v) {
485
519
  <!-- boolean -->
486
520
  <div v-else-if="field.type === 'boolean'" class="flex items-center gap-2 pt-1">
487
521
  <Switch
488
- :aria-label="resolveLocalized(field.label)"
522
+ :aria-label="controlName"
489
523
  class="data-[state=unchecked]:bg-slate-300"
490
524
  :model-value="Boolean(value)"
491
525
  @update:model-value="(v) => write(v)"
@@ -495,9 +529,12 @@ function setArrayItemField(index, fieldKey, v) {
495
529
  </span>
496
530
  </div>
497
531
 
498
- <!-- dynamic schema — child fields resolved at author time, written into the flat config -->
532
+ <!-- dynamic schema — child fields resolved at author time, written into the
533
+ flat config. Withheld inside a list row for the same reason as the
534
+ branch above, and one more: the fields it resolves are written into the
535
+ node's flat config, which a row is not. -->
499
536
  <IntegrationsNodeConfigDynamicSchema
500
- v-else-if="field.type === 'dynamic-schema'"
537
+ v-else-if="field.type === 'dynamic-schema' && !inRow"
501
538
  :slug="slug"
502
539
  :version="version"
503
540
  :field="field"
@@ -511,21 +548,21 @@ function setArrayItemField(index, fieldKey, v) {
511
548
  <!-- select -->
512
549
  <Select
513
550
  v-else-if="field.type === 'select'"
514
- :model-value="value ?? ''"
515
- @update:model-value="(v) => write(v)"
551
+ :model-value="toModelValue(value, field.options)"
552
+ @update:model-value="(v) => write(fromModelValue(v))"
516
553
  >
517
- <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">
554
+ <SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
518
555
  <SelectValue :placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')" />
519
556
  </SelectTrigger>
520
557
  <SelectContent>
521
- <SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
558
+ <SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="toItemValue(opt.value)">
522
559
  {{ resolveLocalized(opt.label) }}
523
560
  </SelectItem>
524
561
  </SelectContent>
525
562
  </Select>
526
563
 
527
564
  <!-- multiselect — pill toggles for now -->
528
- <div v-else-if="field.type === 'multiselect'" role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
565
+ <div v-else-if="field.type === 'multiselect'" role="group" :aria-label="controlName" class="flex flex-wrap gap-1">
529
566
  <button
530
567
  v-for="opt in field.options ?? []"
531
568
  :key="String(opt.value)"
@@ -548,7 +585,7 @@ function setArrayItemField(index, fieldKey, v) {
548
585
  :model-value="value ?? ''"
549
586
  @update:model-value="(v) => write(v)"
550
587
  >
551
- <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">
588
+ <SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
552
589
  <SelectValue :placeholder="t('integration.nodeConfig.secretRef.placeholder')" />
553
590
  </SelectTrigger>
554
591
  <SelectContent>
@@ -564,7 +601,7 @@ function setArrayItemField(index, fieldKey, v) {
564
601
  :model-value="value ?? ''"
565
602
  @update:model-value="(v) => onCredentialSelect(v)"
566
603
  >
567
- <SelectTrigger :aria-label="credTriggerLabel(field)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
604
+ <SelectTrigger :aria-label="credTriggerLabel" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
568
605
  <!-- The trigger renders the bound credential itself instead of letting
569
606
  `SelectValue` echo the chosen row. Left to itself it shows that row's
570
607
  `textContent`, which glues the name and the state pill into one word —
@@ -653,7 +690,7 @@ function setArrayItemField(index, fieldKey, v) {
653
690
  :model-value="value ?? ''"
654
691
  @update:model-value="(v) => onStateSelect(v)"
655
692
  >
656
- <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">
693
+ <SelectTrigger :aria-label="controlName" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
657
694
  <SelectValue :placeholder="t('integration.nodeConfig.state.placeholder')" />
658
695
  </SelectTrigger>
659
696
  <SelectContent>
@@ -687,7 +724,7 @@ function setArrayItemField(index, fieldKey, v) {
687
724
  <div v-else-if="field.type === 'array' && field.items?.type === 'object'" class="space-y-2">
688
725
  <div
689
726
  v-for="(item, idx) in readArray()"
690
- :key="idx"
727
+ :key="rowKeys[idx] ?? idx"
691
728
  class="rounded-md border border-black/[0.06] bg-muted/20 p-2 space-y-1.5"
692
729
  >
693
730
  <div class="flex items-center justify-between">
@@ -696,39 +733,47 @@ function setArrayItemField(index, fieldKey, v) {
696
733
  </span>
697
734
  <button
698
735
  type="button"
699
- :aria-label="t('integration.nodeConfig.array.removeItemAria', { label: resolveLocalized(field.label), n: idx + 1 })"
736
+ :aria-label="t('integration.nodeConfig.array.removeItemAria', { label: controlName, n: idx + 1 })"
700
737
  class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
701
738
  @click="removeArrayItem(idx)"
702
739
  >
703
740
  <TrashBin :size="11" />
704
741
  </button>
705
742
  </div>
706
- <div
707
- v-for="sub in field.items.properties ?? []"
743
+ <!-- Each sub-setting is drawn by this same component, one level down,
744
+ the way a dynamic-schema child is: the row is its config, its own
745
+ `key` addresses its value in it, and what it declares — a list of
746
+ options, a switch, a description, a required marker — decides the
747
+ control, instead of every one of them being a text box.
748
+
749
+ `refused` is deliberately not handed down: it says why the LAST
750
+ SAVE refused this setting as a whole, so passing it on would mark
751
+ every sub-control of every row red with a sentence about a
752
+ different value. -->
753
+ <IntegrationsNodeConfigField
754
+ v-for="sub in applyingFields(field.items.properties ?? [], item ?? {})"
708
755
  :key="sub.key"
709
- class="space-y-1"
710
- >
711
- <Label class="text-mono text-muted-foreground">
712
- {{ resolveLocalized(sub.label) }}
713
- </Label>
714
- <!-- The label above repeats per item, so it cannot own an id; the
715
- input carries its own name including the item number. -->
716
- <Input
717
- :aria-label="t('integration.nodeConfig.array.subFieldAria', { label: resolveLocalized(sub.label), n: idx + 1 })"
718
- :model-value="item[sub.key] ?? ''"
719
- :placeholder="resolveLocalized(sub.placeholder)"
720
- class="h-7 text-sm bg-white"
721
- :class="sub.type === 'expression' ? 'font-mono' : ''"
722
- @update:model-value="(v) => setArrayItemField(idx, sub.key, v)"
723
- />
724
- </div>
756
+ :field="sub"
757
+ :config="item"
758
+ :slug="slug"
759
+ :version="version"
760
+ :secret-keys="secretKeys"
761
+ :static-keys="staticKeys"
762
+ :depth="(depth ?? 0) + 1"
763
+ :variables="variables"
764
+ :state-namespaces="stateNamespaces"
765
+ :upstream-outputs="upstreamOutputs"
766
+ :row-index="idx + 1"
767
+ @set="(k, v) => setArrayItemField(idx, k, v)"
768
+ @declare-state="(ns, role, visibility) => emit('declare-state', ns, role, visibility)"
769
+ />
725
770
  </div>
726
771
  <Button
727
772
  type="button"
728
773
  variant="outline"
729
774
  size="sm"
730
775
  class="h-7 text-sm w-full"
731
- @click="pushArrayItem(Object.fromEntries((field.items?.properties ?? []).map((p) => [p.key, ''])))"
776
+ @click="pushArrayItem(newRow())"
732
777
  >
733
778
  <Plus :size="11" />
734
779
  {{ t("integration.nodeConfig.action.addRow") }}
@@ -738,7 +783,7 @@ function setArrayItemField(index, fieldKey, v) {
738
783
  <!-- fallback: JSON textarea for complex types -->
739
784
  <Textarea
740
785
  v-else
741
- :aria-label="resolveLocalized(field.label)"
786
+ :aria-label="controlName"
742
787
  :model-value="value === void 0 ? '' : JSON.stringify(value, null, 2)"
743
788
  rows="3"
744
789
  class="rounded-md border border-black/[0.08] ring-0 shadow-none text-sm font-mono bg-white"
@@ -21,6 +21,16 @@ type __VLS_Props = {
21
21
  }>;
22
22
  /** Why the last save refused this setting, if it did. */
23
23
  refused?: string;
24
+ /**
25
+ * The 1-based row this setting sits in, when it is a sub-setting of a list
26
+ * row — set only by the list branch below, rendering itself.
27
+ *
28
+ * It is what keeps a repeating control nameable: the visible label repeats
29
+ * per row, so every row's control would otherwise answer to the same name
30
+ * (specs/accessibility.md AC-1). With it, each says which setting AND which
31
+ * row it belongs to.
32
+ */
33
+ rowIndex?: number;
24
34
  };
25
35
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
26
36
  set: (key: string, value: unknown) => any;