@revenexx/studio-integrations 1.1.0 → 1.2.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 (98) hide show
  1. package/CHANGELOG.md +520 -0
  2. package/README.md +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
  7. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
  8. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  9. package/dist/runtime/components/Integrations/ExpressionInput.vue +77 -22
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  11. package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
  12. package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
  13. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
  14. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
  15. package/dist/runtime/components/Integrations/NodeConfigField.vue +23 -0
  16. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  17. package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
  18. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  19. package/dist/runtime/components/Integrations/RunActionsMenu.vue +5 -3
  20. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  21. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  22. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  23. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  24. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  25. package/dist/runtime/components/Integrations/RunsTable.vue +143 -20
  26. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  27. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  28. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  29. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  30. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  31. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  32. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  33. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  34. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  35. package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
  36. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  37. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  38. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  39. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  40. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  41. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  42. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  43. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  44. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  45. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  47. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  48. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  49. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  50. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  51. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  52. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  53. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  54. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  55. package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
  56. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  57. package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
  58. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  59. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  60. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  61. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  62. package/dist/runtime/data/expressionEditor.js +11 -5
  63. package/dist/runtime/data/handleId.d.ts +61 -1
  64. package/dist/runtime/data/handleId.js +8 -1
  65. package/dist/runtime/data/payloadForm.js +3 -2
  66. package/dist/runtime/i18n/messages/de.json +30 -11
  67. package/dist/runtime/i18n/messages/en.json +27 -8
  68. package/dist/runtime/pages/integrations/audit.vue +7 -6
  69. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  70. package/dist/runtime/pages/integrations/index.vue +70 -12
  71. package/dist/runtime/pages/integrations/runs.vue +116 -7
  72. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  73. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  74. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  75. package/dist/runtime/pages/integrations/workflows/[id].vue +177 -28
  76. package/dist/runtime/types/integrations.d.ts +5 -0
  77. package/dist/runtime/utils/apiError.d.ts +21 -1
  78. package/dist/runtime/utils/apiError.js +1 -1
  79. package/dist/runtime/utils/clipboard.d.ts +33 -0
  80. package/dist/runtime/utils/clipboard.js +11 -0
  81. package/dist/runtime/utils/deepClone.d.ts +41 -0
  82. package/dist/runtime/utils/deepClone.js +3 -0
  83. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  84. package/dist/runtime/utils/formatDuration.js +13 -0
  85. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  86. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  87. package/dist/runtime/utils/notify.d.ts +1 -1
  88. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  89. package/dist/runtime/utils/runEnvelope.js +12 -0
  90. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  91. package/dist/runtime/utils/runFeedQuery.js +66 -0
  92. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  93. package/dist/runtime/utils/runOutputs.js +10 -0
  94. package/dist/runtime/utils/statusStyle.d.ts +24 -0
  95. package/dist/runtime/utils/statusStyle.js +1 -0
  96. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  97. package/dist/runtime/utils/validationFindings.js +18 -3
  98. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ import { findManifest } from "../data/integrationNodeCatalog.js";
2
+ import { resolveDisplayName, resolveLocalized } from "./localize.js";
3
+ export function nodeDisplayName(node) {
4
+ const manifest = findManifest(node.nodeSlug, node.nodeVersion);
5
+ return resolveDisplayName(node.name) || resolveLocalized(manifest?.name) || node.nodeSlug;
6
+ }
@@ -18,7 +18,6 @@ export declare const toast: ((message: string | (() => string | import("vue").Co
18
18
  class?: string | undefined;
19
19
  id?: number | string | undefined;
20
20
  position?: ("top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-center" | "bottom-center") | undefined;
21
- cancel?: (import("vue-sonner").Action | import("vue").Component) | undefined;
22
21
  toasterId?: string | undefined;
23
22
  component?: import("vue").Component | undefined;
24
23
  componentProps?: any;
@@ -28,6 +27,7 @@ export declare const toast: ((message: string | (() => string | import("vue").Co
28
27
  dismissible?: boolean | undefined;
29
28
  duration?: number | undefined;
30
29
  important?: boolean | undefined;
30
+ cancel?: (import("vue-sonner").Action | import("vue").Component) | undefined;
31
31
  onDismiss?: ((toast: import("vue-sonner").ToastT) => void) | undefined;
32
32
  onAutoClose?: ((toast: import("vue-sonner").ToastT) => void) | undefined;
33
33
  cancelButtonStyle?: import("vue").CSSProperties | undefined;
@@ -0,0 +1,31 @@
1
+ /** A run as the API sends it, before anything has been read out of it. */
2
+ export type RunLike = Record<string, unknown>;
3
+ /**
4
+ * The run out of a single-run response, enveloped or not.
5
+ *
6
+ * `GET /workflows/{id}/runs/{runId}` answers the run itself, and the studio's own
7
+ * mock and some callers wrap it in `{ data: … }`. So the envelope has to be
8
+ * optional — but unwrapping on the mere presence of a `data` key is wrong: a run
9
+ * that ever carried one would be unwrapped into it, and the caller would be
10
+ * handed a field instead of a run. Hence the two guards: a body that already
11
+ * looks like a run (`status`, `temporal_run_id`) is the run, whatever else it has
12
+ * on it.
13
+ *
14
+ * This is deliberately generous about the *rest* of the shape. It answers "where
15
+ * is the run in this body", not "is this a usable run" — the caller decides that,
16
+ * because a caller following a run has to be able to count an unusable answer as
17
+ * a failed read rather than sit on it.
18
+ */
19
+ /**
20
+ * The raw platform statuses a run does not come back from.
21
+ *
22
+ * The platform's own words, not the studio's five — the single-run endpoint answers
23
+ * `completed` where a row would read "succeeded" (`utils/statusStyle.ts` maps one to
24
+ * the other), and anything reading this is reading the endpoint.
25
+ */
26
+ export declare const RUN_TERMINAL: Set<string>;
27
+ /** Whether this run has ended, as the platform reports it. */
28
+ export declare function isTerminalRun(run: {
29
+ status?: unknown;
30
+ } | null | undefined): boolean;
31
+ export declare function unwrapRun(res: unknown): RunLike | null;
@@ -0,0 +1,12 @@
1
+ export const RUN_TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "canceled", "terminated", "timed_out"]);
2
+ export function isTerminalRun(run) {
3
+ return RUN_TERMINAL.has(String(run?.status ?? "").toLowerCase());
4
+ }
5
+ export function unwrapRun(res) {
6
+ if (!res || typeof res !== "object") return null;
7
+ const o = res;
8
+ if ("data" in o && o.data && typeof o.data === "object" && !("status" in o) && !("temporal_run_id" in o)) {
9
+ return o.data;
10
+ }
11
+ return o;
12
+ }
@@ -0,0 +1,135 @@
1
+ import type { RunStatus, TriggerType } from '../types/integrations.js';
2
+ /**
3
+ * The narrowing of the tenant Runs list, translated for the platform.
4
+ *
5
+ * The list narrows on the server now, so the words the studio puts on its filter
6
+ * have to reach an endpoint that speaks a different vocabulary. The studio draws
7
+ * five outcomes; the platform accepts six statuses, two of which the studio folds
8
+ * into one word. Everything here exists to keep that fold from making a filter
9
+ * contradict the column it sits under — or, as happened once, from asking for a
10
+ * word the platform refuses outright.
11
+ *
12
+ * Kept out of the composable so it can be read on its own — the collapse is the
13
+ * one piece of this feature that is wrong in a way nothing on screen would show.
14
+ */
15
+ /**
16
+ * Every status the platform ACCEPTS in a `status` filter: the six cases of
17
+ * `RunStatus` in the integrations service (`app/Enums/RunStatus.php`), and
18
+ * nothing else.
19
+ *
20
+ * Copied from the enum, not from prose about it. It was written from a comment
21
+ * once and carried a seventh value, `timed_out`, which that enum does not have —
22
+ * so choosing "failed" sent `failed,terminated,timed_out` and the platform
23
+ * answered `422 The selected status.2 is invalid`. The filter did not narrow
24
+ * badly; it failed outright.
25
+ *
26
+ * This is deliberately NOT "every status a run can carry". `workflow_runs.status`
27
+ * is an uncast string column and the service's own summary endpoint accounts for
28
+ * values outside the enum, so a row may arrive carrying one — `mapRunStatus`
29
+ * therefore stays tolerant about what it READS. Only what is SENT is bounded by
30
+ * this list.
31
+ */
32
+ export declare const API_RUN_STATUSES: readonly ["pending", "running", "completed", "failed", "cancelled", "terminated"];
33
+ /**
34
+ * Platform status → the word the studio draws.
35
+ *
36
+ * Tolerant on purpose, and wider than {@link API_RUN_STATUSES}: the status column
37
+ * is an uncast string on the platform's side, so a row can arrive carrying a word
38
+ * the enum does not list. `terminated` and `timed_out` are failures to an
39
+ * operator and the column says so; anything else fails closed for the reason it
40
+ * does everywhere else — a run the platform cannot classify must never read as a
41
+ * stuck `running`.
42
+ */
43
+ export declare function mapRunStatus(status: string): RunStatus;
44
+ /**
45
+ * The platform statuses behind the studio's words.
46
+ *
47
+ * DERIVED from `mapRunStatus` rather than written out beside it. Restating the
48
+ * collapse would leave two lists to keep in step, and the failure is silent: the
49
+ * filter would come back short a few rows the column beside it still calls
50
+ * failed, which reads as runs having disappeared rather than as a narrowing that
51
+ * is narrower than it says.
52
+ */
53
+ export declare function apiStatusesFor(words: readonly RunStatus[]): string[];
54
+ /**
55
+ * The orders this studio can ask `GET /runs` for.
56
+ *
57
+ * Three of the four `RunSort` names the service accepts. Two absences are the
58
+ * platform's: ordering by the workflow's name would need a join on the paging
59
+ * path, and the trigger kind has four values, so it groups the list rather than
60
+ * ordering it — the filter beside it answers that question better.
61
+ *
62
+ * The third absence is the STUDIO'S, and it is the reason this list is not
63
+ * simply a mirror of the service's enum. `sort=status` orders by the word the
64
+ * platform stores — `cancelled`, `completed`, `failed`, `pending`, `running`,
65
+ * `terminated` — and those are not the words on the rows: the studio draws its
66
+ * own five, translated, and folds three of the platform's into one of them. A
67
+ * header reading "Status ↑" over rows ordered by a vocabulary the reader never
68
+ * sees is a control that cannot do what it says, and in German it would not even
69
+ * be alphabetical. Ordering by outcome from here needs the platform to order by
70
+ * something the studio can name, which it cannot: the translation lives in this
71
+ * repository and the ordering happens in the other one.
72
+ */
73
+ export declare const RUN_SORTS: readonly ["created_at", "started_at", "duration"];
74
+ export type RunSort = (typeof RUN_SORTS)[number];
75
+ export type RunSortDirection = 'asc' | 'desc';
76
+ /** The order the list opens in, and the one the platform falls back to. */
77
+ export declare const DEFAULT_RUN_SORT: RunSort;
78
+ export declare const DEFAULT_RUN_DIRECTION: RunSortDirection;
79
+ /** What the Runs list is currently asking the platform for. */
80
+ export interface RunFeedQuery {
81
+ /** 1-based, as the platform pages. */
82
+ page: number;
83
+ perPage: number;
84
+ /** The studio's own outcome words; empty narrows by no outcome. */
85
+ status: RunStatus[];
86
+ /** Empty narrows by no trigger kind. */
87
+ triggerType: TriggerType | '';
88
+ /** Matched against the workflow's name by the platform, not by the browser. */
89
+ search: string;
90
+ /** Which order the page is read in. Not a narrowing — see below. */
91
+ sort: RunSort;
92
+ direction: RunSortDirection;
93
+ }
94
+ /**
95
+ * The query string for `GET /runs` — and for `GET /runs/summary`, which narrows
96
+ * through the same rules and is where a numbered pager gets its total. Both are
97
+ * built from one place so the count can never describe a different set of rows
98
+ * from the page beside it.
99
+ *
100
+ * A filter nobody set is left out rather than sent empty: the platform reads an
101
+ * empty `status` as a value and answers 422.
102
+ */
103
+ export declare function runFeedQueryString(query: RunFeedQuery): string;
104
+ /**
105
+ * The narrowing alone, without the paging — what `GET /runs/summary` is read
106
+ * with so the pager's total describes the same rows as the page beside it.
107
+ *
108
+ * Empty when nothing narrows, and callers lean on that: the tenant-wide figures
109
+ * the dashboard shows come from the same summary state, so an unnarrowed load
110
+ * may refresh them and a narrowed one may not.
111
+ *
112
+ * The ORDER is deliberately absent. The summary counts what matches; a count has
113
+ * no order, and the endpoint ignores one — sending it would advertise an effect
114
+ * it does not have, and would make an ordered list look like it had asked for a
115
+ * differently counted total.
116
+ */
117
+ export declare function runFeedNarrowingString(query: RunFeedQuery): string;
118
+ /**
119
+ * Apply a change to the query, returning to the first page whenever the change
120
+ * is a narrowing rather than a page turn.
121
+ *
122
+ * The reset is the point. Page 4 of the old result set is very often past the
123
+ * end of the narrowed one, and the page that comes back is then empty — which
124
+ * an operator reads as "nothing matches" when the truth is "nothing matches
125
+ * this far in". The page size counts as a narrowing here even though it narrows
126
+ * nothing, because it changes what page 4 refers to.
127
+ */
128
+ export declare function withRunFeedPatch(query: RunFeedQuery, patch: Partial<RunFeedQuery>): RunFeedQuery;
129
+ /**
130
+ * How many pages a total spreads over, for the pager's "page 2 of 7".
131
+ *
132
+ * Never zero: an empty list is still page 1 of 1, and a pager reading "page 1 of
133
+ * 0" is a sentence about nothing.
134
+ */
135
+ export declare function runFeedLastPage(total: number, perPage: number): number;
@@ -0,0 +1,66 @@
1
+ export const API_RUN_STATUSES = [
2
+ "pending",
3
+ "running",
4
+ "completed",
5
+ "failed",
6
+ "cancelled",
7
+ "terminated"
8
+ ];
9
+ export function mapRunStatus(status) {
10
+ switch (status) {
11
+ case "completed":
12
+ return "succeeded";
13
+ case "pending":
14
+ return "queued";
15
+ case "terminated":
16
+ return "failed";
17
+ case "timed_out":
18
+ return "failed";
19
+ case "running":
20
+ return "running";
21
+ case "failed":
22
+ return "failed";
23
+ case "cancelled":
24
+ return "cancelled";
25
+ default:
26
+ return "failed";
27
+ }
28
+ }
29
+ export function apiStatusesFor(words) {
30
+ if (!words.length) return [];
31
+ return API_RUN_STATUSES.filter((status) => words.includes(mapRunStatus(status)));
32
+ }
33
+ export const RUN_SORTS = ["created_at", "started_at", "duration"];
34
+ export const DEFAULT_RUN_SORT = "created_at";
35
+ export const DEFAULT_RUN_DIRECTION = "desc";
36
+ export function runFeedQueryString(query) {
37
+ const params = new URLSearchParams();
38
+ params.set("page", String(query.page));
39
+ params.set("per_page", String(query.perPage));
40
+ for (const [key, value] of narrowingParams(query)) params.set(key, value);
41
+ if (query.sort !== DEFAULT_RUN_SORT || query.direction !== DEFAULT_RUN_DIRECTION) {
42
+ params.set("sort", query.sort);
43
+ params.set("direction", query.direction);
44
+ }
45
+ return params.toString();
46
+ }
47
+ export function runFeedNarrowingString(query) {
48
+ return narrowingParams(query).toString();
49
+ }
50
+ function narrowingParams(query) {
51
+ const params = new URLSearchParams();
52
+ const statuses = apiStatusesFor(query.status);
53
+ if (statuses.length) params.set("status", statuses.join(","));
54
+ if (query.triggerType) params.set("trigger_type", query.triggerType);
55
+ const search = query.search.trim();
56
+ if (search) params.set("search", search);
57
+ return params;
58
+ }
59
+ export function withRunFeedPatch(query, patch) {
60
+ const narrowed = "status" in patch || "triggerType" in patch || "search" in patch || "perPage" in patch || "sort" in patch || "direction" in patch;
61
+ return { ...query, ...patch, ...narrowed ? { page: 1 } : {} };
62
+ }
63
+ export function runFeedLastPage(total, perPage) {
64
+ if (perPage <= 0) return 1;
65
+ return Math.max(1, Math.ceil(total / perPage));
66
+ }
@@ -0,0 +1,33 @@
1
+ import type { NameableNode } from './nodeDisplayName.js';
2
+ /** One node's share of what a run returned. */
3
+ export interface RunOutput {
4
+ /** The blob key the value stood under. Kept for addressing, never shown alone. */
5
+ nodeId: string;
6
+ /** What that node is called on the canvas. */
7
+ name: string;
8
+ value: unknown;
9
+ }
10
+ /** A node as the blob carries it — an id, and enough to name it by. */
11
+ export type OutputNode = NameableNode & {
12
+ id: string;
13
+ };
14
+ /**
15
+ * What a run returned, split per node and named the way the canvas names it.
16
+ *
17
+ * A run's result is a map keyed by node id — `{"n-1787908672612": {…}}` — which
18
+ * is the worker's addressing and not a thing to show a person. Split against the
19
+ * workflow's own nodes it becomes what the author was actually looking at: one
20
+ * block per node, under the name on the card.
21
+ *
22
+ * `null` means "this is not that shape", and the caller shows the value whole
23
+ * instead of inventing structure for it. That covers three real cases: a result
24
+ * that is a bare value rather than a map, one keyed by something other than node
25
+ * ids, and a run of a workflow whose nodes have since changed — where guessing
26
+ * would put a value under the wrong name, which is worse than not naming it.
27
+ * An empty result is `null` too: there is nothing to split, and the caller has a
28
+ * better sentence for it than an empty list.
29
+ *
30
+ * Order follows the blob rather than the result's own keys, so two runs of one
31
+ * workflow read the same way down the page.
32
+ */
33
+ export declare function splitRunOutputs(result: unknown, nodes: readonly OutputNode[]): RunOutput[] | null;
@@ -0,0 +1,10 @@
1
+ import { nodeDisplayName } from "./nodeDisplayName.js";
2
+ export function splitRunOutputs(result, nodes) {
3
+ if (!result || typeof result !== "object" || Array.isArray(result)) return null;
4
+ const keys = Object.keys(result);
5
+ if (!keys.length) return null;
6
+ const byId = new Map(nodes.map((n) => [n.id, n]));
7
+ if (!keys.every((k) => byId.has(k))) return null;
8
+ const values = result;
9
+ return nodes.filter((n) => Object.hasOwn(values, n.id)).map((n) => ({ nodeId: n.id, name: nodeDisplayName(n), value: values[n.id] }));
10
+ }
@@ -23,6 +23,30 @@ export declare function toneClass(tone: StatusTone): string;
23
23
  * order. It used to keep its own copy of the list, so a sixth word added here
24
24
  * would have been labelled everywhere and silently missing from that menu. */
25
25
  export declare const RUN_STATUS_WORDS: readonly ["running", "succeeded", "failed", "cancelled", "queued"];
26
+ /**
27
+ * How wide a list draws its Status column, as a grid track.
28
+ *
29
+ * A fixed width, and it has to be one: the shared table gives the header row, every
30
+ * body row and every skeleton row a grid of their own, so a content-sized track
31
+ * (`max-content`, `auto`, `fit-content`) is resolved per row and a row holding a
32
+ * longer word would step out of line with the header and with the rows around it.
33
+ * The `id` column below says the same thing, and this constant was `minmax(136px,
34
+ * max-content)` for one commit because that warning was read too late.
35
+ *
36
+ * So the number is the promise, and it is a floor rather than a measurement. The
37
+ * column stood at 100px, which fitted every English word with room to spare and let
38
+ * the German "fehlgeschlagen" — 126px as a rendered badge on the machine this was
39
+ * measured on — run out of its cell and under the workflow name beside it (PO-362).
40
+ * 160px is that word plus a quarter, because the rendered width is not ours to know:
41
+ * a font that failed to load, a browser zoom or a reader's own minimum font size all
42
+ * move it, and one of those is why a first attempt at 136 was still reported cut.
43
+ *
44
+ * What happens when it is still not enough, and why that is survivable: the badge
45
+ * carries `max-w-full`, so it is clipped inside its own cell rather than drawn over
46
+ * the column beside it, and the whole word stays readable in its `title`. Raising
47
+ * this number is the fix at that point — not making the track elastic.
48
+ */
49
+ export declare const RUN_STATUS_COLUMN_WIDTH = "160px";
26
50
  /**
27
51
  * Raw API/Temporal run vocabulary → the cockpit's run-status labels, so a run
28
52
  * never reads "completed" in one surface and "succeeded" in another. This IS
@@ -77,6 +77,7 @@ export function toneClass(tone) {
77
77
  return TONE_CLASS[tone];
78
78
  }
79
79
  export const RUN_STATUS_WORDS = ["running", "succeeded", "failed", "cancelled", "queued"];
80
+ export const RUN_STATUS_COLUMN_WIDTH = "160px";
80
81
  const RUN_STATUS_WORD_SET = new Set(RUN_STATUS_WORDS);
81
82
  export function normalisedRunStatus(status) {
82
83
  if (!status) return "";
@@ -38,8 +38,10 @@ export interface FindingNodeGroup {
38
38
  /** The node's human label; `null` when `nodeId` is. */
39
39
  label: string | null;
40
40
  /**
41
- * One compact entry per finding, in order and de-duplicated: the config key
42
- * the path ends in, falling back to the message when the path carries none.
41
+ * One compact entry per finding, in order and de-duplicated by config key: the
42
+ * setting's name as `fieldLabelOf` gives it — the manifest's label for the banner,
43
+ * the bare key where a caller asks for that — falling back to the message when the
44
+ * path carries no config key at all.
43
45
  * **Empty when `nodeId` is `null`** — read `messages` instead.
44
46
  */
45
47
  fields: string[];
@@ -53,6 +55,27 @@ export interface FindingNodeGroup {
53
55
  * two of them prints `revenexx:business-central-create-customer` at the user.
54
56
  */
55
57
  export declare function nodeDisplayLabel(node: WorkflowNode): string;
58
+ /**
59
+ * What a setting is called, for somebody reading *about* it.
60
+ *
61
+ * The same lookup {@link nodeDisplayLabel} does for the node, one level down: the
62
+ * manifest carries a bilingual `label` per config field, and the node's own settings
63
+ * form already renders it. The banner did not ask, so it printed the blob path's last
64
+ * segment — `companyId` beside a form that says **Firma**, in an otherwise German
65
+ * screen, for the same field (PO-401).
66
+ *
67
+ * **The key is the fallback and never the wrong answer.** A workflow can outlive the
68
+ * manifest version it was saved against, and a package can be gone entirely; then there
69
+ * is no label to resolve, and the raw key is worse to read but still names the setting
70
+ * the operator has to find. Falling back to the finding's *message* — what this module
71
+ * does for a finding with no field key at all — was refused here: it turns a short list
72
+ * of names into a list of sentences exactly when the banner is already hardest to read.
73
+ *
74
+ * **For reading about, not for finding by.** `findingsByNodeField` deliberately does NOT
75
+ * use this. Its keys are how the configuration dialog attaches a message to an input, so
76
+ * a translated one would stop matching the field it belongs to.
77
+ */
78
+ export declare function fieldDisplayLabel(node: WorkflowNode, fieldKey: string): string;
56
79
  /**
57
80
  * The setting a finding is about: the config key its path ends in.
58
81
  *
@@ -71,7 +94,7 @@ export declare function findingFieldKey(path: string): string | null;
71
94
  * `null` nodeId), rendered after the node groups — it has no node to hide
72
95
  * behind, and dropping it would lose an issue the user has to fix.
73
96
  */
74
- export declare function groupFindingsByNode(findings: readonly ValidationFinding[], nodes: readonly WorkflowNode[], labelOf?: (node: WorkflowNode) => string): FindingNodeGroup[];
97
+ export declare function groupFindingsByNode(findings: readonly ValidationFinding[], nodes: readonly WorkflowNode[], labelOf?: (node: WorkflowNode) => string, fieldLabelOf?: (node: WorkflowNode, fieldKey: string) => string): FindingNodeGroup[];
75
98
  /**
76
99
  * Node id → config key → the message to show AT that setting
77
100
  * (specs/node-configuration.md AC-8).
@@ -5,6 +5,11 @@ export function nodeDisplayLabel(node) {
5
5
  const manifest = findManifest(node.nodeSlug, node.nodeVersion);
6
6
  return resolveLocalized(node.name) || resolveLocalized(manifest?.name) || node.nodeSlug;
7
7
  }
8
+ export function fieldDisplayLabel(node, fieldKey) {
9
+ const manifest = findManifest(node.nodeSlug, node.nodeVersion);
10
+ const field = manifest?.config?.find((f) => f.key === fieldKey);
11
+ return resolveLocalized(field?.label) || fieldKey;
12
+ }
8
13
  export function findingFieldKey(path) {
9
14
  const m = /^blob\.nodes\.\d+\.config\.([^.[]+)/.exec(path);
10
15
  return m ? m[1] : null;
@@ -13,10 +18,11 @@ function findingNode(path, nodes) {
13
18
  const m = /^blob\.nodes\.(\d+)\b/.exec(path);
14
19
  return m ? nodes[Number(m[1])] : void 0;
15
20
  }
16
- export function groupFindingsByNode(findings, nodes, labelOf = nodeDisplayLabel) {
21
+ export function groupFindingsByNode(findings, nodes, labelOf = nodeDisplayLabel, fieldLabelOf = fieldDisplayLabel) {
17
22
  const nodeGroups = [];
18
23
  const orphans = [];
19
24
  const byNodeId = /* @__PURE__ */ new Map();
25
+ const seenKeys = /* @__PURE__ */ new Map();
20
26
  for (const finding of findings) {
21
27
  const node = findingNode(finding.path, nodes);
22
28
  if (!node) {
@@ -29,8 +35,17 @@ export function groupFindingsByNode(findings, nodes, labelOf = nodeDisplayLabel)
29
35
  byNodeId.set(node.id, group);
30
36
  nodeGroups.push(group);
31
37
  }
32
- const field = findingFieldKey(finding.path) ?? finding.message;
33
- if (!group.fields.includes(field)) group.fields.push(field);
38
+ const key = findingFieldKey(finding.path);
39
+ if (key === null) {
40
+ if (!group.fields.includes(finding.message)) group.fields.push(finding.message);
41
+ } else {
42
+ const seen = seenKeys.get(node.id) ?? /* @__PURE__ */ new Set();
43
+ seenKeys.set(node.id, seen);
44
+ if (!seen.has(key)) {
45
+ seen.add(key);
46
+ group.fields.push(fieldLabelOf(node, key));
47
+ }
48
+ }
34
49
  group.messages.push(finding.message);
35
50
  }
36
51
  return [...nodeGroups, ...orphans];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
6
6
  "repository": {