@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
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { useI18n } from "@arronqzy/i18n/vue";
|
|
4
|
+
import { Button, Checkbox, Input, Modal } from "ant-design-vue";
|
|
5
|
+
import {
|
|
6
|
+
downloadApiCollectionTemplate,
|
|
7
|
+
getApiCollectionTemplateJson,
|
|
8
|
+
type ApiCollectionRecord,
|
|
9
|
+
type ApiCollectionSyncResult,
|
|
10
|
+
} from "@arronqzy/blueprint-dsl";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getApiCollectionRecords,
|
|
14
|
+
removeApiCollection,
|
|
15
|
+
subscribeApiCollections,
|
|
16
|
+
upsertApiCollectionFromJson,
|
|
17
|
+
} from "../library/api-collection-store";
|
|
18
|
+
|
|
19
|
+
export type ApiCollectionUploadStartInfo = {
|
|
20
|
+
fileNames: string[];
|
|
21
|
+
count: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ApiCollectionUploadSuccessInfo = {
|
|
25
|
+
collections: Array<{
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
apiCount: number;
|
|
29
|
+
persistLocal: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ApiCollectionManagerDialogProps = {
|
|
34
|
+
open: boolean;
|
|
35
|
+
onOpenChange: (open: boolean) => void;
|
|
36
|
+
onUploadStart?: (info: ApiCollectionUploadStartInfo) => void;
|
|
37
|
+
onUploadSuccess?: (info: ApiCollectionUploadSuccessInfo) => void;
|
|
38
|
+
onSyncCollection?: (
|
|
39
|
+
record: ApiCollectionRecord
|
|
40
|
+
) => ApiCollectionSyncResult | Promise<ApiCollectionSyncResult>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const props = defineProps<ApiCollectionManagerDialogProps>();
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits<{
|
|
46
|
+
uploadStart: [info: ApiCollectionUploadStartInfo];
|
|
47
|
+
uploadSuccess: [info: ApiCollectionUploadSuccessInfo];
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
const { t } = useI18n();
|
|
51
|
+
|
|
52
|
+
function formatSyncResultMessage(result: ApiCollectionSyncResult): string {
|
|
53
|
+
if (result.updatedCount <= 0) {
|
|
54
|
+
return t("blueprint.apiCollection.syncNone");
|
|
55
|
+
}
|
|
56
|
+
const names = result.nodes.map((node) => node.label).join("、");
|
|
57
|
+
return t("blueprint.apiCollection.syncSuccessDetail", {
|
|
58
|
+
count: result.updatedCount,
|
|
59
|
+
names,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
63
|
+
const records = ref<ApiCollectionRecord[]>([...getApiCollectionRecords()]);
|
|
64
|
+
const persistLocalDefault = ref(true);
|
|
65
|
+
const error = ref<string | null>(null);
|
|
66
|
+
const busy = ref(false);
|
|
67
|
+
|
|
68
|
+
const editorOpen = ref(false);
|
|
69
|
+
const editingId = ref<string | null>(null);
|
|
70
|
+
const editorJson = ref("");
|
|
71
|
+
const editorPersistLocal = ref(true);
|
|
72
|
+
const editorError = ref<string | null>(null);
|
|
73
|
+
|
|
74
|
+
watch(
|
|
75
|
+
() => props.open,
|
|
76
|
+
(open, _prev, onCleanup) => {
|
|
77
|
+
if (!open) return;
|
|
78
|
+
records.value = [...getApiCollectionRecords()];
|
|
79
|
+
const unsubscribe = subscribeApiCollections(() => {
|
|
80
|
+
records.value = [...getApiCollectionRecords()];
|
|
81
|
+
});
|
|
82
|
+
onCleanup(unsubscribe);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const sorted = computed(() =>
|
|
87
|
+
[...records.value].sort((a, b) => b.updatedAt - a.updatedAt)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
function notifyUploadStart(info: ApiCollectionUploadStartInfo) {
|
|
91
|
+
props.onUploadStart?.(info);
|
|
92
|
+
emit("uploadStart", info);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function notifyUploadSuccess(info: ApiCollectionUploadSuccessInfo) {
|
|
96
|
+
props.onUploadSuccess?.(info);
|
|
97
|
+
emit("uploadSuccess", info);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function openCreateEditor() {
|
|
101
|
+
editingId.value = null;
|
|
102
|
+
editorJson.value = getApiCollectionTemplateJson();
|
|
103
|
+
editorPersistLocal.value = persistLocalDefault.value;
|
|
104
|
+
editorError.value = null;
|
|
105
|
+
editorOpen.value = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function openEditEditor(record: ApiCollectionRecord) {
|
|
109
|
+
editingId.value = record.id;
|
|
110
|
+
editorJson.value = record.rawJson;
|
|
111
|
+
editorPersistLocal.value = record.persistLocal;
|
|
112
|
+
editorError.value = null;
|
|
113
|
+
editorOpen.value = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleSaveEditor() {
|
|
117
|
+
busy.value = true;
|
|
118
|
+
editorError.value = null;
|
|
119
|
+
try {
|
|
120
|
+
const result = await upsertApiCollectionFromJson({
|
|
121
|
+
id: editingId.value ?? undefined,
|
|
122
|
+
rawJson: editorJson.value,
|
|
123
|
+
persistLocal: editorPersistLocal.value,
|
|
124
|
+
});
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
editorError.value = result.error;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
editorOpen.value = false;
|
|
130
|
+
} finally {
|
|
131
|
+
busy.value = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function handleDelete(id: string) {
|
|
136
|
+
const ok = window.confirm(t("blueprint.apiCollection.confirmDelete"));
|
|
137
|
+
if (!ok) return;
|
|
138
|
+
busy.value = true;
|
|
139
|
+
error.value = null;
|
|
140
|
+
try {
|
|
141
|
+
await removeApiCollection(id);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
error.value = err instanceof Error ? err.message : String(err);
|
|
144
|
+
} finally {
|
|
145
|
+
busy.value = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function handleSync(record: ApiCollectionRecord) {
|
|
150
|
+
if (!props.onSyncCollection) {
|
|
151
|
+
error.value = t("blueprint.apiCollection.syncUnavailable");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
busy.value = true;
|
|
155
|
+
error.value = null;
|
|
156
|
+
try {
|
|
157
|
+
const result = await props.onSyncCollection(record);
|
|
158
|
+
window.alert(formatSyncResultMessage(result));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
error.value = err instanceof Error ? err.message : String(err);
|
|
161
|
+
} finally {
|
|
162
|
+
busy.value = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleSyncFromEditor() {
|
|
167
|
+
busy.value = true;
|
|
168
|
+
editorError.value = null;
|
|
169
|
+
try {
|
|
170
|
+
const result = await upsertApiCollectionFromJson({
|
|
171
|
+
id: editingId.value ?? undefined,
|
|
172
|
+
rawJson: editorJson.value,
|
|
173
|
+
persistLocal: editorPersistLocal.value,
|
|
174
|
+
});
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
editorError.value = result.error;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
editingId.value = result.record.id;
|
|
180
|
+
if (!props.onSyncCollection) {
|
|
181
|
+
editorError.value = t("blueprint.apiCollection.syncUnavailable");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const syncResult = await props.onSyncCollection(result.record);
|
|
185
|
+
window.alert(formatSyncResultMessage(syncResult));
|
|
186
|
+
} catch (err) {
|
|
187
|
+
editorError.value = err instanceof Error ? err.message : String(err);
|
|
188
|
+
} finally {
|
|
189
|
+
busy.value = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function handleFiles(files: FileList | null) {
|
|
194
|
+
if (!files || files.length === 0) return;
|
|
195
|
+
const list = Array.from(files);
|
|
196
|
+
busy.value = true;
|
|
197
|
+
error.value = null;
|
|
198
|
+
notifyUploadStart({
|
|
199
|
+
fileNames: list.map((file) => file.name),
|
|
200
|
+
count: list.length,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const uploaded: ApiCollectionUploadSuccessInfo["collections"] = [];
|
|
204
|
+
const errors: string[] = [];
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
for (const file of list) {
|
|
208
|
+
const text = await file.text();
|
|
209
|
+
const result = await upsertApiCollectionFromJson({
|
|
210
|
+
rawJson: text,
|
|
211
|
+
persistLocal: persistLocalDefault.value,
|
|
212
|
+
fallbackName: file.name,
|
|
213
|
+
});
|
|
214
|
+
if (!result.ok) {
|
|
215
|
+
errors.push(`${file.name}: ${result.error}`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
uploaded.push({
|
|
219
|
+
id: result.record.id,
|
|
220
|
+
name: result.record.name,
|
|
221
|
+
apiCount: result.record.document.apis.length,
|
|
222
|
+
persistLocal: result.record.persistLocal,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (uploaded.length > 0) {
|
|
226
|
+
notifyUploadSuccess({ collections: uploaded });
|
|
227
|
+
}
|
|
228
|
+
if (errors.length > 0) {
|
|
229
|
+
error.value = errors.join("\n");
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
error.value = err instanceof Error ? err.message : String(err);
|
|
233
|
+
} finally {
|
|
234
|
+
busy.value = false;
|
|
235
|
+
if (fileInputRef.value) fileInputRef.value.value = "";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<template>
|
|
241
|
+
<Modal
|
|
242
|
+
:open="open"
|
|
243
|
+
:title="t('blueprint.apiCollection.managerTitle')"
|
|
244
|
+
:footer="null"
|
|
245
|
+
width="960px"
|
|
246
|
+
:body-style="{ maxHeight: '90vh', display: 'flex', flexDirection: 'column' }"
|
|
247
|
+
@update:open="onOpenChange"
|
|
248
|
+
>
|
|
249
|
+
<p class="mb-3 text-xs text-muted-foreground">
|
|
250
|
+
{{ t("blueprint.apiCollection.managerDescription") }}
|
|
251
|
+
</p>
|
|
252
|
+
|
|
253
|
+
<div class="mb-2 flex flex-wrap items-center gap-2 border-b border-border/60 pb-3">
|
|
254
|
+
<Button size="small" type="primary" :disabled="busy" @click="fileInputRef?.click()">
|
|
255
|
+
{{ t("blueprint.apiCollection.upload") }}
|
|
256
|
+
</Button>
|
|
257
|
+
<Button size="small" :disabled="busy" @click="openCreateEditor">
|
|
258
|
+
{{ t("blueprint.apiCollection.create") }}
|
|
259
|
+
</Button>
|
|
260
|
+
<Button size="small" @click="downloadApiCollectionTemplate()">
|
|
261
|
+
{{ t("blueprint.apiCollection.downloadTemplate") }}
|
|
262
|
+
</Button>
|
|
263
|
+
<label class="ml-auto flex cursor-pointer items-center gap-2 text-[11px] text-muted-foreground">
|
|
264
|
+
<Checkbox v-model:checked="persistLocalDefault" />
|
|
265
|
+
{{ t("blueprint.apiCollection.persistLocal") }}
|
|
266
|
+
</label>
|
|
267
|
+
<input
|
|
268
|
+
ref="fileInputRef"
|
|
269
|
+
type="file"
|
|
270
|
+
accept="application/json,.json"
|
|
271
|
+
multiple
|
|
272
|
+
class="hidden"
|
|
273
|
+
@change="(e) => void handleFiles((e.target as HTMLInputElement).files)"
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<p class="mb-2 text-[11px] text-muted-foreground">
|
|
278
|
+
{{ t("blueprint.apiCollection.persistLocalHint") }}
|
|
279
|
+
</p>
|
|
280
|
+
|
|
281
|
+
<pre
|
|
282
|
+
v-if="error"
|
|
283
|
+
class="mb-2 max-h-24 overflow-auto whitespace-pre-wrap rounded border border-destructive/40 bg-destructive/5 p-2 text-[11px] text-destructive"
|
|
284
|
+
>{{ error }}</pre>
|
|
285
|
+
|
|
286
|
+
<div class="min-h-0 flex-1 space-y-2 overflow-auto py-1">
|
|
287
|
+
<p
|
|
288
|
+
v-if="sorted.length === 0"
|
|
289
|
+
class="py-8 text-center text-xs text-muted-foreground"
|
|
290
|
+
>
|
|
291
|
+
{{ t("blueprint.apiCollection.empty") }}
|
|
292
|
+
</p>
|
|
293
|
+
<div
|
|
294
|
+
v-for="record in sorted"
|
|
295
|
+
:key="record.id"
|
|
296
|
+
class="flex items-start gap-2 rounded-md border border-border/70 bg-muted/20 px-2.5 py-2"
|
|
297
|
+
>
|
|
298
|
+
<div class="min-w-0 flex-1">
|
|
299
|
+
<div class="truncate text-xs font-medium text-foreground">
|
|
300
|
+
{{ record.name }}
|
|
301
|
+
</div>
|
|
302
|
+
<div class="mt-0.5 flex flex-wrap gap-x-2 text-[10px] text-muted-foreground">
|
|
303
|
+
<span>
|
|
304
|
+
{{
|
|
305
|
+
t("blueprint.apiCollection.apiCount", {
|
|
306
|
+
count: record.document.apis.length,
|
|
307
|
+
})
|
|
308
|
+
}}
|
|
309
|
+
</span>
|
|
310
|
+
<span>
|
|
311
|
+
{{
|
|
312
|
+
record.persistLocal
|
|
313
|
+
? t("blueprint.apiCollection.persisted")
|
|
314
|
+
: t("blueprint.apiCollection.memoryOnly")
|
|
315
|
+
}}
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<Button
|
|
320
|
+
size="small"
|
|
321
|
+
:disabled="busy || !onSyncCollection"
|
|
322
|
+
:title="t('blueprint.apiCollection.syncHint')"
|
|
323
|
+
@click="void handleSync(record)"
|
|
324
|
+
>
|
|
325
|
+
{{ t("blueprint.apiCollection.sync") }}
|
|
326
|
+
</Button>
|
|
327
|
+
<Button size="small" :disabled="busy" @click="openEditEditor(record)">
|
|
328
|
+
{{ t("common.edit") }}
|
|
329
|
+
</Button>
|
|
330
|
+
<Button
|
|
331
|
+
size="small"
|
|
332
|
+
danger
|
|
333
|
+
:disabled="busy"
|
|
334
|
+
@click="void handleDelete(record.id)"
|
|
335
|
+
>
|
|
336
|
+
{{ t("common.delete") }}
|
|
337
|
+
</Button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div class="mt-4 flex justify-end">
|
|
342
|
+
<Button @click="onOpenChange(false)">{{ t("common.close") }}</Button>
|
|
343
|
+
</div>
|
|
344
|
+
</Modal>
|
|
345
|
+
|
|
346
|
+
<Modal
|
|
347
|
+
:open="editorOpen"
|
|
348
|
+
:title="
|
|
349
|
+
editingId
|
|
350
|
+
? t('blueprint.apiCollection.editTitle')
|
|
351
|
+
: t('blueprint.apiCollection.createTitle')
|
|
352
|
+
"
|
|
353
|
+
:footer="null"
|
|
354
|
+
width="1100px"
|
|
355
|
+
:style="{ top: '4vh' }"
|
|
356
|
+
@update:open="(v) => (editorOpen = v)"
|
|
357
|
+
>
|
|
358
|
+
<p class="mb-3 text-xs text-muted-foreground">
|
|
359
|
+
{{ t("blueprint.apiCollection.editorDescription") }}
|
|
360
|
+
</p>
|
|
361
|
+
|
|
362
|
+
<div class="space-y-2">
|
|
363
|
+
<div class="flex items-center justify-between gap-2">
|
|
364
|
+
<span class="text-xs">{{ t("blueprint.apiCollection.jsonEditor") }}</span>
|
|
365
|
+
<label class="flex cursor-pointer items-center gap-2 text-[11px] text-muted-foreground">
|
|
366
|
+
<Checkbox v-model:checked="editorPersistLocal" />
|
|
367
|
+
{{ t("blueprint.apiCollection.persistLocal") }}
|
|
368
|
+
</label>
|
|
369
|
+
</div>
|
|
370
|
+
<Input.TextArea
|
|
371
|
+
v-model:value="editorJson"
|
|
372
|
+
:rows="28"
|
|
373
|
+
:spellcheck="false"
|
|
374
|
+
class="!min-h-[62vh] font-mono text-[12px] leading-relaxed"
|
|
375
|
+
:status="editorError ? 'error' : undefined"
|
|
376
|
+
/>
|
|
377
|
+
<p v-if="editorError" class="text-[11px] text-destructive">{{ editorError }}</p>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
|
381
|
+
<Button @click="downloadApiCollectionTemplate()">
|
|
382
|
+
{{ t("blueprint.apiCollection.downloadTemplate") }}
|
|
383
|
+
</Button>
|
|
384
|
+
<Button
|
|
385
|
+
:disabled="busy || !onSyncCollection"
|
|
386
|
+
:title="t('blueprint.apiCollection.syncHint')"
|
|
387
|
+
@click="void handleSyncFromEditor()"
|
|
388
|
+
>
|
|
389
|
+
{{ t("blueprint.apiCollection.sync") }}
|
|
390
|
+
</Button>
|
|
391
|
+
<Button @click="editorOpen = false">{{ t("common.cancel") }}</Button>
|
|
392
|
+
<Button type="primary" :disabled="busy" @click="void handleSaveEditor()">
|
|
393
|
+
{{ t("common.save") }}
|
|
394
|
+
</Button>
|
|
395
|
+
</div>
|
|
396
|
+
</Modal>
|
|
397
|
+
</template>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type NodeMouseEvent,
|
|
9
9
|
type EdgeMouseEvent,
|
|
10
10
|
} from "@vue-flow/core";
|
|
11
|
+
import { Background } from "@vue-flow/background";
|
|
11
12
|
import { computed, ref, toRef, watch } from "vue";
|
|
12
13
|
import "@vue-flow/core/dist/style.css";
|
|
13
14
|
import "@vue-flow/core/dist/theme-default.css";
|
|
@@ -210,7 +211,15 @@ function handleDeleteEdge(edgeId: string) {
|
|
|
210
211
|
@edge-context-menu="onEdgeContextMenu"
|
|
211
212
|
@edge-click="onEdgeClick"
|
|
212
213
|
@pane-click="onPaneClick"
|
|
213
|
-
|
|
214
|
+
>
|
|
215
|
+
<Background
|
|
216
|
+
id="bp-dots"
|
|
217
|
+
variant="dots"
|
|
218
|
+
:gap="18"
|
|
219
|
+
:size="1.25"
|
|
220
|
+
color="rgba(148, 163, 184, 0.45)"
|
|
221
|
+
/>
|
|
222
|
+
</VueFlow>
|
|
214
223
|
<BlueprintContextMenu
|
|
215
224
|
:menu="menu"
|
|
216
225
|
@close="menu = null"
|