@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
@@ -2,23 +2,18 @@
2
2
  import { computed, ref, watch } from "vue";
3
3
  import * as Lucide from "lucide-vue-next";
4
4
  import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
5
+ import { nodeIcon } from "../../data/nodeIcons";
6
+ import { resolveLocalized } from "../../utils/localize";
5
7
  const { load: loadNodes } = useIntegrationsNodes();
6
8
  onMounted(() => {
7
9
  void loadNodes();
8
10
  });
11
+ const t = useIntegrationsT();
9
12
  const emit = defineEmits(["insert"]);
10
13
  const search = ref("");
11
14
  const searchInput = ref(null);
12
15
  defineExpose({ focusSearch: () => searchInput.value?.focus() });
13
- function loc(value) {
14
- if (!value) return "";
15
- return typeof value === "string" ? value : value.en ?? Object.values(value)[0] ?? "";
16
- }
17
- function iconFor(name) {
18
- if (!name) return Lucide.Box;
19
- const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
20
- return Lucide[cleaned] || Lucide.Box;
21
- }
16
+ const iconFor = nodeIcon;
22
17
  const CORE_GROUP_BY_CATEGORY = {
23
18
  control: "Flow",
24
19
  transform: "Data",
@@ -61,7 +56,7 @@ function hasLogo(m) {
61
56
  }
62
57
  function groupPath(m) {
63
58
  if (m.groups?.length) {
64
- const path = m.groups.map((g) => loc(g)).filter(Boolean);
59
+ const path = m.groups.map((g) => resolveLocalized(g)).filter(Boolean);
65
60
  if (isIntegration(m) || !path.length) return path;
66
61
  return [CORE_LABEL_ALIAS[path[0]] ?? path[0], ...path.slice(1)];
67
62
  }
@@ -91,6 +86,31 @@ const latestManifests = computed(() => {
91
86
  }
92
87
  return out;
93
88
  });
89
+ const versionsBySlug = computed(() => {
90
+ const map = /* @__PURE__ */ new Map();
91
+ for (const m of INTEGRATION_NODE_CATALOG) {
92
+ const list = map.get(m.slug);
93
+ if (list) list.push(m);
94
+ else map.set(m.slug, [m]);
95
+ }
96
+ return map;
97
+ });
98
+ function versionsOf(slug) {
99
+ return versionsBySlug.value.get(slug) ?? [];
100
+ }
101
+ const selectedVersion = ref({});
102
+ function setSelectedVersion(slug, version) {
103
+ selectedVersion.value = { ...selectedVersion.value, [slug]: version };
104
+ }
105
+ function effVersion(m) {
106
+ const sel = selectedVersion.value[m.slug];
107
+ return sel && versionsOf(m.slug).some((v) => v.version === sel) ? sel : m.version;
108
+ }
109
+ function effManifest(m) {
110
+ const v = effVersion(m);
111
+ if (v === m.version) return m;
112
+ return versionsOf(m.slug).find((x) => x.version === v) ?? m;
113
+ }
94
114
  const tree = computed(() => {
95
115
  const apps = makeGroup("Integrations");
96
116
  const core = makeGroup("Core");
@@ -141,8 +161,8 @@ const currentGroup = computed(() => {
141
161
  return cursor ?? null;
142
162
  });
143
163
  const levelTitle = computed(() => {
144
- if (triggersLevel.value) return "Triggers";
145
- return currentGroup.value?.label ?? "Add node";
164
+ if (triggersLevel.value) return t("integration.canvas.palette.group.triggers");
165
+ return currentGroup.value?.label ?? t("integration.canvas.palette.title.addNode");
146
166
  });
147
167
  function pushPath(path) {
148
168
  direction.value = "push";
@@ -166,7 +186,7 @@ const searchHits = computed(() => {
166
186
  const scope = currentGroup.value ? subtreeManifests(currentGroup.value) : latestManifests.value.filter((m) => m.category !== "trigger");
167
187
  const hits = [];
168
188
  for (const m of scope) {
169
- const hay = `${loc(m.name)} ${m.slug} ${loc(m.description)}`.toLowerCase();
189
+ const hay = `${resolveLocalized(m.name)} ${m.slug} ${resolveLocalized(m.description)}`.toLowerCase();
170
190
  if (hay.includes(q)) hits.push({ manifest: m, path: groupPath(m) });
171
191
  if (hits.length >= 50) break;
172
192
  }
@@ -175,7 +195,7 @@ const searchHits = computed(() => {
175
195
  const matchedTriggers = computed(() => {
176
196
  const q = search.value.trim().toLowerCase();
177
197
  if (!q || currentGroup.value) return [];
178
- const matches = (t) => loc(t.name).toLowerCase().includes(q) || t.type.includes(q) || loc(t.description).toLowerCase().includes(q);
198
+ const matches = (tpl) => resolveLocalized(tpl.name).toLowerCase().includes(q) || tpl.type.includes(q) || resolveLocalized(tpl.description).toLowerCase().includes(q);
179
199
  return TRIGGER_TEMPLATES.filter(matches);
180
200
  });
181
201
  const nodeDrag = useNodeDrag();
@@ -205,24 +225,31 @@ function logoSrcOf(m) {
205
225
  const imgs = m.images ?? [];
206
226
  return (imgs.find((i) => i.category === "logo") ?? imgs[0])?.src ?? null;
207
227
  }
208
- function onManifestDragStart(m, ev) {
228
+ const paletteDragEnabled = ref(true);
229
+ function onVersionSelectLeave(ev) {
230
+ const el = ev.currentTarget;
231
+ if (el && el !== el.ownerDocument.activeElement) paletteDragEnabled.value = true;
232
+ }
233
+ function onManifestDragStart(leaf, ev) {
209
234
  if (!ev.dataTransfer) return;
235
+ const m = effManifest(leaf);
210
236
  ev.dataTransfer.setData("application/integration-node", JSON.stringify({ kind: "manifest", slug: m.slug, version: m.version }));
211
237
  ev.dataTransfer.effectAllowed = "move";
212
- setCustomDragImage(ev, loc(m.name), logoSrcOf(m), "#8b5cf6");
238
+ setCustomDragImage(ev, resolveLocalized(m.name), logoSrcOf(m), "#8b5cf6");
213
239
  nodeDrag.start({ kind: "manifest", manifest: m });
214
240
  }
215
241
  function onTriggerDragStart(tpl, ev) {
216
242
  if (!ev.dataTransfer) return;
217
243
  ev.dataTransfer.setData("application/integration-node", JSON.stringify({ kind: "trigger", type: tpl.type }));
218
244
  ev.dataTransfer.effectAllowed = "move";
219
- setCustomDragImage(ev, loc(tpl.name), null, "#10b981");
245
+ setCustomDragImage(ev, resolveLocalized(tpl.name), null, "#10b981");
220
246
  nodeDrag.start({ kind: "trigger", tpl });
221
247
  }
222
248
  function onDragEnd() {
223
249
  nodeDrag.end();
224
250
  }
225
- function insertManifest(m) {
251
+ function insertManifest(leaf) {
252
+ const m = effManifest(leaf);
226
253
  emit("insert", { kind: "manifest", slug: m.slug, version: m.version });
227
254
  }
228
255
  function insertTrigger(tpl) {
@@ -234,6 +261,9 @@ function categoryColor(label) {
234
261
  const cat = NODE_CATEGORIES.find((c) => c.label === label || CORE_GROUP_BY_CATEGORY[String(c.key)] === label);
235
262
  return cat?.color ?? "bg-slate-100 text-slate-500";
236
263
  }
264
+ function nodeCategoryColor(m) {
265
+ return NODE_CATEGORIES.find((c) => c.key === m.category)?.color ?? "bg-slate-100 text-slate-500";
266
+ }
237
267
  watch(tree, () => {
238
268
  if (stack.value.length && !triggersLevel.value && !currentGroup.value) stack.value = [];
239
269
  });
@@ -249,7 +279,7 @@ const searching = computed(() => search.value.trim().length > 0);
249
279
  <button
250
280
  type="button"
251
281
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
252
- title="Back"
282
+ :title="t('integration.canvas.palette.action.back')"
253
283
  @click="popLevel"
254
284
  >
255
285
  <Lucide.ArrowLeft :size="14" />
@@ -264,7 +294,8 @@ const searching = computed(() => search.value.trim().length > 0);
264
294
  ref="searchInput"
265
295
  v-model="search"
266
296
  type="text"
267
- :placeholder="stack.length ? `Search in ${levelTitle}\u2026` : 'Search all nodes\u2026'"
297
+ :aria-label="stack.length ? t('integration.canvas.palette.search.ariaGroup', { group: levelTitle }) : t('integration.canvas.palette.search.ariaAll')"
298
+ :placeholder="stack.length ? t('integration.canvas.palette.search.placeholderGroup', { group: levelTitle }) : t('integration.canvas.palette.search.placeholderAll')"
268
299
  class="h-7 w-full rounded-md border border-black/[0.08] bg-white pl-7 pr-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/40"
269
300
  @keydown.esc="search ? search = '' : stack.length && popLevel()"
270
301
  >
@@ -291,45 +322,63 @@ const searching = computed(() => search.value.trim().length > 0);
291
322
  <!-- ── Search results (flat, path as subtitle) ─────────────────── -->
292
323
  <template v-if="searching">
293
324
  <div v-if="matchedTriggers.length" class="mb-1">
294
- <div class="px-2 py-1 text-mono text-muted-foreground">Triggers</div>
325
+ <div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.triggers") }}</div>
295
326
  <div
296
- v-for="t in matchedTriggers"
297
- :key="t.id"
327
+ v-for="tpl in matchedTriggers"
328
+ :key="tpl.id"
298
329
  class="group flex cursor-grab items-center gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
299
330
  draggable="true"
300
- @dragstart="onTriggerDragStart(t, $event)"
331
+ @dragstart="onTriggerDragStart(tpl, $event)"
301
332
  @dragend="onDragEnd"
302
- @click="insertTrigger(t)"
333
+ @click="insertTrigger(tpl)"
303
334
  >
304
335
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
305
- <component :is="iconFor(t.icon)" :size="13" />
336
+ <component :is="iconFor(tpl.icon)" :size="13" />
306
337
  </span>
307
338
  <div class="min-w-0">
308
- <div class="truncate text-sm font-medium">{{ loc(t.name) }}</div>
309
- <div class="text-xs leading-snug text-muted-foreground">Trigger</div>
339
+ <div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
340
+ <div class="text-xs leading-snug text-muted-foreground">{{ t("integration.canvas.badge.trigger") }}</div>
310
341
  </div>
311
342
  </div>
312
343
  </div>
313
344
 
314
345
  <p v-if="!searchHits.length && !matchedTriggers.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
315
- Nothing matches “{{ search }}”.
346
+ {{ t("integration.canvas.palette.search.empty", { query: search }) }}
316
347
  </p>
317
348
  <div
318
349
  v-for="hit in searchHits"
319
350
  :key="`${hit.manifest.slug}@${hit.manifest.version}`"
320
351
  class="group flex cursor-grab items-start gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
321
- draggable="true"
322
- :title="loc(hit.manifest.name)"
352
+ :draggable="paletteDragEnabled"
353
+ :title="resolveLocalized(hit.manifest.name)"
323
354
  @dragstart="onManifestDragStart(hit.manifest, $event)"
324
355
  @dragend="onDragEnd"
325
356
  @click="insertManifest(hit.manifest)"
326
357
  >
327
- <span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="categoryColor(hit.path[0] ?? '')">
328
- <IntegrationsNodeLogo :manifest="hit.manifest" :size="13" />
358
+ <span class="flex size-8 shrink-0 items-center justify-center rounded-md" :class="nodeCategoryColor(hit.manifest)">
359
+ <IntegrationsNodeMark :manifest="hit.manifest" :size="18" :badge="false" />
329
360
  </span>
330
361
  <div class="min-w-0 flex-1">
331
- <div class="truncate text-sm font-medium">{{ loc(hit.manifest.name) }}</div>
332
- <div class="text-xs leading-snug text-muted-foreground">{{ loc(hit.manifest.description) }}</div>
362
+ <div class="flex items-center gap-1.5">
363
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ resolveLocalized(hit.manifest.name) }}</span>
364
+ <select
365
+ v-if="versionsOf(hit.manifest.slug).length > 1"
366
+ :value="effVersion(hit.manifest)"
367
+ class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
368
+ :aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(hit.manifest.name) })"
369
+ @click.stop
370
+ @mousedown.stop
371
+ @change.stop="setSelectedVersion(hit.manifest.slug, $event.target.value)"
372
+ @mouseenter="paletteDragEnabled = false"
373
+ @mouseleave="onVersionSelectLeave"
374
+ @focus="paletteDragEnabled = false"
375
+ @blur="paletteDragEnabled = true"
376
+ >
377
+ <option v-for="v in versionsOf(hit.manifest.slug)" :key="v.version" :value="v.version">v{{ v.version }}</option>
378
+ </select>
379
+ <span v-else class="shrink-0 text-xs tabular-nums text-muted-foreground/60">v{{ hit.manifest.version }}</span>
380
+ </div>
381
+ <div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(hit.manifest.description) }}</div>
333
382
  <div class="truncate text-xs text-muted-foreground/70">{{ hit.path.join(" \u203A ") }}</div>
334
383
  </div>
335
384
  </div>
@@ -338,21 +387,21 @@ const searching = computed(() => search.value.trim().length > 0);
338
387
  <!-- ── Triggers level ──────────────────────────────────────────── -->
339
388
  <template v-else-if="triggersLevel">
340
389
  <div
341
- v-for="t in TRIGGER_TEMPLATES"
342
- :key="t.id"
390
+ v-for="tpl in TRIGGER_TEMPLATES"
391
+ :key="tpl.id"
343
392
  class="group flex cursor-grab items-center gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
344
393
  draggable="true"
345
- :title="loc(t.name)"
346
- @dragstart="onTriggerDragStart(t, $event)"
394
+ :title="resolveLocalized(tpl.name)"
395
+ @dragstart="onTriggerDragStart(tpl, $event)"
347
396
  @dragend="onDragEnd"
348
- @click="insertTrigger(t)"
397
+ @click="insertTrigger(tpl)"
349
398
  >
350
399
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
351
- <component :is="iconFor(t.icon)" :size="13" />
400
+ <component :is="iconFor(tpl.icon)" :size="13" />
352
401
  </span>
353
402
  <div class="min-w-0">
354
- <div class="truncate text-sm font-medium">{{ loc(t.name) }}</div>
355
- <div class="text-xs leading-snug text-muted-foreground">{{ loc(t.description) }}</div>
403
+ <div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
404
+ <div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(tpl.description) }}</div>
356
405
  </div>
357
406
  </div>
358
407
  </template>
@@ -361,7 +410,7 @@ const searching = computed(() => search.value.trim().length > 0);
361
410
  <template v-else-if="!currentGroup">
362
411
  <!-- Triggers group on top -->
363
412
  <div v-if="tree.core.children.size">
364
- <div class="px-2 py-1 text-mono text-muted-foreground">Core</div>
413
+ <div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.core") }}</div>
365
414
  <button
366
415
  type="button"
367
416
  class="flex w-full items-center gap-2 rounded-md border border-transparent px-2 py-1.5 text-left transition hover:border-black/[0.08] hover:bg-muted/40"
@@ -370,7 +419,7 @@ const searching = computed(() => search.value.trim().length > 0);
370
419
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-emerald-600/80">
371
420
  <Lucide.Zap :size="13" />
372
421
  </span>
373
- <span class="min-w-0 flex-1 truncate text-sm font-medium">Triggers</span>
422
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.canvas.palette.group.triggers") }}</span>
374
423
  <span class="text-sm tabular-nums text-muted-foreground">{{ TRIGGER_TEMPLATES.length }}</span>
375
424
  <Lucide.ChevronRight :size="13" class="text-muted-foreground" />
376
425
  </button>
@@ -391,7 +440,7 @@ const searching = computed(() => search.value.trim().length > 0);
391
440
  </div>
392
441
 
393
442
  <div v-if="tree.apps.children.size" class="mt-2">
394
- <div class="px-2 py-1 text-mono text-muted-foreground">Integrations</div>
443
+ <div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.integrations") }}</div>
395
444
  <button
396
445
  v-for="[label, group] in appRows"
397
446
  :key="`app-${label}`"
@@ -400,7 +449,7 @@ const searching = computed(() => search.value.trim().length > 0);
400
449
  @click="pushPath(['__apps', label])"
401
450
  >
402
451
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-white ring-1 ring-black/[0.06]">
403
- <IntegrationsNodeLogo v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" />
452
+ <IntegrationsNodeMark v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" logo-only />
404
453
  <Lucide.Package v-else :size="13" class="text-violet-600" />
405
454
  </span>
406
455
  <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ label }}</span>
@@ -423,7 +472,7 @@ const searching = computed(() => search.value.trim().length > 0);
423
472
  class="flex size-6 shrink-0 items-center justify-center rounded-md"
424
473
  :class="groupLogoManifest(group) ? 'bg-white ring-1 ring-black/[0.06]' : 'bg-slate-100 text-slate-500'"
425
474
  >
426
- <IntegrationsNodeLogo v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" />
475
+ <IntegrationsNodeMark v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" logo-only />
427
476
  <Lucide.FolderOpen v-else :size="13" />
428
477
  </span>
429
478
  <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ label }}</span>
@@ -435,18 +484,36 @@ const searching = computed(() => search.value.trim().length > 0);
435
484
  v-for="m in currentGroup.leaves"
436
485
  :key="`${m.slug}@${m.version}`"
437
486
  class="group flex cursor-grab items-start gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
438
- draggable="true"
439
- :title="loc(m.name)"
487
+ :draggable="paletteDragEnabled"
488
+ :title="resolveLocalized(m.name)"
440
489
  @dragstart="onManifestDragStart(m, $event)"
441
490
  @dragend="onDragEnd"
442
491
  @click="insertManifest(m)"
443
492
  >
444
- <span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="hasLogo(m) ? 'bg-white ring-1 ring-black/[0.06]' : categoryColor(groupPath(m)[0] ?? '')">
445
- <IntegrationsNodeLogo :manifest="m" :size="14" />
493
+ <span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="nodeCategoryColor(m)">
494
+ <IntegrationsNodeMark :manifest="m" :size="14" :badge="false" />
446
495
  </span>
447
496
  <div class="min-w-0 flex-1">
448
- <div class="truncate text-sm font-medium">{{ loc(m.name) }}</div>
449
- <div class="text-xs leading-snug text-muted-foreground">{{ loc(m.description) }}</div>
497
+ <div class="flex items-center gap-1.5">
498
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ resolveLocalized(m.name) }}</span>
499
+ <select
500
+ v-if="versionsOf(m.slug).length > 1"
501
+ :value="effVersion(m)"
502
+ class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
503
+ :aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(m.name) })"
504
+ @click.stop
505
+ @mousedown.stop
506
+ @change.stop="setSelectedVersion(m.slug, $event.target.value)"
507
+ @mouseenter="paletteDragEnabled = false"
508
+ @mouseleave="onVersionSelectLeave"
509
+ @focus="paletteDragEnabled = false"
510
+ @blur="paletteDragEnabled = true"
511
+ >
512
+ <option v-for="v in versionsOf(m.slug)" :key="v.version" :value="v.version">v{{ v.version }}</option>
513
+ </select>
514
+ <span v-else class="shrink-0 text-xs tabular-nums text-muted-foreground/60">v{{ m.version }}</span>
515
+ </div>
516
+ <div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(m.description) }}</div>
450
517
  </div>
451
518
  </div>
452
519
  </template>
@@ -455,7 +522,7 @@ const searching = computed(() => search.value.trim().length > 0);
455
522
  </div>
456
523
 
457
524
  <div class="border-t border-black/[0.06] px-3 py-2 text-sm text-muted-foreground">
458
- Drag onto the canvas — or click to insert.
525
+ {{ t("integration.canvas.palette.footer.hint") }}
459
526
  </div>
460
527
  </aside>
461
528
  </template>
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, 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,
@@ -15,27 +15,28 @@ const props = defineProps({
15
15
  modelValue: { type: null, required: true }
16
16
  });
17
17
  const emit = defineEmits(["update:modelValue"]);
18
- const METHODS = [
19
- { value: "equal", label: "equals", attribute: true, values: "list" },
20
- { value: "notEqual", label: "does not equal", attribute: true, values: "list" },
21
- { value: "contains", label: "contains", attribute: true, values: "list" },
22
- { value: "search", label: "search", attribute: true, values: "single" },
23
- { value: "startsWith", label: "starts with", attribute: true, values: "single" },
24
- { value: "endsWith", label: "ends with", attribute: true, values: "single" },
25
- { value: "lessThan", label: "less than", attribute: true, values: "single" },
26
- { value: "lessThanEqual", label: "less than or equal", attribute: true, values: "single" },
27
- { value: "greaterThan", label: "greater than", attribute: true, values: "single" },
28
- { value: "greaterThanEqual", label: "greater than or equal", attribute: true, values: "single" },
29
- { value: "between", label: "between", attribute: true, values: "list" },
30
- { value: "isNull", label: "is empty", attribute: true, values: "none" },
31
- { value: "isNotNull", label: "is not empty", attribute: true, values: "none" },
32
- { value: "orderAsc", label: "order ascending", attribute: true, values: "none" },
33
- { value: "orderDesc", label: "order descending", attribute: true, values: "none" },
34
- { value: "limit", label: "limit", attribute: false, values: "single" },
35
- { value: "offset", label: "offset", attribute: false, values: "single" }
36
- ];
18
+ const t = useIntegrationsT();
19
+ const METHODS = computed(() => [
20
+ { value: "equal", label: t("integration.nodeConfig.queryBuilder.method.equal"), attribute: true, values: "list" },
21
+ { value: "notEqual", label: t("integration.nodeConfig.queryBuilder.method.notEqual"), attribute: true, values: "list" },
22
+ { value: "contains", label: t("integration.nodeConfig.queryBuilder.method.contains"), attribute: true, values: "list" },
23
+ { value: "search", label: t("integration.nodeConfig.queryBuilder.method.search"), attribute: true, values: "single" },
24
+ { value: "startsWith", label: t("integration.nodeConfig.queryBuilder.method.startsWith"), attribute: true, values: "single" },
25
+ { value: "endsWith", label: t("integration.nodeConfig.queryBuilder.method.endsWith"), attribute: true, values: "single" },
26
+ { value: "lessThan", label: t("integration.nodeConfig.queryBuilder.method.lessThan"), attribute: true, values: "single" },
27
+ { value: "lessThanEqual", label: t("integration.nodeConfig.queryBuilder.method.lessThanEqual"), attribute: true, values: "single" },
28
+ { value: "greaterThan", label: t("integration.nodeConfig.queryBuilder.method.greaterThan"), attribute: true, values: "single" },
29
+ { value: "greaterThanEqual", label: t("integration.nodeConfig.queryBuilder.method.greaterThanEqual"), attribute: true, values: "single" },
30
+ { value: "between", label: t("integration.nodeConfig.queryBuilder.method.between"), attribute: true, values: "list" },
31
+ { value: "isNull", label: t("integration.nodeConfig.queryBuilder.method.isNull"), attribute: true, values: "none" },
32
+ { value: "isNotNull", label: t("integration.nodeConfig.queryBuilder.method.isNotNull"), attribute: true, values: "none" },
33
+ { value: "orderAsc", label: t("integration.nodeConfig.queryBuilder.method.orderAsc"), attribute: true, values: "none" },
34
+ { value: "orderDesc", label: t("integration.nodeConfig.queryBuilder.method.orderDesc"), attribute: true, values: "none" },
35
+ { value: "limit", label: t("integration.nodeConfig.queryBuilder.method.limit"), attribute: false, values: "single" },
36
+ { value: "offset", label: t("integration.nodeConfig.queryBuilder.method.offset"), attribute: false, values: "single" }
37
+ ]);
37
38
  function methodMeta(method) {
38
- return METHODS.find((m) => m.value === method) ?? null;
39
+ return METHODS.value.find((m) => m.value === method) ?? null;
39
40
  }
40
41
  function formatValue(v) {
41
42
  return typeof v === "string" ? v : JSON.stringify(v);
@@ -57,16 +58,16 @@ function parseRow(str) {
57
58
  return { kind: "raw", text: str };
58
59
  }
59
60
  function coerceValue(part) {
60
- const t = part.trim();
61
- if (/^-?\d+(\.\d+)?$/.test(t)) return Number(t);
62
- if (t === "true") return true;
63
- if (t === "false") return false;
64
- return t;
61
+ const t2 = part.trim();
62
+ if (/^-?\d+(\.\d+)?$/.test(t2)) return Number(t2);
63
+ if (t2 === "true") return true;
64
+ if (t2 === "false") return false;
65
+ return t2;
65
66
  }
66
67
  function rowToString(row) {
67
68
  if (row.kind === "raw") {
68
- const t = row.text.trim();
69
- return t === "" ? null : t;
69
+ const t2 = row.text.trim();
70
+ return t2 === "" ? null : t2;
70
71
  }
71
72
  const meta = methodMeta(row.method);
72
73
  if (!meta) return null;
@@ -131,13 +132,13 @@ function writeJson(text) {
131
132
  try {
132
133
  const parsed = JSON.parse(text);
133
134
  if (!Array.isArray(parsed)) {
134
- jsonError.value = "Must be a JSON array of query strings.";
135
+ jsonError.value = t("integration.nodeConfig.queryBuilder.json.notArray");
135
136
  return;
136
137
  }
137
138
  jsonError.value = null;
138
139
  emit("update:modelValue", parsed.map(String));
139
140
  } catch {
140
- jsonError.value = "Not valid JSON yet \u2014 the value is kept once it parses.";
141
+ jsonError.value = t("integration.nodeConfig.json.invalid");
141
142
  }
142
143
  }
143
144
  const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? methodMeta(r.method) : null));
@@ -153,7 +154,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
153
154
  :class="mode === 'builder' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
154
155
  @click="mode = 'builder'"
155
156
  >
156
- Builder
157
+ {{ t("integration.nodeConfig.queryBuilder.mode.builder") }}
157
158
  </button>
158
159
  <button
159
160
  type="button"
@@ -161,7 +162,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
161
162
  :class="mode === 'json' ? 'bg-slate-100 text-foreground' : 'text-muted-foreground hover:text-foreground'"
162
163
  @click="mode = 'json'"
163
164
  >
164
- JSON
165
+ {{ t("integration.nodeConfig.object.toggle.json") }}
165
166
  </button>
166
167
  </div>
167
168
  </div>
@@ -173,7 +174,9 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
173
174
  :model-value="row.method"
174
175
  @update:model-value="(v) => setRow(idx, { method: String(v) })"
175
176
  >
176
- <SelectTrigger class="h-7 w-40 shrink-0 bg-white text-sm">
177
+ <!-- A repeating row has no room for visible labels, so each
178
+ control names itself and says which row it is. -->
179
+ <SelectTrigger :aria-label="t('integration.nodeConfig.queryBuilder.row.comparisonAria', { n: idx + 1 })" class="h-7 w-40 shrink-0 bg-white text-sm">
177
180
  <SelectValue />
178
181
  </SelectTrigger>
179
182
  <SelectContent>
@@ -184,15 +187,17 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
184
187
  </Select>
185
188
  <Input
186
189
  v-if="rowMeta[idx]?.attribute"
190
+ :aria-label="t('integration.nodeConfig.queryBuilder.row.fieldAria', { n: idx + 1 })"
187
191
  :model-value="row.attribute"
188
- placeholder="field"
192
+ :placeholder="t('integration.nodeConfig.queryBuilder.row.fieldPlaceholder')"
189
193
  class="h-7 w-36 shrink-0 bg-white font-mono text-sm"
190
194
  @update:model-value="(v) => setRow(idx, { attribute: String(v) })"
191
195
  />
192
196
  <Input
193
197
  v-if="rowMeta[idx]?.values !== 'none'"
198
+ :aria-label="t('integration.nodeConfig.queryBuilder.row.valueAria', { n: idx + 1 })"
194
199
  :model-value="row.values"
195
- :placeholder="rowMeta[idx]?.values === 'list' ? 'value, value, \u2026' : 'value'"
200
+ :placeholder="rowMeta[idx]?.values === 'list' ? t('integration.nodeConfig.queryBuilder.row.valueListPlaceholder') : t('integration.nodeConfig.queryBuilder.row.valuePlaceholder')"
196
201
  class="h-7 min-w-0 flex-1 bg-white text-sm"
197
202
  @update:model-value="(v) => setRow(idx, { values: String(v) })"
198
203
  />
@@ -200,17 +205,19 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
200
205
  </template>
201
206
  <Input
202
207
  v-else
208
+ :aria-label="t('integration.nodeConfig.queryBuilder.row.rawTextAria', { n: idx + 1 })"
203
209
  :model-value="row.text"
204
210
  class="h-7 min-w-0 flex-1 bg-white font-mono text-sm"
205
- title="This entry is not in the builder vocabulary; edit it as text or in the JSON view."
211
+ :title="t('integration.nodeConfig.queryBuilder.row.rawTextTitle')"
206
212
  @update:model-value="(v) => setRow(idx, { text: String(v) })"
207
213
  />
208
214
  <button
209
215
  type="button"
216
+ :aria-label="t('integration.nodeConfig.queryBuilder.row.removeAria', { n: idx + 1 })"
210
217
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
211
218
  @click="removeRow(idx)"
212
219
  >
213
- <Trash2 :size="11" />
220
+ <TrashBin :size="11" />
214
221
  </button>
215
222
  </div>
216
223
  <Button
@@ -221,12 +228,13 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
221
228
  @click="addRow"
222
229
  >
223
230
  <Plus :size="11" />
224
- Add filter
231
+ {{ t("integration.nodeConfig.queryBuilder.action.addFilter") }}
225
232
  </Button>
226
233
  </template>
227
234
 
228
235
  <template v-else>
229
236
  <Textarea
237
+ :aria-label="t('integration.nodeConfig.queryBuilder.jsonAria')"
230
238
  :model-value="jsonDraft"
231
239
  :rows="6"
232
240
  spellcheck="false"