@arronqzy/vue-blueprint 0.1.13 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-blueprint",
3
- "version": "0.1.13",
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/blueprint-dsl": "1.0.14",
22
+ "@arronqzy/blueprint-dsl": "1.0.15",
23
23
  "@arronqzy/vue-rx-store": "0.1.6",
24
- "@arronqzy/i18n": "0.1.12"
24
+ "@arronqzy/i18n": "0.1.13"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "vue": "^3.4.0"
@@ -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 { validateJsonString } from "@arronqzy/blueprint-dsl";
5
- import type { JsonNodeConfig } from "@arronqzy/blueprint-dsl";
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 type { BlueprintGraphNode } from "../graph/document";
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
- validateJsonString(jsonConfig.value.jsonString)
65
+ validateJsonStringAllowingScope(jsonConfig.value.jsonString)
36
66
  );
37
67
 
38
68
  const parseError = computed(
39
- () => draftError.value ?? (storedValidation.value.ok ? null : storedValidation.value.error)
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 = validateJsonString(jsonString);
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 class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
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">{{ t("blueprint.config.jsonContent") }}</span>
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
- <Input
152
- size="small"
158
+ <StorageKeyAutocomplete
153
159
  :value="storageConfig.read.key"
154
- spellcheck="false"
155
- class="font-mono text-[11px]"
160
+ :storages="storageConfig.read.storage"
156
161
  placeholder="key"
157
- @update:value="(v) => handleReadKeyChange(String(v ?? ''))"
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
- <Input
191
- size="small"
195
+ <StorageKeyAutocomplete
192
196
  :value="storageConfig.set.key"
193
- spellcheck="false"
194
- class="font-mono text-[11px]"
197
+ :storages="setKeySuggestionStorages"
195
198
  placeholder="key"
196
- @update:value="(v) => handleSetKeyChange(String(v ?? ''))"
199
+ @change="handleSetKeyChange"
197
200
  />
198
201
  </label>
199
202
  <label class="block space-y-1">