@revenexx/studio-integrations 0.6.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +643 -0
  2. package/README.md +31 -0
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +12 -3
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
  13. package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
  14. package/dist/runtime/components/Integrations/Layout.vue +28 -3
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
  18. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
  19. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
  21. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  23. package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
  24. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  25. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
  26. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
  27. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
  28. package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
  29. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  30. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  31. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  32. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  33. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  34. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  35. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  36. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  37. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  38. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  39. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  40. package/dist/runtime/components/Integrations/StickyNoteView.vue +82 -34
  41. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  42. package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
  43. package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
  44. package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
  45. package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
  46. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
  47. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  48. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
  49. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  50. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  51. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
  52. package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
  53. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
  54. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
  55. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
  56. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
  57. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  58. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
  59. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
  60. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
  61. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
  62. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
  63. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  64. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
  65. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
  66. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  67. package/dist/runtime/composables/integrationsApi.js +4 -2
  68. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  69. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
  70. package/dist/runtime/composables/useIntegrationsCredentials.js +8 -6
  71. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  72. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  73. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  74. package/dist/runtime/composables/useIntegrationsNodes.js +14 -2
  75. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  76. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  77. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  78. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  79. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  80. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
  81. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  82. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  83. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  84. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  85. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  86. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
  87. package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
  88. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
  89. package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
  90. package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
  91. package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
  92. package/dist/runtime/data/integrationNodeCatalog.js +22 -4
  93. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  94. package/dist/runtime/data/integrationTemplates.js +7 -1
  95. package/dist/runtime/data/nodeInputData.d.ts +58 -1
  96. package/dist/runtime/data/nodeInputData.js +30 -5
  97. package/dist/runtime/data/payloadForm.js +2 -1
  98. package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
  99. package/dist/runtime/data/triggerGlyphs.js +11 -0
  100. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  101. package/dist/runtime/i18n/messages/de.json +1224 -0
  102. package/dist/runtime/i18n/messages/en.json +1224 -0
  103. package/dist/runtime/i18n/translate.d.ts +83 -0
  104. package/dist/runtime/i18n/translate.js +41 -0
  105. package/dist/runtime/pages/integrations/audit.vue +52 -34
  106. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  107. package/dist/runtime/pages/integrations/credentials.vue +73 -52
  108. package/dist/runtime/pages/integrations/index.vue +66 -73
  109. package/dist/runtime/pages/integrations/runs.vue +33 -21
  110. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  111. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  112. package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
  113. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  114. package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
  115. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  116. package/dist/runtime/plugins/locale.d.ts +10 -0
  117. package/dist/runtime/plugins/locale.js +6 -0
  118. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  119. package/dist/runtime/plugins/realtime.client.js +4 -0
  120. package/dist/runtime/types/integrations.d.ts +25 -1
  121. package/dist/runtime/utils/cockpitSources.d.ts +22 -3
  122. package/dist/runtime/utils/credentialState.d.ts +30 -2
  123. package/dist/runtime/utils/credentialState.js +26 -11
  124. package/dist/runtime/utils/localize.d.ts +52 -5
  125. package/dist/runtime/utils/localize.js +47 -9
  126. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  127. package/dist/runtime/utils/pollBudget.js +17 -0
  128. package/dist/runtime/utils/schedule.d.ts +0 -25
  129. package/dist/runtime/utils/schedule.js +37 -25
  130. package/dist/runtime/utils/servedPaths.d.ts +22 -0
  131. package/dist/runtime/utils/servedPaths.js +3 -0
  132. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  133. package/dist/runtime/utils/statusStyle.js +9 -1
  134. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  135. package/dist/runtime/utils/validationFindings.js +4 -3
  136. package/dist/types.d.mts +2 -6
  137. package/package.json +3 -2
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One shortcut into a surface the *host* provides — Import & Export, the SFTP
3
+ * server, the event stream, inbound webhooks. None of the four is a page of this
4
+ * module: they are the cockpit's, and this card is the studio's landing page
5
+ * pointing at them.
6
+ *
7
+ * So the card has two states, and which one it is in is not its own decision:
8
+ *
9
+ * - **`available`** — the host has the surface. The card is a link and behaves
10
+ * exactly as it did before PO-223.
11
+ * - **not available** — the card stays on screen, looks the same, and is a button
12
+ * that explains where the surface lives instead of leading into a route the host
13
+ * does not serve. Before PO-223 it was a link into nothing: in the playground
14
+ * and in the standalone dev studio all four led to a 404.
15
+ *
16
+ * **Staying on screen is the point**, and it is the call the managed credential
17
+ * sources already made (`CredentialRevenexxSource.vue`, PO-222): a control that
18
+ * vanishes leaves nothing to read the explanation from, and the reader cannot tell
19
+ * a broken feature from one that belongs somewhere else.
20
+ *
21
+ * **No `aria-label`, on purpose** — like `TemplateCard`, the card takes its name
22
+ * from what it shows. The state is carried by the *role*: "Import & Export,
23
+ * button" instead of "link" is what says to a screen reader that this does not
24
+ * navigate, and it says it without announcing something no sighted reader can see
25
+ * (`specs/accessibility.md` AC-1 names the on-screen text as the source of a name).
26
+ *
27
+ * Who decides `available`, and why it is the router rather than a guess about the
28
+ * host, is `docs/host-surfaces.md`.
29
+ *
30
+ * The two branches draw the same body twice rather than sharing it: `as-child`
31
+ * wants a real element as its child, and the alternatives — a wrapper element
32
+ * around the card, or a second component for eight lines — cost either the grid
33
+ * layout or a file. **Keep the two bodies identical.**
34
+ */
35
+ import type { Component } from 'vue';
36
+ type __VLS_Props = {
37
+ /** What the surface is called, in the reader's language. */
38
+ label: string;
39
+ /** One line on what it is for. Shown in both states. */
40
+ hint: string;
41
+ icon: Component;
42
+ /** Where the surface lives in a host that has it. */
43
+ to: string;
44
+ /** Whether this host has it. See the comment above; no default on purpose. */
45
+ available: boolean;
46
+ /** What the popover says when it does not. */
47
+ note: string;
48
+ };
49
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
50
+ export default _default;
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, resolveComponent } from "vue";
3
- import { RefreshCircle as Loader2, ClockCircle as Clock, Global as Webhook, Play, Bolt as Zap } from "#solar-icons";
3
+ import { RefreshCircle as Loader2 } from "#solar-icons";
4
+ import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
4
5
  import { templateCategoryLabel, templatePackageLabel } from "../../data/integrationTemplates";
5
6
  import TemplateFlowPreview from "./TemplateFlowPreview.vue";
6
7
  const props = defineProps({
@@ -11,13 +12,22 @@ const props = defineProps({
11
12
  disabled: { type: Boolean, required: false }
12
13
  });
13
14
  const emit = defineEmits(["select"]);
15
+ const t = useIntegrationsT();
14
16
  const NuxtLink = resolveComponent("NuxtLink");
15
17
  const full = computed(() => props.density === "full");
16
18
  const banner = computed(
17
19
  () => full.value ? props.template.images.find((i) => i.category === "banner")?.src ?? null : null
18
20
  );
19
- const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
20
- const TRIGGER_LABEL = { schedule: "Scheduled", webhook: "Webhook", manual: "Run on demand", event: "Event-driven" };
21
+ const TRIGGER_LABEL_KEY = {
22
+ schedule: "integration.templates.card.trigger.schedule",
23
+ webhook: "integration.templates.trigger.webhook",
24
+ manual: "integration.templates.trigger.manual",
25
+ event: "integration.templates.card.trigger.event"
26
+ };
27
+ function triggerTypeLabel(type) {
28
+ const key = TRIGGER_LABEL_KEY[type];
29
+ return key ? t(key) : type;
30
+ }
21
31
  function levelDot(level) {
22
32
  return level === "beginner" ? "bg-emerald-500" : level === "intermediate" ? "bg-amber-500" : "bg-rose-500";
23
33
  }
@@ -66,7 +76,7 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
66
76
  <div
67
77
  v-else
68
78
  role="status"
69
- aria-label="Loading the flow"
79
+ :aria-label="t('integration.templates.card.state.loadingFlow')"
70
80
  class="flex h-full items-center justify-center"
71
81
  >
72
82
  <Loader2 :size="full ? 16 : 14" class="animate-spin text-muted-foreground/50" />
@@ -90,12 +100,12 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
90
100
 
91
101
  <div v-if="full" class="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
92
102
  <span
93
- v-for="t in template.triggerTypes"
94
- :key="t"
103
+ v-for="triggerType in template.triggerTypes"
104
+ :key="triggerType"
95
105
  class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase"
96
106
  >
97
- <component :is="TRIGGER_ICON[t] ?? Zap" :size="10" />
98
- {{ TRIGGER_LABEL[t] ?? t }}
107
+ <component :is="glyphForTrigger(triggerType)" :size="10" />
108
+ {{ triggerTypeLabel(triggerType) }}
99
109
  </span>
100
110
  <span class="flex-1" />
101
111
  <span class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase">
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
- import { computed } from "vue";
2
+ import { computed, onMounted } from "vue";
3
3
  import * as Lucide from "lucide-vue-next";
4
+ import { triggerGlyph } from "../../data/triggerGlyphs";
4
5
  import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
5
6
  import { resolveLocalized } from "../../utils/localize";
6
7
  import { describeCron } from "../../utils/schedule";
@@ -11,6 +12,11 @@ const props = defineProps({
11
12
  padding: { type: Number, required: false, default: 36 },
12
13
  showErrorBranches: { type: Boolean, required: false, default: false }
13
14
  });
15
+ const t = useIntegrationsT();
16
+ const { load: loadNodeCatalog } = useIntegrationsNodes();
17
+ onMounted(() => {
18
+ void loadNodeCatalog();
19
+ });
14
20
  const STOP_SLUG = "revenexx:stop-and-error";
15
21
  const NODE_W = 220;
16
22
  const NODE_H = 58;
@@ -21,22 +27,26 @@ const boxes = computed(() => {
21
27
  if (!blob) return [];
22
28
  const out = [];
23
29
  const triggers = blob.triggers ?? [];
24
- triggers.forEach((t, i) => {
25
- const pos = blob.triggerPositions?.[t.id] ?? { x: 40, y: 40 + i * 160 };
26
- const template = TRIGGER_TEMPLATES.find((tt) => tt.type === t.type);
27
- const subtitle = t.type === "schedule" && t.config?.cron ? describeCron(String(t.config.cron)) ?? String(t.config.cron) : t.type === "webhook" && t.config?.path ? String(t.config.path) : resolveLocalized(template?.description ?? "");
30
+ triggers.forEach((t2, i) => {
31
+ const pos = blob.triggerPositions?.[t2.id] ?? { x: 40, y: 40 + i * 160 };
32
+ const template = TRIGGER_TEMPLATES.find((tt) => tt.type === t2.type);
33
+ const subtitle = t2.type === "schedule" && t2.config?.cron ? describeCron(String(t2.config.cron)) ?? String(t2.config.cron) : t2.type === "webhook" && t2.config?.path ? String(t2.config.path) : resolveLocalized(template?.description ?? "");
28
34
  out.push({
29
- key: `t-${t.id}`,
35
+ key: `t-${t2.id}`,
30
36
  kind: "trigger",
31
37
  x: pos.x,
32
38
  y: pos.y,
33
39
  w: TRIG_W,
34
40
  h: TRIG_H,
35
- label: typeof t.name === "string" && t.name || t.type.charAt(0).toUpperCase() + t.type.slice(1),
41
+ label: typeof t2.name === "string" && t2.name || t2.type.charAt(0).toUpperCase() + t2.type.slice(1),
36
42
  subtitle,
37
- type: t.type,
43
+ type: t2.type,
38
44
  manifest: null,
39
- icon: iconFor(template?.icon ?? "Zap")
45
+ // The trigger's glyph from the one map, like every other trigger box. This
46
+ // resolved `TRIGGER_TEMPLATES`' icon name ('Zap', 'Play', …) out of Lucide
47
+ // instead — the same route the palette's tiles took — so the preview drew a
48
+ // Lucide bolt where the card it previews draws Solar's (PO-329).
49
+ icon: triggerGlyph(t2.type)
40
50
  });
41
51
  });
42
52
  blob.nodes.forEach((n, i) => {
@@ -161,7 +171,7 @@ const bounds = computed(() => {
161
171
  >
162
172
  <div class="flex flex-1 items-center gap-2.5 px-3 py-2">
163
173
  <span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-emerald-500 text-white shadow-sm">
164
- <component :is="b.icon" :size="15" :stroke-width="2.5" />
174
+ <component :is="b.icon" :size="15" />
165
175
  </span>
166
176
  <div class="min-w-0 flex-1">
167
177
  <div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
@@ -169,7 +179,7 @@ const bounds = computed(() => {
169
179
  </div>
170
180
  </div>
171
181
  <div class="rounded-b-md border-t border-emerald-500/15 bg-emerald-500/[0.04] px-3 py-1 text-mono font-semibold text-emerald-700/80">
172
- Trigger · {{ b.type }}
182
+ {{ t("integration.templates.preview.triggerLabel") }} · {{ b.type }}
173
183
  </div>
174
184
  </div>
175
185
 
@@ -192,7 +202,7 @@ const bounds = computed(() => {
192
202
  </foreignObject>
193
203
  </svg>
194
204
  <div v-else class="flex h-full items-center justify-center text-sm text-muted-foreground">
195
- <slot name="empty">Preview loading…</slot>
205
+ <slot name="empty">{{ t("integration.templates.preview.state.loading") }}</slot>
196
206
  </div>
197
207
  </div>
198
208
  </template>
@@ -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;
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
- import { RefreshCircle as Loader2, MagicStick as Sparkles } from "#solar-icons";
3
+ import { RefreshCircle as Loader2, Magnifier as Search, MagicStick as Sparkles } from "#solar-icons";
4
+ import { triggerGlyph } from "../../data/triggerGlyphs";
4
5
  import * as Lucide from "lucide-vue-next";
5
6
  import {
6
7
  Badge,
@@ -25,14 +26,15 @@ import WorkflowIconPicker from "./WorkflowIconPicker.vue";
25
26
  import WorkflowTagsInput from "./WorkflowTagsInput.vue";
26
27
  import { resolveLocalized } from "../../utils/localize";
27
28
  const open = defineModel("open", { type: Boolean, ...{ default: false } });
29
+ const t = useIntegrationsT();
28
30
  const router = useRouter();
29
31
  const { create, workflows } = useIntegrationsWorkflows();
30
32
  const { templates, loading: templatesLoading, error: templatesError, load: loadTemplates, ensureDetails, instantiate } = useIntegrationsTemplates();
31
33
  const mode = ref("blank");
32
- const modeTabs = [
33
- { key: "blank", label: "Start blank" },
34
- { key: "template", label: "Start from template" }
35
- ];
34
+ const modeTabs = computed(() => [
35
+ { key: "blank", label: t("integration.workflows.create.tab.blank") },
36
+ { key: "template", label: t("integration.workflows.create.tab.template") }
37
+ ]);
36
38
  function iconFor(name2) {
37
39
  if (!name2) return Lucide.Box;
38
40
  return Lucide[name2] || Lucide.Box;
@@ -45,18 +47,18 @@ const tags = ref([]);
45
47
  const icon = ref(null);
46
48
  const tagSuggestions = computed(() => {
47
49
  const all = /* @__PURE__ */ new Set();
48
- for (const wf of workflows.value) for (const t of wf.blob.tags ?? []) all.add(t);
50
+ for (const wf of workflows.value) for (const t2 of wf.blob.tags ?? []) all.add(t2);
49
51
  return [...all].sort();
50
52
  });
51
53
  const submitting = ref(false);
52
54
  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]);
55
+ const executionModes = computed(() => [
56
+ { key: "async_only", label: t("integration.workflows.create.executionMode.asyncOnly.label"), hint: t("integration.workflows.create.executionMode.asyncOnly.hint"), icon: "Inbox" },
57
+ { key: "sync_only", label: t("integration.workflows.create.executionMode.syncOnly.label"), hint: t("integration.workflows.create.executionMode.syncOnly.hint"), icon: "Timer" },
58
+ { key: "caller_decides", label: t("integration.workflows.create.executionMode.callerDecides.label"), hint: t("integration.workflows.create.executionMode.callerDecides.hint"), icon: "SlidersHorizontal" }
59
+ ]);
60
+ const selectedTrigger = computed(() => TRIGGER_TEMPLATES.find((tt) => tt.type === triggerType.value) ?? null);
61
+ const selectedMode = computed(() => executionModes.value.find((m) => m.key === executionMode.value) ?? executionModes.value[0]);
60
62
  watch(open, (v) => {
61
63
  if (!v) return;
62
64
  mode.value = "blank";
@@ -72,13 +74,13 @@ watch(open, (v) => {
72
74
  });
73
75
  async function submit() {
74
76
  if (!name.value.trim()) {
75
- error.value = "Name is required.";
77
+ error.value = t("integration.workflows.create.error.nameRequired");
76
78
  return;
77
79
  }
78
80
  submitting.value = true;
79
81
  error.value = null;
80
82
  try {
81
- const tpl = TRIGGER_TEMPLATES.find((t) => t.type === triggerType.value);
83
+ const tpl = TRIGGER_TEMPLATES.find((tt) => tt.type === triggerType.value);
82
84
  const handle = globalThis.crypto?.randomUUID?.() ?? `trg-${Date.now()}`;
83
85
  const blob = {
84
86
  nodeManifestVersion: "v0-draft",
@@ -100,7 +102,7 @@ async function submit() {
100
102
  open.value = false;
101
103
  await router.push(`/integrations/workflows/${created.id}`);
102
104
  } catch (e) {
103
- error.value = e.message || "Could not create the workflow.";
105
+ error.value = e.message || t("integration.workflows.create.error.generic");
104
106
  } finally {
105
107
  submitting.value = false;
106
108
  }
@@ -108,12 +110,12 @@ async function submit() {
108
110
  const instantiatingSlug = ref(null);
109
111
  const templateSearch = ref("");
110
112
  watch(() => templates.value.length, (n) => {
111
- if (n) void ensureDetails(templates.value.map((t) => t.slug));
113
+ if (n) void ensureDetails(templates.value.map((tpl) => tpl.slug));
112
114
  }, { immediate: true });
113
115
  const filteredTemplates = computed(() => {
114
116
  const q = templateSearch.value.trim().toLowerCase();
115
117
  if (!q) return templates.value;
116
- return templates.value.filter((t) => `${t.name} ${t.description} ${t.category}`.toLowerCase().includes(q));
118
+ return templates.value.filter((tpl) => `${tpl.name} ${tpl.description} ${tpl.category}`.toLowerCase().includes(q));
117
119
  });
118
120
  async function useTemplate(slug) {
119
121
  if (instantiatingSlug.value) return;
@@ -124,7 +126,7 @@ async function useTemplate(slug) {
124
126
  open.value = false;
125
127
  await router.push(`/integrations/workflows/${id}`);
126
128
  } catch (e) {
127
- toast.error(e?.message ?? "Could not create the workflow from this template.");
129
+ toast.error(e?.message ?? t("integration.workflows.create.toast.templateError"));
128
130
  } finally {
129
131
  instantiatingSlug.value = null;
130
132
  }
@@ -135,7 +137,7 @@ async function useTemplate(slug) {
135
137
  <Dialog v-model:open="open">
136
138
  <DialogContent class="sm:max-w-4xl">
137
139
  <DialogHeader>
138
- <DialogTitle>New workflow</DialogTitle>
140
+ <DialogTitle>{{ t("integration.workflows.create.dialog.title") }}</DialogTitle>
139
141
  </DialogHeader>
140
142
 
141
143
  <!-- Switcher: the same TabBar the studio surfaces use. -->
@@ -146,39 +148,39 @@ async function useTemplate(slug) {
146
148
  <form v-if="mode === 'blank'" class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
147
149
  <!-- Two-column rows: label rail left, field right. -->
148
150
  <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" />
151
+ <Label for="wf-name">{{ t("integration.workflows.create.field.name.label") }}</Label>
152
+ <Input id="wf-name" v-model="name" :placeholder="t('integration.workflows.create.field.name.placeholder')" class="bg-white" />
151
153
  </div>
152
154
 
153
155
  <div class="grid grid-cols-[130px_1fr] items-start gap-x-4">
154
- <Label for="wf-desc" class="pt-2.5">Description</Label>
156
+ <Label for="wf-desc" class="pt-2.5">{{ t("integration.workflows.create.field.description.label") }}</Label>
155
157
  <Textarea
156
158
  id="wf-desc"
157
159
  v-model="description"
158
160
  rows="2"
159
- placeholder="Optional: what this workflow does and why it exists."
161
+ :placeholder="t('integration.workflows.create.field.description.placeholder')"
160
162
  class="bg-white"
161
163
  />
162
164
  </div>
163
165
 
164
166
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
165
- <Label for="wf-tags">Tags</Label>
167
+ <Label for="wf-tags">{{ t("integration.workflows.create.field.tags.label") }}</Label>
166
168
  <WorkflowTagsInput v-model="tags" input-id="wf-tags" :suggestions="tagSuggestions" />
167
169
  </div>
168
170
 
169
171
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
170
- <Label id="wf-icon-label">Icon</Label>
172
+ <Label id="wf-icon-label">{{ t("integration.workflows.create.field.icon.label") }}</Label>
171
173
  <WorkflowIconPicker v-model="icon" trigger-labelledby="wf-icon-label" />
172
174
  </div>
173
175
 
174
176
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
175
- <Label for="wf-trigger">Trigger</Label>
177
+ <Label for="wf-trigger">{{ t("integration.workflows.create.field.trigger.label") }}</Label>
176
178
  <Select v-model="triggerType">
177
179
  <SelectTrigger id="wf-trigger" class="w-full bg-white">
178
180
  <SelectValue>
179
181
  <span class="flex min-w-0 items-center gap-2">
180
182
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
181
- <component :is="iconFor(selectedTrigger?.icon)" :size="13" />
183
+ <component :is="triggerGlyph(selectedTrigger?.type)" :size="13" />
182
184
  </span>
183
185
  <span class="shrink-0 text-sm font-medium">{{ resolveLocalized(selectedTrigger?.name) }}</span>
184
186
  <span class="truncate text-sm text-muted-foreground">{{ resolveLocalized(selectedTrigger?.description) }}</span>
@@ -189,7 +191,7 @@ async function useTemplate(slug) {
189
191
  <SelectItem v-for="t in TRIGGER_TEMPLATES" :key="t.id" :value="t.type">
190
192
  <span class="flex items-center gap-2.5 py-0.5">
191
193
  <span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-emerald-100 text-emerald-600">
192
- <component :is="iconFor(t.icon)" :size="14" />
194
+ <component :is="triggerGlyph(t.type)" :size="14" />
193
195
  </span>
194
196
  <span class="min-w-0">
195
197
  <span class="block text-sm font-medium">{{ resolveLocalized(t.name) }}</span>
@@ -202,7 +204,7 @@ async function useTemplate(slug) {
202
204
  </div>
203
205
 
204
206
  <div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
205
- <Label for="wf-exec-mode">Execution mode</Label>
207
+ <Label for="wf-exec-mode">{{ t("integration.workflows.create.field.executionMode.label") }}</Label>
206
208
  <Select v-model="executionMode">
207
209
  <SelectTrigger id="wf-exec-mode" class="w-full bg-white">
208
210
  <SelectValue>
@@ -238,10 +240,10 @@ async function useTemplate(slug) {
238
240
  <div class="flex gap-2 pt-1">
239
241
  <Button type="submit" :disabled="submitting" class="h-8 text-sm">
240
242
  <Loader2 v-if="submitting" :size="14" class="animate-spin" />
241
- {{ submitting ? "Creating\u2026" : "Create and open editor" }}
243
+ {{ submitting ? t("integration.workflows.create.action.submitting") : t("integration.workflows.create.action.submit") }}
242
244
  </Button>
243
245
  <Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
244
- Cancel
246
+ {{ t("integration.common.action.cancel") }}
245
247
  </Button>
246
248
  </div>
247
249
  </form>
@@ -249,12 +251,12 @@ async function useTemplate(slug) {
249
251
  <!-- ── From template ─────────────────────────────────────────────────── -->
250
252
  <div v-else class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1">
251
253
  <div class="relative">
252
- <Lucide.Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
254
+ <Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
253
255
  <input
254
256
  v-model="templateSearch"
255
257
  type="text"
256
- aria-label="Search templates"
257
- placeholder="Search templates…"
258
+ :aria-label="t('integration.workflows.create.template.search.aria')"
259
+ :placeholder="t('integration.workflows.create.template.search.placeholder')"
258
260
  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
261
  >
260
262
  </div>
@@ -273,10 +275,10 @@ async function useTemplate(slug) {
273
275
  {{ templatesError }}
274
276
  </div>
275
277
  <p v-else-if="!templates.length" class="p-6 text-center text-sm text-muted-foreground">
276
- No templates in the catalogue yet.
278
+ {{ t("integration.workflows.create.template.empty") }}
277
279
  </p>
278
280
  <p v-else-if="!filteredTemplates.length" class="p-6 text-center text-sm text-muted-foreground">
279
- Nothing matches “{{ templateSearch }}”.
281
+ {{ t("integration.workflows.create.template.noMatch", { query: templateSearch }) }}
280
282
  </p>
281
283
 
282
284
  <div v-else class="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
@@ -294,10 +296,10 @@ async function useTemplate(slug) {
294
296
  <div class="flex items-center justify-between pt-1">
295
297
  <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
298
  <Sparkles :size="13" />
297
- Browse the full template catalogue
299
+ {{ t("integration.workflows.create.template.browseAll") }}
298
300
  </NuxtLink>
299
301
  <Button type="button" variant="outline" class="h-8 text-sm" @click="open = false">
300
- Cancel
302
+ {{ t("integration.common.action.cancel") }}
301
303
  </Button>
302
304
  </div>
303
305
  </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>