@revenexx/studio-integrations 2.0.0 → 2.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.
- package/CHANGELOG.md +141 -0
- package/README.md +1 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -6
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +134 -131
- package/dist/runtime/components/Integrations/CredentialLogo.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +6 -6
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +90 -11
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +121 -121
- package/dist/runtime/components/Integrations/NodeConfigField.vue +28 -9
- package/dist/runtime/components/Integrations/NodeInspector.vue +44 -41
- package/dist/runtime/components/Integrations/NodePalette.vue +52 -37
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +6 -5
- package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +7 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +14 -2
- package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +7 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +7 -26
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +10 -6
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +224 -225
- package/dist/runtime/components/Integrations/WorkflowExportDialog.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue +71 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.d.vue.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue +95 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue.d.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +0 -7
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +2 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +2 -3
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +14 -8
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +3 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -22
- package/dist/runtime/composables/useNow.d.ts +17 -0
- package/dist/runtime/composables/useNow.js +16 -0
- package/dist/runtime/composables/useWorkflowFile.d.ts +21 -0
- package/dist/runtime/composables/useWorkflowFile.js +63 -0
- package/dist/runtime/data/expressionEditor.d.ts +11 -1
- package/dist/runtime/data/expressionEditor.js +2 -1
- package/dist/runtime/data/nodeIcons.d.ts +14 -0
- package/dist/runtime/data/nodeIcons.js +57 -14
- package/dist/runtime/data/nodeRunData.js +1 -1
- package/dist/runtime/data/payloadForm.js +1 -1
- package/dist/runtime/i18n/messages/de.json +42 -0
- package/dist/runtime/i18n/messages/en.json +42 -0
- package/dist/runtime/pages/integrations/audit.vue +3 -3
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +2 -2
- package/dist/runtime/pages/integrations/index.vue +5 -10
- package/dist/runtime/pages/integrations/schedules.vue +13 -1
- package/dist/runtime/pages/integrations/templates/index.vue +40 -40
- package/dist/runtime/pages/integrations/workflows/[id].vue +324 -244
- package/dist/runtime/types/integrations.d.ts +13 -0
- package/dist/runtime/utils/credentialState.d.ts +0 -7
- package/dist/runtime/utils/credentialState.js +0 -12
- package/dist/runtime/utils/localize.d.ts +12 -0
- package/dist/runtime/utils/localize.js +1 -1
- package/dist/runtime/utils/paletteOrder.d.ts +29 -0
- package/dist/runtime/utils/paletteOrder.js +46 -0
- package/dist/runtime/utils/relativeTime.d.ts +29 -0
- package/dist/runtime/utils/relativeTime.js +23 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +12 -6
- package/dist/runtime/utils/runFeedQuery.js +3 -18
- package/dist/runtime/utils/settingValue.d.ts +33 -0
- package/dist/runtime/utils/settingValue.js +8 -0
- package/dist/runtime/utils/statusStyle.d.ts +8 -8
- package/dist/runtime/utils/statusStyle.js +4 -1
- package/dist/runtime/utils/workflowFile.d.ts +119 -0
- package/dist/runtime/utils/workflowFile.js +192 -0
- package/package.json +8 -3
|
@@ -359,6 +359,13 @@ export interface NodeManifest {
|
|
|
359
359
|
* source package + slug heuristics.
|
|
360
360
|
*/
|
|
361
361
|
groups?: LocalizedString[];
|
|
362
|
+
/**
|
|
363
|
+
* Where the node stands inside its innermost palette folder, among the nodes
|
|
364
|
+
* of the same package; lower comes first (PO-537). Optional manifest field —
|
|
365
|
+
* nodes without it follow, in their package's own order
|
|
366
|
+
* (`__packagePosition`), then by slug.
|
|
367
|
+
*/
|
|
368
|
+
paletteOrder?: number;
|
|
362
369
|
/** Brand images (category `logo` drives palette/canvas rendering); src is a signed URL. */
|
|
363
370
|
images?: Array<{
|
|
364
371
|
src: string;
|
|
@@ -405,6 +412,12 @@ export interface NodeManifest {
|
|
|
405
412
|
name: string;
|
|
406
413
|
label: string | null;
|
|
407
414
|
};
|
|
415
|
+
/**
|
|
416
|
+
* Where this node stands in its package's own list of nodes, 0 first
|
|
417
|
+
* (editor-side enrichment from `GET /nodes`; not manifest data). Absent for a
|
|
418
|
+
* node registered before the service recorded it, or without a package.
|
|
419
|
+
*/
|
|
420
|
+
__packagePosition?: number;
|
|
408
421
|
/**
|
|
409
422
|
* Whether this version's code was built and can therefore be executed
|
|
410
423
|
* (editor-side enrichment from `GET /nodes`; not manifest data). Absent means
|
|
@@ -61,11 +61,4 @@ export declare function credentialStateLabel(key: CredentialConnState): string;
|
|
|
61
61
|
* called from inside the table's `computed()` on every render anyway.
|
|
62
62
|
*/
|
|
63
63
|
export declare function credentialState(c: TestableCredential): CredentialStateInfo;
|
|
64
|
-
/**
|
|
65
|
-
* "just now" / "3m ago" / "2h ago" / "5d ago" / a date. Empty for no timestamp.
|
|
66
|
-
*
|
|
67
|
-
* Reuses the studio's common time words (`integration.common.time.*`) rather
|
|
68
|
-
* than minting its own — the same rule as the dashboard's `formatRelative`.
|
|
69
|
-
*/
|
|
70
|
-
export declare function relativeTime(iso?: string | null): string;
|
|
71
64
|
export {};
|
|
@@ -34,15 +34,3 @@ export function credentialState(c) {
|
|
|
34
34
|
}
|
|
35
35
|
return { key: "untested", label: t(MESSAGE_KEY.untested), toneClass: TONE.untested };
|
|
36
36
|
}
|
|
37
|
-
export function relativeTime(iso) {
|
|
38
|
-
if (!iso) return "";
|
|
39
|
-
const at = new Date(iso).getTime();
|
|
40
|
-
if (Number.isNaN(at)) return "";
|
|
41
|
-
const t = useIntegrationsT();
|
|
42
|
-
const ms = Date.now() - at;
|
|
43
|
-
if (ms < 6e4) return t("integration.common.time.justNow");
|
|
44
|
-
if (ms < 36e5) return t("integration.common.time.minutesAgo", Math.floor(ms / 6e4));
|
|
45
|
-
if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
|
|
46
|
-
if (ms < 30 * 864e5) return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
|
|
47
|
-
return new Date(iso).toLocaleDateString();
|
|
48
|
-
}
|
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
export declare function setStudioLocale(locale: string | null | undefined): void;
|
|
12
12
|
/** The language the studio is currently rendering in. Reactive when read in an effect. */
|
|
13
13
|
export declare function studioLocale(): string;
|
|
14
|
+
/**
|
|
15
|
+
* The language a locale tag means, in the form the API accepts as a key.
|
|
16
|
+
*
|
|
17
|
+
* Lowercased, split on `_` as well as `-` — `en_US` is not a tag this studio
|
|
18
|
+
* produces but it is one a host can pass — and refused unless it is the two or
|
|
19
|
+
* three letters a language subtag is, because the key ends up in a map the API
|
|
20
|
+
* validates against `^[a-z]{2,3}(-[A-Z]{2})?$` with `additionalProperties: false`.
|
|
21
|
+
* A host that sets `deutsch` or `x-klingon` would otherwise take the whole workflow
|
|
22
|
+
* save down over a description nobody could see was at fault; it gets `en` instead,
|
|
23
|
+
* which is the same answer reading already gives such a host.
|
|
24
|
+
*/
|
|
25
|
+
export declare function baseLanguage(locale: string): string;
|
|
14
26
|
/**
|
|
15
27
|
* The key a read answers from — which is the key a *deletion* has to remove, since
|
|
16
28
|
* what the user is clearing is the text in front of them.
|
|
@@ -7,7 +7,7 @@ export function setStudioLocale(locale) {
|
|
|
7
7
|
export function studioLocale() {
|
|
8
8
|
return uiLocale.value;
|
|
9
9
|
}
|
|
10
|
-
function baseLanguage(locale) {
|
|
10
|
+
export function baseLanguage(locale) {
|
|
11
11
|
const base = locale.toLowerCase().split(/[-_]/)[0] ?? "";
|
|
12
12
|
return /^[a-z]{2,3}$/.test(base) ? base : "en";
|
|
13
13
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { NodeManifest } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* The order of the nodes inside one palette folder (PO-537,
|
|
4
|
+
* `specs/node-palette.md` AC-8).
|
|
5
|
+
*
|
|
6
|
+
* The package decides the order of its own nodes: those that declare a
|
|
7
|
+
* `paletteOrder` come first, lowest first; the rest follow in the order the
|
|
8
|
+
* package lists them (`__packagePosition`), and a node for which neither is
|
|
9
|
+
* known falls back to its slug. Ties on a declared position are broken the same
|
|
10
|
+
* way, so the result never depends on the order the catalogue arrived in.
|
|
11
|
+
*
|
|
12
|
+
* Between packages nothing moves. A folder can take nodes from several packages
|
|
13
|
+
* (core's own folders do), and no package may reorder another's, so each package
|
|
14
|
+
* keeps exactly the places its nodes held in the incoming order and only the
|
|
15
|
+
* nodes within those places are rearranged.
|
|
16
|
+
*/
|
|
17
|
+
export declare function orderFolderLeaves(leaves: readonly NodeManifest[]): NodeManifest[];
|
|
18
|
+
/**
|
|
19
|
+
* The order of the folders on one palette level: alphabetical, in the language
|
|
20
|
+
* the studio is shown in. Case does not count, so a brand written in lower case
|
|
21
|
+
* (`revenexx`) stands among the others rather than after them, and a letter with
|
|
22
|
+
* an accent stands where a reader of that language looks for it.
|
|
23
|
+
*
|
|
24
|
+
* Before, the folders stood in the order their first node arrived from the
|
|
25
|
+
* catalogue, which the API sorts by slug — so a folder's place depended on what
|
|
26
|
+
* its first node happened to be called (`resolve-playground` put Test Nodes
|
|
27
|
+
* before Shipcloud).
|
|
28
|
+
*/
|
|
29
|
+
export declare function orderFolders<T>(folders: ReadonlyMap<string, T>, locale: string): Map<string, T>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { baseLanguage } from "./localize.js";
|
|
2
|
+
export function orderFolderLeaves(leaves) {
|
|
3
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
4
|
+
const keyOf = (m) => m.__package?.name ?? "";
|
|
5
|
+
for (const m of leaves) {
|
|
6
|
+
const list = byPackage.get(keyOf(m));
|
|
7
|
+
if (list) list.push(m);
|
|
8
|
+
else byPackage.set(keyOf(m), [m]);
|
|
9
|
+
}
|
|
10
|
+
for (const list of byPackage.values()) list.sort(compareWithinPackage);
|
|
11
|
+
const next = /* @__PURE__ */ new Map();
|
|
12
|
+
return leaves.map((m) => {
|
|
13
|
+
const key = keyOf(m);
|
|
14
|
+
const i = next.get(key) ?? 0;
|
|
15
|
+
next.set(key, i + 1);
|
|
16
|
+
return byPackage.get(key)[i];
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function compareWithinPackage(a, b) {
|
|
20
|
+
const declared = compareOptional(finite(a.paletteOrder), finite(b.paletteOrder));
|
|
21
|
+
if (declared !== 0) return declared;
|
|
22
|
+
const listed = compareOptional(a.__packagePosition, b.__packagePosition);
|
|
23
|
+
if (listed !== 0) return listed;
|
|
24
|
+
return a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0;
|
|
25
|
+
}
|
|
26
|
+
function finite(n) {
|
|
27
|
+
return typeof n === "number" && Number.isFinite(n) ? n : void 0;
|
|
28
|
+
}
|
|
29
|
+
function compareOptional(a, b) {
|
|
30
|
+
if (a === void 0 && b === void 0) return 0;
|
|
31
|
+
if (a === void 0) return 1;
|
|
32
|
+
if (b === void 0) return -1;
|
|
33
|
+
return a - b;
|
|
34
|
+
}
|
|
35
|
+
export function orderFolders(folders, locale) {
|
|
36
|
+
const collator = collatorFor(locale);
|
|
37
|
+
return new Map([...folders.entries()].sort(([a], [b]) => collator.compare(a, b)));
|
|
38
|
+
}
|
|
39
|
+
function collatorFor(locale) {
|
|
40
|
+
const options = { sensitivity: "base", numeric: true };
|
|
41
|
+
try {
|
|
42
|
+
return new Intl.Collator(locale, options);
|
|
43
|
+
} catch {
|
|
44
|
+
return new Intl.Collator(baseLanguage(locale), options);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long ago a moment was — `just now`, `40s ago`, `3m ago`, `2h ago`, `5d ago`, or
|
|
3
|
+
* a date beyond thirty days. `—` for a moment that is missing or will not parse.
|
|
4
|
+
*
|
|
5
|
+
* The one place this is worked out, for the dashboard's two panels, the runs list's
|
|
6
|
+
* **Started** column and a credential's last check (PO-393). They used to share the
|
|
7
|
+
* words (`integration.common.time.*`) and each carry its own arithmetic, and the
|
|
8
|
+
* copies disagreed: the same run read "just now" on the dashboard and "8s ago" one
|
|
9
|
+
* click away, and the dashboard's copy could print "NaNd ago".
|
|
10
|
+
*
|
|
11
|
+
* **Under ten seconds is `just now`; from there to a minute it counts seconds.** The
|
|
12
|
+
* runs list and the dashboard are live — a realtime event puts a run there moments
|
|
13
|
+
* after it happened — so "40s ago" is information an operator watching them can use.
|
|
14
|
+
* Below ten seconds the count would be finer than the three-second clock it is read
|
|
15
|
+
* against (`useNow`), and "3s ago" would skip to "6s ago".
|
|
16
|
+
*
|
|
17
|
+
* **Pass `now` from `useNow` wherever the text stays on screen.** Left at
|
|
18
|
+
* `Date.now()`, the answer is re-read only when something else re-renders, so a
|
|
19
|
+
* seconds count stands still beside an entry no event touches.
|
|
20
|
+
*
|
|
21
|
+
* **Beyond thirty days is a date, not a count of days.** "412d ago" makes the reader
|
|
22
|
+
* do the arithmetic the page should have done. The date is written in the studio's
|
|
23
|
+
* language rather than the browser's, like the words beside it.
|
|
24
|
+
*
|
|
25
|
+
* **What was not computed is not printed.** A timestamp that will not parse returns
|
|
26
|
+
* `—`, the same as none at all: `Date.now() - NaN` fails every threshold and would
|
|
27
|
+
* otherwise fall through to `daysAgo` with `NaN` as its count.
|
|
28
|
+
*/
|
|
29
|
+
export declare function relativeTime(iso: string | null | undefined, now?: number): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useIntegrationsT } from "../composables/useIntegrationsT.js";
|
|
2
|
+
import { studioLocale } from "./localize.js";
|
|
3
|
+
const JUST_NOW = 1e4;
|
|
4
|
+
const MINUTE = 6e4;
|
|
5
|
+
const HOUR = 60 * MINUTE;
|
|
6
|
+
const DAY = 24 * HOUR;
|
|
7
|
+
export function relativeTime(iso, now = Date.now()) {
|
|
8
|
+
if (!iso) return "\u2014";
|
|
9
|
+
const at = new Date(iso).getTime();
|
|
10
|
+
if (Number.isNaN(at)) return "\u2014";
|
|
11
|
+
const t = useIntegrationsT();
|
|
12
|
+
const ms = now - at;
|
|
13
|
+
if (ms < JUST_NOW) return t("integration.common.time.justNow");
|
|
14
|
+
if (ms < MINUTE) return t("integration.common.time.secondsAgo", Math.floor(ms / 1e3));
|
|
15
|
+
if (ms < HOUR) return t("integration.common.time.minutesAgo", Math.floor(ms / MINUTE));
|
|
16
|
+
if (ms < DAY) return t("integration.common.time.hoursAgo", Math.floor(ms / HOUR));
|
|
17
|
+
if (ms < 30 * DAY) return t("integration.common.time.daysAgo", Math.floor(ms / DAY));
|
|
18
|
+
try {
|
|
19
|
+
return new Date(at).toLocaleDateString(studioLocale());
|
|
20
|
+
} catch {
|
|
21
|
+
return new Date(at).toLocaleDateString();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -33,12 +33,18 @@ export declare const API_RUN_STATUSES: readonly ["pending", "running", "complete
|
|
|
33
33
|
/**
|
|
34
34
|
* Platform status → the word the studio draws.
|
|
35
35
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
36
|
+
* The fold is not written here: {@link normalisedRunStatus} owns the one table of
|
|
37
|
+
* what each platform word means to the studio — case, separators, Temporal's
|
|
38
|
+
* American `canceled` — and this reads it. It kept a table of its own once, which
|
|
39
|
+
* knew neither `canceled` nor `COMPLETED`, so a row drew as failed a run its own
|
|
40
|
+
* record called cancelled (PO-391).
|
|
41
|
+
*
|
|
42
|
+
* What IS this function's is the clamp. The status column is an uncast string on
|
|
43
|
+
* the platform's side, so a row can arrive carrying a word no table lists; that
|
|
44
|
+
* word fails closed here for the reason it does everywhere else — a run the
|
|
45
|
+
* platform cannot classify must never read as a stuck `running`. The label passes
|
|
46
|
+
* the same word through instead, because it stores it as an identity; both clamps
|
|
47
|
+
* are deliberate, and only the table is shared.
|
|
42
48
|
*/
|
|
43
49
|
export declare function mapRunStatus(status: string): RunStatus;
|
|
44
50
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRunStatusWord, normalisedRunStatus } from "./statusStyle.js";
|
|
1
2
|
export const API_RUN_STATUSES = [
|
|
2
3
|
"pending",
|
|
3
4
|
"running",
|
|
@@ -7,24 +8,8 @@ export const API_RUN_STATUSES = [
|
|
|
7
8
|
"terminated"
|
|
8
9
|
];
|
|
9
10
|
export function mapRunStatus(status) {
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
}
|
|
11
|
+
const word = normalisedRunStatus(status);
|
|
12
|
+
return isRunStatusWord(word) ? word : "failed";
|
|
28
13
|
}
|
|
29
14
|
export function apiStatusesFor(words) {
|
|
30
15
|
if (!words.length) return [];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ConfigField } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a setting stores for what was typed into it.
|
|
4
|
+
*
|
|
5
|
+
* A numeric setting is filled in through either of two boxes — the plain one,
|
|
6
|
+
* and the one that also takes a mapped value — and only the plain one used to
|
|
7
|
+
* keep the number. A number typed in the other was stored as text, saved
|
|
8
|
+
* without a word, and refused the first time the workflow ran, in a message
|
|
9
|
+
* naming a setting whose own box had produced the text (PO-521). So the rule
|
|
10
|
+
* lives here, in one place, rather than in the box that happens to have it.
|
|
11
|
+
*
|
|
12
|
+
* WHEN it is applied differs by box, and has to: the plain one converts as it is
|
|
13
|
+
* typed, the mapped-value one is a document editor that reads its own value back,
|
|
14
|
+
* so it stores what was typed and converts when it is left. See `commitTyped` in
|
|
15
|
+
* NodeConfigField.
|
|
16
|
+
*
|
|
17
|
+
* Only a numeric setting is touched. A yes/no setting has the same shape of
|
|
18
|
+
* defect and is deliberately left alone: the platform refuses text there on
|
|
19
|
+
* purpose, and answering it is a different argument — recorded in the Gaps of
|
|
20
|
+
* `specs/node-configuration.md`.
|
|
21
|
+
*
|
|
22
|
+
* Three things are kept exactly as they were written:
|
|
23
|
+
* - a mapped value, which is what the second box is for, and which resolves to
|
|
24
|
+
* its own type when the workflow runs;
|
|
25
|
+
* - text denoting no number, so the operator still sees what they typed and
|
|
26
|
+
* the setting is refused by name rather than silently emptied;
|
|
27
|
+
* - anything that is not text at all.
|
|
28
|
+
*
|
|
29
|
+
* An emptied box reads as no value rather than as `''`, which is the answer the
|
|
30
|
+
* plain numeric box has always given: `''` is a value a node reads, while the
|
|
31
|
+
* absence is what its own default answers to.
|
|
32
|
+
*/
|
|
33
|
+
export declare function coerceSettingValue(field: Pick<ConfigField, 'type'>, value: unknown): unknown;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function coerceSettingValue(field, value) {
|
|
2
|
+
if (field?.type !== "number" || typeof value !== "string") return value;
|
|
3
|
+
if (value.includes("${{")) return value;
|
|
4
|
+
if (value.trim() === "") return null;
|
|
5
|
+
const n = Number(value);
|
|
6
|
+
if (!Number.isFinite(n) || /^\s*[+-]?0[xbo]/i.test(value)) return value;
|
|
7
|
+
return n;
|
|
8
|
+
}
|
|
@@ -47,14 +47,8 @@ export declare const RUN_STATUS_WORDS: readonly ["running", "succeeded", "failed
|
|
|
47
47
|
* this number is the fix at that point — not making the track elastic.
|
|
48
48
|
*/
|
|
49
49
|
export declare const RUN_STATUS_COLUMN_WIDTH = "160px";
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
* never reads "completed" in one surface and "succeeded" in another. This IS
|
|
53
|
-
* the studio's own chrome (Temporal's "completed" becomes our "succeeded"), so
|
|
54
|
-
* unlike a raw `run.status` read elsewhere, it is translated. Unknown statuses
|
|
55
|
-
* still pass through lowercased, untranslated — this function only relabels
|
|
56
|
-
* the fixed run-status vocabulary, not arbitrary strings.
|
|
57
|
-
*/
|
|
50
|
+
/** Whether a normalised status is one of the studio's own five words. */
|
|
51
|
+
export declare function isRunStatusWord(word: string): word is (typeof RUN_STATUS_WORDS)[number];
|
|
58
52
|
/**
|
|
59
53
|
* The platform's run vocabulary reduced to the studio's own five words — Temporal's
|
|
60
54
|
* `completed` is our `succeeded`, `pending` is `queued`, and two ways of dying are
|
|
@@ -64,6 +58,12 @@ export declare const RUN_STATUS_COLUMN_WIDTH = "160px";
|
|
|
64
58
|
* list narrows by it, and what a filter stores has to be something that does not move
|
|
65
59
|
* when the reader's language does. Both sides read this, so a filter and a row can
|
|
66
60
|
* never disagree about which status they mean.
|
|
61
|
+
*
|
|
62
|
+
* **The table below is the only one.** `mapRunStatus` reads it too and adds a clamp of
|
|
63
|
+
* its own — an unknown word fails closed there. The clamp here is this function's: an
|
|
64
|
+
* unknown word passes through, lower-cased, because it is stored as an identity and
|
|
65
|
+
* labelled as a raw field rather than as one of the studio's words. Change what a
|
|
66
|
+
* platform word *means* here; change what an *unknown* one becomes at the caller.
|
|
67
67
|
*/
|
|
68
68
|
export declare function normalisedRunStatus(status: string | null | undefined): string;
|
|
69
69
|
/**
|
|
@@ -79,6 +79,9 @@ export function toneClass(tone) {
|
|
|
79
79
|
export const RUN_STATUS_WORDS = ["running", "succeeded", "failed", "cancelled", "queued"];
|
|
80
80
|
export const RUN_STATUS_COLUMN_WIDTH = "160px";
|
|
81
81
|
const RUN_STATUS_WORD_SET = new Set(RUN_STATUS_WORDS);
|
|
82
|
+
export function isRunStatusWord(word) {
|
|
83
|
+
return RUN_STATUS_WORD_SET.has(word);
|
|
84
|
+
}
|
|
82
85
|
export function normalisedRunStatus(status) {
|
|
83
86
|
if (!status) return "";
|
|
84
87
|
const s = String(status).toLowerCase().replace(/[\s-]+/g, "_");
|
|
@@ -106,7 +109,7 @@ export function runStatusClass(status) {
|
|
|
106
109
|
export function runStatusLabel(status) {
|
|
107
110
|
const normalized = normalisedRunStatus(status);
|
|
108
111
|
if (!normalized) return "";
|
|
109
|
-
if (!
|
|
112
|
+
if (!isRunStatusWord(normalized)) return normalized;
|
|
110
113
|
return useIntegrationsT()(`integration.runs.status.${normalized}`);
|
|
111
114
|
}
|
|
112
115
|
const MISSED_RUN_STATUS_TONE = /* @__PURE__ */ new Map([
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ConfigField, Workflow, WorkflowBlob, WorkflowExecutionMode, WorkflowNode, WorkflowTrigger } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* A workflow as a file — what Export writes and Import reads (specs/workflow-file.md).
|
|
4
|
+
*
|
|
5
|
+
* Versioned like the editor's clipboard envelope (`revenexx.workflow.nodes`), and for
|
|
6
|
+
* the same reason: a file outlives the build that wrote it, so the reader has to be
|
|
7
|
+
* able to tell "not one of ours" and "one of ours from a format I do not know" apart
|
|
8
|
+
* from "broken", and refuse each of them rather than guess.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is pure. The stores, the download and the dialogs live with the
|
|
11
|
+
* callers; what a file may carry and how it lands in a tenant is decided here once, so
|
|
12
|
+
* the list, the new-workflow dialog and the editor cannot disagree about it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const WORKFLOW_FILE_KIND = "revenexx.workflow";
|
|
15
|
+
export declare const WORKFLOW_FILE_VERSION = 1;
|
|
16
|
+
export interface WorkflowFile {
|
|
17
|
+
kind: typeof WORKFLOW_FILE_KIND;
|
|
18
|
+
version: typeof WORKFLOW_FILE_VERSION;
|
|
19
|
+
workflow: {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string | null;
|
|
22
|
+
executionMode: WorkflowExecutionMode;
|
|
23
|
+
blobDefinitionVersion: string;
|
|
24
|
+
blob: WorkflowBlob;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The settings a node declares, or null where this environment does not know the node. */
|
|
28
|
+
export type FieldsFor = (node: WorkflowNode) => ReadonlyArray<ConfigField> | null;
|
|
29
|
+
/**
|
|
30
|
+
* The blob with every credential reference taken out of every node's settings.
|
|
31
|
+
*
|
|
32
|
+
* Two sweeps, because neither is complete alone:
|
|
33
|
+
* - **By declaration.** A setting the node declares as a credential is removed,
|
|
34
|
+
* also inside an object setting and inside each row of a list setting. This is
|
|
35
|
+
* what catches a reference to a credential of ANOTHER tenant — a file written
|
|
36
|
+
* elsewhere — which no list of this tenant's credentials would recognise.
|
|
37
|
+
* - **By value.** Any string equal to one of `credentialIds` is removed wherever
|
|
38
|
+
* it stands. This is what catches the settings no declaration describes: a node
|
|
39
|
+
* this environment does not have, and a setting whose shape is resolved while
|
|
40
|
+
* authoring rather than declared up front.
|
|
41
|
+
*
|
|
42
|
+
* Removed, not blanked: an absent key is what an unset setting looks like, and it
|
|
43
|
+
* is what the editor and the platform both report as "needs a credential".
|
|
44
|
+
*/
|
|
45
|
+
export declare function withoutCredentials(blob: WorkflowBlob, fieldsFor: FieldsFor, credentialIds?: ReadonlySet<string>): WorkflowBlob;
|
|
46
|
+
/** The file for a saved workflow: its settings, never its credentials. */
|
|
47
|
+
export declare function toWorkflowFile(wf: Workflow, fieldsFor: FieldsFor, credentialIds: ReadonlySet<string>): WorkflowFile;
|
|
48
|
+
/**
|
|
49
|
+
* Whether the workflow holds values of its own the operator may not mean to send
|
|
50
|
+
* along — the only case Export stops to say what the file contains. A variable
|
|
51
|
+
* declared without a value carries nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function hasVariableValues(blob: WorkflowBlob): boolean;
|
|
54
|
+
/** `Order sync (staging)` → `order-sync-staging.workflow.json`. */
|
|
55
|
+
export declare function workflowFileName(name: string): string;
|
|
56
|
+
/** Why a file was refused — each one is a different sentence to the operator. */
|
|
57
|
+
export type WorkflowFileRefusal = 'unreadable' | 'kind' | 'version' | 'definition';
|
|
58
|
+
export type ParsedWorkflowFile = {
|
|
59
|
+
ok: true;
|
|
60
|
+
file: WorkflowFile;
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
reason: WorkflowFileRefusal;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Reads a file, or says why not. Nothing a refused file contains reaches the caller.
|
|
67
|
+
*
|
|
68
|
+
* A definition version this studio does not know is refused here, not left to the
|
|
69
|
+
* platform: the studio writes its own version on every save, so a newer definition
|
|
70
|
+
* would not be refused on the way in — it would be relabelled and saved as one it
|
|
71
|
+
* is not.
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseWorkflowFile(text: string): ParsedWorkflowFile;
|
|
74
|
+
/**
|
|
75
|
+
* Gives each trigger a handle, keeping the one it has wherever the tenant can take it.
|
|
76
|
+
*
|
|
77
|
+
* A trigger's id IS its handle at the platform, unique per tenant. Keeping it across
|
|
78
|
+
* environments is what lets a later import over the same workflow recognise its
|
|
79
|
+
* triggers — and with them the webhook addresses callers already use. Where it cannot
|
|
80
|
+
* be kept (another workflow of this tenant holds it, it is not a handle the platform
|
|
81
|
+
* accepts, or it is `taken` for any other reason) the trigger gets a fresh one, and
|
|
82
|
+
* every connection and canvas position keyed by the old one follows.
|
|
83
|
+
*
|
|
84
|
+
* With every id in `taken`, this is exactly what duplicating does.
|
|
85
|
+
*/
|
|
86
|
+
export declare function assignTriggerHandles(blob: WorkflowBlob, taken: ReadonlySet<string>, keep?: (trigger: WorkflowTrigger) => boolean): WorkflowBlob;
|
|
87
|
+
/** Every trigger off: nothing a copy brings may act before somebody has looked at it. */
|
|
88
|
+
export declare function pausedTriggers(blob: WorkflowBlob): WorkflowBlob;
|
|
89
|
+
/** The blob a new workflow is created from: no credentials, handles kept where free, nothing on. */
|
|
90
|
+
export declare function blobForNewWorkflow(file: WorkflowFile, fieldsFor: FieldsFor, tenantHandles: ReadonlySet<string>, credentialIds?: ReadonlySet<string>): WorkflowBlob;
|
|
91
|
+
export interface OverwritePlan {
|
|
92
|
+
blob: WorkflowBlob;
|
|
93
|
+
/** What the workflow holds now, and what it will hold — for the confirmation. */
|
|
94
|
+
replaced: {
|
|
95
|
+
nodes: number;
|
|
96
|
+
triggers: number;
|
|
97
|
+
};
|
|
98
|
+
incoming: {
|
|
99
|
+
nodes: number;
|
|
100
|
+
triggers: number;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The content a file puts in place of an existing workflow's — all of it.
|
|
105
|
+
*
|
|
106
|
+
* Nodes, connections, variables, groups and notes come from the file. What the target
|
|
107
|
+
* keeps is what callers outside the studio depend on: a trigger in the file with the
|
|
108
|
+
* same id and kind as one the workflow already has stays THAT trigger — its handle,
|
|
109
|
+
* its endpoint, its secret and whether it is on — and takes its settings from the
|
|
110
|
+
* file. A trigger only the target had is gone; one only the file has arrives off,
|
|
111
|
+
* keeping its id where the tenant can take it.
|
|
112
|
+
*
|
|
113
|
+
* Credentials are not carried over from the target either. A node that had one before
|
|
114
|
+
* the import has none after it, and saving an active workflow is refused until they
|
|
115
|
+
* are chosen again — which is what keeps the running version running meanwhile.
|
|
116
|
+
*
|
|
117
|
+
* `otherHandles` are the trigger handles held by the tenant's OTHER workflows.
|
|
118
|
+
*/
|
|
119
|
+
export declare function overwritePlan(target: WorkflowBlob, file: WorkflowFile, fieldsFor: FieldsFor, otherHandles: ReadonlySet<string>, credentialIds?: ReadonlySet<string>): OverwritePlan;
|