@arronqzy/vue-blueprint 0.1.13 → 0.1.16
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/README.md +20 -12
- package/package.json +5 -5
- package/src/BlueprintNodeConfigSidebar.vue +3 -0
- package/src/blueprint.css +336 -54
- package/src/components/ApiCollectionManagerDialog.vue +397 -0
- package/src/components/BlueprintCanvas.vue +10 -1
- package/src/components/FetchNodeConfigPanel.vue +335 -3
- package/src/components/JsonLintTextarea.vue +112 -0
- package/src/components/JsonNodeConfigPanel.vue +79 -18
- package/src/components/ScopeTemplateAutocompleteHost.vue +7 -9
- package/src/components/StorageKeyAutocomplete.vue +157 -0
- package/src/components/StorageNodeConfigPanel.vue +13 -10
- package/src/edges/BlueprintSmoothEdge.vue +2 -2
- package/src/graph/blueprint-graph.ts +9 -0
- package/src/graph/document.ts +12 -0
- package/src/graph/sync-edges.ts +2 -2
- package/src/index.ts +27 -0
- package/src/library/api-collection-db.ts +98 -0
- package/src/library/api-collection-store.ts +143 -0
- package/src/nodes/AndFlowNode.vue +3 -2
- package/src/nodes/BlueprintFlowNode.vue +9 -2
- package/src/nodes/BlueprintNodeCard.vue +78 -122
- package/src/nodes/BlueprintNodeShell.vue +1 -0
- package/src/nodes/LifecycleFlowNode.vue +1 -0
|
@@ -66,17 +66,13 @@ function closeDropdown() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function refreshForInput(input: HTMLInputElement | HTMLTextAreaElement) {
|
|
69
|
-
if (props.scope === undefined) {
|
|
70
|
-
closeDropdown();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
69
|
const cursor = input.selectionStart ?? input.value.length;
|
|
74
|
-
const parsed = parseScopeAutocomplete(input.value, cursor, props.scope);
|
|
70
|
+
const parsed = parseScopeAutocomplete(input.value, cursor, props.scope ?? {});
|
|
75
71
|
if (!parsed) {
|
|
76
72
|
closeDropdown();
|
|
77
73
|
return;
|
|
78
74
|
}
|
|
79
|
-
const suggestions = getScopeAutocompleteSuggestions(props.scope, parsed);
|
|
75
|
+
const suggestions = getScopeAutocompleteSuggestions(props.scope ?? {}, parsed);
|
|
80
76
|
if (suggestions.length === 0) {
|
|
81
77
|
closeDropdown();
|
|
82
78
|
return;
|
|
@@ -170,7 +166,7 @@ function onPointerDown(event: MouseEvent) {
|
|
|
170
166
|
watch(
|
|
171
167
|
() => toValue(props.containerRef),
|
|
172
168
|
(container, _prev, onCleanup) => {
|
|
173
|
-
if (!container
|
|
169
|
+
if (!container) return;
|
|
174
170
|
|
|
175
171
|
container.addEventListener("focusin", onFocusIn);
|
|
176
172
|
container.addEventListener("input", onInput, true);
|
|
@@ -194,13 +190,15 @@ watch(
|
|
|
194
190
|
watch(
|
|
195
191
|
() => props.scope,
|
|
196
192
|
() => {
|
|
197
|
-
|
|
193
|
+
// scope 变化时刷新;system 联想不依赖 scope,勿因 undefined 关掉
|
|
194
|
+
const current = dropdown.value;
|
|
195
|
+
if (current) refreshForInput(current.input);
|
|
198
196
|
}
|
|
199
197
|
);
|
|
200
198
|
</script>
|
|
201
199
|
|
|
202
200
|
<template>
|
|
203
|
-
<Teleport v-if="dropdown
|
|
201
|
+
<Teleport v-if="dropdown" to="body">
|
|
204
202
|
<div
|
|
205
203
|
data-scope-ac-list
|
|
206
204
|
class="fixed z-[10120] max-h-44 min-w-[140px] overflow-auto rounded-md border border-border bg-white py-1 text-gray-800 shadow-md"
|
|
@@ -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">
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { BaseEdge, getSmoothStepPath, type EdgeProps } from "@vue-flow/core";
|
|
3
3
|
import { computed } from "vue";
|
|
4
4
|
|
|
5
|
-
const EDGE_STROKE = "#
|
|
6
|
-
const EDGE_WIDTH =
|
|
5
|
+
const EDGE_STROKE = "#94a3b8";
|
|
6
|
+
const EDGE_WIDTH = 1.75;
|
|
7
7
|
|
|
8
8
|
const props = defineProps<EdgeProps>();
|
|
9
9
|
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createNodeId,
|
|
4
4
|
defaultBlueprintNodeLabel,
|
|
5
5
|
filterInvalidBlueprintEdges,
|
|
6
|
+
isDefaultBlueprintNodeLabel,
|
|
6
7
|
patchNodeConfigSource,
|
|
7
8
|
resolveNodeClockConfig,
|
|
8
9
|
resolveNodeEventConfig,
|
|
@@ -218,6 +219,14 @@ export class BlueprintGraph {
|
|
|
218
219
|
nodes: this.document.nodes.map((n) => {
|
|
219
220
|
if (n.id !== nodeId) return n;
|
|
220
221
|
const nextNode = { ...n, ...configPatch, ...patch };
|
|
222
|
+
if (
|
|
223
|
+
patch.configSource &&
|
|
224
|
+
patch.configSource !== exists.configSource &&
|
|
225
|
+
patch.label === undefined &&
|
|
226
|
+
isDefaultBlueprintNodeLabel(exists.label)
|
|
227
|
+
) {
|
|
228
|
+
nextNode.label = defaultBlueprintNodeLabel(patch.configSource);
|
|
229
|
+
}
|
|
221
230
|
if (
|
|
222
231
|
patch.viewElementIds !== undefined ||
|
|
223
232
|
patch.viewElementId !== undefined
|
package/src/graph/document.ts
CHANGED
|
@@ -289,6 +289,18 @@ export function defaultBlueprintNodeLabel(
|
|
|
289
289
|
return getBlueprintNodeTypeLabel(t, configSource);
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
/** 当前名称是否仍是「类型默认名」(空或等于任一类型文案) */
|
|
293
|
+
export function isDefaultBlueprintNodeLabel(
|
|
294
|
+
label: string | undefined | null,
|
|
295
|
+
t: TranslateFn = defaultTranslate()
|
|
296
|
+
): boolean {
|
|
297
|
+
const trimmed = (label ?? "").trim();
|
|
298
|
+
if (!trimmed) return true;
|
|
299
|
+
return (
|
|
300
|
+
Object.keys(BLUEPRINT_CONFIG_TYPE_LABEL_KEYS) as BlueprintConfigSource[]
|
|
301
|
+
).some((src) => getBlueprintNodeTypeLabel(t, src) === trimmed);
|
|
302
|
+
}
|
|
303
|
+
|
|
292
304
|
export function resolveNodeFetchConfig(
|
|
293
305
|
node: Pick<BlueprintGraphNode, "fetchConfig">
|
|
294
306
|
): FetchRequestConfig {
|
package/src/graph/sync-edges.ts
CHANGED
|
@@ -6,8 +6,8 @@ import type { BlueprintGraph } from "./blueprint-graph";
|
|
|
6
6
|
|
|
7
7
|
/** 边线描边(写入 edge.style;使用 hex 避免 SVG style 解析 hsl 异常) */
|
|
8
8
|
export const BP_EDGE_STYLE = {
|
|
9
|
-
stroke: "#
|
|
10
|
-
strokeWidth:
|
|
9
|
+
stroke: "#94a3b8",
|
|
10
|
+
strokeWidth: 1.75,
|
|
11
11
|
} as const;
|
|
12
12
|
|
|
13
13
|
export const BP_FLOW_EDGE_TYPE = "blueprintSmooth" as const;
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,33 @@ export { default as BlueprintExecutionLogPanel } from "./components/BlueprintExe
|
|
|
15
15
|
export type { BlueprintExecutionLogPanelProps } from "./components/BlueprintExecutionLogPanel.vue";
|
|
16
16
|
export { default as BlueprintMetaDialog } from "./components/BlueprintMetaDialog.vue";
|
|
17
17
|
export type { BlueprintMetaDialogProps } from "./components/BlueprintMetaDialog.vue";
|
|
18
|
+
export { default as ApiCollectionManagerDialog } from "./components/ApiCollectionManagerDialog.vue";
|
|
19
|
+
export type {
|
|
20
|
+
ApiCollectionManagerDialogProps,
|
|
21
|
+
ApiCollectionUploadStartInfo,
|
|
22
|
+
ApiCollectionUploadSuccessInfo,
|
|
23
|
+
} from "./components/ApiCollectionManagerDialog.vue";
|
|
24
|
+
export {
|
|
25
|
+
hydrateApiCollections,
|
|
26
|
+
getApiCollectionRecords,
|
|
27
|
+
getApiCollectionById,
|
|
28
|
+
getApiCollectionListItems,
|
|
29
|
+
upsertApiCollectionFromJson,
|
|
30
|
+
removeApiCollection,
|
|
31
|
+
subscribeApiCollections,
|
|
32
|
+
useApiCollections,
|
|
33
|
+
useApiCollectionListItems,
|
|
34
|
+
} from "./library/api-collection-store";
|
|
35
|
+
export type {
|
|
36
|
+
UpsertApiCollectionInput,
|
|
37
|
+
UpsertApiCollectionResult,
|
|
38
|
+
} from "./library/api-collection-store";
|
|
39
|
+
export {
|
|
40
|
+
listApiCollectionRecords,
|
|
41
|
+
getApiCollectionRecord,
|
|
42
|
+
putApiCollectionRecord,
|
|
43
|
+
deleteApiCollectionRecord,
|
|
44
|
+
} from "./library/api-collection-db";
|
|
18
45
|
export { default as BlueprintNodeSwitchTaskDialog } from "./components/BlueprintNodeSwitchTaskDialog.vue";
|
|
19
46
|
export type { BlueprintNodeSwitchTaskDialogProps } from "./components/BlueprintNodeSwitchTaskDialog.vue";
|
|
20
47
|
export type { ExecutionLogSettings } from "./library/execution-log-settings";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
2
|
+
import type { ApiCollectionRecord } from "@arronqzy/blueprint-dsl";
|
|
3
|
+
|
|
4
|
+
const DB_NAME = "arronqzy-api-collections";
|
|
5
|
+
const DB_VERSION = 1;
|
|
6
|
+
const STORE_NAME = "collections";
|
|
7
|
+
|
|
8
|
+
function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
|
|
11
|
+
request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
|
|
12
|
+
request.onupgradeneeded = () => {
|
|
13
|
+
const db = request.result;
|
|
14
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
15
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
16
|
+
store.createIndex("updatedAt", "updatedAt", { unique: false });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
request.onsuccess = () => resolve(request.result);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runTransaction<T>(
|
|
24
|
+
mode: IDBTransactionMode,
|
|
25
|
+
runner: (store: IDBObjectStore) => IDBRequest<T>,
|
|
26
|
+
nameSpace?: string | null
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
return openDb(nameSpace).then(
|
|
29
|
+
(db) =>
|
|
30
|
+
new Promise<T>((resolve, reject) => {
|
|
31
|
+
const tx = db.transaction(STORE_NAME, mode);
|
|
32
|
+
const store = tx.objectStore(STORE_NAME);
|
|
33
|
+
const request = runner(store);
|
|
34
|
+
let result!: T;
|
|
35
|
+
|
|
36
|
+
request.onerror = () =>
|
|
37
|
+
reject(request.error ?? new Error("indexeddb-request-failed"));
|
|
38
|
+
request.onsuccess = () => {
|
|
39
|
+
result = request.result as T;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
tx.oncomplete = () => {
|
|
43
|
+
db.close();
|
|
44
|
+
resolve(result);
|
|
45
|
+
};
|
|
46
|
+
tx.onerror = () => {
|
|
47
|
+
db.close();
|
|
48
|
+
reject(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
49
|
+
};
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function listApiCollectionRecords(
|
|
55
|
+
nameSpace?: string | null
|
|
56
|
+
): Promise<ApiCollectionRecord[]> {
|
|
57
|
+
const records = await runTransaction<ApiCollectionRecord[]>(
|
|
58
|
+
"readonly",
|
|
59
|
+
(store) => store.getAll(),
|
|
60
|
+
nameSpace
|
|
61
|
+
);
|
|
62
|
+
return records.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getApiCollectionRecord(
|
|
66
|
+
id: string,
|
|
67
|
+
nameSpace?: string | null
|
|
68
|
+
): Promise<ApiCollectionRecord | null> {
|
|
69
|
+
const record = await runTransaction<ApiCollectionRecord | undefined>(
|
|
70
|
+
"readonly",
|
|
71
|
+
(store) => store.get(id),
|
|
72
|
+
nameSpace
|
|
73
|
+
);
|
|
74
|
+
return record ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function putApiCollectionRecord(
|
|
78
|
+
record: ApiCollectionRecord,
|
|
79
|
+
nameSpace?: string | null
|
|
80
|
+
): Promise<ApiCollectionRecord> {
|
|
81
|
+
await runTransaction<IDBValidKey>(
|
|
82
|
+
"readwrite",
|
|
83
|
+
(store) => store.put(record),
|
|
84
|
+
nameSpace
|
|
85
|
+
);
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function deleteApiCollectionRecord(
|
|
90
|
+
id: string,
|
|
91
|
+
nameSpace?: string | null
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
await runTransaction<undefined>(
|
|
94
|
+
"readwrite",
|
|
95
|
+
(store) => store.delete(id),
|
|
96
|
+
nameSpace
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { computed, onScopeDispose, ref, type ComputedRef, type Ref } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
apiCollectionToListItem,
|
|
4
|
+
createApiCollectionId,
|
|
5
|
+
parseApiCollectionJson,
|
|
6
|
+
type ApiCollectionDocument,
|
|
7
|
+
type ApiCollectionListItem,
|
|
8
|
+
type ApiCollectionRecord,
|
|
9
|
+
} from "@arronqzy/blueprint-dsl";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
deleteApiCollectionRecord,
|
|
13
|
+
listApiCollectionRecords,
|
|
14
|
+
putApiCollectionRecord,
|
|
15
|
+
} from "./api-collection-db";
|
|
16
|
+
|
|
17
|
+
type Listener = () => void;
|
|
18
|
+
|
|
19
|
+
let nameSpaceRef: string | null | undefined = null;
|
|
20
|
+
let records: ApiCollectionRecord[] = [];
|
|
21
|
+
let hydrated = false;
|
|
22
|
+
const listeners = new Set<Listener>();
|
|
23
|
+
|
|
24
|
+
function emit() {
|
|
25
|
+
for (const listener of listeners) listener();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sortRecords(list: ApiCollectionRecord[]): ApiCollectionRecord[] {
|
|
29
|
+
return [...list].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getApiCollectionRecords(): ApiCollectionRecord[] {
|
|
33
|
+
return records;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getApiCollectionListItems(): ApiCollectionListItem[] {
|
|
37
|
+
return records.map(apiCollectionToListItem);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getApiCollectionById(id: string): ApiCollectionRecord | null {
|
|
41
|
+
return records.find((item) => item.id === id) ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function subscribeApiCollections(listener: Listener): () => void {
|
|
45
|
+
listeners.add(listener);
|
|
46
|
+
return () => listeners.delete(listener);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function hydrateApiCollections(
|
|
50
|
+
nameSpace?: string | null
|
|
51
|
+
): Promise<ApiCollectionRecord[]> {
|
|
52
|
+
nameSpaceRef = nameSpace;
|
|
53
|
+
const persisted = await listApiCollectionRecords(nameSpace);
|
|
54
|
+
const memoryOnly = records.filter((item) => !item.persistLocal);
|
|
55
|
+
const persistedIds = new Set(persisted.map((item) => item.id));
|
|
56
|
+
records = sortRecords([
|
|
57
|
+
...persisted,
|
|
58
|
+
...memoryOnly.filter((item) => !persistedIds.has(item.id)),
|
|
59
|
+
]);
|
|
60
|
+
hydrated = true;
|
|
61
|
+
emit();
|
|
62
|
+
return records;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type UpsertApiCollectionInput = {
|
|
66
|
+
id?: string;
|
|
67
|
+
rawJson: string;
|
|
68
|
+
persistLocal: boolean;
|
|
69
|
+
/** 未写 document.name 时用文件名 */
|
|
70
|
+
fallbackName?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type UpsertApiCollectionResult =
|
|
74
|
+
| { ok: true; record: ApiCollectionRecord }
|
|
75
|
+
| { ok: false; error: string };
|
|
76
|
+
|
|
77
|
+
export async function upsertApiCollectionFromJson(
|
|
78
|
+
input: UpsertApiCollectionInput
|
|
79
|
+
): Promise<UpsertApiCollectionResult> {
|
|
80
|
+
const parsed = parseApiCollectionJson(input.rawJson);
|
|
81
|
+
if (!parsed.ok) return parsed;
|
|
82
|
+
|
|
83
|
+
const document: ApiCollectionDocument = {
|
|
84
|
+
...parsed.document,
|
|
85
|
+
name:
|
|
86
|
+
parsed.document.name.trim() ||
|
|
87
|
+
input.fallbackName?.replace(/\.json$/i, "").trim() ||
|
|
88
|
+
parsed.document.name,
|
|
89
|
+
};
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const existing = input.id
|
|
92
|
+
? records.find((item) => item.id === input.id) ?? null
|
|
93
|
+
: null;
|
|
94
|
+
const record: ApiCollectionRecord = {
|
|
95
|
+
id: existing?.id ?? createApiCollectionId(),
|
|
96
|
+
name: document.name,
|
|
97
|
+
persistLocal: input.persistLocal,
|
|
98
|
+
document,
|
|
99
|
+
rawJson: JSON.stringify(document, null, 2),
|
|
100
|
+
createdAt: existing?.createdAt ?? now,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (record.persistLocal) {
|
|
105
|
+
await putApiCollectionRecord(record, nameSpaceRef);
|
|
106
|
+
} else if (existing?.persistLocal) {
|
|
107
|
+
await deleteApiCollectionRecord(record.id, nameSpaceRef);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const next = records.filter((item) => item.id !== record.id);
|
|
111
|
+
next.push(record);
|
|
112
|
+
records = sortRecords(next);
|
|
113
|
+
emit();
|
|
114
|
+
return { ok: true, record };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function removeApiCollection(id: string): Promise<void> {
|
|
118
|
+
const existing = records.find((item) => item.id === id);
|
|
119
|
+
if (existing?.persistLocal || hydrated) {
|
|
120
|
+
try {
|
|
121
|
+
await deleteApiCollectionRecord(id, nameSpaceRef);
|
|
122
|
+
} catch {
|
|
123
|
+
// 内存-only 记录在库里可能不存在
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
records = records.filter((item) => item.id !== id);
|
|
127
|
+
emit();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Vue composable:返回响应式集合列表 */
|
|
131
|
+
export function useApiCollections(): Ref<ApiCollectionRecord[]> {
|
|
132
|
+
const list = ref<ApiCollectionRecord[]>([...records]);
|
|
133
|
+
const unsubscribe = subscribeApiCollections(() => {
|
|
134
|
+
list.value = [...records];
|
|
135
|
+
});
|
|
136
|
+
onScopeDispose(unsubscribe);
|
|
137
|
+
return list;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function useApiCollectionListItems(): ComputedRef<ApiCollectionListItem[]> {
|
|
141
|
+
const list = useApiCollections();
|
|
142
|
+
return computed(() => list.value.map(apiCollectionToListItem));
|
|
143
|
+
}
|
|
@@ -22,6 +22,7 @@ const executionTone = resolveBlueprintNodeExecutionTone(nodeData);
|
|
|
22
22
|
:class="
|
|
23
23
|
cn(
|
|
24
24
|
'bp-node bp-node--and',
|
|
25
|
+
nodeData.isSelected && 'bp-node--selected',
|
|
25
26
|
executionTone === 'success' && 'bp-node--execution-true',
|
|
26
27
|
executionTone === 'error' && 'bp-node--execution-false'
|
|
27
28
|
)
|
|
@@ -32,7 +33,7 @@ const executionTone = resolveBlueprintNodeExecutionTone(nodeData);
|
|
|
32
33
|
:position="Position.Left"
|
|
33
34
|
id="inA"
|
|
34
35
|
class="bp-flow-handle bp-flow-handle--target"
|
|
35
|
-
:style="{ top: '
|
|
36
|
+
:style="{ top: '32%' }"
|
|
36
37
|
:title="t('blueprint.node.andInA')"
|
|
37
38
|
/>
|
|
38
39
|
<Handle
|
|
@@ -40,7 +41,7 @@ const executionTone = resolveBlueprintNodeExecutionTone(nodeData);
|
|
|
40
41
|
:position="Position.Left"
|
|
41
42
|
id="inB"
|
|
42
43
|
class="bp-flow-handle bp-flow-handle--target"
|
|
43
|
-
:style="{ top: '
|
|
44
|
+
:style="{ top: '68%' }"
|
|
44
45
|
:title="t('blueprint.node.andInB')"
|
|
45
46
|
/>
|
|
46
47
|
<BlueprintNodeCard
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
3
3
|
import type { NodeProps } from "@vue-flow/core";
|
|
4
|
-
import {
|
|
4
|
+
import { computed } from "vue";
|
|
5
|
+
import {
|
|
6
|
+
resolveBlueprintConfigSource,
|
|
7
|
+
resolveBlueprintNodeTypeLabel,
|
|
8
|
+
} from "../graph/document";
|
|
5
9
|
import { resolveBlueprintNodeSummary } from "../graph/node-summary";
|
|
6
10
|
import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
|
|
7
11
|
import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
|
|
@@ -13,6 +17,9 @@ const { t } = useI18n();
|
|
|
13
17
|
const props = defineProps<NodeProps<BlueprintFlowNodeData>>();
|
|
14
18
|
const onSelect = useBlueprintNodeSelect();
|
|
15
19
|
const nodeData = props.data;
|
|
20
|
+
const variant = computed(() =>
|
|
21
|
+
resolveBlueprintConfigSource(nodeData) === "view" ? "view" : "blueprint"
|
|
22
|
+
);
|
|
16
23
|
</script>
|
|
17
24
|
|
|
18
25
|
<template>
|
|
@@ -21,7 +28,7 @@ const nodeData = props.data;
|
|
|
21
28
|
:label="nodeData.label"
|
|
22
29
|
:meta="resolveBlueprintNodeTypeLabel(nodeData, t)"
|
|
23
30
|
:subtitle="resolveBlueprintNodeSummary(nodeData, t)"
|
|
24
|
-
variant="
|
|
31
|
+
:variant="variant"
|
|
25
32
|
:selected="Boolean(nodeData.isSelected)"
|
|
26
33
|
:execution-tone="resolveBlueprintNodeExecutionTone(nodeData)"
|
|
27
34
|
@select="onSelect"
|