@revenexx/studio-integrations 1.2.0 → 1.4.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 +581 -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/EditorTip.vue +2 -15
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +15 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue +158 -49
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +15 -1
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.vue +136 -81
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
- 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 +200 -54
- 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/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
- 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 +10 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.js +11 -7
- package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
- package/dist/runtime/data/canvasShortcuts.js +68 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -0
- package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
- package/dist/runtime/data/nodeVersionMove.js +65 -0
- package/dist/runtime/i18n/messages/de.json +83 -20
- package/dist/runtime/i18n/messages/en.json +83 -20
- package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
- package/dist/runtime/pages/integrations/workflows/[id].vue +158 -53
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
- package/dist/runtime/types/integrations.d.ts +44 -1
- package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
- package/dist/runtime/utils/apiEnvelope.js +20 -0
- package/dist/runtime/utils/buildSettle.d.ts +68 -0
- package/dist/runtime/utils/buildSettle.js +29 -0
- 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/settingCondition.d.ts +48 -0
- package/dist/runtime/utils/settingCondition.js +84 -0
- package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
- package/dist/runtime/utils/shortcutLabel.js +48 -0
- package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
- package/dist/runtime/utils/stateNamespaceName.js +8 -0
- package/package.json +1 -1
|
@@ -3,15 +3,30 @@ import { AddCircle as Plus } from "#solar-icons";
|
|
|
3
3
|
import { Button } from "@revenexx/studio";
|
|
4
4
|
import WorkflowsTable from "../../../components/Integrations/WorkflowsTable.vue";
|
|
5
5
|
import WorkflowCreateDialog from "../../../components/Integrations/WorkflowCreateDialog.vue";
|
|
6
|
+
import { createBuildSettle, isBuildUnsettled } from "../../../utils/buildSettle";
|
|
6
7
|
const t = useIntegrationsT();
|
|
7
8
|
useHead({ title: () => t("integration.workflows.list.head.title") });
|
|
8
9
|
const { workflows, loading, error, load } = useIntegrationsWorkflows();
|
|
9
|
-
onMounted(() => {
|
|
10
|
-
|
|
10
|
+
onMounted(async () => {
|
|
11
|
+
await load();
|
|
12
|
+
settleBuildStates();
|
|
13
|
+
});
|
|
14
|
+
let onScreen = true;
|
|
15
|
+
const buildSettle = createBuildSettle();
|
|
16
|
+
function settleBuildStates() {
|
|
17
|
+
if (!onScreen) return;
|
|
18
|
+
void buildSettle.settle({
|
|
19
|
+
unsettled: () => workflows.value.some((wf) => isBuildUnsettled(wf.buildStatus)),
|
|
20
|
+
read: () => load(true)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
onUnmounted(() => {
|
|
24
|
+
onScreen = false;
|
|
25
|
+
buildSettle.stop();
|
|
11
26
|
});
|
|
12
27
|
const { buildTick } = useIntegrationsRealtime();
|
|
13
28
|
watch(buildTick, () => {
|
|
14
|
-
void load(true);
|
|
29
|
+
void load(true).then(() => settleBuildStates());
|
|
15
30
|
});
|
|
16
31
|
const loadFailed = computed(() => !!error && !!error.value && !workflows.value.length);
|
|
17
32
|
const createOpen = ref(false);
|
|
@@ -43,7 +58,11 @@ const createOpen = ref(false);
|
|
|
43
58
|
</Button>
|
|
44
59
|
</template>
|
|
45
60
|
</StateCard>
|
|
46
|
-
|
|
61
|
+
<!-- `loading` is the composable's app-wide flag and goes up for every read, the
|
|
62
|
+
build watch's included — so passing it straight through would flip the table
|
|
63
|
+
between its skeleton and its empty text every few seconds while a build
|
|
64
|
+
runs. What the table's skeleton means is "nothing to show yet". -->
|
|
65
|
+
<WorkflowsTable v-else :workflows="workflows" :loading="loading && !workflows.length" />
|
|
47
66
|
</div>
|
|
48
67
|
|
|
49
68
|
<WorkflowCreateDialog v-model:open="createOpen" />
|
|
@@ -158,11 +158,28 @@ export interface WorkflowIcon {
|
|
|
158
158
|
/** Image URL, e.g. a node-package logo (kind=image). */
|
|
159
159
|
src?: string;
|
|
160
160
|
}
|
|
161
|
+
/** The four things a workflow can remember between runs (PO-374). */
|
|
162
|
+
export type WorkflowStateRole = 'mapping' | 'cursor' | 'dedupe' | 'digest';
|
|
163
|
+
/** `private` belongs to this workflow; `shared` is tenant-wide and outlives it. */
|
|
164
|
+
export type WorkflowStateVisibility = 'private' | 'shared';
|
|
165
|
+
/**
|
|
166
|
+
* One state namespace a workflow declares it may reach. The engine mints the
|
|
167
|
+
* run's grant from exactly this list, so an undeclared namespace is not
|
|
168
|
+
* undocumented — it is refused at run time.
|
|
169
|
+
*/
|
|
170
|
+
export interface WorkflowStateNamespace {
|
|
171
|
+
namespace: string;
|
|
172
|
+
role: WorkflowStateRole;
|
|
173
|
+
/** Absent means `private`, which is what the engine's schema defaults to. */
|
|
174
|
+
visibility?: WorkflowStateVisibility;
|
|
175
|
+
}
|
|
161
176
|
export interface WorkflowBlob {
|
|
162
177
|
nodeManifestVersion: string;
|
|
163
178
|
name: LocalizedString;
|
|
164
179
|
description?: LocalizedString;
|
|
165
180
|
variables?: Record<string, WorkflowVariable>;
|
|
181
|
+
/** State namespaces this workflow may reach at run time (BlobDef `state`). */
|
|
182
|
+
state?: WorkflowStateNamespace[];
|
|
166
183
|
triggers: WorkflowTrigger[];
|
|
167
184
|
nodes: WorkflowNode[];
|
|
168
185
|
edges: WorkflowEdge[];
|
|
@@ -247,11 +264,25 @@ export interface NodeManifestOutput {
|
|
|
247
264
|
* publishes them. Author-time schema only, never live run data. */
|
|
248
265
|
fields?: string[] | Record<string, unknown>;
|
|
249
266
|
}
|
|
250
|
-
export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref'
|
|
267
|
+
export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref'
|
|
268
|
+
/** Names one of the workflow's declared state namespaces (PO-374). */
|
|
269
|
+
| 'state-ref' | 'dynamic-schema';
|
|
251
270
|
export interface ConfigFieldOption {
|
|
252
271
|
value: string | number | boolean;
|
|
253
272
|
label: LocalizedString;
|
|
254
273
|
}
|
|
274
|
+
/** The comparison words a settings condition may use. One vocabulary with the
|
|
275
|
+
* condition and filter nodes, so an author who has met these words in one
|
|
276
|
+
* place does not learn a second set for the other. */
|
|
277
|
+
export type SettingConditionOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual' | 'exists' | 'notExists' | 'isEmpty' | 'isNotEmpty';
|
|
278
|
+
/** A condition on another setting's value, deciding whether this one applies. */
|
|
279
|
+
export interface SettingCondition {
|
|
280
|
+
/** The `key` of another setting at the same level. */
|
|
281
|
+
key: string;
|
|
282
|
+
op: SettingConditionOperator;
|
|
283
|
+
/** Left out by the four operators that read presence rather than content. */
|
|
284
|
+
value?: string | number | boolean;
|
|
285
|
+
}
|
|
255
286
|
export interface ConfigField {
|
|
256
287
|
key: string;
|
|
257
288
|
label: LocalizedString;
|
|
@@ -278,8 +309,20 @@ export interface ConfigField {
|
|
|
278
309
|
* (e.g. 'revenexx:business-central'). An array lets a node accept more than
|
|
279
310
|
* one credential type; the picker merges the tenant's credentials across all. */
|
|
280
311
|
credentialType?: string | string[];
|
|
312
|
+
/**
|
|
313
|
+
* Only for `type: 'state-ref'` (PO-374): which kind of state namespace the
|
|
314
|
+
* field accepts. The picker lists the workflow's declarations of that role;
|
|
315
|
+
* the stored value is the namespace NAME.
|
|
316
|
+
*/
|
|
317
|
+
stateRole?: WorkflowStateRole;
|
|
281
318
|
/** keys of other config fields this field's options/schema depend on. */
|
|
282
319
|
dependsOn?: string[];
|
|
320
|
+
/** When this setting applies at all (PO-410): another setting's key, an
|
|
321
|
+
* operator, and where the operator takes one, the value to compare against.
|
|
322
|
+
* Drawn only while the condition holds — see utils/settingCondition. Not
|
|
323
|
+
* `dependsOn`, which decides what a setting resolves to rather than whether
|
|
324
|
+
* it is drawn. */
|
|
325
|
+
showIf?: SettingCondition;
|
|
283
326
|
validation?: {
|
|
284
327
|
pattern?: string;
|
|
285
328
|
min?: number;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One record, from `{ data: record }` or from a bare record.
|
|
3
|
+
*
|
|
4
|
+
* Throws when the response carries an envelope with nothing in it, or no body at
|
|
5
|
+
* all — the case the old expression turned into an object with none of the
|
|
6
|
+
* caller's fields, which every page downstream drew as empty.
|
|
7
|
+
*
|
|
8
|
+
* **The thrown message is deliberately empty.** `errorReason(err, fallback)` in
|
|
9
|
+
* `apiError.ts` falls back to the caller's own translated sentence when the
|
|
10
|
+
* message is blank, so a shape violation surfaces as the page's ordinary "could
|
|
11
|
+
* not load this" copy rather than as an untranslated technical string in the UI.
|
|
12
|
+
* The response that caused it rides along in `body`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function unwrapRecord<T>(res: unknown, ownFields?: readonly string[]): T;
|
|
15
|
+
/**
|
|
16
|
+
* A list, from `{ data: [ … ] }` or from a bare array.
|
|
17
|
+
*
|
|
18
|
+
* An empty collection is `[]` or `{ "data": [] }` and answers empty. A body that is
|
|
19
|
+
* neither throws, for the same reason the record above does — and it was drafted the
|
|
20
|
+
* other way round, returning empty for anything unreadable, until a review pointed
|
|
21
|
+
* out what that costs: the workflows listing used to raise (`ingestList` mapped over
|
|
22
|
+
* the body and threw inside its own `try`), so an unreadable answer reached the
|
|
23
|
+
* reader as an error. Empty would have turned that into "no workflows yet" for a
|
|
24
|
+
* tenant whose workflows are all still there — a silent wrong answer in place of a
|
|
25
|
+
* loud one, in the change that exists to remove exactly that.
|
|
26
|
+
*/
|
|
27
|
+
export declare function unwrapList<T>(res: unknown): T[];
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watching a queued bundle build out, on any surface that reports one.
|
|
3
|
+
*
|
|
4
|
+
* A save queues a build, and until the platform has finished it the workflow cannot
|
|
5
|
+
* run. Both surfaces that say so read the state off the workflow row — the editor's
|
|
6
|
+
* chip beside Save, the build column in the workflows list — so neither of them
|
|
7
|
+
* learns that a build finished unless the row is read again.
|
|
8
|
+
*
|
|
9
|
+
* Two things can make that happen. A realtime build report, which is the fast path
|
|
10
|
+
* and is not always there: the topics have no producer in every environment, the
|
|
11
|
+
* connection can be off for a tenant, and a socket can drop in the second after the
|
|
12
|
+
* save. And re-reading the row, which is this — the floor under the fast path, and
|
|
13
|
+
* the only one of the two that works everywhere today.
|
|
14
|
+
*
|
|
15
|
+
* The editor has had it since PO-398, where a chip that never cleared was reported
|
|
16
|
+
* as "saving does not work"; the list kept the same defect for as long as it had no
|
|
17
|
+
* loop of its own (PO-416). So the loop lives here rather than in either surface:
|
|
18
|
+
* one interval, one ceiling, and one rule about which call wins, for both.
|
|
19
|
+
*
|
|
20
|
+
* WHAT A CALLER SUPPLIES is what differs between the two surfaces, and it is only
|
|
21
|
+
* ever these two things: whether anything is still unsettled — one workflow in the
|
|
22
|
+
* editor, any row in the list — and how to read it again. `settle()` decides
|
|
23
|
+
* nothing about the answer; the caller's own state is what every surface renders
|
|
24
|
+
* from.
|
|
25
|
+
*
|
|
26
|
+
* A GENERATION RATHER THAN A BUSY FLAG. Two saves in quick succession must not
|
|
27
|
+
* leave the second one unwatched, and the earlier loop cannot be relied on to cover
|
|
28
|
+
* it: a read already in flight can land after the newer save and put the pre-save
|
|
29
|
+
* row back into the cache, after which the older loop reads `ready` and stops. So
|
|
30
|
+
* the newest call wins and every older loop retires at its next turn.
|
|
31
|
+
*
|
|
32
|
+
* THE CEILING is not a timeout in the usual sense — nothing is reported when it is
|
|
33
|
+
* reached. A build is seconds; a build still queued after five minutes is a state
|
|
34
|
+
* worth leaving on screen rather than asking about forever, and the operator's own
|
|
35
|
+
* reload starts a fresh watch (which is why arriving on an unsettled row starts one).
|
|
36
|
+
*/
|
|
37
|
+
/** Between reads. Same cadence the editor's queued-run wait uses. */
|
|
38
|
+
export declare const BUILD_SETTLE_POLL_MS = 2500;
|
|
39
|
+
/** After this, the state stands as last read and nothing asks again. */
|
|
40
|
+
export declare const BUILD_SETTLE_TIMEOUT_MS: number;
|
|
41
|
+
/**
|
|
42
|
+
* Whether a build state is one that changes on its own.
|
|
43
|
+
*
|
|
44
|
+
* `ready` and `failed` are where a build lands; anything else — including a row
|
|
45
|
+
* whose state the platform did not send — is nothing to wait for.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isBuildUnsettled(status?: string | null): boolean;
|
|
48
|
+
export interface BuildSettleWatch {
|
|
49
|
+
/** Whether anything is still worth waiting for. Read fresh on every turn. */
|
|
50
|
+
unsettled: () => boolean;
|
|
51
|
+
/** Reads the state again. Its own failures are its business — see below. */
|
|
52
|
+
read: () => Promise<unknown>;
|
|
53
|
+
}
|
|
54
|
+
export interface BuildSettleOptions {
|
|
55
|
+
pollMs?: number;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface BuildSettle {
|
|
59
|
+
/**
|
|
60
|
+
* Reads until nothing is unsettled, or until the ceiling. Resolves either way and
|
|
61
|
+
* throws nothing: a read that fails leaves the state as it stands and the next
|
|
62
|
+
* turn tries again, which is the same answer a surface gives its reader anyway.
|
|
63
|
+
*/
|
|
64
|
+
settle: (watch: BuildSettleWatch) => Promise<void>;
|
|
65
|
+
/** Retires whatever is watching — for a surface being left. */
|
|
66
|
+
stop: () => void;
|
|
67
|
+
}
|
|
68
|
+
export declare function createBuildSettle(options?: BuildSettleOptions): BuildSettle;
|
|
@@ -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,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,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
|
+
}
|