@revenexx/studio-integrations 1.1.1 → 1.3.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 (119) hide show
  1. package/CHANGELOG.md +638 -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 +45 -19
  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/NodeConfigDynamicSchema.vue +10 -2
  16. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +88 -1
  18. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
  19. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +7 -3
  20. package/dist/runtime/components/Integrations/NodeInspector.vue +125 -54
  21. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +7 -3
  22. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  23. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  24. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  25. package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
  26. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  27. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  28. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  29. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  30. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  31. package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
  32. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  33. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  34. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  35. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  36. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  38. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  39. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  40. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  41. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  42. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  43. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  44. package/dist/runtime/components/Integrations/WorkflowEditor.vue +78 -30
  45. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  46. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  47. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  48. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  49. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  50. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  51. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  52. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  53. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  54. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  55. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  56. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  57. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  58. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  59. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  60. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  61. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  62. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  63. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  64. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  65. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  66. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  67. package/dist/runtime/composables/useIntegrationsTemplates.js +7 -5
  68. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  69. package/dist/runtime/composables/useIntegrationsWorkflows.js +10 -7
  70. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  71. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  72. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  73. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  74. package/dist/runtime/data/expressionEditor.js +11 -5
  75. package/dist/runtime/data/handleId.d.ts +61 -1
  76. package/dist/runtime/data/handleId.js +8 -1
  77. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  78. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  79. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  80. package/dist/runtime/data/nodeVersionMove.js +65 -0
  81. package/dist/runtime/data/payloadForm.js +3 -2
  82. package/dist/runtime/i18n/messages/de.json +79 -12
  83. package/dist/runtime/i18n/messages/en.json +79 -12
  84. package/dist/runtime/pages/integrations/audit.vue +7 -6
  85. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  86. package/dist/runtime/pages/integrations/index.vue +55 -8
  87. package/dist/runtime/pages/integrations/runs.vue +116 -7
  88. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  90. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +169 -24
  92. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  93. package/dist/runtime/types/integrations.d.ts +49 -1
  94. package/dist/runtime/utils/apiError.d.ts +21 -1
  95. package/dist/runtime/utils/apiError.js +1 -1
  96. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  97. package/dist/runtime/utils/buildSettle.js +29 -0
  98. package/dist/runtime/utils/clipboard.d.ts +33 -0
  99. package/dist/runtime/utils/clipboard.js +11 -0
  100. package/dist/runtime/utils/deepClone.d.ts +41 -0
  101. package/dist/runtime/utils/deepClone.js +3 -0
  102. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  103. package/dist/runtime/utils/formatDuration.js +13 -0
  104. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  105. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  106. package/dist/runtime/utils/notify.d.ts +1 -1
  107. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  108. package/dist/runtime/utils/runEnvelope.js +12 -0
  109. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  110. package/dist/runtime/utils/runFeedQuery.js +66 -0
  111. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  112. package/dist/runtime/utils/runOutputs.js +10 -0
  113. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  114. package/dist/runtime/utils/settingCondition.js +84 -0
  115. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  116. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  117. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  118. package/dist/runtime/utils/validationFindings.js +18 -3
  119. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ export const BUILD_SETTLE_POLL_MS = 2500;
2
+ export const BUILD_SETTLE_TIMEOUT_MS = 5 * 6e4;
3
+ export function isBuildUnsettled(status) {
4
+ return status === "pending" || status === "building";
5
+ }
6
+ export function createBuildSettle(options = {}) {
7
+ const pollMs = options.pollMs ?? BUILD_SETTLE_POLL_MS;
8
+ const timeoutMs = options.timeoutMs ?? BUILD_SETTLE_TIMEOUT_MS;
9
+ let generation = 0;
10
+ return {
11
+ async settle({ unsettled, read }) {
12
+ const mine = ++generation;
13
+ const started = Date.now();
14
+ while (Date.now() - started < timeoutMs) {
15
+ if (mine !== generation) return;
16
+ if (!unsettled()) return;
17
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
18
+ if (mine !== generation || !unsettled()) return;
19
+ try {
20
+ await read();
21
+ } catch {
22
+ }
23
+ }
24
+ },
25
+ stop() {
26
+ generation += 1;
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Put something on the clipboard, and tell the operator what actually happened.
3
+ *
4
+ * **The waiting is the point.** `writeText` returns a promise, and a browser can refuse
5
+ * it after the fact — a page that is not focused, a context that is not secure, a
6
+ * permission the operator declined. A caller that fires its "Copied" message beside the
7
+ * call rather than after it announces a success it has not been told about, and the
8
+ * operator pastes whatever was on the clipboard before. That shipped on the audit log,
9
+ * where the promise was neither awaited nor caught: the toast had already gone up by the
10
+ * time the rejection arrived, and the rejection went nowhere.
11
+ *
12
+ * **A missing `navigator.clipboard` is a failure, not a reason to do nothing.** The audit
13
+ * log guarded on it and skipped the whole block, so on a browser without the API the
14
+ * button did nothing at all — no copy, no message — which reads as broken rather than as
15
+ * refused. Reading through it and letting the `TypeError` land in the same `catch` is
16
+ * what makes "it did not work" one path instead of two.
17
+ *
18
+ * `vue-sonner` directly, not `notify.ts`: that wrapper exists so a *save* is never lost
19
+ * to a notification that cannot be drawn, and says in its own docblock that everywhere
20
+ * else imports the library. A copy has no such underlying action — the message IS the
21
+ * outcome — so routing six more surfaces through it would spend the indirection on the
22
+ * case it was explicitly not written for.
23
+ *
24
+ * The two messages stay with the caller because they are the only part that differs:
25
+ * copying a node's config, a webhook URL and an audit entry are three different things to
26
+ * have failed, and one message covering all of them would name none of them.
27
+ *
28
+ * Returns whether it worked, for a caller that has more to do than say so.
29
+ */
30
+ export declare function copyToClipboard(text: string, said: {
31
+ copied: string;
32
+ failed: string;
33
+ }): Promise<boolean>;
@@ -0,0 +1,11 @@
1
+ import { toast } from "vue-sonner";
2
+ export async function copyToClipboard(text, said) {
3
+ try {
4
+ await navigator.clipboard.writeText(text);
5
+ } catch {
6
+ toast.error(said.failed);
7
+ return false;
8
+ }
9
+ toast.success(said.copied);
10
+ return true;
11
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A deep copy of a value the studio is about to let someone edit.
3
+ *
4
+ * Every surface that edits a workflow takes one first, so that an unsaved change can
5
+ * be thrown away by dropping the copy rather than by undoing it: the editor's canvas,
6
+ * the node inspector's draft, the variables dialog's working set, a duplicate on its
7
+ * way to a new name, and the payload form's expert extras.
8
+ *
9
+ * **It is a JSON round trip, and that is a claim about the input rather than a
10
+ * shortcut.** A workflow blob is JSON — it is what the API stores and what the wire
11
+ * carries — so serialising and parsing it loses nothing. That is the whole guarantee,
12
+ * and it was the guarantee that made eight hand-written copies of this expression safe.
13
+ * What it cost is that the guarantee was never written down, so nothing said which
14
+ * inputs it holds for.
15
+ *
16
+ * **What it does not survive**, for whoever reaches for this with something that is not
17
+ * a blob: `undefined` becomes a missing key rather than a present one, a `Date` comes
18
+ * back as a string, a `Map`, a `Set` and a function are lost entirely, and a cycle
19
+ * throws. None of those can appear in a blob. All of them can appear in editor state
20
+ * that has not been through the API yet — a resolved sample, a cached schema — so a
21
+ * new caller is the moment to check the claim rather than to assume it.
22
+ *
23
+ * **The signature cannot enforce any of that, and says so here rather than pretending.**
24
+ * `<T>(value: T): T` claims the type it was handed comes back, which is false for exactly
25
+ * the five: `deepClone({ at: new Date() }).at` is typed `Date` and is a string, so
26
+ * `.getTime()` type-checks and throws. Constraining the parameter to a JSON-shaped type
27
+ * was tried and does not work here — `WorkflowBlob`, `WorkflowNode` and `WorkflowTrigger`
28
+ * are interfaces, which have no implicit index signature and so satisfy no recursive JSON
29
+ * type; it turned 13 type errors into 73, and collapsed the inference that gives callers
30
+ * their concrete type back. So the guarantee above is advisory, and the tests beside this
31
+ * file are where it is actually held.
32
+ *
33
+ * **Not `structuredClone`, deliberately.** It is available everywhere the module runs
34
+ * and it preserves all five of the things above, which sounds strictly better and is
35
+ * not: it would *change* what the callers get. A field that is dropped today because
36
+ * JSON cannot carry it would start surviving the copy, and something downstream may be
37
+ * relying on that loss without saying so. Swapping the mechanism is a behaviour change
38
+ * that wants its own proof; this module exists so that it becomes one edit when
39
+ * somebody makes it — which is PO-395's other half, and is not done here.
40
+ */
41
+ export declare function deepClone<T>(value: T): T;
@@ -0,0 +1,3 @@
1
+ export function deepClone(value) {
2
+ return JSON.parse(JSON.stringify(value));
3
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * How long something took, in the shortest unit that still reads.
3
+ *
4
+ * One function for every surface that prints a duration: the runs table, the run
5
+ * record's step list, the result of a run started by hand, the replay panel's step
6
+ * and run figures, and the dashboard's latest executions. Private copies are how two
7
+ * surfaces come to spell the same milliseconds differently, which is what PO-383
8
+ * closed — three implementations across those five, two of them disagreeing. The
9
+ * dashboard's was not in the ticket's enumeration: it divided by a thousand inside
10
+ * the template, so it was invisible to a search for a formatter.
11
+ *
12
+ * The form is `900ms`, `1.5s`, `1m 3s`, and it was chosen rather than inherited.
13
+ * Three candidates stood in PO-383: this one, the two-decimal `1.50s`/`1.1m` these
14
+ * surfaces used to print, and the same shape as this one with a space before the
15
+ * unit. The unit sits against the number because that is how the product already
16
+ * writes a duration everywhere it is copy rather than a computed figure — `{n}s
17
+ * ago`, `{n}m ago`, `{n}h ago` in the message catalogue, read out beside these very
18
+ * values in the runs list. A space would also have had to stop applying above a
19
+ * minute, where `1 m 3 s` is unreadable, so it was one form under two rules.
20
+ *
21
+ * One decimal under a minute, and minutes-and-seconds above: a duration is read to
22
+ * answer "was that quick", so `1m 3s` beats `1.1m` for a step that took a minute,
23
+ * and the second decimal of `1.50s` is precision nobody acts on.
24
+ *
25
+ * **A trailing zero is dropped, because it is not always a measurement.** Two kinds of
26
+ * value arrive here. A *step's* duration is measured in milliseconds and a tenth of a
27
+ * second is a real reading — `3.4s`. A *run's* is the difference between two
28
+ * timestamps the platform stores to the second (`timestamp(0)`) and serialises without
29
+ * a fraction, so it is always a whole number of seconds: there is no run-level
30
+ * millisecond value anywhere in the API to be had. Printing `4.0s` there claimed a
31
+ * precision that cannot exist. Nothing has to be told which kind it is — a value that
32
+ * measured nothing below the unit has nothing after the point, so `4s` and `1m` fall
33
+ * out of the arithmetic while `3.4s` and `1m 3s` keep what they measured.
34
+ *
35
+ * Nothing promises the form. `specs/run-row.md` AC-3 promises that a row says how
36
+ * long a run has been going and deliberately not in what shape, and that stays the
37
+ * right altitude: a criterion pinning the spelling would freeze a decision the
38
+ * product may want to revisit — the next one being a locale, which this function
39
+ * does not take (German wants `1,5 s`).
40
+ *
41
+ * `null` is not zero: a run that has not started has no duration, and an em dash
42
+ * says so where `0ms` would claim it finished instantly.
43
+ */
44
+ export declare function formatDuration(ms: number | null | undefined): string;
@@ -0,0 +1,13 @@
1
+ export function formatDuration(ms) {
2
+ if (ms == null) return "\u2014";
3
+ if (ms < 1e3) return `${ms}ms`;
4
+ const tenths = Math.round(ms / 100);
5
+ if (tenths < 600) {
6
+ const seconds2 = tenths / 10;
7
+ return tenths % 10 === 0 ? `${seconds2}s` : `${seconds2.toFixed(1)}s`;
8
+ }
9
+ const seconds = Math.round(ms / 1e3);
10
+ const rest = seconds % 60;
11
+ const minutes = Math.floor(seconds / 60);
12
+ return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
13
+ }
@@ -0,0 +1,22 @@
1
+ /** What a node display name can be derived from — the blob's own fields. */
2
+ export interface NameableNode {
3
+ name?: unknown;
4
+ nodeSlug: string;
5
+ nodeVersion?: string;
6
+ }
7
+ /**
8
+ * What a node is called, wherever it is named.
9
+ *
10
+ * The three sources in order: the name the author typed, the manifest's name for
11
+ * that kind of node, and — where the catalogue does not know the slug — the slug
12
+ * itself. Never the node's id, which is a key the studio generated and nobody
13
+ * chose.
14
+ *
15
+ * The two resolvers are not interchangeable and the difference has already cost
16
+ * once (PO-204): the author's own name is passed through untouched, while a
17
+ * manifest string is trimmed and may carry its languages as an object. That is
18
+ * why this is one function rather than the rule restated wherever a node is
19
+ * named — the canvas card and the result of a run started by hand have to agree
20
+ * on what a node is called, or the dialog names something the graph does not.
21
+ */
22
+ export declare function nodeDisplayName(node: NameableNode): string;
@@ -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
+ }
@@ -0,0 +1,48 @@
1
+ import type { ConfigField, SettingConditionOperator } from '../types/integrations.js';
2
+ /**
3
+ * What an operator means. Mirrors the SDK's `evaluate` row for row, including
4
+ * the leniency of `equals` across the string/number line: a select carries `1`
5
+ * where a text box carries `'1'`, and an author means one thing by both.
6
+ */
7
+ export declare function evaluateOperator(left: unknown, op: SettingConditionOperator, right: unknown): boolean;
8
+ /**
9
+ * Whether one setting applies, given the values at its own level — inside a
10
+ * group, the siblings in that group, which is where the node wrote the
11
+ * condition and where the platform resolves it.
12
+ *
13
+ * A setting with no condition always applies, and so does one whose condition
14
+ * is malformed: whatever the node meant, an author is better off seeing a field
15
+ * than having it silently withheld.
16
+ */
17
+ export declare function settingApplies(field: Pick<ConfigField, 'showIf'>, values: Record<string, unknown>): boolean;
18
+ /**
19
+ * Whether a change to `changedKey` leaves this setting's value stale.
20
+ *
21
+ * Two reasons a setting can depend on another, and they do not go stale at the
22
+ * same moment. `dependsOn` names what a dynamic field resolves its options
23
+ * from, and any change to it invalidates what was chosen out of the old
24
+ * options. A condition names the setting that decides whether the field is
25
+ * drawn at all, and what was typed only becomes unreachable when the condition
26
+ * stops holding: while it still holds the field is on screen, and clearing it
27
+ * on every keystroke in the setting it reads would empty a field the author is
28
+ * looking at — `showIf: { key: 'url', op: 'isNotEmpty' }` stays true from the
29
+ * first character on.
30
+ */
31
+ export declare function valueGoesStale(field: Pick<ConfigField, 'dependsOn' | 'showIf'>, changedKey: string, values: Record<string, unknown>): boolean;
32
+ /**
33
+ * The settings of one level whose values are stale once `changedKey` has been
34
+ * set — the cascade, walked to a fixpoint over that level's own fields.
35
+ *
36
+ * `after` is the config as it stands *after* the write, because a condition is
37
+ * read against it: whether a setting still applies is the whole question for a
38
+ * `showIf` dependant. Each cleared key is taken out of that config as the walk
39
+ * goes, so a setting withdrawn by a clearing is found in the same pass.
40
+ *
41
+ * The setting being written is never cleared, which is also what guards a
42
+ * cyclic `dependsOn`.
43
+ */
44
+ export declare function staleKeys(fields: Pick<ConfigField, 'key' | 'dependsOn' | 'showIf'>[], changedKey: string, after: Record<string, unknown>): Set<string>;
45
+ /**
46
+ * The settings of a level that apply, in the order the node declared them.
47
+ */
48
+ export declare function applyingFields(fields: ConfigField[], values: Record<string, unknown>): ConfigField[];
@@ -0,0 +1,84 @@
1
+ const READS_PRESENCE = ["exists", "notExists", "isEmpty", "isNotEmpty"];
2
+ const OPERATORS = [
3
+ "equals",
4
+ "notEquals",
5
+ "contains",
6
+ "notContains",
7
+ "startsWith",
8
+ "endsWith",
9
+ "greaterThan",
10
+ "greaterThanOrEqual",
11
+ "lessThan",
12
+ "lessThanOrEqual",
13
+ ...READS_PRESENCE
14
+ ];
15
+ export function evaluateOperator(left, op, right) {
16
+ switch (op) {
17
+ case "exists":
18
+ return left !== void 0 && left !== null;
19
+ case "notExists":
20
+ return left === void 0 || left === null;
21
+ case "isEmpty":
22
+ return left === "" || left === null || left === void 0 || Array.isArray(left) && left.length === 0;
23
+ case "isNotEmpty":
24
+ return !evaluateOperator(left, "isEmpty", right);
25
+ case "equals":
26
+ return String(left) === String(right);
27
+ case "notEquals":
28
+ return String(left) !== String(right);
29
+ case "contains":
30
+ return typeof left === "string" && left.includes(String(right));
31
+ case "notContains":
32
+ return typeof left === "string" && !left.includes(String(right));
33
+ case "startsWith":
34
+ return typeof left === "string" && left.startsWith(String(right));
35
+ case "endsWith":
36
+ return typeof left === "string" && left.endsWith(String(right));
37
+ case "greaterThan":
38
+ return Number(left) > Number(right);
39
+ case "greaterThanOrEqual":
40
+ return Number(left) >= Number(right);
41
+ case "lessThan":
42
+ return Number(left) < Number(right);
43
+ case "lessThanOrEqual":
44
+ return Number(left) <= Number(right);
45
+ default: {
46
+ const unhandled = op;
47
+ void unhandled;
48
+ return false;
49
+ }
50
+ }
51
+ }
52
+ export function settingApplies(field, values) {
53
+ const condition = field.showIf;
54
+ if (!condition || typeof condition !== "object")
55
+ return true;
56
+ const { key, op } = condition;
57
+ if (typeof key !== "string" || !OPERATORS.includes(op))
58
+ return true;
59
+ return evaluateOperator(values[key], op, condition.value);
60
+ }
61
+ export function valueGoesStale(field, changedKey, values) {
62
+ if ((field.dependsOn ?? []).includes(changedKey))
63
+ return true;
64
+ return field.showIf?.key === changedKey && !settingApplies(field, values);
65
+ }
66
+ export function staleKeys(fields, changedKey, after) {
67
+ const remaining = { ...after };
68
+ const out = /* @__PURE__ */ new Set();
69
+ const queue = [changedKey];
70
+ while (queue.length > 0) {
71
+ const dep = queue.shift();
72
+ for (const f of fields) {
73
+ if (f.key === changedKey || out.has(f.key)) continue;
74
+ if (!valueGoesStale(f, dep, remaining)) continue;
75
+ out.add(f.key);
76
+ delete remaining[f.key];
77
+ queue.push(f.key);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ export function applyingFields(fields, values) {
83
+ return fields.filter((field) => settingApplies(field, values));
84
+ }