@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.
@@ -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
- () => fetchConfig.value.urlInputMode ?? (hasSwaggerEndpoints.value ? "swagger" : "manual")
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 setUrlInputMode(mode: "swagger" | "manual") {
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 { validateJsonString } from "@arronqzy/blueprint-dsl";
5
- import type { JsonNodeConfig } from "@arronqzy/blueprint-dsl";
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 type { BlueprintGraphNode } from "../graph/document";
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
- validateJsonString(jsonConfig.value.jsonString)
68
+ validateJsonStringAllowingScope(jsonConfig.value.jsonString)
36
69
  );
37
70
 
38
71
  const parseError = computed(
39
- () => draftError.value ?? (storedValidation.value.ok ? null : storedValidation.value.error)
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(event: Event) {
43
- const jsonString = (event.target as HTMLTextAreaElement).value;
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 class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
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">{{ t("blueprint.config.jsonContent") }}</span>
61
- <textarea
62
- :value="jsonConfig.jsonString"
63
- rows="12"
64
- spellcheck="false"
65
- 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"
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
- @input="handleChange"
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>