@revenexx/studio-integrations 0.6.0 → 1.0.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 (137) hide show
  1. package/CHANGELOG.md +643 -0
  2. package/README.md +31 -0
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +12 -3
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
  13. package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
  14. package/dist/runtime/components/Integrations/Layout.vue +28 -3
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
  18. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
  19. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
  21. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  23. package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
  24. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  25. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
  26. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
  27. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
  28. package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
  29. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  30. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  31. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  32. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  33. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  34. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  35. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  36. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  37. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  38. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  39. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  40. package/dist/runtime/components/Integrations/StickyNoteView.vue +82 -34
  41. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  42. package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
  43. package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
  44. package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
  45. package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
  46. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
  47. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  48. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
  49. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  50. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  51. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
  52. package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
  53. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
  54. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
  55. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
  56. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
  57. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  58. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
  59. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
  60. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
  61. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
  62. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
  63. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  64. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
  65. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
  66. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  67. package/dist/runtime/composables/integrationsApi.js +4 -2
  68. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  69. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
  70. package/dist/runtime/composables/useIntegrationsCredentials.js +8 -6
  71. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  72. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  73. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  74. package/dist/runtime/composables/useIntegrationsNodes.js +14 -2
  75. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  76. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  77. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  78. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  79. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  80. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
  81. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  82. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  83. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  84. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  85. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  86. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
  87. package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
  88. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
  89. package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
  90. package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
  91. package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
  92. package/dist/runtime/data/integrationNodeCatalog.js +22 -4
  93. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  94. package/dist/runtime/data/integrationTemplates.js +7 -1
  95. package/dist/runtime/data/nodeInputData.d.ts +58 -1
  96. package/dist/runtime/data/nodeInputData.js +30 -5
  97. package/dist/runtime/data/payloadForm.js +2 -1
  98. package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
  99. package/dist/runtime/data/triggerGlyphs.js +11 -0
  100. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  101. package/dist/runtime/i18n/messages/de.json +1224 -0
  102. package/dist/runtime/i18n/messages/en.json +1224 -0
  103. package/dist/runtime/i18n/translate.d.ts +83 -0
  104. package/dist/runtime/i18n/translate.js +41 -0
  105. package/dist/runtime/pages/integrations/audit.vue +52 -34
  106. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  107. package/dist/runtime/pages/integrations/credentials.vue +73 -52
  108. package/dist/runtime/pages/integrations/index.vue +66 -73
  109. package/dist/runtime/pages/integrations/runs.vue +33 -21
  110. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  111. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  112. package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
  113. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  114. package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
  115. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  116. package/dist/runtime/plugins/locale.d.ts +10 -0
  117. package/dist/runtime/plugins/locale.js +6 -0
  118. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  119. package/dist/runtime/plugins/realtime.client.js +4 -0
  120. package/dist/runtime/types/integrations.d.ts +25 -1
  121. package/dist/runtime/utils/cockpitSources.d.ts +22 -3
  122. package/dist/runtime/utils/credentialState.d.ts +30 -2
  123. package/dist/runtime/utils/credentialState.js +26 -11
  124. package/dist/runtime/utils/localize.d.ts +52 -5
  125. package/dist/runtime/utils/localize.js +47 -9
  126. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  127. package/dist/runtime/utils/pollBudget.js +17 -0
  128. package/dist/runtime/utils/schedule.d.ts +0 -25
  129. package/dist/runtime/utils/schedule.js +37 -25
  130. package/dist/runtime/utils/servedPaths.d.ts +22 -0
  131. package/dist/runtime/utils/servedPaths.js +3 -0
  132. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  133. package/dist/runtime/utils/statusStyle.js +9 -1
  134. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  135. package/dist/runtime/utils/validationFindings.js +4 -3
  136. package/dist/types.d.mts +2 -6
  137. package/package.json +3 -2
@@ -28,13 +28,13 @@ type __VLS_Props = {
28
28
  refusedFields?: Record<string, string>;
29
29
  };
30
30
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
31
- close: () => any;
32
31
  update: (next: WorkflowTrigger | WorkflowNode) => any;
32
+ close: () => any;
33
33
  remove: () => any;
34
34
  navigate: (id: string) => any;
35
35
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
36
- onClose?: (() => any) | undefined;
37
36
  onUpdate?: ((next: WorkflowTrigger | WorkflowNode) => any) | undefined;
37
+ onClose?: (() => any) | undefined;
38
38
  onRemove?: (() => any) | undefined;
39
39
  onNavigate?: ((id: string) => any) | undefined;
40
40
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -3,7 +3,8 @@ import type { PortEntry } from '../../data/nodeInputData.js';
3
3
  * The ports a node hands downstream, with their declared fields and whatever
4
4
  * sample value is known for each.
5
5
  *
6
- * Rendered from one place — the Output tab (specs/node-inspector.md AC-4). It
6
+ * Rendered from one place — the Output tab (specs/node-configuration.md AC-4,
7
+ * and AC-19 for the sentences the publisher wrote about a port and its fields). It
7
8
  * keeps its own file for readability rather than for reuse: the schema list is
8
9
  * a self-contained tree with its own type glyphs and sample formatting, and
9
10
  * inlining it would bury the tab's own markup.
@@ -1,9 +1,11 @@
1
1
  <script setup>
2
2
  import { portDotClass } from "../../data/portTone";
3
- defineProps({
3
+ import { resolveLocalized } from "../../utils/localize";
4
+ const props = defineProps({
4
5
  ports: { type: Array, required: true },
5
6
  valueAt: { type: Function, required: true }
6
7
  });
8
+ const t = useIntegrationsT();
7
9
  const TYPE_GLYPHS = {
8
10
  string: { char: "A", cls: "text-sky-600" },
9
11
  number: { char: "#", cls: "text-amber-600" },
@@ -15,6 +17,15 @@ const TYPE_GLYPHS = {
15
17
  function typeGlyph(dataType) {
16
18
  return dataType ? TYPE_GLYPHS[dataType] ?? null : null;
17
19
  }
20
+ function portText(p) {
21
+ return resolveLocalized(p.description);
22
+ }
23
+ function fieldText(f) {
24
+ return resolveLocalized(f.description);
25
+ }
26
+ function saysNoFields(p) {
27
+ return !portText(p) || props.valueAt(p.name) !== null;
28
+ }
18
29
  </script>
19
30
 
20
31
  <template>
@@ -24,19 +35,35 @@ function typeGlyph(dataType) {
24
35
  <span class="size-1.5 rounded-full" :class="portDotClass(p.kind, p.name)" />
25
36
  <span class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{{ p.name }}</span>
26
37
  </div>
27
- <div v-if="p.fields.length" class="space-y-0.5">
28
- <div v-for="f in p.fields" :key="f.name" class="flex min-w-0 items-baseline gap-1 font-mono text-[11px]">
29
- <span v-if="typeGlyph(f.dataType)" class="shrink-0 font-semibold" :class="typeGlyph(f.dataType).cls">{{ typeGlyph(f.dataType).char }}</span>
30
- <span class="shrink-0 text-slate-700">{{ f.name }}</span>
31
- <span v-if="valueAt(p.name, f.name) !== null" class="min-w-0 truncate text-slate-400">: {{ valueAt(p.name, f.name) }}</span>
32
- </div>
38
+ <p v-if="portText(p)" class="text-[11px] leading-snug text-muted-foreground">
39
+ {{ portText(p) }}
40
+ </p>
41
+ <!-- Two columns, sized to the widest field rather than split down the
42
+ middle: `max-content` puts every sentence one gap after the longest
43
+ name, so short names like `count` do not leave the reader crossing the
44
+ dialog to find what they mean, and the names still line up with each
45
+ other. The sample value is capped for the same reason — left free it
46
+ would widen the column and push the sentences back out. Both cells are
47
+ always emitted, or a field with nothing to say would slide the next
48
+ row's name into the sentence column. -->
49
+ <div v-if="p.fields.length" class="grid grid-cols-[max-content_minmax(0,1fr)] items-baseline gap-x-6 gap-y-0.5">
50
+ <template v-for="f in p.fields" :key="f.name">
51
+ <div class="flex min-w-0 items-baseline gap-1 font-mono text-[11px]">
52
+ <span v-if="typeGlyph(f.dataType)" class="shrink-0 font-semibold" :class="typeGlyph(f.dataType).cls">{{ typeGlyph(f.dataType).char }}</span>
53
+ <span class="shrink-0 text-slate-700">{{ f.name }}</span>
54
+ <span v-if="valueAt(p.name, f.name) !== null" class="max-w-56 truncate text-slate-400">: {{ valueAt(p.name, f.name) }}</span>
55
+ </div>
56
+ <p class="min-w-0 text-[11px] leading-snug text-muted-foreground">
57
+ {{ fieldText(f) }}
58
+ </p>
59
+ </template>
33
60
  </div>
34
- <p v-else class="text-[11px] leading-snug text-muted-foreground">
35
- No declared fields<template v-if="valueAt(p.name) !== null">; sampled: <span class="font-mono">{{ valueAt(p.name) }}</span></template>.
61
+ <p v-else-if="saysNoFields(p)" class="text-[11px] leading-snug text-muted-foreground">
62
+ {{ t("integration.nodeInspector.output.schema.noFields") }}<template v-if="valueAt(p.name) !== null">{{ t("integration.nodeInspector.output.schema.sampledSuffix") }} <span class="font-mono">{{ valueAt(p.name) }}</span></template>.
36
63
  </p>
37
64
  </div>
38
65
  <p v-if="!ports.length" class="text-xs leading-snug text-muted-foreground">
39
- This node declares no outputs.
66
+ {{ t("integration.nodeInspector.output.schema.noOutputs") }}
40
67
  </p>
41
68
  </div>
42
69
  </template>
@@ -3,7 +3,8 @@ import type { PortEntry } from '../../data/nodeInputData.js';
3
3
  * The ports a node hands downstream, with their declared fields and whatever
4
4
  * sample value is known for each.
5
5
  *
6
- * Rendered from one place — the Output tab (specs/node-inspector.md AC-4). It
6
+ * Rendered from one place — the Output tab (specs/node-configuration.md AC-4,
7
+ * and AC-19 for the sentences the publisher wrote about a port and its fields). It
7
8
  * keeps its own file for readability rather than for reuse: the schema list is
8
9
  * a self-contained tree with its own type glyphs and sample formatting, and
9
10
  * inlining it would bury the tab's own markup.
@@ -1,18 +1,19 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
+ import { AltArrowLeft as ArrowLeft, Bolt as Zap, Magnifier as Search } from "#solar-icons";
4
+ import { triggerGlyph } from "../../data/triggerGlyphs";
3
5
  import * as Lucide from "lucide-vue-next";
4
6
  import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
5
- import { nodeIcon } from "../../data/nodeIcons";
6
7
  import { resolveLocalized } from "../../utils/localize";
7
8
  const { load: loadNodes } = useIntegrationsNodes();
8
9
  onMounted(() => {
9
10
  void loadNodes();
10
11
  });
12
+ const t = useIntegrationsT();
11
13
  const emit = defineEmits(["insert"]);
12
14
  const search = ref("");
13
15
  const searchInput = ref(null);
14
16
  defineExpose({ focusSearch: () => searchInput.value?.focus() });
15
- const iconFor = nodeIcon;
16
17
  const CORE_GROUP_BY_CATEGORY = {
17
18
  control: "Flow",
18
19
  transform: "Data",
@@ -75,11 +76,14 @@ function isIntegration(m) {
75
76
  function makeGroup(label) {
76
77
  return { label, children: /* @__PURE__ */ new Map(), leaves: [], count: 0 };
77
78
  }
79
+ function isExecutable(m) {
80
+ return m.__hasBundle !== false;
81
+ }
78
82
  const latestManifests = computed(() => {
79
83
  const seen = /* @__PURE__ */ new Set();
80
84
  const out = [];
81
85
  for (const m of INTEGRATION_NODE_CATALOG) {
82
- if (seen.has(m.slug)) continue;
86
+ if (seen.has(m.slug) || !isExecutable(m)) continue;
83
87
  seen.add(m.slug);
84
88
  out.push(m);
85
89
  }
@@ -88,6 +92,7 @@ const latestManifests = computed(() => {
88
92
  const versionsBySlug = computed(() => {
89
93
  const map = /* @__PURE__ */ new Map();
90
94
  for (const m of INTEGRATION_NODE_CATALOG) {
95
+ if (!isExecutable(m)) continue;
91
96
  const list = map.get(m.slug);
92
97
  if (list) list.push(m);
93
98
  else map.set(m.slug, [m]);
@@ -160,8 +165,8 @@ const currentGroup = computed(() => {
160
165
  return cursor ?? null;
161
166
  });
162
167
  const levelTitle = computed(() => {
163
- if (triggersLevel.value) return "Triggers";
164
- return currentGroup.value?.label ?? "Add node";
168
+ if (triggersLevel.value) return t("integration.canvas.palette.group.triggers");
169
+ return currentGroup.value?.label ?? t("integration.canvas.palette.title.addNode");
165
170
  });
166
171
  function pushPath(path) {
167
172
  direction.value = "push";
@@ -194,7 +199,7 @@ const searchHits = computed(() => {
194
199
  const matchedTriggers = computed(() => {
195
200
  const q = search.value.trim().toLowerCase();
196
201
  if (!q || currentGroup.value) return [];
197
- const matches = (t) => resolveLocalized(t.name).toLowerCase().includes(q) || t.type.includes(q) || resolveLocalized(t.description).toLowerCase().includes(q);
202
+ const matches = (tpl) => resolveLocalized(tpl.name).toLowerCase().includes(q) || tpl.type.includes(q) || resolveLocalized(tpl.description).toLowerCase().includes(q);
198
203
  return TRIGGER_TEMPLATES.filter(matches);
199
204
  });
200
205
  const nodeDrag = useNodeDrag();
@@ -278,23 +283,23 @@ const searching = computed(() => search.value.trim().length > 0);
278
283
  <button
279
284
  type="button"
280
285
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
281
- title="Back"
286
+ :title="t('integration.canvas.palette.action.back')"
282
287
  @click="popLevel"
283
288
  >
284
- <Lucide.ArrowLeft :size="14" />
289
+ <ArrowLeft :size="14" />
285
290
  </button>
286
291
  <div class="min-w-0 flex-1 truncate text-mono text-muted-foreground">
287
292
  {{ levelTitle }}
288
293
  </div>
289
294
  </div>
290
295
  <div class="relative">
291
- <Lucide.Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
296
+ <Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
292
297
  <input
293
298
  ref="searchInput"
294
299
  v-model="search"
295
300
  type="text"
296
- :aria-label="stack.length ? `Search in ${levelTitle}` : 'Search all nodes'"
297
- :placeholder="stack.length ? `Search in ${levelTitle}\u2026` : 'Search all nodes\u2026'"
301
+ :aria-label="stack.length ? t('integration.canvas.palette.search.ariaGroup', { group: levelTitle }) : t('integration.canvas.palette.search.ariaAll')"
302
+ :placeholder="stack.length ? t('integration.canvas.palette.search.placeholderGroup', { group: levelTitle }) : t('integration.canvas.palette.search.placeholderAll')"
298
303
  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"
299
304
  @keydown.esc="search ? search = '' : stack.length && popLevel()"
300
305
  >
@@ -321,28 +326,28 @@ const searching = computed(() => search.value.trim().length > 0);
321
326
  <!-- ── Search results (flat, path as subtitle) ─────────────────── -->
322
327
  <template v-if="searching">
323
328
  <div v-if="matchedTriggers.length" class="mb-1">
324
- <div class="px-2 py-1 text-mono text-muted-foreground">Triggers</div>
329
+ <div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.triggers") }}</div>
325
330
  <div
326
- v-for="t in matchedTriggers"
327
- :key="t.id"
331
+ v-for="tpl in matchedTriggers"
332
+ :key="tpl.id"
328
333
  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"
329
334
  draggable="true"
330
- @dragstart="onTriggerDragStart(t, $event)"
335
+ @dragstart="onTriggerDragStart(tpl, $event)"
331
336
  @dragend="onDragEnd"
332
- @click="insertTrigger(t)"
337
+ @click="insertTrigger(tpl)"
333
338
  >
334
339
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
335
- <component :is="iconFor(t.icon)" :size="13" />
340
+ <component :is="triggerGlyph(tpl.type)" :size="13" />
336
341
  </span>
337
342
  <div class="min-w-0">
338
- <div class="truncate text-sm font-medium">{{ resolveLocalized(t.name) }}</div>
339
- <div class="text-xs leading-snug text-muted-foreground">Trigger</div>
343
+ <div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
344
+ <div class="text-xs leading-snug text-muted-foreground">{{ t("integration.canvas.badge.trigger") }}</div>
340
345
  </div>
341
346
  </div>
342
347
  </div>
343
348
 
344
349
  <p v-if="!searchHits.length && !matchedTriggers.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
345
- Nothing matches “{{ search }}”.
350
+ {{ t("integration.canvas.palette.search.empty", { query: search }) }}
346
351
  </p>
347
352
  <div
348
353
  v-for="hit in searchHits"
@@ -364,7 +369,7 @@ const searching = computed(() => search.value.trim().length > 0);
364
369
  v-if="versionsOf(hit.manifest.slug).length > 1"
365
370
  :value="effVersion(hit.manifest)"
366
371
  class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
367
- :aria-label="`Version to insert for ${resolveLocalized(hit.manifest.name)}`"
372
+ :aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(hit.manifest.name) })"
368
373
  @click.stop
369
374
  @mousedown.stop
370
375
  @change.stop="setSelectedVersion(hit.manifest.slug, $event.target.value)"
@@ -386,21 +391,21 @@ const searching = computed(() => search.value.trim().length > 0);
386
391
  <!-- ── Triggers level ──────────────────────────────────────────── -->
387
392
  <template v-else-if="triggersLevel">
388
393
  <div
389
- v-for="t in TRIGGER_TEMPLATES"
390
- :key="t.id"
394
+ v-for="tpl in TRIGGER_TEMPLATES"
395
+ :key="tpl.id"
391
396
  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"
392
397
  draggable="true"
393
- :title="resolveLocalized(t.name)"
394
- @dragstart="onTriggerDragStart(t, $event)"
398
+ :title="resolveLocalized(tpl.name)"
399
+ @dragstart="onTriggerDragStart(tpl, $event)"
395
400
  @dragend="onDragEnd"
396
- @click="insertTrigger(t)"
401
+ @click="insertTrigger(tpl)"
397
402
  >
398
403
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
399
- <component :is="iconFor(t.icon)" :size="13" />
404
+ <component :is="triggerGlyph(tpl.type)" :size="13" />
400
405
  </span>
401
406
  <div class="min-w-0">
402
- <div class="truncate text-sm font-medium">{{ resolveLocalized(t.name) }}</div>
403
- <div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(t.description) }}</div>
407
+ <div class="truncate text-sm font-medium">{{ resolveLocalized(tpl.name) }}</div>
408
+ <div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(tpl.description) }}</div>
404
409
  </div>
405
410
  </div>
406
411
  </template>
@@ -409,16 +414,16 @@ const searching = computed(() => search.value.trim().length > 0);
409
414
  <template v-else-if="!currentGroup">
410
415
  <!-- Triggers group on top -->
411
416
  <div v-if="tree.core.children.size">
412
- <div class="px-2 py-1 text-mono text-muted-foreground">Core</div>
417
+ <div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.core") }}</div>
413
418
  <button
414
419
  type="button"
415
420
  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"
416
421
  @click="pushPath(['__triggers'])"
417
422
  >
418
423
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-emerald-600/80">
419
- <Lucide.Zap :size="13" />
424
+ <Zap :size="13" />
420
425
  </span>
421
- <span class="min-w-0 flex-1 truncate text-sm font-medium">Triggers</span>
426
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.canvas.palette.group.triggers") }}</span>
422
427
  <span class="text-sm tabular-nums text-muted-foreground">{{ TRIGGER_TEMPLATES.length }}</span>
423
428
  <Lucide.ChevronRight :size="13" class="text-muted-foreground" />
424
429
  </button>
@@ -439,7 +444,7 @@ const searching = computed(() => search.value.trim().length > 0);
439
444
  </div>
440
445
 
441
446
  <div v-if="tree.apps.children.size" class="mt-2">
442
- <div class="px-2 py-1 text-mono text-muted-foreground">Integrations</div>
447
+ <div class="px-2 py-1 text-mono text-muted-foreground">{{ t("integration.canvas.palette.group.integrations") }}</div>
443
448
  <button
444
449
  v-for="[label, group] in appRows"
445
450
  :key="`app-${label}`"
@@ -499,7 +504,7 @@ const searching = computed(() => search.value.trim().length > 0);
499
504
  v-if="versionsOf(m.slug).length > 1"
500
505
  :value="effVersion(m)"
501
506
  class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
502
- :aria-label="`Version to insert for ${resolveLocalized(m.name)}`"
507
+ :aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(m.name) })"
503
508
  @click.stop
504
509
  @mousedown.stop
505
510
  @change.stop="setSelectedVersion(m.slug, $event.target.value)"
@@ -521,7 +526,7 @@ const searching = computed(() => search.value.trim().length > 0);
521
526
  </div>
522
527
 
523
528
  <div class="border-t border-black/[0.06] px-3 py-2 text-sm text-muted-foreground">
524
- Drag onto the canvas — or click to insert.
529
+ {{ t("integration.canvas.palette.footer.hint") }}
525
530
  </div>
526
531
  </aside>
527
532
  </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>
@@ -175,7 +176,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
175
176
  >
176
177
  <!-- A repeating row has no room for visible labels, so each
177
178
  control names itself and says which row it is. -->
178
- <SelectTrigger :aria-label="`Filter ${idx + 1} \u2014 comparison`" class="h-7 w-40 shrink-0 bg-white text-sm">
179
+ <SelectTrigger :aria-label="t('integration.nodeConfig.queryBuilder.row.comparisonAria', { n: idx + 1 })" class="h-7 w-40 shrink-0 bg-white text-sm">
179
180
  <SelectValue />
180
181
  </SelectTrigger>
181
182
  <SelectContent>
@@ -186,17 +187,17 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
186
187
  </Select>
187
188
  <Input
188
189
  v-if="rowMeta[idx]?.attribute"
189
- :aria-label="`Filter ${idx + 1} \u2014 field`"
190
+ :aria-label="t('integration.nodeConfig.queryBuilder.row.fieldAria', { n: idx + 1 })"
190
191
  :model-value="row.attribute"
191
- placeholder="field"
192
+ :placeholder="t('integration.nodeConfig.queryBuilder.row.fieldPlaceholder')"
192
193
  class="h-7 w-36 shrink-0 bg-white font-mono text-sm"
193
194
  @update:model-value="(v) => setRow(idx, { attribute: String(v) })"
194
195
  />
195
196
  <Input
196
197
  v-if="rowMeta[idx]?.values !== 'none'"
197
- :aria-label="`Filter ${idx + 1} \u2014 value`"
198
+ :aria-label="t('integration.nodeConfig.queryBuilder.row.valueAria', { n: idx + 1 })"
198
199
  :model-value="row.values"
199
- :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')"
200
201
  class="h-7 min-w-0 flex-1 bg-white text-sm"
201
202
  @update:model-value="(v) => setRow(idx, { values: String(v) })"
202
203
  />
@@ -204,19 +205,19 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
204
205
  </template>
205
206
  <Input
206
207
  v-else
207
- :aria-label="`Filter ${idx + 1} \u2014 raw text`"
208
+ :aria-label="t('integration.nodeConfig.queryBuilder.row.rawTextAria', { n: idx + 1 })"
208
209
  :model-value="row.text"
209
210
  class="h-7 min-w-0 flex-1 bg-white font-mono text-sm"
210
- 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')"
211
212
  @update:model-value="(v) => setRow(idx, { text: String(v) })"
212
213
  />
213
214
  <button
214
215
  type="button"
215
- :aria-label="`Remove filter ${idx + 1}`"
216
+ :aria-label="t('integration.nodeConfig.queryBuilder.row.removeAria', { n: idx + 1 })"
216
217
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
217
218
  @click="removeRow(idx)"
218
219
  >
219
- <Trash2 :size="11" />
220
+ <TrashBin :size="11" />
220
221
  </button>
221
222
  </div>
222
223
  <Button
@@ -227,13 +228,13 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
227
228
  @click="addRow"
228
229
  >
229
230
  <Plus :size="11" />
230
- Add filter
231
+ {{ t("integration.nodeConfig.queryBuilder.action.addFilter") }}
231
232
  </Button>
232
233
  </template>
233
234
 
234
235
  <template v-else>
235
236
  <Textarea
236
- aria-label="Filters as JSON"
237
+ :aria-label="t('integration.nodeConfig.queryBuilder.jsonAria')"
237
238
  :model-value="jsonDraft"
238
239
  :rows="6"
239
240
  spellcheck="false"
@@ -14,6 +14,7 @@ const props = defineProps({
14
14
  run: { type: Object, required: true }
15
15
  });
16
16
  const emit = defineEmits(["changed"]);
17
+ const t = useIntegrationsT();
17
18
  const router = useRouter();
18
19
  const { cancelRun, terminateRun, retryRun, resumeRun } = useIntegrationsRuns();
19
20
  function viewDetails() {
@@ -26,8 +27,31 @@ const isActive = computed(() => props.run.status === "running" || props.run.stat
26
27
  const isTerminal = computed(() => ["succeeded", "failed", "cancelled"].includes(props.run.status));
27
28
  const canResume = computed(() => props.run.status === "failed");
28
29
  const hasAny = computed(() => isActive.value || isTerminal.value);
29
- const LABEL = { cancel: "Cancel", terminate: "Terminate", retry: "Retry", resume: "Resume" };
30
30
  const busy = ref(false);
31
+ function actionLabel(a) {
32
+ switch (a) {
33
+ case "cancel":
34
+ return t("integration.runs.menu.cancel");
35
+ case "terminate":
36
+ return t("integration.runs.menu.terminate");
37
+ case "retry":
38
+ return t("integration.runs.menu.retry");
39
+ case "resume":
40
+ return t("integration.runs.menu.resume");
41
+ }
42
+ }
43
+ function actionErrorFallback(a) {
44
+ switch (a) {
45
+ case "cancel":
46
+ return t("integration.runs.error.cancel");
47
+ case "terminate":
48
+ return t("integration.runs.error.terminate");
49
+ case "retry":
50
+ return t("integration.runs.error.retry");
51
+ case "resume":
52
+ return t("integration.runs.error.resume");
53
+ }
54
+ }
31
55
  async function runAction(a) {
32
56
  if (busy.value) return;
33
57
  busy.value = true;
@@ -49,18 +73,18 @@ async function runAction(a) {
49
73
  }
50
74
  const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
51
75
  toast.success(
52
- (a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ? `Started run #${newId}` : `${LABEL[a]} requested`
76
+ (a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ? t("integration.runs.toast.action.started", { id: newId }) : t("integration.runs.toast.action.requested", { action: actionLabel(a) })
53
77
  );
54
78
  emit("changed");
55
79
  } catch (e) {
56
- toast.error(e?.message ?? `Could not ${LABEL[a].toLowerCase()} the run.`);
80
+ toast.error(e?.message ?? actionErrorFallback(a));
57
81
  } finally {
58
82
  busy.value = false;
59
83
  }
60
84
  }
61
85
  function confirmTerminate() {
62
- toast(`Terminate run #${props.run.id}?`, {
63
- action: { label: "Terminate", onClick: () => void runAction("terminate") }
86
+ toast(t("integration.runs.toast.terminate.confirm", { id: props.run.id }), {
87
+ action: { label: actionLabel("terminate"), onClick: () => void runAction("terminate") }
64
88
  });
65
89
  }
66
90
  </script>
@@ -70,19 +94,19 @@ function confirmTerminate() {
70
94
  <DropdownMenuTrigger as-child>
71
95
  <!-- Icon-only, so it needs a name of its own — the run id is the only
72
96
  thing that tells two rows' menus apart. -->
73
- <Button variant="ghost" size="icon" class="size-7" :disabled="busy" :aria-label="`Actions for run #${run.id}`" @click.stop>
97
+ <Button variant="ghost" size="icon" class="size-7" :disabled="busy" :aria-label="t('integration.runs.menu.aria.actions', { id: run.id })" @click.stop>
74
98
  <MoreHorizontal :size="16" />
75
99
  </Button>
76
100
  </DropdownMenuTrigger>
77
101
  <DropdownMenuContent align="end" class="w-40" @click.stop>
78
- <DropdownMenuItem @select="viewDetails">View details</DropdownMenuItem>
79
- <DropdownMenuItem @select="viewReplay">Replay</DropdownMenuItem>
102
+ <DropdownMenuItem @select="viewDetails">{{ t("integration.runs.menu.viewDetails") }}</DropdownMenuItem>
103
+ <DropdownMenuItem @select="viewReplay">{{ t("integration.runs.menu.replay") }}</DropdownMenuItem>
80
104
  <DropdownMenuSeparator v-if="hasAny" />
81
- <DropdownMenuItem v-if="isActive" @select="runAction('cancel')">Cancel run</DropdownMenuItem>
82
- <DropdownMenuItem v-if="isActive" class="text-rose-600" @select="confirmTerminate">Terminate</DropdownMenuItem>
105
+ <DropdownMenuItem v-if="isActive" @select="runAction('cancel')">{{ actionLabel("cancel") }}</DropdownMenuItem>
106
+ <DropdownMenuItem v-if="isActive" class="text-rose-600" @select="confirmTerminate">{{ actionLabel("terminate") }}</DropdownMenuItem>
83
107
  <DropdownMenuSeparator v-if="isActive && (canResume || isTerminal)" />
84
- <DropdownMenuItem v-if="canResume" @select="runAction('resume')">Resume</DropdownMenuItem>
85
- <DropdownMenuItem v-if="isTerminal" @select="runAction('retry')">Retry</DropdownMenuItem>
108
+ <DropdownMenuItem v-if="canResume" @select="runAction('resume')">{{ actionLabel("resume") }}</DropdownMenuItem>
109
+ <DropdownMenuItem v-if="isTerminal" @select="runAction('retry')">{{ actionLabel("retry") }}</DropdownMenuItem>
86
110
  </DropdownMenuContent>
87
111
  </DropdownMenu>
88
112
  </template>