@revenexx/studio-integrations 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -17,6 +17,7 @@ defineProps({
17
17
  runId: { type: [String, null], required: false }
18
18
  });
19
19
  const emit = defineEmits(["update:open", "view-run"]);
20
+ const t = useIntegrationsT();
20
21
  </script>
21
22
 
22
23
  <template>
@@ -25,16 +26,16 @@ const emit = defineEmits(["update:open", "view-run"]);
25
26
  JSON <pre> scrolling INSIDE instead of blowing the dialog open. -->
26
27
  <DialogContent class="max-w-[94vw] overflow-hidden sm:max-w-4xl">
27
28
  <DialogHeader>
28
- <DialogTitle>{{ mode === "async" ? "Run started" : "Run response" }}</DialogTitle>
29
+ <DialogTitle>{{ mode === "async" ? t("integration.runs.response.title.async") : t("integration.runs.response.title.sync") }}</DialogTitle>
29
30
  <DialogDescription class="font-mono text-sm">
30
- {{ mode === "async" ? "Queued for background execution" : "Synchronous run result" }}
31
+ {{ mode === "async" ? t("integration.runs.response.description.async") : t("integration.runs.response.description.sync") }}
31
32
  </DialogDescription>
32
33
  </DialogHeader>
33
34
 
34
35
  <div class="min-w-0 max-w-full text-sm">
35
36
  <div v-if="loading" class="flex items-center gap-2 py-6 text-muted-foreground">
36
37
  <Loader2 :size="16" class="animate-spin" />
37
- {{ mode === "async" ? "Starting\u2026" : "Running\u2026" }}
38
+ {{ mode === "async" ? t("integration.runs.response.loading.async") : t("integration.runs.response.loading.sync") }}
38
39
  </div>
39
40
 
40
41
  <template v-else-if="error">
@@ -43,7 +44,7 @@ const emit = defineEmits(["update:open", "view-run"]);
43
44
  </div>
44
45
  <div v-if="runId" class="mt-3 flex justify-end">
45
46
  <Button class="h-8 text-sm" @click="emit('view-run', runId)">
46
- View run #{{ runId }}
47
+ {{ t("integration.runs.response.action.viewRunWithId", { id: runId }) }}
47
48
  </Button>
48
49
  </div>
49
50
  </template>
@@ -51,15 +52,15 @@ const emit = defineEmits(["update:open", "view-run"]);
51
52
  <template v-else-if="mode === 'async'">
52
53
  <div class="flex items-center gap-2 rounded-md border border-emerald-500/20 bg-emerald-500/5 px-3 py-2 text-emerald-700">
53
54
  <CheckCircle2 :size="15" class="shrink-0" />
54
- <span v-if="runId">Run #{{ runId }} started and is executing in the background.</span>
55
- <span v-else>Run started and is executing in the background.</span>
55
+ <span v-if="runId">{{ t("integration.runs.response.async.startedWithId", { id: runId }) }}</span>
56
+ <span v-else>{{ t("integration.runs.response.async.started") }}</span>
56
57
  </div>
57
58
  <div class="mt-3 flex justify-end gap-2">
58
59
  <Button variant="outline" class="h-8 text-sm" @click="emit('update:open', false)">
59
- Close
60
+ {{ t("integration.common.action.close") }}
60
61
  </Button>
61
62
  <Button v-if="runId" class="h-8 text-sm" @click="emit('view-run', runId)">
62
- View run
63
+ {{ t("integration.runs.response.action.viewRun") }}
63
64
  </Button>
64
65
  </div>
65
66
  </template>
@@ -70,13 +71,13 @@ const emit = defineEmits(["update:open", "view-run"]);
70
71
  >{{ JSON.stringify(response, null, 2) }}</pre>
71
72
  <div v-if="runId" class="mt-3 flex justify-end">
72
73
  <Button variant="outline" class="h-8 text-sm" @click="emit('view-run', runId)">
73
- View run
74
+ {{ t("integration.runs.response.action.viewRun") }}
74
75
  </Button>
75
76
  </div>
76
77
  </template>
77
78
 
78
79
  <p v-else class="py-6 text-muted-foreground">
79
- No response.
80
+ {{ t("integration.runs.response.empty") }}
80
81
  </p>
81
82
  </div>
82
83
  </DialogContent>
@@ -14,7 +14,6 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {
14
14
  onChanged?: (() => any) | undefined;
15
15
  }>, {
16
16
  loading: boolean;
17
- emptyText: string;
18
17
  pagination: boolean;
19
18
  showWorkflow: boolean;
20
19
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,16 +1,19 @@
1
1
  <script setup>
2
2
  import { computed, onMounted, onUnmounted, ref } from "vue";
3
3
  import { Badge } from "@revenexx/studio";
4
- import { statusClass } from "../../utils/statusStyle";
4
+ import { RUN_STATUS_WORDS, normalisedRunStatus, runStatusLabel, statusClass } from "../../utils/statusStyle";
5
+ import { TRIGGER_TYPE_WORDS, triggerTypeLabel } from "../../data/integrationNodeCatalog";
5
6
  const props = defineProps({
6
7
  runs: { type: Array, required: true },
7
8
  loading: { type: Boolean, required: false, default: false },
8
- emptyText: { type: String, required: false, default: "No runs yet \u2014 kick off a workflow to populate this view." },
9
+ emptyText: { type: String, required: false },
9
10
  pagination: { type: Boolean, required: false, default: true },
10
11
  showWorkflow: { type: Boolean, required: false, default: true }
11
12
  });
12
13
  const emit = defineEmits(["changed"]);
14
+ const t = useIntegrationsT();
13
15
  const router = useRouter();
16
+ const emptyText = computed(() => props.emptyText ?? t("integration.runs.table.empty"));
14
17
  const now = ref(Date.now());
15
18
  let tick = null;
16
19
  onMounted(() => {
@@ -31,10 +34,10 @@ function formatRelative(iso) {
31
34
  if (!iso) return "\u2014";
32
35
  const ms = Date.now() - new Date(iso).getTime();
33
36
  if (Number.isNaN(ms)) return "\u2014";
34
- if (ms < 6e4) return `${Math.max(1, Math.floor(ms / 1e3))}s ago`;
35
- if (ms < 36e5) return `${Math.floor(ms / 6e4)}m ago`;
36
- if (ms < 864e5) return `${Math.floor(ms / 36e5)}h ago`;
37
- return `${Math.floor(ms / 864e5)}d ago`;
37
+ if (ms < 6e4) return t("integration.common.time.secondsAgo", Math.max(1, Math.floor(ms / 1e3)));
38
+ if (ms < 36e5) return t("integration.common.time.minutesAgo", Math.floor(ms / 6e4));
39
+ if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
40
+ return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
38
41
  }
39
42
  function formatDuration(ms) {
40
43
  if (ms == null) return "\u2014";
@@ -49,16 +52,35 @@ function liveDuration(run) {
49
52
  const end = run.finishedAt ? Date.parse(run.finishedAt) : now.value;
50
53
  return end - start;
51
54
  }
55
+ const filterTabs = computed(() => props.pagination ? [
56
+ {
57
+ key: "status",
58
+ label: t("integration.runs.table.column.status"),
59
+ getter: (r) => normalisedRunStatus(r.status),
60
+ options: RUN_STATUS_WORDS.map((v) => ({ value: v, label: runStatusLabel(v) }))
61
+ },
62
+ {
63
+ key: "trigger",
64
+ label: t("integration.runs.table.column.trigger"),
65
+ getter: (r) => r.triggerType ?? "",
66
+ options: TRIGGER_TYPE_WORDS.map((v) => ({ value: v, label: triggerTypeLabel(v) }))
67
+ }
68
+ ] : void 0);
52
69
  const columns = computed(() => [
53
- { key: "status", label: "Status", width: "100px", sortable: true, sortGetter: (r) => r.status },
54
- ...props.showWorkflow ? [{ key: "workflow", label: "Workflow", sortable: true, sortGetter: (r) => r.workflowName.toLowerCase(), searchGetter: (r) => r.workflowName }] : [],
70
+ // Sorted and searched by the word on screen, not by the raw field: a column
71
+ // with no `searchGetter` is searched through its `sortGetter`, so a German
72
+ // reader typing the status they can see has to find the row that shows it.
73
+ { key: "status", label: t("integration.runs.table.column.status"), width: "100px", sortable: true, sortGetter: (r) => runStatusLabel(r.status) },
74
+ ...props.showWorkflow ? [{ key: "workflow", label: t("integration.runs.table.column.workflow"), sortable: true, sortGetter: (r) => r.workflowName.toLowerCase(), searchGetter: (r) => r.workflowName }] : [],
55
75
  // Fixed floor wide enough for a full UUID, flexible above it — header and
56
76
  // body rows are separate grids, so a content-sized track (max-content)
57
77
  // would misalign them. The flex share also lets the table fill the width.
58
- { key: "id", label: "ID", width: "minmax(280px,1fr)", searchGetter: (r) => `${r.temporalRunId ?? ""} ${r.id}` },
59
- { key: "trigger", label: "Trigger", width: "120px", sortable: true, sortGetter: (r) => r.triggerType },
60
- { key: "started", label: "Started", width: "120px", sortable: true, sortGetter: (r) => r.startedAt },
61
- { key: "duration", label: "Duration", width: "100px", sortable: true, sortGetter: (r) => r.durationMs ?? -1 },
78
+ { key: "id", label: t("integration.runs.table.column.id"), width: "minmax(280px,1fr)", searchGetter: (r) => `${r.temporalRunId ?? ""} ${r.id}` },
79
+ // Same rule as the status column above: the word on screen is what sorts and
80
+ // what a search has to match.
81
+ { key: "trigger", label: t("integration.runs.table.column.trigger"), width: "120px", sortable: true, sortGetter: (r) => triggerTypeLabel(r.triggerType) },
82
+ { key: "started", label: t("integration.runs.table.column.started"), width: "120px", sortable: true, sortGetter: (r) => r.startedAt },
83
+ { key: "duration", label: t("integration.runs.table.column.duration"), width: "100px", sortable: true, sortGetter: (r) => r.durationMs ?? -1 },
62
84
  { key: "actions", label: "", width: "52px" }
63
85
  ]);
64
86
  </script>
@@ -69,8 +91,8 @@ const columns = computed(() => [
69
91
  :columns="columns"
70
92
  :row-key="(r) => r.id"
71
93
  :default-sort="{ column: 'started', direction: 'desc' }"
72
- :search="{ placeholder: 'Search runs\u2026' }"
73
- :filter="true"
94
+ :search="{ placeholder: t('integration.runs.table.search.placeholder') }"
95
+ :filter-tabs="filterTabs"
74
96
  :loading="loading"
75
97
  :pagination="pagination"
76
98
  :empty-text="emptyText"
@@ -78,7 +100,7 @@ const columns = computed(() => [
78
100
  >
79
101
  <template #cell-status="{ row: run }">
80
102
  <Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(run.status)]">
81
- {{ run.status }}
103
+ {{ runStatusLabel(run.status) }}
82
104
  </Badge>
83
105
  </template>
84
106
 
@@ -93,7 +115,7 @@ const columns = computed(() => [
93
115
 
94
116
  <template #cell-trigger="{ row: run }">
95
117
  <span class="inline-flex items-center whitespace-nowrap rounded-md border border-black/[0.06] bg-muted/50 px-1.5 py-0.5 text-xs font-medium uppercase text-muted-foreground">
96
- {{ run.triggerType }}
118
+ {{ triggerTypeLabel(run.triggerType) }}
97
119
  </span>
98
120
  </template>
99
121
 
@@ -14,7 +14,6 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {
14
14
  onChanged?: (() => any) | undefined;
15
15
  }>, {
16
16
  loading: boolean;
17
- emptyText: string;
18
17
  pagination: boolean;
19
18
  showWorkflow: boolean;
20
19
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -5,6 +5,7 @@ import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogHeader,
5
5
  import { useIntegrationsCredentials } from "../../composables/useIntegrationsCredentials";
6
6
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
7
7
  const emit = defineEmits(["created"]);
8
+ const t = useIntegrationsT();
8
9
  const sftp = typeof useConsoleSftp === "function" ? useConsoleSftp() : {
9
10
  accounts: ref([]),
10
11
  loading: ref(false),
@@ -30,7 +31,7 @@ async function importAccount(account) {
30
31
  try {
31
32
  const creds = await sftp.revealPassword(tenant, account.id);
32
33
  if (!creds?.password) {
33
- error.value = "Could not read the account password.";
34
+ error.value = t("integration.credentials.sftpImport.error.readPassword");
34
35
  return;
35
36
  }
36
37
  const created = await create({
@@ -46,7 +47,7 @@ async function importAccount(account) {
46
47
  emit("created", created.id);
47
48
  open.value = false;
48
49
  } catch (err) {
49
- error.value = err instanceof Error ? err.message : "Import failed.";
50
+ error.value = err instanceof Error ? err.message : t("integration.credentials.sftpImport.error.importFailed");
50
51
  } finally {
51
52
  busyId.value = null;
52
53
  }
@@ -57,9 +58,9 @@ async function importAccount(account) {
57
58
  <Dialog v-model:open="open">
58
59
  <DialogContent class="sm:max-w-md">
59
60
  <DialogHeader>
60
- <DialogTitle>Import platform SFTP account</DialogTitle>
61
+ <DialogTitle>{{ t("integration.credentials.sftpImport.title") }}</DialogTitle>
61
62
  <DialogDescription class="text-sm">
62
- Creates an SFTP credential from a managed platform account — host, username and password are copied once. Later password changes must be re-imported.
63
+ {{ t("integration.credentials.sftpImport.description") }}
63
64
  </DialogDescription>
64
65
  </DialogHeader>
65
66
 
@@ -67,7 +68,7 @@ async function importAccount(account) {
67
68
  <div v-for="n in 3" :key="`sk-${n}`" class="h-9 rounded-md bg-black/[0.07] animate-pulse" />
68
69
  </div>
69
70
  <p v-else-if="!activeAccounts.length" class="py-4 text-center text-sm text-muted-foreground">
70
- No active platform SFTP accounts. Create one under Integration Studio → SFTP first.
71
+ {{ t("integration.credentials.sftpImport.empty") }}
71
72
  </p>
72
73
  <div v-else class="max-h-72 space-y-1 overflow-y-auto">
73
74
  <button
@@ -82,7 +83,7 @@ async function importAccount(account) {
82
83
  <span class="block truncate font-mono text-sm">{{ account.login_username }}</span>
83
84
  <span class="block truncate text-xs text-muted-foreground">{{ account.home_directory ?? account.server ?? "" }}</span>
84
85
  </span>
85
- <Badge variant="outline" class="text-xs font-medium uppercase shrink-0">Active</Badge>
86
+ <Badge variant="outline" class="text-xs font-medium uppercase shrink-0">{{ t("integration.credentials.sftpImport.badge.active") }}</Badge>
86
87
  <Loader2 v-if="busyId === account.id" :size="14" class="shrink-0 animate-spin text-muted-foreground" />
87
88
  </button>
88
89
  </div>
@@ -90,7 +91,7 @@ async function importAccount(account) {
90
91
  <p v-if="error" class="text-sm text-rose-600">{{ error }}</p>
91
92
 
92
93
  <div class="flex justify-end">
93
- <Button variant="outline" size="sm" class="h-8 text-sm" @click="open = false">Close</Button>
94
+ <Button variant="outline" size="sm" class="h-8 text-sm" @click="open = false">{{ t("integration.common.action.close") }}</Button>
94
95
  </div>
95
96
  </DialogContent>
96
97
  </Dialog>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { Pulse as Activity, ClockCircle as Clock, Home, Key as KeyRound, Layers, ShieldCheck, ShieldKeyhole as Credentials, Programming as Workflow } from "#solar-icons";
3
3
  import SidebarNav from "@revenexx/studio-shared/components/Shared/SidebarNav.vue";
4
+ const t = useIntegrationsT();
4
5
  const { workflows } = useIntegrationsWorkflows();
5
6
  const { runningCount } = useIntegrationsRuns();
6
7
  const { activeCount: schedulesActive } = useIntegrationsSchedules();
@@ -10,25 +11,28 @@ const sections = computed(() => [
10
11
  {
11
12
  key: "pinned",
12
13
  items: [
13
- { label: "Dashboard", to: "/integrations", icon: Home, exact: true }
14
+ { label: t("integration.nav.item.dashboard"), to: "/integrations", icon: Home, exact: true }
14
15
  ]
15
16
  },
16
17
  {
17
18
  key: "integrations",
18
- label: "Integrations",
19
+ // A rail's section heading is a proper noun and stays English in every UI
20
+ // language. It is still a key, so the decision is written down where a
21
+ // translator will look for it rather than only in a review comment.
22
+ label: t("integration.nav.section.integrations"),
19
23
  items: [
20
- { label: "Workflows", to: "/integrations/workflows", icon: Workflow, badge: workflows.value.length || null },
21
- { label: "Templates", to: "/integrations/templates", icon: Layers },
22
- { label: "Runs", to: "/integrations/runs", icon: Activity, badge: runningCount.value || null },
23
- { label: "Schedules", to: "/integrations/schedules", icon: Clock, badge: schedulesActive.value || null },
24
- { label: "Credentials", to: "/integrations/credentials", icon: Credentials, badge: credentialsTotal.value || null },
25
- { label: "Secrets", to: "/integrations/secrets", icon: KeyRound, badge: secretsTotal.value || null },
26
- { label: "Audit Log", to: "/integrations/audit", icon: ShieldCheck }
24
+ { label: t("integration.nav.item.workflows"), to: "/integrations/workflows", icon: Workflow, badge: workflows.value.length || null },
25
+ { label: t("integration.nav.item.templates"), to: "/integrations/templates", icon: Layers },
26
+ { label: t("integration.nav.item.runs"), to: "/integrations/runs", icon: Activity, badge: runningCount.value || null },
27
+ { label: t("integration.nav.item.schedules"), to: "/integrations/schedules", icon: Clock, badge: schedulesActive.value || null },
28
+ { label: t("integration.nav.item.credentials"), to: "/integrations/credentials", icon: Credentials, badge: credentialsTotal.value || null },
29
+ { label: t("integration.nav.item.secrets"), to: "/integrations/secrets", icon: KeyRound, badge: secretsTotal.value || null },
30
+ { label: t("integration.nav.item.audit"), to: "/integrations/audit", icon: ShieldCheck }
27
31
  ]
28
32
  }
29
33
  ]);
30
34
  </script>
31
35
 
32
36
  <template>
33
- <SidebarNav title="Integration Studio" :icon="Workflow" :sections="sections" minimal />
37
+ <SidebarNav :title="t('integration.nav.title')" :icon="Workflow" :sections="sections" minimal />
34
38
  </template>
@@ -11,13 +11,23 @@ import type { WorkflowSticky } from '../../types/integrations.js';
11
11
  * - NodeToolbar on hover/selection: 7-color pastel palette + delete.
12
12
  * - Hand-rolled bottom-right resize handle (no @vue-flow/node-resizer dep);
13
13
  * size persists once on pointer-up.
14
+ *
15
+ * **Absence of the callbacks is what makes a note read-only** — the run replay and
16
+ * the history preview hand it none, and then it draws no toolbar, no resize handle
17
+ * and cannot be opened for typing (behaviour specs/sticky-notes.md AC-9). They used
18
+ * to be handed no-ops instead, which left every one of those affordances on screen
19
+ * doing nothing: a reader could not tell "not allowed here" from "broken" (PO-284).
20
+ * `GroupFrameView` gates its own toolbar on the same absence.
14
21
  */
15
22
  export interface StickyNodeData {
16
23
  sticky: WorkflowSticky;
17
- /** Persist a partial change into the blob (editor-side, syncs + pushes). */
18
- onUpdate: (patch: Partial<Pick<WorkflowSticky, 'content' | 'size' | 'color'>>) => void;
19
- /** Remove this sticky from the blob (editor-side). */
20
- onRemove: () => void;
24
+ /**
25
+ * Persist a partial change into the blob (editor-side, syncs + pushes).
26
+ * Absent on a read-only canvas — see the note above.
27
+ */
28
+ onUpdate?: (patch: Partial<Pick<WorkflowSticky, 'content' | 'size' | 'color'>>) => void;
29
+ /** Remove this sticky from the blob (editor-side). Absent when read-only. */
30
+ onRemove?: () => void;
21
31
  }
22
32
  type __VLS_Props = {
23
33
  id: string;
@@ -2,7 +2,7 @@
2
2
  import { computed, nextTick, ref, watch } from "vue";
3
3
  import { Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
- import { Trash2 } from "lucide-vue-next";
5
+ import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
6
6
  const props = defineProps({
7
7
  id: { type: String, required: true },
8
8
  data: { type: Object, required: true },
@@ -10,20 +10,22 @@ const props = defineProps({
10
10
  });
11
11
  defineOptions({ inheritAttrs: false });
12
12
  defineEmits(["updateNodeInternals"]);
13
+ const t = useIntegrationsT();
13
14
  const flow = useVueFlow();
14
15
  const STICKY_COLORS = [
15
- { bg: "#fef3c7", border: "#fde68a", name: "Amber" },
16
- { bg: "#ffedd5", border: "#fed7aa", name: "Orange" },
17
- { bg: "#ffe4e6", border: "#fecdd3", name: "Rose" },
18
- { bg: "#dcfce7", border: "#bbf7d0", name: "Green" },
19
- { bg: "#dbeafe", border: "#bfdbfe", name: "Blue" },
20
- { bg: "#ede9fe", border: "#ddd6fe", name: "Violet" },
21
- { bg: "#f5f5f4", border: "#e7e5e4", name: "Gray" }
16
+ { bg: "#fef3c7", border: "#fde68a", nameKey: "integration.canvas.color.amber" },
17
+ { bg: "#ffedd5", border: "#fed7aa", nameKey: "integration.canvas.color.orange" },
18
+ { bg: "#ffe4e6", border: "#fecdd3", nameKey: "integration.canvas.color.rose" },
19
+ { bg: "#dcfce7", border: "#bbf7d0", nameKey: "integration.canvas.color.green" },
20
+ { bg: "#dbeafe", border: "#bfdbfe", nameKey: "integration.canvas.color.blue" },
21
+ { bg: "#ede9fe", border: "#ddd6fe", nameKey: "integration.canvas.color.violet" },
22
+ { bg: "#f5f5f4", border: "#e7e5e4", nameKey: "integration.canvas.color.gray" }
22
23
  ];
23
24
  const color = computed(() => STICKY_COLORS[props.data.sticky.color ?? 0] ?? STICKY_COLORS[0]);
25
+ const editable = computed(() => !!props.data.onUpdate && !!props.data.onRemove);
24
26
  function pickColor(i) {
25
27
  if ((props.data.sticky.color ?? 0) === i) return;
26
- props.data.onUpdate({ color: i });
28
+ props.data.onUpdate?.({ color: i });
27
29
  }
28
30
  const DEFAULT_WIDTH = 240;
29
31
  const DEFAULT_HEIGHT = 160;
@@ -54,13 +56,18 @@ function onResizeMove(e) {
54
56
  function onResizeEnd() {
55
57
  if (!resizing.value) return;
56
58
  resizing.value = false;
57
- props.data.onUpdate({ size: { width: width.value, height: height.value } });
59
+ props.data.onUpdate?.({ size: { width: width.value, height: height.value } });
58
60
  }
59
61
  const editing = ref(false);
60
62
  const draft = ref("");
61
63
  const textareaEl = ref(null);
64
+ function onDoubleClick(event) {
65
+ if (!editable.value) return;
66
+ event.stopPropagation();
67
+ startEdit();
68
+ }
62
69
  function startEdit() {
63
- if (editing.value) return;
70
+ if (!editable.value || editing.value) return;
64
71
  draft.value = props.data.sticky.content;
65
72
  editing.value = true;
66
73
  nextTick(() => textareaEl.value?.focus());
@@ -69,7 +76,7 @@ function commitEdit() {
69
76
  if (!editing.value) return;
70
77
  editing.value = false;
71
78
  if (draft.value !== props.data.sticky.content) {
72
- props.data.onUpdate({ content: draft.value });
79
+ props.data.onUpdate?.({ content: draft.value });
73
80
  }
74
81
  }
75
82
  function onTextareaKeydown(e) {
@@ -91,7 +98,7 @@ function onHoverLeave() {
91
98
  hovered.value = false;
92
99
  }, 250);
93
100
  }
94
- const toolbarVisible = computed(() => !editing.value && (hovered.value || !!props.selected));
101
+ const toolbarVisible = computed(() => editable.value && !editing.value && (hovered.value || !!props.selected));
95
102
  function escapeHtml(s) {
96
103
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
97
104
  }
@@ -133,17 +140,17 @@ function onViewMouseDown(e) {
133
140
  class="size-5 rounded-full border transition hover:scale-110"
134
141
  :class="(data.sticky.color ?? 0) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
135
142
  :style="{ backgroundColor: c.bg, borderColor: c.border }"
136
- :title="c.name"
143
+ :title="t(c.nameKey)"
137
144
  @click="pickColor(i)"
138
145
  />
139
146
  <div class="mx-0.5 h-4 w-px bg-black/10" />
140
147
  <button
141
148
  type="button"
142
149
  class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
143
- title="Delete note"
144
- @click="data.onRemove()"
150
+ :title="t('integration.canvas.sticky.action.delete')"
151
+ @click="data.onRemove?.()"
145
152
  >
146
- <Trash2 :size="13" />
153
+ <TrashBin :size="13" />
147
154
  </button>
148
155
  </div>
149
156
  </NodeToolbar>
@@ -163,7 +170,7 @@ function onViewMouseDown(e) {
163
170
  }"
164
171
  @mouseenter="onHoverEnter"
165
172
  @mouseleave="onHoverLeave"
166
- @dblclick.stop="startEdit"
173
+ @dblclick="onDoubleClick"
167
174
  >
168
175
  <!-- View mode -->
169
176
  <div
@@ -172,7 +179,11 @@ function onViewMouseDown(e) {
172
179
  @mousedown="onViewMouseDown"
173
180
  >
174
181
  <div v-if="data.sticky.content" v-html="rendered" />
175
- <span v-else class="select-none text-stone-400">Double-click to edit</span>
182
+ <!-- The hint is an instruction, not content, so it goes with the rest of the
183
+ affordances: on a read-only canvas it would name the one gesture
184
+ `startEdit` refuses, which is the false affordance AC-9 removes rather
185
+ than a smaller version of it. An empty note there simply reads empty. -->
186
+ <span v-else-if="editable" class="select-none text-stone-400">{{ t("integration.canvas.sticky.state.emptyHint") }}</span>
176
187
  </div>
177
188
 
178
189
  <!-- Edit mode — nodrag/nowheel so typing and scrolling never drag or zoom
@@ -181,8 +192,9 @@ function onViewMouseDown(e) {
181
192
  v-else
182
193
  ref="textareaEl"
183
194
  v-model="draft"
184
- class="nodrag nowheel h-full w-full resize-none bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400"
185
- placeholder="Write a note… (# heading, **bold**, *italic*, `code`, [link](https://…), - list)"
195
+ :aria-label="t('integration.canvas.sticky.textarea.aria')"
196
+ class="nodrag nowheel h-full w-full resize-none bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-stone-500/40"
197
+ :placeholder="t('integration.canvas.sticky.textarea.placeholder')"
186
198
  spellcheck="false"
187
199
  @blur="commitEdit"
188
200
  @keydown="onTextareaKeydown"
@@ -190,10 +202,13 @@ function onViewMouseDown(e) {
190
202
  @dblclick.stop
191
203
  />
192
204
 
193
- <!-- Bottom-right resize handle (hand-rolled; min 150x80) -->
205
+ <!-- Bottom-right resize handle (hand-rolled; min 150x80). Gone on a
206
+ read-only canvas: a handle that cannot resize is the same lie as a bin
207
+ that cannot delete. -->
194
208
  <div
209
+ v-if="editable"
195
210
  class="nodrag absolute bottom-0 right-0 flex size-4 cursor-se-resize items-end justify-end p-0.5 text-stone-400/80"
196
- title="Resize"
211
+ :title="t('integration.canvas.sticky.action.resize')"
197
212
  @pointerdown="onResizeStart"
198
213
  @pointermove="onResizeMove"
199
214
  @pointerup="onResizeEnd"
@@ -11,13 +11,23 @@ import type { WorkflowSticky } from '../../types/integrations.js';
11
11
  * - NodeToolbar on hover/selection: 7-color pastel palette + delete.
12
12
  * - Hand-rolled bottom-right resize handle (no @vue-flow/node-resizer dep);
13
13
  * size persists once on pointer-up.
14
+ *
15
+ * **Absence of the callbacks is what makes a note read-only** — the run replay and
16
+ * the history preview hand it none, and then it draws no toolbar, no resize handle
17
+ * and cannot be opened for typing (behaviour specs/sticky-notes.md AC-9). They used
18
+ * to be handed no-ops instead, which left every one of those affordances on screen
19
+ * doing nothing: a reader could not tell "not allowed here" from "broken" (PO-284).
20
+ * `GroupFrameView` gates its own toolbar on the same absence.
14
21
  */
15
22
  export interface StickyNodeData {
16
23
  sticky: WorkflowSticky;
17
- /** Persist a partial change into the blob (editor-side, syncs + pushes). */
18
- onUpdate: (patch: Partial<Pick<WorkflowSticky, 'content' | 'size' | 'color'>>) => void;
19
- /** Remove this sticky from the blob (editor-side). */
20
- onRemove: () => void;
24
+ /**
25
+ * Persist a partial change into the blob (editor-side, syncs + pushes).
26
+ * Absent on a read-only canvas — see the note above.
27
+ */
28
+ onUpdate?: (patch: Partial<Pick<WorkflowSticky, 'content' | 'size' | 'color'>>) => void;
29
+ /** Remove this sticky from the blob (editor-side). Absent when read-only. */
30
+ onRemove?: () => void;
21
31
  }
22
32
  type __VLS_Props = {
23
33
  id: string;
@@ -0,0 +1,18 @@
1
+ import type { WorkflowTemplate } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ template: WorkflowTemplate;
4
+ /** How much room the surface has. See the comment above; no default on purpose. */
5
+ density: 'full' | 'compact';
6
+ /** Given → the card is a link there. Omitted → a button that emits `select`. */
7
+ to?: string;
8
+ /** This card is the one being acted on: it says so and cannot be pressed again. */
9
+ busy?: boolean;
10
+ /** Another card on the surface is being acted on, so this one is out of reach. */
11
+ disabled?: boolean;
12
+ };
13
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ select: () => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onSelect?: (() => any) | undefined;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ export default _default;