@revenexx/studio-integrations 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, onMounted, ref, watch } from "vue";
3
- import { AddCircle as Plus, TrashBinMinimalistic as Trash2 } from "#solar-icons";
3
+ import { AddCircle as Plus, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import {
5
5
  Button,
6
6
  Input,
@@ -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,45 +25,43 @@ 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"]);
32
+ const t = useIntegrationsT();
28
33
  const value = computed(() => props.config[props.field.key]);
29
34
  function write(v) {
30
35
  emit("set", props.field.key, v);
31
36
  }
32
- function labelText(v) {
33
- if (!v) return "";
34
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
35
- }
36
37
  const exprHint = "${{ \u2026 }}";
37
- const descriptionText = computed(() => labelText(props.field.description));
38
+ const descriptionText = computed(() => resolveLocalized(props.field.description));
38
39
  function isEmpty(v) {
39
40
  return v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
40
41
  }
41
- const requiredMissing = computed(() => !!props.field.required && isEmpty(value.value));
42
42
  const formatError = computed(() => {
43
43
  const v = value.value;
44
44
  if (isEmpty(v)) return null;
45
45
  const val = props.field.validation;
46
46
  if (!val) return null;
47
47
  if (typeof v === "string") {
48
- if (val.minLength != null && v.length < val.minLength) return `Must be at least ${val.minLength} characters`;
49
- if (val.maxLength != null && v.length > val.maxLength) return `Must be at most ${val.maxLength} characters`;
48
+ if (val.minLength != null && v.length < val.minLength) return t("integration.nodeConfig.validation.minLength", { min: val.minLength });
49
+ if (val.maxLength != null && v.length > val.maxLength) return t("integration.nodeConfig.validation.maxLength", { max: val.maxLength });
50
50
  if (val.pattern) {
51
51
  try {
52
- if (!new RegExp(val.pattern).test(v)) return "Does not match the required format";
52
+ if (!new RegExp(val.pattern).test(v)) return t("integration.nodeConfig.validation.pattern");
53
53
  } catch {
54
54
  }
55
55
  }
56
56
  }
57
57
  if (typeof v === "number") {
58
- if (val.min != null && v < val.min) return `Must be \u2265 ${val.min}`;
59
- if (val.max != null && v > val.max) return `Must be \u2264 ${val.max}`;
58
+ if (val.min != null && v < val.min) return t("integration.nodeConfig.validation.min", { min: val.min });
59
+ if (val.max != null && v > val.max) return t("integration.nodeConfig.validation.max", { max: val.max });
60
60
  }
61
61
  return null;
62
62
  });
63
- const invalid = computed(() => !!formatError.value);
63
+ const fieldError = computed(() => formatError.value ?? props.refused ?? null);
64
+ const invalid = computed(() => !!fieldError.value);
64
65
  const constraintHint = computed(() => {
65
66
  const val = props.field.validation;
66
67
  if (!val) return "";
@@ -69,13 +70,13 @@ const constraintHint = computed(() => {
69
70
  if (val.min != null) return `\u2265 ${val.min}`;
70
71
  if (val.max != null) return `\u2264 ${val.max}`;
71
72
  } else {
72
- if (val.minLength != null && val.maxLength != null) return `${val.minLength}\u2013${val.maxLength} characters`;
73
- if (val.minLength != null) return `at least ${val.minLength} characters`;
74
- if (val.maxLength != null) return `up to ${val.maxLength} characters`;
73
+ if (val.minLength != null && val.maxLength != null) return t("integration.nodeConfig.hint.charRange", { min: val.minLength, max: val.maxLength });
74
+ if (val.minLength != null) return t("integration.nodeConfig.hint.charMin", { min: val.minLength });
75
+ if (val.maxLength != null) return t("integration.nodeConfig.hint.charMax", { max: val.maxLength });
75
76
  }
76
77
  return "";
77
78
  });
78
- const { credentials, load: loadCredentials } = useIntegrationsCredentials();
79
+ const { credentials, loaded: credentialsLoaded, load: loadCredentials } = useIntegrationsCredentials();
79
80
  onMounted(() => {
80
81
  void loadCredentials();
81
82
  });
@@ -95,14 +96,39 @@ function credsForField(field) {
95
96
  }
96
97
  return list;
97
98
  }
98
- function credNeedsReauth(id) {
99
- return credentials.value.some((c) => c.id === id && c.status === "needs_reauth");
99
+ const bound = computed(() => {
100
+ const id = value.value;
101
+ const c = typeof id === "string" && id ? credentials.value.find((x) => x.id === id) : void 0;
102
+ return c ? { credential: c, state: credentialState(c) } : null;
103
+ });
104
+ const credOptions = computed(
105
+ () => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c) }))
106
+ );
107
+ const UNKNOWN_CREDENTIAL = computed(() => t("integration.nodeConfig.credential.unknown"));
108
+ function credTriggerLabel(field) {
109
+ const label = resolveLocalized(field.label);
110
+ if (bound.value) return t("integration.nodeConfig.credential.trigger.bound", { label, name: bound.value.credential.name, state: bound.value.state.label });
111
+ if (value.value && credentialsLoaded.value) return t("integration.nodeConfig.credential.trigger.unknown", { label, unknown: UNKNOWN_CREDENTIAL.value });
112
+ return label;
100
113
  }
101
114
  const credCreateOpen = ref(false);
102
115
  const sftpImportOpen = ref(false);
116
+ const CRED_ACTION_NEW = "__action_new_credential__";
117
+ const CRED_ACTION_SFTP = "__action_import_sftp__";
118
+ function onCredentialSelect(v) {
119
+ if (v === CRED_ACTION_NEW) {
120
+ credCreateOpen.value = true;
121
+ return;
122
+ }
123
+ if (v === CRED_ACTION_SFTP) {
124
+ sftpImportOpen.value = true;
125
+ return;
126
+ }
127
+ write(v);
128
+ }
103
129
  function acceptsSftp(field) {
104
- const t = field.credentialType;
105
- return t === "revenexx:sftp" || Array.isArray(t) && t.includes("revenexx:sftp");
130
+ const t2 = field.credentialType;
131
+ return t2 === "revenexx:sftp" || Array.isArray(t2) && t2.includes("revenexx:sftp");
106
132
  }
107
133
  const EXPR_TOKEN = "${{";
108
134
  const canToggleExpression = computed(
@@ -206,7 +232,7 @@ function writeJson(text) {
206
232
  jsonError.value = null;
207
233
  emit("set", props.field.key, parsed);
208
234
  } catch {
209
- jsonError.value = "Not valid JSON yet \u2014 the value is kept once it parses.";
235
+ jsonError.value = t("integration.nodeConfig.json.invalid");
210
236
  }
211
237
  }
212
238
  function readArray() {
@@ -230,29 +256,39 @@ function setArrayItemField(index, fieldKey, v) {
230
256
  <template>
231
257
  <div class="space-y-1.5">
232
258
  <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>
259
+ <!-- The label carries the fault as well as the control does. Only a few
260
+ of the widget types below are a bare input that can take a red
261
+ border, so a credential picker or a switch at fault would otherwise
262
+ be marked only by the sentence underneath.
263
+
264
+ No `for`: which control this heads depends on the field's type — one
265
+ of a dozen branches, some of them a group rather than a single
266
+ element. Each branch instead carries the same text as its own name,
267
+ without the required marker, which is a state and not part of a
268
+ name. -->
269
+ <Label class="text-mono" :class="invalid ? 'text-rose-600' : 'text-muted-foreground'">
270
+ {{ resolveLocalized(field.label) }}<span v-if="field.required" class="text-rose-500" :title="t('integration.nodeConfig.field.requiredMark')"> *</span>
235
271
  </Label>
236
272
  <div class="flex shrink-0 items-center gap-1.5">
237
273
  <div v-if="isObjectField && !showExpression" class="flex items-center rounded border border-black/[0.08] bg-white p-px">
238
274
  <button
239
275
  type="button"
240
276
  :disabled="!flatCapable"
241
- title="Edit as key/value rows"
277
+ :title="t('integration.nodeConfig.object.toggle.fieldsTitle')"
242
278
  class="rounded px-1 py-px text-[10px] font-medium transition disabled:pointer-events-none disabled:opacity-40"
243
279
  :class="objectMode === 'form' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
244
280
  @click="objectMode = 'form'"
245
281
  >
246
- Fields
282
+ {{ t("integration.nodeConfig.object.toggle.fields") }}
247
283
  </button>
248
284
  <button
249
285
  type="button"
250
- title="Edit as raw JSON"
286
+ :title="t('integration.nodeConfig.object.toggle.jsonTitle')"
251
287
  class="rounded px-1 py-px text-[10px] font-medium transition"
252
288
  :class="objectMode === 'json' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
253
289
  @click="objectMode = 'json'"
254
290
  >
255
- JSON
291
+ {{ t("integration.nodeConfig.object.toggle.json") }}
256
292
  </button>
257
293
  </div>
258
294
  <button
@@ -260,7 +296,7 @@ function setArrayItemField(index, fieldKey, v) {
260
296
  type="button"
261
297
  class="rounded border px-1 py-0.5 font-mono text-[10px] transition"
262
298
  :class="showExpression ? 'border-emerald-500/40 bg-emerald-500/15 text-emerald-700' : 'border-black/[0.08] bg-white text-muted-foreground hover:text-foreground'"
263
- :title="showExpression ? 'Back to the regular control (clears the expression)' : 'Map this value from a previous step instead'"
299
+ :title="showExpression ? t('integration.nodeConfig.expression.toggle.off') : t('integration.nodeConfig.expression.toggle.on')"
264
300
  @click="toggleExpression"
265
301
  >
266
302
  {{ exprHint }}
@@ -268,17 +304,12 @@ function setArrayItemField(index, fieldKey, v) {
268
304
  <span
269
305
  v-else-if="field.expressionAllowed"
270
306
  class="rounded border border-emerald-500/25 bg-emerald-500/10 px-1 py-0.5 font-mono text-[10px] text-emerald-600"
271
- title="Accepts ${{ … }} expressions"
307
+ :title="t('integration.nodeConfig.expression.badge.title')"
272
308
  >
273
309
  {{ exprHint }}
274
310
  </span>
275
311
  </div>
276
312
  </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
313
  <!-- object / plain-array fields: JSON editor (expressions stay usable
283
314
  inside string values). Wins over expressionAllowed — an object value
284
315
  in a text input would render as [object Object]. -->
@@ -291,11 +322,12 @@ function setArrayItemField(index, fieldKey, v) {
291
322
 
292
323
  <template v-else-if="isJsonField">
293
324
  <Textarea
325
+ :aria-label="resolveLocalized(field.label)"
294
326
  :model-value="jsonDraft"
295
327
  :rows="8"
296
328
  spellcheck="false"
297
- class="font-mono text-xs"
298
- :placeholder="labelText(field.placeholder) || '{ }'"
329
+ class="rounded-md border border-black/[0.08] ring-0 shadow-none bg-white font-mono text-xs"
330
+ :placeholder="resolveLocalized(field.placeholder) || '{ }'"
299
331
  @update:model-value="(v) => writeJson(String(v))"
300
332
  />
301
333
  <p v-if="jsonError" class="text-sm text-amber-600">{{ jsonError }}</p>
@@ -304,24 +336,29 @@ function setArrayItemField(index, fieldKey, v) {
304
336
  <!-- object field in key/value mode -->
305
337
  <div v-else-if="isObjectField && objectMode === 'form'" class="space-y-1.5">
306
338
  <div v-for="(row, idx) in kvRows" :key="idx" class="flex items-center gap-1.5">
339
+ <!-- A repeating row has no room for visible labels, so each control
340
+ names itself and says which row of which field it belongs to. -->
307
341
  <Input
342
+ :aria-label="t('integration.nodeConfig.object.row.keyAria', { label: resolveLocalized(field.label), n: idx + 1 })"
308
343
  :model-value="row.key"
309
- placeholder="key"
310
- class="h-7 w-[38%] bg-white font-mono text-sm"
344
+ :placeholder="t('integration.nodeConfig.object.row.keyPlaceholder')"
345
+ class="h-7 w-[38%] rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
311
346
  @update:model-value="(v) => setKvRow(idx, { key: String(v) })"
312
347
  />
313
348
  <Input
349
+ :aria-label="t('integration.nodeConfig.object.row.valueAria', { label: resolveLocalized(field.label), n: idx + 1 })"
314
350
  :model-value="row.value"
315
- placeholder="value"
316
- class="h-7 min-w-0 flex-1 bg-white text-sm"
351
+ :placeholder="t('integration.nodeConfig.object.row.valuePlaceholder')"
352
+ class="h-7 min-w-0 flex-1 rounded-md border border-black/[0.08] ring-0 shadow-none bg-white text-sm"
317
353
  @update:model-value="(v) => setKvRow(idx, { value: String(v) })"
318
354
  />
319
355
  <button
320
356
  type="button"
357
+ :aria-label="t('integration.nodeConfig.object.row.removeAria', { label: resolveLocalized(field.label), n: idx + 1 })"
321
358
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
322
359
  @click="removeKvRow(idx)"
323
360
  >
324
- <Trash2 :size="11" />
361
+ <TrashBin :size="11" />
325
362
  </button>
326
363
  </div>
327
364
  <Button
@@ -332,7 +369,7 @@ function setArrayItemField(index, fieldKey, v) {
332
369
  @click="addKvRow"
333
370
  >
334
371
  <Plus :size="11" />
335
- Add row
372
+ {{ t("integration.nodeConfig.action.addRow") }}
336
373
  </Button>
337
374
  </div>
338
375
 
@@ -340,9 +377,11 @@ function setArrayItemField(index, fieldKey, v) {
340
377
  <IntegrationsExpressionInput
341
378
  v-else-if="showExpression"
342
379
  :model-value="typeof value === 'string' ? value : ''"
343
- :placeholder="labelText(field.placeholder)"
380
+ :placeholder="resolveLocalized(field.placeholder)"
344
381
  :variables="variables"
345
382
  :upstream-outputs="upstreamOutputs"
383
+ :invalid="invalid"
384
+ :label="resolveLocalized(field.label)"
346
385
  @update:model-value="(v) => write(v)"
347
386
  />
348
387
 
@@ -363,18 +402,21 @@ function setArrayItemField(index, fieldKey, v) {
363
402
  <IntegrationsExpressionInput
364
403
  v-else-if="field.type === 'expression' || field.expressionAllowed"
365
404
  :model-value="value"
366
- :placeholder="labelText(field.placeholder)"
405
+ :placeholder="resolveLocalized(field.placeholder)"
367
406
  :variables="variables"
368
407
  :upstream-outputs="upstreamOutputs"
408
+ :invalid="invalid"
409
+ :label="resolveLocalized(field.label)"
369
410
  @update:model-value="(v) => write(v)"
370
411
  />
371
412
 
372
413
  <!-- plain string (no expressions) -->
373
414
  <Input
374
415
  v-else-if="field.type === 'string'"
416
+ :aria-label="resolveLocalized(field.label)"
375
417
  :model-value="value ?? ''"
376
- :placeholder="labelText(field.placeholder)"
377
- class="h-8 text-sm bg-white"
418
+ :placeholder="resolveLocalized(field.placeholder)"
419
+ class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
378
420
  :class="invalid ? 'border-rose-400 focus-visible:ring-rose-400/40' : ''"
379
421
  @update:model-value="(v) => write(v)"
380
422
  />
@@ -382,9 +424,10 @@ function setArrayItemField(index, fieldKey, v) {
382
424
  <!-- number -->
383
425
  <Input
384
426
  v-else-if="field.type === 'number'"
427
+ :aria-label="resolveLocalized(field.label)"
385
428
  type="number"
386
429
  :model-value="value ?? ''"
387
- class="h-8 text-sm bg-white"
430
+ class="h-8 rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white"
388
431
  :class="invalid ? 'border-rose-400 focus-visible:ring-rose-400/40' : ''"
389
432
  @update:model-value="(v) => write(v === '' ? null : Number(v))"
390
433
  />
@@ -392,11 +435,13 @@ function setArrayItemField(index, fieldKey, v) {
392
435
  <!-- boolean -->
393
436
  <div v-else-if="field.type === 'boolean'" class="flex items-center gap-2 pt-1">
394
437
  <Switch
438
+ :aria-label="resolveLocalized(field.label)"
439
+ class="data-[state=unchecked]:bg-slate-300"
395
440
  :model-value="Boolean(value)"
396
441
  @update:model-value="(v) => write(v)"
397
442
  />
398
443
  <span class="text-sm text-muted-foreground">
399
- {{ labelText(field.description) }}
444
+ {{ resolveLocalized(field.description) }}
400
445
  </span>
401
446
  </div>
402
447
 
@@ -419,18 +464,18 @@ function setArrayItemField(index, fieldKey, v) {
419
464
  :model-value="value ?? ''"
420
465
  @update:model-value="(v) => write(v)"
421
466
  >
422
- <SelectTrigger class="h-8 text-sm bg-white">
423
- <SelectValue :placeholder="labelText(field.placeholder) || 'Pick one'" />
467
+ <SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
468
+ <SelectValue :placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')" />
424
469
  </SelectTrigger>
425
470
  <SelectContent>
426
471
  <SelectItem v-for="opt in field.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
427
- {{ labelText(opt.label) }}
472
+ {{ resolveLocalized(opt.label) }}
428
473
  </SelectItem>
429
474
  </SelectContent>
430
475
  </Select>
431
476
 
432
477
  <!-- multiselect — pill toggles for now -->
433
- <div v-else-if="field.type === 'multiselect'" class="flex flex-wrap gap-1">
478
+ <div v-else-if="field.type === 'multiselect'" role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
434
479
  <button
435
480
  v-for="opt in field.options ?? []"
436
481
  :key="String(opt.value)"
@@ -443,7 +488,7 @@ function setArrayItemField(index, fieldKey, v) {
443
488
  write(has ? current.filter((v) => v !== opt.value) : [...current, opt.value]);
444
489
  }"
445
490
  >
446
- {{ labelText(opt.label) }}
491
+ {{ resolveLocalized(opt.label) }}
447
492
  </button>
448
493
  </div>
449
494
 
@@ -453,52 +498,90 @@ function setArrayItemField(index, fieldKey, v) {
453
498
  :model-value="value ?? ''"
454
499
  @update:model-value="(v) => write(v)"
455
500
  >
456
- <SelectTrigger class="h-8 text-sm bg-white">
457
- <SelectValue placeholder="Pick a secret" />
501
+ <SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
502
+ <SelectValue :placeholder="t('integration.nodeConfig.secretRef.placeholder')" />
458
503
  </SelectTrigger>
459
504
  <SelectContent>
460
505
  <SelectItem v-for="k in secretKeys" :key="k" :value="k">{{ k }}</SelectItem>
461
506
  </SelectContent>
462
507
  </Select>
463
508
 
464
- <!-- credentials-ref — pick a credential of the required type -->
509
+ <!-- credentials-ref — pick a credential of the required type. Compact:
510
+ creating and importing live INSIDE the picker as action rows; no
511
+ Manage link (it navigated out of the editor mid-edit). -->
465
512
  <template v-else-if="field.type === 'credentials-ref'">
466
513
  <Select
467
514
  :model-value="value ?? ''"
468
- @update:model-value="(v) => write(v)"
515
+ @update:model-value="(v) => onCredentialSelect(v)"
469
516
  >
470
- <SelectTrigger class="h-8 text-sm bg-white">
471
- <SelectValue placeholder="Pick a credential" />
517
+ <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">
518
+ <!-- The trigger renders the bound credential itself instead of letting
519
+ `SelectValue` echo the chosen row. Left to itself it shows that row's
520
+ `textContent`, which glues the name and the state pill into one word —
521
+ "Revenexx APIConnected". Rendering it here keeps the pill a pill, and
522
+ the placeholder has to be drawn by hand because a slot replaces the
523
+ fallback that would otherwise show it. -->
524
+ <SelectValue :placeholder="t('integration.nodeConfig.credential.placeholder')">
525
+ <span v-if="bound" class="flex min-w-0 items-center gap-1.5">
526
+ <span class="min-w-0 truncate">{{ bound.credential.name }}</span>
527
+ <!-- No `title` on this badge: `SelectValue` renders itself with
528
+ `pointer-events: none`, so nothing inside it is ever hovered and a
529
+ tooltip here would be a reason nobody can reach. A failed test's
530
+ reason is prose beneath the field instead. -->
531
+ <span
532
+ class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
533
+ :class="bound.state.toneClass"
534
+ >
535
+ {{ bound.state.label }}
536
+ </span>
537
+ </span>
538
+ <!-- A stored id the tenant's credentials no longer contain: the
539
+ credential was deleted, and the field would otherwise look untouched
540
+ while the workflow still runs into the dead reference. `loaded`
541
+ keeps the wait before the first response out of it. -->
542
+ <span v-else-if="value && credentialsLoaded" class="text-rose-600">{{ UNKNOWN_CREDENTIAL }}</span>
543
+ <span v-else class="text-muted-foreground">{{ t("integration.nodeConfig.credential.placeholder") }}</span>
544
+ </SelectValue>
472
545
  </SelectTrigger>
473
546
  <SelectContent>
474
- <SelectItem v-for="c in credsForField(field)" :key="c.id" :value="c.id">
547
+ <!-- The state comes from `credentialState`, the same derivation the
548
+ credentials table and the detail dialog use. This picker used to read
549
+ `status` alone and call everything that was not `needs_reauth`
550
+ "Connected" — including a credential nobody had ever tested, which is
551
+ a pass claimed for a test that never ran. -->
552
+ <SelectItem v-for="o in credOptions" :key="o.credential.id" :value="o.credential.id">
475
553
  <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>
554
+ <span class="min-w-0 truncate">{{ o.credential.name }}</span>
555
+ <!-- `title` carries why a failed test failed, the way the credentials
556
+ table does it. Without it "Failed" is a verdict with no appeal. A
557
+ row is a real hit target, unlike the closed trigger above. -->
558
+ <span
559
+ class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
560
+ :class="o.state.toneClass"
561
+ :title="o.state.message ?? void 0"
562
+ >{{ o.state.label }}</span>
479
563
  </span>
480
564
  </SelectItem>
565
+ <div v-if="!credOptions.length" class="px-2 py-1.5 text-sm text-muted-foreground">
566
+ {{ t("integration.nodeConfig.credential.empty") }}
567
+ </div>
568
+ <SelectSeparator />
569
+ <SelectItem :value="CRED_ACTION_NEW" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
570
+ {{ t("integration.nodeConfig.credential.action.new") }}
571
+ </SelectItem>
572
+ <SelectItem v-if="acceptsSftp(field)" :value="CRED_ACTION_SFTP" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
573
+ {{ t("integration.nodeConfig.credential.action.importSftp") }}
574
+ </SelectItem>
481
575
  </SelectContent>
482
576
  </Select>
483
- <p v-if="credNeedsReauth(value)" class="text-sm text-amber-600">
484
- This credential needs reconnect.
577
+ <!-- What the state has to add, as text rather than as a tooltip — the shape
578
+ the detail dialog uses beneath its own badge. -->
579
+ <p v-if="bound?.state.key === 'needs_reauth'" class="text-sm text-amber-600">
580
+ {{ t("integration.nodeConfig.credential.state.needsReauth") }}
581
+ </p>
582
+ <p v-else-if="bound?.state.key === 'failed' && bound.state.message" class="text-sm break-words text-rose-600">
583
+ {{ t("integration.nodeConfig.credential.state.failed", { message: bound.state.message }) }}
485
584
  </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>
502
585
  <IntegrationsCredentialCreateDialog
503
586
  v-model:open="credCreateOpen"
504
587
  :credential-type="field.credentialType"
@@ -524,10 +607,11 @@ function setArrayItemField(index, fieldKey, v) {
524
607
  </span>
525
608
  <button
526
609
  type="button"
610
+ :aria-label="t('integration.nodeConfig.array.removeItemAria', { label: resolveLocalized(field.label), n: idx + 1 })"
527
611
  class="flex size-5 items-center justify-center rounded text-muted-foreground hover:text-rose-600"
528
612
  @click="removeArrayItem(idx)"
529
613
  >
530
- <Trash2 :size="11" />
614
+ <TrashBin :size="11" />
531
615
  </button>
532
616
  </div>
533
617
  <div
@@ -536,11 +620,14 @@ function setArrayItemField(index, fieldKey, v) {
536
620
  class="space-y-1"
537
621
  >
538
622
  <Label class="text-mono text-muted-foreground">
539
- {{ labelText(sub.label) }}
623
+ {{ resolveLocalized(sub.label) }}
540
624
  </Label>
625
+ <!-- The label above repeats per item, so it cannot own an id; the
626
+ input carries its own name including the item number. -->
541
627
  <Input
628
+ :aria-label="t('integration.nodeConfig.array.subFieldAria', { label: resolveLocalized(sub.label), n: idx + 1 })"
542
629
  :model-value="item[sub.key] ?? ''"
543
- :placeholder="labelText(sub.placeholder)"
630
+ :placeholder="resolveLocalized(sub.placeholder)"
544
631
  class="h-7 text-sm bg-white"
545
632
  :class="sub.type === 'expression' ? 'font-mono' : ''"
546
633
  @update:model-value="(v) => setArrayItemField(idx, sub.key, v)"
@@ -555,16 +642,17 @@ function setArrayItemField(index, fieldKey, v) {
555
642
  @click="pushArrayItem(Object.fromEntries((field.items?.properties ?? []).map((p) => [p.key, ''])))"
556
643
  >
557
644
  <Plus :size="11" />
558
- Add row
645
+ {{ t("integration.nodeConfig.action.addRow") }}
559
646
  </Button>
560
647
  </div>
561
648
 
562
649
  <!-- fallback: JSON textarea for complex types -->
563
650
  <Textarea
564
651
  v-else
652
+ :aria-label="resolveLocalized(field.label)"
565
653
  :model-value="value === void 0 ? '' : JSON.stringify(value, null, 2)"
566
654
  rows="3"
567
- class="text-sm font-mono bg-white"
655
+ class="rounded-md border border-black/[0.08] ring-0 shadow-none text-sm font-mono bg-white"
568
656
  @update:model-value="(v) => {
569
657
  try {
570
658
  write(v ? JSON.parse(String(v)) : void 0);
@@ -573,12 +661,18 @@ function setArrayItemField(index, fieldKey, v) {
573
661
  }"
574
662
  />
575
663
 
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 }}
664
+ <!-- inline help BELOW the control, quiet — same pattern as the Settings
665
+ tab (boolean uses its description as the toggle label instead) -->
666
+ <p v-if="descriptionText && field.type !== 'boolean'" class="text-xs text-muted-foreground leading-snug">
667
+ {{ descriptionText }}
579
668
  </p>
580
- <p v-else-if="requiredMissing" class="text-sm text-amber-600">
581
- Required
669
+
670
+ <!-- validation feedback: the error (local format violation, else what the
671
+ last save refused), otherwise the constraint hint. A required field the
672
+ user has simply not reached yet says nothing beyond the * on its
673
+ label — the nagging would outnumber the help. -->
674
+ <p v-if="fieldError" class="text-sm text-rose-600">
675
+ {{ fieldError }}
582
676
  </p>
583
677
  <p v-else-if="constraintHint" class="text-sm text-muted-foreground">
584
678
  {{ 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;