@revenexx/studio-integrations 0.6.0 → 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 (110) hide show
  1. package/CHANGELOG.md +227 -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 +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +29 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
  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 +16 -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.vue +15 -14
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
  18. package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
  19. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
  20. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
  21. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
  22. package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
  23. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  24. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  25. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  26. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  27. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  28. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  29. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  30. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  31. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  32. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  33. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  34. package/dist/runtime/components/Integrations/StickyNoteView.vue +37 -23
  35. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  36. package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
  37. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
  38. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  39. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
  40. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  41. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  42. package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
  43. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
  44. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  45. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
  46. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
  47. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
  48. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  49. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
  50. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
  51. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  52. package/dist/runtime/composables/integrationsApi.js +4 -2
  53. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  54. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
  55. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  56. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  57. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  58. package/dist/runtime/composables/useIntegrationsNodes.js +3 -1
  59. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  60. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  61. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  62. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  63. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  64. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  65. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  66. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  67. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  68. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  69. package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
  70. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
  71. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  72. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  73. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  74. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  75. package/dist/runtime/data/integrationTemplates.js +7 -1
  76. package/dist/runtime/data/payloadForm.js +2 -1
  77. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  78. package/dist/runtime/i18n/messages/de.json +1207 -0
  79. package/dist/runtime/i18n/messages/en.json +1207 -0
  80. package/dist/runtime/i18n/translate.d.ts +83 -0
  81. package/dist/runtime/i18n/translate.js +41 -0
  82. package/dist/runtime/pages/integrations/audit.vue +52 -34
  83. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  84. package/dist/runtime/pages/integrations/credentials.vue +70 -52
  85. package/dist/runtime/pages/integrations/index.vue +50 -56
  86. package/dist/runtime/pages/integrations/runs.vue +33 -21
  87. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  88. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
  90. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
  92. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  93. package/dist/runtime/plugins/locale.d.ts +10 -0
  94. package/dist/runtime/plugins/locale.js +6 -0
  95. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  96. package/dist/runtime/plugins/realtime.client.js +4 -0
  97. package/dist/runtime/utils/credentialState.d.ts +24 -1
  98. package/dist/runtime/utils/credentialState.js +26 -11
  99. package/dist/runtime/utils/localize.d.ts +18 -5
  100. package/dist/runtime/utils/localize.js +32 -9
  101. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  102. package/dist/runtime/utils/pollBudget.js +17 -0
  103. package/dist/runtime/utils/schedule.d.ts +0 -25
  104. package/dist/runtime/utils/schedule.js +37 -25
  105. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  106. package/dist/runtime/utils/statusStyle.js +9 -1
  107. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  108. package/dist/runtime/utils/validationFindings.js +4 -3
  109. package/dist/types.d.mts +2 -6
  110. package/package.json +3 -2
@@ -3,7 +3,7 @@ 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). It
7
7
  * keeps its own file for readability rather than for reuse: the schema list is
8
8
  * a self-contained tree with its own type glyphs and sample formatting, and
9
9
  * inlining it would bury the tab's own markup.
@@ -4,6 +4,7 @@ defineProps({
4
4
  ports: { type: Array, required: true },
5
5
  valueAt: { type: Function, required: true }
6
6
  });
7
+ const t = useIntegrationsT();
7
8
  const TYPE_GLYPHS = {
8
9
  string: { char: "A", cls: "text-sky-600" },
9
10
  number: { char: "#", cls: "text-amber-600" },
@@ -32,11 +33,11 @@ function typeGlyph(dataType) {
32
33
  </div>
33
34
  </div>
34
35
  <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>.
36
+ {{ 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
37
  </p>
37
38
  </div>
38
39
  <p v-if="!ports.length" class="text-xs leading-snug text-muted-foreground">
39
- This node declares no outputs.
40
+ {{ t("integration.nodeInspector.output.schema.noOutputs") }}
40
41
  </p>
41
42
  </div>
42
43
  </template>
@@ -3,7 +3,7 @@ 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). It
7
7
  * keeps its own file for readability rather than for reuse: the schema list is
8
8
  * a self-contained tree with its own type glyphs and sample formatting, and
9
9
  * inlining it would bury the tab's own markup.
@@ -8,6 +8,7 @@ const { load: loadNodes } = useIntegrationsNodes();
8
8
  onMounted(() => {
9
9
  void loadNodes();
10
10
  });
11
+ const t = useIntegrationsT();
11
12
  const emit = defineEmits(["insert"]);
12
13
  const search = ref("");
13
14
  const searchInput = ref(null);
@@ -160,8 +161,8 @@ const currentGroup = computed(() => {
160
161
  return cursor ?? null;
161
162
  });
162
163
  const levelTitle = computed(() => {
163
- if (triggersLevel.value) return "Triggers";
164
- 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");
165
166
  });
166
167
  function pushPath(path) {
167
168
  direction.value = "push";
@@ -194,7 +195,7 @@ const searchHits = computed(() => {
194
195
  const matchedTriggers = computed(() => {
195
196
  const q = search.value.trim().toLowerCase();
196
197
  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);
198
+ const matches = (tpl) => resolveLocalized(tpl.name).toLowerCase().includes(q) || tpl.type.includes(q) || resolveLocalized(tpl.description).toLowerCase().includes(q);
198
199
  return TRIGGER_TEMPLATES.filter(matches);
199
200
  });
200
201
  const nodeDrag = useNodeDrag();
@@ -278,7 +279,7 @@ const searching = computed(() => search.value.trim().length > 0);
278
279
  <button
279
280
  type="button"
280
281
  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"
282
+ :title="t('integration.canvas.palette.action.back')"
282
283
  @click="popLevel"
283
284
  >
284
285
  <Lucide.ArrowLeft :size="14" />
@@ -293,8 +294,8 @@ const searching = computed(() => search.value.trim().length > 0);
293
294
  ref="searchInput"
294
295
  v-model="search"
295
296
  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'"
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')"
298
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"
299
300
  @keydown.esc="search ? search = '' : stack.length && popLevel()"
300
301
  >
@@ -321,28 +322,28 @@ const searching = computed(() => search.value.trim().length > 0);
321
322
  <!-- ── Search results (flat, path as subtitle) ─────────────────── -->
322
323
  <template v-if="searching">
323
324
  <div v-if="matchedTriggers.length" class="mb-1">
324
- <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>
325
326
  <div
326
- v-for="t in matchedTriggers"
327
- :key="t.id"
327
+ v-for="tpl in matchedTriggers"
328
+ :key="tpl.id"
328
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"
329
330
  draggable="true"
330
- @dragstart="onTriggerDragStart(t, $event)"
331
+ @dragstart="onTriggerDragStart(tpl, $event)"
331
332
  @dragend="onDragEnd"
332
- @click="insertTrigger(t)"
333
+ @click="insertTrigger(tpl)"
333
334
  >
334
335
  <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" />
336
+ <component :is="iconFor(tpl.icon)" :size="13" />
336
337
  </span>
337
338
  <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>
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>
340
341
  </div>
341
342
  </div>
342
343
  </div>
343
344
 
344
345
  <p v-if="!searchHits.length && !matchedTriggers.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
345
- Nothing matches “{{ search }}”.
346
+ {{ t("integration.canvas.palette.search.empty", { query: search }) }}
346
347
  </p>
347
348
  <div
348
349
  v-for="hit in searchHits"
@@ -364,7 +365,7 @@ const searching = computed(() => search.value.trim().length > 0);
364
365
  v-if="versionsOf(hit.manifest.slug).length > 1"
365
366
  :value="effVersion(hit.manifest)"
366
367
  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)}`"
368
+ :aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(hit.manifest.name) })"
368
369
  @click.stop
369
370
  @mousedown.stop
370
371
  @change.stop="setSelectedVersion(hit.manifest.slug, $event.target.value)"
@@ -386,21 +387,21 @@ const searching = computed(() => search.value.trim().length > 0);
386
387
  <!-- ── Triggers level ──────────────────────────────────────────── -->
387
388
  <template v-else-if="triggersLevel">
388
389
  <div
389
- v-for="t in TRIGGER_TEMPLATES"
390
- :key="t.id"
390
+ v-for="tpl in TRIGGER_TEMPLATES"
391
+ :key="tpl.id"
391
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"
392
393
  draggable="true"
393
- :title="resolveLocalized(t.name)"
394
- @dragstart="onTriggerDragStart(t, $event)"
394
+ :title="resolveLocalized(tpl.name)"
395
+ @dragstart="onTriggerDragStart(tpl, $event)"
395
396
  @dragend="onDragEnd"
396
- @click="insertTrigger(t)"
397
+ @click="insertTrigger(tpl)"
397
398
  >
398
399
  <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" />
400
+ <component :is="iconFor(tpl.icon)" :size="13" />
400
401
  </span>
401
402
  <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>
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>
404
405
  </div>
405
406
  </div>
406
407
  </template>
@@ -409,7 +410,7 @@ const searching = computed(() => search.value.trim().length > 0);
409
410
  <template v-else-if="!currentGroup">
410
411
  <!-- Triggers group on top -->
411
412
  <div v-if="tree.core.children.size">
412
- <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>
413
414
  <button
414
415
  type="button"
415
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"
@@ -418,7 +419,7 @@ const searching = computed(() => search.value.trim().length > 0);
418
419
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-emerald-600/80">
419
420
  <Lucide.Zap :size="13" />
420
421
  </span>
421
- <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>
422
423
  <span class="text-sm tabular-nums text-muted-foreground">{{ TRIGGER_TEMPLATES.length }}</span>
423
424
  <Lucide.ChevronRight :size="13" class="text-muted-foreground" />
424
425
  </button>
@@ -439,7 +440,7 @@ const searching = computed(() => search.value.trim().length > 0);
439
440
  </div>
440
441
 
441
442
  <div v-if="tree.apps.children.size" class="mt-2">
442
- <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>
443
444
  <button
444
445
  v-for="[label, group] in appRows"
445
446
  :key="`app-${label}`"
@@ -499,7 +500,7 @@ const searching = computed(() => search.value.trim().length > 0);
499
500
  v-if="versionsOf(m.slug).length > 1"
500
501
  :value="effVersion(m)"
501
502
  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)}`"
503
+ :aria-label="t('integration.canvas.palette.version.selectAria', { name: resolveLocalized(m.name) })"
503
504
  @click.stop
504
505
  @mousedown.stop
505
506
  @change.stop="setSelectedVersion(m.slug, $event.target.value)"
@@ -521,7 +522,7 @@ const searching = computed(() => search.value.trim().length > 0);
521
522
  </div>
522
523
 
523
524
  <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.
525
+ {{ t("integration.canvas.palette.footer.hint") }}
525
526
  </div>
526
527
  </aside>
527
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>
@@ -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>