@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
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
3
3
|
import { computed, ref } from "vue";
|
|
4
|
-
import { Button, Input, Select } from "ant-design-vue";
|
|
4
|
+
import { Button, Input, Select, Tooltip } from "ant-design-vue";
|
|
5
5
|
import type {
|
|
6
6
|
ExecutionTraceEntry,
|
|
7
7
|
FetchHttpMethod,
|
|
8
8
|
FetchRequestConfig,
|
|
9
9
|
FetchResponseType,
|
|
10
|
+
FetchUrlInputMode,
|
|
10
11
|
} from "@arronqzy/blueprint-dsl";
|
|
11
12
|
import {
|
|
13
|
+
applyApiCollectionEndpointToFetchConfig,
|
|
12
14
|
applyFetchConfigScope,
|
|
13
15
|
draftFetchHeadersText,
|
|
14
16
|
FETCH_CACHES,
|
|
@@ -38,6 +40,7 @@ import {
|
|
|
38
40
|
useFetchDebugTask,
|
|
39
41
|
useSwaggerLoadTask,
|
|
40
42
|
} from "../fetch-config-task-store";
|
|
43
|
+
import { useApiCollections } from "../library/api-collection-store";
|
|
41
44
|
import ConfigHintIcon from "./ConfigHintIcon.vue";
|
|
42
45
|
import FetchUrlAutocomplete from "./FetchUrlAutocomplete.vue";
|
|
43
46
|
import ScopeTemplateAutocompleteHost from "./ScopeTemplateAutocompleteHost.vue";
|
|
@@ -127,10 +130,28 @@ const autocompleteScope = computed(() =>
|
|
|
127
130
|
resolveFetchScopeAutocompleteRoot(incomingScope.value)
|
|
128
131
|
);
|
|
129
132
|
const formRef = ref<HTMLElement | null>(null);
|
|
133
|
+
const apiCollections = useApiCollections();
|
|
130
134
|
const endpoints = computed(() => fetchConfig.value.swaggerEndpoints ?? []);
|
|
131
135
|
const hasSwaggerEndpoints = computed(() => endpoints.value.length > 0);
|
|
132
136
|
const urlInputMode = computed(
|
|
133
|
-
() =>
|
|
137
|
+
(): FetchUrlInputMode =>
|
|
138
|
+
fetchConfig.value.urlInputMode ?? (hasSwaggerEndpoints.value ? "swagger" : "manual")
|
|
139
|
+
);
|
|
140
|
+
const usingCollection = computed(() => urlInputMode.value === "collection");
|
|
141
|
+
const selectedCollection = computed(
|
|
142
|
+
() =>
|
|
143
|
+
apiCollections.value.find((item) => item.id === fetchConfig.value.apiCollectionId) ??
|
|
144
|
+
null
|
|
145
|
+
);
|
|
146
|
+
const collectionEndpoints = computed(() => selectedCollection.value?.document.apis ?? []);
|
|
147
|
+
const selectedCollectionEndpoint = computed(
|
|
148
|
+
() =>
|
|
149
|
+
collectionEndpoints.value.find(
|
|
150
|
+
(item) => item.name === fetchConfig.value.apiCollectionEndpointName
|
|
151
|
+
) ?? null
|
|
152
|
+
);
|
|
153
|
+
const selectedEndpointDescription = computed(
|
|
154
|
+
() => selectedCollectionEndpoint.value?.description?.trim() || ""
|
|
134
155
|
);
|
|
135
156
|
|
|
136
157
|
const swaggerTask = useSwaggerLoadTask(props.node.id);
|
|
@@ -151,10 +172,85 @@ function patchFetchConfig(node: BlueprintGraphNode, patch: Partial<FetchRequestC
|
|
|
151
172
|
};
|
|
152
173
|
}
|
|
153
174
|
|
|
154
|
-
function
|
|
175
|
+
function dismissSyncNotice() {
|
|
176
|
+
props.onUpdateNode(
|
|
177
|
+
props.node.id,
|
|
178
|
+
patchFetchConfig(props.node, { apiCollectionSyncNotice: null })
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function setUrlInputMode(mode: FetchUrlInputMode) {
|
|
155
183
|
props.onUpdateNode(props.node.id, patchFetchConfig(props.node, { urlInputMode: mode }));
|
|
156
184
|
}
|
|
157
185
|
|
|
186
|
+
function setFetchSourceMode(mode: "direct" | "collection") {
|
|
187
|
+
if (mode === "collection") {
|
|
188
|
+
props.onUpdateNode(
|
|
189
|
+
props.node.id,
|
|
190
|
+
patchFetchConfig(props.node, { urlInputMode: "collection" })
|
|
191
|
+
);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const nextMode: FetchUrlInputMode = hasSwaggerEndpoints.value ? "swagger" : "manual";
|
|
195
|
+
props.onUpdateNode(
|
|
196
|
+
props.node.id,
|
|
197
|
+
patchFetchConfig(props.node, {
|
|
198
|
+
urlInputMode: nextMode,
|
|
199
|
+
apiCollectionId: undefined,
|
|
200
|
+
apiCollectionEndpointName: undefined,
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function handleSelectCollection(collectionId: string) {
|
|
206
|
+
if (!collectionId) {
|
|
207
|
+
props.onUpdateNode(
|
|
208
|
+
props.node.id,
|
|
209
|
+
patchFetchConfig(props.node, {
|
|
210
|
+
apiCollectionId: undefined,
|
|
211
|
+
apiCollectionEndpointName: undefined,
|
|
212
|
+
urlInputMode: "collection",
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
props.onUpdateNode(
|
|
218
|
+
props.node.id,
|
|
219
|
+
patchFetchConfig(props.node, {
|
|
220
|
+
apiCollectionId: collectionId,
|
|
221
|
+
apiCollectionEndpointName: undefined,
|
|
222
|
+
urlInputMode: "collection",
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function handleSelectCollectionEndpoint(endpointName: string) {
|
|
228
|
+
const collection = selectedCollection.value;
|
|
229
|
+
if (!collection || !endpointName) {
|
|
230
|
+
props.onUpdateNode(
|
|
231
|
+
props.node.id,
|
|
232
|
+
patchFetchConfig(props.node, {
|
|
233
|
+
apiCollectionEndpointName: undefined,
|
|
234
|
+
urlInputMode: "collection",
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const endpoint = collection.document.apis.find((item) => item.name === endpointName);
|
|
240
|
+
if (!endpoint) return;
|
|
241
|
+
props.onUpdateNode(
|
|
242
|
+
props.node.id,
|
|
243
|
+
patchFetchConfig(
|
|
244
|
+
props.node,
|
|
245
|
+
applyApiCollectionEndpointToFetchConfig({
|
|
246
|
+
collectionId: collection.id,
|
|
247
|
+
document: collection.document,
|
|
248
|
+
endpoint,
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
158
254
|
function handleLoadSwagger() {
|
|
159
255
|
const docsUrl = fetchConfig.value.swaggerDocsUrl?.trim();
|
|
160
256
|
if (!docsUrl) {
|
|
@@ -275,6 +371,84 @@ function handleHeadersInput(event: Event) {
|
|
|
275
371
|
{{ t("blueprint.config.fetchHint") }}
|
|
276
372
|
</ConfigHintIcon>
|
|
277
373
|
</div>
|
|
374
|
+
|
|
375
|
+
<div
|
|
376
|
+
v-if="fetchConfig.apiCollectionSyncNotice"
|
|
377
|
+
class="rounded-md border border-emerald-500/40 bg-emerald-500/10 p-2.5 text-[11px] text-foreground"
|
|
378
|
+
>
|
|
379
|
+
<div class="flex items-start justify-between gap-2">
|
|
380
|
+
<div class="space-y-1">
|
|
381
|
+
<p class="font-medium text-emerald-800">
|
|
382
|
+
{{ t("blueprint.config.apiCollectionSyncUpdatedTitle") }}
|
|
383
|
+
</p>
|
|
384
|
+
<p class="text-muted-foreground">
|
|
385
|
+
{{ t("blueprint.config.apiCollectionSyncUpdatedSource") }}
|
|
386
|
+
</p>
|
|
387
|
+
<p>
|
|
388
|
+
{{
|
|
389
|
+
t("blueprint.config.apiCollectionSyncUpdatedCollection", {
|
|
390
|
+
collection: fetchConfig.apiCollectionSyncNotice.collectionName,
|
|
391
|
+
endpoint: fetchConfig.apiCollectionSyncNotice.endpointName,
|
|
392
|
+
})
|
|
393
|
+
}}
|
|
394
|
+
</p>
|
|
395
|
+
<p>
|
|
396
|
+
{{
|
|
397
|
+
fetchConfig.apiCollectionSyncNotice.changedFields.length > 0
|
|
398
|
+
? t("blueprint.config.apiCollectionSyncUpdatedFields", {
|
|
399
|
+
fields: fetchConfig.apiCollectionSyncNotice.changedFields
|
|
400
|
+
.map((field) => t(`blueprint.config.apiCollectionSyncField.${field}`))
|
|
401
|
+
.join("、"),
|
|
402
|
+
})
|
|
403
|
+
: t("blueprint.config.apiCollectionSyncUpdatedNoFieldChange")
|
|
404
|
+
}}
|
|
405
|
+
</p>
|
|
406
|
+
</div>
|
|
407
|
+
<Button size="small" @click="dismissSyncNotice">
|
|
408
|
+
{{ t("common.close") }}
|
|
409
|
+
</Button>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<div class="space-y-1">
|
|
414
|
+
<span class="text-[11px] text-muted-foreground">
|
|
415
|
+
{{ t("blueprint.config.fetchSourceMode") }}
|
|
416
|
+
</span>
|
|
417
|
+
<div class="flex rounded-md border border-border p-0.5">
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
class="flex-1 rounded px-2 py-1 text-[11px] transition-colors"
|
|
421
|
+
:class="
|
|
422
|
+
!usingCollection
|
|
423
|
+
? 'bg-primary text-primary-foreground'
|
|
424
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
425
|
+
"
|
|
426
|
+
@click="setFetchSourceMode('direct')"
|
|
427
|
+
>
|
|
428
|
+
{{ t("blueprint.config.fetchSourceDirect") }}
|
|
429
|
+
</button>
|
|
430
|
+
<button
|
|
431
|
+
type="button"
|
|
432
|
+
class="flex-1 rounded px-2 py-1 text-[11px] transition-colors"
|
|
433
|
+
:class="
|
|
434
|
+
usingCollection
|
|
435
|
+
? 'bg-primary text-primary-foreground'
|
|
436
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
437
|
+
"
|
|
438
|
+
@click="setFetchSourceMode('collection')"
|
|
439
|
+
>
|
|
440
|
+
{{ t("blueprint.config.fetchSourceCollection") }}
|
|
441
|
+
</button>
|
|
442
|
+
</div>
|
|
443
|
+
<p class="text-[10px] text-muted-foreground">
|
|
444
|
+
{{
|
|
445
|
+
usingCollection
|
|
446
|
+
? t("blueprint.config.fetchSourceCollectionHint")
|
|
447
|
+
: t("blueprint.config.fetchSourceDirectHint")
|
|
448
|
+
}}
|
|
449
|
+
</p>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
278
452
|
<div class="space-y-1 rounded-md border border-gray-200/80 bg-white/80 p-2">
|
|
279
453
|
<div class="flex items-center gap-1.5 text-[11px] font-medium text-gray-800">
|
|
280
454
|
{{ t("blueprint.config.fetchScopeTitle") }}
|
|
@@ -292,6 +466,163 @@ function handleHeadersInput(event: Event) {
|
|
|
292
466
|
</template>
|
|
293
467
|
</div>
|
|
294
468
|
|
|
469
|
+
<template v-if="usingCollection">
|
|
470
|
+
<div class="space-y-2 rounded-md border border-border/60 bg-background/70 p-2">
|
|
471
|
+
<label class="block space-y-1">
|
|
472
|
+
<span class="text-muted-foreground">{{ t("blueprint.config.apiCollectionSelect") }}</span>
|
|
473
|
+
<Select
|
|
474
|
+
size="small"
|
|
475
|
+
class="w-full"
|
|
476
|
+
:value="fetchConfig.apiCollectionId || undefined"
|
|
477
|
+
:placeholder="t('blueprint.config.apiCollectionSelectPlaceholder')"
|
|
478
|
+
allow-clear
|
|
479
|
+
@change="(v) => handleSelectCollection(v == null ? '' : String(v))"
|
|
480
|
+
>
|
|
481
|
+
<Select.Option v-if="apiCollections.length === 0" value="__empty" disabled>
|
|
482
|
+
{{ t("blueprint.config.apiCollectionEmpty") }}
|
|
483
|
+
</Select.Option>
|
|
484
|
+
<Select.Option
|
|
485
|
+
v-for="item in apiCollections"
|
|
486
|
+
:key="item.id"
|
|
487
|
+
:value="item.id"
|
|
488
|
+
>
|
|
489
|
+
{{ item.name }}
|
|
490
|
+
</Select.Option>
|
|
491
|
+
</Select>
|
|
492
|
+
</label>
|
|
493
|
+
<label class="block space-y-1">
|
|
494
|
+
<span class="text-muted-foreground">{{ t("blueprint.config.apiCollectionEndpoint") }}</span>
|
|
495
|
+
<div class="flex gap-1.5">
|
|
496
|
+
<Tooltip
|
|
497
|
+
:title="selectedEndpointDescription || undefined"
|
|
498
|
+
:mouse-enter-delay="0.2"
|
|
499
|
+
placement="top"
|
|
500
|
+
overlay-class-name="max-w-sm whitespace-pre-wrap text-[11px]"
|
|
501
|
+
>
|
|
502
|
+
<div class="min-w-0 flex-1">
|
|
503
|
+
<Select
|
|
504
|
+
size="small"
|
|
505
|
+
class="w-full"
|
|
506
|
+
:value="fetchConfig.apiCollectionEndpointName || undefined"
|
|
507
|
+
:placeholder="t('blueprint.config.apiCollectionEndpointPlaceholder')"
|
|
508
|
+
:disabled="!selectedCollection"
|
|
509
|
+
allow-clear
|
|
510
|
+
@change="(v) => handleSelectCollectionEndpoint(v == null ? '' : String(v))"
|
|
511
|
+
>
|
|
512
|
+
<Select.Option v-if="collectionEndpoints.length === 0" value="__empty" disabled>
|
|
513
|
+
{{ t("blueprint.config.apiCollectionNoEndpoints") }}
|
|
514
|
+
</Select.Option>
|
|
515
|
+
<Select.Option
|
|
516
|
+
v-for="item in collectionEndpoints"
|
|
517
|
+
:key="item.name"
|
|
518
|
+
:value="item.name"
|
|
519
|
+
:title="item.description?.trim() || undefined"
|
|
520
|
+
>
|
|
521
|
+
{{ item.name }}
|
|
522
|
+
</Select.Option>
|
|
523
|
+
</Select>
|
|
524
|
+
</div>
|
|
525
|
+
</Tooltip>
|
|
526
|
+
<Button
|
|
527
|
+
v-if="loadingFetchDebug"
|
|
528
|
+
size="small"
|
|
529
|
+
class="h-8 w-8 shrink-0"
|
|
530
|
+
:title="t('blueprint.config.abortRequest')"
|
|
531
|
+
@click="handleAbortFetchDebug"
|
|
532
|
+
>
|
|
533
|
+
■
|
|
534
|
+
</Button>
|
|
535
|
+
<Button
|
|
536
|
+
v-else
|
|
537
|
+
size="small"
|
|
538
|
+
class="h-8 w-8 shrink-0"
|
|
539
|
+
:disabled="!fetchConfig.url?.trim()"
|
|
540
|
+
:title="t('blueprint.config.sendDebugRequest')"
|
|
541
|
+
@click="handleSendFetchDebug"
|
|
542
|
+
>
|
|
543
|
+
➤
|
|
544
|
+
</Button>
|
|
545
|
+
</div>
|
|
546
|
+
</label>
|
|
547
|
+
<p v-if="selectedCollection" class="text-[10px] text-muted-foreground">
|
|
548
|
+
{{ t("blueprint.config.apiCollectionFilledHint") }}
|
|
549
|
+
</p>
|
|
550
|
+
<label class="block space-y-1">
|
|
551
|
+
<span class="text-muted-foreground">{{ t("blueprint.config.requestUrl") }}</span>
|
|
552
|
+
<Input
|
|
553
|
+
size="small"
|
|
554
|
+
:value="fetchConfig.url"
|
|
555
|
+
:disabled="loadingFetchDebug"
|
|
556
|
+
class="font-mono text-[11px]"
|
|
557
|
+
@update:value="(v) => onUpdateNode(node.id, patchFetchConfig(node, { url: String(v) }))"
|
|
558
|
+
/>
|
|
559
|
+
</label>
|
|
560
|
+
<p
|
|
561
|
+
v-if="usesScopeTemplate && resolvedUrlPreview"
|
|
562
|
+
class="break-all font-mono text-[10px] text-gray-500"
|
|
563
|
+
>
|
|
564
|
+
{{ t("blueprint.config.fetchScopeResolvedUrl") }}: {{ resolvedUrlPreview }}
|
|
565
|
+
</p>
|
|
566
|
+
<p v-if="loadingFetchDebug" class="text-[11px] text-muted-foreground">
|
|
567
|
+
{{ t("blueprint.config.sendingDebug") }}
|
|
568
|
+
</p>
|
|
569
|
+
<p v-if="fetchValidationError" class="text-[11px] text-destructive">
|
|
570
|
+
{{ fetchValidationError }}
|
|
571
|
+
</p>
|
|
572
|
+
<div
|
|
573
|
+
v-if="fetchDebugTask.status === 'error'"
|
|
574
|
+
class="rounded-md border border-destructive/40 bg-destructive/5 p-2"
|
|
575
|
+
>
|
|
576
|
+
<p class="text-[11px] text-destructive">
|
|
577
|
+
{{ fetchDebugTask.error ?? t("blueprint.config.requestFailed") }}
|
|
578
|
+
</p>
|
|
579
|
+
</div>
|
|
580
|
+
<div
|
|
581
|
+
v-else-if="fetchDebugTask.status === 'success' && fetchDebugTask.result"
|
|
582
|
+
class="rounded-md border border-border/70 bg-background/80"
|
|
583
|
+
>
|
|
584
|
+
<button
|
|
585
|
+
type="button"
|
|
586
|
+
class="flex w-full items-center gap-2 px-2 py-1.5 text-left"
|
|
587
|
+
@click="debugExpanded = !debugExpanded"
|
|
588
|
+
>
|
|
589
|
+
<span
|
|
590
|
+
class="text-muted-foreground transition-transform"
|
|
591
|
+
:class="debugExpanded && 'rotate-180'"
|
|
592
|
+
>
|
|
593
|
+
▼
|
|
594
|
+
</span>
|
|
595
|
+
<span class="text-[11px] font-medium text-foreground">{{ t("blueprint.config.debugResponse") }}</span>
|
|
596
|
+
<span
|
|
597
|
+
class="rounded px-1.5 py-0.5 font-mono text-[10px]"
|
|
598
|
+
:class="
|
|
599
|
+
debugOk
|
|
600
|
+
? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300'
|
|
601
|
+
: 'bg-destructive/15 text-destructive'
|
|
602
|
+
"
|
|
603
|
+
>
|
|
604
|
+
{{ fetchDebugTask.result.status }} {{ fetchDebugTask.result.statusText }}
|
|
605
|
+
</span>
|
|
606
|
+
<span
|
|
607
|
+
v-if="!debugExpanded"
|
|
608
|
+
class="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground"
|
|
609
|
+
>
|
|
610
|
+
{{ debugPreview }}
|
|
611
|
+
</span>
|
|
612
|
+
</button>
|
|
613
|
+
<div v-if="debugExpanded" class="border-t border-border/60 px-2 py-2">
|
|
614
|
+
<div class="mb-1 truncate font-mono text-[10px] text-muted-foreground">
|
|
615
|
+
{{ fetchDebugTask.result.url }}
|
|
616
|
+
</div>
|
|
617
|
+
<pre
|
|
618
|
+
class="max-h-56 overflow-auto whitespace-pre-wrap break-all rounded bg-muted/30 p-2 font-mono text-[10px] leading-relaxed text-foreground"
|
|
619
|
+
>{{ debugBodyText }}</pre>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
</template>
|
|
624
|
+
|
|
625
|
+
<template v-else>
|
|
295
626
|
<label class="block space-y-1">
|
|
296
627
|
<span class="text-muted-foreground">{{ t("blueprint.config.swaggerUrlOptional") }}</span>
|
|
297
628
|
<div class="flex gap-1.5">
|
|
@@ -527,6 +858,7 @@ function handleHeadersInput(event: Event) {
|
|
|
527
858
|
</div>
|
|
528
859
|
</div>
|
|
529
860
|
</label>
|
|
861
|
+
</template>
|
|
530
862
|
|
|
531
863
|
<label class="block space-y-1">
|
|
532
864
|
<span class="text-muted-foreground">{{ t("blueprint.config.requestMethod") }}</span>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { useI18n } from "@arronqzy/i18n/vue";
|
|
4
|
+
import {
|
|
5
|
+
renderJsonWithErrorHighlights,
|
|
6
|
+
type JsonErrorRange,
|
|
7
|
+
} from "@arronqzy/blueprint-dsl";
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(
|
|
10
|
+
defineProps<{
|
|
11
|
+
modelValue: string;
|
|
12
|
+
ranges?: JsonErrorRange[];
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
rows?: number;
|
|
15
|
+
class?: string;
|
|
16
|
+
}>(),
|
|
17
|
+
{
|
|
18
|
+
ranges: () => [],
|
|
19
|
+
rows: 12,
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<{
|
|
24
|
+
"update:modelValue": [value: string];
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const { t } = useI18n();
|
|
28
|
+
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
|
29
|
+
|
|
30
|
+
const highlightedHtml = computed(() =>
|
|
31
|
+
renderJsonWithErrorHighlights(props.modelValue, props.ranges)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const rootClass = computed(() =>
|
|
35
|
+
["space-y-1.5", props.class].filter(Boolean).join(" ")
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const textareaClass = computed(() =>
|
|
39
|
+
[
|
|
40
|
+
"w-full resize-y 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",
|
|
41
|
+
props.ranges.length > 0 ? "border-destructive/60" : "",
|
|
42
|
+
]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(" ")
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function onInput(e: Event) {
|
|
48
|
+
emit("update:modelValue", (e.target as HTMLTextAreaElement).value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function jumpToFirstError() {
|
|
52
|
+
const ta = textareaRef.value;
|
|
53
|
+
const first = props.ranges[0];
|
|
54
|
+
if (!ta || !first) return;
|
|
55
|
+
ta.focus();
|
|
56
|
+
const start = Math.max(0, first.start);
|
|
57
|
+
const end = Math.max(start, Math.min(props.modelValue.length, first.end));
|
|
58
|
+
ta.setSelectionRange(start, end);
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<div :class="rootClass">
|
|
64
|
+
<textarea
|
|
65
|
+
ref="textareaRef"
|
|
66
|
+
:value="modelValue"
|
|
67
|
+
:rows="rows"
|
|
68
|
+
spellcheck="false"
|
|
69
|
+
:placeholder="placeholder"
|
|
70
|
+
:class="textareaClass"
|
|
71
|
+
@input="onInput"
|
|
72
|
+
/>
|
|
73
|
+
<div v-if="ranges.length > 0" class="space-y-1">
|
|
74
|
+
<div class="flex items-center justify-between gap-2">
|
|
75
|
+
<span class="text-[10px] text-destructive">
|
|
76
|
+
{{ t("blueprint.config.jsonErrorPreviewHint") }}
|
|
77
|
+
</span>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="shrink-0 text-[10px] text-destructive underline-offset-2 hover:underline"
|
|
81
|
+
@click="jumpToFirstError"
|
|
82
|
+
>
|
|
83
|
+
{{ t("blueprint.config.jsonJumpToError") }}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
<pre
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
class="max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-md border border-destructive/30 bg-destructive/5 px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground"
|
|
89
|
+
v-html="highlightedHtml || '\n'"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
:deep(.json-lint-error) {
|
|
97
|
+
margin: 0;
|
|
98
|
+
padding: 0;
|
|
99
|
+
border: 0;
|
|
100
|
+
font: inherit;
|
|
101
|
+
line-height: inherit;
|
|
102
|
+
color: hsl(var(--destructive, 0 84% 60%));
|
|
103
|
+
background: color-mix(
|
|
104
|
+
in oklab,
|
|
105
|
+
hsl(var(--destructive, 0 84% 60%)) 22%,
|
|
106
|
+
transparent
|
|
107
|
+
);
|
|
108
|
+
border-radius: 2px;
|
|
109
|
+
text-decoration: underline wavy hsl(var(--destructive, 0 84% 60%));
|
|
110
|
+
text-underline-offset: 2px;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -1,26 +1,46 @@
|
|
|
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
|
+
JsonErrorRange,
|
|
7
|
+
JsonNodeConfig,
|
|
8
|
+
} from "@arronqzy/blueprint-dsl";
|
|
9
|
+
import {
|
|
10
|
+
latestTraceOutputsByNode,
|
|
11
|
+
resolveFetchIncomingScope,
|
|
12
|
+
resolveFetchScopeAutocompleteRoot,
|
|
13
|
+
validateJsonStringAllowingScope,
|
|
14
|
+
} from "@arronqzy/blueprint-dsl";
|
|
6
15
|
|
|
7
16
|
import ConfigHintIcon from "./ConfigHintIcon.vue";
|
|
8
|
-
import
|
|
17
|
+
import JsonLintTextarea from "./JsonLintTextarea.vue";
|
|
18
|
+
import ScopeTemplateAutocompleteHost from "./ScopeTemplateAutocompleteHost.vue";
|
|
19
|
+
import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
|
|
9
20
|
import { resolveNodeJsonConfig } from "../graph/document";
|
|
10
21
|
|
|
11
22
|
const { t } = useI18n();
|
|
12
23
|
|
|
13
24
|
export type JsonNodeConfigPanelProps = {
|
|
14
25
|
node: BlueprintGraphNode;
|
|
26
|
+
graphNodes?: BlueprintGraphNode[];
|
|
27
|
+
graphEdges?: BlueprintGraphEdge[];
|
|
28
|
+
traceEntries?: ExecutionTraceEntry[];
|
|
15
29
|
onUpdateNode: (
|
|
16
30
|
nodeId: string,
|
|
17
31
|
patch: Partial<Pick<BlueprintGraphNode, "jsonConfig" | "configSource">>
|
|
18
32
|
) => void;
|
|
19
33
|
};
|
|
20
34
|
|
|
21
|
-
const props = defineProps<JsonNodeConfigPanelProps>()
|
|
35
|
+
const props = withDefaults(defineProps<JsonNodeConfigPanelProps>(), {
|
|
36
|
+
graphNodes: () => [],
|
|
37
|
+
graphEdges: () => [],
|
|
38
|
+
traceEntries: () => [],
|
|
39
|
+
});
|
|
22
40
|
|
|
41
|
+
const formRef = ref<HTMLDivElement | null>(null);
|
|
23
42
|
const draftError = ref<string | null>(null);
|
|
43
|
+
const draftRanges = ref<JsonErrorRange[] | null>(null);
|
|
24
44
|
|
|
25
45
|
function patchJsonConfig(node: BlueprintGraphNode, patch: Partial<JsonNodeConfig>) {
|
|
26
46
|
return {
|
|
@@ -31,24 +51,58 @@ function patchJsonConfig(node: BlueprintGraphNode, patch: Partial<JsonNodeConfig
|
|
|
31
51
|
|
|
32
52
|
const jsonConfig = computed(() => resolveNodeJsonConfig(props.node));
|
|
33
53
|
|
|
54
|
+
const incomingScope = computed(() => {
|
|
55
|
+
const outputs = latestTraceOutputsByNode(props.traceEntries);
|
|
56
|
+
return resolveFetchIncomingScope({
|
|
57
|
+
fetchNodeId: props.node.id,
|
|
58
|
+
nodes: props.graphNodes,
|
|
59
|
+
edges: props.graphEdges,
|
|
60
|
+
getOutput: (sourceId, port) => outputs[sourceId]?.[port],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const autocompleteScope = computed(() =>
|
|
64
|
+
resolveFetchScopeAutocompleteRoot(incomingScope.value)
|
|
65
|
+
);
|
|
66
|
+
|
|
34
67
|
const storedValidation = computed(() =>
|
|
35
|
-
|
|
68
|
+
validateJsonStringAllowingScope(jsonConfig.value.jsonString)
|
|
36
69
|
);
|
|
37
70
|
|
|
38
71
|
const parseError = computed(
|
|
39
|
-
() =>
|
|
72
|
+
() =>
|
|
73
|
+
draftError.value ??
|
|
74
|
+
(storedValidation.value.ok ? null : storedValidation.value.error)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const errorRanges = computed<JsonErrorRange[]>(() => {
|
|
78
|
+
if (draftRanges.value) return draftRanges.value;
|
|
79
|
+
if (!storedValidation.value.ok && "ranges" in storedValidation.value) {
|
|
80
|
+
return storedValidation.value.ranges;
|
|
81
|
+
}
|
|
82
|
+
return [];
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const deferredScope = computed(
|
|
86
|
+
() =>
|
|
87
|
+
storedValidation.value.ok &&
|
|
88
|
+
"deferred" in storedValidation.value &&
|
|
89
|
+
storedValidation.value.deferred === true
|
|
40
90
|
);
|
|
41
91
|
|
|
42
|
-
function handleChange(
|
|
43
|
-
const
|
|
44
|
-
const result = validateJsonString(jsonString);
|
|
92
|
+
function handleChange(jsonString: string) {
|
|
93
|
+
const result = validateJsonStringAllowingScope(jsonString);
|
|
45
94
|
draftError.value = result.ok ? null : result.error;
|
|
95
|
+
draftRanges.value = result.ok ? null : result.ranges;
|
|
46
96
|
props.onUpdateNode(props.node.id, patchJsonConfig(props.node, { jsonString }));
|
|
47
97
|
}
|
|
48
98
|
</script>
|
|
49
99
|
|
|
50
100
|
<template>
|
|
51
|
-
<div
|
|
101
|
+
<div
|
|
102
|
+
ref="formRef"
|
|
103
|
+
class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5"
|
|
104
|
+
>
|
|
105
|
+
<ScopeTemplateAutocompleteHost :scope="autocompleteScope" :container-ref="formRef" />
|
|
52
106
|
<div class="flex items-center gap-1.5">
|
|
53
107
|
<div class="font-medium text-foreground">{{ t("blueprint.config.jsonTitle") }}</div>
|
|
54
108
|
<ConfigHintIcon :label="t('blueprint.config.jsonTitle')">
|
|
@@ -57,20 +111,27 @@ function handleChange(event: Event) {
|
|
|
57
111
|
</div>
|
|
58
112
|
|
|
59
113
|
<label class="block space-y-1">
|
|
60
|
-
<span class="text-muted-foreground">
|
|
61
|
-
|
|
62
|
-
:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
114
|
+
<span class="inline-flex items-center gap-1 text-muted-foreground">
|
|
115
|
+
{{ t("blueprint.config.jsonContent") }}
|
|
116
|
+
<ConfigHintIcon :label="t('blueprint.config.jsonContent')">
|
|
117
|
+
{{ t("blueprint.config.jsonScopeHint") }}
|
|
118
|
+
</ConfigHintIcon>
|
|
119
|
+
</span>
|
|
120
|
+
<JsonLintTextarea
|
|
121
|
+
:model-value="jsonConfig.jsonString"
|
|
122
|
+
:ranges="errorRanges"
|
|
123
|
+
:rows="12"
|
|
66
124
|
placeholder='{
|
|
67
|
-
"key": "value"
|
|
125
|
+
"key": "{scope?.value}"
|
|
68
126
|
}'
|
|
69
|
-
@
|
|
127
|
+
@update:model-value="handleChange"
|
|
70
128
|
/>
|
|
71
129
|
<p v-if="parseError" class="text-[11px] text-destructive">
|
|
72
130
|
{{ t("blueprint.config.jsonFormatError", { error: parseError }) }}
|
|
73
131
|
</p>
|
|
132
|
+
<p v-else-if="deferredScope" class="text-[11px] text-muted-foreground">
|
|
133
|
+
{{ t("blueprint.config.jsonScopeDeferred") }}
|
|
134
|
+
</p>
|
|
74
135
|
<p v-else class="text-[11px] text-muted-foreground">{{ t("blueprint.config.jsonFormatOk") }}</p>
|
|
75
136
|
</label>
|
|
76
137
|
</div>
|