@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
@@ -4,6 +4,22 @@ import '@vue-flow/minimap/dist/style.css';
4
4
  import type { Workflow, WorkflowBlob } from '../../types/integrations.js';
5
5
  type __VLS_Props = {
6
6
  workflow: Workflow;
7
+ /**
8
+ * Unsaved work the page is still holding for this workflow, read ONCE, at
9
+ * mount (behaviour specs/workflow-save.md).
10
+ *
11
+ * The editor is mounted per tab, so a trip to the Runs tab and back seeded a
12
+ * fresh working copy from the SAVED blob while the page still held the draft:
13
+ * the canvas showed one graph, the header said "Unsaved changes" about
14
+ * another, and Save wrote the one nobody could see. Seeding from the draft is
15
+ * what makes a move inside the workflow keep the work.
16
+ *
17
+ * Deliberately not watched. The page's draft is a copy of what this editor
18
+ * last emitted, so following it would re-seed the canvas — and drop the
19
+ * selection, the undo stack and any open dialog with it — on the user's own
20
+ * keystrokes.
21
+ */
22
+ draft?: WorkflowBlob | null;
7
23
  /** Busy flag for the control-pane Play button (a run is being triggered). */
8
24
  runBusy?: boolean;
9
25
  /** The run is queued behind a bundle build — busy, but worth saying why. */
@@ -3,6 +3,12 @@ import '@vue-flow/core/dist/theme-default.css';
3
3
  import type { Workflow } from '../../types/integrations.js';
4
4
  type __VLS_Props = {
5
5
  workflow: Workflow;
6
+ /**
7
+ * The editor has unsaved work the page is holding. Nothing here shows it — the
8
+ * canvas behind this tab is not mounted — but a restore is what destroys it, so
9
+ * the question this panel asks has to name it (specs/workflow-save.md).
10
+ */
11
+ draftPending?: boolean;
6
12
  };
7
13
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
14
  restored: () => any;
@@ -20,10 +20,12 @@ import NodeDynamicOutputsResolver from "./NodeDynamicOutputsResolver.vue";
20
20
  import { findManifest } from "../../data/integrationNodeCatalog";
21
21
  import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
22
22
  const props = defineProps({
23
- workflow: { type: Object, required: true }
23
+ workflow: { type: Object, required: true },
24
+ draftPending: { type: Boolean, required: false }
24
25
  });
25
26
  const emit = defineEmits(["restored"]);
26
27
  const revisionsApi = useIntegrationsRevisions();
28
+ const t = useIntegrationsT();
27
29
  const entries = ref([]);
28
30
  const page = ref(1);
29
31
  const lastPage = ref(1);
@@ -110,7 +112,7 @@ function buildPreview(blob) {
110
112
  const nodePos = (id) => {
111
113
  const wn = blob.nodes.find((n) => n.id === id);
112
114
  if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
113
- const ti = blob.triggers.findIndex((t) => t.id === id);
115
+ const ti = blob.triggers.findIndex((t2) => t2.id === id);
114
116
  if (ti >= 0) return triggerPos(id, ti);
115
117
  return { x: 0, y: 0 };
116
118
  };
@@ -149,16 +151,16 @@ function buildPreview(blob) {
149
151
  position: { x: s.position.x, y: s.position.y },
150
152
  zIndex: -100 + i,
151
153
  draggable: false,
152
- data: { sticky: s, onUpdate: () => {
153
- }, onRemove: () => {
154
- } }
154
+ // No callbacks: that absence is what makes the note read-only — it then
155
+ // draws no toolbar, no resize handle and does not open for typing (PO-284).
156
+ data: { sticky: s }
155
157
  })),
156
- ...blob.triggers.map((t, i) => ({
157
- id: t.id,
158
+ ...blob.triggers.map((t2, i) => ({
159
+ id: t2.id,
158
160
  type: "trigger",
159
- position: triggerPos(t.id, i),
161
+ position: triggerPos(t2.id, i),
160
162
  draggable: false,
161
- data: t
163
+ data: t2
162
164
  })),
163
165
  ...blob.nodes.map((n) => ({
164
166
  id: n.id,
@@ -218,23 +220,24 @@ async function select(revision) {
218
220
  buildPreview(detail.blob);
219
221
  }
220
222
  } catch (e) {
221
- toast.error(e?.message ?? "Could not load the revision.");
223
+ toast.error(e?.message ?? t("integration.history.toast.loadRevisionFailed"));
222
224
  } finally {
223
225
  detailLoading.value = false;
224
226
  }
225
227
  }
226
228
  async function restoreRevision(rev) {
227
- if (!confirm(`Restore revision ${rev.revision}? The current version is archived first, then the workflow is rebuilt from this snapshot.`)) return;
229
+ const question = props.draftPending ? t("integration.history.confirm.restoreWithDraft", { revision: rev.revision }) : t("integration.history.confirm.restore", { revision: rev.revision });
230
+ if (!confirm(question)) return;
228
231
  restoring.value = rev.revision;
229
232
  try {
230
233
  await revisionsApi.restore(props.workflow.id, rev.revision);
231
- toast.success(`Revision ${rev.revision} restored`);
234
+ toast.success(t("integration.history.toast.restored", { revision: rev.revision }));
232
235
  detailCache.clear();
233
236
  emit("restored");
234
237
  await loadList(true);
235
238
  void select(null);
236
239
  } catch (e) {
237
- toast.error(e?.message ?? "Could not restore the revision.");
240
+ toast.error(e?.message ?? t("integration.history.toast.restoreFailed"));
238
241
  } finally {
239
242
  restoring.value = null;
240
243
  }
@@ -259,7 +262,7 @@ const nodeTypes = {
259
262
  const edgeTypes = {
260
263
  flow: markRaw(WorkflowEdgeView)
261
264
  };
262
- const previewTitle = computed(() => selected.value === null ? `Current version \xB7 revision ${props.workflow.revision}` : `Revision ${selected.value} (read-only)`);
265
+ const previewTitle = computed(() => selected.value === null ? t("integration.history.preview.title.current", { revision: props.workflow.revision }) : t("integration.history.preview.title.revision", { revision: selected.value }));
263
266
  onMounted(() => {
264
267
  void loadList(true);
265
268
  void select(null);
@@ -311,9 +314,9 @@ onMounted(() => {
311
314
  <!-- Revision list -->
312
315
  <aside class="flex w-80 shrink-0 flex-col border-l border-black/[0.06] bg-white">
313
316
  <div class="border-b border-black/[0.06] px-3 py-2">
314
- <div class="text-sm font-semibold">Version history</div>
317
+ <div class="text-sm font-semibold">{{ t("integration.history.heading.title") }}</div>
315
318
  <p class="text-sm text-muted-foreground">
316
- Every save archives the previous revision; the newest one is what runs while the workflow is active.
319
+ {{ t("integration.history.heading.description") }}
317
320
  </p>
318
321
  </div>
319
322
 
@@ -327,17 +330,17 @@ onMounted(() => {
327
330
  >
328
331
  <div class="flex items-center justify-between gap-2">
329
332
  <span class="text-sm font-medium tabular-nums">{{ formatDateTime(workflow.updated_at) }}</span>
330
- <Badge variant="outline" class="text-xs font-medium uppercase border-emerald-500/30 text-emerald-700">Current</Badge>
333
+ <Badge variant="outline" class="text-xs font-medium uppercase border-emerald-500/30 text-emerald-700">{{ t("integration.history.badge.current") }}</Badge>
331
334
  <Badge
332
335
  variant="outline"
333
336
  class="text-xs font-medium uppercase"
334
337
  :class="workflow.active ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700' : 'border-black/[0.08] text-muted-foreground'"
335
- :title="workflow.active ? 'Triggers are armed; this revision is what runs.' : 'Workflow is paused; activating runs this revision.'"
338
+ :title="workflow.active ? t('integration.history.badge.activeHint') : t('integration.history.badge.pausedHint')"
336
339
  >
337
- {{ workflow.active ? "Active" : "Paused" }}
340
+ {{ workflow.active ? t("integration.history.badge.active") : t("integration.history.badge.paused") }}
338
341
  </Badge>
339
342
  </div>
340
- <div class="mt-0.5 text-sm text-muted-foreground">Revision {{ workflow.revision }}</div>
343
+ <div class="mt-0.5 text-sm text-muted-foreground">{{ t("integration.history.list.revisionLabel", { revision: workflow.revision }) }}</div>
341
344
  </button>
342
345
 
343
346
  <!-- Archived revisions -->
@@ -351,7 +354,7 @@ onMounted(() => {
351
354
  <div class="flex items-center justify-between gap-2">
352
355
  <span class="text-sm font-medium tabular-nums">{{ formatDateTime(rev.archived_at) }}</span>
353
356
  </div>
354
- <div class="mt-0.5 text-sm text-muted-foreground">Revision {{ rev.revision }}</div>
357
+ <div class="mt-0.5 text-sm text-muted-foreground">{{ t("integration.history.list.revisionLabel", { revision: rev.revision }) }}</div>
355
358
  </button>
356
359
  <div class="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 transition group-hover:opacity-100">
357
360
  <Button
@@ -362,13 +365,13 @@ onMounted(() => {
362
365
  @click.stop="restoreRevision(rev)"
363
366
  >
364
367
  <Loader2 v-if="restoring === rev.revision" :size="11" class="mr-1 animate-spin" />
365
- Restore
368
+ {{ t("integration.history.action.restore") }}
366
369
  </Button>
367
370
  </div>
368
371
  </div>
369
372
 
370
373
  <p v-if="!entries.length && !listLoading" class="px-2 py-6 text-center text-sm text-muted-foreground">
371
- No archived revisions yet — save a change to create the first snapshot.
374
+ {{ t("integration.history.state.empty") }}
372
375
  </p>
373
376
  <p v-if="revisionsApi.error.value" class="px-2 py-2 text-sm text-rose-600">
374
377
  {{ revisionsApi.error.value }}
@@ -393,7 +396,7 @@ onMounted(() => {
393
396
  class="w-full text-sm"
394
397
  @click="loadMore"
395
398
  >
396
- Load older revisions
399
+ {{ t("integration.history.action.loadMore") }}
397
400
  </Button>
398
401
  </div>
399
402
  </aside>
@@ -3,6 +3,12 @@ import '@vue-flow/core/dist/theme-default.css';
3
3
  import type { Workflow } from '../../types/integrations.js';
4
4
  type __VLS_Props = {
5
5
  workflow: Workflow;
6
+ /**
7
+ * The editor has unsaved work the page is holding. Nothing here shows it — the
8
+ * canvas behind this tab is not mounted — but a restore is what destroys it, so
9
+ * the question this panel asks has to name it (specs/workflow-save.md).
10
+ */
11
+ draftPending?: boolean;
6
12
  };
7
13
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
14
  restored: () => any;
@@ -8,6 +8,7 @@ const model = defineModel({ type: [Object, null], ...{ default: null } });
8
8
  const props = defineProps({
9
9
  triggerLabelledby: { type: String, required: false }
10
10
  });
11
+ const t = useIntegrationsT();
11
12
  const valueId = useId();
12
13
  const triggerLabelledby = computed(
13
14
  () => props.triggerLabelledby ? `${props.triggerLabelledby} ${valueId}` : void 0
@@ -26,10 +27,10 @@ function isImage(id) {
26
27
  return (imageIdFromSrc(model.value.src) ?? model.value.src) === id;
27
28
  }
28
29
  const selectedLabel = computed(() => {
29
- if (!model.value) return "Default icon";
30
- if (model.value.kind === "solar") return model.value.name ?? "Icon";
30
+ if (!model.value) return t("integration.workflows.iconPicker.label.default");
31
+ if (model.value.kind === "solar") return model.value.name ?? t("integration.workflows.iconPicker.label.iconFallback");
31
32
  const id = imageIdFromSrc(model.value.src) ?? model.value.src;
32
- return logos.value.find((l) => l.id === id)?.label ?? "Image";
33
+ return logos.value.find((l) => l.id === id)?.label ?? t("integration.workflows.iconPicker.label.imageFallback");
33
34
  });
34
35
  </script>
35
36
 
@@ -57,8 +58,8 @@ const selectedLabel = computed(() => {
57
58
  type="button"
58
59
  class="flex size-8 items-center justify-center rounded-md border text-sm text-muted-foreground transition hover:bg-muted/50"
59
60
  :class="!model ? 'border-emerald-500/40 bg-emerald-500/10' : 'border-transparent'"
60
- title="No icon (default glyph)"
61
- aria-label="No icon (default glyph)"
61
+ :title="t('integration.workflows.iconPicker.action.none')"
62
+ :aria-label="t('integration.workflows.iconPicker.action.none')"
62
63
  @click="pick(null)"
63
64
  >
64
65
  —
@@ -77,7 +78,7 @@ const selectedLabel = computed(() => {
77
78
  </div>
78
79
 
79
80
  <template v-if="logos.length">
80
- <div class="text-mono text-muted-foreground">Integrations</div>
81
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.iconPicker.section.integrations") }}</div>
81
82
  <div class="flex flex-wrap gap-1">
82
83
  <button
83
84
  v-for="l in logos"
@@ -3,7 +3,8 @@ import { computed, inject, nextTick, ref, watch } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
5
  import * as Lucide from "lucide-vue-next";
6
- import { NODE_CATEGORIES, findManifest } from "../../data/integrationNodeCatalog";
6
+ import { Copy, DangerTriangle as AlertTriangle, RefreshCircle as Loader2, TrashBinMinimalistic as TrashBin } from "#solar-icons";
7
+ import { NODE_CATEGORIES, findManifest, isVersionExecutable } from "../../data/integrationNodeCatalog";
7
8
  import { isTerminalManifest } from "../../data/nodeInputData";
8
9
  import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
9
10
  const props = defineProps({
@@ -13,17 +14,14 @@ const props = defineProps({
13
14
  });
14
15
  defineOptions({ inheritAttrs: false });
15
16
  defineEmits(["updateNodeInternals"]);
17
+ const t = useIntegrationsT();
16
18
  const manifest = computed(() => findManifest(props.data.nodeSlug, props.data.nodeVersion));
17
19
  const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.value?.category));
18
20
  const title = computed(
19
21
  () => resolveDisplayName(props.data.name) || resolveLocalized(manifest.value?.name) || props.data.nodeSlug
20
22
  );
21
- const inputRequired = computed(() => manifest.value?.input?.required !== false);
22
23
  const invalidFields = computed(() => props.data.__invalidFields ?? []);
23
- const invalidLabel = computed(() => {
24
- const n = invalidFields.value.length;
25
- return `Config incomplete \u2014 ${n} setting${n === 1 ? "" : "s"} still need${n === 1 ? "s" : ""} fixing: ${invalidFields.value.join(", ")}`;
26
- });
24
+ const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, { fields: invalidFields.value.join(", ") }));
27
25
  const notes = computed(() => (props.data.notes ?? "").trim());
28
26
  const noteOpen = ref(false);
29
27
  const noteReady = ref(false);
@@ -32,11 +30,15 @@ const notePlacement = ref(Position.Bottom);
32
30
  const noteMaxHeight = ref(0);
33
31
  let noteTimer = null;
34
32
  const notePanelId = computed(() => `node-note-${props.id}`);
33
+ function handleTitle(p) {
34
+ return p.hint ? p.label + " \u2014 " + p.hint : p.label;
35
+ }
35
36
  function portsFromOutputs(list) {
36
37
  const ports = [];
37
38
  for (const o of list) {
38
39
  if (o.name) {
39
- ports.push({ name: o.name, kind: o.kind, label: resolveLocalized(o.label) || o.name });
40
+ const hint = resolveLocalized(o.description);
41
+ ports.push({ name: o.name, kind: o.kind, label: resolveLocalized(o.label) || o.name, ...hint ? { hint } : {} });
40
42
  } else if (o.sourceFromConfig) {
41
43
  const raw = props.data.config?.[o.sourceFromConfig];
42
44
  const values = Array.isArray(raw) ? raw : [];
@@ -62,12 +64,12 @@ const outputs = computed(() => {
62
64
  const resolved = props.data.__resolvedOutputs;
63
65
  if (Array.isArray(resolved) && resolved.length) {
64
66
  const ports2 = portsFromOutputs(resolved);
65
- if (!ports2.length) ports2.push({ name: "default", kind: "default", label: "out" });
67
+ if (!ports2.length) ports2.push({ name: "default", kind: "default", label: t("integration.canvas.node.port.outputFallback") });
66
68
  return ports2;
67
69
  }
68
- if (!m) return [{ name: "default", kind: "default", label: "out" }];
70
+ if (!m) return [{ name: "default", kind: "default", label: t("integration.canvas.node.port.outputFallback") }];
69
71
  const ports = portsFromOutputs(m.outputs ?? []);
70
- if (!ports.length) ports.push({ name: "default", kind: "default", label: "out" });
72
+ if (!ports.length) ports.push({ name: "default", kind: "default", label: t("integration.canvas.node.port.outputFallback") });
71
73
  return ports;
72
74
  });
73
75
  function portSemantic(p) {
@@ -161,6 +163,8 @@ function releaseNote() {
161
163
  }, 250);
162
164
  }
163
165
  const nodeActions = inject("workflow-node-actions", null);
166
+ const unbuildableVersion = computed(() => nodeActions !== null && !isVersionExecutable(props.data.nodeSlug, props.data.nodeVersion));
167
+ const unbuildableLabel = computed(() => t("integration.canvas.node.badge.noBuiltCode", { version: props.data.nodeVersion }));
164
168
  const canConfigure = computed(() => typeof nodeActions?.configure === "function");
165
169
  function configure() {
166
170
  nodeActions?.configure?.(props.id);
@@ -183,21 +187,21 @@ function remove() {
183
187
  <button
184
188
  type="button"
185
189
  class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-foreground/80 hover:bg-muted/50"
186
- title="Duplicate"
187
- aria-label="Duplicate node"
190
+ :title="t('integration.canvas.node.action.duplicate')"
191
+ :aria-label="t('integration.canvas.node.action.duplicateAria')"
188
192
  @click="duplicate"
189
193
  >
190
- <Lucide.Copy :size="11" />
194
+ <Copy :size="11" />
191
195
  </button>
192
196
  <div class="h-4 w-px bg-black/10" />
193
197
  <button
194
198
  type="button"
195
199
  class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
196
- title="Delete"
197
- aria-label="Delete node"
200
+ :title="t('integration.common.action.delete')"
201
+ :aria-label="t('integration.canvas.node.action.deleteAria')"
198
202
  @click="remove"
199
203
  >
200
- <Lucide.Trash2 :size="11" />
204
+ <TrashBin :size="11" />
201
205
  </button>
202
206
  </NodeToolbar>
203
207
 
@@ -219,7 +223,7 @@ function remove() {
219
223
  >
220
224
  <div ref="notePanel" role="tooltip">
221
225
  <div class="mb-1 text-[8px] font-semibold uppercase tracking-[0.08em] text-muted-foreground/60">
222
- Note
226
+ {{ t("integration.canvas.node.note.heading") }}
223
227
  </div>
224
228
  <p class="whitespace-pre-wrap break-words text-[11px] leading-snug text-foreground/80">{{ notes }}</p>
225
229
  </div>
@@ -227,17 +231,15 @@ function remove() {
227
231
 
228
232
  <!-- Input: ONE logical input, attachable from the left or the top — both
229
233
  dots feed the same `__in` port. Clearly visible, ring + shadow. -->
230
- <template v-if="inputRequired">
231
- <Handle
232
- v-for="side in IN_SIDES"
233
- :id="`__in__${side}`"
234
- :key="`in-${side}`"
235
- type="target"
236
- :position="sideToPosition(side)"
237
- title="Input"
238
- class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-slate-700"
239
- />
240
- </template>
234
+ <Handle
235
+ v-for="side in IN_SIDES"
236
+ :id="`__in__${side}`"
237
+ :key="`in-${side}`"
238
+ type="target"
239
+ :position="sideToPosition(side)"
240
+ :title="t('integration.canvas.node.handle.input')"
241
+ class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-slate-700"
242
+ />
241
243
 
242
244
  <!-- Output handles: right + bottom, distributed per side. Color encodes the
243
245
  port semantics (true/false/error/branch/default), matching the edges. -->
@@ -248,7 +250,7 @@ function remove() {
248
250
  :key="`${p.name}-${side}`"
249
251
  type="source"
250
252
  :position="sideToPosition(side)"
251
- :title="p.label"
253
+ :title="handleTitle(p)"
252
254
  :style="distributedStyle(side, i, outputs.length)"
253
255
  class="!size-3 !rounded-full !border-2 !border-white !shadow-sm transition-all hover:!size-3.5"
254
256
  :class="portTone(p).handle"
@@ -266,19 +268,43 @@ function remove() {
266
268
  showPortLabels ? 'pb-3.5' : ''
267
269
  ]"
268
270
  >
269
- <!-- Incomplete settings at the last save: the count, amber, at the corner
270
- the input handles leave free. Hoverable on purpose — the count alone
271
- doesn't say what is wrong, so the title spells it out and lists the
272
- fields; `nodrag` keeps that hover from starting a card drag. -->
271
+ <!-- The card's marks, in one row at the corner the input handles leave free.
272
+ A row rather than two absolutely-placed badges: the amber one's width
273
+ follows its count, so any fixed offset for a second badge overlaps it as
274
+ soon as the count reaches two digits. Hoverable on purpose — a symbol or
275
+ a bare count at a corner is a riddle, so each carries its own title;
276
+ `nodrag` keeps that hover from starting a card drag. -->
273
277
  <span
274
- v-if="invalidFields.length"
275
- role="img"
276
- :aria-label="invalidLabel"
277
- :title="invalidLabel"
278
- class="nodrag absolute -left-2 -top-2 z-10 flex cursor-help items-center gap-0.5 rounded-full bg-amber-500 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
278
+ v-if="invalidFields.length || unbuildableVersion"
279
+ class="nodrag absolute -left-2 -top-2 z-10 flex items-center gap-1"
279
280
  >
280
- <Lucide.TriangleAlert :size="9" :stroke-width="2.5" />
281
- {{ invalidFields.length }}
281
+ <span
282
+ v-if="invalidFields.length"
283
+ role="img"
284
+ :aria-label="invalidLabel"
285
+ :title="invalidLabel"
286
+ class="flex cursor-help items-center gap-0.5 rounded-full bg-amber-500 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
287
+ >
288
+ <!-- `weight="Bold"` for the same reason the Lucide draw carried
289
+ `stroke-width="2.5"`: 9px of white on amber. Solar's default
290
+ BoldDuotone washes out at that size against the pill. -->
291
+ <AlertTriangle :size="9" weight="Bold" />
292
+ {{ invalidFields.length }}
293
+ </span>
294
+ <!-- The pinned version cannot be run. Rose rather than amber, and no
295
+ count: amber on this card means unfinished work of the user's own —
296
+ settings still to fill in — while this is the platform missing a piece,
297
+ which the user cannot fix by typing. Hence the stronger colour despite
298
+ being the one that does NOT block activation. -->
299
+ <span
300
+ v-if="unbuildableVersion"
301
+ role="img"
302
+ :aria-label="unbuildableLabel"
303
+ :title="unbuildableLabel"
304
+ class="flex cursor-help items-center rounded-full bg-rose-600 px-1.5 py-0.5 text-[9px] font-bold leading-none text-white shadow-sm"
305
+ >
306
+ <AlertTriangle :size="9" weight="Bold" />
307
+ </span>
282
308
  </span>
283
309
  <!-- Always-visible edit affordance: single click opens the config lightbox.
284
310
  (Double-click on the whole card works too.) Hidden in read-only hosts
@@ -288,8 +314,8 @@ function remove() {
288
314
  v-if="canConfigure"
289
315
  type="button"
290
316
  class="nodrag absolute right-1.5 top-1.5 z-10 flex size-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:bg-muted hover:text-foreground"
291
- title="Configure"
292
- aria-label="Configure"
317
+ :title="t('integration.canvas.node.action.configure')"
318
+ :aria-label="t('integration.canvas.node.action.configure')"
293
319
  @click.stop="configure"
294
320
  @mousedown.stop
295
321
  @dblclick.stop
@@ -305,7 +331,7 @@ function remove() {
305
331
  type="button"
306
332
  class="nodrag absolute right-1.5 z-10 flex size-5 items-center justify-center rounded text-muted-foreground/45 transition-colors hover:bg-muted hover:text-foreground"
307
333
  :class="showPortLabels ? 'bottom-4' : 'bottom-1.5'"
308
- aria-label="Note"
334
+ :aria-label="t('integration.canvas.node.note.heading')"
309
335
  :aria-describedby="notePanelId"
310
336
  @mouseenter="openNote"
311
337
  @mouseleave="releaseNote"
@@ -331,8 +357,12 @@ function remove() {
331
357
  v-if="resolvePending"
332
358
  class="pointer-events-none absolute bottom-1 left-1/2 flex -translate-x-1/2 select-none items-center gap-1 whitespace-nowrap text-[9px] font-semibold uppercase tracking-wide text-muted-foreground"
333
359
  >
334
- <Lucide.LoaderCircle :size="9" class="animate-spin" />
335
- Ports
360
+ <!-- `weight="Bold"` for the badge's reason one size down: the default
361
+ BoldDuotone draws a filled disc across the whole viewBox, and a disc
362
+ turning looks like a disc standing still. Bold knocks the arrows out
363
+ of the ring, so 9px of it still reads as spinning. -->
364
+ <Loader2 :size="9" weight="Bold" class="animate-spin" />
365
+ {{ t("integration.canvas.node.port.resolving") }}
336
366
  </span>
337
367
  <template v-else>
338
368
  <span
@@ -364,7 +394,7 @@ function remove() {
364
394
  <div v-if="data.nodeVersion" class="mt-0.5 flex items-center gap-1.5 text-sm text-muted-foreground">
365
395
  <span
366
396
  class="shrink-0 text-[10px] font-medium leading-none tabular-nums text-muted-foreground/55"
367
- :title="`Version ${data.nodeVersion}`"
397
+ :title="t('integration.canvas.node.meta.versionTitle', { version: data.nodeVersion })"
368
398
  >v{{ data.nodeVersion }}</span>
369
399
  </div>
370
400
  </div>