@revenexx/studio-integrations 0.5.0 → 0.6.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 (129) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +17 -4
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
  7. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
  8. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  9. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
  10. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  11. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
  13. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  14. package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
  15. package/dist/runtime/components/Integrations/Layout.vue +21 -2
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
  18. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  19. package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  21. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
  23. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  24. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  25. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  26. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  27. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  28. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  29. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  30. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  31. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
  32. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  33. package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
  34. package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
  35. package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
  36. package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
  38. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  39. package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
  40. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  41. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  42. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
  43. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  44. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
  45. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  46. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
  47. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  48. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
  49. package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
  50. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
  51. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
  52. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  53. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
  54. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  55. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  56. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
  57. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  58. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
  59. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
  60. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  61. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
  62. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  63. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
  64. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
  65. package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
  66. package/dist/runtime/composables/useCredentialTypes.js +4 -4
  67. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  68. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
  69. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  70. package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
  71. package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
  72. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  73. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  74. package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
  75. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
  76. package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
  77. package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
  78. package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
  79. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  80. package/dist/runtime/composables/useLoadGate.js +8 -0
  81. package/dist/runtime/data/branchReorder.d.ts +37 -0
  82. package/dist/runtime/data/branchReorder.js +71 -0
  83. package/dist/runtime/data/handleId.d.ts +23 -0
  84. package/dist/runtime/data/handleId.js +6 -0
  85. package/dist/runtime/data/integrationTemplates.d.ts +39 -0
  86. package/dist/runtime/data/integrationTemplates.js +15 -0
  87. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  88. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  89. package/dist/runtime/data/nodeIcons.js +73 -0
  90. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  91. package/dist/runtime/data/nodeInputData.js +4 -0
  92. package/dist/runtime/data/payloadForm.d.ts +9 -2
  93. package/dist/runtime/data/payloadForm.js +27 -3
  94. package/dist/runtime/data/portTone.d.ts +17 -0
  95. package/dist/runtime/data/portTone.js +7 -0
  96. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  97. package/dist/runtime/data/webhookAuth.js +19 -0
  98. package/dist/runtime/data/workflowIconCatalog.js +2 -5
  99. package/dist/runtime/pages/integrations/audit.vue +91 -6
  100. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
  101. package/dist/runtime/pages/integrations/credentials.vue +119 -21
  102. package/dist/runtime/pages/integrations/index.vue +49 -44
  103. package/dist/runtime/pages/integrations/runs.vue +1 -1
  104. package/dist/runtime/pages/integrations/schedules.vue +51 -7
  105. package/dist/runtime/pages/integrations/secrets.vue +29 -7
  106. package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
  107. package/dist/runtime/pages/integrations/templates/index.vue +57 -86
  108. package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
  109. package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
  110. package/dist/runtime/types/integrations.d.ts +65 -6
  111. package/dist/runtime/utils/apiError.d.ts +47 -0
  112. package/dist/runtime/utils/apiError.js +32 -0
  113. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  114. package/dist/runtime/utils/cockpitSources.js +14 -0
  115. package/dist/runtime/utils/loadGate.d.ts +61 -0
  116. package/dist/runtime/utils/loadGate.js +22 -0
  117. package/dist/runtime/utils/localize.d.ts +29 -0
  118. package/dist/runtime/utils/localize.js +4 -0
  119. package/dist/runtime/utils/notify.d.ts +59 -0
  120. package/dist/runtime/utils/notify.js +21 -0
  121. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  122. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  123. package/dist/runtime/utils/schedule.d.ts +141 -0
  124. package/dist/runtime/utils/schedule.js +207 -0
  125. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  126. package/dist/runtime/utils/validationFindings.js +53 -0
  127. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  128. package/dist/runtime/utils/whenIdle.js +12 -0
  129. package/package.json +8 -2
@@ -20,6 +20,12 @@ type __VLS_Props = {
20
20
  dynamicOutputs?: Record<string, NodeManifestOutput[]>;
21
21
  /** Workflow id — enables sampling real values from the latest run. */
22
22
  workflowId?: number | string;
23
+ /**
24
+ * Config key → why the LAST SAVE refused it. Not live validation: the server
25
+ * is the only judge, so a fix shows once it is saved. Absent for a node the
26
+ * last save had no complaint about.
27
+ */
28
+ refusedFields?: Record<string, string>;
23
29
  };
24
30
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
25
31
  close: () => any;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * @deprecated Use `NodeMark` directly. This is a thin back-compat shim kept so
3
+ * that hosts still referencing the auto-registered `<IntegrationsNodeLogo>`
4
+ * (e.g. integrations/studio-ui `local-nodes.vue`) keep resolving the component
5
+ * after PO-194 renamed NodeLogo → NodeMark. Auto-registered components are
6
+ * public API surface for the hosts, so a silent delete breaks them at a
7
+ * different repo's build, invisible to this repo's CI.
8
+ *
9
+ * The old NodeLogo rendered "logo OR glyph, logo preferred" — exactly
10
+ * `NodeMark`'s `logo-only` behaviour (logo when present, glyph fallback). Drop
11
+ * this shim one release after the hosts have migrated to `NodeMark`.
12
+ */
1
13
  import type { NodeManifest } from '../../types/integrations.js';
2
14
  type __VLS_Props = {
3
15
  manifest: NodeManifest | null;
@@ -1,38 +1,10 @@
1
1
  <script setup>
2
- import { computed, ref, watch } from "vue";
3
- import * as Lucide from "lucide-vue-next";
4
- const props = defineProps({
2
+ defineProps({
5
3
  manifest: { type: [Object, null], required: true },
6
4
  size: { type: Number, required: false, default: 16 }
7
5
  });
8
- const failed = ref(false);
9
- watch(() => props.manifest?.slug, () => {
10
- failed.value = false;
11
- });
12
- const logoSrc = computed(() => {
13
- const imgs = props.manifest?.images ?? [];
14
- const logo = imgs.find((i) => i.category === "logo") ?? imgs[0];
15
- return logo?.src ?? null;
16
- });
17
- const glyph = computed(() => {
18
- const name = props.manifest?.icon;
19
- if (!name) return Lucide.Box;
20
- const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
21
- return Lucide[cleaned] || Lucide.Box;
22
- });
23
6
  </script>
24
7
 
25
8
  <template>
26
- <img
27
- v-if="logoSrc && !failed"
28
- :src="logoSrc"
29
- :width="size"
30
- :height="size"
31
- class="shrink-0 rounded-[3px] object-contain"
32
- :style="{ width: `${size}px`, height: `${size}px` }"
33
- alt=""
34
- loading="lazy"
35
- @error="failed = true"
36
- >
37
- <component :is="glyph" v-else :size="size" />
9
+ <IntegrationsNodeMark :manifest="manifest" :size="size" logo-only />
38
10
  </template>
@@ -1,3 +1,15 @@
1
+ /**
2
+ * @deprecated Use `NodeMark` directly. This is a thin back-compat shim kept so
3
+ * that hosts still referencing the auto-registered `<IntegrationsNodeLogo>`
4
+ * (e.g. integrations/studio-ui `local-nodes.vue`) keep resolving the component
5
+ * after PO-194 renamed NodeLogo → NodeMark. Auto-registered components are
6
+ * public API surface for the hosts, so a silent delete breaks them at a
7
+ * different repo's build, invisible to this repo's CI.
8
+ *
9
+ * The old NodeLogo rendered "logo OR glyph, logo preferred" — exactly
10
+ * `NodeMark`'s `logo-only` behaviour (logo when present, glyph fallback). Drop
11
+ * this shim one release after the hosts have migrated to `NodeMark`.
12
+ */
1
13
  import type { NodeManifest } from '../../types/integrations.js';
2
14
  type __VLS_Props = {
3
15
  manifest: NodeManifest | null;
@@ -0,0 +1,15 @@
1
+ import type { NodeManifest } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ manifest: NodeManifest | null;
4
+ size?: number;
5
+ /** Render only the vendor logo (folder/group rows), never the glyph+badge composite. */
6
+ logoOnly?: boolean;
7
+ /** Overlay the vendor logo as a corner badge. Off in rows that already name the vendor. */
8
+ badge?: boolean;
9
+ };
10
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
+ size: number;
12
+ badge: boolean;
13
+ logoOnly: boolean;
14
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ export default _default;
@@ -0,0 +1,79 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import { nodeIcon } from "../../data/nodeIcons";
4
+ const props = defineProps({
5
+ manifest: { type: [Object, null], required: true },
6
+ size: { type: Number, required: false, default: 16 },
7
+ logoOnly: { type: Boolean, required: false, default: false },
8
+ badge: { type: Boolean, required: false, default: true }
9
+ });
10
+ const failed = ref(false);
11
+ const { load: reloadCatalog } = useIntegrationsNodes();
12
+ const refreshed = ref(false);
13
+ watch(() => props.manifest?.slug, () => {
14
+ failed.value = false;
15
+ refreshed.value = false;
16
+ });
17
+ function onImageError() {
18
+ if (!refreshed.value) {
19
+ refreshed.value = true;
20
+ void reloadCatalog(true);
21
+ return;
22
+ }
23
+ failed.value = true;
24
+ }
25
+ function onImageLoad() {
26
+ refreshed.value = false;
27
+ }
28
+ const logoSrc = computed(() => {
29
+ const imgs = props.manifest?.images ?? [];
30
+ const logo = imgs.find((i) => i.category === "logo") ?? imgs[0];
31
+ return logo?.src ?? null;
32
+ });
33
+ const hasIcon = computed(() => typeof props.manifest?.icon === "string" && props.manifest.icon.trim() !== "");
34
+ const glyph = computed(() => nodeIcon(props.manifest?.icon));
35
+ const logoOk = computed(() => !!logoSrc.value && !failed.value);
36
+ const logoPrimary = computed(() => logoOk.value && (props.logoOnly || !hasIcon.value));
37
+ const composite = computed(() => props.badge && !props.logoOnly && logoOk.value && hasIcon.value);
38
+ const badgeSize = computed(() => Math.max(12, Math.round(props.size * 0.72)));
39
+ </script>
40
+
41
+ <template>
42
+ <!-- Logo as the primary mark (folder rows, or a node without its own glyph). -->
43
+ <img
44
+ v-if="logoPrimary"
45
+ :src="logoSrc"
46
+ :width="size"
47
+ :height="size"
48
+ class="shrink-0 rounded-[3px] object-contain"
49
+ :style="{ width: `${size}px`, height: `${size}px` }"
50
+ alt=""
51
+ loading="lazy"
52
+ @error="onImageError"
53
+ @load="onImageLoad"
54
+ >
55
+ <!-- Per-node glyph, with the vendor logo as a plated corner badge when present. -->
56
+ <span
57
+ v-else
58
+ class="relative inline-flex shrink-0"
59
+ :style="{ width: `${size}px`, height: `${size}px` }"
60
+ >
61
+ <component :is="glyph" :size="size" :stroke-width="2.25" />
62
+ <!-- The badge deliberately sits 3px OUTSIDE this box (integrations-ui
63
+ geometry): it overlaps the glyph's corner instead of shrinking the
64
+ glyph. Consequence for call sites — a chip hosting a mark has to leave
65
+ its overflow visible; an `overflow-hidden` wrapper clips the badge. -->
66
+ <img
67
+ v-if="composite"
68
+ :src="logoSrc"
69
+ :width="badgeSize"
70
+ :height="badgeSize"
71
+ class="absolute rounded-[3px] bg-white object-contain p-px ring-1 ring-black/10"
72
+ :style="{ width: `${badgeSize}px`, height: `${badgeSize}px`, right: '-3px', bottom: '-3px' }"
73
+ alt=""
74
+ loading="lazy"
75
+ @error="onImageError"
76
+ @load="onImageLoad"
77
+ >
78
+ </span>
79
+ </template>
@@ -0,0 +1,15 @@
1
+ import type { NodeManifest } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ manifest: NodeManifest | null;
4
+ size?: number;
5
+ /** Render only the vendor logo (folder/group rows), never the glyph+badge composite. */
6
+ logoOnly?: boolean;
7
+ /** Overlay the vendor logo as a corner badge. Off in rows that already name the vendor. */
8
+ badge?: boolean;
9
+ };
10
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
+ size: number;
12
+ badge: boolean;
13
+ logoOnly: boolean;
14
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import type { PortEntry } from '../../data/nodeInputData.js';
2
+ /**
3
+ * The ports a node hands downstream, with their declared fields and whatever
4
+ * sample value is known for each.
5
+ *
6
+ * Rendered from one place — the Output tab (specs/node-inspector.md AC-4). It
7
+ * keeps its own file for readability rather than for reuse: the schema list is
8
+ * a self-contained tree with its own type glyphs and sample formatting, and
9
+ * inlining it would bury the tab's own markup.
10
+ *
11
+ * It was briefly read from a second place, an always-visible summary strip
12
+ * above the form. That was AC-5, retired before it ever held — see the Gaps
13
+ * section of the spec. Do not go looking for the second call site.
14
+ */
15
+ type __VLS_Props = {
16
+ ports: PortEntry[];
17
+ /** Sample preview for a port, or for one field of it; null when unknown. */
18
+ valueAt: (port: string, field?: string) => string | null;
19
+ };
20
+ 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>;
21
+ export default _default;
@@ -0,0 +1,42 @@
1
+ <script setup>
2
+ import { portDotClass } from "../../data/portTone";
3
+ defineProps({
4
+ ports: { type: Array, required: true },
5
+ valueAt: { type: Function, required: true }
6
+ });
7
+ const TYPE_GLYPHS = {
8
+ string: { char: "A", cls: "text-sky-600" },
9
+ number: { char: "#", cls: "text-amber-600" },
10
+ integer: { char: "#", cls: "text-amber-600" },
11
+ boolean: { char: "\u2713", cls: "text-emerald-600" },
12
+ object: { char: "{}", cls: "text-violet-600" },
13
+ array: { char: "[]", cls: "text-violet-600" }
14
+ };
15
+ function typeGlyph(dataType) {
16
+ return dataType ? TYPE_GLYPHS[dataType] ?? null : null;
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <div>
22
+ <div v-for="p in ports" :key="p.name" class="mb-3 space-y-1">
23
+ <div class="flex items-center gap-1.5">
24
+ <span class="size-1.5 rounded-full" :class="portDotClass(p.kind, p.name)" />
25
+ <span class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{{ p.name }}</span>
26
+ </div>
27
+ <div v-if="p.fields.length" class="space-y-0.5">
28
+ <div v-for="f in p.fields" :key="f.name" class="flex min-w-0 items-baseline gap-1 font-mono text-[11px]">
29
+ <span v-if="typeGlyph(f.dataType)" class="shrink-0 font-semibold" :class="typeGlyph(f.dataType).cls">{{ typeGlyph(f.dataType).char }}</span>
30
+ <span class="shrink-0 text-slate-700">{{ f.name }}</span>
31
+ <span v-if="valueAt(p.name, f.name) !== null" class="min-w-0 truncate text-slate-400">: {{ valueAt(p.name, f.name) }}</span>
32
+ </div>
33
+ </div>
34
+ <p v-else class="text-[11px] leading-snug text-muted-foreground">
35
+ No declared fields<template v-if="valueAt(p.name) !== null">; sampled: <span class="font-mono">{{ valueAt(p.name) }}</span></template>.
36
+ </p>
37
+ </div>
38
+ <p v-if="!ports.length" class="text-xs leading-snug text-muted-foreground">
39
+ This node declares no outputs.
40
+ </p>
41
+ </div>
42
+ </template>
@@ -0,0 +1,21 @@
1
+ import type { PortEntry } from '../../data/nodeInputData.js';
2
+ /**
3
+ * The ports a node hands downstream, with their declared fields and whatever
4
+ * sample value is known for each.
5
+ *
6
+ * Rendered from one place — the Output tab (specs/node-inspector.md AC-4). It
7
+ * keeps its own file for readability rather than for reuse: the schema list is
8
+ * a self-contained tree with its own type glyphs and sample formatting, and
9
+ * inlining it would bury the tab's own markup.
10
+ *
11
+ * It was briefly read from a second place, an always-visible summary strip
12
+ * above the form. That was AC-5, retired before it ever held — see the Gaps
13
+ * section of the spec. Do not go looking for the second call site.
14
+ */
15
+ type __VLS_Props = {
16
+ ports: PortEntry[];
17
+ /** Sample preview for a port, or for one field of it; null when unknown. */
18
+ valueAt: (port: string, field?: string) => string | null;
19
+ };
20
+ 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>;
21
+ export default _default;
@@ -2,6 +2,8 @@
2
2
  import { computed, ref, watch } from "vue";
3
3
  import * as Lucide from "lucide-vue-next";
4
4
  import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
5
+ import { nodeIcon } from "../../data/nodeIcons";
6
+ import { resolveLocalized } from "../../utils/localize";
5
7
  const { load: loadNodes } = useIntegrationsNodes();
6
8
  onMounted(() => {
7
9
  void loadNodes();
@@ -10,15 +12,7 @@ const emit = defineEmits(["insert"]);
10
12
  const search = ref("");
11
13
  const searchInput = ref(null);
12
14
  defineExpose({ focusSearch: () => searchInput.value?.focus() });
13
- function loc(value) {
14
- if (!value) return "";
15
- return typeof value === "string" ? value : value.en ?? Object.values(value)[0] ?? "";
16
- }
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
- }
15
+ const iconFor = nodeIcon;
22
16
  const CORE_GROUP_BY_CATEGORY = {
23
17
  control: "Flow",
24
18
  transform: "Data",
@@ -61,7 +55,7 @@ function hasLogo(m) {
61
55
  }
62
56
  function groupPath(m) {
63
57
  if (m.groups?.length) {
64
- const path = m.groups.map((g) => loc(g)).filter(Boolean);
58
+ const path = m.groups.map((g) => resolveLocalized(g)).filter(Boolean);
65
59
  if (isIntegration(m) || !path.length) return path;
66
60
  return [CORE_LABEL_ALIAS[path[0]] ?? path[0], ...path.slice(1)];
67
61
  }
@@ -91,6 +85,31 @@ const latestManifests = computed(() => {
91
85
  }
92
86
  return out;
93
87
  });
88
+ const versionsBySlug = computed(() => {
89
+ const map = /* @__PURE__ */ new Map();
90
+ for (const m of INTEGRATION_NODE_CATALOG) {
91
+ const list = map.get(m.slug);
92
+ if (list) list.push(m);
93
+ else map.set(m.slug, [m]);
94
+ }
95
+ return map;
96
+ });
97
+ function versionsOf(slug) {
98
+ return versionsBySlug.value.get(slug) ?? [];
99
+ }
100
+ const selectedVersion = ref({});
101
+ function setSelectedVersion(slug, version) {
102
+ selectedVersion.value = { ...selectedVersion.value, [slug]: version };
103
+ }
104
+ function effVersion(m) {
105
+ const sel = selectedVersion.value[m.slug];
106
+ return sel && versionsOf(m.slug).some((v) => v.version === sel) ? sel : m.version;
107
+ }
108
+ function effManifest(m) {
109
+ const v = effVersion(m);
110
+ if (v === m.version) return m;
111
+ return versionsOf(m.slug).find((x) => x.version === v) ?? m;
112
+ }
94
113
  const tree = computed(() => {
95
114
  const apps = makeGroup("Integrations");
96
115
  const core = makeGroup("Core");
@@ -166,7 +185,7 @@ const searchHits = computed(() => {
166
185
  const scope = currentGroup.value ? subtreeManifests(currentGroup.value) : latestManifests.value.filter((m) => m.category !== "trigger");
167
186
  const hits = [];
168
187
  for (const m of scope) {
169
- const hay = `${loc(m.name)} ${m.slug} ${loc(m.description)}`.toLowerCase();
188
+ const hay = `${resolveLocalized(m.name)} ${m.slug} ${resolveLocalized(m.description)}`.toLowerCase();
170
189
  if (hay.includes(q)) hits.push({ manifest: m, path: groupPath(m) });
171
190
  if (hits.length >= 50) break;
172
191
  }
@@ -175,7 +194,7 @@ const searchHits = computed(() => {
175
194
  const matchedTriggers = computed(() => {
176
195
  const q = search.value.trim().toLowerCase();
177
196
  if (!q || currentGroup.value) return [];
178
- const matches = (t) => loc(t.name).toLowerCase().includes(q) || t.type.includes(q) || loc(t.description).toLowerCase().includes(q);
197
+ const matches = (t) => resolveLocalized(t.name).toLowerCase().includes(q) || t.type.includes(q) || resolveLocalized(t.description).toLowerCase().includes(q);
179
198
  return TRIGGER_TEMPLATES.filter(matches);
180
199
  });
181
200
  const nodeDrag = useNodeDrag();
@@ -205,24 +224,31 @@ function logoSrcOf(m) {
205
224
  const imgs = m.images ?? [];
206
225
  return (imgs.find((i) => i.category === "logo") ?? imgs[0])?.src ?? null;
207
226
  }
208
- function onManifestDragStart(m, ev) {
227
+ const paletteDragEnabled = ref(true);
228
+ function onVersionSelectLeave(ev) {
229
+ const el = ev.currentTarget;
230
+ if (el && el !== el.ownerDocument.activeElement) paletteDragEnabled.value = true;
231
+ }
232
+ function onManifestDragStart(leaf, ev) {
209
233
  if (!ev.dataTransfer) return;
234
+ const m = effManifest(leaf);
210
235
  ev.dataTransfer.setData("application/integration-node", JSON.stringify({ kind: "manifest", slug: m.slug, version: m.version }));
211
236
  ev.dataTransfer.effectAllowed = "move";
212
- setCustomDragImage(ev, loc(m.name), logoSrcOf(m), "#8b5cf6");
237
+ setCustomDragImage(ev, resolveLocalized(m.name), logoSrcOf(m), "#8b5cf6");
213
238
  nodeDrag.start({ kind: "manifest", manifest: m });
214
239
  }
215
240
  function onTriggerDragStart(tpl, ev) {
216
241
  if (!ev.dataTransfer) return;
217
242
  ev.dataTransfer.setData("application/integration-node", JSON.stringify({ kind: "trigger", type: tpl.type }));
218
243
  ev.dataTransfer.effectAllowed = "move";
219
- setCustomDragImage(ev, loc(tpl.name), null, "#10b981");
244
+ setCustomDragImage(ev, resolveLocalized(tpl.name), null, "#10b981");
220
245
  nodeDrag.start({ kind: "trigger", tpl });
221
246
  }
222
247
  function onDragEnd() {
223
248
  nodeDrag.end();
224
249
  }
225
- function insertManifest(m) {
250
+ function insertManifest(leaf) {
251
+ const m = effManifest(leaf);
226
252
  emit("insert", { kind: "manifest", slug: m.slug, version: m.version });
227
253
  }
228
254
  function insertTrigger(tpl) {
@@ -234,6 +260,9 @@ function categoryColor(label) {
234
260
  const cat = NODE_CATEGORIES.find((c) => c.label === label || CORE_GROUP_BY_CATEGORY[String(c.key)] === label);
235
261
  return cat?.color ?? "bg-slate-100 text-slate-500";
236
262
  }
263
+ function nodeCategoryColor(m) {
264
+ return NODE_CATEGORIES.find((c) => c.key === m.category)?.color ?? "bg-slate-100 text-slate-500";
265
+ }
237
266
  watch(tree, () => {
238
267
  if (stack.value.length && !triggersLevel.value && !currentGroup.value) stack.value = [];
239
268
  });
@@ -264,6 +293,7 @@ const searching = computed(() => search.value.trim().length > 0);
264
293
  ref="searchInput"
265
294
  v-model="search"
266
295
  type="text"
296
+ :aria-label="stack.length ? `Search in ${levelTitle}` : 'Search all nodes'"
267
297
  :placeholder="stack.length ? `Search in ${levelTitle}\u2026` : 'Search all nodes\u2026'"
268
298
  class="h-7 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"
269
299
  @keydown.esc="search ? search = '' : stack.length && popLevel()"
@@ -305,7 +335,7 @@ const searching = computed(() => search.value.trim().length > 0);
305
335
  <component :is="iconFor(t.icon)" :size="13" />
306
336
  </span>
307
337
  <div class="min-w-0">
308
- <div class="truncate text-sm font-medium">{{ loc(t.name) }}</div>
338
+ <div class="truncate text-sm font-medium">{{ resolveLocalized(t.name) }}</div>
309
339
  <div class="text-xs leading-snug text-muted-foreground">Trigger</div>
310
340
  </div>
311
341
  </div>
@@ -318,18 +348,36 @@ const searching = computed(() => search.value.trim().length > 0);
318
348
  v-for="hit in searchHits"
319
349
  :key="`${hit.manifest.slug}@${hit.manifest.version}`"
320
350
  class="group flex cursor-grab items-start gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
321
- draggable="true"
322
- :title="loc(hit.manifest.name)"
351
+ :draggable="paletteDragEnabled"
352
+ :title="resolveLocalized(hit.manifest.name)"
323
353
  @dragstart="onManifestDragStart(hit.manifest, $event)"
324
354
  @dragend="onDragEnd"
325
355
  @click="insertManifest(hit.manifest)"
326
356
  >
327
- <span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="categoryColor(hit.path[0] ?? '')">
328
- <IntegrationsNodeLogo :manifest="hit.manifest" :size="13" />
357
+ <span class="flex size-8 shrink-0 items-center justify-center rounded-md" :class="nodeCategoryColor(hit.manifest)">
358
+ <IntegrationsNodeMark :manifest="hit.manifest" :size="18" :badge="false" />
329
359
  </span>
330
360
  <div class="min-w-0 flex-1">
331
- <div class="truncate text-sm font-medium">{{ loc(hit.manifest.name) }}</div>
332
- <div class="text-xs leading-snug text-muted-foreground">{{ loc(hit.manifest.description) }}</div>
361
+ <div class="flex items-center gap-1.5">
362
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ resolveLocalized(hit.manifest.name) }}</span>
363
+ <select
364
+ v-if="versionsOf(hit.manifest.slug).length > 1"
365
+ :value="effVersion(hit.manifest)"
366
+ class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
367
+ :aria-label="`Version to insert for ${resolveLocalized(hit.manifest.name)}`"
368
+ @click.stop
369
+ @mousedown.stop
370
+ @change.stop="setSelectedVersion(hit.manifest.slug, $event.target.value)"
371
+ @mouseenter="paletteDragEnabled = false"
372
+ @mouseleave="onVersionSelectLeave"
373
+ @focus="paletteDragEnabled = false"
374
+ @blur="paletteDragEnabled = true"
375
+ >
376
+ <option v-for="v in versionsOf(hit.manifest.slug)" :key="v.version" :value="v.version">v{{ v.version }}</option>
377
+ </select>
378
+ <span v-else class="shrink-0 text-xs tabular-nums text-muted-foreground/60">v{{ hit.manifest.version }}</span>
379
+ </div>
380
+ <div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(hit.manifest.description) }}</div>
333
381
  <div class="truncate text-xs text-muted-foreground/70">{{ hit.path.join(" \u203A ") }}</div>
334
382
  </div>
335
383
  </div>
@@ -342,7 +390,7 @@ const searching = computed(() => search.value.trim().length > 0);
342
390
  :key="t.id"
343
391
  class="group flex cursor-grab items-center gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
344
392
  draggable="true"
345
- :title="loc(t.name)"
393
+ :title="resolveLocalized(t.name)"
346
394
  @dragstart="onTriggerDragStart(t, $event)"
347
395
  @dragend="onDragEnd"
348
396
  @click="insertTrigger(t)"
@@ -351,8 +399,8 @@ const searching = computed(() => search.value.trim().length > 0);
351
399
  <component :is="iconFor(t.icon)" :size="13" />
352
400
  </span>
353
401
  <div class="min-w-0">
354
- <div class="truncate text-sm font-medium">{{ loc(t.name) }}</div>
355
- <div class="text-xs leading-snug text-muted-foreground">{{ loc(t.description) }}</div>
402
+ <div class="truncate text-sm font-medium">{{ resolveLocalized(t.name) }}</div>
403
+ <div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(t.description) }}</div>
356
404
  </div>
357
405
  </div>
358
406
  </template>
@@ -400,7 +448,7 @@ const searching = computed(() => search.value.trim().length > 0);
400
448
  @click="pushPath(['__apps', label])"
401
449
  >
402
450
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-white ring-1 ring-black/[0.06]">
403
- <IntegrationsNodeLogo v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" />
451
+ <IntegrationsNodeMark v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" logo-only />
404
452
  <Lucide.Package v-else :size="13" class="text-violet-600" />
405
453
  </span>
406
454
  <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ label }}</span>
@@ -423,7 +471,7 @@ const searching = computed(() => search.value.trim().length > 0);
423
471
  class="flex size-6 shrink-0 items-center justify-center rounded-md"
424
472
  :class="groupLogoManifest(group) ? 'bg-white ring-1 ring-black/[0.06]' : 'bg-slate-100 text-slate-500'"
425
473
  >
426
- <IntegrationsNodeLogo v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" />
474
+ <IntegrationsNodeMark v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" logo-only />
427
475
  <Lucide.FolderOpen v-else :size="13" />
428
476
  </span>
429
477
  <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ label }}</span>
@@ -435,18 +483,36 @@ const searching = computed(() => search.value.trim().length > 0);
435
483
  v-for="m in currentGroup.leaves"
436
484
  :key="`${m.slug}@${m.version}`"
437
485
  class="group flex cursor-grab items-start gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
438
- draggable="true"
439
- :title="loc(m.name)"
486
+ :draggable="paletteDragEnabled"
487
+ :title="resolveLocalized(m.name)"
440
488
  @dragstart="onManifestDragStart(m, $event)"
441
489
  @dragend="onDragEnd"
442
490
  @click="insertManifest(m)"
443
491
  >
444
- <span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="hasLogo(m) ? 'bg-white ring-1 ring-black/[0.06]' : categoryColor(groupPath(m)[0] ?? '')">
445
- <IntegrationsNodeLogo :manifest="m" :size="14" />
492
+ <span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="nodeCategoryColor(m)">
493
+ <IntegrationsNodeMark :manifest="m" :size="14" :badge="false" />
446
494
  </span>
447
495
  <div class="min-w-0 flex-1">
448
- <div class="truncate text-sm font-medium">{{ loc(m.name) }}</div>
449
- <div class="text-xs leading-snug text-muted-foreground">{{ loc(m.description) }}</div>
496
+ <div class="flex items-center gap-1.5">
497
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ resolveLocalized(m.name) }}</span>
498
+ <select
499
+ v-if="versionsOf(m.slug).length > 1"
500
+ :value="effVersion(m)"
501
+ class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
502
+ :aria-label="`Version to insert for ${resolveLocalized(m.name)}`"
503
+ @click.stop
504
+ @mousedown.stop
505
+ @change.stop="setSelectedVersion(m.slug, $event.target.value)"
506
+ @mouseenter="paletteDragEnabled = false"
507
+ @mouseleave="onVersionSelectLeave"
508
+ @focus="paletteDragEnabled = false"
509
+ @blur="paletteDragEnabled = true"
510
+ >
511
+ <option v-for="v in versionsOf(m.slug)" :key="v.version" :value="v.version">v{{ v.version }}</option>
512
+ </select>
513
+ <span v-else class="shrink-0 text-xs tabular-nums text-muted-foreground/60">v{{ m.version }}</span>
514
+ </div>
515
+ <div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(m.description) }}</div>
450
516
  </div>
451
517
  </div>
452
518
  </template>
@@ -173,7 +173,9 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
173
173
  :model-value="row.method"
174
174
  @update:model-value="(v) => setRow(idx, { method: String(v) })"
175
175
  >
176
- <SelectTrigger class="h-7 w-40 shrink-0 bg-white text-sm">
176
+ <!-- A repeating row has no room for visible labels, so each
177
+ control names itself and says which row it is. -->
178
+ <SelectTrigger :aria-label="`Filter ${idx + 1} \u2014 comparison`" class="h-7 w-40 shrink-0 bg-white text-sm">
177
179
  <SelectValue />
178
180
  </SelectTrigger>
179
181
  <SelectContent>
@@ -184,6 +186,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
184
186
  </Select>
185
187
  <Input
186
188
  v-if="rowMeta[idx]?.attribute"
189
+ :aria-label="`Filter ${idx + 1} \u2014 field`"
187
190
  :model-value="row.attribute"
188
191
  placeholder="field"
189
192
  class="h-7 w-36 shrink-0 bg-white font-mono text-sm"
@@ -191,6 +194,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
191
194
  />
192
195
  <Input
193
196
  v-if="rowMeta[idx]?.values !== 'none'"
197
+ :aria-label="`Filter ${idx + 1} \u2014 value`"
194
198
  :model-value="row.values"
195
199
  :placeholder="rowMeta[idx]?.values === 'list' ? 'value, value, \u2026' : 'value'"
196
200
  class="h-7 min-w-0 flex-1 bg-white text-sm"
@@ -200,6 +204,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
200
204
  </template>
201
205
  <Input
202
206
  v-else
207
+ :aria-label="`Filter ${idx + 1} \u2014 raw text`"
203
208
  :model-value="row.text"
204
209
  class="h-7 min-w-0 flex-1 bg-white font-mono text-sm"
205
210
  title="This entry is not in the builder vocabulary; edit it as text or in the JSON view."
@@ -207,6 +212,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
207
212
  />
208
213
  <button
209
214
  type="button"
215
+ :aria-label="`Remove filter ${idx + 1}`"
210
216
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
211
217
  @click="removeRow(idx)"
212
218
  >
@@ -227,6 +233,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
227
233
 
228
234
  <template v-else>
229
235
  <Textarea
236
+ aria-label="Filters as JSON"
230
237
  :model-value="jsonDraft"
231
238
  :rows="6"
232
239
  spellcheck="false"
@@ -68,7 +68,9 @@ function confirmTerminate() {
68
68
  <template>
69
69
  <DropdownMenu>
70
70
  <DropdownMenuTrigger as-child>
71
- <Button variant="ghost" size="icon" class="size-7" :disabled="busy" @click.stop>
71
+ <!-- Icon-only, so it needs a name of its own — the run id is the only
72
+ thing that tells two rows' menus apart. -->
73
+ <Button variant="ghost" size="icon" class="size-7" :disabled="busy" :aria-label="`Actions for run #${run.id}`" @click.stop>
72
74
  <MoreHorizontal :size="16" />
73
75
  </Button>
74
76
  </DropdownMenuTrigger>