@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
@@ -20,22 +20,20 @@ import {
20
20
  } from "@revenexx/studio";
21
21
  import { toast } from "vue-sonner";
22
22
  import { TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
23
- import TemplateFlowPreview from "./TemplateFlowPreview.vue";
23
+ import TemplateCard from "./TemplateCard.vue";
24
24
  import WorkflowIconPicker from "./WorkflowIconPicker.vue";
25
25
  import WorkflowTagsInput from "./WorkflowTagsInput.vue";
26
+ import { resolveLocalized } from "../../utils/localize";
26
27
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
28
+ const t = useIntegrationsT();
27
29
  const router = useRouter();
28
30
  const { create, workflows } = useIntegrationsWorkflows();
29
31
  const { templates, loading: templatesLoading, error: templatesError, load: loadTemplates, ensureDetails, instantiate } = useIntegrationsTemplates();
30
32
  const mode = ref("blank");
31
- const modeTabs = [
32
- { key: "blank", label: "Start blank" },
33
- { key: "template", label: "Start from template" }
34
- ];
35
- function loc(v) {
36
- if (!v) return "";
37
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
38
- }
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
+ ]);
39
37
  function iconFor(name2) {
40
38
  if (!name2) return Lucide.Box;
41
39
  return Lucide[name2] || Lucide.Box;
@@ -48,18 +46,18 @@ const tags = ref([]);
48
46
  const icon = ref(null);
49
47
  const tagSuggestions = computed(() => {
50
48
  const all = /* @__PURE__ */ new Set();
51
- 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);
52
50
  return [...all].sort();
53
51
  });
54
52
  const submitting = ref(false);
55
53
  const error = ref(null);
56
- const executionModes = [
57
- { key: "async_only", label: "Async only", hint: "Always queued.", icon: "Inbox" },
58
- { key: "sync_only", label: "Sync only", hint: "Waits for the result.", icon: "Timer" },
59
- { key: "caller_decides", label: "Caller decides", hint: "The request chooses.", icon: "SlidersHorizontal" }
60
- ];
61
- const selectedTrigger = computed(() => TRIGGER_TEMPLATES.find((t) => t.type === triggerType.value) ?? null);
62
- 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]);
63
61
  watch(open, (v) => {
64
62
  if (!v) return;
65
63
  mode.value = "blank";
@@ -75,13 +73,13 @@ watch(open, (v) => {
75
73
  });
76
74
  async function submit() {
77
75
  if (!name.value.trim()) {
78
- error.value = "Name is required.";
76
+ error.value = t("integration.workflows.create.error.nameRequired");
79
77
  return;
80
78
  }
81
79
  submitting.value = true;
82
80
  error.value = null;
83
81
  try {
84
- const tpl = TRIGGER_TEMPLATES.find((t) => t.type === triggerType.value);
82
+ const tpl = TRIGGER_TEMPLATES.find((tt) => tt.type === triggerType.value);
85
83
  const handle = globalThis.crypto?.randomUUID?.() ?? `trg-${Date.now()}`;
86
84
  const blob = {
87
85
  nodeManifestVersion: "v0-draft",
@@ -103,7 +101,7 @@ async function submit() {
103
101
  open.value = false;
104
102
  await router.push(`/integrations/workflows/${created.id}`);
105
103
  } catch (e) {
106
- error.value = e.message || "Could not create the workflow.";
104
+ error.value = e.message || t("integration.workflows.create.error.generic");
107
105
  } finally {
108
106
  submitting.value = false;
109
107
  }
@@ -111,12 +109,12 @@ async function submit() {
111
109
  const instantiatingSlug = ref(null);
112
110
  const templateSearch = ref("");
113
111
  watch(() => templates.value.length, (n) => {
114
- if (n) void ensureDetails(templates.value.map((t) => t.slug));
112
+ if (n) void ensureDetails(templates.value.map((tpl) => tpl.slug));
115
113
  }, { immediate: true });
116
114
  const filteredTemplates = computed(() => {
117
115
  const q = templateSearch.value.trim().toLowerCase();
118
116
  if (!q) return templates.value;
119
- 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));
120
118
  });
121
119
  async function useTemplate(slug) {
122
120
  if (instantiatingSlug.value) return;
@@ -127,7 +125,7 @@ async function useTemplate(slug) {
127
125
  open.value = false;
128
126
  await router.push(`/integrations/workflows/${id}`);
129
127
  } catch (e) {
130
- toast.error(e?.message ?? "Could not create the workflow from this template.");
128
+ toast.error(e?.message ?? t("integration.workflows.create.toast.templateError"));
131
129
  } finally {
132
130
  instantiatingSlug.value = null;
133
131
  }
@@ -138,7 +136,7 @@ async function useTemplate(slug) {
138
136
  <Dialog v-model:open="open">
139
137
  <DialogContent class="sm:max-w-4xl">
140
138
  <DialogHeader>
141
- <DialogTitle>New workflow</DialogTitle>
139
+ <DialogTitle>{{ t("integration.workflows.create.dialog.title") }}</DialogTitle>
142
140
  </DialogHeader>
143
141
 
144
142
  <!-- Switcher: the same TabBar the studio surfaces use. -->
@@ -149,42 +147,42 @@ async function useTemplate(slug) {
149
147
  <form v-if="mode === 'blank'" class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
150
148
  <!-- Two-column rows: label rail left, field right. -->
151
149
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
152
- <Label for="wf-name">Name</Label>
153
- <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" />
154
152
  </div>
155
153
 
156
154
  <div class="grid grid-cols-[130px_1fr] items-start gap-x-4">
157
- <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>
158
156
  <Textarea
159
157
  id="wf-desc"
160
158
  v-model="description"
161
159
  rows="2"
162
- placeholder="Optional: what this workflow does and why it exists."
160
+ :placeholder="t('integration.workflows.create.field.description.placeholder')"
163
161
  class="bg-white"
164
162
  />
165
163
  </div>
166
164
 
167
165
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
168
- <Label>Tags</Label>
169
- <WorkflowTagsInput v-model="tags" :suggestions="tagSuggestions" />
166
+ <Label for="wf-tags">{{ t("integration.workflows.create.field.tags.label") }}</Label>
167
+ <WorkflowTagsInput v-model="tags" input-id="wf-tags" :suggestions="tagSuggestions" />
170
168
  </div>
171
169
 
172
170
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
173
- <Label>Icon</Label>
174
- <WorkflowIconPicker v-model="icon" />
171
+ <Label id="wf-icon-label">{{ t("integration.workflows.create.field.icon.label") }}</Label>
172
+ <WorkflowIconPicker v-model="icon" trigger-labelledby="wf-icon-label" />
175
173
  </div>
176
174
 
177
175
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
178
- <Label>Trigger</Label>
176
+ <Label for="wf-trigger">{{ t("integration.workflows.create.field.trigger.label") }}</Label>
179
177
  <Select v-model="triggerType">
180
- <SelectTrigger class="w-full bg-white">
178
+ <SelectTrigger id="wf-trigger" class="w-full bg-white">
181
179
  <SelectValue>
182
180
  <span class="flex min-w-0 items-center gap-2">
183
181
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
184
182
  <component :is="iconFor(selectedTrigger?.icon)" :size="13" />
185
183
  </span>
186
- <span class="shrink-0 text-sm font-medium">{{ loc(selectedTrigger?.name) }}</span>
187
- <span class="truncate text-sm text-muted-foreground">{{ loc(selectedTrigger?.description) }}</span>
184
+ <span class="shrink-0 text-sm font-medium">{{ resolveLocalized(selectedTrigger?.name) }}</span>
185
+ <span class="truncate text-sm text-muted-foreground">{{ resolveLocalized(selectedTrigger?.description) }}</span>
188
186
  </span>
189
187
  </SelectValue>
190
188
  </SelectTrigger>
@@ -195,8 +193,8 @@ async function useTemplate(slug) {
195
193
  <component :is="iconFor(t.icon)" :size="14" />
196
194
  </span>
197
195
  <span class="min-w-0">
198
- <span class="block text-sm font-medium">{{ loc(t.name) }}</span>
199
- <span class="block text-sm text-muted-foreground">{{ loc(t.description) }}</span>
196
+ <span class="block text-sm font-medium">{{ resolveLocalized(t.name) }}</span>
197
+ <span class="block text-sm text-muted-foreground">{{ resolveLocalized(t.description) }}</span>
200
198
  </span>
201
199
  </span>
202
200
  </SelectItem>
@@ -205,9 +203,9 @@ async function useTemplate(slug) {
205
203
  </div>
206
204
 
207
205
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
208
- <Label>Execution mode</Label>
206
+ <Label for="wf-exec-mode">{{ t("integration.workflows.create.field.executionMode.label") }}</Label>
209
207
  <Select v-model="executionMode">
210
- <SelectTrigger class="w-full bg-white">
208
+ <SelectTrigger id="wf-exec-mode" class="w-full bg-white">
211
209
  <SelectValue>
212
210
  <span class="flex min-w-0 items-center gap-2">
213
211
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
@@ -241,10 +239,10 @@ async function useTemplate(slug) {
241
239
  <div class="flex gap-2 pt-1">
242
240
  <Button type="submit" :disabled="submitting" class="h-8 text-sm">
243
241
  <Loader2 v-if="submitting" :size="14" class="animate-spin" />
244
- {{ submitting ? "Creating\u2026" : "Create and open editor" }}
242
+ {{ submitting ? t("integration.workflows.create.action.submitting") : t("integration.workflows.create.action.submit") }}
245
243
  </Button>
246
244
  <Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
247
- Cancel
245
+ {{ t("integration.common.action.cancel") }}
248
246
  </Button>
249
247
  </div>
250
248
  </form>
@@ -256,7 +254,8 @@ async function useTemplate(slug) {
256
254
  <input
257
255
  v-model="templateSearch"
258
256
  type="text"
259
- placeholder="Search templates…"
257
+ :aria-label="t('integration.workflows.create.template.search.aria')"
258
+ :placeholder="t('integration.workflows.create.template.search.placeholder')"
260
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"
261
260
  >
262
261
  </div>
@@ -275,45 +274,31 @@ async function useTemplate(slug) {
275
274
  {{ templatesError }}
276
275
  </div>
277
276
  <p v-else-if="!templates.length" class="p-6 text-center text-sm text-muted-foreground">
278
- No templates in the catalogue yet.
277
+ {{ t("integration.workflows.create.template.empty") }}
279
278
  </p>
280
279
  <p v-else-if="!filteredTemplates.length" class="p-6 text-center text-sm text-muted-foreground">
281
- Nothing matches “{{ templateSearch }}”.
280
+ {{ t("integration.workflows.create.template.noMatch", { query: templateSearch }) }}
282
281
  </p>
283
282
 
284
283
  <div v-else class="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
285
- <button
284
+ <TemplateCard
286
285
  v-for="tpl in filteredTemplates"
287
286
  :key="tpl.slug"
288
- type="button"
289
- class="flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white text-left transition hover:border-black/20 hover:shadow-sm disabled:opacity-60"
287
+ :template="tpl"
288
+ density="compact"
289
+ :busy="instantiatingSlug === tpl.slug"
290
290
  :disabled="!!instantiatingSlug"
291
- @click="useTemplate(tpl.slug)"
292
- >
293
- <div class="relative h-24 shrink-0 border-b border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:12px_12px]">
294
- <TemplateFlowPreview v-if="tpl.detailLoaded" :blob="tpl.blob" :height="96" class="px-1" />
295
- <div v-else class="flex h-full items-center justify-center">
296
- <Loader2 :size="14" class="animate-spin text-muted-foreground/40" />
297
- </div>
298
- <span class="absolute left-1.5 top-1.5 inline-flex items-center rounded-full bg-slate-100 px-1.5 py-px uppercase text-[11px] font-medium text-slate-700">
299
- {{ tpl.category.charAt(0).toUpperCase() + tpl.category.slice(1) }}
300
- </span>
301
- <Loader2 v-if="instantiatingSlug === tpl.slug" :size="13" class="absolute right-1.5 top-1.5 animate-spin text-muted-foreground" />
302
- </div>
303
- <div class="flex min-h-0 flex-1 flex-col p-3">
304
- <div class="truncate text-sm font-medium">{{ tpl.name }}</div>
305
- <p class="mt-0.5 line-clamp-2 text-sm text-muted-foreground">{{ tpl.description }}</p>
306
- </div>
307
- </button>
291
+ @select="useTemplate(tpl.slug)"
292
+ />
308
293
  </div>
309
294
 
310
295
  <div class="flex items-center justify-between pt-1">
311
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">
312
297
  <Sparkles :size="13" />
313
- Browse the full template catalogue
298
+ {{ t("integration.workflows.create.template.browseAll") }}
314
299
  </NuxtLink>
315
300
  <Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
316
- Cancel
301
+ {{ t("integration.common.action.cancel") }}
317
302
  </Button>
318
303
  </div>
319
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>
@@ -4,10 +4,14 @@ import { type EdgeProps } from '@vue-flow/core';
4
4
  *
5
5
  * - hard right-angle routing (`getSmoothStepPath`, radius 0)
6
6
  * - arrow-head pointing into the target
7
- * - color encodes the *source port kind* — branch outputs (`true`/`false`,
8
- * switch cases) get a soft color, error outputs get rose, defaults stay
9
- * slate
10
- * - optional center label (used by the canvas to print branch names)
7
+ * - colour encodes IMPORTANCE, not meaning (PO-197): the main flow leads in
8
+ * slate, the error/exception path recedes in a paler slate. The semantic
9
+ * colour (green = normal/active output incl. branch & switch cases, rose =
10
+ * error) lives on the source PORT DOT — a small, localised legend — so the
11
+ * connective edges stay calm.
12
+ * - optional center label (used by the canvas to print branch names) — branch
13
+ * routes only, never the error route (promised: `specs/editor-output-paths.md`,
14
+ * reasoning: `docs/editor-path-colors.md`)
11
15
  * - n8n-style midpoint tools on hover/selection: [+] splices a node into
12
16
  * the connection (editor-provided action), [trash] deletes the edge
13
17
  *
@@ -19,6 +23,16 @@ interface Data {
19
23
  kind: EdgeKind;
20
24
  /** Optional human label shown mid-edge (e.g. "true", "cases:paid"). */
21
25
  label?: string;
26
+ /**
27
+ * Fan-out lane: this edge's rank among its source node's ACTUAL outgoing
28
+ * edges (ordered left-to-right by output handle) and how many there are.
29
+ * Siblings leaving a multi-output node (Switch, out/error) would otherwise
30
+ * bend on the same horizontal line and overlap — the renderer staggers the
31
+ * bend height by lane so the horizontal runs sit on distinct levels. A lone
32
+ * edge has `laneCount` 1 and gets no stagger.
33
+ */
34
+ laneIndex?: number;
35
+ laneCount?: number;
22
36
  /**
23
37
  * Run-replay overlay (WorkflowRunReplayPanel): the edge is on the path the
24
38
  * run actually took — drawn blue and thicker. `ahead` marks the part of the
@@ -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,8 +40,22 @@ 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);
46
+ const centerY = computed(() => {
47
+ const n = props.data?.laneCount ?? 1;
48
+ const i = props.data?.laneIndex ?? 0;
49
+ if (n <= 1) return void 0;
50
+ const mid = (props.sourceY + props.targetY) / 2;
51
+ const STEP = 11;
52
+ const span = Math.abs(props.targetY - props.sourceY);
53
+ const dist = Math.abs(i - (n - 1) / 2);
54
+ const tie = i / Math.max(1, n - 1) * (STEP / 3);
55
+ const lift = Math.min(dist * STEP + tie, Math.max(0, span / 2 - span / 6));
56
+ const towardSource = Math.sign(props.sourceY - props.targetY) || -1;
57
+ return mid + towardSource * lift;
58
+ });
44
59
  const path = computed(() => getSmoothStepPath({
45
60
  sourceX: props.sourceX,
46
61
  sourceY: props.sourceY,
@@ -48,15 +63,18 @@ const path = computed(() => getSmoothStepPath({
48
63
  targetX: props.targetX,
49
64
  targetY: props.targetY,
50
65
  targetPosition: props.targetPosition,
51
- borderRadius: 0
66
+ borderRadius: 0,
67
+ centerY: centerY.value
52
68
  }));
69
+ const NEUTRAL = { stroke: "#94a3b8", label: "#475569", bg: "#f1f5f9" };
70
+ const MUTED = { stroke: "#cbd5e1", label: "#94a3b8", bg: "#f8fafc" };
53
71
  const palette = {
54
- default: { stroke: "#94a3b8", label: "#475569", bg: "#f1f5f9" },
55
- success: { stroke: "#10b981", label: "#047857", bg: "#ecfdf5" },
56
- error: { stroke: "#f43f5e", label: "#be123c", bg: "#fff1f2" },
57
- "branch-true": { stroke: "#10b981", label: "#047857", bg: "#ecfdf5" },
58
- "branch-false": { stroke: "#f59e0b", label: "#b45309", bg: "#fffbeb" },
59
- branch: { stroke: "#8b5cf6", label: "#6d28d9", bg: "#f5f3ff" }
72
+ default: NEUTRAL,
73
+ success: NEUTRAL,
74
+ error: MUTED,
75
+ "branch-true": NEUTRAL,
76
+ "branch-false": NEUTRAL,
77
+ branch: NEUTRAL
60
78
  };
61
79
  const color = computed(() => palette[props.data?.kind ?? "default"] ?? palette.default);
62
80
  const isSelected = computed(() => props.selected);
@@ -129,7 +147,7 @@ function spliceNode() {
129
147
  <button
130
148
  type="button"
131
149
  class="flex size-5 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-foreground"
132
- title="Insert a node into this connection"
150
+ :title="t('integration.canvas.edge.action.insertNode')"
133
151
  @click.stop="spliceNode"
134
152
  >
135
153
  <Plus :size="12" />
@@ -138,10 +156,10 @@ function spliceNode() {
138
156
  <button
139
157
  type="button"
140
158
  class="flex size-5 items-center justify-center rounded text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
141
- title="Delete this connection"
159
+ :title="t('integration.canvas.edge.action.delete')"
142
160
  @click.stop="removeEdge"
143
161
  >
144
- <Trash2 :size="12" />
162
+ <TrashBin :size="12" />
145
163
  </button>
146
164
  </div>
147
165
  </EdgeLabelRenderer>
@@ -4,10 +4,14 @@ import { type EdgeProps } from '@vue-flow/core';
4
4
  *
5
5
  * - hard right-angle routing (`getSmoothStepPath`, radius 0)
6
6
  * - arrow-head pointing into the target
7
- * - color encodes the *source port kind* — branch outputs (`true`/`false`,
8
- * switch cases) get a soft color, error outputs get rose, defaults stay
9
- * slate
10
- * - optional center label (used by the canvas to print branch names)
7
+ * - colour encodes IMPORTANCE, not meaning (PO-197): the main flow leads in
8
+ * slate, the error/exception path recedes in a paler slate. The semantic
9
+ * colour (green = normal/active output incl. branch & switch cases, rose =
10
+ * error) lives on the source PORT DOT — a small, localised legend — so the
11
+ * connective edges stay calm.
12
+ * - optional center label (used by the canvas to print branch names) — branch
13
+ * routes only, never the error route (promised: `specs/editor-output-paths.md`,
14
+ * reasoning: `docs/editor-path-colors.md`)
11
15
  * - n8n-style midpoint tools on hover/selection: [+] splices a node into
12
16
  * the connection (editor-provided action), [trash] deletes the edge
13
17
  *
@@ -19,6 +23,16 @@ interface Data {
19
23
  kind: EdgeKind;
20
24
  /** Optional human label shown mid-edge (e.g. "true", "cases:paid"). */
21
25
  label?: string;
26
+ /**
27
+ * Fan-out lane: this edge's rank among its source node's ACTUAL outgoing
28
+ * edges (ordered left-to-right by output handle) and how many there are.
29
+ * Siblings leaving a multi-output node (Switch, out/error) would otherwise
30
+ * bend on the same horizontal line and overlap — the renderer staggers the
31
+ * bend height by lane so the horizontal runs sit on distinct levels. A lone
32
+ * edge has `laneCount` 1 and gets no stagger.
33
+ */
34
+ laneIndex?: number;
35
+ laneCount?: number;
22
36
  /**
23
37
  * Run-replay overlay (WorkflowRunReplayPanel): the edge is on the path the
24
38
  * run actually took — drawn blue and thicker. `ahead` marks the part of the
@@ -8,8 +8,27 @@ type __VLS_Props = {
8
8
  runBusy?: boolean;
9
9
  /** The run is queued behind a bundle build — busy, but worth saying why. */
10
10
  runWaiting?: boolean;
11
+ /**
12
+ * One of the page's own lightboxes is open above the editor (settings,
13
+ * duplicate, run payload, the sync response, the deactivation prompt). The
14
+ * canvas stands its keyboard shortcuts down while one is up — see
15
+ * `canvasHasKeyboard` below. A new page-level dialog belongs in here too.
16
+ */
17
+ overlayOpen?: boolean;
11
18
  };
12
- declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ /**
20
+ * Show a node the user was sent to from outside the canvas — the validation
21
+ * banner's per-node line (PO-209). Centres the card and opens its settings,
22
+ * which is where the fields at fault are.
23
+ *
24
+ * A node inside a collapsed group has no card on the canvas: centring would pan
25
+ * to empty space, so it only opens the settings. The reverse can't happen — an
26
+ * id that is on the canvas is in the blob by construction.
27
+ */
28
+ declare function revealNode(id: string): void;
29
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {
30
+ revealNode: typeof revealNode;
31
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
32
  change: (blob: WorkflowBlob) => any;
14
33
  run: () => any;
15
34
  toggleActive: () => any;