@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WorkflowNode } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
node: WorkflowNode | null;
|
|
4
|
+
};
|
|
5
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
6
|
+
'open'?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:open": (value: boolean) => any;
|
|
10
|
+
move: (payload: {
|
|
11
|
+
version: string;
|
|
12
|
+
config: Record<string, unknown>;
|
|
13
|
+
}) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
onMove?: ((payload: {
|
|
17
|
+
version: string;
|
|
18
|
+
config: Record<string, unknown>;
|
|
19
|
+
}) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
AlertDialog,
|
|
5
|
+
AlertDialogContent,
|
|
6
|
+
AlertDialogDescription,
|
|
7
|
+
AlertDialogFooter,
|
|
8
|
+
AlertDialogHeader,
|
|
9
|
+
AlertDialogTitle,
|
|
10
|
+
Button
|
|
11
|
+
} from "@revenexx/studio";
|
|
12
|
+
import { executableVersionsOf, findManifest, newestExecutableVersion } from "../../data/integrationNodeCatalog";
|
|
13
|
+
import { planVersionMove } from "../../data/nodeVersionMove";
|
|
14
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
15
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
node: { type: [Object, null], required: true }
|
|
18
|
+
});
|
|
19
|
+
const emit = defineEmits(["move"]);
|
|
20
|
+
const t = useIntegrationsT();
|
|
21
|
+
const versions = computed(() => props.node ? executableVersionsOf(props.node.nodeSlug) : []);
|
|
22
|
+
const target = ref("");
|
|
23
|
+
watch(open, (isOpen) => {
|
|
24
|
+
if (!isOpen || !props.node) return;
|
|
25
|
+
const newest = newestExecutableVersion(props.node.nodeSlug);
|
|
26
|
+
target.value = newest && newest !== props.node.nodeVersion ? newest : props.node.nodeVersion;
|
|
27
|
+
});
|
|
28
|
+
const plan = computed(() => {
|
|
29
|
+
const n = props.node;
|
|
30
|
+
if (!n) return null;
|
|
31
|
+
return planVersionMove(
|
|
32
|
+
findManifestExact(n.nodeSlug, n.nodeVersion),
|
|
33
|
+
findManifestExact(n.nodeSlug, target.value),
|
|
34
|
+
n.config
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
function findManifestExact(slug, version) {
|
|
38
|
+
const m = findManifest(slug, version);
|
|
39
|
+
return m && m.version === version ? m : null;
|
|
40
|
+
}
|
|
41
|
+
const unchanged = computed(() => !props.node || target.value === props.node.nodeVersion);
|
|
42
|
+
function labelOf(entry) {
|
|
43
|
+
return resolveLocalized(entry.field?.label) || entry.key;
|
|
44
|
+
}
|
|
45
|
+
function confirm() {
|
|
46
|
+
const p = plan.value;
|
|
47
|
+
if (!props.node || !p || unchanged.value) return;
|
|
48
|
+
emit("move", { version: target.value, config: p.nextConfig });
|
|
49
|
+
open.value = false;
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<AlertDialog :open="open" @update:open="(v) => {
|
|
55
|
+
if (!v) open = false;
|
|
56
|
+
}">
|
|
57
|
+
<AlertDialogContent>
|
|
58
|
+
<AlertDialogHeader>
|
|
59
|
+
<AlertDialogTitle>{{ t("integration.nodeInspector.version.move.title") }}</AlertDialogTitle>
|
|
60
|
+
<AlertDialogDescription>
|
|
61
|
+
{{ t("integration.nodeInspector.version.move.description") }}
|
|
62
|
+
</AlertDialogDescription>
|
|
63
|
+
</AlertDialogHeader>
|
|
64
|
+
|
|
65
|
+
<div v-if="props.node && plan" class="space-y-3 text-sm">
|
|
66
|
+
<div class="flex items-center gap-2">
|
|
67
|
+
<span class="tabular-nums text-muted-foreground">v{{ props.node.nodeVersion }}</span>
|
|
68
|
+
<span aria-hidden="true">→</span>
|
|
69
|
+
<select
|
|
70
|
+
v-model="target"
|
|
71
|
+
class="rounded border border-black/10 bg-white px-1.5 py-0.5 tabular-nums"
|
|
72
|
+
:aria-label="t('integration.nodeInspector.version.move.targetAria')"
|
|
73
|
+
>
|
|
74
|
+
<option v-for="m in versions" :key="m.version" :value="m.version">
|
|
75
|
+
v{{ m.version }}
|
|
76
|
+
</option>
|
|
77
|
+
</select>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- A version the catalogue no longer carries cannot be compared against,
|
|
81
|
+
so the lists below are what arrives rather than what was checked. Said
|
|
82
|
+
once, here, instead of marking every setting as suspect. -->
|
|
83
|
+
<p v-if="!plan.comparable" class="text-amber-600">
|
|
84
|
+
{{ t("integration.nodeInspector.version.move.notComparable", { version: props.node.nodeVersion }) }}
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<p v-if="unchanged" class="text-muted-foreground">
|
|
88
|
+
{{ t("integration.nodeInspector.version.move.same") }}
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<template v-else>
|
|
92
|
+
<p v-if="plan.lossless" class="text-muted-foreground">
|
|
93
|
+
{{ t("integration.nodeInspector.version.move.lossless") }}
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
<div v-if="plan.dropped.length">
|
|
97
|
+
<p class="font-medium">{{ t("integration.nodeInspector.version.move.dropped") }}</p>
|
|
98
|
+
<ul class="mt-0.5 list-disc pl-5 text-muted-foreground">
|
|
99
|
+
<li v-for="e in plan.dropped" :key="e.key">
|
|
100
|
+
{{ labelOf(e) }}
|
|
101
|
+
<!-- The key, where it is not already the whole label. A setting no
|
|
102
|
+
version declares any more has nothing but its key to go by, and
|
|
103
|
+
printing that twice reads as a bug rather than as precision. -->
|
|
104
|
+
<span v-if="labelOf(e) !== e.key" class="text-xs">({{ e.key }})</span>
|
|
105
|
+
</li>
|
|
106
|
+
</ul>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div v-if="plan.review.length">
|
|
110
|
+
<p class="font-medium">{{ t("integration.nodeInspector.version.move.review") }}</p>
|
|
111
|
+
<ul class="mt-0.5 list-disc pl-5 text-muted-foreground">
|
|
112
|
+
<li v-for="e in plan.review" :key="e.key">
|
|
113
|
+
{{ labelOf(e) }}
|
|
114
|
+
<span class="text-xs">
|
|
115
|
+
({{ e.reason === "nowRequired" ? t("integration.nodeInspector.version.move.reason.nowRequired") : t("integration.nodeInspector.version.move.reason.changed") }})
|
|
116
|
+
</span>
|
|
117
|
+
</li>
|
|
118
|
+
</ul>
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<AlertDialogFooter>
|
|
124
|
+
<Button variant="outline" @click="open = false">
|
|
125
|
+
{{ t("integration.common.action.cancel") }}
|
|
126
|
+
</Button>
|
|
127
|
+
<Button :disabled="unchanged" @click="confirm">
|
|
128
|
+
{{ t("integration.nodeInspector.version.move.confirm") }}
|
|
129
|
+
</Button>
|
|
130
|
+
</AlertDialogFooter>
|
|
131
|
+
</AlertDialogContent>
|
|
132
|
+
</AlertDialog>
|
|
133
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WorkflowNode } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
node: WorkflowNode | null;
|
|
4
|
+
};
|
|
5
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
6
|
+
'open'?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:open": (value: boolean) => any;
|
|
10
|
+
move: (payload: {
|
|
11
|
+
version: string;
|
|
12
|
+
config: Record<string, unknown>;
|
|
13
|
+
}) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
onMove?: ((payload: {
|
|
17
|
+
version: string;
|
|
18
|
+
config: Record<string, unknown>;
|
|
19
|
+
}) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkflowStateRole, WorkflowStateVisibility } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
/** Fixed by the `state-ref` field that opened this. */
|
|
5
|
+
role: WorkflowStateRole;
|
|
6
|
+
/** Names the workflow already declares, so a collision is caught while typing. */
|
|
7
|
+
taken: string[];
|
|
8
|
+
};
|
|
9
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
10
|
+
"update:open": (args_0: boolean) => any;
|
|
11
|
+
declared: (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
14
|
+
onDeclared?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
Input,
|
|
12
|
+
Label,
|
|
13
|
+
Select,
|
|
14
|
+
SelectContent,
|
|
15
|
+
SelectItem,
|
|
16
|
+
SelectTrigger,
|
|
17
|
+
SelectValue
|
|
18
|
+
} from "@revenexx/studio";
|
|
19
|
+
import { stateNamespaceErrorKey } from "../../utils/stateNamespaceName";
|
|
20
|
+
const props = defineProps({
|
|
21
|
+
open: { type: Boolean, required: true },
|
|
22
|
+
role: { type: String, required: true },
|
|
23
|
+
taken: { type: Array, required: true }
|
|
24
|
+
});
|
|
25
|
+
const emit = defineEmits(["update:open", "declared"]);
|
|
26
|
+
const t = useIntegrationsT();
|
|
27
|
+
const name = ref("");
|
|
28
|
+
const visibility = ref("private");
|
|
29
|
+
watch(() => props.open, (isOpen) => {
|
|
30
|
+
if (!isOpen) return;
|
|
31
|
+
name.value = "";
|
|
32
|
+
visibility.value = "private";
|
|
33
|
+
});
|
|
34
|
+
const error = computed(() => {
|
|
35
|
+
const key = stateNamespaceErrorKey(name.value, props.taken);
|
|
36
|
+
return key ? t(key) : null;
|
|
37
|
+
});
|
|
38
|
+
function declare() {
|
|
39
|
+
const value = name.value.trim();
|
|
40
|
+
if (!value || error.value) return;
|
|
41
|
+
emit("declared", value, props.role, visibility.value);
|
|
42
|
+
emit("update:open", false);
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
48
|
+
<DialogContent class="max-w-[94vw] sm:max-w-lg">
|
|
49
|
+
<DialogHeader>
|
|
50
|
+
<DialogTitle>{{ t("integration.nodeConfig.state.create.title") }}</DialogTitle>
|
|
51
|
+
<DialogDescription class="text-sm">
|
|
52
|
+
{{ t("integration.nodeConfig.state.create.description", { role: t(`integration.workflows.state.role.${role}.label`) }) }}
|
|
53
|
+
</DialogDescription>
|
|
54
|
+
</DialogHeader>
|
|
55
|
+
|
|
56
|
+
<div class="space-y-3">
|
|
57
|
+
<div class="space-y-1">
|
|
58
|
+
<Label for="state-create-name" class="text-mono text-muted-foreground">
|
|
59
|
+
{{ t("integration.workflows.state.field.newName.label") }}
|
|
60
|
+
</Label>
|
|
61
|
+
<Input
|
|
62
|
+
id="state-create-name"
|
|
63
|
+
v-model="name"
|
|
64
|
+
class="h-8 bg-white font-mono text-sm"
|
|
65
|
+
:placeholder="t('integration.workflows.state.field.newName.placeholder')"
|
|
66
|
+
@keydown.enter.prevent="declare"
|
|
67
|
+
/>
|
|
68
|
+
<p v-if="error" class="text-xs text-rose-600">{{ error }}</p>
|
|
69
|
+
<p class="text-xs text-muted-foreground">{{ t(`integration.workflows.state.role.${role}.hint`) }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="space-y-1">
|
|
73
|
+
<Label for="state-create-visibility" class="text-mono text-muted-foreground">
|
|
74
|
+
{{ t("integration.workflows.state.field.visibility.label") }}
|
|
75
|
+
</Label>
|
|
76
|
+
<Select v-model="visibility">
|
|
77
|
+
<SelectTrigger
|
|
78
|
+
id="state-create-visibility"
|
|
79
|
+
:aria-label="t('integration.workflows.state.field.visibility.label')"
|
|
80
|
+
class="h-8 bg-white text-sm"
|
|
81
|
+
>
|
|
82
|
+
<SelectValue />
|
|
83
|
+
</SelectTrigger>
|
|
84
|
+
<SelectContent>
|
|
85
|
+
<SelectItem value="private">
|
|
86
|
+
{{ t("integration.workflows.state.visibility.private.label") }}
|
|
87
|
+
</SelectItem>
|
|
88
|
+
<SelectItem value="shared">
|
|
89
|
+
{{ t("integration.workflows.state.visibility.shared.label") }}
|
|
90
|
+
</SelectItem>
|
|
91
|
+
</SelectContent>
|
|
92
|
+
</Select>
|
|
93
|
+
<!-- The same consequence the workflow-level dialog states, in the same
|
|
94
|
+
place relative to the control that chooses it. -->
|
|
95
|
+
<p class="text-xs text-muted-foreground">
|
|
96
|
+
{{ t(`integration.workflows.state.visibility.${visibility}.hint`) }}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<DialogFooter>
|
|
102
|
+
<Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
|
|
103
|
+
{{ t("integration.common.action.cancel") }}
|
|
104
|
+
</Button>
|
|
105
|
+
<Button size="sm" class="h-8" :disabled="!name.trim() || !!error" @click="declare">
|
|
106
|
+
{{ t("integration.workflows.state.action.add") }}
|
|
107
|
+
</Button>
|
|
108
|
+
</DialogFooter>
|
|
109
|
+
</DialogContent>
|
|
110
|
+
</Dialog>
|
|
111
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkflowStateRole, WorkflowStateVisibility } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
/** Fixed by the `state-ref` field that opened this. */
|
|
5
|
+
role: WorkflowStateRole;
|
|
6
|
+
/** Names the workflow already declares, so a collision is caught while typing. */
|
|
7
|
+
taken: string[];
|
|
8
|
+
};
|
|
9
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
10
|
+
"update:open": (args_0: boolean) => any;
|
|
11
|
+
declared: (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
14
|
+
onDeclared?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
export default _default;
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
DropdownMenuTrigger,
|
|
29
29
|
TooltipProvider
|
|
30
30
|
} from "@revenexx/studio";
|
|
31
|
-
import { Braces, EllipsisVertical, GripVertical, Group as GroupIcon, Hand, Map as MapIcon, Minus, MousePointer2, Plus, Settings2, StickyNote } from "lucide-vue-next";
|
|
31
|
+
import { Braces, Database, EllipsisVertical, GripVertical, Group as GroupIcon, Hand, Map as MapIcon, Minus, MousePointer2, Plus, Settings2, StickyNote } from "lucide-vue-next";
|
|
32
32
|
import "@vue-flow/core/dist/style.css";
|
|
33
33
|
import "@vue-flow/core/dist/theme-default.css";
|
|
34
34
|
import "@vue-flow/minimap/dist/style.css";
|
|
@@ -40,6 +40,7 @@ import WorkflowTriggerView from "./WorkflowTriggerView.vue";
|
|
|
40
40
|
import StickyNoteView from "./StickyNoteView.vue";
|
|
41
41
|
import GroupFrameView from "./GroupFrameView.vue";
|
|
42
42
|
import WorkflowContextMenu, {} from "./WorkflowContextMenu.vue";
|
|
43
|
+
import WorkflowStateNamespacesDialog from "./WorkflowStateNamespacesDialog.vue";
|
|
43
44
|
import WorkflowVariablesDialog from "./WorkflowVariablesDialog.vue";
|
|
44
45
|
import { useCanvasShortcuts } from "../../composables/useCanvasShortcuts";
|
|
45
46
|
import { usePaletteLayout } from "../../composables/usePaletteLayout";
|
|
@@ -570,6 +571,18 @@ function updateSticky(id, patch) {
|
|
|
570
571
|
push();
|
|
571
572
|
}
|
|
572
573
|
const variablesOpen = ref(false);
|
|
574
|
+
const stateOpen = ref(false);
|
|
575
|
+
const stateCount = computed(() => (blob.value.state ?? []).length);
|
|
576
|
+
function updateStateNamespaces(next) {
|
|
577
|
+
blob.value.state = next.length ? next : void 0;
|
|
578
|
+
push();
|
|
579
|
+
}
|
|
580
|
+
function declareStateNamespace(namespace, role, visibility) {
|
|
581
|
+
const declared = blob.value.state ?? [];
|
|
582
|
+
if (declared.some((n) => n.namespace === namespace)) return;
|
|
583
|
+
blob.value.state = [...declared, { namespace, role, visibility }];
|
|
584
|
+
push();
|
|
585
|
+
}
|
|
573
586
|
const variableCount = computed(() => Object.keys(blob.value.variables ?? {}).length);
|
|
574
587
|
function updateVariables(next) {
|
|
575
588
|
blob.value.variables = Object.keys(next).length ? next : void 0;
|
|
@@ -1290,7 +1303,7 @@ function onConfigOpenAutoFocus(event) {
|
|
|
1290
1303
|
event.preventDefault();
|
|
1291
1304
|
event.target?.focus();
|
|
1292
1305
|
}
|
|
1293
|
-
const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
|
|
1306
|
+
const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !stateOpen.value && !props.overlayOpen);
|
|
1294
1307
|
const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
|
|
1295
1308
|
useCanvasShortcuts(() => ({
|
|
1296
1309
|
"enter": {
|
|
@@ -1335,6 +1348,9 @@ useCanvasShortcuts(() => ({
|
|
|
1335
1348
|
"shift+v": () => {
|
|
1336
1349
|
variablesOpen.value = true;
|
|
1337
1350
|
},
|
|
1351
|
+
"shift+t": () => {
|
|
1352
|
+
stateOpen.value = true;
|
|
1353
|
+
},
|
|
1338
1354
|
"shift+alt+s": () => emit("openSettings"),
|
|
1339
1355
|
"-": () => {
|
|
1340
1356
|
void flow.zoomOut({ duration: 150 });
|
|
@@ -1739,6 +1755,17 @@ const edgeTypes = {
|
|
|
1739
1755
|
<span v-if="variableCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ variableCount }}</span>
|
|
1740
1756
|
</button>
|
|
1741
1757
|
</IntegrationsEditorTip>
|
|
1758
|
+
<IntegrationsEditorTip :label="t('integration.editor.tool.state.tooltip')" shortcut="shift+t">
|
|
1759
|
+
<button
|
|
1760
|
+
type="button"
|
|
1761
|
+
class="flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground transition hover:bg-black/[0.04]"
|
|
1762
|
+
@click="stateOpen = true"
|
|
1763
|
+
>
|
|
1764
|
+
<Database :size="13" />
|
|
1765
|
+
{{ t("integration.editor.tool.state.label") }}
|
|
1766
|
+
<span v-if="stateCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ stateCount }}</span>
|
|
1767
|
+
</button>
|
|
1768
|
+
</IntegrationsEditorTip>
|
|
1742
1769
|
<IntegrationsEditorTip :label="t('integration.editor.tool.settings.tooltip')" shortcut="shift+alt+s">
|
|
1743
1770
|
<button
|
|
1744
1771
|
type="button"
|
|
@@ -1838,6 +1865,14 @@ const edgeTypes = {
|
|
|
1838
1865
|
</div>
|
|
1839
1866
|
</div>
|
|
1840
1867
|
|
|
1868
|
+
<!-- State namespaces lightbox (control-pane entry) — what this workflow may
|
|
1869
|
+
remember between runs. -->
|
|
1870
|
+
<WorkflowStateNamespacesDialog
|
|
1871
|
+
v-model:open="stateOpen"
|
|
1872
|
+
:state="blob.state ?? []"
|
|
1873
|
+
@update:state="updateStateNamespaces"
|
|
1874
|
+
/>
|
|
1875
|
+
|
|
1841
1876
|
<!-- Workflow variables lightbox (control-pane entry). -->
|
|
1842
1877
|
<WorkflowVariablesDialog
|
|
1843
1878
|
v-model:open="variablesOpen"
|
|
@@ -1876,6 +1911,7 @@ const edgeTypes = {
|
|
|
1876
1911
|
:selection="selection"
|
|
1877
1912
|
:secret-keys="secretKeys"
|
|
1878
1913
|
:variables="blob.variables"
|
|
1914
|
+
:state-namespaces="blob.state ?? []"
|
|
1879
1915
|
:upstream-outputs="selectedUpstreamOutputs"
|
|
1880
1916
|
:autofocus-name="renameIntent"
|
|
1881
1917
|
:nodes="blob.nodes"
|
|
@@ -1891,6 +1927,7 @@ const edgeTypes = {
|
|
|
1891
1927
|
selectedId = id;
|
|
1892
1928
|
renameIntent = false;
|
|
1893
1929
|
}"
|
|
1930
|
+
@declare-state="declareStateNamespace"
|
|
1894
1931
|
/>
|
|
1895
1932
|
</DialogContent>
|
|
1896
1933
|
</Dialog>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorkflowStateNamespace } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
state: WorkflowStateNamespace[];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
"update:open": (args_0: boolean) => any;
|
|
8
|
+
"update:state": (next: WorkflowStateNamespace[]) => any;
|
|
9
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
11
|
+
"onUpdate:state"?: ((next: WorkflowStateNamespace[]) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { AddCircle as Plus, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
Input,
|
|
13
|
+
Label,
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue
|
|
19
|
+
} from "@revenexx/studio";
|
|
20
|
+
import { deepClone } from "../../utils/deepClone";
|
|
21
|
+
import { stateNamespaceErrorKey } from "../../utils/stateNamespaceName";
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
open: { type: Boolean, required: true },
|
|
24
|
+
state: { type: Array, required: true }
|
|
25
|
+
});
|
|
26
|
+
const emit = defineEmits(["update:open", "update:state"]);
|
|
27
|
+
const t = useIntegrationsT();
|
|
28
|
+
const ROLES = ["mapping", "cursor", "dedupe", "digest"];
|
|
29
|
+
const working = ref([]);
|
|
30
|
+
watch(() => props.open, (isOpen) => {
|
|
31
|
+
if (!isOpen) return;
|
|
32
|
+
working.value = deepClone(props.state ?? []);
|
|
33
|
+
newName.value = "";
|
|
34
|
+
newRole.value = "mapping";
|
|
35
|
+
newVisibility.value = "private";
|
|
36
|
+
});
|
|
37
|
+
const changed = computed(() => JSON.stringify(working.value) !== JSON.stringify(props.state ?? []));
|
|
38
|
+
function applyAndClose() {
|
|
39
|
+
if (changed.value) emit("update:state", working.value);
|
|
40
|
+
emit("update:open", false);
|
|
41
|
+
}
|
|
42
|
+
function setVisibility(index, visibility) {
|
|
43
|
+
const next = [...working.value];
|
|
44
|
+
const row = next[index];
|
|
45
|
+
if (!row) return;
|
|
46
|
+
next[index] = { ...row, visibility };
|
|
47
|
+
working.value = next;
|
|
48
|
+
}
|
|
49
|
+
function removeNamespace(index) {
|
|
50
|
+
working.value = working.value.filter((_, i) => i !== index);
|
|
51
|
+
}
|
|
52
|
+
const newName = ref("");
|
|
53
|
+
const newRole = ref("mapping");
|
|
54
|
+
const newVisibility = ref("private");
|
|
55
|
+
const newNameError = computed(() => {
|
|
56
|
+
const key = stateNamespaceErrorKey(newName.value, working.value.map((n) => n.namespace));
|
|
57
|
+
return key ? t(key) : null;
|
|
58
|
+
});
|
|
59
|
+
function addNamespace() {
|
|
60
|
+
const namespace = newName.value.trim();
|
|
61
|
+
if (!namespace || newNameError.value) return;
|
|
62
|
+
working.value = [...working.value, { namespace, role: newRole.value, visibility: newVisibility.value }];
|
|
63
|
+
newName.value = "";
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
69
|
+
<DialogContent class="max-w-[94vw] sm:max-w-3xl">
|
|
70
|
+
<DialogHeader>
|
|
71
|
+
<DialogTitle>{{ t("integration.workflows.state.dialog.title") }}</DialogTitle>
|
|
72
|
+
<DialogDescription class="text-sm">
|
|
73
|
+
{{ t("integration.workflows.state.dialog.description") }}
|
|
74
|
+
</DialogDescription>
|
|
75
|
+
</DialogHeader>
|
|
76
|
+
|
|
77
|
+
<div class="max-h-[62vh] space-y-3 overflow-y-auto pr-1">
|
|
78
|
+
<p v-if="!working.length" class="rounded-lg border border-dashed border-black/[0.12] px-4 py-6 text-center text-sm text-muted-foreground">
|
|
79
|
+
{{ t("integration.workflows.state.empty") }}
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
<div
|
|
83
|
+
v-for="(row, index) in working"
|
|
84
|
+
:key="row.namespace"
|
|
85
|
+
role="group"
|
|
86
|
+
:aria-label="t('integration.workflows.state.row.aria', { namespace: row.namespace })"
|
|
87
|
+
class="rounded-lg border border-black/[0.06] bg-white p-3"
|
|
88
|
+
>
|
|
89
|
+
<div class="flex items-center gap-2">
|
|
90
|
+
<code class="min-w-0 truncate font-mono text-sm font-semibold">{{ row.namespace }}</code>
|
|
91
|
+
<!-- The role is stated, not offered: entries written under one
|
|
92
|
+
meaning cannot be re-read under another (specs AC-3). -->
|
|
93
|
+
<span class="shrink-0 rounded-lg bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
|
94
|
+
{{ t(`integration.workflows.state.role.${row.role}.label`) }}
|
|
95
|
+
</span>
|
|
96
|
+
<div class="flex-1" />
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
|
|
100
|
+
:title="t('integration.workflows.state.action.remove')"
|
|
101
|
+
@click="removeNamespace(index)"
|
|
102
|
+
>
|
|
103
|
+
<TrashBin :size="13" />
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<p class="mt-1 text-xs text-muted-foreground">
|
|
108
|
+
{{ t(`integration.workflows.state.role.${row.role}.hint`) }}
|
|
109
|
+
{{ t("integration.workflows.state.role.fixed") }}
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
<div class="mt-2 grid gap-2 sm:grid-cols-[10rem_1fr]">
|
|
113
|
+
<div class="space-y-1">
|
|
114
|
+
<Label :for="`state-${row.namespace}-visibility`" class="text-mono text-muted-foreground">
|
|
115
|
+
{{ t("integration.workflows.state.field.visibility.label") }}
|
|
116
|
+
</Label>
|
|
117
|
+
<Select
|
|
118
|
+
:model-value="row.visibility ?? 'private'"
|
|
119
|
+
@update:model-value="(v) => setVisibility(index, String(v))"
|
|
120
|
+
>
|
|
121
|
+
<SelectTrigger
|
|
122
|
+
:id="`state-${row.namespace}-visibility`"
|
|
123
|
+
:aria-label="t('integration.workflows.state.field.visibility.label')"
|
|
124
|
+
class="h-8 bg-white text-sm"
|
|
125
|
+
>
|
|
126
|
+
<SelectValue />
|
|
127
|
+
</SelectTrigger>
|
|
128
|
+
<SelectContent>
|
|
129
|
+
<SelectItem value="private">
|
|
130
|
+
{{ t("integration.workflows.state.visibility.private.label") }}
|
|
131
|
+
</SelectItem>
|
|
132
|
+
<SelectItem value="shared">
|
|
133
|
+
{{ t("integration.workflows.state.visibility.shared.label") }}
|
|
134
|
+
</SelectItem>
|
|
135
|
+
</SelectContent>
|
|
136
|
+
</Select>
|
|
137
|
+
</div>
|
|
138
|
+
<!-- What the choice costs, beside the choice — a shared namespace is
|
|
139
|
+
written by every workflow in the tenant and outlives this one
|
|
140
|
+
(specs AC-4). -->
|
|
141
|
+
<p class="self-end pb-1 text-xs text-muted-foreground">
|
|
142
|
+
{{ t(`integration.workflows.state.visibility.${row.visibility ?? "private"}.hint`) }}
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<p class="mt-2 text-xs text-muted-foreground">
|
|
147
|
+
{{ t("integration.workflows.state.action.remove.hint") }}
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="rounded-lg border border-black/[0.06] bg-muted/30 p-3">
|
|
152
|
+
<Label for="state-new-name" class="text-mono text-muted-foreground">
|
|
153
|
+
{{ t("integration.workflows.state.field.newName.label") }}
|
|
154
|
+
</Label>
|
|
155
|
+
<div class="mt-1 grid gap-2 sm:grid-cols-[1fr_10rem_10rem_auto]">
|
|
156
|
+
<Input
|
|
157
|
+
id="state-new-name"
|
|
158
|
+
v-model="newName"
|
|
159
|
+
class="h-8 bg-white font-mono text-sm"
|
|
160
|
+
:placeholder="t('integration.workflows.state.field.newName.placeholder')"
|
|
161
|
+
@keydown.enter.prevent="addNamespace"
|
|
162
|
+
/>
|
|
163
|
+
<!-- The role IS chosen here, and only here — the positive half of
|
|
164
|
+
AC-3, without which "no role control on a row" proves nothing. -->
|
|
165
|
+
<Select v-model="newRole">
|
|
166
|
+
<SelectTrigger
|
|
167
|
+
id="state-new-role"
|
|
168
|
+
:aria-label="t('integration.workflows.state.field.newRole.label')"
|
|
169
|
+
class="h-8 bg-white text-sm"
|
|
170
|
+
>
|
|
171
|
+
<SelectValue />
|
|
172
|
+
</SelectTrigger>
|
|
173
|
+
<SelectContent>
|
|
174
|
+
<SelectItem v-for="role in ROLES" :key="role" :value="role">
|
|
175
|
+
{{ t(`integration.workflows.state.role.${role}.label`) }}
|
|
176
|
+
</SelectItem>
|
|
177
|
+
</SelectContent>
|
|
178
|
+
</Select>
|
|
179
|
+
<Select v-model="newVisibility">
|
|
180
|
+
<SelectTrigger
|
|
181
|
+
id="state-new-visibility"
|
|
182
|
+
:aria-label="t('integration.workflows.state.field.newVisibility.label')"
|
|
183
|
+
class="h-8 bg-white text-sm"
|
|
184
|
+
>
|
|
185
|
+
<SelectValue />
|
|
186
|
+
</SelectTrigger>
|
|
187
|
+
<SelectContent>
|
|
188
|
+
<SelectItem value="private">
|
|
189
|
+
{{ t("integration.workflows.state.visibility.private.label") }}
|
|
190
|
+
</SelectItem>
|
|
191
|
+
<SelectItem value="shared">
|
|
192
|
+
{{ t("integration.workflows.state.visibility.shared.label") }}
|
|
193
|
+
</SelectItem>
|
|
194
|
+
</SelectContent>
|
|
195
|
+
</Select>
|
|
196
|
+
<Button size="sm" class="h-8" :disabled="!newName.trim() || !!newNameError" @click="addNamespace">
|
|
197
|
+
<Plus :size="14" class="mr-1" />
|
|
198
|
+
{{ t("integration.workflows.state.action.add") }}
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
<p v-if="newNameError" class="mt-1 text-xs text-rose-600">{{ newNameError }}</p>
|
|
202
|
+
<p class="mt-1 text-xs text-muted-foreground">{{ t(`integration.workflows.state.role.${newRole}.hint`) }}</p>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<DialogFooter>
|
|
207
|
+
<Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
|
|
208
|
+
{{ t("integration.common.action.cancel") }}
|
|
209
|
+
</Button>
|
|
210
|
+
<Button size="sm" class="h-8" @click="applyAndClose">
|
|
211
|
+
{{ t("integration.workflows.state.action.done") }}
|
|
212
|
+
</Button>
|
|
213
|
+
</DialogFooter>
|
|
214
|
+
</DialogContent>
|
|
215
|
+
</Dialog>
|
|
216
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorkflowStateNamespace } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
state: WorkflowStateNamespace[];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
"update:open": (args_0: boolean) => any;
|
|
8
|
+
"update:state": (next: WorkflowStateNamespace[]) => any;
|
|
9
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
11
|
+
"onUpdate:state"?: ((next: WorkflowStateNamespace[]) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
export default _default;
|