@revenexx/studio-integrations 0.5.1 → 0.6.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 (129) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +17 -4
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
  7. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
  8. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  9. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
  10. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  11. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
  13. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  14. package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
  15. package/dist/runtime/components/Integrations/Layout.vue +21 -2
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
  18. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  19. package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  21. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
  23. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  24. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  25. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  26. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  27. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  28. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  29. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  30. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  31. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
  32. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  33. package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
  34. package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
  35. package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
  36. package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
  38. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  39. package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
  40. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  41. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  42. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
  43. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  44. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
  45. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  46. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
  47. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  48. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  49. package/dist/runtime/components/Integrations/WorkflowEditor.vue +167 -46
  50. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  51. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
  52. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  53. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
  54. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  55. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  56. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
  57. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  58. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
  59. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
  60. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  61. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
  62. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  63. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
  64. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
  65. package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
  66. package/dist/runtime/composables/useCredentialTypes.js +4 -4
  67. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  68. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
  69. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  70. package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
  71. package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
  72. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  73. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  74. package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
  75. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
  76. package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
  77. package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
  78. package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
  79. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  80. package/dist/runtime/composables/useLoadGate.js +8 -0
  81. package/dist/runtime/data/branchReorder.d.ts +37 -0
  82. package/dist/runtime/data/branchReorder.js +71 -0
  83. package/dist/runtime/data/handleId.d.ts +23 -0
  84. package/dist/runtime/data/handleId.js +6 -0
  85. package/dist/runtime/data/integrationTemplates.d.ts +39 -0
  86. package/dist/runtime/data/integrationTemplates.js +15 -0
  87. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  88. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  89. package/dist/runtime/data/nodeIcons.js +73 -0
  90. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  91. package/dist/runtime/data/nodeInputData.js +4 -0
  92. package/dist/runtime/data/payloadForm.d.ts +9 -2
  93. package/dist/runtime/data/payloadForm.js +27 -3
  94. package/dist/runtime/data/portTone.d.ts +17 -0
  95. package/dist/runtime/data/portTone.js +7 -0
  96. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  97. package/dist/runtime/data/webhookAuth.js +19 -0
  98. package/dist/runtime/data/workflowIconCatalog.js +2 -5
  99. package/dist/runtime/pages/integrations/audit.vue +91 -6
  100. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
  101. package/dist/runtime/pages/integrations/credentials.vue +119 -21
  102. package/dist/runtime/pages/integrations/index.vue +49 -44
  103. package/dist/runtime/pages/integrations/runs.vue +1 -1
  104. package/dist/runtime/pages/integrations/schedules.vue +51 -7
  105. package/dist/runtime/pages/integrations/secrets.vue +29 -7
  106. package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
  107. package/dist/runtime/pages/integrations/templates/index.vue +57 -86
  108. package/dist/runtime/pages/integrations/workflows/[id].vue +196 -8
  109. package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
  110. package/dist/runtime/types/integrations.d.ts +65 -6
  111. package/dist/runtime/utils/apiError.d.ts +47 -0
  112. package/dist/runtime/utils/apiError.js +32 -0
  113. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  114. package/dist/runtime/utils/cockpitSources.js +14 -0
  115. package/dist/runtime/utils/loadGate.d.ts +61 -0
  116. package/dist/runtime/utils/loadGate.js +22 -0
  117. package/dist/runtime/utils/localize.d.ts +29 -0
  118. package/dist/runtime/utils/localize.js +4 -0
  119. package/dist/runtime/utils/notify.d.ts +59 -0
  120. package/dist/runtime/utils/notify.js +21 -0
  121. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  122. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  123. package/dist/runtime/utils/schedule.d.ts +141 -0
  124. package/dist/runtime/utils/schedule.js +207 -0
  125. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  126. package/dist/runtime/utils/validationFindings.js +53 -0
  127. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  128. package/dist/runtime/utils/whenIdle.js +12 -0
  129. package/package.json +8 -2
@@ -8,11 +8,14 @@ import {
8
8
  Select,
9
9
  SelectContent,
10
10
  SelectItem,
11
+ SelectSeparator,
11
12
  SelectTrigger,
12
13
  SelectValue,
13
14
  Switch,
14
15
  Textarea
15
16
  } from "@revenexx/studio";
17
+ import { resolveLocalized } from "../../utils/localize";
18
+ import { credentialState } from "../../utils/credentialState";
16
19
  const props = defineProps({
17
20
  field: { type: Object, required: true },
18
21
  config: { type: Object, required: true },
@@ -22,23 +25,19 @@ const props = defineProps({
22
25
  staticKeys: { type: Array, required: false },
23
26
  depth: { type: Number, required: false },
24
27
  variables: { type: Object, required: false },
25
- upstreamOutputs: { type: Array, required: false }
28
+ upstreamOutputs: { type: Array, required: false },
29
+ refused: { type: String, required: false }
26
30
  });
27
31
  const emit = defineEmits(["set"]);
28
32
  const value = computed(() => props.config[props.field.key]);
29
33
  function write(v) {
30
34
  emit("set", props.field.key, v);
31
35
  }
32
- function labelText(v) {
33
- if (!v) return "";
34
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
35
- }
36
36
  const exprHint = "${{ \u2026 }}";
37
- const descriptionText = computed(() => labelText(props.field.description));
37
+ const descriptionText = computed(() => resolveLocalized(props.field.description));
38
38
  function isEmpty(v) {
39
39
  return v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
40
40
  }
41
- const requiredMissing = computed(() => !!props.field.required && isEmpty(value.value));
42
41
  const formatError = computed(() => {
43
42
  const v = value.value;
44
43
  if (isEmpty(v)) return null;
@@ -60,7 +59,8 @@ const formatError = computed(() => {
60
59
  }
61
60
  return null;
62
61
  });
63
- const invalid = computed(() => !!formatError.value);
62
+ const fieldError = computed(() => formatError.value ?? props.refused ?? null);
63
+ const invalid = computed(() => !!fieldError.value);
64
64
  const constraintHint = computed(() => {
65
65
  const val = props.field.validation;
66
66
  if (!val) return "";
@@ -75,7 +75,7 @@ const constraintHint = computed(() => {
75
75
  }
76
76
  return "";
77
77
  });
78
- const { credentials, load: loadCredentials } = useIntegrationsCredentials();
78
+ const { credentials, loaded: credentialsLoaded, load: loadCredentials } = useIntegrationsCredentials();
79
79
  onMounted(() => {
80
80
  void loadCredentials();
81
81
  });
@@ -95,11 +95,36 @@ function credsForField(field) {
95
95
  }
96
96
  return list;
97
97
  }
98
- function credNeedsReauth(id) {
99
- return credentials.value.some((c) => c.id === id && c.status === "needs_reauth");
98
+ const bound = computed(() => {
99
+ const id = value.value;
100
+ const c = typeof id === "string" && id ? credentials.value.find((x) => x.id === id) : void 0;
101
+ return c ? { credential: c, state: credentialState(c) } : null;
102
+ });
103
+ const credOptions = computed(
104
+ () => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c) }))
105
+ );
106
+ const UNKNOWN_CREDENTIAL = "Unknown credential \u2014 pick again";
107
+ function credTriggerLabel(field) {
108
+ 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}`;
111
+ return label;
100
112
  }
101
113
  const credCreateOpen = ref(false);
102
114
  const sftpImportOpen = ref(false);
115
+ const CRED_ACTION_NEW = "__action_new_credential__";
116
+ const CRED_ACTION_SFTP = "__action_import_sftp__";
117
+ function onCredentialSelect(v) {
118
+ if (v === CRED_ACTION_NEW) {
119
+ credCreateOpen.value = true;
120
+ return;
121
+ }
122
+ if (v === CRED_ACTION_SFTP) {
123
+ sftpImportOpen.value = true;
124
+ return;
125
+ }
126
+ write(v);
127
+ }
103
128
  function acceptsSftp(field) {
104
129
  const t = field.credentialType;
105
130
  return t === "revenexx:sftp" || Array.isArray(t) && t.includes("revenexx:sftp");
@@ -230,8 +255,18 @@ function setArrayItemField(index, fieldKey, v) {
230
255
  <template>
231
256
  <div class="space-y-1.5">
232
257
  <div class="flex items-center justify-between gap-2">
233
- <Label class="text-mono text-muted-foreground">
234
- {{ labelText(field.label) }}<span v-if="field.required" class="text-rose-500" title="Required"> *</span>
258
+ <!-- The label carries the fault as well as the control does. Only a few
259
+ of the widget types below are a bare input that can take a red
260
+ border, so a credential picker or a switch at fault would otherwise
261
+ be marked only by the sentence underneath.
262
+
263
+ No `for`: which control this heads depends on the field's type — one
264
+ of a dozen branches, some of them a group rather than a single
265
+ element. Each branch instead carries the same text as its own name,
266
+ without the required marker, which is a state and not part of a
267
+ name. -->
268
+ <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>
235
270
  </Label>
236
271
  <div class="flex shrink-0 items-center gap-1.5">
237
272
  <div v-if="isObjectField && !showExpression" class="flex items-center rounded border border-black/[0.08] bg-white p-px">
@@ -274,11 +309,6 @@ function setArrayItemField(index, fieldKey, v) {
274
309
  </span>
275
310
  </div>
276
311
  </div>
277
- <!-- inline help (boolean uses its description as the toggle label instead) -->
278
- <p v-if="descriptionText && field.type !== 'boolean'" class="text-sm text-muted-foreground leading-snug">
279
- {{ descriptionText }}
280
- </p>
281
-
282
312
  <!-- object / plain-array fields: JSON editor (expressions stay usable
283
313
  inside string values). Wins over expressionAllowed — an object value
284
314
  in a text input would render as [object Object]. -->
@@ -291,11 +321,12 @@ function setArrayItemField(index, fieldKey, v) {
291
321
 
292
322
  <template v-else-if="isJsonField">
293
323
  <Textarea
324
+ :aria-label="resolveLocalized(field.label)"
294
325
  :model-value="jsonDraft"
295
326
  :rows="8"
296
327
  spellcheck="false"
297
- class="font-mono text-xs"
298
- :placeholder="labelText(field.placeholder) || '{ }'"
328
+ class="rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
329
+ :placeholder="resolveLocalized(field.placeholder) || '{ }'"
299
330
  @update:model-value="(v) => writeJson(String(v))"
300
331
  />
301
332
  <p v-if="jsonError" class="text-sm text-amber-600">{{ jsonError }}</p>
@@ -304,20 +335,25 @@ function setArrayItemField(index, fieldKey, v) {
304
335
  <!-- object field in key/value mode -->
305
336
  <div v-else-if="isObjectField && objectMode === 'form'" class="space-y-1.5">
306
337
  <div v-for="(row, idx) in kvRows" :key="idx" class="flex items-center gap-1.5">
338
+ <!-- A repeating row has no room for visible labels, so each control
339
+ names itself and says which row of which field it belongs to. -->
307
340
  <Input
341
+ :aria-label="`${resolveLocalized(field.label)} \u2014 key of row ${idx + 1}`"
308
342
  :model-value="row.key"
309
343
  placeholder="key"
310
- class="h-7 w-[38%] bg-white font-mono text-sm"
344
+ class="h-7 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
311
345
  @update:model-value="(v) => setKvRow(idx, { key: String(v) })"
312
346
  />
313
347
  <Input
348
+ :aria-label="`${resolveLocalized(field.label)} \u2014 value of row ${idx + 1}`"
314
349
  :model-value="row.value"
315
350
  placeholder="value"
316
- class="h-7 min-w-0 flex-1 bg-white text-sm"
351
+ class="h-7 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
317
352
  @update:model-value="(v) => setKvRow(idx, { value: String(v) })"
318
353
  />
319
354
  <button
320
355
  type="button"
356
+ :aria-label="`Remove row ${idx + 1} of ${resolveLocalized(field.label)}`"
321
357
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
322
358
  @click="removeKvRow(idx)"
323
359
  >
@@ -340,9 +376,11 @@ function setArrayItemField(index, fieldKey, v) {
340
376
  <IntegrationsExpressionInput
341
377
  v-else-if="showExpression"
342
378
  :model-value="typeof value === 'string' ? value : ''"
343
- :placeholder="labelText(field.placeholder)"
379
+ :placeholder="resolveLocalized(field.placeholder)"
344
380
  :variables="variables"
345
381
  :upstream-outputs="upstreamOutputs"
382
+ :invalid="invalid"
383
+ :label="resolveLocalized(field.label)"
346
384
  @update:model-value="(v) => write(v)"
347
385
  />
348
386
 
@@ -363,18 +401,21 @@ function setArrayItemField(index, fieldKey, v) {
363
401
  <IntegrationsExpressionInput
364
402
  v-else-if="field.type === 'expression' || field.expressionAllowed"
365
403
  :model-value="value"
366
- :placeholder="labelText(field.placeholder)"
404
+ :placeholder="resolveLocalized(field.placeholder)"
367
405
  :variables="variables"
368
406
  :upstream-outputs="upstreamOutputs"
407
+ :invalid="invalid"
408
+ :label="resolveLocalized(field.label)"
369
409
  @update:model-value="(v) => write(v)"
370
410
  />
371
411
 
372
412
  <!-- plain string (no expressions) -->
373
413
  <Input
374
414
  v-else-if="field.type === 'string'"
415
+ :aria-label="resolveLocalized(field.label)"
375
416
  :model-value="value ?? ''"
376
- :placeholder="labelText(field.placeholder)"
377
- class="h-8 text-sm bg-white"
417
+ :placeholder="resolveLocalized(field.placeholder)"
418
+ class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
378
419
  :class="invalid ? 'border-rose-400 focus-visible:ring-rose-400/40' : ''"
379
420
  @update:model-value="(v) => write(v)"
380
421
  />
@@ -382,9 +423,10 @@ function setArrayItemField(index, fieldKey, v) {
382
423
  <!-- number -->
383
424
  <Input
384
425
  v-else-if="field.type === 'number'"
426
+ :aria-label="resolveLocalized(field.label)"
385
427
  type="number"
386
428
  :model-value="value ?? ''"
387
- class="h-8 text-sm bg-white"
429
+ class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
388
430
  :class="invalid ? 'border-rose-400 focus-visible:ring-rose-400/40' : ''"
389
431
  @update:model-value="(v) => write(v === '' ? null : Number(v))"
390
432
  />
@@ -392,11 +434,13 @@ function setArrayItemField(index, fieldKey, v) {
392
434
  <!-- boolean -->
393
435
  <div v-else-if="field.type === 'boolean'" class="flex items-center gap-2 pt-1">
394
436
  <Switch
437
+ :aria-label="resolveLocalized(field.label)"
438
+ class="data-[state=unchecked]:bg-slate-300"
395
439
  :model-value="Boolean(value)"
396
440
  @update:model-value="(v) => write(v)"
397
441
  />
398
442
  <span class="text-sm text-muted-foreground">
399
- {{ labelText(field.description) }}
443
+ {{ resolveLocalized(field.description) }}
400
444
  </span>
401
445
  </div>
402
446
 
@@ -419,18 +463,18 @@ function setArrayItemField(index, fieldKey, v) {
419
463
  :model-value="value ?? ''"
420
464
  @update:model-value="(v) => write(v)"
421
465
  >
422
- <SelectTrigger class="h-8 text-sm bg-white">
423
- <SelectValue :placeholder="labelText(field.placeholder) || 'Pick one'" />
466
+ <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'" />
424
468
  </SelectTrigger>
425
469
  <SelectContent>
426
470
  <SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
427
- {{ labelText(opt.label) }}
471
+ {{ resolveLocalized(opt.label) }}
428
472
  </SelectItem>
429
473
  </SelectContent>
430
474
  </Select>
431
475
 
432
476
  <!-- multiselect — pill toggles for now -->
433
- <div v-else-if="field.type === 'multiselect'" class="flex flex-wrap gap-1">
477
+ <div v-else-if="field.type === 'multiselect'" role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
434
478
  <button
435
479
  v-for="opt in field.options ?? []"
436
480
  :key="String(opt.value)"
@@ -443,7 +487,7 @@ function setArrayItemField(index, fieldKey, v) {
443
487
  write(has ? current.filter((v) => v !== opt.value) : [...current, opt.value]);
444
488
  }"
445
489
  >
446
- {{ labelText(opt.label) }}
490
+ {{ resolveLocalized(opt.label) }}
447
491
  </button>
448
492
  </div>
449
493
 
@@ -453,7 +497,7 @@ function setArrayItemField(index, fieldKey, v) {
453
497
  :model-value="value ?? ''"
454
498
  @update:model-value="(v) => write(v)"
455
499
  >
456
- <SelectTrigger class="h-8 text-sm bg-white">
500
+ <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">
457
501
  <SelectValue placeholder="Pick a secret" />
458
502
  </SelectTrigger>
459
503
  <SelectContent>
@@ -461,44 +505,82 @@ function setArrayItemField(index, fieldKey, v) {
461
505
  </SelectContent>
462
506
  </Select>
463
507
 
464
- <!-- credentials-ref — pick a credential of the required type -->
508
+ <!-- credentials-ref — pick a credential of the required type. Compact:
509
+ creating and importing live INSIDE the picker as action rows; no
510
+ Manage link (it navigated out of the editor mid-edit). -->
465
511
  <template v-else-if="field.type === 'credentials-ref'">
466
512
  <Select
467
513
  :model-value="value ?? ''"
468
- @update:model-value="(v) => write(v)"
514
+ @update:model-value="(v) => onCredentialSelect(v)"
469
515
  >
470
- <SelectTrigger class="h-8 text-sm bg-white">
471
- <SelectValue placeholder="Pick a credential" />
516
+ <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">
517
+ <!-- The trigger renders the bound credential itself instead of letting
518
+ `SelectValue` echo the chosen row. Left to itself it shows that row's
519
+ `textContent`, which glues the name and the state pill into one word —
520
+ "Revenexx APIConnected". Rendering it here keeps the pill a pill, and
521
+ the placeholder has to be drawn by hand because a slot replaces the
522
+ fallback that would otherwise show it. -->
523
+ <SelectValue placeholder="Pick a credential">
524
+ <span v-if="bound" class="flex min-w-0 items-center gap-1.5">
525
+ <span class="min-w-0 truncate">{{ bound.credential.name }}</span>
526
+ <!-- No `title` on this badge: `SelectValue` renders itself with
527
+ `pointer-events: none`, so nothing inside it is ever hovered and a
528
+ tooltip here would be a reason nobody can reach. A failed test's
529
+ reason is prose beneath the field instead. -->
530
+ <span
531
+ class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
532
+ :class="bound.state.toneClass"
533
+ >
534
+ {{ bound.state.label }}
535
+ </span>
536
+ </span>
537
+ <!-- A stored id the tenant's credentials no longer contain: the
538
+ credential was deleted, and the field would otherwise look untouched
539
+ while the workflow still runs into the dead reference. `loaded`
540
+ keeps the wait before the first response out of it. -->
541
+ <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
+ </SelectValue>
472
544
  </SelectTrigger>
473
545
  <SelectContent>
474
- <SelectItem v-for="c in credsForField(field)" :key="c.id" :value="c.id">
546
+ <!-- The state comes from `credentialState`, the same derivation the
547
+ credentials table and the detail dialog use. This picker used to read
548
+ `status` alone and call everything that was not `needs_reauth`
549
+ "Connected" — including a credential nobody had ever tested, which is
550
+ a pass claimed for a test that never ran. -->
551
+ <SelectItem v-for="o in credOptions" :key="o.credential.id" :value="o.credential.id">
475
552
  <span class="flex items-center gap-1.5">
476
- <span class="size-1.5 shrink-0 rounded-full" :class="c.status === 'needs_reauth' ? 'bg-amber-500' : 'bg-emerald-500'" />
477
- <span class="min-w-0 truncate">{{ c.name }}</span>
478
- <span v-if="c.status === 'needs_reauth'" class="shrink-0 text-[10px] text-amber-600">reconnect</span>
553
+ <span class="min-w-0 truncate">{{ o.credential.name }}</span>
554
+ <!-- `title` carries why a failed test failed, the way the credentials
555
+ table does it. Without it "Failed" is a verdict with no appeal. A
556
+ row is a real hit target, unlike the closed trigger above. -->
557
+ <span
558
+ class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
559
+ :class="o.state.toneClass"
560
+ :title="o.state.message ?? void 0"
561
+ >{{ o.state.label }}</span>
479
562
  </span>
480
563
  </SelectItem>
564
+ <div v-if="!credOptions.length" class="px-2 py-1.5 text-sm text-muted-foreground">
565
+ No credentials yet.
566
+ </div>
567
+ <SelectSeparator />
568
+ <SelectItem :value="CRED_ACTION_NEW" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
569
+ New credential
570
+ </SelectItem>
571
+ <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
+ </SelectItem>
481
574
  </SelectContent>
482
575
  </Select>
483
- <p v-if="credNeedsReauth(value)" class="text-sm text-amber-600">
576
+ <!-- What the state has to add, as text rather than as a tooltip — the shape
577
+ the detail dialog uses beneath its own badge. -->
578
+ <p v-if="bound?.state.key === 'needs_reauth'" class="text-sm text-amber-600">
484
579
  This credential needs reconnect.
485
580
  </p>
486
- <div class="flex items-center gap-3">
487
- <button type="button" class="text-sm font-medium text-violet-600 hover:underline" @click="credCreateOpen = true">
488
- + New credential
489
- </button>
490
- <button
491
- v-if="acceptsSftp(field)"
492
- type="button"
493
- class="text-sm font-medium text-violet-600 hover:underline"
494
- @click="sftpImportOpen = true"
495
- >
496
- Import SFTP account
497
- </button>
498
- <NuxtLink to="/integrations/credentials" class="text-sm text-muted-foreground hover:underline">
499
- Manage →
500
- </NuxtLink>
501
- </div>
581
+ <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
+ </p>
502
584
  <IntegrationsCredentialCreateDialog
503
585
  v-model:open="credCreateOpen"
504
586
  :credential-type="field.credentialType"
@@ -524,6 +606,7 @@ function setArrayItemField(index, fieldKey, v) {
524
606
  </span>
525
607
  <button
526
608
  type="button"
609
+ :aria-label="`Remove item ${idx + 1} of ${resolveLocalized(field.label)}`"
527
610
  class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
528
611
  @click="removeArrayItem(idx)"
529
612
  >
@@ -536,11 +619,14 @@ function setArrayItemField(index, fieldKey, v) {
536
619
  class="space-y-1"
537
620
  >
538
621
  <Label class="text-mono text-muted-foreground">
539
- {{ labelText(sub.label) }}
622
+ {{ resolveLocalized(sub.label) }}
540
623
  </Label>
624
+ <!-- The label above repeats per item, so it cannot own an id; the
625
+ input carries its own name including the item number. -->
541
626
  <Input
627
+ :aria-label="`${resolveLocalized(sub.label)} of item ${idx + 1}`"
542
628
  :model-value="item[sub.key] ?? ''"
543
- :placeholder="labelText(sub.placeholder)"
629
+ :placeholder="resolveLocalized(sub.placeholder)"
544
630
  class="h-7 text-sm bg-white"
545
631
  :class="sub.type === 'expression' ? 'font-mono' : ''"
546
632
  @update:model-value="(v) => setArrayItemField(idx, sub.key, v)"
@@ -562,9 +648,10 @@ function setArrayItemField(index, fieldKey, v) {
562
648
  <!-- fallback: JSON textarea for complex types -->
563
649
  <Textarea
564
650
  v-else
651
+ :aria-label="resolveLocalized(field.label)"
565
652
  :model-value="value === void 0 ? '' : JSON.stringify(value, null, 2)"
566
653
  rows="3"
567
- class="text-sm font-mono bg-white"
654
+ class="rounded-md border border-black/[0.08] ring-0 shadow-none text-sm font-mono bg-white"
568
655
  @update:model-value="(v) => {
569
656
  try {
570
657
  write(v ? JSON.parse(String(v)) : void 0);
@@ -573,12 +660,18 @@ function setArrayItemField(index, fieldKey, v) {
573
660
  }"
574
661
  />
575
662
 
576
- <!-- validation feedback: hard error, else soft required hint, else constraint hint -->
577
- <p v-if="formatError" class="text-sm text-rose-600">
578
- {{ formatError }}
663
+ <!-- inline help BELOW the control, quiet — same pattern as the Settings
664
+ tab (boolean uses its description as the toggle label instead) -->
665
+ <p v-if="descriptionText && field.type !== 'boolean'" class="text-xs text-muted-foreground leading-snug">
666
+ {{ descriptionText }}
579
667
  </p>
580
- <p v-else-if="requiredMissing" class="text-sm text-amber-600">
581
- Required
668
+
669
+ <!-- validation feedback: the error (local format violation, else what the
670
+ last save refused), otherwise the constraint hint. A required field the
671
+ user has simply not reached yet says nothing beyond the * on its
672
+ label — the nagging would outnumber the help. -->
673
+ <p v-if="fieldError" class="text-sm text-rose-600">
674
+ {{ fieldError }}
582
675
  </p>
583
676
  <p v-else-if="constraintHint" class="text-sm text-muted-foreground">
584
677
  {{ constraintHint }}
@@ -14,6 +14,8 @@ type __VLS_Props = {
14
14
  output: string;
15
15
  fields?: string[];
16
16
  }>;
17
+ /** Why the last save refused this setting, if it did. */
18
+ refused?: string;
17
19
  };
18
20
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
21
  set: (key: string, value: unknown) => any;
@@ -20,6 +20,12 @@ type __VLS_Props = {
20
20
  dynamicOutputs?: Record<string, NodeManifestOutput[]>;
21
21
  /** Workflow id — enables sampling real values from the latest run. */
22
22
  workflowId?: number | string;
23
+ /**
24
+ * Config key → why the LAST SAVE refused it. Not live validation: the server
25
+ * is the only judge, so a fix shows once it is saved. Absent for a node the
26
+ * last save had no complaint about.
27
+ */
28
+ refusedFields?: Record<string, string>;
23
29
  };
24
30
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
25
31
  close: () => any;