@revenexx/studio-integrations 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue +65 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.vue +103 -25
- 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/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/WorkflowEditor.vue +39 -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/composables/useIntegrationsTemplates.js +5 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.js +6 -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 +53 -4
- package/dist/runtime/i18n/messages/en.json +53 -4
- package/dist/runtime/pages/integrations/workflows/[id].vue +30 -20
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
- package/dist/runtime/types/integrations.d.ts +44 -1
- package/dist/runtime/utils/buildSettle.d.ts +68 -0
- package/dist/runtime/utils/buildSettle.js +29 -0
- package/dist/runtime/utils/settingCondition.d.ts +48 -0
- package/dist/runtime/utils/settingCondition.js +84 -0
- package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
- package/dist/runtime/utils/stateNamespaceName.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,160 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 593c0d5: A step can be changed onto another version of its node
|
|
8
|
+
|
|
9
|
+
The editor could say precisely that a step sat on a node version the platform
|
|
10
|
+
cannot run, and name the version that would work — and there it stopped. Getting
|
|
11
|
+
there meant deleting the step and inserting a fresh one, which throws away its
|
|
12
|
+
settings, its name, its note and its connections.
|
|
13
|
+
|
|
14
|
+
The version is now a control wherever there is somewhere to go, and two states
|
|
15
|
+
reach it. Where the pinned version cannot run, the red notice keeps every word it
|
|
16
|
+
had and gains the button it was missing. Where the step runs fine and a newer
|
|
17
|
+
version exists, one grey line in its dialog says so — **and nothing else does**.
|
|
18
|
+
No mark on the canvas card: nothing is wrong there, and a fourth mark for a
|
|
19
|
+
non-problem is what teaches an operator to stop reading marks. A node with a
|
|
20
|
+
single usable version says nothing at all.
|
|
21
|
+
|
|
22
|
+
Older versions are offered too, not only newer ones. A version that turns out to
|
|
23
|
+
behave badly otherwise has no way back.
|
|
24
|
+
|
|
25
|
+
**What the change costs is said before it happens.** The two versions are read
|
|
26
|
+
against the settings the step actually holds, and split three ways: what travels,
|
|
27
|
+
what the chosen version does not have and will be taken out, and what arrives
|
|
28
|
+
needing an eye — a setting it requires and the step has no answer for, or one
|
|
29
|
+
whose type or choices moved. A setting nobody filled in appears nowhere; there is
|
|
30
|
+
nothing to lose. A change that costs nothing says so in a sentence rather than
|
|
31
|
+
showing three empty lists, and where the version being left is no longer in the
|
|
32
|
+
catalogue the answer says the two could not be compared at all.
|
|
33
|
+
|
|
34
|
+
Values the chosen version does not declare are removed rather than left behind.
|
|
35
|
+
The service keeps such a key on save without complaint, so it would otherwise sit
|
|
36
|
+
in the workflow as invisible dead data.
|
|
37
|
+
|
|
38
|
+
Nothing about the step's identity moves: same id, same connections, same name,
|
|
39
|
+
same note, same place on the canvas. It lands as an unsaved change like editing
|
|
40
|
+
any setting, so discarding the editor's changes undoes it.
|
|
41
|
+
|
|
42
|
+
Promised as `specs/node-configuration.md` AC-24 to AC-26. `specs/node-palette.md`
|
|
43
|
+
AC-7 gains the clause that the version it names is somewhere the step can
|
|
44
|
+
actually go, and loses the gap entry that described the dead end.
|
|
45
|
+
|
|
46
|
+
- bb8a80a: Declare what a workflow may remember between runs (PO-374).
|
|
47
|
+
|
|
48
|
+
A new **State** entry in the editor's floating control pane (Shift+T) opens a
|
|
49
|
+
dialog for the blob's `state` block: each namespace with its role — mapping,
|
|
50
|
+
cursor, dedupe or digest — and whether it belongs to this workflow alone or to
|
|
51
|
+
every workflow in the tenant. The engine mints a run's grant from exactly this
|
|
52
|
+
list, so until now a workflow could not reach the state store from the studio at
|
|
53
|
+
all.
|
|
54
|
+
|
|
55
|
+
The role is chosen when the namespace is added and stated as text afterwards:
|
|
56
|
+
entries written with one meaning cannot be read back with another. What sharing
|
|
57
|
+
costs — tenant-wide reads _and_ writes, and a namespace that outlives this
|
|
58
|
+
workflow — is on screen next to the control that chooses it.
|
|
59
|
+
|
|
60
|
+
Also fixes a silent loss in both directions of the workflow mapper, which
|
|
61
|
+
enumerates blob keys one by one: `state` was neither read from the API nor sent
|
|
62
|
+
back on save, so a declaration made anywhere else would have disappeared on the
|
|
63
|
+
next save from the studio.
|
|
64
|
+
|
|
65
|
+
A node setting of the new type `state-ref` picks one of the workflow's declared
|
|
66
|
+
namespaces **of that setting's kind**, and declares a new one from inside the
|
|
67
|
+
picker — the same shape a `credentials-ref` has, so the author never leaves the
|
|
68
|
+
node to type a name that nothing would have compared.
|
|
69
|
+
|
|
70
|
+
A namespace declared inside the picker waits in the node's draft until the
|
|
71
|
+
inspector keeps it — Save, or prev/next — so Esc discards the declaration
|
|
72
|
+
together with the setting that named it, and the workflow does not report
|
|
73
|
+
unsaved changes for something nobody chose to keep.
|
|
74
|
+
|
|
75
|
+
A node's test run now names the workflow being edited, which lets it read that
|
|
76
|
+
workflow's namespaces. Writes stay refused at author time.
|
|
77
|
+
|
|
78
|
+
Promised behaviour: `specs/state-namespaces.md`.
|
|
79
|
+
|
|
80
|
+
- ddff18a: A setting that cannot apply is no longer put in front of the author
|
|
81
|
+
|
|
82
|
+
A node can now say in its manifest when one of its settings applies — another
|
|
83
|
+
setting's key, an operator, and the value to compare against — and the
|
|
84
|
+
configuration form draws it only while that holds. Until now the only thing
|
|
85
|
+
warning an author off a meaningless field was a sentence in small print under
|
|
86
|
+
it, which they read last, having already filled it in. Date Time's **Source**
|
|
87
|
+
is the plainest case: under "Now" it still asked which field to read the date
|
|
88
|
+
out of.
|
|
89
|
+
|
|
90
|
+
What changed on screen:
|
|
91
|
+
|
|
92
|
+
- A setting whose condition does not hold is not drawn, and is not counted
|
|
93
|
+
among the optional ones either — a fold that offers four settings and shows
|
|
94
|
+
three teaches the reader to distrust the number.
|
|
95
|
+
- What was typed into a setting that stops applying is dropped, the same
|
|
96
|
+
cascade the studio already runs for a setting whose options another one
|
|
97
|
+
resolves. A value nowhere on screen is a value nobody can correct.
|
|
98
|
+
- Settings a node resolves while the author edits are treated the same way.
|
|
99
|
+
|
|
100
|
+
The words a condition is written in are the fourteen the condition and filter
|
|
101
|
+
nodes already offer — `equals`, `isEmpty`, `contains`, `>` and the rest — so an
|
|
102
|
+
author who has met them in one place does not learn a second set for the other.
|
|
103
|
+
`specs/node-configuration.md` AC-27 and AC-28 carry the promise.
|
|
104
|
+
|
|
105
|
+
Nothing changes for a node that says nothing: every setting is drawn exactly as
|
|
106
|
+
before. A condition written on a setting _inside a group_ is not honoured here —
|
|
107
|
+
a group is drawn as one value editor rather than as its own settings — and that
|
|
108
|
+
is written into the spec's Gaps rather than left to be discovered.
|
|
109
|
+
|
|
110
|
+
### Patch Changes
|
|
111
|
+
|
|
112
|
+
- 4dd5b12: The answer that keeps unsaved work is legible, and is the one the question emphasises
|
|
113
|
+
|
|
114
|
+
Leaving a workflow with unsaved changes asks whether to discard them, and the
|
|
115
|
+
question offers two answers. Only one of them looked like an answer: "Keep editing"
|
|
116
|
+
was drawn in a colour its own background shares, so it read as an empty button beside
|
|
117
|
+
"Discard and leave" and its label appeared only once the pointer rested on it. Which
|
|
118
|
+
is the wrong half of the dialog to hide — that button is the one the keyboard lands
|
|
119
|
+
on, so the answer the question exists to make easy was the answer nobody could see.
|
|
120
|
+
|
|
121
|
+
It is now drawn as the filled answer, the way every other dialog in the module draws
|
|
122
|
+
the answer on the right, so the question also says which answer it falls back to. The
|
|
123
|
+
cause was two button variants on one element: the dialog's cancel element applies the
|
|
124
|
+
outline variant itself, and the button inside it was left on the default one, so the
|
|
125
|
+
merge took the background from the first and the text colour from the second. Nothing
|
|
126
|
+
else about the question moves — the wording, the keyboard default and what each answer
|
|
127
|
+
does are unchanged.
|
|
128
|
+
|
|
129
|
+
Promised as `specs/workflow-save.md` AC-13 and AC-14: the label is checked as a
|
|
130
|
+
contrast ratio against the button it sits on, unhovered, and the emphasis as a
|
|
131
|
+
difference between the two answers' surfaces rather than against a class name or a
|
|
132
|
+
colour value.
|
|
133
|
+
|
|
134
|
+
- ab2eb96: A build state stops saying "still building" wherever it is shown
|
|
135
|
+
|
|
136
|
+
Saving a workflow queues the platform to build an executable bundle from it, and
|
|
137
|
+
until that build lands the workflow cannot run. Two surfaces say so: the chip beside
|
|
138
|
+
Save in the editor, and the build column in the workflows list. Only the editor's
|
|
139
|
+
ever became true on its own — the list said whatever its last read said, so a
|
|
140
|
+
workflow that finished building while the list stood open kept reading "Building…"
|
|
141
|
+
until something reloaded the page. Save in the editor, come back to the list, and the
|
|
142
|
+
row never moved.
|
|
143
|
+
|
|
144
|
+
The list now watches a build out the way the editor does: one read of the tenant per
|
|
145
|
+
turn, so thirty workflows building at once cost what one costs, and it stops as soon
|
|
146
|
+
as nothing is waiting. A platform build report still makes it quicker where there is
|
|
147
|
+
one; nothing depends on one arriving any more, on either surface.
|
|
148
|
+
|
|
149
|
+
The two waits now share one interval and one ceiling instead of the editor owning
|
|
150
|
+
both, so neither surface can drift into asking twice as often as the other.
|
|
151
|
+
|
|
152
|
+
Promised as `specs/workflow-build.md`, a new spec for the subject: what becomes of a
|
|
153
|
+
saved workflow before it can run. AC-1 is the editor's promise, moved out of
|
|
154
|
+
`specs/workflow-save.md`, where a reader asking what a build state means would not
|
|
155
|
+
have looked; AC-2 is the list's. `specs/realtime-updates.md` AC-7 keeps the
|
|
156
|
+
announcement half and now says which of the two makes a build state true.
|
|
157
|
+
|
|
3
158
|
## 1.2.0
|
|
4
159
|
|
|
5
160
|
### Minor Changes
|
package/dist/module.json
CHANGED
|
@@ -3,6 +3,7 @@ import { computed } from "vue";
|
|
|
3
3
|
import { Button } from "@revenexx/studio";
|
|
4
4
|
import { IntegrationsApiError } from "../../utils/apiError";
|
|
5
5
|
import { resolveLocalized } from "../../utils/localize";
|
|
6
|
+
import { applyingFields, staleKeys } from "../../utils/settingCondition";
|
|
6
7
|
const props = defineProps({
|
|
7
8
|
slug: { type: String, required: true },
|
|
8
9
|
version: { type: String, required: true },
|
|
@@ -24,6 +25,13 @@ const q = useNodeConfigResolve({
|
|
|
24
25
|
dependsOn: () => props.field.dependsOn
|
|
25
26
|
});
|
|
26
27
|
const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
28
|
+
const applyingResolvedFields = computed(() => applyingFields(resolvedFields.value, props.config));
|
|
29
|
+
function writeResolved(key, value) {
|
|
30
|
+
emit("set", key, value);
|
|
31
|
+
const after = { ...props.config, [key]: value };
|
|
32
|
+
for (const stale of staleKeys(resolvedFields.value, key, after))
|
|
33
|
+
emit("set", stale, void 0);
|
|
34
|
+
}
|
|
27
35
|
</script>
|
|
28
36
|
|
|
29
37
|
<template>
|
|
@@ -67,7 +75,7 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
|
67
75
|
<legend class="px-1 text-mono text-muted-foreground">
|
|
68
76
|
{{ resolveLocalized(field.label) }}
|
|
69
77
|
</legend>
|
|
70
|
-
<template v-for="f in
|
|
78
|
+
<template v-for="f in applyingResolvedFields" :key="f.key">
|
|
71
79
|
<!-- collision guard: a resolved key that shadows a static config field is skipped -->
|
|
72
80
|
<p v-if="staticKeys.includes(f.key)" class="text-sm text-amber-600">
|
|
73
81
|
{{ t("integration.nodeConfig.dynamicSchema.fieldCollision", { key: f.key }) }}
|
|
@@ -89,7 +97,7 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
|
|
|
89
97
|
:static-keys="staticKeys"
|
|
90
98
|
:sibling-fields="resolvedFields"
|
|
91
99
|
:depth="(depth ?? 0) + 1"
|
|
92
|
-
@set="
|
|
100
|
+
@set="writeResolved"
|
|
93
101
|
/>
|
|
94
102
|
</template>
|
|
95
103
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConfigField, WorkflowVariable } from '../../types/integrations.js';
|
|
1
|
+
import type { ConfigField, WorkflowStateNamespace, WorkflowStateRole, WorkflowStateVisibility, WorkflowVariable } from '../../types/integrations.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
field: ConfigField;
|
|
4
4
|
config: Record<string, unknown>;
|
|
@@ -11,6 +11,8 @@ type __VLS_Props = {
|
|
|
11
11
|
siblingFields?: ConfigField[];
|
|
12
12
|
depth?: number;
|
|
13
13
|
variables?: Record<string, WorkflowVariable>;
|
|
14
|
+
/** The workflow's declared state namespaces — the options a `state-ref` offers. */
|
|
15
|
+
stateNamespaces?: WorkflowStateNamespace[];
|
|
14
16
|
upstreamOutputs?: Array<{
|
|
15
17
|
nodeId: string;
|
|
16
18
|
nodeName?: string;
|
|
@@ -22,7 +24,9 @@ type __VLS_Props = {
|
|
|
22
24
|
};
|
|
23
25
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
24
26
|
set: (key: string, value: unknown) => any;
|
|
27
|
+
"declare-state": (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
|
|
25
28
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
26
29
|
onSet?: ((key: string, value: unknown) => any) | undefined;
|
|
30
|
+
"onDeclare-state"?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
|
|
27
31
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
32
|
export default _default;
|
|
@@ -26,10 +26,11 @@ const props = defineProps({
|
|
|
26
26
|
siblingFields: { type: Array, required: false },
|
|
27
27
|
depth: { type: Number, required: false },
|
|
28
28
|
variables: { type: Object, required: false },
|
|
29
|
+
stateNamespaces: { type: Array, required: false },
|
|
29
30
|
upstreamOutputs: { type: Array, required: false },
|
|
30
31
|
refused: { type: String, required: false }
|
|
31
32
|
});
|
|
32
|
-
const emit = defineEmits(["set"]);
|
|
33
|
+
const emit = defineEmits(["set", "declare-state"]);
|
|
33
34
|
const t = useIntegrationsT();
|
|
34
35
|
const value = computed(() => props.config[props.field.key]);
|
|
35
36
|
function write(v) {
|
|
@@ -127,6 +128,30 @@ function onCredentialSelect(v) {
|
|
|
127
128
|
}
|
|
128
129
|
write(v);
|
|
129
130
|
}
|
|
131
|
+
const stateOptions = computed(() => {
|
|
132
|
+
const role = props.field.stateRole;
|
|
133
|
+
const declared = props.stateNamespaces ?? [];
|
|
134
|
+
const list = role ? declared.filter((n) => n.role === role) : declared;
|
|
135
|
+
const selected = value.value;
|
|
136
|
+
if (typeof selected === "string" && selected && !list.some((n) => n.namespace === selected)) {
|
|
137
|
+
return [{ namespace: selected, role: role ?? "mapping", visibility: void 0 }, ...list];
|
|
138
|
+
}
|
|
139
|
+
return list;
|
|
140
|
+
});
|
|
141
|
+
const stateTakenNames = computed(() => (props.stateNamespaces ?? []).map((n) => n.namespace));
|
|
142
|
+
const stateCreateOpen = ref(false);
|
|
143
|
+
const STATE_ACTION_NEW = "__action_new_state_namespace__";
|
|
144
|
+
function onStateSelect(v) {
|
|
145
|
+
if (v === STATE_ACTION_NEW) {
|
|
146
|
+
stateCreateOpen.value = true;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
write(v);
|
|
150
|
+
}
|
|
151
|
+
function onStateDeclared(namespace, role, visibility) {
|
|
152
|
+
emit("declare-state", namespace, role, visibility);
|
|
153
|
+
write(namespace);
|
|
154
|
+
}
|
|
130
155
|
function acceptsSftp(field) {
|
|
131
156
|
const t2 = field.credentialType;
|
|
132
157
|
return t2 === "revenexx:sftp" || Array.isArray(t2) && t2.includes("revenexx:sftp");
|
|
@@ -619,6 +644,45 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
619
644
|
/>
|
|
620
645
|
</template>
|
|
621
646
|
|
|
647
|
+
<!-- state-ref — pick one of the workflow's declared namespaces of this
|
|
648
|
+
field's role. Declaring lives INSIDE the picker, like creating a
|
|
649
|
+
credential does: a picker whose list is empty must not be a dead end
|
|
650
|
+
that sends the author to another dialog and back. -->
|
|
651
|
+
<template v-else-if="field.type === 'state-ref'">
|
|
652
|
+
<Select
|
|
653
|
+
:model-value="value ?? ''"
|
|
654
|
+
@update:model-value="(v) => onStateSelect(v)"
|
|
655
|
+
>
|
|
656
|
+
<SelectTrigger :aria-label="resolveLocalized(field.label)" size="sm" class="h-8 w-full rounded-md border border-black/[0.08] ring-0 shadow-none text-sm bg-white">
|
|
657
|
+
<SelectValue :placeholder="t('integration.nodeConfig.state.placeholder')" />
|
|
658
|
+
</SelectTrigger>
|
|
659
|
+
<SelectContent>
|
|
660
|
+
<SelectItem v-for="o in stateOptions" :key="o.namespace" :value="o.namespace">
|
|
661
|
+
<span class="flex items-center gap-1.5">
|
|
662
|
+
<span class="min-w-0 truncate font-mono">{{ o.namespace }}</span>
|
|
663
|
+
<span
|
|
664
|
+
v-if="o.visibility === 'shared'"
|
|
665
|
+
class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium text-muted-foreground"
|
|
666
|
+
>{{ t("integration.workflows.state.visibility.shared.label") }}</span>
|
|
667
|
+
</span>
|
|
668
|
+
</SelectItem>
|
|
669
|
+
<div v-if="!stateOptions.length" class="px-2 py-1.5 text-sm text-muted-foreground">
|
|
670
|
+
{{ t("integration.nodeConfig.state.empty") }}
|
|
671
|
+
</div>
|
|
672
|
+
<SelectSeparator />
|
|
673
|
+
<SelectItem :value="STATE_ACTION_NEW" class="font-medium text-violet-600 data-[highlighted]:text-violet-700">
|
|
674
|
+
{{ t("integration.nodeConfig.state.action.new") }}
|
|
675
|
+
</SelectItem>
|
|
676
|
+
</SelectContent>
|
|
677
|
+
</Select>
|
|
678
|
+
<IntegrationsStateNamespaceCreateDialog
|
|
679
|
+
v-model:open="stateCreateOpen"
|
|
680
|
+
:role="field.stateRole ?? 'mapping'"
|
|
681
|
+
:taken="stateTakenNames"
|
|
682
|
+
@declared="onStateDeclared"
|
|
683
|
+
/>
|
|
684
|
+
</template>
|
|
685
|
+
|
|
622
686
|
<!-- array of objects (e.g. headers / mappings) -->
|
|
623
687
|
<div v-else-if="field.type === 'array' && field.items?.type === 'object'" class="space-y-2">
|
|
624
688
|
<div
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConfigField, WorkflowVariable } from '../../types/integrations.js';
|
|
1
|
+
import type { ConfigField, WorkflowStateNamespace, WorkflowStateRole, WorkflowStateVisibility, WorkflowVariable } from '../../types/integrations.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
field: ConfigField;
|
|
4
4
|
config: Record<string, unknown>;
|
|
@@ -11,6 +11,8 @@ type __VLS_Props = {
|
|
|
11
11
|
siblingFields?: ConfigField[];
|
|
12
12
|
depth?: number;
|
|
13
13
|
variables?: Record<string, WorkflowVariable>;
|
|
14
|
+
/** The workflow's declared state namespaces — the options a `state-ref` offers. */
|
|
15
|
+
stateNamespaces?: WorkflowStateNamespace[];
|
|
14
16
|
upstreamOutputs?: Array<{
|
|
15
17
|
nodeId: string;
|
|
16
18
|
nodeName?: string;
|
|
@@ -22,7 +24,9 @@ type __VLS_Props = {
|
|
|
22
24
|
};
|
|
23
25
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
24
26
|
set: (key: string, value: unknown) => any;
|
|
27
|
+
"declare-state": (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
|
|
25
28
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
26
29
|
onSet?: ((key: string, value: unknown) => any) | undefined;
|
|
30
|
+
"onDeclare-state"?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
|
|
27
31
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
32
|
export default _default;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { NodeManifestOutput, WorkflowEdge, WorkflowNode, WorkflowTrigger, WorkflowVariable } from '../../types/integrations.js';
|
|
1
|
+
import type { NodeManifestOutput, WorkflowEdge, WorkflowNode, WorkflowStateNamespace, WorkflowStateRole, WorkflowStateVisibility, WorkflowTrigger, WorkflowVariable } from '../../types/integrations.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
/** Either a workflow node OR a trigger. Distinguished by presence of `nodeSlug`. */
|
|
4
4
|
selection: WorkflowNode | WorkflowTrigger | null;
|
|
5
5
|
secretKeys: string[];
|
|
6
6
|
variables?: Record<string, WorkflowVariable>;
|
|
7
|
+
/** The workflow's declared state namespaces — options for `state-ref` fields. */
|
|
8
|
+
stateNamespaces?: WorkflowStateNamespace[];
|
|
7
9
|
upstreamOutputs?: Array<{
|
|
8
10
|
nodeId: string;
|
|
9
11
|
nodeName?: string;
|
|
@@ -31,11 +33,13 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {
|
|
|
31
33
|
update: (next: WorkflowTrigger | WorkflowNode) => any;
|
|
32
34
|
remove: () => any;
|
|
33
35
|
close: () => any;
|
|
36
|
+
"declare-state": (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
|
|
34
37
|
navigate: (id: string) => any;
|
|
35
38
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
36
39
|
onUpdate?: ((next: WorkflowTrigger | WorkflowNode) => any) | undefined;
|
|
37
40
|
onRemove?: (() => any) | undefined;
|
|
38
41
|
onClose?: (() => any) | undefined;
|
|
42
|
+
"onDeclare-state"?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
|
|
39
43
|
onNavigate?: ((id: string) => any) | undefined;
|
|
40
44
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
41
45
|
export default _default;
|
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
ToggleGroupItem
|
|
29
29
|
} from "@revenexx/studio";
|
|
30
30
|
import EventTypeCombobox from "./EventTypeCombobox.vue";
|
|
31
|
-
import { findManifest, nodeCategoryLabel, isVersionExecutable, newestExecutableVersion } from "../../data/integrationNodeCatalog";
|
|
31
|
+
import { findManifest, nodeCategoryLabel, isVersionExecutable, newestExecutableVersion, executableVersionsOf, newerExecutableVersions } from "../../data/integrationNodeCatalog";
|
|
32
|
+
import NodeVersionMoveDialog from "./NodeVersionMoveDialog.vue";
|
|
32
33
|
import {
|
|
33
34
|
collectUpstreamNodes,
|
|
34
35
|
inputPortsForManifest,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
} from "../../data/nodeInputData";
|
|
40
41
|
import { portDotClass } from "../../data/portTone";
|
|
41
42
|
import { computeLoopBodies, manifestIterates } from "../../data/loopBody";
|
|
43
|
+
import { applyingFields, staleKeys } from "../../utils/settingCondition";
|
|
42
44
|
import { deepClone } from "../../utils/deepClone";
|
|
43
45
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
44
46
|
import { nodeDisplayLabel } from "../../utils/validationFindings";
|
|
@@ -57,6 +59,7 @@ const props = defineProps({
|
|
|
57
59
|
selection: { type: [Object, null], required: true },
|
|
58
60
|
secretKeys: { type: Array, required: true },
|
|
59
61
|
variables: { type: Object, required: false },
|
|
62
|
+
stateNamespaces: { type: Array, required: false },
|
|
60
63
|
upstreamOutputs: { type: Array, required: false },
|
|
61
64
|
autofocusName: { type: Boolean, required: false },
|
|
62
65
|
nodes: { type: Array, required: false },
|
|
@@ -66,17 +69,35 @@ const props = defineProps({
|
|
|
66
69
|
workflowId: { type: [Number, String], required: false },
|
|
67
70
|
refusedFields: { type: Object, required: false }
|
|
68
71
|
});
|
|
69
|
-
const emit = defineEmits(["update", "close", "remove", "navigate"]);
|
|
72
|
+
const emit = defineEmits(["update", "close", "remove", "navigate", "declare-state"]);
|
|
70
73
|
const draft = ref(deepClone(props.selection));
|
|
71
74
|
watch(() => props.selection?.id, () => {
|
|
72
|
-
if (props.selection && props.selection.id !== draft.value.id)
|
|
75
|
+
if (props.selection && props.selection.id !== draft.value.id) {
|
|
76
|
+
draft.value = deepClone(props.selection);
|
|
77
|
+
pendingState.value = [];
|
|
78
|
+
}
|
|
73
79
|
});
|
|
74
80
|
const isNode = computed(() => "nodeSlug" in draft.value);
|
|
75
81
|
const draftNode = computed(() => isNode.value ? draft.value : null);
|
|
76
82
|
const draftTrigger = computed(() => isNode.value ? null : draft.value);
|
|
77
|
-
const
|
|
83
|
+
const pendingState = ref([]);
|
|
84
|
+
const offeredStateNamespaces = computed(() => [
|
|
85
|
+
...props.stateNamespaces ?? [],
|
|
86
|
+
...pendingState.value
|
|
87
|
+
]);
|
|
88
|
+
function stageStateNamespace(namespace, role, visibility) {
|
|
89
|
+
if (offeredStateNamespaces.value.some((n) => n.namespace === namespace)) return;
|
|
90
|
+
pendingState.value = [...pendingState.value, { namespace, role, visibility }];
|
|
91
|
+
}
|
|
92
|
+
const draftChanged = computed(() => JSON.stringify(draft.value) !== JSON.stringify(props.selection));
|
|
93
|
+
const dirty = computed(() => draftChanged.value || pendingState.value.length > 0);
|
|
94
|
+
function commit() {
|
|
95
|
+
for (const ns of pendingState.value) emit("declare-state", ns.namespace, ns.role, ns.visibility);
|
|
96
|
+
pendingState.value = [];
|
|
97
|
+
if (draftChanged.value) emit("update", deepClone(draft.value));
|
|
98
|
+
}
|
|
78
99
|
function save() {
|
|
79
|
-
|
|
100
|
+
commit();
|
|
80
101
|
emit("close");
|
|
81
102
|
}
|
|
82
103
|
const nameInput = ref(null);
|
|
@@ -99,6 +120,21 @@ const readyVersion = computed(() => {
|
|
|
99
120
|
const wn = draftNode.value;
|
|
100
121
|
return wn ? newestExecutableVersion(wn.nodeSlug) : null;
|
|
101
122
|
});
|
|
123
|
+
const versionMoveOpen = ref(false);
|
|
124
|
+
const newerVersions = computed(() => {
|
|
125
|
+
const wn = draftNode.value;
|
|
126
|
+
return wn ? newerExecutableVersions(wn.nodeSlug, wn.nodeVersion) : [];
|
|
127
|
+
});
|
|
128
|
+
const canMoveVersion = computed(() => {
|
|
129
|
+
const wn = draftNode.value;
|
|
130
|
+
if (!wn) return false;
|
|
131
|
+
const runnable = executableVersionsOf(wn.nodeSlug);
|
|
132
|
+
if (!runnable.length) return false;
|
|
133
|
+
return runnable.length > 1 || runnable[0]?.version !== wn.nodeVersion;
|
|
134
|
+
});
|
|
135
|
+
function applyVersionMove(payload) {
|
|
136
|
+
patchNode({ nodeVersion: payload.version, config: payload.config });
|
|
137
|
+
}
|
|
102
138
|
const TRIGGER_LABELS = computed(() => ({
|
|
103
139
|
manual: t("integration.nodeInspector.trigger.type.manual"),
|
|
104
140
|
schedule: t("integration.nodeInspector.trigger.type.schedule"),
|
|
@@ -150,8 +186,11 @@ watch(showSettingsTab, (hasThem) => {
|
|
|
150
186
|
if (!hasThem) tab.value = "parameters";
|
|
151
187
|
});
|
|
152
188
|
const SETTINGS_FIELD_KEY = /(^|_)(timeout|retry|retries|backoff)|(timeout|retry|retries|backoff)(ms|sec|seconds|attempts)?$/i;
|
|
153
|
-
const
|
|
154
|
-
|
|
189
|
+
const applyingConfig = computed(
|
|
190
|
+
() => applyingFields(manifest.value?.config ?? [], draftNode.value?.config ?? {})
|
|
191
|
+
);
|
|
192
|
+
const parameterFields = computed(() => applyingConfig.value.filter((f) => !SETTINGS_FIELD_KEY.test(f.key)));
|
|
193
|
+
const settingsFields = computed(() => applyingConfig.value.filter((f) => SETTINGS_FIELD_KEY.test(f.key)));
|
|
155
194
|
const requiredFields = computed(() => parameterFields.value.filter((f) => f.required));
|
|
156
195
|
const optionalFields = computed(() => parameterFields.value.filter((f) => !f.required));
|
|
157
196
|
function optionalShouldOpen() {
|
|
@@ -162,27 +201,14 @@ const optionalOpen = ref(optionalShouldOpen());
|
|
|
162
201
|
watch([() => props.selection?.id, manifest], () => {
|
|
163
202
|
optionalOpen.value = optionalShouldOpen();
|
|
164
203
|
});
|
|
165
|
-
function dependentKeys(changedKey) {
|
|
166
|
-
|
|
167
|
-
const out = /* @__PURE__ */ new Set();
|
|
168
|
-
const queue = [changedKey];
|
|
169
|
-
while (queue.length > 0) {
|
|
170
|
-
const dep = queue.shift();
|
|
171
|
-
for (const f of fields) {
|
|
172
|
-
if ((f.dependsOn ?? []).includes(dep) && !out.has(f.key)) {
|
|
173
|
-
out.add(f.key);
|
|
174
|
-
queue.push(f.key);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
out.delete(changedKey);
|
|
179
|
-
return out;
|
|
204
|
+
function dependentKeys(changedKey, after) {
|
|
205
|
+
return staleKeys(manifest.value?.config ?? [], changedKey, after);
|
|
180
206
|
}
|
|
181
207
|
function writeConfig(key, value) {
|
|
182
208
|
const wn = draftNode.value;
|
|
183
209
|
if (!wn) return;
|
|
184
210
|
const next = { ...wn.config, [key]: value };
|
|
185
|
-
const cleared = dependentKeys(key);
|
|
211
|
+
const cleared = dependentKeys(key, next);
|
|
186
212
|
for (const k of cleared) delete next[k];
|
|
187
213
|
const clearedDynamicSchema = [...cleared].some(
|
|
188
214
|
(k) => (manifest.value?.config ?? []).some((f) => f.key === k && f.type === "dynamic-schema")
|
|
@@ -760,7 +786,7 @@ const hasNext = computed(() => seqIndex.value >= 0 && seqIndex.value < sequence.
|
|
|
760
786
|
function navigate(offset) {
|
|
761
787
|
const target = sequence.value[seqIndex.value + offset];
|
|
762
788
|
if (!target) return;
|
|
763
|
-
|
|
789
|
+
commit();
|
|
764
790
|
emit("navigate", target);
|
|
765
791
|
}
|
|
766
792
|
const outputView = ref("schema");
|
|
@@ -866,7 +892,16 @@ async function runTestStep() {
|
|
|
866
892
|
try {
|
|
867
893
|
const res = await api.post(
|
|
868
894
|
`/nodes/${encodeURIComponent(wn.nodeSlug)}/${encodeURIComponent(wn.nodeVersion)}/execute:test`,
|
|
869
|
-
{
|
|
895
|
+
{
|
|
896
|
+
config: wn.config ?? {},
|
|
897
|
+
inputs,
|
|
898
|
+
// Names the workflow being edited, which is what lets the test run READ
|
|
899
|
+
// the state namespaces this workflow declares (PO-374). Writes stay
|
|
900
|
+
// refused: a correlation created by a test click would be
|
|
901
|
+
// indistinguishable from one a real run made. Omitted for an unsaved
|
|
902
|
+
// workflow, which has nothing declared yet anyway.
|
|
903
|
+
...props.workflowId ? { workflow_id: Number(props.workflowId) } : {}
|
|
904
|
+
}
|
|
870
905
|
);
|
|
871
906
|
testLogs.value = res.logs ?? [];
|
|
872
907
|
testOutputs.value = {
|
|
@@ -959,6 +994,26 @@ async function runTestStep() {
|
|
|
959
994
|
</PopoverContent>
|
|
960
995
|
</Popover>
|
|
961
996
|
</div>
|
|
997
|
+
<!-- The version, once it is something the author can change (PO-339).
|
|
998
|
+
Deliberately a line of its own and deliberately grey: where a newer
|
|
999
|
+
version merely exists, nothing is wrong, and a coloured mark for a
|
|
1000
|
+
non-problem is what teaches people to stop reading marks. It is
|
|
1001
|
+
absent altogether for a node with one runnable version — there is no
|
|
1002
|
+
choice to offer — and for a node whose pinned version cannot run,
|
|
1003
|
+
where the notice below the tabs carries the same route in the louder
|
|
1004
|
+
tone that state has earned. -->
|
|
1005
|
+
<div v-if="isNode && canMoveVersion && versionExecutable" class="flex items-center gap-1 text-xs text-muted-foreground">
|
|
1006
|
+
<span v-if="newerVersions.length" class="tabular-nums">
|
|
1007
|
+
{{ t("integration.nodeInspector.version.available", { version: newerVersions[0]?.version ?? "" }) }}
|
|
1008
|
+
</span>
|
|
1009
|
+
<button
|
|
1010
|
+
type="button"
|
|
1011
|
+
class="rounded-sm underline decoration-dotted underline-offset-2 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
|
1012
|
+
@click="versionMoveOpen = true"
|
|
1013
|
+
>
|
|
1014
|
+
{{ t("integration.nodeInspector.version.change") }}
|
|
1015
|
+
</button>
|
|
1016
|
+
</div>
|
|
962
1017
|
</div>
|
|
963
1018
|
<span class="shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-slate-600">
|
|
964
1019
|
{{ groupLabel }}
|
|
@@ -1298,6 +1353,18 @@ async function runTestStep() {
|
|
|
1298
1353
|
class="mx-4 mb-1 shrink-0 rounded-md border border-rose-200 bg-rose-50 px-2.5 py-1.5 text-sm text-rose-700"
|
|
1299
1354
|
>
|
|
1300
1355
|
{{ readyVersion ? t("integration.nodeInspector.notice.versionNotReady.upgrade", { version: draftNode?.nodeVersion ?? "", ready: readyVersion }) : t("integration.nodeInspector.notice.versionNotReady.none", { version: draftNode?.nodeVersion ?? "" }) }}
|
|
1356
|
+
<!-- The sentence named a version and led nowhere (PO-339). It leads
|
|
1357
|
+
here now; where the node has no runnable version at all there is
|
|
1358
|
+
still nothing to offer, and the notice stays the statement it
|
|
1359
|
+
was. -->
|
|
1360
|
+
<button
|
|
1361
|
+
v-if="readyVersion && canMoveVersion"
|
|
1362
|
+
type="button"
|
|
1363
|
+
class="ml-1 rounded-sm font-medium underline underline-offset-2 hover:text-rose-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-400"
|
|
1364
|
+
@click="versionMoveOpen = true"
|
|
1365
|
+
>
|
|
1366
|
+
{{ t("integration.nodeInspector.version.moveTo", { version: readyVersion }) }}
|
|
1367
|
+
</button>
|
|
1301
1368
|
</p>
|
|
1302
1369
|
|
|
1303
1370
|
<div
|
|
@@ -1750,6 +1817,8 @@ async function runTestStep() {
|
|
|
1750
1817
|
:sibling-fields="manifest.config ?? []"
|
|
1751
1818
|
:depth="0"
|
|
1752
1819
|
:variables="variables"
|
|
1820
|
+
:state-namespaces="offeredStateNamespaces"
|
|
1821
|
+
@declare-state="stageStateNamespace"
|
|
1753
1822
|
:upstream-outputs="upstreamOutputs"
|
|
1754
1823
|
:refused="refusedFields?.[field.key]"
|
|
1755
1824
|
@set="(k, v) => writeConfig(k, v)"
|
|
@@ -1790,6 +1859,8 @@ async function runTestStep() {
|
|
|
1790
1859
|
:sibling-fields="manifest.config ?? []"
|
|
1791
1860
|
:depth="0"
|
|
1792
1861
|
:variables="variables"
|
|
1862
|
+
:state-namespaces="offeredStateNamespaces"
|
|
1863
|
+
@declare-state="stageStateNamespace"
|
|
1793
1864
|
:upstream-outputs="upstreamOutputs"
|
|
1794
1865
|
:refused="refusedFields?.[field.key]"
|
|
1795
1866
|
@set="(k, v) => writeConfig(k, v)"
|
|
@@ -1832,6 +1903,8 @@ async function runTestStep() {
|
|
|
1832
1903
|
:sibling-fields="manifest?.config ?? []"
|
|
1833
1904
|
:depth="0"
|
|
1834
1905
|
:variables="variables"
|
|
1906
|
+
:state-namespaces="offeredStateNamespaces"
|
|
1907
|
+
@declare-state="stageStateNamespace"
|
|
1835
1908
|
:upstream-outputs="upstreamOutputs"
|
|
1836
1909
|
:refused="refusedFields?.[field.key]"
|
|
1837
1910
|
@set="(k, v) => writeConfig(k, v)"
|
|
@@ -2121,6 +2194,11 @@ async function runTestStep() {
|
|
|
2121
2194
|
</Button>
|
|
2122
2195
|
</div>
|
|
2123
2196
|
</div>
|
|
2197
|
+
|
|
2198
|
+
<!-- Asked outside the panes so it is not clipped by either, and mounted
|
|
2199
|
+
unconditionally: the two places that open it sit in different parts of
|
|
2200
|
+
the dialog and neither owns it. -->
|
|
2201
|
+
<NodeVersionMoveDialog v-model:open="versionMoveOpen" :node="draftNode" @move="applyVersionMove" />
|
|
2124
2202
|
</div>
|
|
2125
2203
|
</template>
|
|
2126
2204
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { NodeManifestOutput, WorkflowEdge, WorkflowNode, WorkflowTrigger, WorkflowVariable } from '../../types/integrations.js';
|
|
1
|
+
import type { NodeManifestOutput, WorkflowEdge, WorkflowNode, WorkflowStateNamespace, WorkflowStateRole, WorkflowStateVisibility, WorkflowTrigger, WorkflowVariable } from '../../types/integrations.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
/** Either a workflow node OR a trigger. Distinguished by presence of `nodeSlug`. */
|
|
4
4
|
selection: WorkflowNode | WorkflowTrigger | null;
|
|
5
5
|
secretKeys: string[];
|
|
6
6
|
variables?: Record<string, WorkflowVariable>;
|
|
7
|
+
/** The workflow's declared state namespaces — options for `state-ref` fields. */
|
|
8
|
+
stateNamespaces?: WorkflowStateNamespace[];
|
|
7
9
|
upstreamOutputs?: Array<{
|
|
8
10
|
nodeId: string;
|
|
9
11
|
nodeName?: string;
|
|
@@ -31,11 +33,13 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {
|
|
|
31
33
|
update: (next: WorkflowTrigger | WorkflowNode) => any;
|
|
32
34
|
remove: () => any;
|
|
33
35
|
close: () => any;
|
|
36
|
+
"declare-state": (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
|
|
34
37
|
navigate: (id: string) => any;
|
|
35
38
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
36
39
|
onUpdate?: ((next: WorkflowTrigger | WorkflowNode) => any) | undefined;
|
|
37
40
|
onRemove?: (() => any) | undefined;
|
|
38
41
|
onClose?: (() => any) | undefined;
|
|
42
|
+
"onDeclare-state"?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
|
|
39
43
|
onNavigate?: ((id: string) => any) | undefined;
|
|
40
44
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
41
45
|
export default _default;
|