@revenexx/studio-integrations 1.3.0 → 1.4.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 (58) hide show
  1. package/CHANGELOG.md +426 -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/EditorTip.vue +2 -15
  7. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +10 -0
  8. package/dist/runtime/components/Integrations/NodeConfigField.vue +94 -49
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +10 -0
  10. package/dist/runtime/components/Integrations/NodeInspector.vue +37 -60
  11. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  12. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  13. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  14. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  15. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  16. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  17. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  18. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  19. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  20. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  21. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  22. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  23. package/dist/runtime/components/Integrations/WorkflowEditor.vue +165 -56
  24. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  25. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  26. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  27. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  28. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  29. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  30. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  31. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  32. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  33. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  34. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  35. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  36. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -8
  37. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  38. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -7
  39. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  40. package/dist/runtime/data/canvasShortcuts.js +68 -0
  41. package/dist/runtime/i18n/messages/de.json +30 -16
  42. package/dist/runtime/i18n/messages/en.json +30 -16
  43. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  44. package/dist/runtime/pages/integrations/workflows/[id].vue +128 -33
  45. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  46. package/dist/runtime/utils/apiEnvelope.js +20 -0
  47. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  48. package/dist/runtime/utils/configDefaults.js +7 -0
  49. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  50. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  51. package/dist/runtime/utils/notify.d.ts +2 -2
  52. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  53. package/dist/runtime/utils/runEnvelope.js +2 -1
  54. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  55. package/dist/runtime/utils/selectOptions.js +19 -0
  56. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  57. package/dist/runtime/utils/shortcutLabel.js +48 -0
  58. package/package.json +1 -1
@@ -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;
@@ -191,11 +191,16 @@ const applyingConfig = computed(
191
191
  );
192
192
  const parameterFields = computed(() => applyingConfig.value.filter((f) => !SETTINGS_FIELD_KEY.test(f.key)));
193
193
  const settingsFields = computed(() => applyingConfig.value.filter((f) => SETTINGS_FIELD_KEY.test(f.key)));
194
- const requiredFields = computed(() => parameterFields.value.filter((f) => f.required));
195
- const optionalFields = computed(() => parameterFields.value.filter((f) => !f.required));
194
+ const FOLD_ABOVE = 4;
195
+ const ALWAYS_DRAWN = 3;
196
+ const foldedFields = computed(
197
+ () => parameterFields.value.length > FOLD_ABOVE ? parameterFields.value.slice(ALWAYS_DRAWN).filter((f) => !f.required) : []
198
+ );
199
+ const foldedKeys = computed(() => new Set(foldedFields.value.map((f) => f.key)));
200
+ const foldAnchorKey = computed(() => foldedFields.value[0]?.key);
196
201
  function optionalShouldOpen() {
197
- if (!requiredFields.value.length) return true;
198
- return optionalFields.value.some((f) => props.refusedFields?.[f.key] !== void 0);
202
+ if (!parameterFields.value.some((f) => f.required)) return true;
203
+ return foldedFields.value.some((f) => props.refusedFields?.[f.key] !== void 0);
199
204
  }
200
205
  const optionalOpen = ref(optionalShouldOpen());
201
206
  watch([() => props.selection?.id, manifest], () => {
@@ -1805,67 +1810,39 @@ async function runTestStep() {
1805
1810
  </button>
1806
1811
  </div>
1807
1812
  </div>
1808
- <IntegrationsNodeConfigField
1809
- v-for="field in requiredFields"
1810
- :key="field.key"
1811
- :field="field"
1812
- :config="draftNode.config ?? {}"
1813
- :slug="draftNode.nodeSlug"
1814
- :version="draftNode.nodeVersion"
1815
- :secret-keys="secretKeys"
1816
- :static-keys="(manifest.config ?? []).map((f2) => f2.key)"
1817
- :sibling-fields="manifest.config ?? []"
1818
- :depth="0"
1819
- :variables="variables"
1820
- :state-namespaces="offeredStateNamespaces"
1821
- @declare-state="stageStateNamespace"
1822
- :upstream-outputs="upstreamOutputs"
1823
- :refused="refusedFields?.[field.key]"
1824
- @set="(k, v) => writeConfig(k, v)"
1825
- />
1826
-
1827
- <!-- Optional settings, folded. The count is on the control on
1828
- purpose: "Show 9 optional settings" is a decision the user can
1829
- take without opening it, "Show more" is not. -->
1830
- <template v-if="optionalFields.length">
1813
+ <!-- One list, in the order the node declared it. The fold's
1814
+ control is drawn at the first setting it hides rather than
1815
+ under the form, so opening it inserts the tail where it
1816
+ belongs instead of moving anything. The count is on the
1817
+ control on purpose: "Show 9 optional settings" is a decision
1818
+ the user can take without opening it, "Show more" is not. -->
1819
+ <template v-for="field in parameterFields" :key="field.key">
1831
1820
  <button
1832
- v-if="!optionalOpen"
1821
+ v-if="field.key === foldAnchorKey"
1833
1822
  type="button"
1834
1823
  class="flex items-center gap-1.5 rounded px-1 py-1 text-sm text-muted-foreground transition hover:text-foreground"
1835
- @click="optionalOpen = true"
1824
+ @click="optionalOpen = !optionalOpen"
1836
1825
  >
1837
- <ChevronRight :size="13" />
1838
- {{ t("integration.nodeInspector.parameters.optional.show", optionalFields.length) }}
1826
+ <component :is="optionalOpen ? ChevronDown : ChevronRight" :size="13" />
1827
+ {{ optionalOpen ? t("integration.nodeInspector.parameters.optional.hide", foldedFields.length) : t("integration.nodeInspector.parameters.optional.show", foldedFields.length) }}
1839
1828
  </button>
1840
- <template v-else>
1841
- <button
1842
- v-if="requiredFields.length"
1843
- type="button"
1844
- class="flex items-center gap-1.5 rounded px-1 py-1 text-sm text-muted-foreground transition hover:text-foreground"
1845
- @click="optionalOpen = false"
1846
- >
1847
- <ChevronDown :size="13" />
1848
- {{ t("integration.nodeInspector.parameters.optional.hide", optionalFields.length) }}
1849
- </button>
1850
- <IntegrationsNodeConfigField
1851
- v-for="field in optionalFields"
1852
- :key="field.key"
1853
- :field="field"
1854
- :config="draftNode.config ?? {}"
1855
- :slug="draftNode.nodeSlug"
1856
- :version="draftNode.nodeVersion"
1857
- :secret-keys="secretKeys"
1858
- :static-keys="(manifest.config ?? []).map((f2) => f2.key)"
1859
- :sibling-fields="manifest.config ?? []"
1860
- :depth="0"
1861
- :variables="variables"
1862
- :state-namespaces="offeredStateNamespaces"
1863
- @declare-state="stageStateNamespace"
1864
- :upstream-outputs="upstreamOutputs"
1865
- :refused="refusedFields?.[field.key]"
1866
- @set="(k, v) => writeConfig(k, v)"
1867
- />
1868
- </template>
1829
+ <IntegrationsNodeConfigField
1830
+ v-if="optionalOpen || !foldedKeys.has(field.key)"
1831
+ :field="field"
1832
+ :config="draftNode.config ?? {}"
1833
+ :slug="draftNode.nodeSlug"
1834
+ :version="draftNode.nodeVersion"
1835
+ :secret-keys="secretKeys"
1836
+ :static-keys="(manifest.config ?? []).map((f2) => f2.key)"
1837
+ :sibling-fields="manifest.config ?? []"
1838
+ :depth="0"
1839
+ :variables="variables"
1840
+ :state-namespaces="offeredStateNamespaces"
1841
+ @declare-state="stageStateNamespace"
1842
+ :upstream-outputs="upstreamOutputs"
1843
+ :refused="refusedFields?.[field.key]"
1844
+ @set="(k, v) => writeConfig(k, v)"
1845
+ />
1869
1846
  </template>
1870
1847
 
1871
1848
  <p v-if="!parameterFields.length" class="text-sm text-muted-foreground italic">