@revenexx/studio-integrations 1.3.0 → 1.4.1
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 +457 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +61 -29
- package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +94 -49
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +37 -60
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +165 -56
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
- package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
- package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
- package/dist/runtime/composables/useCredentialTypes.js +3 -9
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
- package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
- package/dist/runtime/composables/useIntegrationsTemplates.js +5 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -7
- package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
- package/dist/runtime/data/canvasShortcuts.js +68 -0
- package/dist/runtime/i18n/messages/de.json +30 -16
- package/dist/runtime/i18n/messages/en.json +30 -16
- package/dist/runtime/pages/integrations/credentials.vue +40 -18
- package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
- package/dist/runtime/pages/integrations/workflows/[id].vue +128 -33
- package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
- package/dist/runtime/utils/apiEnvelope.js +20 -0
- package/dist/runtime/utils/apiError.d.ts +35 -8
- package/dist/runtime/utils/apiError.js +12 -12
- package/dist/runtime/utils/configDefaults.d.ts +18 -0
- package/dist/runtime/utils/configDefaults.js +7 -0
- package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
- package/dist/runtime/utils/errorPathAdvice.js +19 -0
- package/dist/runtime/utils/notify.d.ts +2 -2
- package/dist/runtime/utils/runEnvelope.d.ts +12 -0
- package/dist/runtime/utils/runEnvelope.js +2 -1
- package/dist/runtime/utils/selectOptions.d.ts +48 -0
- package/dist/runtime/utils/selectOptions.js +19 -0
- package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
- package/dist/runtime/utils/shortcutLabel.js +48 -0
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IntegrationsApiError } from "./apiError.js";
|
|
2
|
+
export function unwrapRecord(res, ownFields = []) {
|
|
3
|
+
if (res === null || res === void 0) {
|
|
4
|
+
throw new IntegrationsApiError(200, res, "");
|
|
5
|
+
}
|
|
6
|
+
if (typeof res === "object" && "data" in res && !ownFields.some((f) => f in res)) {
|
|
7
|
+
const data = res.data;
|
|
8
|
+
if (data === null || data === void 0) {
|
|
9
|
+
throw new IntegrationsApiError(200, res, "");
|
|
10
|
+
}
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
return res;
|
|
14
|
+
}
|
|
15
|
+
export function unwrapList(res) {
|
|
16
|
+
if (Array.isArray(res)) return res;
|
|
17
|
+
const data = res?.data;
|
|
18
|
+
if (Array.isArray(data)) return data;
|
|
19
|
+
throw new IntegrationsApiError(200, res, "");
|
|
20
|
+
}
|
|
@@ -35,21 +35,48 @@ export declare function apiValidationIssues(err: unknown): {
|
|
|
35
35
|
path: string;
|
|
36
36
|
message: string;
|
|
37
37
|
}[];
|
|
38
|
+
/** One message the service keyed to a field, as it keyed it. */
|
|
39
|
+
export interface RefusalMessage {
|
|
40
|
+
key: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A refusal split by what the form on screen can show of it.
|
|
45
|
+
*
|
|
46
|
+
* `atFields` is what goes under an input, keyed the way the form draws it; `unshown`
|
|
47
|
+
* is everything left over — the reasons that have no input to stand at, in the order
|
|
48
|
+
* the service listed them.
|
|
49
|
+
*/
|
|
50
|
+
export interface Refusal {
|
|
51
|
+
atFields: Record<string, string>;
|
|
52
|
+
unshown: RefusalMessage[];
|
|
53
|
+
}
|
|
38
54
|
/**
|
|
39
|
-
*
|
|
55
|
+
* A rejection split into the messages a form can show at a field and the messages it
|
|
56
|
+
* cannot, given the keys that form actually draws.
|
|
40
57
|
*
|
|
41
58
|
* The counterpart to `apiValidationIssues` above, for the ordinary case that has
|
|
42
59
|
* nothing to do with the workflow blob: a form field the service refused, named the
|
|
43
60
|
* way the form names it. Laravel answers a `422` with `errors` keyed by field and a
|
|
44
|
-
* list of messages per key; the first message is the one a person reads,
|
|
45
|
-
* the one kept.
|
|
61
|
+
* list of messages per key; at a field the first message is the one a person reads,
|
|
62
|
+
* so that is the one kept.
|
|
63
|
+
*
|
|
64
|
+
* **`drawn` is the form's own list, in the order the form draws it** — the name, then
|
|
65
|
+
* the fields of the chosen type, and nothing while those are hidden behind another
|
|
66
|
+
* source. A key absent from it has no input to be shown at, whether because no form
|
|
67
|
+
* draws that field (`config` as a whole, a field the type never declared, a key that
|
|
68
|
+
* is not a field at all) or because this form is currently drawing something else.
|
|
69
|
+
* Both cases go to `unshown`, which is what the surface states below the form: a
|
|
70
|
+
* refusal nobody can see is the same as no answer at all (PO-275, PO-477).
|
|
71
|
+
*
|
|
72
|
+
* A container that is only at fault because one of its members is — `config` beside
|
|
73
|
+
* `config.authKey`, which the service reports as two — is dropped wherever the member
|
|
74
|
+
* is reported too: it says the same thing twice, and the member carries it either way.
|
|
46
75
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* else about a field they then have to find (PO-275). Keys that address the blob are
|
|
50
|
-
* left out: those belong to `apiValidationIssues`, which resolves them to nodes.
|
|
76
|
+
* Keys that address the blob are left out: those belong to `apiValidationIssues`,
|
|
77
|
+
* which resolves them to nodes. No credential surface can receive one.
|
|
51
78
|
*/
|
|
52
|
-
export declare function
|
|
79
|
+
export declare function apiRefusal(err: unknown, drawn: readonly string[]): Refusal;
|
|
53
80
|
/**
|
|
54
81
|
* The reason a rejection gives, without the count of everything else it found.
|
|
55
82
|
*
|
|
@@ -18,20 +18,20 @@ export function apiValidationIssues(err) {
|
|
|
18
18
|
}
|
|
19
19
|
return issues;
|
|
20
20
|
}
|
|
21
|
-
export function
|
|
22
|
-
|
|
21
|
+
export function apiRefusal(err, drawn) {
|
|
22
|
+
const refusal = { atFields: {}, unshown: [] };
|
|
23
|
+
if (!(err instanceof IntegrationsApiError) || err.status !== 422) return refusal;
|
|
23
24
|
const errors = err.body?.errors;
|
|
24
|
-
if (!errors || typeof errors !== "object") return
|
|
25
|
-
const
|
|
26
|
-
for (const
|
|
27
|
-
if (
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (Object.keys(out).some((other) => other.startsWith(`${key}.`))) delete out[key];
|
|
25
|
+
if (!errors || typeof errors !== "object") return refusal;
|
|
26
|
+
const keys = Object.keys(errors).filter((key) => key !== "blob" && !key.startsWith("blob."));
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
if (keys.some((other) => other.startsWith(`${key}.`))) continue;
|
|
29
|
+
const messages = (Array.isArray(errors[key]) ? errors[key] : [errors[key]]).filter((message) => message !== void 0 && message !== null).map(String);
|
|
30
|
+
if (!messages.length) continue;
|
|
31
|
+
if (drawn.includes(key)) refusal.atFields[key] = messages[0];
|
|
32
|
+
else refusal.unshown.push(...messages.map((message) => ({ key, message })));
|
|
33
33
|
}
|
|
34
|
-
return
|
|
34
|
+
return refusal;
|
|
35
35
|
}
|
|
36
36
|
export function errorReason(err, fallback) {
|
|
37
37
|
const raw = err?.message?.trim();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConfigField } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* The config a set of settings starts life with: what each one declares as its
|
|
4
|
+
* default, and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* A setting with no `default` is left OUT rather than written as an empty
|
|
7
|
+
* string, because the two are different answers. `''` is a value — the engine
|
|
8
|
+
* reads it, `??` does not replace it, and a node whose own default would have
|
|
9
|
+
* applied never gets to apply it. An absent key is the absence the node's
|
|
10
|
+
* default answers to.
|
|
11
|
+
*
|
|
12
|
+
* Used in the two places a fresh set of values is made: a node dropped on the
|
|
13
|
+
* canvas (WorkflowEditor), and a row added to a list setting (NodeConfigField).
|
|
14
|
+
* Flat on purpose — it reads `default` at the level it is given and does not
|
|
15
|
+
* descend into `properties` or `items`, so what a newly inserted node carries
|
|
16
|
+
* is exactly what it carried before this became shared.
|
|
17
|
+
*/
|
|
18
|
+
export declare function defaultsFor(fields?: ReadonlyArray<Pick<ConfigField, 'key' | 'default'>>): Record<string, unknown>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { WorkflowBlob } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* Nodes that route failures to an error output nothing is connected to
|
|
4
|
+
* (specs/workflow-advice.md).
|
|
5
|
+
*
|
|
6
|
+
* A step that fails on a path its author drew is handled. A step that fails on a
|
|
7
|
+
* path nobody drew is not: the engine ends the run there
|
|
8
|
+
* (`specs/node-failure-handling.md` AC-8 in the service repository), and before
|
|
9
|
+
* it did, the failure vanished and the run reported success. Either way the
|
|
10
|
+
* author never chose it — they simply never saw the port.
|
|
11
|
+
*
|
|
12
|
+
* ## Why this is advice and not a finding
|
|
13
|
+
*
|
|
14
|
+
* The backend's validation findings feed the activation gate AND an
|
|
15
|
+
* unconditional refusal to start a run. A finding here would therefore stop
|
|
16
|
+
* every workflow with an unwired error port from running at all — turning
|
|
17
|
+
* "delivers nothing quietly" into "does not start", for authors who never asked
|
|
18
|
+
* for either. Computing it in the editor makes that impossible rather than
|
|
19
|
+
* merely unlikely: there is no value for a blocking consumer to read.
|
|
20
|
+
*
|
|
21
|
+
* What the client cannot do is be authoritative — the catalogue may not be
|
|
22
|
+
* loaded, and a workflow built through the API never passes here at all. For
|
|
23
|
+
* advice that is the right trade; for a gate it would not be, which is why the
|
|
24
|
+
* gate stays where the manifests are.
|
|
25
|
+
*
|
|
26
|
+
* ## Deliberately not in `validationFindings.ts`
|
|
27
|
+
*
|
|
28
|
+
* Everything there is a backend finding addressed by blob path, and the node
|
|
29
|
+
* card counts those as *incomplete settings*. An unwired port is neither. Sent
|
|
30
|
+
* through `groupFindingsByNode` it would type-check perfectly and quietly
|
|
31
|
+
* corrupt two counts — the banner's and the card badge's.
|
|
32
|
+
*/
|
|
33
|
+
export interface ErrorPathAdvice {
|
|
34
|
+
/** Blob node id — not an index, so it survives a node being added or removed. */
|
|
35
|
+
nodeId: string;
|
|
36
|
+
/** The error output with nothing attached. */
|
|
37
|
+
port: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every unwired error output in the blob, in node order.
|
|
41
|
+
*
|
|
42
|
+
* Runs against the blob the author is *editing*, not the saved one, so wiring a
|
|
43
|
+
* port clears its line in the same tick — no save, and no second copy of this
|
|
44
|
+
* rule to decide when the advice went stale.
|
|
45
|
+
*
|
|
46
|
+
* **Resolved against whichever manifest version the catalogue has.**
|
|
47
|
+
* `findManifest` falls back to another version of the same slug when the pinned
|
|
48
|
+
* one is absent, and that fallback is accepted here: a node pinned to a version
|
|
49
|
+
* nobody serves any more is better described by a sibling version than by
|
|
50
|
+
* silence. The cost is real though — where the versions disagree about their
|
|
51
|
+
* ports, the advice describes the sibling's, so a port could be named that the
|
|
52
|
+
* author's own node does not have.
|
|
53
|
+
*
|
|
54
|
+
* **Reads the manifest's static outputs only.** A node whose ports are named at
|
|
55
|
+
* author time (`sourceFromConfig` / `resolveOutputs`) cannot be judged from the
|
|
56
|
+
* declaration alone, so nothing is claimed about it — the same forbearance the
|
|
57
|
+
* engine shows. No shipped node has a dynamic error port today; if one appears,
|
|
58
|
+
* this is where it would silently escape notice.
|
|
59
|
+
*/
|
|
60
|
+
export declare function unwiredErrorPorts(blob: Pick<WorkflowBlob, 'nodes' | 'edges'>): ErrorPathAdvice[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { findManifest } from "../data/integrationNodeCatalog.js";
|
|
2
|
+
export function unwiredErrorPorts(blob) {
|
|
3
|
+
const advice = [];
|
|
4
|
+
for (const node of blob.nodes) {
|
|
5
|
+
const manifest = findManifest(node.nodeSlug, node.nodeVersion);
|
|
6
|
+
if (!manifest) continue;
|
|
7
|
+
const errorPorts = (manifest.outputs ?? []).filter((o) => o.kind === "error" && o.name).map((o) => o.name);
|
|
8
|
+
if (!errorPorts.length) continue;
|
|
9
|
+
if (!isReachable(node, blob, errorPorts)) continue;
|
|
10
|
+
for (const port of errorPorts) {
|
|
11
|
+
const wired = blob.edges.some((e) => e.from.nodeId === node.id && e.from.output === port);
|
|
12
|
+
if (!wired) advice.push({ nodeId: node.id, port });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return advice;
|
|
16
|
+
}
|
|
17
|
+
function isReachable(node, blob, errorPorts) {
|
|
18
|
+
return blob.edges.some((e) => e.to.nodeId === node.id) || blob.edges.some((e) => e.from.nodeId === node.id && !errorPorts.includes(e.from.output));
|
|
19
|
+
}
|
|
@@ -13,10 +13,10 @@ export declare const toast: ((message: string | (() => string | import("vue").Co
|
|
|
13
13
|
custom: (component: import("vue").Component, data?: import("vue-sonner").ExternalToast) => string | number;
|
|
14
14
|
message: (message: string | (() => string | import("vue").Component) | import("vue").Component, data?: import("vue-sonner").ExternalToast) => string | number;
|
|
15
15
|
promise: <ToastData>(promise: Promise<ToastData> | (() => Promise<ToastData>), data?: ({
|
|
16
|
+
id?: number | string | undefined;
|
|
17
|
+
class?: string | undefined;
|
|
16
18
|
action?: (import("vue-sonner").Action | import("vue").Component) | undefined;
|
|
17
19
|
icon?: import("vue").Component | undefined;
|
|
18
|
-
class?: string | undefined;
|
|
19
|
-
id?: number | string | undefined;
|
|
20
20
|
position?: ("top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-center" | "bottom-center") | undefined;
|
|
21
21
|
toasterId?: string | undefined;
|
|
22
22
|
component?: import("vue").Component | undefined;
|
|
@@ -11,6 +11,13 @@ export type RunLike = Record<string, unknown>;
|
|
|
11
11
|
* looks like a run (`status`, `temporal_run_id`) is the run, whatever else it has
|
|
12
12
|
* on it.
|
|
13
13
|
*
|
|
14
|
+
* The two guards are `RUN_OWN_FIELDS`, exported because `unwrapRecord` reads the
|
|
15
|
+
* same endpoint from `useIntegrationsRuns.ts` and has to apply the same rule —
|
|
16
|
+
* one list, two readers, rather than the two literals that used to sit here
|
|
17
|
+
* (PO-394). What stays different is deliberate: this one answers `null` where
|
|
18
|
+
* `unwrapRecord` throws, because a caller following a run has to count an
|
|
19
|
+
* unusable answer as a failed read rather than have it raised at it.
|
|
20
|
+
*
|
|
14
21
|
* This is deliberately generous about the *rest* of the shape. It answers "where
|
|
15
22
|
* is the run in this body", not "is this a usable run" — the caller decides that,
|
|
16
23
|
* because a caller following a run has to be able to count an unusable answer as
|
|
@@ -28,4 +35,9 @@ export declare const RUN_TERMINAL: Set<string>;
|
|
|
28
35
|
export declare function isTerminalRun(run: {
|
|
29
36
|
status?: unknown;
|
|
30
37
|
} | null | undefined): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The fields that mark a body as the run itself rather than an envelope around
|
|
40
|
+
* one. A run carrying its own `data` field would otherwise be unwrapped into it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const RUN_OWN_FIELDS: readonly ["status", "temporal_run_id"];
|
|
31
43
|
export declare function unwrapRun(res: unknown): RunLike | null;
|
|
@@ -2,10 +2,11 @@ export const RUN_TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "can
|
|
|
2
2
|
export function isTerminalRun(run) {
|
|
3
3
|
return RUN_TERMINAL.has(String(run?.status ?? "").toLowerCase());
|
|
4
4
|
}
|
|
5
|
+
export const RUN_OWN_FIELDS = ["status", "temporal_run_id"];
|
|
5
6
|
export function unwrapRun(res) {
|
|
6
7
|
if (!res || typeof res !== "object") return null;
|
|
7
8
|
const o = res;
|
|
8
|
-
if ("data" in o && o.data && typeof o.data === "object" && !(
|
|
9
|
+
if ("data" in o && o.data && typeof o.data === "object" && !RUN_OWN_FIELDS.some((f) => f in o)) {
|
|
9
10
|
return o.data;
|
|
10
11
|
}
|
|
11
12
|
return o;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying a node's declared options through a select that reserves the empty
|
|
3
|
+
* string for "nothing chosen".
|
|
4
|
+
*
|
|
5
|
+
* "No choice" is a legitimate thing for a node to offer — "as the file came in",
|
|
6
|
+
* "unchanged", "all kinds" — and three node types in two packages arrived at the
|
|
7
|
+
* same way of writing it: an option with an empty value. The select underneath
|
|
8
|
+
* this UI refuses it: the empty string is how it says nothing is selected, so an
|
|
9
|
+
* item may not also carry it. Handed one anyway it throws while the list mounts,
|
|
10
|
+
* and takes the surrounding dialog's dismissal machinery down with it — nothing
|
|
11
|
+
* opened afterwards could be closed again.
|
|
12
|
+
*
|
|
13
|
+
* So every option value is carried under a substitute and swapped back on the way
|
|
14
|
+
* out. **Every** one, rather than only the empty value: a sentinel that stood
|
|
15
|
+
* beside untouched values would be a value a node could declare itself, and
|
|
16
|
+
* declaring it would silently store the empty string instead. Here a declared
|
|
17
|
+
* value always arrives prefixed and the empty one never is, so the two cannot
|
|
18
|
+
* collide whatever a node writes. What the node reads is unchanged.
|
|
19
|
+
*/
|
|
20
|
+
/** What the option meaning "no choice" is carried under. */
|
|
21
|
+
export declare const EMPTY_OPTION_VALUE = "option-empty";
|
|
22
|
+
/** Does this list of options ask for the substitute at all? */
|
|
23
|
+
export declare function hasEmptyOption(options: Array<{
|
|
24
|
+
value: unknown;
|
|
25
|
+
}> | undefined): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* An option's value as the select may receive it. That is the guard: the item
|
|
28
|
+
* component cannot be handed an empty value from here, whatever a node declares.
|
|
29
|
+
*/
|
|
30
|
+
export declare function toItemValue(value: unknown): string;
|
|
31
|
+
/**
|
|
32
|
+
* The stored value as the select's model — encoded the way the items are, or it
|
|
33
|
+
* matches none of them and the trigger falls back to its placeholder.
|
|
34
|
+
*
|
|
35
|
+
* `null` is read as the empty option alongside the empty string, because that is
|
|
36
|
+
* what the platform hands back for one: Laravel converts an empty string to null
|
|
37
|
+
* anywhere in a request body, so the value chosen here returns as null on the next
|
|
38
|
+
* open and the choice would otherwise be gone. A setting with no value at all is
|
|
39
|
+
* not: nothing is stored for it, and a trigger claiming a choice the config does
|
|
40
|
+
* not hold would be the placeholder's lie in the other direction. A node that
|
|
41
|
+
* offers "no choice" declares it as the field's default, so a step placed on the
|
|
42
|
+
* canvas has the empty string from the start.
|
|
43
|
+
*/
|
|
44
|
+
export declare function toModelValue(value: unknown, options: Array<{
|
|
45
|
+
value: unknown;
|
|
46
|
+
}> | undefined): string;
|
|
47
|
+
/** What the select hands back, as the node wants it stored. */
|
|
48
|
+
export declare function fromModelValue(value: string): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const OPTION_VALUE_PREFIX = "option:";
|
|
2
|
+
export const EMPTY_OPTION_VALUE = "option-empty";
|
|
3
|
+
export function hasEmptyOption(options) {
|
|
4
|
+
return (options ?? []).some((o) => String(o.value) === "");
|
|
5
|
+
}
|
|
6
|
+
export function toItemValue(value) {
|
|
7
|
+
const s = String(value);
|
|
8
|
+
return s === "" ? EMPTY_OPTION_VALUE : OPTION_VALUE_PREFIX + s;
|
|
9
|
+
}
|
|
10
|
+
export function toModelValue(value, options) {
|
|
11
|
+
if (value === "" || value === null || value === void 0) {
|
|
12
|
+
return value !== void 0 && hasEmptyOption(options) ? EMPTY_OPTION_VALUE : "";
|
|
13
|
+
}
|
|
14
|
+
return OPTION_VALUE_PREFIX + String(value);
|
|
15
|
+
}
|
|
16
|
+
export function fromModelValue(value) {
|
|
17
|
+
if (value === EMPTY_OPTION_VALUE) return "";
|
|
18
|
+
return value.startsWith(OPTION_VALUE_PREFIX) ? value.slice(OPTION_VALUE_PREFIX.length) : value;
|
|
19
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a keyboard shortcut is written for a reader, and which physical modifier
|
|
3
|
+
* `mod` stands for.
|
|
4
|
+
*
|
|
5
|
+
* ONE COPY, deliberately. This table existed twice — in the canvas context
|
|
6
|
+
* menu and in the control pane's tooltip — and the platform test existed a
|
|
7
|
+
* third time inside the shortcuts composable. The two tables had already
|
|
8
|
+
* drifted: the tooltip's knew nothing about `backspace`, `tab` or `space`, so a
|
|
9
|
+
* token it was never handed would have rendered as `BACKSPACE`. A shortcut is
|
|
10
|
+
* about to be shown in a third place (the overview the canvas opens), which is
|
|
11
|
+
* two copies too many.
|
|
12
|
+
*
|
|
13
|
+
* `IS_MAC` reads `navigator.platform`, which is deprecated and reports
|
|
14
|
+
* `MacIntel` for an iPad. It is kept because the replacement
|
|
15
|
+
* (`navigator.userAgentData.platform`) is not in every browser we serve, and
|
|
16
|
+
* because being wrong here costs a wrong glyph rather than a wrong key: the
|
|
17
|
+
* MATCHING side reads the same flag, so a machine we misread is a machine where
|
|
18
|
+
* ⌘ and Ctrl swap together and nothing goes silent.
|
|
19
|
+
*/
|
|
20
|
+
export declare const IS_MAC: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* A shortcut token string as a reader sees it: `'mod+enter'` → `⌘↵` on a mac,
|
|
23
|
+
* `Ctrl+↵` elsewhere.
|
|
24
|
+
*
|
|
25
|
+
* Alternatives are not rendered — `'f2|space'` is two ways to press one thing,
|
|
26
|
+
* and a caller that wants both writes both. Pass `isMac` to render for the other
|
|
27
|
+
* platform, which is what the unit suite does.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* A token string split into its tokens, where one of the tokens may be the
|
|
31
|
+
* separator itself.
|
|
32
|
+
*
|
|
33
|
+
* `'+'.split('+')` is two empty strings, and `'mod++'` is `['mod', '', '']` — so
|
|
34
|
+
* a pair of empties is a literal `+`. Without this the character disappeared:
|
|
35
|
+
* on a mac, where caps are joined with nothing, `+` rendered as an empty key cap,
|
|
36
|
+
* and elsewhere the join only put it back by accident. Exported so the matcher
|
|
37
|
+
* reads a binding the same way this writes it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function shortcutTokens(tokens: string): string[];
|
|
40
|
+
export declare function formatShortcut(tokens: string, isMac?: boolean): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
|
|
2
|
+
const MAC_TOKENS = {
|
|
3
|
+
mod: "\u2318",
|
|
4
|
+
shift: "\u21E7",
|
|
5
|
+
alt: "\u2325",
|
|
6
|
+
ctrl: "\u2303"
|
|
7
|
+
};
|
|
8
|
+
const OTHER_TOKENS = {
|
|
9
|
+
mod: "Ctrl",
|
|
10
|
+
shift: "Shift",
|
|
11
|
+
alt: "Alt",
|
|
12
|
+
ctrl: "Ctrl"
|
|
13
|
+
};
|
|
14
|
+
const KEY_TOKENS = {
|
|
15
|
+
enter: "\u21B5",
|
|
16
|
+
backspace: "\u232B",
|
|
17
|
+
delete: "Del",
|
|
18
|
+
escape: "Esc",
|
|
19
|
+
tab: "Tab",
|
|
20
|
+
space: "Space",
|
|
21
|
+
arrowup: "\u2191",
|
|
22
|
+
arrowdown: "\u2193",
|
|
23
|
+
arrowleft: "\u2190",
|
|
24
|
+
arrowright: "\u2192"
|
|
25
|
+
};
|
|
26
|
+
export function shortcutTokens(tokens) {
|
|
27
|
+
const parts = tokens.split("+");
|
|
28
|
+
const out = [];
|
|
29
|
+
for (let i = 0; i < parts.length; i++) {
|
|
30
|
+
if (parts[i] === "" && parts[i + 1] === "") {
|
|
31
|
+
out.push("+");
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (parts[i] === "") continue;
|
|
36
|
+
out.push(parts[i]);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
export function formatShortcut(tokens, isMac = IS_MAC) {
|
|
41
|
+
const mods = isMac ? MAC_TOKENS : OTHER_TOKENS;
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const raw of shortcutTokens(tokens)) {
|
|
44
|
+
const t = raw.toLowerCase();
|
|
45
|
+
out.push(mods[t] ?? KEY_TOKENS[t] ?? (t.length === 1 ? t.toUpperCase() : t.charAt(0).toUpperCase() + t.slice(1)));
|
|
46
|
+
}
|
|
47
|
+
return isMac ? out.join("") : out.join("+");
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-integrations",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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": {
|