@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  4. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
  5. package/dist/runtime/components/Integrations/NodeConfigField.vue +65 -1
  6. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
  7. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
  8. package/dist/runtime/components/Integrations/NodeInspector.vue +103 -25
  9. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
  10. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  11. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  12. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  13. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  14. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  15. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  16. package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -2
  17. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  18. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  19. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  20. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -0
  21. package/dist/runtime/composables/useIntegrationsWorkflows.js +6 -0
  22. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  23. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  24. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  25. package/dist/runtime/data/nodeVersionMove.js +65 -0
  26. package/dist/runtime/i18n/messages/de.json +53 -4
  27. package/dist/runtime/i18n/messages/en.json +53 -4
  28. package/dist/runtime/pages/integrations/workflows/[id].vue +30 -20
  29. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  30. package/dist/runtime/types/integrations.d.ts +44 -1
  31. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  32. package/dist/runtime/utils/buildSettle.js +29 -0
  33. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  34. package/dist/runtime/utils/settingCondition.js +84 -0
  35. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  36. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  37. 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;