@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
@@ -17,23 +17,24 @@ const props = defineProps({
17
17
  runBusy: { type: Boolean, required: false }
18
18
  });
19
19
  const emit = defineEmits(["action", "update:open"]);
20
+ const t = useIntegrationsT();
20
21
  const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
21
22
  function formatShortcut(tokens) {
22
23
  const parts = tokens.split("+");
23
24
  const out = [];
24
25
  for (const raw of parts) {
25
- const t = raw.toLowerCase();
26
- if (t === "mod") out.push(isMac ? "\u2318" : "Ctrl");
27
- else if (t === "shift") out.push("\u21E7");
28
- else if (t === "alt") out.push(isMac ? "\u2325" : "Alt");
29
- else if (t === "enter") out.push("\u21B5");
30
- else if (t === "backspace") out.push("\u232B");
31
- else if (t === "delete") out.push("Del");
32
- else if (t === "escape") out.push("Esc");
33
- else if (t === "tab") out.push("Tab");
34
- else if (t === "space") out.push("Space");
35
- else if (t === "f2") out.push("F2");
36
- else out.push(t.toUpperCase());
26
+ const t2 = raw.toLowerCase();
27
+ if (t2 === "mod") out.push(isMac ? "\u2318" : "Ctrl");
28
+ else if (t2 === "shift") out.push("\u21E7");
29
+ else if (t2 === "alt") out.push(isMac ? "\u2325" : "Alt");
30
+ else if (t2 === "enter") out.push("\u21B5");
31
+ else if (t2 === "backspace") out.push("\u232B");
32
+ else if (t2 === "delete") out.push("Del");
33
+ else if (t2 === "escape") out.push("Esc");
34
+ else if (t2 === "tab") out.push("Tab");
35
+ else if (t2 === "space") out.push("Space");
36
+ else if (t2 === "f2") out.push("F2");
37
+ else out.push(t2.toUpperCase());
37
38
  }
38
39
  return isMac ? out.join("") : out.join("+");
39
40
  }
@@ -42,19 +43,19 @@ const sections = computed(() => {
42
43
  if (props.kind === "node") {
43
44
  return [
44
45
  [
45
- { id: "open", label: "Open", shortcut: "enter" },
46
- { id: "rename", label: "Rename", shortcut: "f2" }
46
+ { id: "open", label: t("integration.canvas.contextMenu.item.open"), shortcut: "enter" },
47
+ { id: "rename", label: t("integration.canvas.contextMenu.item.rename"), shortcut: "f2" }
47
48
  ],
48
49
  [
49
- { id: "copy", label: "Copy", shortcut: "mod+c", disabled: props.targetIsTrigger },
50
- { id: "duplicate", label: "Duplicate", shortcut: "mod+d", disabled: props.targetIsTrigger }
50
+ { id: "copy", label: t("integration.canvas.contextMenu.item.copy"), shortcut: "mod+c", disabled: props.targetIsTrigger },
51
+ { id: "duplicate", label: t("integration.canvas.contextMenu.item.duplicate"), shortcut: "mod+d", disabled: props.targetIsTrigger }
51
52
  ],
52
53
  [
53
- { id: "run", label: "Run workflow", shortcut: "mod+enter", disabled: props.runBusy },
54
- { id: "tidy-up", label: "Tidy up", shortcut: "shift+alt+t" }
54
+ { id: "run", label: t("integration.canvas.contextMenu.item.runWorkflow"), shortcut: "mod+enter", disabled: props.runBusy },
55
+ { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: "shift+alt+t" }
55
56
  ],
56
57
  [
57
- { id: "delete", label: "Delete", shortcut: "backspace", danger: true }
58
+ { id: "delete", label: t("integration.common.action.delete"), shortcut: "backspace", danger: true }
58
59
  ]
59
60
  ];
60
61
  }
@@ -62,37 +63,37 @@ const sections = computed(() => {
62
63
  const n = props.selectionCount;
63
64
  return [
64
65
  [
65
- { id: "copy", label: `Copy ${n} nodes`, shortcut: "mod+c" },
66
- { id: "duplicate", label: `Duplicate ${n} nodes`, shortcut: "mod+d" }
66
+ { id: "copy", label: t("integration.canvas.contextMenu.item.copyNodes", n), shortcut: "mod+c" },
67
+ { id: "duplicate", label: t("integration.canvas.contextMenu.item.duplicateNodes", n), shortcut: "mod+d" }
67
68
  ],
68
69
  [
69
- { id: "group", label: `Group ${n} nodes`, shortcut: "mod+g" },
70
- { id: "tidy-up", label: "Tidy up", shortcut: "shift+alt+t" },
71
- { id: "select-all", label: "Select all", shortcut: "mod+a", disabled: allSelected.value },
72
- { id: "clear-selection", label: "Clear selection", shortcut: "escape" }
70
+ { id: "group", label: t("integration.canvas.contextMenu.item.groupNodes", n), shortcut: "mod+g" },
71
+ { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: "shift+alt+t" },
72
+ { id: "select-all", label: t("integration.canvas.contextMenu.item.selectAll"), shortcut: "mod+a", disabled: allSelected.value },
73
+ { id: "clear-selection", label: t("integration.canvas.contextMenu.item.clearSelection"), shortcut: "escape" }
73
74
  ],
74
75
  [
75
- { id: "delete", label: `Delete ${n} nodes`, shortcut: "backspace", danger: true }
76
+ { id: "delete", label: t("integration.canvas.contextMenu.item.deleteNodes", n), shortcut: "backspace", danger: true }
76
77
  ]
77
78
  ];
78
79
  }
79
80
  return [
80
81
  [
81
- { id: "add-node", label: "Add node\u2026", shortcut: "tab" },
82
- { id: "add-sticky", label: "Add sticky", shortcut: "shift+s" },
83
- { id: "paste", label: "Paste", shortcut: "mod+v" }
82
+ { id: "add-node", label: t("integration.canvas.contextMenu.item.addNode"), shortcut: "tab" },
83
+ { id: "add-sticky", label: t("integration.canvas.contextMenu.item.addSticky"), shortcut: "shift+s" },
84
+ { id: "paste", label: t("integration.canvas.contextMenu.item.paste"), shortcut: "mod+v" }
84
85
  ],
85
86
  [
86
- { id: "tidy-up", label: "Tidy up", shortcut: "shift+alt+t" },
87
- { id: "fit-view", label: "Fit view", shortcut: "1" }
87
+ { id: "tidy-up", label: t("integration.canvas.contextMenu.item.tidyUp"), shortcut: "shift+alt+t" },
88
+ { id: "fit-view", label: t("integration.canvas.contextMenu.item.fitView"), shortcut: "1" }
88
89
  ],
89
90
  [
90
- { id: "select-all", label: "Select all", shortcut: "mod+a", disabled: allSelected.value || props.totalNodes === 0 }
91
+ { id: "select-all", label: t("integration.canvas.contextMenu.item.selectAll"), shortcut: "mod+a", disabled: allSelected.value || props.totalNodes === 0 }
91
92
  ]
92
93
  ];
93
94
  });
94
95
  const headerLabel = computed(
95
- () => props.kind === "selection" ? `${props.selectionCount} nodes selected` : null
96
+ () => props.kind === "selection" ? t("integration.canvas.contextMenu.header.selectionCount", props.selectionCount) : null
96
97
  );
97
98
  function onSelect(item) {
98
99
  if (item.disabled) return;
@@ -25,14 +25,15 @@ import WorkflowIconPicker from "./WorkflowIconPicker.vue";
25
25
  import WorkflowTagsInput from "./WorkflowTagsInput.vue";
26
26
  import { resolveLocalized } from "../../utils/localize";
27
27
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
28
+ const t = useIntegrationsT();
28
29
  const router = useRouter();
29
30
  const { create, workflows } = useIntegrationsWorkflows();
30
31
  const { templates, loading: templatesLoading, error: templatesError, load: loadTemplates, ensureDetails, instantiate } = useIntegrationsTemplates();
31
32
  const mode = ref("blank");
32
- const modeTabs = [
33
- { key: "blank", label: "Start blank" },
34
- { key: "template", label: "Start from template" }
35
- ];
33
+ const modeTabs = computed(() => [
34
+ { key: "blank", label: t("integration.workflows.create.tab.blank") },
35
+ { key: "template", label: t("integration.workflows.create.tab.template") }
36
+ ]);
36
37
  function iconFor(name2) {
37
38
  if (!name2) return Lucide.Box;
38
39
  return Lucide[name2] || Lucide.Box;
@@ -45,18 +46,18 @@ const tags = ref([]);
45
46
  const icon = ref(null);
46
47
  const tagSuggestions = computed(() => {
47
48
  const all = /* @__PURE__ */ new Set();
48
- for (const wf of workflows.value) for (const t of wf.blob.tags ?? []) all.add(t);
49
+ for (const wf of workflows.value) for (const t2 of wf.blob.tags ?? []) all.add(t2);
49
50
  return [...all].sort();
50
51
  });
51
52
  const submitting = ref(false);
52
53
  const error = ref(null);
53
- const executionModes = [
54
- { key: "async_only", label: "Async only", hint: "Always queued.", icon: "Inbox" },
55
- { key: "sync_only", label: "Sync only", hint: "Waits for the result.", icon: "Timer" },
56
- { key: "caller_decides", label: "Caller decides", hint: "The request chooses.", icon: "SlidersHorizontal" }
57
- ];
58
- const selectedTrigger = computed(() => TRIGGER_TEMPLATES.find((t) => t.type === triggerType.value) ?? null);
59
- const selectedMode = computed(() => executionModes.find((m) => m.key === executionMode.value) ?? executionModes[0]);
54
+ const executionModes = computed(() => [
55
+ { key: "async_only", label: t("integration.workflows.create.executionMode.asyncOnly.label"), hint: t("integration.workflows.create.executionMode.asyncOnly.hint"), icon: "Inbox" },
56
+ { key: "sync_only", label: t("integration.workflows.create.executionMode.syncOnly.label"), hint: t("integration.workflows.create.executionMode.syncOnly.hint"), icon: "Timer" },
57
+ { key: "caller_decides", label: t("integration.workflows.create.executionMode.callerDecides.label"), hint: t("integration.workflows.create.executionMode.callerDecides.hint"), icon: "SlidersHorizontal" }
58
+ ]);
59
+ const selectedTrigger = computed(() => TRIGGER_TEMPLATES.find((tt) => tt.type === triggerType.value) ?? null);
60
+ const selectedMode = computed(() => executionModes.value.find((m) => m.key === executionMode.value) ?? executionModes.value[0]);
60
61
  watch(open, (v) => {
61
62
  if (!v) return;
62
63
  mode.value = "blank";
@@ -72,13 +73,13 @@ watch(open, (v) => {
72
73
  });
73
74
  async function submit() {
74
75
  if (!name.value.trim()) {
75
- error.value = "Name is required.";
76
+ error.value = t("integration.workflows.create.error.nameRequired");
76
77
  return;
77
78
  }
78
79
  submitting.value = true;
79
80
  error.value = null;
80
81
  try {
81
- const tpl = TRIGGER_TEMPLATES.find((t) => t.type === triggerType.value);
82
+ const tpl = TRIGGER_TEMPLATES.find((tt) => tt.type === triggerType.value);
82
83
  const handle = globalThis.crypto?.randomUUID?.() ?? `trg-${Date.now()}`;
83
84
  const blob = {
84
85
  nodeManifestVersion: "v0-draft",
@@ -100,7 +101,7 @@ async function submit() {
100
101
  open.value = false;
101
102
  await router.push(`/integrations/workflows/${created.id}`);
102
103
  } catch (e) {
103
- error.value = e.message || "Could not create the workflow.";
104
+ error.value = e.message || t("integration.workflows.create.error.generic");
104
105
  } finally {
105
106
  submitting.value = false;
106
107
  }
@@ -108,12 +109,12 @@ async function submit() {
108
109
  const instantiatingSlug = ref(null);
109
110
  const templateSearch = ref("");
110
111
  watch(() => templates.value.length, (n) => {
111
- if (n) void ensureDetails(templates.value.map((t) => t.slug));
112
+ if (n) void ensureDetails(templates.value.map((tpl) => tpl.slug));
112
113
  }, { immediate: true });
113
114
  const filteredTemplates = computed(() => {
114
115
  const q = templateSearch.value.trim().toLowerCase();
115
116
  if (!q) return templates.value;
116
- return templates.value.filter((t) => `${t.name} ${t.description} ${t.category}`.toLowerCase().includes(q));
117
+ return templates.value.filter((tpl) => `${tpl.name} ${tpl.description} ${tpl.category}`.toLowerCase().includes(q));
117
118
  });
118
119
  async function useTemplate(slug) {
119
120
  if (instantiatingSlug.value) return;
@@ -124,7 +125,7 @@ async function useTemplate(slug) {
124
125
  open.value = false;
125
126
  await router.push(`/integrations/workflows/${id}`);
126
127
  } catch (e) {
127
- toast.error(e?.message ?? "Could not create the workflow from this template.");
128
+ toast.error(e?.message ?? t("integration.workflows.create.toast.templateError"));
128
129
  } finally {
129
130
  instantiatingSlug.value = null;
130
131
  }
@@ -135,7 +136,7 @@ async function useTemplate(slug) {
135
136
  <Dialog v-model:open="open">
136
137
  <DialogContent class="sm:max-w-4xl">
137
138
  <DialogHeader>
138
- <DialogTitle>New workflow</DialogTitle>
139
+ <DialogTitle>{{ t("integration.workflows.create.dialog.title") }}</DialogTitle>
139
140
  </DialogHeader>
140
141
 
141
142
  <!-- Switcher: the same TabBar the studio surfaces use. -->
@@ -146,33 +147,33 @@ async function useTemplate(slug) {
146
147
  <form v-if="mode === 'blank'" class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
147
148
  <!-- Two-column rows: label rail left, field right. -->
148
149
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
149
- <Label for="wf-name">Name</Label>
150
- <Input id="wf-name" v-model="name" placeholder="Sync orders nightly" class="bg-white" />
150
+ <Label for="wf-name">{{ t("integration.workflows.create.field.name.label") }}</Label>
151
+ <Input id="wf-name" v-model="name" :placeholder="t('integration.workflows.create.field.name.placeholder')" class="bg-white" />
151
152
  </div>
152
153
 
153
154
  <div class="grid grid-cols-[130px_1fr] items-start gap-x-4">
154
- <Label for="wf-desc" class="pt-2.5">Description</Label>
155
+ <Label for="wf-desc" class="pt-2.5">{{ t("integration.workflows.create.field.description.label") }}</Label>
155
156
  <Textarea
156
157
  id="wf-desc"
157
158
  v-model="description"
158
159
  rows="2"
159
- placeholder="Optional: what this workflow does and why it exists."
160
+ :placeholder="t('integration.workflows.create.field.description.placeholder')"
160
161
  class="bg-white"
161
162
  />
162
163
  </div>
163
164
 
164
165
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
165
- <Label for="wf-tags">Tags</Label>
166
+ <Label for="wf-tags">{{ t("integration.workflows.create.field.tags.label") }}</Label>
166
167
  <WorkflowTagsInput v-model="tags" input-id="wf-tags" :suggestions="tagSuggestions" />
167
168
  </div>
168
169
 
169
170
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
170
- <Label id="wf-icon-label">Icon</Label>
171
+ <Label id="wf-icon-label">{{ t("integration.workflows.create.field.icon.label") }}</Label>
171
172
  <WorkflowIconPicker v-model="icon" trigger-labelledby="wf-icon-label" />
172
173
  </div>
173
174
 
174
175
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
175
- <Label for="wf-trigger">Trigger</Label>
176
+ <Label for="wf-trigger">{{ t("integration.workflows.create.field.trigger.label") }}</Label>
176
177
  <Select v-model="triggerType">
177
178
  <SelectTrigger id="wf-trigger" class="w-full bg-white">
178
179
  <SelectValue>
@@ -202,7 +203,7 @@ async function useTemplate(slug) {
202
203
  </div>
203
204
 
204
205
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
205
- <Label for="wf-exec-mode">Execution mode</Label>
206
+ <Label for="wf-exec-mode">{{ t("integration.workflows.create.field.executionMode.label") }}</Label>
206
207
  <Select v-model="executionMode">
207
208
  <SelectTrigger id="wf-exec-mode" class="w-full bg-white">
208
209
  <SelectValue>
@@ -238,10 +239,10 @@ async function useTemplate(slug) {
238
239
  <div class="flex gap-2 pt-1">
239
240
  <Button type="submit" :disabled="submitting" class="h-8 text-sm">
240
241
  <Loader2 v-if="submitting" :size="14" class="animate-spin" />
241
- {{ submitting ? "Creating\u2026" : "Create and open editor" }}
242
+ {{ submitting ? t("integration.workflows.create.action.submitting") : t("integration.workflows.create.action.submit") }}
242
243
  </Button>
243
244
  <Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
244
- Cancel
245
+ {{ t("integration.common.action.cancel") }}
245
246
  </Button>
246
247
  </div>
247
248
  </form>
@@ -253,8 +254,8 @@ async function useTemplate(slug) {
253
254
  <input
254
255
  v-model="templateSearch"
255
256
  type="text"
256
- aria-label="Search templates"
257
- placeholder="Search templates…"
257
+ :aria-label="t('integration.workflows.create.template.search.aria')"
258
+ :placeholder="t('integration.workflows.create.template.search.placeholder')"
258
259
  class="h-8 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"
259
260
  >
260
261
  </div>
@@ -273,10 +274,10 @@ async function useTemplate(slug) {
273
274
  {{ templatesError }}
274
275
  </div>
275
276
  <p v-else-if="!templates.length" class="p-6 text-center text-sm text-muted-foreground">
276
- No templates in the catalogue yet.
277
+ {{ t("integration.workflows.create.template.empty") }}
277
278
  </p>
278
279
  <p v-else-if="!filteredTemplates.length" class="p-6 text-center text-sm text-muted-foreground">
279
- Nothing matches “{{ templateSearch }}”.
280
+ {{ t("integration.workflows.create.template.noMatch", { query: templateSearch }) }}
280
281
  </p>
281
282
 
282
283
  <div v-else class="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
@@ -294,10 +295,10 @@ async function useTemplate(slug) {
294
295
  <div class="flex items-center justify-between pt-1">
295
296
  <NuxtLink to="/integrations/templates" class="inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary" @click="open = false">
296
297
  <Sparkles :size="13" />
297
- Browse the full template catalogue
298
+ {{ t("integration.workflows.create.template.browseAll") }}
298
299
  </NuxtLink>
299
300
  <Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
300
- Cancel
301
+ {{ t("integration.common.action.cancel") }}
301
302
  </Button>
302
303
  </div>
303
304
  </div>
@@ -8,13 +8,14 @@ const props = defineProps({
8
8
  workflow: { type: [Object, null], required: true }
9
9
  });
10
10
  const emit = defineEmits(["duplicated"]);
11
+ const t = useIntegrationsT();
11
12
  const { duplicate } = useIntegrationsWorkflows();
12
13
  const name = ref("");
13
14
  const busy = ref(false);
14
15
  const error = ref(null);
15
16
  watch(open, (o) => {
16
17
  if (o) {
17
- name.value = props.workflow ? `${props.workflow.name} (Copy)` : "";
18
+ name.value = props.workflow ? t("integration.workflows.duplicate.field.name.default", { name: props.workflow.name }) : "";
18
19
  error.value = null;
19
20
  }
20
21
  });
@@ -28,7 +29,7 @@ async function submit() {
28
29
  open.value = false;
29
30
  emit("duplicated", copy);
30
31
  } catch (e) {
31
- error.value = e?.message || "Could not duplicate the workflow.";
32
+ error.value = e?.message || t("integration.workflows.duplicate.error.generic");
32
33
  } finally {
33
34
  busy.value = false;
34
35
  }
@@ -39,23 +40,23 @@ async function submit() {
39
40
  <Dialog v-model:open="open">
40
41
  <DialogContent class="sm:max-w-md">
41
42
  <DialogHeader>
42
- <DialogTitle>Duplicate workflow</DialogTitle>
43
+ <DialogTitle>{{ t("integration.workflows.duplicate.dialog.title") }}</DialogTitle>
43
44
  <DialogDescription class="text-sm">
44
- Creates a paused copy of “{{ props.workflow?.name }}” with all triggers off.
45
+ {{ t("integration.workflows.duplicate.dialog.description", { name: props.workflow?.name ?? "" }) }}
45
46
  </DialogDescription>
46
47
  </DialogHeader>
47
48
 
48
49
  <form class="space-y-2" @submit.prevent="submit">
49
- <label class="text-sm font-medium" for="wf-duplicate-name">Name</label>
50
+ <label class="text-sm font-medium" for="wf-duplicate-name">{{ t("integration.workflows.duplicate.field.name.label") }}</label>
50
51
  <Input id="wf-duplicate-name" v-model="name" autofocus :disabled="busy" />
51
52
  <p v-if="error" class="text-sm text-rose-600">{{ error }}</p>
52
53
  </form>
53
54
 
54
55
  <DialogFooter>
55
- <Button variant="outline" size="sm" class="h-8 text-sm" :disabled="busy" @click="open = false">Cancel</Button>
56
+ <Button variant="outline" size="sm" class="h-8 text-sm" :disabled="busy" @click="open = false">{{ t("integration.common.action.cancel") }}</Button>
56
57
  <Button size="sm" class="h-8 text-sm" :disabled="!canSubmit" @click="submit">
57
58
  <Loader2 v-if="busy" :size="14" class="animate-spin" />
58
- Duplicate
59
+ {{ t("integration.workflows.action.duplicate") }}
59
60
  </Button>
60
61
  </DialogFooter>
61
62
  </DialogContent>
@@ -6,7 +6,8 @@ import {
6
6
  getSmoothStepPath,
7
7
  useVueFlow
8
8
  } from "@vue-flow/core";
9
- import { Plus, Trash2 } from "lucide-vue-next";
9
+ import { Plus } from "lucide-vue-next";
10
+ import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
10
11
  const props = defineProps({
11
12
  id: { type: String, required: true },
12
13
  sourceNode: { type: Object, required: true },
@@ -39,6 +40,7 @@ const props = defineProps({
39
40
  targetX: { type: Number, required: true },
40
41
  targetY: { type: Number, required: true }
41
42
  });
43
+ const t = useIntegrationsT();
42
44
  const flow = useVueFlow();
43
45
  const edgeActions = inject("workflow-edge-actions", null);
44
46
  const centerY = computed(() => {
@@ -145,7 +147,7 @@ function spliceNode() {
145
147
  <button
146
148
  type="button"
147
149
  class="flex size-5 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-foreground"
148
- title="Insert a node into this connection"
150
+ :title="t('integration.canvas.edge.action.insertNode')"
149
151
  @click.stop="spliceNode"
150
152
  >
151
153
  <Plus :size="12" />
@@ -154,10 +156,10 @@ function spliceNode() {
154
156
  <button
155
157
  type="button"
156
158
  class="flex size-5 items-center justify-center rounded text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
157
- title="Delete this connection"
159
+ :title="t('integration.canvas.edge.action.delete')"
158
160
  @click.stop="removeEdge"
159
161
  >
160
- <Trash2 :size="12" />
162
+ <TrashBin :size="12" />
161
163
  </button>
162
164
  </div>
163
165
  </EdgeLabelRenderer>