@arronqzy/vue-blueprint 0.1.12 → 0.1.14
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/package.json +6 -6
- package/src/BlueprintNodeConfigSidebar.vue +3 -0
- package/src/components/JsonNodeConfigPanel.vue +60 -10
- package/src/components/StorageKeyAutocomplete.vue +157 -0
- package/src/components/StorageNodeConfigPanel.vue +13 -10
- package/src/composables/useBlueprintPageLifecycle.ts +12 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/vue-blueprint",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Vue 3 blueprint editor for Abuilder (Vue Flow + Ant Design Vue)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"ant-design-vue": "^4.2.6",
|
|
20
20
|
"dayjs": "^1.11.13",
|
|
21
21
|
"vue": "^3.5.13",
|
|
22
|
-
"@arronqzy/
|
|
23
|
-
"@arronqzy/
|
|
24
|
-
"@arronqzy/
|
|
22
|
+
"@arronqzy/blueprint-dsl": "1.0.15",
|
|
23
|
+
"@arronqzy/vue-rx-store": "0.1.6",
|
|
24
|
+
"@arronqzy/i18n": "0.1.13"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"vue": "^3.4.0"
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"eslint": "^8.57.0",
|
|
32
32
|
"typescript": "^5.5.4",
|
|
33
33
|
"vue-tsc": "^2.2.0",
|
|
34
|
-
"@arronqzy/
|
|
35
|
-
"@arronqzy/
|
|
34
|
+
"@arronqzy/typescript-config": "1.0.0",
|
|
35
|
+
"@arronqzy/eslint-config": "0.0.0"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -383,6 +383,9 @@ function toggleEventType(eventType: ViewEventType, checked: boolean) {
|
|
|
383
383
|
<JsonNodeConfigPanel
|
|
384
384
|
v-if="configSource === 'json'"
|
|
385
385
|
:node="node"
|
|
386
|
+
:graph-nodes="graphNodes"
|
|
387
|
+
:graph-edges="graphEdges"
|
|
388
|
+
:trace-entries="traceEntries"
|
|
386
389
|
:on-update-node="onUpdateNode"
|
|
387
390
|
/>
|
|
388
391
|
<StorageNodeConfigPanel
|
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
3
3
|
import { computed, ref } from "vue";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import type {
|
|
5
|
+
ExecutionTraceEntry,
|
|
6
|
+
JsonNodeConfig,
|
|
7
|
+
} from "@arronqzy/blueprint-dsl";
|
|
8
|
+
import {
|
|
9
|
+
latestTraceOutputsByNode,
|
|
10
|
+
resolveFetchIncomingScope,
|
|
11
|
+
resolveFetchScopeAutocompleteRoot,
|
|
12
|
+
validateJsonStringAllowingScope,
|
|
13
|
+
} from "@arronqzy/blueprint-dsl";
|
|
6
14
|
|
|
7
15
|
import ConfigHintIcon from "./ConfigHintIcon.vue";
|
|
8
|
-
import
|
|
16
|
+
import ScopeTemplateAutocompleteHost from "./ScopeTemplateAutocompleteHost.vue";
|
|
17
|
+
import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
|
|
9
18
|
import { resolveNodeJsonConfig } from "../graph/document";
|
|
10
19
|
|
|
11
20
|
const { t } = useI18n();
|
|
12
21
|
|
|
13
22
|
export type JsonNodeConfigPanelProps = {
|
|
14
23
|
node: BlueprintGraphNode;
|
|
24
|
+
graphNodes?: BlueprintGraphNode[];
|
|
25
|
+
graphEdges?: BlueprintGraphEdge[];
|
|
26
|
+
traceEntries?: ExecutionTraceEntry[];
|
|
15
27
|
onUpdateNode: (
|
|
16
28
|
nodeId: string,
|
|
17
29
|
patch: Partial<Pick<BlueprintGraphNode, "jsonConfig" | "configSource">>
|
|
18
30
|
) => void;
|
|
19
31
|
};
|
|
20
32
|
|
|
21
|
-
const props = defineProps<JsonNodeConfigPanelProps>()
|
|
33
|
+
const props = withDefaults(defineProps<JsonNodeConfigPanelProps>(), {
|
|
34
|
+
graphNodes: () => [],
|
|
35
|
+
graphEdges: () => [],
|
|
36
|
+
traceEntries: () => [],
|
|
37
|
+
});
|
|
22
38
|
|
|
39
|
+
const formRef = ref<HTMLDivElement | null>(null);
|
|
23
40
|
const draftError = ref<string | null>(null);
|
|
24
41
|
|
|
25
42
|
function patchJsonConfig(node: BlueprintGraphNode, patch: Partial<JsonNodeConfig>) {
|
|
@@ -31,24 +48,49 @@ function patchJsonConfig(node: BlueprintGraphNode, patch: Partial<JsonNodeConfig
|
|
|
31
48
|
|
|
32
49
|
const jsonConfig = computed(() => resolveNodeJsonConfig(props.node));
|
|
33
50
|
|
|
51
|
+
const incomingScope = computed(() => {
|
|
52
|
+
const outputs = latestTraceOutputsByNode(props.traceEntries);
|
|
53
|
+
return resolveFetchIncomingScope({
|
|
54
|
+
fetchNodeId: props.node.id,
|
|
55
|
+
nodes: props.graphNodes,
|
|
56
|
+
edges: props.graphEdges,
|
|
57
|
+
getOutput: (sourceId, port) => outputs[sourceId]?.[port],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
const autocompleteScope = computed(() =>
|
|
61
|
+
resolveFetchScopeAutocompleteRoot(incomingScope.value)
|
|
62
|
+
);
|
|
63
|
+
|
|
34
64
|
const storedValidation = computed(() =>
|
|
35
|
-
|
|
65
|
+
validateJsonStringAllowingScope(jsonConfig.value.jsonString)
|
|
36
66
|
);
|
|
37
67
|
|
|
38
68
|
const parseError = computed(
|
|
39
|
-
() =>
|
|
69
|
+
() =>
|
|
70
|
+
draftError.value ??
|
|
71
|
+
(storedValidation.value.ok ? null : storedValidation.value.error)
|
|
72
|
+
);
|
|
73
|
+
const deferredScope = computed(
|
|
74
|
+
() =>
|
|
75
|
+
storedValidation.value.ok &&
|
|
76
|
+
"deferred" in storedValidation.value &&
|
|
77
|
+
storedValidation.value.deferred === true
|
|
40
78
|
);
|
|
41
79
|
|
|
42
80
|
function handleChange(event: Event) {
|
|
43
81
|
const jsonString = (event.target as HTMLTextAreaElement).value;
|
|
44
|
-
const result =
|
|
82
|
+
const result = validateJsonStringAllowingScope(jsonString);
|
|
45
83
|
draftError.value = result.ok ? null : result.error;
|
|
46
84
|
props.onUpdateNode(props.node.id, patchJsonConfig(props.node, { jsonString }));
|
|
47
85
|
}
|
|
48
86
|
</script>
|
|
49
87
|
|
|
50
88
|
<template>
|
|
51
|
-
<div
|
|
89
|
+
<div
|
|
90
|
+
ref="formRef"
|
|
91
|
+
class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5"
|
|
92
|
+
>
|
|
93
|
+
<ScopeTemplateAutocompleteHost :scope="autocompleteScope" :container-ref="formRef" />
|
|
52
94
|
<div class="flex items-center gap-1.5">
|
|
53
95
|
<div class="font-medium text-foreground">{{ t("blueprint.config.jsonTitle") }}</div>
|
|
54
96
|
<ConfigHintIcon :label="t('blueprint.config.jsonTitle')">
|
|
@@ -57,20 +99,28 @@ function handleChange(event: Event) {
|
|
|
57
99
|
</div>
|
|
58
100
|
|
|
59
101
|
<label class="block space-y-1">
|
|
60
|
-
<span class="text-muted-foreground">
|
|
102
|
+
<span class="inline-flex items-center gap-1 text-muted-foreground">
|
|
103
|
+
{{ t("blueprint.config.jsonContent") }}
|
|
104
|
+
<ConfigHintIcon :label="t('blueprint.config.jsonContent')">
|
|
105
|
+
{{ t("blueprint.config.jsonScopeHint") }}
|
|
106
|
+
</ConfigHintIcon>
|
|
107
|
+
</span>
|
|
61
108
|
<textarea
|
|
62
109
|
:value="jsonConfig.jsonString"
|
|
63
110
|
rows="12"
|
|
64
111
|
spellcheck="false"
|
|
65
112
|
class="w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
|
|
66
113
|
placeholder='{
|
|
67
|
-
"key": "value"
|
|
114
|
+
"key": "{scope?.value}"
|
|
68
115
|
}'
|
|
69
116
|
@input="handleChange"
|
|
70
117
|
/>
|
|
71
118
|
<p v-if="parseError" class="text-[11px] text-destructive">
|
|
72
119
|
{{ t("blueprint.config.jsonFormatError", { error: parseError }) }}
|
|
73
120
|
</p>
|
|
121
|
+
<p v-else-if="deferredScope" class="text-[11px] text-muted-foreground">
|
|
122
|
+
{{ t("blueprint.config.jsonScopeDeferred") }}
|
|
123
|
+
</p>
|
|
74
124
|
<p v-else class="text-[11px] text-muted-foreground">{{ t("blueprint.config.jsonFormatOk") }}</p>
|
|
75
125
|
</label>
|
|
76
126
|
</div>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/vue";
|
|
3
|
+
import {
|
|
4
|
+
filterStorageKeySuggestions,
|
|
5
|
+
listBrowserStorageKeys,
|
|
6
|
+
type StorageKind,
|
|
7
|
+
} from "@arronqzy/blueprint-dsl";
|
|
8
|
+
import { computed, onUnmounted, ref, watch } from "vue";
|
|
9
|
+
|
|
10
|
+
import { cn } from "../utils/cn";
|
|
11
|
+
|
|
12
|
+
const { t } = useI18n();
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(
|
|
15
|
+
defineProps<{
|
|
16
|
+
value: string;
|
|
17
|
+
storages: StorageKind | readonly StorageKind[];
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
}>(),
|
|
20
|
+
{
|
|
21
|
+
placeholder: "",
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
change: [value: string];
|
|
27
|
+
}>();
|
|
28
|
+
|
|
29
|
+
const open = ref(false);
|
|
30
|
+
const activeIndex = ref(0);
|
|
31
|
+
const keys = ref<string[]>([]);
|
|
32
|
+
const containerRef = ref<HTMLDivElement | null>(null);
|
|
33
|
+
|
|
34
|
+
const storageList = computed(() =>
|
|
35
|
+
Array.isArray(props.storages) ? [...props.storages] : [props.storages]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function refreshKeys() {
|
|
39
|
+
keys.value = listBrowserStorageKeys(storageList.value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
watch(
|
|
43
|
+
storageList,
|
|
44
|
+
() => {
|
|
45
|
+
refreshKeys();
|
|
46
|
+
},
|
|
47
|
+
{ immediate: true, deep: true }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const filtered = computed(() =>
|
|
51
|
+
filterStorageKeySuggestions(keys.value, props.value)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
watch([() => props.value, () => filtered.value.length], () => {
|
|
55
|
+
activeIndex.value = 0;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let pointerCleanup: (() => void) | null = null;
|
|
59
|
+
|
|
60
|
+
watch(open, (isOpen) => {
|
|
61
|
+
pointerCleanup?.();
|
|
62
|
+
pointerCleanup = null;
|
|
63
|
+
if (!isOpen) return;
|
|
64
|
+
const onPointerDown = (event: MouseEvent) => {
|
|
65
|
+
if (!containerRef.value?.contains(event.target as globalThis.Node)) {
|
|
66
|
+
open.value = false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
window.addEventListener("pointerdown", onPointerDown);
|
|
70
|
+
pointerCleanup = () => window.removeEventListener("pointerdown", onPointerDown);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
onUnmounted(() => {
|
|
74
|
+
pointerCleanup?.();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const showSuggestions = computed(() => open.value && keys.value.length > 0);
|
|
78
|
+
|
|
79
|
+
function selectSuggestion(index: number) {
|
|
80
|
+
const item = filtered.value[index];
|
|
81
|
+
if (!item) return;
|
|
82
|
+
emit("change", item);
|
|
83
|
+
open.value = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function openDropdown() {
|
|
87
|
+
refreshKeys();
|
|
88
|
+
open.value = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleInput(event: Event) {
|
|
92
|
+
emit("change", (event.target as HTMLInputElement).value);
|
|
93
|
+
open.value = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
97
|
+
if (!showSuggestions.value || filtered.value.length === 0) {
|
|
98
|
+
if (event.key === "Escape") open.value = false;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (event.key === "ArrowDown") {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
activeIndex.value = Math.min(activeIndex.value + 1, filtered.value.length - 1);
|
|
104
|
+
} else if (event.key === "ArrowUp") {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
activeIndex.value = Math.max(activeIndex.value - 1, 0);
|
|
107
|
+
} else if (event.key === "Enter") {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
selectSuggestion(activeIndex.value);
|
|
110
|
+
} else if (event.key === "Escape") {
|
|
111
|
+
open.value = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div ref="containerRef" class="relative">
|
|
118
|
+
<input
|
|
119
|
+
:value="value"
|
|
120
|
+
:placeholder="placeholder"
|
|
121
|
+
spellcheck="false"
|
|
122
|
+
class="flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 font-mono text-[11px] text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
|
|
123
|
+
@input="handleInput"
|
|
124
|
+
@focus="openDropdown"
|
|
125
|
+
@click="openDropdown"
|
|
126
|
+
@keydown="handleKeyDown"
|
|
127
|
+
/>
|
|
128
|
+
<div
|
|
129
|
+
v-if="showSuggestions"
|
|
130
|
+
class="absolute z-[10150] mt-1 max-h-52 w-full overflow-auto rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-md"
|
|
131
|
+
>
|
|
132
|
+
<div
|
|
133
|
+
v-if="filtered.length === 0"
|
|
134
|
+
class="px-2 py-1.5 text-[11px] text-muted-foreground"
|
|
135
|
+
>
|
|
136
|
+
{{ t("blueprint.config.noMatchingStorageKey") }}
|
|
137
|
+
</div>
|
|
138
|
+
<button
|
|
139
|
+
v-for="(key, index) in filtered"
|
|
140
|
+
:key="key"
|
|
141
|
+
type="button"
|
|
142
|
+
class="flex w-full items-center px-2 py-1.5 text-left font-mono text-[11px] hover:bg-accent"
|
|
143
|
+
:class="cn(index === activeIndex && 'bg-accent')"
|
|
144
|
+
@mousedown.prevent
|
|
145
|
+
@click="selectSuggestion(index)"
|
|
146
|
+
>
|
|
147
|
+
{{ key }}
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div
|
|
151
|
+
v-else-if="open && keys.length === 0"
|
|
152
|
+
class="absolute z-[10150] mt-1 w-full rounded-md border border-border bg-popover px-2 py-1.5 text-[11px] text-muted-foreground shadow-md"
|
|
153
|
+
>
|
|
154
|
+
{{ t("blueprint.config.noStorageKeys") }}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
|
@@ -19,6 +19,7 @@ import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
|
|
|
19
19
|
import { resolveNodeStorageConfig } from "../graph/document";
|
|
20
20
|
import ConfigHintIcon from "./ConfigHintIcon.vue";
|
|
21
21
|
import ScopeTemplateAutocompleteHost from "./ScopeTemplateAutocompleteHost.vue";
|
|
22
|
+
import StorageKeyAutocomplete from "./StorageKeyAutocomplete.vue";
|
|
22
23
|
|
|
23
24
|
const { t } = useI18n();
|
|
24
25
|
|
|
@@ -112,6 +113,12 @@ function handleSetValueChange(value: string) {
|
|
|
112
113
|
patchStorageConfig(props.node, { set: { value: String(value ?? "") } })
|
|
113
114
|
);
|
|
114
115
|
}
|
|
116
|
+
|
|
117
|
+
const setKeySuggestionStorages = computed(() =>
|
|
118
|
+
storageConfig.value.set.storages.length > 0
|
|
119
|
+
? storageConfig.value.set.storages
|
|
120
|
+
: (["session", "local"] as const)
|
|
121
|
+
);
|
|
115
122
|
</script>
|
|
116
123
|
|
|
117
124
|
<template>
|
|
@@ -148,13 +155,11 @@ function handleSetValueChange(value: string) {
|
|
|
148
155
|
</label>
|
|
149
156
|
<label class="block space-y-1">
|
|
150
157
|
<span class="text-muted-foreground">{{ t("blueprint.config.storageKey") }}</span>
|
|
151
|
-
<
|
|
152
|
-
size="small"
|
|
158
|
+
<StorageKeyAutocomplete
|
|
153
159
|
:value="storageConfig.read.key"
|
|
154
|
-
|
|
155
|
-
class="font-mono text-[11px]"
|
|
160
|
+
:storages="storageConfig.read.storage"
|
|
156
161
|
placeholder="key"
|
|
157
|
-
@
|
|
162
|
+
@change="handleReadKeyChange"
|
|
158
163
|
/>
|
|
159
164
|
</label>
|
|
160
165
|
</div>
|
|
@@ -187,13 +192,11 @@ function handleSetValueChange(value: string) {
|
|
|
187
192
|
</div>
|
|
188
193
|
<label class="block space-y-1">
|
|
189
194
|
<span class="text-muted-foreground">{{ t("blueprint.config.storageKey") }}</span>
|
|
190
|
-
<
|
|
191
|
-
size="small"
|
|
195
|
+
<StorageKeyAutocomplete
|
|
192
196
|
:value="storageConfig.set.key"
|
|
193
|
-
|
|
194
|
-
class="font-mono text-[11px]"
|
|
197
|
+
:storages="setKeySuggestionStorages"
|
|
195
198
|
placeholder="key"
|
|
196
|
-
@
|
|
199
|
+
@change="handleSetKeyChange"
|
|
197
200
|
/>
|
|
198
201
|
</label>
|
|
199
202
|
<label class="block space-y-1">
|
|
@@ -162,6 +162,7 @@ export function useBlueprintPageLifecycle(options: UseBlueprintPageLifecycleOpti
|
|
|
162
162
|
const hadBoot = ref(false);
|
|
163
163
|
const prevActive = ref<boolean | undefined>(undefined);
|
|
164
164
|
let bootCancelled = false;
|
|
165
|
+
let updatedTimer = 0;
|
|
165
166
|
|
|
166
167
|
const resetFiredPhases = () => {
|
|
167
168
|
firedLifecyclePhases.value = new Set();
|
|
@@ -291,16 +292,21 @@ export function useBlueprintPageLifecycle(options: UseBlueprintPageLifecycleOpti
|
|
|
291
292
|
() => [bootCompleted.value, resolveValue(options.enabled ?? true), resolveValue(options.onUpdated)],
|
|
292
293
|
([completed, enabled]) => {
|
|
293
294
|
if (!enabled || !completed) return;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
295
|
+
if (updatedTimer) window.clearTimeout(updatedTimer);
|
|
296
|
+
updatedTimer = window.setTimeout(() => {
|
|
297
|
+
updatedTimer = 0;
|
|
298
|
+
void emitPhases(
|
|
299
|
+
graphRef.value,
|
|
300
|
+
["updated"],
|
|
301
|
+
runnerOptionsRef.value,
|
|
302
|
+
markPhaseFired
|
|
303
|
+
);
|
|
304
|
+
}, 160);
|
|
300
305
|
}
|
|
301
306
|
);
|
|
302
307
|
|
|
303
308
|
onUnmounted(() => {
|
|
309
|
+
if (updatedTimer) window.clearTimeout(updatedTimer);
|
|
304
310
|
teardownBoot();
|
|
305
311
|
});
|
|
306
312
|
|