@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
@@ -11,9 +11,7 @@ import {
11
11
  AltArrowRight as Next,
12
12
  Play,
13
13
  Pause,
14
- RefreshCircle as Loader2,
15
- ClockCircle as Clock,
16
- LinkRound as Webhook
14
+ RefreshCircle as Loader2
17
15
  } from "#solar-icons";
18
16
  import { Badge, Button } from "@revenexx/studio";
19
17
  import { toast } from "vue-sonner";
@@ -26,6 +24,8 @@ import StickyNoteView from "./StickyNoteView.vue";
26
24
  import GroupFrameView from "./GroupFrameView.vue";
27
25
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
28
26
  import { findManifest } from "../../data/integrationNodeCatalog";
27
+ import { triggerGlyph } from "../../data/triggerGlyphs";
28
+ import { SAFETY_POLL_MS } from "../../utils/pollBudget";
29
29
  import { runStatusLabel, statusClass } from "../../utils/statusStyle";
30
30
  const props = defineProps({
31
31
  workflow: { type: Object, required: true },
@@ -33,9 +33,14 @@ const props = defineProps({
33
33
  single: { type: Boolean, required: false }
34
34
  });
35
35
  const { loadForWorkflow, getSteps } = useIntegrationsRuns();
36
+ const t = useIntegrationsT();
36
37
  const TERMINAL = /* @__PURE__ */ new Set(["completed", "succeeded", "failed", "cancelled", "terminated"]);
37
38
  const STEPS_POLL_MS = 4e3;
38
39
  const PLAY_INTERVAL_MS = 3e3;
40
+ const { delivering: realtimeDelivering, runTick, lastRunEvent } = useIntegrationsRealtime();
41
+ function stepsPollDelay() {
42
+ return realtimeDelivering.value ? SAFETY_POLL_MS : STEPS_POLL_MS;
43
+ }
39
44
  const runs = ref([]);
40
45
  const runsPage = ref(1);
41
46
  const runsLastPage = ref(1);
@@ -53,7 +58,7 @@ async function loadRuns(reset = false) {
53
58
  runsPage.value = res.page;
54
59
  runsLastPage.value = res.lastPage;
55
60
  } catch (e) {
56
- toast.error(e?.message ?? "Could not load the runs.");
61
+ toast.error(e?.message ?? t("integration.replay.toast.loadRunsFailed"));
57
62
  } finally {
58
63
  runsLoading.value = false;
59
64
  }
@@ -91,16 +96,23 @@ async function loadSteps(runId, isPoll = false) {
91
96
  if (runStatus.value && !TERMINAL.has(runStatus.value)) {
92
97
  pollTimer = window.setTimeout(() => {
93
98
  void loadSteps(runId, true);
94
- }, STEPS_POLL_MS);
99
+ }, stepsPollDelay());
95
100
  }
96
101
  } catch (e) {
97
102
  if (selectedRunId.value === runId) {
98
- stepsError.value = e?.message ?? "Could not load the run steps.";
103
+ stepsError.value = e?.message ?? t("integration.replay.error.loadStepsFailed");
99
104
  }
100
105
  } finally {
101
106
  if (!isPoll) stepsLoading.value = false;
102
107
  }
103
108
  }
109
+ watch(runTick, () => {
110
+ const runId = selectedRunId.value;
111
+ if (!runId) return;
112
+ const event = lastRunEvent.value;
113
+ if (event && event.topicId !== runId) return;
114
+ void loadSteps(runId, true);
115
+ });
104
116
  function selectRun(id) {
105
117
  if (selectedRunId.value === id) return;
106
118
  selectedRunId.value = id;
@@ -173,7 +185,7 @@ const finalStatus = computed(() => {
173
185
  return m;
174
186
  });
175
187
  const runNodeIds = computed(() => new Set(steps.value.map((s) => s.node_id)));
176
- const triggerIds = computed(() => new Set(props.workflow.blob.triggers.map((t) => t.id)));
188
+ const triggerIds = computed(() => new Set(props.workflow.blob.triggers.map((t2) => t2.id)));
177
189
  function edgeColorByKind(kind) {
178
190
  return kind === "error" ? "#cbd5e1" : "#94a3b8";
179
191
  }
@@ -230,7 +242,7 @@ const vfNodes = computed(() => {
230
242
  const posOf = (id) => {
231
243
  const wn = blob.nodes.find((n) => n.id === id);
232
244
  if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
233
- const ti = blob.triggers.findIndex((t) => t.id === id);
245
+ const ti = blob.triggers.findIndex((t2) => t2.id === id);
234
246
  if (ti >= 0) return triggerPos(id, ti);
235
247
  return { x: 0, y: 0 };
236
248
  };
@@ -244,17 +256,17 @@ const vfNodes = computed(() => {
244
256
  zIndex: -100 + i,
245
257
  draggable: false,
246
258
  class: "replay-dim",
247
- data: { sticky: s, onUpdate: () => {
248
- }, onRemove: () => {
249
- } }
259
+ // No callbacks: that absence is what makes the note read-only — it then
260
+ // draws no toolbar, no resize handle and does not open for typing (PO-284).
261
+ data: { sticky: s }
250
262
  })),
251
- ...blob.triggers.map((t, i) => ({
252
- id: t.id,
263
+ ...blob.triggers.map((t2, i) => ({
264
+ id: t2.id,
253
265
  type: "trigger",
254
- position: triggerPos(t.id, i),
266
+ position: triggerPos(t2.id, i),
255
267
  draggable: false,
256
- class: replayNodeClass(t.id, "trigger"),
257
- data: t
268
+ class: replayNodeClass(t2.id, "trigger"),
269
+ data: t2
258
270
  })),
259
271
  ...blob.nodes.map((n) => ({
260
272
  id: n.id,
@@ -274,7 +286,7 @@ const vfEdges = computed(() => {
274
286
  const nodePos = (id) => {
275
287
  const wn = blob.nodes.find((n) => n.id === id);
276
288
  if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
277
- const ti = blob.triggers.findIndex((t) => t.id === id);
289
+ const ti = blob.triggers.findIndex((t2) => t2.id === id);
278
290
  if (ti >= 0) return blob.triggerPositions?.[id] ?? { x: -40, y: 60 + ti * 140 };
279
291
  return { x: 0, y: 0 };
280
292
  };
@@ -395,7 +407,7 @@ function pretty(v) {
395
407
  }
396
408
  }
397
409
  function triggerIcon(type) {
398
- return type === "schedule" ? Clock : type === "webhook" ? Webhook : Play;
410
+ return triggerGlyph(type);
399
411
  }
400
412
  const activeStepTitle = computed(() => {
401
413
  const s = activeStep.value;
@@ -458,12 +470,12 @@ watch(() => props.initialRunId, (id) => {
458
470
  <!-- Which run is on the canvas -->
459
471
  <div class="absolute top-3 left-3 z-10 flex items-center gap-2 rounded-lg border border-black/[0.08] bg-white/95 px-2.5 py-1.5 text-sm shadow-sm backdrop-blur-sm">
460
472
  <Play :size="13" class="text-muted-foreground" />
461
- <span class="font-medium">{{ selectedRunId ? `Run #${selectedRunId}` : "Pick a run" }}</span>
473
+ <span class="font-medium">{{ selectedRunId ? t("integration.replay.canvas.runLabel", { id: selectedRunId }) : t("integration.replay.canvas.pickRun") }}</span>
462
474
  <Badge v-if="runStatus" variant="outline" class="border text-xs font-medium uppercase" :class="statusClass(runStatus)">
463
475
  {{ runStatusLabel(runStatus) }}
464
476
  </Badge>
465
477
  <Badge v-if="stepsSource" variant="outline" class="border text-xs font-medium uppercase text-muted-foreground">
466
- {{ stepsSource === "temporal" ? "Live" : "Snapshot" }}
478
+ {{ stepsSource === "temporal" ? t("integration.replay.badge.live") : t("integration.replay.badge.snapshot") }}
467
479
  </Badge>
468
480
  <Loader2 v-if="stepsLoading" :size="13" class="animate-spin text-muted-foreground" />
469
481
  </div>
@@ -479,14 +491,14 @@ watch(() => props.initialRunId, (id) => {
479
491
  <div class="flex items-center gap-1">
480
492
  <Button variant="ghost" size="sm" class="h-7" :disabled="safeCursor === 0" @click="seek(safeCursor - 1)">
481
493
  <Prev :size="13" />
482
- Prev
494
+ {{ t("integration.replay.transport.action.prev") }}
483
495
  </Button>
484
496
  <Button size="sm" class="h-7" @click="togglePlay">
485
497
  <component :is="playing ? Pause : Play" :size="13" class="mr-1" />
486
- {{ playing ? "Pause" : "Play" }}
498
+ {{ playing ? t("integration.replay.transport.action.pause") : t("integration.replay.transport.action.play") }}
487
499
  </Button>
488
500
  <Button variant="ghost" size="sm" class="h-7" :disabled="safeCursor >= steps.length - 1" @click="seek(safeCursor + 1)">
489
- Next
501
+ {{ t("integration.replay.transport.action.next") }}
490
502
  <Next :size="13" />
491
503
  </Button>
492
504
  </div>
@@ -497,17 +509,17 @@ watch(() => props.initialRunId, (id) => {
497
509
  :max="steps.length - 1"
498
510
  :value="safeCursor"
499
511
  class="h-1.5 flex-1 cursor-pointer accent-primary"
500
- aria-label="Replay step"
512
+ :aria-label="t('integration.replay.transport.scrubberAria')"
501
513
  @input="seek(Number($event.target.value))"
502
514
  >
503
515
 
504
516
  <span class="shrink-0 text-sm tabular-nums text-muted-foreground">
505
- Step {{ safeCursor + 1 }}/{{ steps.length }}
517
+ {{ t("integration.replay.transport.stepCounter", { current: safeCursor + 1, total: steps.length }) }}
506
518
  <span class="font-mono">· {{ activeStepTitle }}</span>
507
519
  </span>
508
520
  </template>
509
521
  <p v-else-if="!stepsLoading" class="text-sm text-muted-foreground">
510
- {{ selectedRunId ? "No steps recorded for this run." : "Pick a run on the right to replay it." }}
522
+ {{ selectedRunId ? t("integration.replay.state.noSteps") : t("integration.replay.state.pickRunHint") }}
511
523
  </p>
512
524
  </div>
513
525
  </div>
@@ -517,10 +529,10 @@ watch(() => props.initialRunId, (id) => {
517
529
  <aside class="flex w-[32rem] shrink-0 flex-col border-l border-black/[0.06] bg-white">
518
530
  <div class="flex items-center gap-2 border-b border-black/[0.06] px-3 py-2">
519
531
  <div class="min-w-0 flex-1">
520
- <div class="truncate font-mono text-sm font-medium">{{ activeStepTitle || "Debug" }}</div>
532
+ <div class="truncate font-mono text-sm font-medium">{{ activeStepTitle || t("integration.replay.debug.titleFallback") }}</div>
521
533
  <div class="text-xs tabular-nums text-muted-foreground">
522
- <template v-if="steps.length">Step {{ safeCursor + 1 }} / {{ steps.length }}</template>
523
- <template v-else>No step selected</template>
534
+ <template v-if="steps.length">{{ t("integration.replay.debug.stepCounter", { current: safeCursor + 1, total: steps.length }) }}</template>
535
+ <template v-else>{{ t("integration.replay.debug.noStepSelected") }}</template>
524
536
  </div>
525
537
  </div>
526
538
  <Badge v-if="activeStep" variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="statusClass(activeStep.status)">
@@ -531,59 +543,59 @@ watch(() => props.initialRunId, (id) => {
531
543
  <div v-if="activeStep" class="min-h-0 flex-1 space-y-4 overflow-y-auto p-3">
532
544
  <div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
533
545
  <div>
534
- <div class="text-mono text-muted-foreground">Attempt</div>
546
+ <div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.attempt") }}</div>
535
547
  <div class="tabular-nums">{{ activeStep.attempt + 1 }}</div>
536
548
  </div>
537
549
  <div>
538
- <div class="text-mono text-muted-foreground">Duration</div>
550
+ <div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.duration") }}</div>
539
551
  <div class="tabular-nums">{{ formatDuration(activeStep.duration_ms) }}</div>
540
552
  </div>
541
553
  <div>
542
- <div class="text-mono text-muted-foreground">Started</div>
554
+ <div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.started") }}</div>
543
555
  <div class="tabular-nums">{{ formatTime(activeStep.started_at) }}</div>
544
556
  </div>
545
557
  <div>
546
- <div class="text-mono text-muted-foreground">Completed</div>
558
+ <div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.completed") }}</div>
547
559
  <div class="tabular-nums">{{ formatTime(activeStep.completed_at) }}</div>
548
560
  </div>
549
561
  </div>
550
562
 
551
563
  <div v-if="activeStep.branch" class="text-sm">
552
- <div class="text-mono text-muted-foreground mb-1">Branch</div>
564
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.branch") }}</div>
553
565
  <code class="rounded bg-black/[0.05] px-1.5 py-0.5 font-mono">{{ activeStep.branch }}</code>
554
566
  </div>
555
567
 
556
568
  <div v-if="activeStep.error" class="text-sm">
557
- <div class="text-mono text-muted-foreground mb-1">Error</div>
569
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.error") }}</div>
558
570
  <pre class="max-h-40 overflow-auto rounded-md border border-rose-500/20 bg-rose-500/5 p-2 font-mono text-sm text-rose-700">{{ activeStep.error }}</pre>
559
571
  </div>
560
572
 
561
573
  <div v-if="activeStep.input !== void 0 && activeStep.input !== null" class="text-sm">
562
- <div class="text-mono text-muted-foreground mb-1">Input</div>
574
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.input") }}</div>
563
575
  <pre class="max-h-56 overflow-auto rounded-md border border-black/[0.06] bg-muted/20 p-2 font-mono text-sm">{{ pretty(activeStep.input) }}</pre>
564
576
  </div>
565
577
 
566
578
  <div v-if="activeStep.config !== void 0 && activeStep.config !== null" class="text-sm">
567
- <div class="text-mono text-muted-foreground mb-1">Config</div>
579
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.config") }}</div>
568
580
  <pre class="max-h-56 overflow-auto rounded-md border border-black/[0.06] bg-muted/20 p-2 font-mono text-sm">{{ pretty(activeStep.config) }}</pre>
569
581
  </div>
570
582
 
571
583
  <div v-if="activeStep.output !== void 0 && activeStep.output !== null" class="text-sm">
572
- <div class="text-mono text-muted-foreground mb-1">Output</div>
584
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.output") }}</div>
573
585
  <pre class="max-h-56 overflow-auto rounded-md border border-black/[0.06] bg-muted/20 p-2 font-mono text-sm">{{ pretty(activeStep.output) }}</pre>
574
586
  </div>
575
587
  </div>
576
588
  <p v-else class="p-4 text-sm text-muted-foreground">
577
- The active step's status, timings and payloads land here while the replay walks the run.
589
+ {{ t("integration.replay.debug.emptyHint") }}
578
590
  </p>
579
591
  </aside>
580
592
 
581
593
  <!-- Run list — hidden in single mode (the run detail replays ONE run). -->
582
594
  <aside v-if="!single" class="flex w-80 shrink-0 flex-col border-l border-black/[0.06] bg-white">
583
595
  <div class="border-b border-black/[0.06] px-3 py-2">
584
- <div class="text-sm font-semibold">Runs</div>
596
+ <div class="text-sm font-semibold">{{ t("integration.replay.runsList.heading") }}</div>
585
597
  <p class="text-sm text-muted-foreground">
586
- Click through the executions to replay them on the canvas.
598
+ {{ t("integration.replay.runsList.description") }}
587
599
  </p>
588
600
  </div>
589
601
 
@@ -612,7 +624,7 @@ watch(() => props.initialRunId, (id) => {
612
624
  </button>
613
625
 
614
626
  <p v-if="!runs.length && !runsLoading" class="px-2 py-6 text-center text-sm text-muted-foreground">
615
- No runs yet — trigger one from the editor's Run button.
627
+ {{ t("integration.replay.runsList.empty") }}
616
628
  </p>
617
629
 
618
630
  <div v-if="runsLoading" class="flex justify-center py-3">
@@ -625,7 +637,7 @@ watch(() => props.initialRunId, (id) => {
625
637
  class="w-full text-sm"
626
638
  @click="loadMoreRuns"
627
639
  >
628
- Load older runs
640
+ {{ t("integration.replay.runsList.loadMore") }}
629
641
  </Button>
630
642
  </div>
631
643
  </aside>
@@ -4,33 +4,32 @@ export interface ExecutionModeOption {
4
4
  label: string;
5
5
  hint: string;
6
6
  }
7
+ /** What this dialog edits — the values as they stand, saved or not. */
8
+ export interface WorkflowSettingsValues {
9
+ name: string;
10
+ description: string;
11
+ tags: string[];
12
+ icon: WorkflowIcon | null;
13
+ executionMode: string;
14
+ }
7
15
  type __VLS_Props = {
8
16
  open: boolean;
9
17
  workflow: Workflow;
10
18
  modes: ExecutionModeOption[];
11
- modeDraft: string;
12
- savingMode?: boolean;
13
- savingMeta?: boolean;
19
+ /**
20
+ * The values to edit: the editor's unsaved ones where it has any, the saved ones
21
+ * otherwise. Reopening the dialog therefore shows what the operator last entered
22
+ * rather than what the platform still holds.
23
+ */
24
+ current: WorkflowSettingsValues;
14
25
  /** Tag suggestions (tags already used across the tenant's workflows). */
15
26
  tagSuggestions?: string[];
16
27
  };
17
28
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
29
  "update:open": (args_0: boolean) => any;
19
- "change-mode": (mode: string) => any;
20
- "save-meta": (meta: {
21
- name: string;
22
- description: string;
23
- tags: string[];
24
- icon: WorkflowIcon | null;
25
- }) => any;
30
+ apply: (values: WorkflowSettingsValues) => any;
26
31
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
27
32
  "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
28
- "onChange-mode"?: ((mode: string) => any) | undefined;
29
- "onSave-meta"?: ((meta: {
30
- name: string;
31
- description: string;
32
- tags: string[];
33
- icon: WorkflowIcon | null;
34
- }) => any) | undefined;
33
+ onApply?: ((values: WorkflowSettingsValues) => any) | undefined;
35
34
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
35
  export default _default;
@@ -1,11 +1,11 @@
1
1
  <script setup>
2
- import { ref, watch } from "vue";
3
- import { RefreshCircle as Loader2 } from "#solar-icons";
2
+ import { computed, ref, watch } from "vue";
4
3
  import {
5
4
  Button,
6
5
  Dialog,
7
6
  DialogContent,
8
7
  DialogDescription,
8
+ DialogFooter,
9
9
  DialogHeader,
10
10
  DialogTitle,
11
11
  Input,
@@ -23,23 +23,35 @@ const props = defineProps({
23
23
  open: { type: Boolean, required: true },
24
24
  workflow: { type: Object, required: true },
25
25
  modes: { type: Array, required: true },
26
- modeDraft: { type: String, required: true },
27
- savingMode: { type: Boolean, required: false },
28
- savingMeta: { type: Boolean, required: false },
26
+ current: { type: Object, required: true },
29
27
  tagSuggestions: { type: Array, required: false }
30
28
  });
31
- const emit = defineEmits(["update:open", "change-mode", "save-meta"]);
29
+ const emit = defineEmits(["update:open", "apply"]);
30
+ const t = useIntegrationsT();
32
31
  const nameDraft = ref("");
33
32
  const descriptionDraft = ref("");
34
33
  const tagsDraft = ref([]);
35
34
  const iconDraft = ref(null);
35
+ const modeDraft = ref("");
36
36
  watch(() => props.open, (v) => {
37
37
  if (!v) return;
38
- nameDraft.value = props.workflow.name;
39
- descriptionDraft.value = props.workflow.description ?? "";
40
- tagsDraft.value = [...props.workflow.blob.tags ?? []];
41
- iconDraft.value = props.workflow.blob.icon ?? null;
38
+ nameDraft.value = props.current.name;
39
+ descriptionDraft.value = props.current.description;
40
+ tagsDraft.value = [...props.current.tags];
41
+ iconDraft.value = props.current.icon;
42
+ modeDraft.value = props.current.executionMode;
42
43
  }, { immediate: true });
44
+ const edited = computed(() => ({
45
+ name: nameDraft.value.trim(),
46
+ description: descriptionDraft.value.trim(),
47
+ tags: tagsDraft.value,
48
+ icon: iconDraft.value,
49
+ executionMode: modeDraft.value
50
+ }));
51
+ function applyAndClose() {
52
+ if (JSON.stringify(edited.value) !== JSON.stringify(props.current)) emit("apply", edited.value);
53
+ emit("update:open", false);
54
+ }
43
55
  function formatDateTime(iso) {
44
56
  if (!iso) return "\u2014";
45
57
  try {
@@ -54,25 +66,26 @@ function formatDateTime(iso) {
54
66
  <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
55
67
  <DialogContent class="sm:max-w-lg">
56
68
  <DialogHeader>
57
- <DialogTitle>Workflow settings</DialogTitle>
69
+ <DialogTitle>{{ t("integration.workflows.settings.dialog.title") }}</DialogTitle>
58
70
  <DialogDescription class="text-sm">
59
- Execution behavior and metadata for “{{ workflow.name }}”.
71
+ <!-- The name as it stands, drafted included: this dialog is where a rename is
72
+ typed, so naming the workflow by its stored name here contradicts the
73
+ field directly below. -->
74
+ {{ t("integration.workflows.settings.dialog.description", { name: current.name }) }}
60
75
  </DialogDescription>
61
76
  </DialogHeader>
62
77
 
63
78
  <div class="space-y-4 text-sm">
64
79
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-3">
65
- <div class="text-mono text-muted-foreground">Execution</div>
80
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.execution") }}</div>
66
81
  <div class="flex items-center gap-2">
67
82
  <Select
68
- :model-value="modeDraft"
69
- :disabled="savingMode"
70
- @update:model-value="(v) => emit('change-mode', v)"
83
+ v-model="modeDraft"
71
84
  >
72
85
  <!-- Named explicitly: the section caption above says "Execution",
73
86
  which is not a name the control itself carries. -->
74
- <SelectTrigger aria-label="Execution mode" class="h-8 w-full bg-white text-sm">
75
- <SelectValue placeholder="Pick a mode" />
87
+ <SelectTrigger :aria-label="t('integration.workflows.settings.field.executionMode.aria')" class="h-8 w-full bg-white text-sm">
88
+ <SelectValue :placeholder="t('integration.workflows.settings.field.executionMode.placeholder')" />
76
89
  </SelectTrigger>
77
90
  <SelectContent>
78
91
  <SelectItem v-for="m in modes" :key="m.value" :value="m.value">
@@ -80,7 +93,6 @@ function formatDateTime(iso) {
80
93
  </SelectItem>
81
94
  </SelectContent>
82
95
  </Select>
83
- <Loader2 v-if="savingMode" :size="14" class="shrink-0 animate-spin text-muted-foreground" />
84
96
  </div>
85
97
  <p class="text-xs text-muted-foreground">
86
98
  {{ modes.find((m2) => m2.value === modeDraft)?.hint }}
@@ -88,55 +100,55 @@ function formatDateTime(iso) {
88
100
  </div>
89
101
 
90
102
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-3">
91
- <div class="text-mono text-muted-foreground">Appearance</div>
103
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.appearance") }}</div>
92
104
  <!-- Real labels, not styled captions: each one names its own control
93
105
  (`for`/`id`), otherwise a screen reader reads four unlabelled
94
106
  fields in a row. -->
95
107
  <div class="space-y-1.5">
96
- <Label for="wf-set-name" class="text-sm font-normal text-muted-foreground">Name</Label>
97
- <Input id="wf-set-name" v-model="nameDraft" class="bg-white" placeholder="Workflow name" />
108
+ <Label for="wf-set-name" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.name.label") }}</Label>
109
+ <Input id="wf-set-name" v-model="nameDraft" class="bg-white" :placeholder="t('integration.workflows.settings.field.name.placeholder')" />
98
110
  </div>
99
111
  <div class="space-y-1.5">
100
- <Label for="wf-set-desc" class="text-sm font-normal text-muted-foreground">Description</Label>
101
- <Textarea id="wf-set-desc" v-model="descriptionDraft" rows="2" class="bg-white" placeholder="What this workflow does and why it exists." />
112
+ <Label for="wf-set-desc" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.description.label") }}</Label>
113
+ <Textarea id="wf-set-desc" v-model="descriptionDraft" rows="2" class="bg-white" :placeholder="t('integration.workflows.settings.field.description.placeholder')" />
102
114
  </div>
103
115
  <div class="space-y-1.5">
104
- <Label for="wf-set-tags" class="text-sm font-normal text-muted-foreground">Tags</Label>
116
+ <Label for="wf-set-tags" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.tags.label") }}</Label>
105
117
  <WorkflowTagsInput v-model="tagsDraft" input-id="wf-set-tags" :suggestions="tagSuggestions ?? []" />
106
118
  </div>
107
119
  <div class="space-y-1.5">
108
- <Label id="wf-set-icon-label" class="text-sm font-normal text-muted-foreground">Icon</Label>
120
+ <Label id="wf-set-icon-label" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.icon.label") }}</Label>
109
121
  <WorkflowIconPicker v-model="iconDraft" trigger-labelledby="wf-set-icon-label" />
110
122
  </div>
111
- <div class="flex justify-end">
112
- <Button
113
- size="sm"
114
- class="h-7 text-sm"
115
- :disabled="savingMeta || !nameDraft.trim()"
116
- @click="emit('save-meta', { name: nameDraft.trim(), description: descriptionDraft.trim(), tags: tagsDraft, icon: iconDraft })"
117
- >
118
- <Loader2 v-if="savingMeta" :size="12" class="animate-spin" />
119
- Save changes
120
- </Button>
121
- </div>
122
123
  </div>
123
124
 
124
125
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-2">
125
- <div class="text-mono text-muted-foreground">Metadata</div>
126
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.metadata") }}</div>
126
127
  <div class="grid grid-cols-[110px_1fr] gap-x-3 gap-y-2">
127
- <span class="text-muted-foreground">ID</span>
128
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.id") }}</span>
128
129
  <span class="font-mono">{{ workflow.id }}</span>
129
- <span class="text-muted-foreground">Definition</span>
130
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.definition") }}</span>
130
131
  <span class="font-mono">{{ workflow.blob_definition_version }}</span>
131
- <span class="text-muted-foreground">Revision</span>
132
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.revision") }}</span>
132
133
  <span class="font-mono">r{{ workflow.revision }}</span>
133
- <span class="text-muted-foreground">Created</span>
134
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.created") }}</span>
134
135
  <span>{{ formatDateTime(workflow.created_at) }}</span>
135
- <span class="text-muted-foreground">Updated</span>
136
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.updated") }}</span>
136
137
  <span>{{ formatDateTime(workflow.updated_at) }}</span>
137
138
  </div>
138
139
  </div>
139
140
  </div>
141
+
142
+ <!-- Cancel / Done, the pair every dialog in the editor closes on. Done reaches
143
+ the workflow's unsaved state; the header's Save reaches the platform. -->
144
+ <DialogFooter>
145
+ <Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
146
+ {{ t("integration.common.action.cancel") }}
147
+ </Button>
148
+ <Button size="sm" class="h-8" :disabled="!nameDraft.trim()" @click="applyAndClose">
149
+ {{ t("integration.workflows.settings.action.done") }}
150
+ </Button>
151
+ </DialogFooter>
140
152
  </DialogContent>
141
153
  </Dialog>
142
154
  </template>
@@ -4,33 +4,32 @@ export interface ExecutionModeOption {
4
4
  label: string;
5
5
  hint: string;
6
6
  }
7
+ /** What this dialog edits — the values as they stand, saved or not. */
8
+ export interface WorkflowSettingsValues {
9
+ name: string;
10
+ description: string;
11
+ tags: string[];
12
+ icon: WorkflowIcon | null;
13
+ executionMode: string;
14
+ }
7
15
  type __VLS_Props = {
8
16
  open: boolean;
9
17
  workflow: Workflow;
10
18
  modes: ExecutionModeOption[];
11
- modeDraft: string;
12
- savingMode?: boolean;
13
- savingMeta?: boolean;
19
+ /**
20
+ * The values to edit: the editor's unsaved ones where it has any, the saved ones
21
+ * otherwise. Reopening the dialog therefore shows what the operator last entered
22
+ * rather than what the platform still holds.
23
+ */
24
+ current: WorkflowSettingsValues;
14
25
  /** Tag suggestions (tags already used across the tenant's workflows). */
15
26
  tagSuggestions?: string[];
16
27
  };
17
28
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
29
  "update:open": (args_0: boolean) => any;
19
- "change-mode": (mode: string) => any;
20
- "save-meta": (meta: {
21
- name: string;
22
- description: string;
23
- tags: string[];
24
- icon: WorkflowIcon | null;
25
- }) => any;
30
+ apply: (values: WorkflowSettingsValues) => any;
26
31
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
27
32
  "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
28
- "onChange-mode"?: ((mode: string) => any) | undefined;
29
- "onSave-meta"?: ((meta: {
30
- name: string;
31
- description: string;
32
- tags: string[];
33
- icon: WorkflowIcon | null;
34
- }) => any) | undefined;
33
+ onApply?: ((values: WorkflowSettingsValues) => any) | undefined;
35
34
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
35
  export default _default;
@@ -5,6 +5,7 @@ const props = defineProps({
5
5
  suggestions: { type: Array, required: false, default: () => [] },
6
6
  inputId: { type: String, required: false }
7
7
  });
8
+ const t = useIntegrationsT();
8
9
  const draft = ref("");
9
10
  const inputRef = ref(null);
10
11
  const listId = useId();
@@ -15,7 +16,7 @@ function commit() {
15
16
  if (!model.value.includes(tag)) model.value = [...model.value, tag];
16
17
  }
17
18
  function remove(tag) {
18
- model.value = model.value.filter((t) => t !== tag);
19
+ model.value = model.value.filter((t2) => t2 !== tag);
19
20
  }
20
21
  function onKeydown(e) {
21
22
  if (e.key === "Enter" || e.key === ",") {
@@ -33,17 +34,17 @@ function onKeydown(e) {
33
34
  @click="inputRef?.focus()"
34
35
  >
35
36
  <span
36
- v-for="t in model"
37
- :key="t"
37
+ v-for="tag in model"
38
+ :key="tag"
38
39
  class="inline-flex items-center gap-1 rounded-md border border-black/[0.06] bg-muted/50 px-1.5 py-0.5 text-xs text-foreground"
39
40
  >
40
- {{ t }}
41
+ {{ tag }}
41
42
  <button
42
43
  type="button"
43
44
  class="text-muted-foreground transition hover:text-foreground"
44
- :title="`Remove ${t}`"
45
- :aria-label="`Remove ${t}`"
46
- @click.stop="remove(t)"
45
+ :title="t('integration.workflows.tagsInput.action.remove', { tag })"
46
+ :aria-label="t('integration.workflows.tagsInput.action.remove', { tag })"
47
+ @click.stop="remove(tag)"
47
48
  >
48
49
  ×
49
50
  </button>
@@ -57,7 +58,7 @@ function onKeydown(e) {
57
58
  type="text"
58
59
  :list="listId"
59
60
  class="h-6 min-w-24 flex-1 bg-transparent text-sm focus:outline-none"
60
- :placeholder="model.length ? 'Add tag\u2026' : 'Add tags \u2014 Enter confirms\u2026'"
61
+ :placeholder="model.length ? t('integration.workflows.tagsInput.placeholder.add') : t('integration.workflows.tagsInput.placeholder.addFirst')"
61
62
  @keydown="onKeydown"
62
63
  @blur="commit()"
63
64
  >