@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
@@ -1,12 +1,14 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
2
  import { IntegrationsApiError } from "../utils/apiError.js";
3
+ import { useLoadGate } from "./useLoadGate.js";
3
4
  export function useIntegrationsTriggers() {
4
5
  const all = useState("integrations.triggers.all", () => []);
5
6
  const loading = useState("integrations.triggers.loading", () => false);
6
7
  const error = useState("integrations.triggers.error", () => null);
7
8
  const loaded = useState("integrations.triggers.loaded", () => false);
8
9
  const api = useIntegrationsApi();
9
- const { workflows, load: loadWorkflows } = useIntegrationsWorkflows();
10
+ const { workflows, load: loadWorkflows, loaded: workflowsLoaded } = useIntegrationsWorkflows();
11
+ const gate = useLoadGate("integrations.triggers", { loading, loaded });
10
12
  function unwrapList(res) {
11
13
  if (Array.isArray(res)) return res;
12
14
  return res?.data ?? [];
@@ -15,9 +17,7 @@ export function useIntegrationsTriggers() {
15
17
  return unwrapList(await api.get(`/workflows/${workflowId}/triggers`));
16
18
  }
17
19
  async function loadAll(force = false) {
18
- if (loading.value) return all.value;
19
- if (loaded.value && !force) return all.value;
20
- loading.value = true;
20
+ if (!await gate.claim(force)) return all.value;
21
21
  error.value = null;
22
22
  try {
23
23
  await loadWorkflows();
@@ -25,11 +25,11 @@ export function useIntegrationsTriggers() {
25
25
  workflows.value.map(async (wf) => (await list(wf.id)).map((t) => ({ ...t, workflowName: wf.name })))
26
26
  );
27
27
  all.value = settled.flatMap((p) => p.status === "fulfilled" ? p.value : []);
28
- loaded.value = true;
28
+ loaded.value = workflowsLoaded.value && settled.every((p) => p.status === "fulfilled");
29
29
  } catch (e) {
30
30
  error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? "Could not load triggers.";
31
31
  } finally {
32
- loading.value = false;
32
+ gate.release();
33
33
  }
34
34
  return all.value;
35
35
  }
@@ -1,6 +1,7 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { IntegrationsApiError } from "../utils/apiError.js";
4
+ import { useLoadGate } from "./useLoadGate.js";
4
5
  const BLOB_VERSION = "v0-draft";
5
6
  function toCockpit(api) {
6
7
  const apiBlob = api.blob ?? {};
@@ -35,6 +36,10 @@ function toCockpit(api) {
35
36
  execution_mode: api.execution_mode,
36
37
  buildStatus: api.build_status ?? null,
37
38
  buildError: api.build_error ?? null,
39
+ // Carried through as-is: `undefined` (e.g. the list response, which omits
40
+ // it) means "unknown", NOT "valid" — callers must fail open and only gate
41
+ // activation on a non-empty array.
42
+ warnings: api.warnings,
38
43
  created_at: api.created_at ?? "",
39
44
  updated_at: api.updated_at ?? ""
40
45
  };
@@ -76,6 +81,7 @@ export function useIntegrationsWorkflows() {
76
81
  const loading = useState("integrations.workflows.loading", () => false);
77
82
  const error = useState("integrations.workflows.error", () => null);
78
83
  const loaded = useState("integrations.workflows.loaded", () => false);
84
+ const gate = useLoadGate("integrations.workflows", { loading, loaded });
79
85
  const api = useIntegrationsApi();
80
86
  function unwrap(res) {
81
87
  return res?.data ?? res;
@@ -89,9 +95,7 @@ export function useIntegrationsWorkflows() {
89
95
  workflows.value = idx < 0 ? [...workflows.value, wf] : workflows.value.map((w) => w.id === wf.id ? wf : w);
90
96
  }
91
97
  async function load(force = false) {
92
- if (loading.value) return workflows.value;
93
- if (loaded.value && !force) return workflows.value;
94
- loading.value = true;
98
+ if (!await gate.claim(force)) return workflows.value;
95
99
  error.value = null;
96
100
  try {
97
101
  const list2 = unwrap(await api.get("/workflows")) ?? [];
@@ -100,7 +104,7 @@ export function useIntegrationsWorkflows() {
100
104
  } catch (e) {
101
105
  error.value = captureError(e, "Could not load workflows.");
102
106
  } finally {
103
- loading.value = false;
107
+ gate.release();
104
108
  }
105
109
  return workflows.value;
106
110
  }
@@ -0,0 +1,17 @@
1
+ import type { Ref } from 'vue';
2
+ import type { LoadGate } from '../utils/loadGate.js';
3
+ /**
4
+ * A composable's load gate, keyed by the namespace its own state already uses:
5
+ * `useLoadGate('integrations.triggers', { loading, loaded })`.
6
+ *
7
+ * The rule lives in `utils/loadGate.ts`; this is the wiring, and the wiring is the
8
+ * reason it is a composable. The counters have to share the flag's fate — one per
9
+ * Nuxt app on the client, one per request on the server — so they are `useState`
10
+ * like the flag itself. A module-level counter would be shared between two
11
+ * concurrent server-rendered requests, and then one tenant's finished load would
12
+ * answer another tenant's forced caller.
13
+ */
14
+ export declare function useLoadGate(key: string, flags: {
15
+ loading: Ref<boolean>;
16
+ loaded?: Ref<boolean>;
17
+ }): LoadGate;
@@ -0,0 +1,8 @@
1
+ import { createLoadGate } from "../utils/loadGate.js";
2
+ export function useLoadGate(key, flags) {
3
+ return createLoadGate({
4
+ ...flags,
5
+ started: useState(`${key}.started`, () => 0),
6
+ finished: useState(`${key}.finished`, () => 0)
7
+ });
8
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Pure core of the tidy-up branch reorder (see WorkflowEditor's
3
+ * `reorderBranchesByHandle`): given laid-out node positions and the outgoing
4
+ * edges tagged with their output-port index, compute the x moves that put
5
+ * each source's branch subtrees into handle order.
6
+ *
7
+ * dagre places nodes as points and is blind to which output HANDLE an edge
8
+ * leaves from, so a node's branches can land in the opposite left-to-right
9
+ * order to their handles and cross right under the node. Only self-contained,
10
+ * pairwise-disjoint subtrees are moved — a reconverging branch (diamond,
11
+ * shared descendant, outside parent) leaves the whole node untouched rather
12
+ * than corrupted. Idempotent: an already-ordered node yields no moves.
13
+ *
14
+ * The crossing is owned by the direct CHILD NODE's own x, not the subtree's
15
+ * extent: dagre centres a node over its (possibly wide) subtree, so a spine —
16
+ * a long main chain where each step also hangs off a short error/leaf branch —
17
+ * puts the leaf to the LEFT while the continuation node sits at its subtree's
18
+ * centre to the right, crossing out/error even though the two subtrees never
19
+ * share a node. So we reorder by the child nodes' positions: the children keep
20
+ * exactly the set of x slots they already occupy, re-slotted into handle order.
21
+ * That's a pure permutation — the rank's footprint (and the source's centre
22
+ * over it) is preserved and no branch is flung wide, whatever its subtree size.
23
+ *
24
+ * Kept free of vue-flow/component state so it can be unit-tested against
25
+ * hand-built graphs.
26
+ */
27
+ export interface ReorderNode {
28
+ x: number;
29
+ }
30
+ export interface ReorderEdge {
31
+ source: string;
32
+ target: string;
33
+ /** Left-to-right index of the source's output handle this edge leaves from. */
34
+ portIndex: number;
35
+ }
36
+ /** Node id → new x, for exactly the nodes that need to move. */
37
+ export declare function reorderBranchSubtrees(nodes: Map<string, ReorderNode>, edges: ReorderEdge[]): Map<string, number>;
@@ -0,0 +1,71 @@
1
+ export function reorderBranchSubtrees(nodes, edges) {
2
+ const inGraph = (e) => nodes.has(e.source) && nodes.has(e.target);
3
+ const children = /* @__PURE__ */ new Map();
4
+ const parents = /* @__PURE__ */ new Map();
5
+ for (const e of edges) {
6
+ if (!inGraph(e)) continue;
7
+ (children.get(e.source) ?? (children.set(e.source, []), children.get(e.source))).push(e.target);
8
+ (parents.get(e.target) ?? (parents.set(e.target, []), parents.get(e.target))).push(e.source);
9
+ }
10
+ const subtreeOf = (root) => {
11
+ const seen = /* @__PURE__ */ new Set();
12
+ const stack = [root];
13
+ while (stack.length) {
14
+ const v = stack.pop();
15
+ if (seen.has(v)) continue;
16
+ seen.add(v);
17
+ for (const c of children.get(v) ?? []) if (!seen.has(c)) stack.push(c);
18
+ }
19
+ return seen;
20
+ };
21
+ const branchesBySource = /* @__PURE__ */ new Map();
22
+ for (const e of edges) {
23
+ if (!inGraph(e)) continue;
24
+ const byTarget = branchesBySource.get(e.source) ?? (branchesBySource.set(e.source, /* @__PURE__ */ new Map()), branchesBySource.get(e.source));
25
+ byTarget.set(e.target, Math.min(byTarget.get(e.target) ?? Infinity, e.portIndex));
26
+ }
27
+ const moved = /* @__PURE__ */ new Map();
28
+ const xOf = (id) => moved.get(id) ?? nodes.get(id).x;
29
+ for (const [source, byTarget] of branchesBySource) {
30
+ const targets = [...byTarget].map(([target, idx]) => ({ target, idx }));
31
+ if (targets.length < 2) continue;
32
+ const subtrees = new Map(targets.map((t) => [t.target, subtreeOf(t.target)]));
33
+ let safe = true;
34
+ const union = /* @__PURE__ */ new Set();
35
+ outer: for (const t of targets) {
36
+ const st = subtrees.get(t.target);
37
+ if (st.has(source)) {
38
+ safe = false;
39
+ break;
40
+ }
41
+ for (const m of st) {
42
+ if (union.has(m)) {
43
+ safe = false;
44
+ break outer;
45
+ }
46
+ union.add(m);
47
+ for (const p of parents.get(m) ?? []) {
48
+ if (p !== source && !st.has(p)) {
49
+ safe = false;
50
+ break outer;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ if (!safe) continue;
56
+ const desired = [...targets].sort((a, b) => a.idx - b.idx || a.target.localeCompare(b.target));
57
+ const current = [...targets].sort((a, b) => xOf(a.target) - xOf(b.target) || a.target.localeCompare(b.target));
58
+ if (current.every((t, i) => t.target === desired[i].target)) continue;
59
+ const slots = current.map((t) => xOf(t.target));
60
+ for (let i = 0; i < desired.length; i++) {
61
+ const target = desired[i].target;
62
+ const dx = slots[i] - xOf(target);
63
+ if (dx !== 0) {
64
+ for (const m of subtrees.get(target)) {
65
+ moved.set(m, xOf(m) + dx);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return moved;
71
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reading the logical port name out of a canvas handle id.
3
+ *
4
+ * Each node on the canvas renders four connection points per port, one per
5
+ * side, so that an edge can leave and arrive without crossing a card body. The
6
+ * side rides along in the handle id as a `__<side>` suffix; the workflow blob
7
+ * stores only the port name. Everything that persists an edge, or reasons about
8
+ * which port an edge left from, has to read the name back out — and an id whose
9
+ * name is missing has to be told apart from one whose name is unusual.
10
+ *
11
+ * An output dot is `<port>__<side>`. A node's single input dot is `__in__<side>`
12
+ * and a group frame's is `__in__top`: the separator sits at the very front, so
13
+ * there is no port name in front of it. Those answer `null` — **not** the empty
14
+ * string, which is what a `slice(0, 0)` would hand back. Every caller writes
15
+ * `portFromHandleId(id) ?? 'default'`, and `??` does not fire on `''`, so an
16
+ * empty name would travel on past the fallback that exists to catch it.
17
+ *
18
+ * It lives here rather than in the editor because it is the one place that knows
19
+ * how to read an id, and because that is what makes it testable: the editor is
20
+ * an SFC, out of reach of the unit suite, and the defect above shipped once
21
+ * already without anything going red.
22
+ */
23
+ export declare function portFromHandleId(handleId: string | null | undefined): string | null;
@@ -0,0 +1,6 @@
1
+ const SIDE_SUFFIX = /__(?:top|right|bottom|left)$/;
2
+ export function portFromHandleId(handleId) {
3
+ if (!handleId) return null;
4
+ const name = handleId.replace(SIDE_SUFFIX, "");
5
+ return name.startsWith("__") || !name ? null : name;
6
+ }
@@ -3,9 +3,48 @@ import type { WorkflowTemplate } from '../types/integrations.js';
3
3
  * Category lens config for the Templates catalogue. The templates themselves
4
4
  * come live from the Integrations registry (`GET /templates`) — there is no
5
5
  * mock catalogue; an empty registry is an empty catalogue.
6
+ *
7
+ * The two categories that templates actually ship with today are `commerce` and
8
+ * `sales`: the eight templates in `@revenexx/integrations-nodes-core` are all
9
+ * `commerce`, the two in the Business Central package `sales`. Neither was named
10
+ * here, so both were shown by the fallback rather than by a label — the studio's
11
+ * one list of categories described none of the catalogue it had. The other six
12
+ * are named on spec and nothing publishes them yet.
6
13
  */
7
14
  export declare const TEMPLATE_CATEGORIES: Array<{
8
15
  key: WorkflowTemplate['category'];
9
16
  label: string;
10
17
  color: string;
11
18
  }>;
19
+ /**
20
+ * What a template's category is called, wherever the studio shows one.
21
+ *
22
+ * Four surfaces draw a category chip — the gallery, a template's own page, the
23
+ * dashboard's template panel and the start-from-a-template list in the dialog that
24
+ * creates a workflow — and each worked the name out for itself: a lookup with a
25
+ * capitalisation fallback, the raw registry key, and two separate capitalisations.
26
+ * They agreed only by coincidence, because every label in `TEMPLATE_CATEGORIES`
27
+ * above happens to be its key capitalised.
28
+ *
29
+ * That coincidence ends with the first category whose name is not a capitalisation
30
+ * — `crm`, `b2b`, `erp` — and categories arrive with node packages rather than with
31
+ * a release of the studio, so it is a matter of when. Every chip goes through here
32
+ * instead.
33
+ *
34
+ * The fallback is the catalogue's own promise (`specs/templates.md` AC-2): a
35
+ * category the studio has never been taught still gets a chip, named as well as it
36
+ * can be rather than dropped. That guarantee held on the gallery alone before; it
37
+ * now holds on all four.
38
+ */
39
+ export declare function templateCategoryLabel(key: string): string;
40
+ /**
41
+ * The integration a template ships with, as a card badges it — and as the
42
+ * gallery's search matches it. The core toolbox stays unbadged: every template
43
+ * would carry it, so it would say nothing.
44
+ *
45
+ * It lives here for the same reason `templateCategoryLabel` does. The gallery
46
+ * both shows this label and searches by it, so a copy inside the card component
47
+ * would be a second implementation of a name — the exact duplication PO-243
48
+ * removes.
49
+ */
50
+ export declare function templatePackageLabel(tpl: Pick<WorkflowTemplate, 'package'>): string | null;
@@ -1,5 +1,7 @@
1
1
  export const TEMPLATE_CATEGORIES = [
2
2
  // Uniform muted pill — the label carries the meaning, no rainbow needed.
3
+ { key: "commerce", label: "Commerce", color: "bg-slate-100 text-slate-700" },
4
+ { key: "sales", label: "Sales", color: "bg-slate-100 text-slate-700" },
3
5
  { key: "sync", label: "Sync", color: "bg-slate-100 text-slate-700" },
4
6
  { key: "notification", label: "Notification", color: "bg-slate-100 text-slate-700" },
5
7
  { key: "fulfilment", label: "Fulfilment", color: "bg-slate-100 text-slate-700" },
@@ -7,3 +9,16 @@ export const TEMPLATE_CATEGORIES = [
7
9
  { key: "finance", label: "Finance", color: "bg-slate-100 text-slate-700" },
8
10
  { key: "reporting", label: "Reporting", color: "bg-slate-100 text-slate-700" }
9
11
  ];
12
+ export function templateCategoryLabel(key) {
13
+ if (!key) return "";
14
+ const known = TEMPLATE_CATEGORIES.find((c) => c.key === key);
15
+ if (known) return known.label;
16
+ return key.charAt(0).toUpperCase() + key.slice(1);
17
+ }
18
+ export function templatePackageLabel(tpl) {
19
+ const pkg = tpl.package;
20
+ if (!pkg) return null;
21
+ const tail = (pkg.name.split("/").pop() ?? pkg.name).replace(/^integrations-nodes-/, "");
22
+ if (tail === "core") return null;
23
+ return pkg.label ?? tail.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
24
+ }
@@ -137,7 +137,7 @@ export const SAMPLE_WORKFLOWS = [
137
137
  }
138
138
  ],
139
139
  variables: {
140
- source: { type: "string", default: "primary", required: false }
140
+ source: { type: "string", default: "primary" }
141
141
  },
142
142
  nodes: [
143
143
  {
@@ -0,0 +1,2 @@
1
+ /** Resolve a manifest icon name to a Lucide component (Box as last resort). */
2
+ export declare function nodeIcon(name?: string | null): unknown;
@@ -0,0 +1,73 @@
1
+ import * as Lucide from "lucide-vue-next";
2
+ const MDI_TO_LUCIDE = {
3
+ "account-group-outline": "Users",
4
+ "account-outline": "User",
5
+ "account-plus-outline": "UserPlus",
6
+ "alert-octagon": "OctagonAlert",
7
+ "api": "Webhook",
8
+ "book-open-outline": "BookOpen",
9
+ "bullseye-arrow": "Target",
10
+ "calendar-check-outline": "CalendarCheck",
11
+ "card-account-details-outline": "IdCard",
12
+ "cart-outline": "ShoppingCart",
13
+ "cart-plus": "ShoppingBasket",
14
+ "circle-outline": "Circle",
15
+ "clock-outline": "Clock",
16
+ "cloud-download": "CloudDownload",
17
+ "cloud-upload": "CloudUpload",
18
+ "cloud-upload-outline": "CloudUpload",
19
+ "code-brackets": "Brackets",
20
+ "code-json": "Braces",
21
+ "code-tags": "Code",
22
+ "compare-horizontal": "ArrowLeftRight",
23
+ "currency-eur": "Euro",
24
+ "database-cog-outline": "Database",
25
+ "database-export-outline": "HardDriveUpload",
26
+ "database-import-outline": "HardDriveDownload",
27
+ "domain": "Building2",
28
+ "email-send": "Send",
29
+ "file-edit-outline": "FilePen",
30
+ "file-export-outline": "FileOutput",
31
+ "file-import-outline": "FileInput",
32
+ "file-move-outline": "FileSymlink",
33
+ "file-plus": "FilePlus",
34
+ "file-question-outline": "FileQuestion",
35
+ "file-remove-outline": "FileX",
36
+ "filter": "Filter",
37
+ "find-replace": "Replace",
38
+ "folder-download-outline": "FolderDown",
39
+ "folder-plus-outline": "FolderPlus",
40
+ "folder-search-outline": "FolderSearch",
41
+ "folder-upload-outline": "FolderUp",
42
+ "folder-zip-outline": "FolderArchive",
43
+ "format-list-bulleted-type": "List",
44
+ "handshake": "Handshake",
45
+ "handshake-outline": "Handshake",
46
+ "link-lock": "Link2",
47
+ "magnify": "Search",
48
+ "office-building-outline": "Building",
49
+ "pencil-box-outline": "SquarePen",
50
+ "pipe": "Workflow",
51
+ "receipt-outline": "Receipt",
52
+ "receipt-text-outline": "ReceiptText",
53
+ "regex": "Regex",
54
+ "repeat": "Repeat",
55
+ "ruler": "Ruler",
56
+ "source-branch": "GitBranch",
57
+ "tag-multiple-outline": "Tags",
58
+ "transfer": "ArrowRightLeft",
59
+ "translate": "Languages",
60
+ "truck-outline": "Truck",
61
+ "web": "Globe",
62
+ "xml": "CodeXml"
63
+ };
64
+ export function nodeIcon(name) {
65
+ if (!name) return Lucide.Box;
66
+ const bare = name.includes(":") ? name.split(":").pop() : name;
67
+ const mapped = MDI_TO_LUCIDE[bare];
68
+ if (mapped && Lucide[mapped]) {
69
+ return Lucide[mapped];
70
+ }
71
+ const pascal = bare.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
72
+ return Lucide[pascal] || Lucide.Box;
73
+ }
@@ -27,11 +27,23 @@ export interface PortEntry {
27
27
  /** Declared payload fields of this port (schema; may be empty). */
28
28
  fields: FieldEntry[];
29
29
  }
30
+ /**
31
+ * A manifest declaring zero output ports describes a TERMINAL node (PO-201):
32
+ * the run ends there, so it gets no output handle and no edge may leave it.
33
+ *
34
+ * The distinction that matters is **declared empty vs. not known**. An absent
35
+ * manifest — a node that is not installed — is not terminal; we simply cannot
36
+ * work out its ports, and it has to stay wirable. Same for a `sourceFromConfig`
37
+ * or `resolveOutputs` set that expands to nothing: unresolved, not terminal.
38
+ * Only an explicit `[]` is the node making a statement about itself.
39
+ */
40
+ export declare function isTerminalManifest(manifest: NodeManifest | null | undefined): boolean;
30
41
  /**
31
42
  * The output ports one upstream node exposes, mirroring the canvas port
32
- * derivation: named ports map straight over, `sourceFromConfig` ports expand
33
- * from the node's config values (plus the optional fallback), and a node
34
- * without any resolvable port still gets its `default`. Server-resolved
35
- * dynamic outputs, when present, replace the manifest's static list.
43
+ * derivation in `WorkflowNodeView.vue`. Named ports map straight over,
44
+ * `sourceFromConfig` ports expand from the node's config values (plus the
45
+ * optional fallback), and a node whose ports could not be worked out still
46
+ * gets its `default`. Server-resolved dynamic outputs, when present, replace
47
+ * the manifest's static list.
36
48
  */
37
49
  export declare function portsForNode(node: WorkflowNode, manifest: NodeManifest | null, resolvedOutputs?: NodeManifestOutput[]): PortEntry[];
@@ -44,7 +44,11 @@ function fieldEntries(raw) {
44
44
  }
45
45
  return [];
46
46
  }
47
+ export function isTerminalManifest(manifest) {
48
+ return !!manifest && Array.isArray(manifest.outputs) && !manifest.outputs.length;
49
+ }
47
50
  export function portsForNode(node, manifest, resolvedOutputs) {
51
+ if (isTerminalManifest(manifest)) return [];
48
52
  const list = resolvedOutputs?.length ? resolvedOutputs : manifest?.outputs ?? [];
49
53
  const ports = [];
50
54
  for (const o of list) {
@@ -16,8 +16,15 @@ export interface PayloadField {
16
16
  path: string;
17
17
  label: string;
18
18
  }
19
- /** Scan the blob's node configs and derive the payload form fields. */
20
- export declare function payloadFieldsFromBlob(blob: Pick<WorkflowBlob, 'nodes'>): PayloadField[];
19
+ /**
20
+ * Payload form fields: fields DECLARED on a manual trigger come first (the
21
+ * author's explicit contract, e.g. a file field for a manual import), then
22
+ * every `${{ trigger.payload.* }}` reference found in node configs that the
23
+ * declarations don't already cover.
24
+ */
25
+ export declare function payloadFieldsFromBlob(blob: Pick<WorkflowBlob, 'nodes'> & {
26
+ triggers?: WorkflowBlob['triggers'];
27
+ }): PayloadField[];
21
28
  export declare function setPath(target: Record<string, unknown>, path: string, value: unknown): void;
22
29
  export declare function getPath(source: Record<string, unknown>, path: string): unknown;
23
30
  export interface AttachedFile {
@@ -14,9 +14,23 @@ function labelFor(path) {
14
14
  return last.replace(/[_-]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
15
15
  }
16
16
  export function payloadFieldsFromBlob(blob) {
17
+ const declared = [];
18
+ for (const t of blob.triggers ?? []) {
19
+ if (t.type !== "manual") continue;
20
+ for (const d of t.config?.payloadFields ?? []) {
21
+ const path = d.path?.trim();
22
+ if (!path && d.kind !== "file") continue;
23
+ declared.push({
24
+ kind: d.kind === "file" ? "file" : "text",
25
+ path: path ?? "",
26
+ label: d.label?.trim() || labelFor(path || "file")
27
+ });
28
+ }
29
+ }
17
30
  const refs = /* @__PURE__ */ new Set();
18
31
  for (const node of blob.nodes ?? []) collectRefs(node.config, refs);
19
- if (refs.size === 0) return [];
32
+ if (refs.size === 0 && declared.length === 0) return [];
33
+ const declaredPaths = new Set(declared.map((f) => f.path));
20
34
  const byPrefix = /* @__PURE__ */ new Map();
21
35
  for (const ref of refs) {
22
36
  const idx = ref.lastIndexOf(".");
@@ -26,9 +40,15 @@ export function payloadFieldsFromBlob(blob) {
26
40
  byPrefix.get(prefix).add(leaf);
27
41
  }
28
42
  const fields = [];
29
- const consumed = /* @__PURE__ */ new Set();
43
+ const consumed = new Set(declaredPaths);
30
44
  for (const [prefix, leaves] of byPrefix) {
31
45
  if (leaves.has("data")) {
46
+ if (declaredPaths.has(prefix)) {
47
+ for (const leaf of leaves) {
48
+ if (FILE_KEYS.has(leaf)) consumed.add(prefix === "" ? leaf : `${prefix}.${leaf}`);
49
+ }
50
+ continue;
51
+ }
32
52
  fields.push({ kind: "file", path: prefix, label: prefix === "" ? "File" : labelFor(prefix) });
33
53
  for (const leaf of leaves) {
34
54
  if (FILE_KEYS.has(leaf)) consumed.add(prefix === "" ? leaf : `${prefix}.${leaf}`);
@@ -37,9 +57,13 @@ export function payloadFieldsFromBlob(blob) {
37
57
  }
38
58
  for (const ref of [...refs].sort()) {
39
59
  if (consumed.has(ref)) continue;
60
+ if (declared.some((d) => d.kind === "file" && (d.path === "" ? FILE_KEYS.has(ref) : ref.startsWith(d.path + ".")))) continue;
40
61
  fields.push({ kind: "text", path: ref, label: labelFor(ref) });
41
62
  }
42
- return fields.sort((a, b) => a.kind === b.kind ? a.path.localeCompare(b.path) : a.kind === "file" ? -1 : 1);
63
+ return [
64
+ ...declared,
65
+ ...fields.sort((a, b) => a.kind === b.kind ? a.path.localeCompare(b.path) : a.kind === "file" ? -1 : 1)
66
+ ];
43
67
  }
44
68
  export function setPath(target, path, value) {
45
69
  if (path === "") {
@@ -0,0 +1,17 @@
1
+ import type { OutputKind } from '../types/integrations.js';
2
+ /**
3
+ * The colour of the dot in front of a port name, wherever a port is listed
4
+ * rather than drawn on the canvas: the Input panel's upstream trees and the
5
+ * node's own output schema.
6
+ *
7
+ * The legend mirrors `portTone` on the canvas node (PO-197): green = a real,
8
+ * active output — the normal continuation, including switch cases, iterator
9
+ * `item` and If/Filter `true`; soft rose = the error output that only runs on
10
+ * failure; neutral slate for `false` and fallbacks. There is no separate branch
11
+ * colour — the labels already say "one of several".
12
+ *
13
+ * It lives here rather than in a component because two lists render it and they
14
+ * must never disagree. They did once: the schema list kept a private copy and
15
+ * silently held the pre-PO-197 palette while the Input panel moved on.
16
+ */
17
+ export declare function portDotClass(kind: OutputKind, name: string): string;
@@ -0,0 +1,7 @@
1
+ export function portDotClass(kind, name) {
2
+ if (name === "true") return "bg-emerald-500";
3
+ if (name === "false") return "bg-slate-500";
4
+ if (name === "error" || kind === "error") return "bg-rose-300";
5
+ if (name === "done") return "bg-emerald-500";
6
+ return "bg-emerald-500";
7
+ }
@@ -0,0 +1,18 @@
1
+ import type { WorkflowTrigger } from '../types/integrations.js';
2
+ /**
3
+ * Mapping between the webhook Protection select and the ingress verification
4
+ * profiles stored on the trigger (`authProfile`/`authConfig`). Bearer is the
5
+ * token profile with a fixed Authorization + "Bearer " prefix.
6
+ *
7
+ * An ABSENT authProfile reads as 'token': that is what the exposure defaults
8
+ * to server-side (and what every endpoint provisioned before this feature
9
+ * got), so the select shows the effective behavior. The UI persists the
10
+ * choice explicitly the moment the public toggle is enabled, so anything
11
+ * exposed from here on carries what it shows.
12
+ */
13
+ export type WebhookAuthChoice = 'none' | 'token' | 'bearer' | 'basic';
14
+ type TriggerConfig = WorkflowTrigger['config'];
15
+ export declare function authChoiceFromConfig(config: TriggerConfig | undefined): WebhookAuthChoice;
16
+ /** The config patch that makes a Protection choice stored state. */
17
+ export declare function configPatchForAuthChoice(choice: WebhookAuthChoice): Partial<TriggerConfig>;
18
+ export {};
@@ -0,0 +1,19 @@
1
+ export function authChoiceFromConfig(config) {
2
+ if (config?.authProfile === "none") return "none";
3
+ if (config?.authProfile === "basic") return "basic";
4
+ if (config?.authProfile === "token" && config?.authConfig?.prefix === "Bearer ") return "bearer";
5
+ return "token";
6
+ }
7
+ export function configPatchForAuthChoice(choice) {
8
+ switch (choice) {
9
+ case "none":
10
+ return { authProfile: "none", authConfig: void 0 };
11
+ case "bearer":
12
+ return { authProfile: "token", authConfig: { header: "Authorization", prefix: "Bearer " } };
13
+ case "basic":
14
+ return { authProfile: "basic", authConfig: void 0 };
15
+ case "token":
16
+ default:
17
+ return { authProfile: "token", authConfig: void 0 };
18
+ }
19
+ }
@@ -37,6 +37,7 @@ import {
37
37
  WalletMoney
38
38
  } from "#solar-icons";
39
39
  import { INTEGRATION_NODE_CATALOG } from "./integrationNodeCatalog.js";
40
+ import { resolveLocalized } from "../utils/localize.js";
40
41
  export const WORKFLOW_SOLAR_ICONS = {
41
42
  Programming,
42
43
  Bolt,
@@ -78,10 +79,6 @@ export const WORKFLOW_SOLAR_ICONS = {
78
79
  export function solarIconFor(name) {
79
80
  return (name ? WORKFLOW_SOLAR_ICONS[name] : void 0) ?? Programming;
80
81
  }
81
- function loc(v) {
82
- if (!v) return "";
83
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
84
- }
85
82
  const UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
86
83
  const IMAGE_URL_ID_RE = new RegExp(`node-packages/images/(${UUID})`, "i");
87
84
  const BARE_UUID_RE = new RegExp(`^${UUID}$`, "i");
@@ -109,7 +106,7 @@ export function nodeLogoOptions() {
109
106
  if (!logo?.src) continue;
110
107
  const id = imageIdFromSrc(logo.src) ?? logo.src;
111
108
  if (byId.has(id)) continue;
112
- const label = m.__package?.label || (m.groups?.length ? loc(m.groups[0]) : "") || loc(m.name);
109
+ const label = m.__package?.label || (m.groups?.length ? resolveLocalized(m.groups[0]) : "") || resolveLocalized(m.name);
113
110
  byId.set(id, { label: label || "Integration", src: logo.src });
114
111
  }
115
112
  return [...byId.entries()].map(([id, v]) => ({ id, src: v.src, label: v.label })).sort((a, b) => a.label.localeCompare(b.label));