@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
@@ -1,9 +1,12 @@
1
1
  <script setup>
2
- import { computed, inject, ref, watch } from "vue";
2
+ 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 { Copy, TrashBinMinimalistic as TrashBin } from "#solar-icons";
6
7
  import { NODE_CATEGORIES, findManifest } from "../../data/integrationNodeCatalog";
8
+ import { isTerminalManifest } from "../../data/nodeInputData";
9
+ import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
7
10
  const props = defineProps({
8
11
  id: { type: String, required: true },
9
12
  data: { type: Object, required: true },
@@ -11,44 +14,28 @@ const props = defineProps({
11
14
  });
12
15
  defineOptions({ inheritAttrs: false });
13
16
  defineEmits(["updateNodeInternals"]);
17
+ const t = useIntegrationsT();
14
18
  const manifest = computed(() => findManifest(props.data.nodeSlug, props.data.nodeVersion));
15
19
  const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.value?.category));
16
- const hasLogo = computed(() => (manifest.value?.images ?? []).some((i) => i.category === "logo"));
17
- function iconFor(name) {
18
- if (!name) return Lucide.Box;
19
- const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
20
- return Lucide[cleaned] || Lucide.Box;
21
- }
22
- function labelText(value) {
23
- if (!value) return "";
24
- return typeof value === "string" ? value : value.en ?? Object.values(value)[0] ?? "";
25
- }
26
20
  const title = computed(
27
- () => labelText(props.data.name) || labelText(manifest.value?.name) || props.data.nodeSlug
21
+ () => resolveDisplayName(props.data.name) || resolveLocalized(manifest.value?.name) || props.data.nodeSlug
28
22
  );
29
23
  const inputRequired = computed(() => manifest.value?.input?.required !== false);
30
- const subtitleKeys = computed(() => {
31
- const fields = manifest.value?.config ?? [];
32
- const driving = /* @__PURE__ */ new Set();
33
- for (const f of fields) for (const dep of f.dependsOn ?? []) driving.add(dep);
34
- const drivingSelects = fields.filter((f) => f.type === "select" && driving.has(f.key)).map((f) => f.key);
35
- if (drivingSelects.length) return drivingSelects;
36
- return fields.filter((f) => f.type === "select" || f.type === "multiselect").slice(0, 2).map((f) => f.key);
37
- });
38
- function scalarText(v) {
39
- if (Array.isArray(v)) {
40
- return v.filter((x) => ["string", "number", "boolean"].includes(typeof x)).map((x) => String(x).trim()).filter(Boolean).join(", ");
41
- }
42
- return ["string", "number", "boolean"].includes(typeof v) ? String(v).trim() : "";
43
- }
44
- const subtitle = computed(
45
- () => subtitleKeys.value.map((k) => scalarText(props.data.config?.[k])).filter((s) => s !== "").join(" \xB7 ")
46
- );
24
+ const invalidFields = computed(() => props.data.__invalidFields ?? []);
25
+ const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, { fields: invalidFields.value.join(", ") }));
26
+ const notes = computed(() => (props.data.notes ?? "").trim());
27
+ const noteOpen = ref(false);
28
+ const noteReady = ref(false);
29
+ const notePanel = ref(null);
30
+ const notePlacement = ref(Position.Bottom);
31
+ const noteMaxHeight = ref(0);
32
+ let noteTimer = null;
33
+ const notePanelId = computed(() => `node-note-${props.id}`);
47
34
  function portsFromOutputs(list) {
48
35
  const ports = [];
49
36
  for (const o of list) {
50
37
  if (o.name) {
51
- ports.push({ name: o.name, kind: o.kind, label: labelText(o.label) || o.name });
38
+ ports.push({ name: o.name, kind: o.kind, label: resolveLocalized(o.label) || o.name });
52
39
  } else if (o.sourceFromConfig) {
53
40
  const raw = props.data.config?.[o.sourceFromConfig];
54
41
  const values = Array.isArray(raw) ? raw : [];
@@ -60,7 +47,7 @@ function portsFromOutputs(list) {
60
47
  ports.push({
61
48
  name: o.fallback.name,
62
49
  kind: o.kind,
63
- label: labelText(o.fallback.label) || o.fallback.name
50
+ label: resolveLocalized(o.fallback.label) || o.fallback.name
64
51
  });
65
52
  }
66
53
  }
@@ -69,24 +56,48 @@ function portsFromOutputs(list) {
69
56
  }
70
57
  const resolvePending = computed(() => (manifest.value?.outputs ?? []).some((o) => o.resolveOutputs === true) && !Array.isArray(props.data.__resolvedOutputs));
71
58
  const outputs = computed(() => {
59
+ const m = manifest.value;
60
+ if (isTerminalManifest(m)) return [];
72
61
  const resolved = props.data.__resolvedOutputs;
73
62
  if (Array.isArray(resolved) && resolved.length) {
74
63
  const ports2 = portsFromOutputs(resolved);
75
- if (!ports2.length) ports2.push({ name: "default", kind: "default", label: "out" });
64
+ if (!ports2.length) ports2.push({ name: "default", kind: "default", label: t("integration.canvas.node.port.outputFallback") });
76
65
  return ports2;
77
66
  }
78
- const m = manifest.value;
79
- if (!m) return [{ name: "default", kind: "default", label: "out" }];
67
+ if (!m) return [{ name: "default", kind: "default", label: t("integration.canvas.node.port.outputFallback") }];
80
68
  const ports = portsFromOutputs(m.outputs ?? []);
81
- if (!ports.length) ports.push({ name: "default", kind: "default", label: "out" });
69
+ if (!ports.length) ports.push({ name: "default", kind: "default", label: t("integration.canvas.node.port.outputFallback") });
82
70
  return ports;
83
71
  });
72
+ function portSemantic(p) {
73
+ if (p.name === "true") return "branch-true";
74
+ if (p.name === "false") return "branch-false";
75
+ if (p.name === "error" || p.kind === "error") return "error";
76
+ if (p.name === "done") return "success";
77
+ if (p.kind === "branch") return "branch";
78
+ const portDef = (manifest.value?.outputs ?? []).find((o) => o.name === p.name);
79
+ return portDef ? "success" : "default";
80
+ }
84
81
  function portTone(p) {
85
- if (p.name === "true") return { handle: "!bg-emerald-500", label: "text-emerald-600" };
86
- if (p.name === "false") return { handle: "!bg-amber-400", label: "text-amber-600" };
87
- if (p.kind === "error") return { handle: "!bg-rose-500", label: "text-rose-600" };
88
- if (p.kind === "branch") return { handle: "!bg-violet-500", label: "text-violet-600" };
89
- return { handle: "!bg-slate-500", label: "text-slate-500" };
82
+ switch (portSemantic(p)) {
83
+ case "success":
84
+ case "branch-true":
85
+ // Switch cases / iterator `item`: a taken branch is a normal continuation,
86
+ // so it reads green like any other output rather than a special colour.
87
+ case "branch":
88
+ return { handle: "!bg-emerald-500", label: "text-emerald-600" };
89
+ // `false` is the neutral "didn't match" route, not a warning — muted slate
90
+ // so it recedes behind the green primary path (matches `default`).
91
+ case "branch-false":
92
+ return { handle: "!bg-slate-500", label: "text-slate-500" };
93
+ // Soft rose dot marks the error port; the LABEL stays neutral slate (not
94
+ // rose) so it's legible without a red-alert pull that would out-shout the
95
+ // green success label. Colour lives on the dot, the label just names it.
96
+ case "error":
97
+ return { handle: "!bg-rose-300", label: "text-slate-500" };
98
+ default:
99
+ return { handle: "!bg-slate-500", label: "text-slate-500" };
100
+ }
90
101
  }
91
102
  const showPortLabels = computed(() => resolvePending.value || outputs.value.length > 1 || outputs.value.some((p) => p.kind !== "default"));
92
103
  const IN_SIDES = ["top"];
@@ -112,38 +123,107 @@ function onHoverLeave() {
112
123
  hovered.value = false;
113
124
  }, 200);
114
125
  }
115
- watch(subtitle, () => flow.updateNodeInternals([props.id]));
126
+ watch(() => Boolean(props.data.nodeVersion), () => flow.updateNodeInternals([props.id]));
127
+ const NOTE_GAP = 6;
128
+ const NOTE_TOOLBAR_CLEARANCE = 42;
129
+ const canvasBottomInset = inject("workflow-canvas-bottom-inset", null);
130
+ const noteTopOffset = ref(NOTE_GAP);
131
+ async function openNote(ev) {
132
+ holdNote();
133
+ if (noteOpen.value) return;
134
+ const card = ev.currentTarget?.closest(".vue-flow__node");
135
+ noteTopOffset.value = nodeActions && (hovered.value || props.selected) ? NOTE_TOOLBAR_CLEARANCE : NOTE_GAP;
136
+ noteOpen.value = true;
137
+ noteReady.value = false;
138
+ await nextTick();
139
+ const bounds = flow.vueFlowRef.value?.getBoundingClientRect();
140
+ if (notePanel.value && card && bounds) {
141
+ const rect = card.getBoundingClientRect();
142
+ const floor = Math.min(bounds.bottom, window.innerHeight) - (canvasBottomInset?.() ?? 0);
143
+ const ceiling = Math.max(bounds.top, 0);
144
+ const below = floor - rect.bottom - NOTE_GAP;
145
+ const above = rect.top - ceiling - noteTopOffset.value;
146
+ const flip = notePanel.value.offsetHeight > below && above > below;
147
+ notePlacement.value = flip ? Position.Top : Position.Bottom;
148
+ noteMaxHeight.value = Math.max(80, Math.floor(flip ? above : below));
149
+ }
150
+ noteReady.value = true;
151
+ }
152
+ function holdNote() {
153
+ if (noteTimer) clearTimeout(noteTimer);
154
+ noteTimer = null;
155
+ }
156
+ function releaseNote() {
157
+ if (noteTimer) clearTimeout(noteTimer);
158
+ noteTimer = setTimeout(() => {
159
+ noteOpen.value = false;
160
+ }, 250);
161
+ }
116
162
  const nodeActions = inject("workflow-node-actions", null);
163
+ const canConfigure = computed(() => typeof nodeActions?.configure === "function");
164
+ function configure() {
165
+ nodeActions?.configure?.(props.id);
166
+ }
117
167
  function duplicate() {
118
168
  nodeActions?.duplicate(props.id);
119
169
  }
120
170
  function remove() {
121
- if (nodeActions) nodeActions.remove(props.id);
122
- else flow.removeNodes([props.id]);
171
+ nodeActions?.remove(props.id);
123
172
  }
124
173
  </script>
125
174
 
126
175
  <template>
127
- <NodeToolbar :is-visible="hovered || selected" :position="Position.Top" :offset="8" class="flex items-center gap-1 rounded-md border border-black/[0.08] bg-white px-1 py-1 shadow-md" @mouseenter="onHoverEnter" @mouseleave="onHoverLeave">
176
+ <!-- No node-actions injected → read-only canvas (e.g. run replay): the
177
+ duplicate/delete affordance would be dead, so don't offer it. -->
178
+ <!-- Icon-only, so each action carries a name — and it names the node, not
179
+ just the verb: the editor header has its own Duplicate (the workflow),
180
+ and "Duplicate" alone would be the same announcement for both. -->
181
+ <NodeToolbar v-if="nodeActions" :is-visible="hovered || selected" :position="Position.Top" :offset="8" class="flex items-center gap-1 rounded-md border border-black/[0.08] bg-white px-1 py-1 shadow-md" @mouseenter="onHoverEnter" @mouseleave="onHoverLeave">
128
182
  <button
129
183
  type="button"
130
184
  class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-foreground/80 hover:bg-muted/50"
131
- title="Duplicate"
185
+ :title="t('integration.canvas.node.action.duplicate')"
186
+ :aria-label="t('integration.canvas.node.action.duplicateAria')"
132
187
  @click="duplicate"
133
188
  >
134
- <Lucide.Copy :size="11" />
189
+ <Copy :size="11" />
135
190
  </button>
136
191
  <div class="h-4 w-px bg-black/10" />
137
192
  <button
138
193
  type="button"
139
194
  class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
140
- title="Delete"
195
+ :title="t('integration.common.action.delete')"
196
+ :aria-label="t('integration.canvas.node.action.deleteAria')"
141
197
  @click="remove"
142
198
  >
143
- <Lucide.Trash2 :size="11" />
199
+ <TrashBin :size="11" />
144
200
  </button>
145
201
  </NodeToolbar>
146
202
 
203
+ <!-- The note itself, hanging off the corner the badge sits in — below the card
204
+ by default, above it when a long note would otherwise run past the bottom
205
+ of the canvas. A NodeToolbar rather than an in-card panel: it escapes the
206
+ node's stacking context, so the text can't end up behind a neighbour. -->
207
+ <NodeToolbar
208
+ v-if="notes"
209
+ :is-visible="noteOpen"
210
+ :position="notePlacement"
211
+ align="end"
212
+ :offset="notePlacement === Position.Top ? noteTopOffset : NOTE_GAP"
213
+ class="nowheel max-w-[240px] overflow-y-auto overscroll-contain rounded-lg border border-black/[0.08] bg-white px-2.5 py-2 text-left shadow-lg"
214
+ :class="noteReady ? '' : 'invisible'"
215
+ :style="noteMaxHeight ? { maxHeight: `${noteMaxHeight}px` } : void 0"
216
+ @mouseenter="holdNote"
217
+ @mouseleave="releaseNote"
218
+ >
219
+ <div ref="notePanel" role="tooltip">
220
+ <div class="mb-1 text-[8px] font-semibold uppercase tracking-[0.08em] text-muted-foreground/60">
221
+ {{ t("integration.canvas.node.note.heading") }}
222
+ </div>
223
+ <p class="whitespace-pre-wrap break-words text-[11px] leading-snug text-foreground/80">{{ notes }}</p>
224
+ </div>
225
+ </NodeToolbar>
226
+
147
227
  <!-- Input: ONE logical input, attachable from the left or the top — both
148
228
  dots feed the same `__in` port. Clearly visible, ring + shadow. -->
149
229
  <template v-if="inputRequired">
@@ -153,7 +233,7 @@ function remove() {
153
233
  :key="`in-${side}`"
154
234
  type="target"
155
235
  :position="sideToPosition(side)"
156
- title="Input"
236
+ :title="t('integration.canvas.node.handle.input')"
157
237
  class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-slate-700"
158
238
  />
159
239
  </template>
@@ -181,10 +261,66 @@ function remove() {
181
261
  @mouseenter="onHoverEnter"
182
262
  @mouseleave="onHoverLeave"
183
263
  :class="[
184
- selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]',
264
+ selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : invalidFields.length ? 'border-amber-500/70 shadow-[0_0_0_2px_rgba(245,158,11,0.22)] hover:shadow-md' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]',
185
265
  showPortLabels ? 'pb-3.5' : ''
186
266
  ]"
187
267
  >
268
+ <!-- Incomplete settings at the last save: the count, amber, at the corner
269
+ the input handles leave free. Hoverable on purpose — the count alone
270
+ doesn't say what is wrong, so the title spells it out and lists the
271
+ fields; `nodrag` keeps that hover from starting a card drag. -->
272
+ <span
273
+ v-if="invalidFields.length"
274
+ role="img"
275
+ :aria-label="invalidLabel"
276
+ :title="invalidLabel"
277
+ 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
+ >
279
+ <Lucide.TriangleAlert :size="9" :stroke-width="2.5" />
280
+ {{ invalidFields.length }}
281
+ </span>
282
+ <!-- Always-visible edit affordance: single click opens the config lightbox.
283
+ (Double-click on the whole card works too.) Hidden in read-only hosts
284
+ that don't provide `configure`. `nodrag` so pressing it never starts a
285
+ node drag; `.stop` so it doesn't bubble to pane/canvas. -->
286
+ <button
287
+ v-if="canConfigure"
288
+ type="button"
289
+ 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"
290
+ :title="t('integration.canvas.node.action.configure')"
291
+ :aria-label="t('integration.canvas.node.action.configure')"
292
+ @click.stop="configure"
293
+ @mousedown.stop
294
+ @dblclick.stop
295
+ >
296
+ <Lucide.Pencil :size="11" />
297
+ </button>
298
+ <!-- Note badge, mirroring the edit affordance into the opposite corner. Its
299
+ hit area is the whole 20px square, not the 12px glyph. Sits clear of the
300
+ port-label strip when there is one; `nodrag` + `.stop` so grabbing it
301
+ neither drags the node nor opens the config. -->
302
+ <button
303
+ v-if="notes"
304
+ type="button"
305
+ 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"
306
+ :class="showPortLabels ? 'bottom-4' : 'bottom-1.5'"
307
+ :aria-label="t('integration.canvas.node.note.heading')"
308
+ :aria-describedby="notePanelId"
309
+ @mouseenter="openNote"
310
+ @mouseleave="releaseNote"
311
+ @focus="openNote"
312
+ @blur="releaseNote"
313
+ @click.stop
314
+ @mousedown.stop
315
+ @dblclick.stop
316
+ >
317
+ <Lucide.MessageSquareText :size="12" :stroke-width="2.25" />
318
+ </button>
319
+ <!-- The badge's description. `sr-only` keeps it out of the picture and out of
320
+ the layout (it is absolutely positioned, so the card's height — which
321
+ feeds the handle positions — is untouched), while keeping it in the
322
+ accessibility tree, where the visible panel cannot be relied on. -->
323
+ <span v-if="notes" :id="notePanelId" class="sr-only">{{ notes }}</span>
188
324
  <!-- Port labels sit ABOVE their bottom dots, inside the card's label
189
325
  strip, whenever ports are distinguishable (true/false, error, cases). -->
190
326
  <template v-if="showPortLabels">
@@ -195,7 +331,7 @@ function remove() {
195
331
  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"
196
332
  >
197
333
  <Lucide.LoaderCircle :size="9" class="animate-spin" />
198
- Ports
334
+ {{ t("integration.canvas.node.port.resolving") }}
199
335
  </span>
200
336
  <template v-else>
201
337
  <span
@@ -212,17 +348,23 @@ function remove() {
212
348
  <div class="flex items-center gap-2.5 px-3 py-2.5">
213
349
  <span
214
350
  class="flex size-8 shrink-0 items-center justify-center rounded-md"
215
- :class="hasLogo ? 'bg-white ring-1 ring-black/[0.06]' : category?.color ?? 'bg-muted text-foreground'"
351
+ :class="category?.color ?? 'bg-muted text-foreground'"
216
352
  >
217
- <IntegrationsNodeLogo v-if="hasLogo" :manifest="manifest" :size="18" />
218
- <component :is="iconFor(manifest?.icon)" v-else :size="15" :stroke-width="2.25" />
353
+ <IntegrationsNodeMark :manifest="manifest" :size="18" />
219
354
  </span>
220
355
  <div class="min-w-0 flex-1">
221
356
  <div class="text-sm font-semibold leading-tight text-foreground truncate">
222
357
  {{ title }}
223
358
  </div>
224
- <div v-if="subtitle" class="mt-0.5 text-sm text-muted-foreground truncate" :title="subtitle">
225
- {{ subtitle }}
359
+ <!-- Meta line: the pinned version, small + muted but always visible so
360
+ you can tell which version an instance runs without opening the
361
+ inspector. Left-aligned and on the second line, clear of the
362
+ top-right edit affordance (PO-192). -->
363
+ <div v-if="data.nodeVersion" class="mt-0.5 flex items-center gap-1.5 text-sm text-muted-foreground">
364
+ <span
365
+ class="shrink-0 text-[10px] font-medium leading-none tabular-nums text-muted-foreground/55"
366
+ :title="t('integration.canvas.node.meta.versionTitle', { version: data.nodeVersion })"
367
+ >v{{ data.nodeVersion }}</span>
226
368
  </div>
227
369
  </div>
228
370
  </div>
@@ -5,6 +5,10 @@ interface NodeData extends WorkflowNode {
5
5
  * editor without persisting to the blob. When present and non-empty, these
6
6
  * drive the rendered handles instead of the static manifest computation. */
7
7
  __resolvedOutputs?: NodeManifestOutput[];
8
+ /** Config keys the backend reported as incomplete at the last save (PO-209),
9
+ * injected by the editor the same non-persisting way. Read-only canvases
10
+ * (run replay, history preview) never pass them, so nothing is marked there. */
11
+ __invalidFields?: string[];
8
12
  }
9
13
  type __VLS_Props = {
10
14
  id: string;