@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
@@ -12,9 +12,12 @@ const props = defineProps({
12
12
  modelValue: { type: null, required: true },
13
13
  placeholder: { type: String, required: false },
14
14
  variables: { type: Object, required: false },
15
- upstreamOutputs: { type: Array, required: false }
15
+ upstreamOutputs: { type: Array, required: false },
16
+ invalid: { type: Boolean, required: false },
17
+ label: { type: String, required: false }
16
18
  });
17
19
  const emit = defineEmits(["update:modelValue"]);
20
+ const t = useIntegrationsT();
18
21
  const text = computed(() => String(props.modelValue ?? ""));
19
22
  const OPEN = "${{ ";
20
23
  const CLOSE = " }}";
@@ -32,12 +35,12 @@ function pillLabel(expr) {
32
35
  }
33
36
  if (path[0] === "trigger" && path[1] === "payload") {
34
37
  const rest = path.slice(2).join(".");
35
- return rest ? "Trigger \xB7 " + rest : "Trigger payload";
38
+ return rest ? t("integration.nodeConfig.expression.pill.triggerPath", { rest }) : t("integration.nodeConfig.expression.pill.triggerPayload");
36
39
  }
37
- if (path[0] === "vars") return "Variable \xB7 " + path.slice(1).join(".");
40
+ if (path[0] === "vars") return t("integration.nodeConfig.expression.pill.variable", { path: path.slice(1).join(".") });
38
41
  if (path[0] === "in") {
39
42
  const rest = path.slice(1).join(".");
40
- return rest ? "Input \xB7 " + rest : "Input payload";
43
+ return rest ? t("integration.nodeConfig.expression.pill.inputPath", { rest }) : t("integration.nodeConfig.expression.pill.inputPayload");
41
44
  }
42
45
  return expr;
43
46
  }
@@ -78,7 +81,11 @@ const editor = new Editor({
78
81
  content: textToDoc(text.value),
79
82
  editorProps: {
80
83
  // Single line: swallow Enter (paragraph splits would break the field).
81
- handleKeyDown: (_view, event) => event.key === "Enter" && !event.isComposing
84
+ handleKeyDown: (_view, event) => event.key === "Enter" && !event.isComposing,
85
+ // A contenteditable region is not a text box to assistive technology
86
+ // unless it says so, and its label sits in a sibling element — so it would
87
+ // otherwise be an anonymous blob to a screen reader.
88
+ attributes: { role: "textbox", ...props.label ? { "aria-label": props.label } : {} }
82
89
  },
83
90
  onUpdate({ editor: ed }) {
84
91
  const json = ed.getJSON();
@@ -98,9 +105,9 @@ onBeforeUnmount(() => {
98
105
  if (insertHandler?.value === insertToken) insertHandler.value = null;
99
106
  editor.destroy();
100
107
  });
101
- watch(text, (t) => {
102
- if (docToText(editor.getJSON()) !== t) {
103
- editor.commands.setContent(textToDoc(t));
108
+ watch(text, (t2) => {
109
+ if (docToText(editor.getJSON()) !== t2) {
110
+ editor.commands.setContent(textToDoc(t2));
104
111
  }
105
112
  });
106
113
  function insertToken(expr) {
@@ -120,7 +127,7 @@ const sections = computed(() => {
120
127
  label: key,
121
128
  expr: tok("vars." + key)
122
129
  }));
123
- if (varItems.length) out.push({ heading: "VARIABLES", items: varItems });
130
+ if (varItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.variables"), items: varItems });
124
131
  const upstream = props.upstreamOutputs ?? [];
125
132
  const nodeItems = upstream.flatMap((u) => {
126
133
  const base = "nodes." + u.nodeId + ".outputs." + u.output;
@@ -133,16 +140,22 @@ const sections = computed(() => {
133
140
  }
134
141
  return rows;
135
142
  });
136
- if (nodeItems.length) out.push({ heading: "NODE OUTPUTS", items: nodeItems });
143
+ if (nodeItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.nodeOutputs"), items: nodeItems });
137
144
  const primary = upstream[0];
138
- const inItems = [{ label: "input payload", expr: tok("in") }];
145
+ const inItems = [{ label: t("integration.nodeConfig.expression.suggestion.inputPayload"), expr: tok("in") }];
139
146
  for (const f of primary?.fields ?? []) {
140
147
  inItems.push({ label: "input." + f, expr: tok("in." + f) });
141
148
  }
142
- out.push({ heading: "INPUT", items: inItems });
149
+ out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.input"), items: inItems });
143
150
  return out;
144
151
  });
145
152
  const hasSuggestions = computed(() => sections.value.some((s) => s.items.length > 0));
153
+ const query = ref("");
154
+ const filteredSections = computed(() => {
155
+ const q = query.value.trim().toLowerCase();
156
+ if (!q) return sections.value;
157
+ return sections.value.map((s) => ({ ...s, items: s.items.filter((i) => i.label.toLowerCase().includes(q) || i.expr.toLowerCase().includes(q)) })).filter((s) => s.items.length > 0);
158
+ });
146
159
  const open = ref(false);
147
160
  const wrapRef = ref(null);
148
161
  function toggle() {
@@ -163,6 +176,9 @@ function onKeydown(e) {
163
176
  function onDocMousedown(e) {
164
177
  if (wrapRef.value && !wrapRef.value.contains(e.target)) open.value = false;
165
178
  }
179
+ watch(open, (isOpen) => {
180
+ if (!isOpen) query.value = "";
181
+ });
166
182
  onMounted(() => document.addEventListener("mousedown", onDocMousedown));
167
183
  onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown));
168
184
  function insert(expr) {
@@ -174,7 +190,10 @@ function insert(expr) {
174
190
  <template>
175
191
  <div ref="wrapRef" class="relative" @focusout="onFocusOut">
176
192
  <div class="flex items-center gap-1">
177
- <div class="expr-editor relative h-8 min-w-0 flex-1 overflow-hidden rounded-md border border-black/[0.08] bg-white px-2 font-mono text-sm transition focus-within:border-black/[0.16]">
193
+ <div
194
+ class="expr-editor relative h-8 min-w-0 flex-1 overflow-hidden rounded-md border bg-white px-2 text-sm transition focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring"
195
+ :class="invalid ? 'border-rose-400' : 'border-black/[0.08]'"
196
+ >
178
197
  <span
179
198
  v-if="!text"
180
199
  class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 truncate text-muted-foreground/60"
@@ -183,7 +202,7 @@ function insert(expr) {
183
202
  </div>
184
203
  <button
185
204
  type="button"
186
- title="Insert expression"
205
+ :title="t('integration.nodeConfig.expression.insertTitle')"
187
206
  class="flex h-8 shrink-0 items-center rounded-md border border-black/[0.08] bg-white px-1.5 font-mono text-sm text-muted-foreground transition hover:text-foreground hover:bg-muted/30"
188
207
  :class="open ? 'ring-2 ring-emerald-500/40' : ''"
189
208
  @mousedown.prevent="toggle"
@@ -195,7 +214,7 @@ function insert(expr) {
195
214
  <p
196
215
  v-if="preview"
197
216
  class="mt-1 flex min-w-0 items-baseline gap-1 text-[11px]"
198
- :title="preview.misses ? 'Parts of this expression have no sample data yet' : preview.resolved"
217
+ :title="preview.misses ? t('integration.nodeConfig.expression.preview.missTitle') : preview.resolved"
199
218
  >
200
219
  <span class="shrink-0 text-muted-foreground">→</span>
201
220
  <span class="min-w-0 truncate font-mono" :class="preview.misses ? 'text-amber-600' : 'text-emerald-700'">{{ preview.resolved }}</span>
@@ -203,34 +222,45 @@ function insert(expr) {
203
222
 
204
223
  <div
205
224
  v-if="open && hasSuggestions"
206
- class="absolute right-0 top-full z-50 mt-1 max-h-64 w-72 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 text-sm shadow-lg"
225
+ class="absolute right-0 top-full z-50 mt-1 flex max-h-72 w-80 flex-col rounded-md border border-black/[0.08] bg-white text-sm shadow-lg"
207
226
  >
208
- <template v-for="section in sections" :key="section.heading">
209
- <div
210
- v-if="section.items.length"
211
- class="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"
212
- >
213
- {{ section.heading }}
214
- </div>
215
- <button
216
- v-for="item in section.items"
217
- :key="item.expr"
218
- type="button"
219
- class="block w-full px-2 py-1 text-left transition hover:bg-muted/50"
220
- @mousedown.prevent="insert(item.expr)"
227
+ <div class="shrink-0 border-b border-black/[0.06] p-1">
228
+ <input
229
+ v-model="query"
230
+ :aria-label="t('integration.nodeConfig.expression.picker.filterAria')"
231
+ :placeholder="t('integration.nodeConfig.expression.picker.filterPlaceholder')"
232
+ class="w-full rounded px-1.5 py-1 text-sm outline-none placeholder:text-muted-foreground/60 focus:ring-2 focus:ring-primary/40"
233
+ @mousedown.stop
221
234
  >
222
- <div class="truncate text-muted-foreground">
223
- {{ item.label }}
224
- </div>
225
- <div class="truncate font-mono text-emerald-700">
226
- {{ item.expr }}
235
+ </div>
236
+ <div class="min-h-0 flex-1 overflow-y-auto py-1">
237
+ <p v-if="!filteredSections.length" class="px-2 py-1.5 text-muted-foreground">
238
+ {{ t("integration.nodeConfig.expression.picker.empty", { query }) }}
239
+ </p>
240
+ <template v-for="section in filteredSections" :key="section.heading">
241
+ <div class="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
242
+ {{ section.heading }}
227
243
  </div>
228
- </button>
229
- </template>
244
+ <button
245
+ v-for="item in section.items"
246
+ :key="item.expr"
247
+ type="button"
248
+ class="block w-full px-2 py-1 text-left transition hover:bg-muted/50"
249
+ @mousedown.prevent="insert(item.expr)"
250
+ >
251
+ <div class="truncate text-muted-foreground">
252
+ {{ item.label }}
253
+ </div>
254
+ <div class="truncate font-mono text-emerald-700">
255
+ {{ item.expr }}
256
+ </div>
257
+ </button>
258
+ </template>
259
+ </div>
230
260
  </div>
231
261
  </div>
232
262
  </template>
233
263
 
234
264
  <style>
235
- .expr-editor .ProseMirror{align-items:center;display:flex;height:100%;outline:none;overflow-x:auto;scrollbar-width:none}.expr-editor .ProseMirror p{line-height:30px;margin:0;white-space:pre}.expr-pill{background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:6px;color:#047857;display:inline-block;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;line-height:18px;margin:0 1px;padding:0 6px;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:baseline;white-space:nowrap}.expr-pill.ProseMirror-selectednode{outline:2px solid rgba(16,185,129,.5)}
265
+ .expr-editor .ProseMirror{align-items:center;display:flex;height:100%;outline:none;overflow-x:auto;scrollbar-width:none}.expr-editor .ProseMirror p{line-height:30px;margin:0;white-space:pre}.expr-pill{align-items:center;background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:6px;color:#047857;display:inline-flex;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;height:20px;line-height:1;margin:0 2px;padding:0 7px;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.expr-pill.ProseMirror-selectednode{outline:2px solid rgba(16,185,129,.5)}
236
266
  </style>
@@ -10,6 +10,10 @@ type __VLS_Props = {
10
10
  placeholder?: string;
11
11
  variables?: Record<string, WorkflowVariable>;
12
12
  upstreamOutputs?: UpstreamOutput[];
13
+ /** Draw the editor as at fault — the field above owns the reason. */
14
+ invalid?: boolean;
15
+ /** The setting's label, so the editable region has an accessible name. */
16
+ label?: string;
13
17
  };
14
18
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
19
  "update:modelValue": (value: string) => any;
@@ -2,33 +2,37 @@
2
2
  import { computed, nextTick, ref } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
- import { ChevronsDownUp, ChevronsUpDown, Trash2, Ungroup } from "lucide-vue-next";
5
+ import { ChevronsDownUp, ChevronsUpDown, Ungroup } from "lucide-vue-next";
6
+ import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
7
+ import { resolveDisplayName } from "../../utils/localize";
6
8
  const props = defineProps({
7
9
  id: { type: String, required: true },
8
10
  data: { type: Object, required: true },
9
11
  selected: { type: Boolean, required: false }
10
12
  });
11
13
  defineEmits(["updateNodeInternals"]);
14
+ const t = useIntegrationsT();
12
15
  const GROUP_COLORS = [
13
- { bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
14
- { bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
15
- { bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
16
- { bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
17
- { bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
18
- { bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
19
- { bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
16
+ { nameKey: "integration.canvas.color.amber", bg: "rgba(254,243,199,0.45)", border: "#fcd34d", chip: "#f59e0b" },
17
+ { nameKey: "integration.canvas.color.orange", bg: "rgba(255,237,213,0.45)", border: "#fdba74", chip: "#f97316" },
18
+ { nameKey: "integration.canvas.color.rose", bg: "rgba(255,228,230,0.45)", border: "#fda4af", chip: "#f43f5e" },
19
+ { nameKey: "integration.canvas.color.green", bg: "rgba(220,252,231,0.45)", border: "#86efac", chip: "#10b981" },
20
+ { nameKey: "integration.canvas.color.blue", bg: "rgba(219,234,254,0.45)", border: "#93c5fd", chip: "#3b82f6" },
21
+ { nameKey: "integration.canvas.color.violet", bg: "rgba(237,233,254,0.45)", border: "#c4b5fd", chip: "#8b5cf6" },
22
+ { nameKey: "integration.canvas.color.gray", bg: "rgba(245,245,244,0.6)", border: "#d6d3d1", chip: "#78716c" }
20
23
  ];
21
24
  const color = computed(() => GROUP_COLORS[props.data.group.color ?? 4] ?? GROUP_COLORS[4]);
22
- function loc(v) {
23
- if (!v) return "";
24
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
25
- }
26
- const label = computed(() => loc(props.data.group.label) || "Group");
25
+ const label = computed(() => resolveDisplayName(props.data.group.label) || t("integration.canvas.group.defaultLabel"));
27
26
  const collapsed = computed(() => props.data.group.collapsed === true);
28
27
  const editable = computed(() => !!props.data.actions);
29
28
  const editing = ref(false);
30
29
  const draft = ref("");
31
30
  const inputEl = ref(null);
31
+ function onLabelDoubleClick(event) {
32
+ if (!editable.value) return;
33
+ event.stopPropagation();
34
+ startRename();
35
+ }
32
36
  function startRename() {
33
37
  if (!editable.value) return;
34
38
  draft.value = label.value;
@@ -54,7 +58,7 @@ function onHoverLeave() {
54
58
  }, 250);
55
59
  }
56
60
  const toolbarVisible = computed(() => editable.value && !editing.value && (hovered.value || !!props.selected));
57
- const memberCountText = computed(() => `${props.data.memberCount} node${props.data.memberCount === 1 ? "" : "s"}`);
61
+ const memberCountText = computed(() => t("integration.canvas.group.memberCount", props.data.memberCount));
58
62
  const flow = useVueFlow();
59
63
  const resizing = ref(false);
60
64
  const liveW = ref(0);
@@ -99,13 +103,15 @@ function onResizeEnd() {
99
103
  class="size-4.5 rounded-full border transition hover:scale-110"
100
104
  :class="(data.group.color ?? 4) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
101
105
  :style="{ backgroundColor: c.chip, borderColor: c.border, width: '16px', height: '16px' }"
106
+ :title="t(c.nameKey)"
107
+ :aria-label="t(c.nameKey)"
102
108
  @click="data.actions?.setColor(i)"
103
109
  />
104
110
  <div class="mx-0.5 h-4 w-px bg-black/10" />
105
111
  <button
106
112
  type="button"
107
113
  class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
108
- :title="collapsed ? 'Expand group' : 'Collapse group'"
114
+ :title="collapsed ? t('integration.canvas.group.action.expand') : t('integration.canvas.group.action.collapse')"
109
115
  @click="data.actions?.toggleCollapse()"
110
116
  >
111
117
  <component :is="collapsed ? ChevronsUpDown : ChevronsDownUp" :size="13" />
@@ -113,7 +119,7 @@ function onResizeEnd() {
113
119
  <button
114
120
  type="button"
115
121
  class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
116
- title="Ungroup (keep the nodes)"
122
+ :title="t('integration.canvas.group.action.ungroup')"
117
123
  @click="data.actions?.ungroup()"
118
124
  >
119
125
  <Ungroup :size="13" />
@@ -121,10 +127,10 @@ function onResizeEnd() {
121
127
  <button
122
128
  type="button"
123
129
  class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
124
- title="Remove group frame"
130
+ :title="t('integration.canvas.group.action.removeFrame')"
125
131
  @click="data.actions?.ungroup()"
126
132
  >
127
- <Trash2 :size="13" />
133
+ <TrashBin :size="13" />
128
134
  </button>
129
135
  </div>
130
136
  </NodeToolbar>
@@ -137,6 +143,9 @@ function onResizeEnd() {
137
143
  :position="Position.Top"
138
144
  class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm"
139
145
  />
146
+ <!-- The collapsed card stops its own double-click unconditionally, which is
147
+ safe: both read-only panels build every frame with `collapsed: false`, so
148
+ this surface is the editor's alone. -->
140
149
  <div
141
150
  class="w-[220px] rounded-lg border-2 bg-white transition-all duration-150"
142
151
  :style="{ borderColor: color.border }"
@@ -184,6 +193,7 @@ function onResizeEnd() {
184
193
  v-if="editing"
185
194
  ref="inputEl"
186
195
  v-model="draft"
196
+ :aria-label="t('integration.canvas.group.input.nameAria')"
187
197
  class="nodrag w-40 rounded border border-black/[0.1] bg-white px-1 py-0.5 text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/40"
188
198
  @blur="commitRename"
189
199
  @keydown.enter.prevent="commitRename"
@@ -194,7 +204,7 @@ function onResizeEnd() {
194
204
  v-else
195
205
  class="select-none text-sm font-semibold"
196
206
  :style="{ color: color.chip }"
197
- @dblclick.stop="startRename"
207
+ @dblclick="onLabelDoubleClick"
198
208
  >
199
209
  {{ label }}
200
210
  </span>
@@ -206,7 +216,7 @@ function onResizeEnd() {
206
216
  v-if="editable"
207
217
  class="nodrag pointer-events-auto absolute bottom-0 right-0 flex size-5 cursor-se-resize items-end justify-end p-1"
208
218
  :style="{ color: color.chip }"
209
- title="Resize group"
219
+ :title="t('integration.canvas.group.action.resize')"
210
220
  @pointerdown="onResizeStart"
211
221
  @pointermove="onResizeMove"
212
222
  @pointerup="onResizeEnd"
@@ -1,20 +1,64 @@
1
1
  <script setup>
2
+ import { nextTick, onMounted, ref } from "vue";
2
3
  import Sidebar from "./Sidebar.vue";
4
+ const t = useIntegrationsT();
3
5
  const { focused } = useIntegrationsFocus();
6
+ const animate = ref(false);
7
+ onMounted(() => nextTick(() => {
8
+ animate.value = true;
9
+ }));
4
10
  </script>
5
11
 
6
12
  <template>
7
13
  <div class="flex h-[calc(100%-0.75rem)] mb-3 mr-3 ml-1 overflow-hidden rounded-lg border border-gray-400/10 outline-1 outline-white/30">
14
+ <!-- First thing in the tab order: jumps past the sidebar into the page. -->
15
+ <a
16
+ href="#integrations-main"
17
+ class="sr-only focus:not-sr-only focus:absolute focus:left-3 focus:top-3 focus:z-50 focus:rounded-md focus:bg-white focus:px-3 focus:py-1.5 focus:text-sm focus:font-medium focus:ring-2 focus:ring-primary/40"
18
+ >
19
+ {{ t("integration.shell.action.skipToContent") }}
20
+ </a>
21
+ <!-- `inert` + `aria-hidden` are what make the fold real. The inner wrapper
22
+ below keeps its own `w-64` so the slide-out animates, so a folded rail
23
+ is merely clipped by `overflow-hidden`: its links keep their size and
24
+ stay focusable at a negative x, and a keyboard walks into a rail nobody
25
+ can see (PO-273). `display: none` would fix that too and take the
26
+ animation with it.
27
+
28
+ Both attributes are written to be *absent* rather than false when the
29
+ rail stands: `inert` is presence-based, so a renderer that emitted
30
+ `inert="false"` as an attribute would hold the standing rail back
31
+ instead. That the standing rail really does take focus is asserted in
32
+ its own right — specs/studio-navigation.md AC-1 is the control for AC-5.
33
+
34
+ The id is how a test reaches the rail while it is folded: out of the
35
+ accessibility tree, it can no longer be addressed by its role, and
36
+ `nav` alone would catch any navigation the page grows later. -->
8
37
  <div
9
- class="shrink-0 overflow-hidden transition-[width] duration-200 ease-out border-r border-black/[0.06] bg-white"
10
- :class="focused ? 'w-0 !border-r-0' : 'w-64'"
38
+ id="integrations-nav"
39
+ class="shrink-0 overflow-hidden border-r border-black/[0.06] bg-white"
40
+ :class="[focused ? 'w-0 !border-r-0' : 'w-64', animate ? 'transition-[width] duration-200 ease-out' : '']"
41
+ :inert="focused || void 0"
42
+ :aria-hidden="focused ? 'true' : void 0"
11
43
  >
12
44
  <div class="h-full w-64 overflow-y-auto">
13
45
  <Sidebar />
14
46
  </div>
15
47
  </div>
16
- <div class="flex-1 min-w-0 overflow-y-auto bg-white/40 backdrop-blur-sm">
48
+ <!-- `tabindex="-1"` is what makes the skip link actually skip: a <main> is
49
+ not focusable on its own, so the browser would move the fragment but
50
+ leave focus on the link — and the next Tab would walk into the sidebar
51
+ the user just asked to be past. It stays out of the tab order (-1, not
52
+ 0); only the fragment and a script can put focus here — which is also
53
+ why it draws no focus ring: a ring around the whole page would be noise
54
+ on a region nobody tabs to, and the promise of a visible mark is about
55
+ the fields, buttons and links inside it. -->
56
+ <main
57
+ id="integrations-main"
58
+ tabindex="-1"
59
+ class="flex-1 min-w-0 overflow-y-auto bg-white/40 backdrop-blur-sm outline-none"
60
+ >
17
61
  <slot />
18
- </div>
62
+ </main>
19
63
  </div>
20
64
  </template>
@@ -2,6 +2,7 @@
2
2
  import { computed, ref } from "vue";
3
3
  import { ChevronDown } from "lucide-vue-next";
4
4
  import { Input } from "@revenexx/studio";
5
+ import { resolveLocalized } from "../../utils/localize";
5
6
  const props = defineProps({
6
7
  slug: { type: String, required: true },
7
8
  version: { type: String, required: true },
@@ -10,10 +11,7 @@ const props = defineProps({
10
11
  modelValue: { type: null, required: true }
11
12
  });
12
13
  const emit = defineEmits(["update:modelValue"]);
13
- function labelText(v) {
14
- if (!v) return "";
15
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
16
- }
14
+ const t = useIntegrationsT();
17
15
  const search = ref("");
18
16
  const focused = ref(false);
19
17
  const q = useNodeConfigResolve({
@@ -33,14 +31,14 @@ const currentKnown = computed(
33
31
  );
34
32
  const currentLabel = computed(() => {
35
33
  const hit = options.value.find((o) => String(o.value) === currentValue.value);
36
- return hit ? labelText(hit.label) || String(hit.value) : currentValue.value;
34
+ return hit ? resolveLocalized(hit.label) || String(hit.value) : currentValue.value;
37
35
  });
38
36
  const listOpen = computed(() => focused.value && !q.gated.value);
39
37
  const visibleOptions = computed(() => {
40
38
  const term = search.value.trim().toLowerCase();
41
39
  if (!focused.value || !term) return options.value;
42
40
  const hits = options.value.filter(
43
- (o) => (labelText(o.label) || "").toLowerCase().includes(term) || String(o.value).toLowerCase().includes(term)
41
+ (o) => (resolveLocalized(o.label) || "").toLowerCase().includes(term) || String(o.value).toLowerCase().includes(term)
44
42
  );
45
43
  return hits.length ? hits : options.value;
46
44
  });
@@ -66,7 +64,7 @@ const visibleMultiOptions = computed(() => {
66
64
  const f = multiFilter.value.trim().toLowerCase();
67
65
  if (!f) return options.value;
68
66
  return options.value.filter(
69
- (o) => (labelText(o.label) || String(o.value)).toLowerCase().includes(f) || String(o.value).toLowerCase().includes(f)
67
+ (o) => (resolveLocalized(o.label) || String(o.value)).toLowerCase().includes(f) || String(o.value).toLowerCase().includes(f)
70
68
  );
71
69
  });
72
70
  function toggleMulti(optValue) {
@@ -81,25 +79,25 @@ function toggleMulti(optValue) {
81
79
  <!-- gated: required dependencies still unset -->
82
80
  <template v-if="q.gated.value">
83
81
  <div :class="disabledControlClass">—</div>
84
- <p class="mt-1 text-sm text-amber-600">Set {{ q.unsatisfied.value.join(", ") }} first</p>
82
+ <p class="mt-1 text-sm text-amber-600">{{ t("integration.nodeConfig.dynamicField.gated", { keys: q.unsatisfied.value.join(", ") }) }}</p>
85
83
  </template>
86
84
 
87
85
  <!-- first load: no data yet -->
88
86
  <template v-else-if="q.isFetching.value && options.length === 0">
89
- <div :class="disabledControlClass">resolving options…</div>
87
+ <div :class="disabledControlClass">{{ t("integration.nodeConfig.dynamicField.resolving") }}</div>
90
88
  </template>
91
89
 
92
90
  <!-- error: build/runtime/etc -->
93
91
  <template v-else-if="q.error.value">
94
92
  <div :class="disabledControlClass">—</div>
95
93
  <div class="mt-1 flex items-center gap-2 text-sm text-amber-600">
96
- <span>{{ q.error.value.kind === "building" ? "Node bundle still building." : q.error.value.message }}</span>
94
+ <span>{{ q.error.value.kind === "building" ? t("integration.nodeConfig.resolve.error.building") : q.error.value.message }}</span>
97
95
  <button
98
96
  type="button"
99
97
  class="rounded border border-black/[0.08] px-1.5 py-0.5 text-mono text-muted-foreground transition hover:text-foreground"
100
98
  @click="q.refetch()"
101
99
  >
102
- RETRY
100
+ {{ t("integration.nodeConfig.resolve.action.retry") }}
103
101
  </button>
104
102
  </div>
105
103
  </template>
@@ -109,12 +107,13 @@ function toggleMulti(optValue) {
109
107
  <Input
110
108
  v-if="options.length > 10"
111
109
  v-model="multiFilter"
112
- placeholder="Filter options"
110
+ :aria-label="t('integration.nodeConfig.dynamicField.multiselect.filterAria', { label: resolveLocalized(field.label) })"
111
+ :placeholder="t('integration.nodeConfig.dynamicField.multiselect.filterPlaceholder')"
113
112
  class="mb-1.5 h-7 bg-white text-sm"
114
113
  />
115
- <div class="flex flex-wrap gap-1">
114
+ <div role="group" :aria-label="resolveLocalized(field.label)" class="flex flex-wrap gap-1">
116
115
  <span v-if="visibleMultiOptions.length === 0" class="text-sm text-muted-foreground">
117
- {{ options.length === 0 ? "no options available" : "no options match the filter" }}
116
+ {{ options.length === 0 ? t("integration.nodeConfig.dynamicField.empty") : t("integration.nodeConfig.dynamicField.emptyFiltered") }}
118
117
  </span>
119
118
  <button
120
119
  v-for="opt in visibleMultiOptions"
@@ -125,7 +124,7 @@ function toggleMulti(optValue) {
125
124
  :class="selectedArray.includes(String(opt.value)) ? 'bg-emerald-500/15 text-emerald-700 border-emerald-500/30' : 'bg-white text-muted-foreground hover:text-foreground'"
126
125
  @click="toggleMulti(String(opt.value))"
127
126
  >
128
- {{ labelText(opt.label) || String(opt.value) }}
127
+ {{ resolveLocalized(opt.label) || String(opt.value) }}
129
128
  </button>
130
129
  <!-- stored values no longer offered — shown read-only so they stay visible -->
131
130
  <span
@@ -133,10 +132,10 @@ function toggleMulti(optValue) {
133
132
  :key="m"
134
133
  class="rounded-md border border-black/[0.08] bg-white px-1.5 py-0.5 text-sm text-muted-foreground opacity-60"
135
134
  >
136
- {{ m }} (not in list)
135
+ {{ t("integration.nodeConfig.dynamicField.notInList", { value: m }) }}
137
136
  </span>
138
137
  </div>
139
- <p v-if="q.isFetching.value && q.hasData.value" class="mt-1 text-sm text-muted-foreground">reloading…</p>
138
+ <p v-if="q.isFetching.value && q.hasData.value" class="mt-1 text-sm text-muted-foreground">{{ t("integration.nodeConfig.dynamicField.reloading") }}</p>
140
139
  </template>
141
140
 
142
141
  <!-- single select: searchable combobox; typing re-resolves with the term -->
@@ -144,7 +143,8 @@ function toggleMulti(optValue) {
144
143
  <div class="relative">
145
144
  <Input
146
145
  :model-value="focused ? search : currentLabel"
147
- :placeholder="labelText(field.placeholder) || 'Pick one'"
146
+ :aria-label="resolveLocalized(field.label)"
147
+ :placeholder="resolveLocalized(field.placeholder) || t('integration.nodeConfig.select.placeholder')"
148
148
  class="h-8 bg-white pr-7 text-sm"
149
149
  @update:model-value="(v) => {
150
150
  search = String(v);
@@ -158,7 +158,7 @@ function toggleMulti(optValue) {
158
158
  class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
159
159
  >
160
160
  <div v-if="q.isFetching.value" class="px-2.5 py-1.5 text-sm text-muted-foreground">
161
- {{ search ? "searching\u2026" : "resolving options\u2026" }}
161
+ {{ search ? t("integration.nodeConfig.dynamicField.searching") : t("integration.nodeConfig.dynamicField.resolving") }}
162
162
  </div>
163
163
  <button
164
164
  v-for="opt in visibleOptions"
@@ -168,17 +168,17 @@ function toggleMulti(optValue) {
168
168
  :class="String(opt.value) === currentValue ? 'bg-emerald-500/10 text-emerald-800' : ''"
169
169
  @mousedown.prevent="pick(String(opt.value))"
170
170
  >
171
- <span class="min-w-0 truncate">{{ labelText(opt.label) || String(opt.value) }}</span>
171
+ <span class="min-w-0 truncate">{{ resolveLocalized(opt.label) || String(opt.value) }}</span>
172
172
  </button>
173
173
  <div v-if="!q.isFetching.value && visibleOptions.length === 0" class="px-2.5 py-1.5 text-sm text-muted-foreground">
174
- {{ search ? 'nothing matches "' + search + '"' : "no options available" }}
174
+ {{ search ? t("integration.nodeConfig.dynamicField.noMatch", { query: search }) : t("integration.nodeConfig.dynamicField.empty") }}
175
175
  </div>
176
176
  </div>
177
177
  </div>
178
178
  <p v-if="currentValue !== '' && !currentKnown" class="mt-1 text-sm text-amber-600">
179
- current value not in resolved options
179
+ {{ t("integration.nodeConfig.dynamicField.currentNotResolved") }}
180
180
  </p>
181
- <p v-if="q.isFetching.value && q.hasData.value && !focused" class="mt-1 text-sm text-muted-foreground">reloading…</p>
181
+ <p v-if="q.isFetching.value && q.hasData.value && !focused" class="mt-1 text-sm text-muted-foreground">{{ t("integration.nodeConfig.dynamicField.reloading") }}</p>
182
182
  </template>
183
183
  </div>
184
184
  </template>