@arronqzy/react-blueprint 1.0.18 → 1.0.21

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.
Files changed (43) hide show
  1. package/README.md +36 -26
  2. package/dist/blueprint.css +336 -54
  3. package/index.ts +23 -0
  4. package/package.json +5 -5
  5. package/src/BlueprintContextMenu.tsx +1 -1
  6. package/src/BlueprintNodeConfigSidebar.tsx +10 -2
  7. package/src/blueprint.css +336 -54
  8. package/src/components/ApiCollectionManagerDialog.tsx +448 -0
  9. package/src/components/BlueprintExecutionLogPanel.tsx +10 -2
  10. package/src/components/BlueprintLibrarySelect.tsx +1 -1
  11. package/src/components/BlueprintMetaDialog.tsx +1 -1
  12. package/src/components/BlueprintNodeSwitchTaskDialog.tsx +1 -1
  13. package/src/components/BlueprintPanelToolbar.tsx +10 -2
  14. package/src/components/ClockNodeConfigPanel.tsx +1 -1
  15. package/src/components/ConfigHintIcon.tsx +1 -1
  16. package/src/components/FetchNodeConfigPanel.tsx +314 -3
  17. package/src/components/FetchUrlAutocomplete.tsx +1 -1
  18. package/src/components/JsonLintTextarea.tsx +97 -0
  19. package/src/components/JsonNodeConfigPanel.tsx +14 -7
  20. package/src/components/LogicNodeConfigPanel.tsx +1 -1
  21. package/src/components/ScopeTemplateAutocompleteHost.tsx +4 -8
  22. package/src/components/StorageKeyAutocomplete.tsx +1 -1
  23. package/src/components/StorageNodeConfigPanel.tsx +1 -1
  24. package/src/components/ViewElementMultiSelect.tsx +1 -1
  25. package/src/edges/BlueprintSmoothEdge.tsx +2 -2
  26. package/src/graph/blueprint-graph.ts +9 -0
  27. package/src/graph/document.ts +12 -0
  28. package/src/graph/sync-edges.ts +2 -2
  29. package/src/hooks/useBlueprintFlowState.ts +1 -1
  30. package/src/index.tsx +41 -28
  31. package/src/library/api-collection-db.ts +98 -0
  32. package/src/library/api-collection-store.ts +139 -0
  33. package/src/nodes/AndFlowNode.tsx +4 -3
  34. package/src/nodes/BlueprintFlowNode.tsx +8 -3
  35. package/src/nodes/BlueprintNodeCard.tsx +137 -121
  36. package/src/nodes/BlueprintNodeShell.tsx +2 -1
  37. package/src/nodes/ClockFlowNode.tsx +1 -1
  38. package/src/nodes/EventFlowNode.tsx +1 -1
  39. package/src/nodes/FetchFlowNode.tsx +1 -1
  40. package/src/nodes/JsonFlowNode.tsx +1 -1
  41. package/src/nodes/LifecycleFlowNode.tsx +2 -1
  42. package/src/nodes/LogicFlowNode.tsx +1 -1
  43. package/src/nodes/StorageFlowNode.tsx +1 -1
@@ -7,6 +7,10 @@ import {
7
7
  SelectItem,
8
8
  SelectTrigger,
9
9
  SelectValue,
10
+ Tooltip,
11
+ TooltipContent,
12
+ TooltipProvider,
13
+ TooltipTrigger,
10
14
  cn,
11
15
  } from "@arronqzy/ui";
12
16
  import type {
@@ -14,8 +18,10 @@ import type {
14
18
  FetchHttpMethod,
15
19
  FetchRequestConfig,
16
20
  FetchResponseType,
21
+ FetchUrlInputMode,
17
22
  } from "@arronqzy/blueprint-dsl";
18
23
  import {
24
+ applyApiCollectionEndpointToFetchConfig,
19
25
  applyFetchConfigScope,
20
26
  draftFetchHeadersText,
21
27
  FETCH_CACHES,
@@ -45,7 +51,8 @@ import {
45
51
  useFetchDebugTask,
46
52
  useSwaggerLoadTask,
47
53
  } from "../fetch-config-task-store";
48
- import { useI18n } from "@arronqzy/i18n/react";
54
+ import { useApiCollections } from "../library/api-collection-store";
55
+ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
49
56
 
50
57
  import { ConfigFieldLabel, ConfigHintIcon, ConfigSectionTitle } from "./ConfigHintIcon";
51
58
  import { FetchUrlAutocomplete } from "./FetchUrlAutocomplete";
@@ -274,10 +281,22 @@ export function FetchNodeConfigPanel({
274
281
  }: FetchNodeConfigPanelProps) {
275
282
  const { t } = useI18n();
276
283
  const fetchConfig = resolveNodeFetchConfig(node);
284
+ const apiCollections = useApiCollections();
277
285
  const endpoints = fetchConfig.swaggerEndpoints ?? [];
278
286
  const hasSwaggerEndpoints = endpoints.length > 0;
279
- const urlInputMode =
287
+ const urlInputMode: FetchUrlInputMode =
280
288
  fetchConfig.urlInputMode ?? (hasSwaggerEndpoints ? "swagger" : "manual");
289
+ const usingCollection = urlInputMode === "collection";
290
+ const selectedCollection =
291
+ apiCollections.find((item) => item.id === fetchConfig.apiCollectionId) ??
292
+ null;
293
+ const collectionEndpoints = selectedCollection?.document.apis ?? [];
294
+ const selectedCollectionEndpoint =
295
+ collectionEndpoints.find(
296
+ (item) => item.name === fetchConfig.apiCollectionEndpointName
297
+ ) ?? null;
298
+ const selectedEndpointDescription =
299
+ selectedCollectionEndpoint?.description?.trim() || "";
281
300
  const swaggerTask = useSwaggerLoadTask(node.id);
282
301
  const fetchDebugTask = useFetchDebugTask(node.id);
283
302
  const loadingSwagger = swaggerTask.status === "loading";
@@ -352,12 +371,90 @@ export function FetchNodeConfigPanel({
352
371
  const [formEl, setFormEl] = useState<HTMLDivElement | null>(null);
353
372
 
354
373
  const setUrlInputMode = useCallback(
355
- (mode: "swagger" | "manual") => {
374
+ (mode: FetchUrlInputMode) => {
356
375
  onUpdateNode(node.id, patchFetchConfig(node, { urlInputMode: mode }));
357
376
  },
358
377
  [node, onUpdateNode]
359
378
  );
360
379
 
380
+ const setFetchSourceMode = useCallback(
381
+ (mode: "direct" | "collection") => {
382
+ if (mode === "collection") {
383
+ onUpdateNode(
384
+ node.id,
385
+ patchFetchConfig(node, { urlInputMode: "collection" })
386
+ );
387
+ return;
388
+ }
389
+ const nextMode: FetchUrlInputMode = hasSwaggerEndpoints ? "swagger" : "manual";
390
+ onUpdateNode(
391
+ node.id,
392
+ patchFetchConfig(node, {
393
+ urlInputMode: nextMode,
394
+ apiCollectionId: undefined,
395
+ apiCollectionEndpointName: undefined,
396
+ })
397
+ );
398
+ },
399
+ [hasSwaggerEndpoints, node, onUpdateNode]
400
+ );
401
+
402
+ const handleSelectCollection = useCallback(
403
+ (collectionId: string) => {
404
+ if (!collectionId) {
405
+ onUpdateNode(
406
+ node.id,
407
+ patchFetchConfig(node, {
408
+ apiCollectionId: undefined,
409
+ apiCollectionEndpointName: undefined,
410
+ urlInputMode: "collection",
411
+ })
412
+ );
413
+ return;
414
+ }
415
+ onUpdateNode(
416
+ node.id,
417
+ patchFetchConfig(node, {
418
+ apiCollectionId: collectionId,
419
+ apiCollectionEndpointName: undefined,
420
+ urlInputMode: "collection",
421
+ })
422
+ );
423
+ },
424
+ [node, onUpdateNode]
425
+ );
426
+
427
+ const handleSelectCollectionEndpoint = useCallback(
428
+ (endpointName: string) => {
429
+ if (!selectedCollection || !endpointName) {
430
+ onUpdateNode(
431
+ node.id,
432
+ patchFetchConfig(node, {
433
+ apiCollectionEndpointName: undefined,
434
+ urlInputMode: "collection",
435
+ })
436
+ );
437
+ return;
438
+ }
439
+ const endpoint = selectedCollection.document.apis.find(
440
+ (item) => item.name === endpointName
441
+ );
442
+ if (!endpoint) return;
443
+ onUpdateNode(
444
+ node.id,
445
+ patchFetchConfig(
446
+ node,
447
+ applyApiCollectionEndpointToFetchConfig({
448
+ collectionId: selectedCollection.id,
449
+ document: selectedCollection.document,
450
+ endpoint,
451
+ })
452
+ )
453
+ );
454
+ },
455
+ [node, onUpdateNode, selectedCollection]
456
+ );
457
+
361
458
  const handleLoadSwagger = useCallback(() => {
362
459
  const docsUrl = fetchConfig.swaggerDocsUrl?.trim();
363
460
  if (!docsUrl) {
@@ -454,6 +551,90 @@ export function FetchNodeConfigPanel({
454
551
  title={t("blueprint.config.fetchTitle")}
455
552
  hint={t("blueprint.config.fetchHint")}
456
553
  />
554
+
555
+ {fetchConfig.apiCollectionSyncNotice ? (
556
+ <div className="rounded-md border border-emerald-500/40 bg-emerald-500/10 p-2.5 text-[11px] text-foreground">
557
+ <div className="flex items-start justify-between gap-2">
558
+ <div className="space-y-1">
559
+ <p className="font-medium text-emerald-800 dark:text-emerald-200">
560
+ {t("blueprint.config.apiCollectionSyncUpdatedTitle")}
561
+ </p>
562
+ <p className="text-muted-foreground">
563
+ {t("blueprint.config.apiCollectionSyncUpdatedSource")}
564
+ </p>
565
+ <p>
566
+ {t("blueprint.config.apiCollectionSyncUpdatedCollection", {
567
+ collection: fetchConfig.apiCollectionSyncNotice.collectionName,
568
+ endpoint: fetchConfig.apiCollectionSyncNotice.endpointName,
569
+ })}
570
+ </p>
571
+ <p>
572
+ {fetchConfig.apiCollectionSyncNotice.changedFields.length > 0
573
+ ? t("blueprint.config.apiCollectionSyncUpdatedFields", {
574
+ fields: fetchConfig.apiCollectionSyncNotice.changedFields
575
+ .map((field) =>
576
+ t(`blueprint.config.apiCollectionSyncField.${field}`)
577
+ )
578
+ .join("、"),
579
+ })
580
+ : t("blueprint.config.apiCollectionSyncUpdatedNoFieldChange")}
581
+ </p>
582
+ </div>
583
+ <Button
584
+ type="button"
585
+ variant="outline"
586
+ size="sm"
587
+ className="h-7 shrink-0 px-2 text-[11px]"
588
+ onClick={() =>
589
+ onUpdateNode(
590
+ node.id,
591
+ patchFetchConfig(node, { apiCollectionSyncNotice: null })
592
+ )
593
+ }
594
+ >
595
+ {t("common.close")}
596
+ </Button>
597
+ </div>
598
+ </div>
599
+ ) : null}
600
+
601
+ <div className="space-y-1">
602
+ <span className="text-[11px] text-muted-foreground">
603
+ {t("blueprint.config.fetchSourceMode")}
604
+ </span>
605
+ <div className="flex rounded-md border border-border p-0.5">
606
+ <button
607
+ type="button"
608
+ className={cn(
609
+ "flex-1 rounded px-2 py-1 text-[11px] transition-colors",
610
+ !usingCollection
611
+ ? "bg-primary text-primary-foreground"
612
+ : "text-muted-foreground hover:text-foreground"
613
+ )}
614
+ onClick={() => setFetchSourceMode("direct")}
615
+ >
616
+ {t("blueprint.config.fetchSourceDirect")}
617
+ </button>
618
+ <button
619
+ type="button"
620
+ className={cn(
621
+ "flex-1 rounded px-2 py-1 text-[11px] transition-colors",
622
+ usingCollection
623
+ ? "bg-primary text-primary-foreground"
624
+ : "text-muted-foreground hover:text-foreground"
625
+ )}
626
+ onClick={() => setFetchSourceMode("collection")}
627
+ >
628
+ {t("blueprint.config.fetchSourceCollection")}
629
+ </button>
630
+ </div>
631
+ <p className="text-[10px] text-muted-foreground">
632
+ {usingCollection
633
+ ? t("blueprint.config.fetchSourceCollectionHint")
634
+ : t("blueprint.config.fetchSourceDirectHint")}
635
+ </p>
636
+ </div>
637
+
457
638
  <div className="space-y-1 rounded-md border border-border/60 bg-background/70 p-2">
458
639
  <div className="flex items-center gap-1.5 text-[11px] font-medium text-foreground">
459
640
  {t("blueprint.config.fetchScopeTitle")}
@@ -477,6 +658,133 @@ export function FetchNodeConfigPanel({
477
658
  )}
478
659
  </div>
479
660
 
661
+ {usingCollection ? (
662
+ <div className="space-y-2 rounded-md border border-border/60 bg-background/70 p-2">
663
+ <label className="block space-y-1">
664
+ <span className="text-muted-foreground">
665
+ {t("blueprint.config.apiCollectionSelect")}
666
+ </span>
667
+ <Select
668
+ value={fetchConfig.apiCollectionId || undefined}
669
+ onValueChange={handleSelectCollection}
670
+ >
671
+ <SelectTrigger className="h-8 text-[11px]">
672
+ <SelectValue
673
+ placeholder={t("blueprint.config.apiCollectionSelectPlaceholder")}
674
+ />
675
+ </SelectTrigger>
676
+ <SelectContent className="z-[10200]">
677
+ {apiCollections.length === 0 ? (
678
+ <SelectItem value="__empty" disabled>
679
+ {t("blueprint.config.apiCollectionEmpty")}
680
+ </SelectItem>
681
+ ) : (
682
+ apiCollections.map((item) => (
683
+ <SelectItem key={item.id} value={item.id}>
684
+ {item.name}
685
+ </SelectItem>
686
+ ))
687
+ )}
688
+ </SelectContent>
689
+ </Select>
690
+ </label>
691
+ <label className="block space-y-1">
692
+ <span className="text-muted-foreground">
693
+ {t("blueprint.config.apiCollectionEndpoint")}
694
+ </span>
695
+ <div className="flex gap-1.5">
696
+ <TooltipProvider delayDuration={200}>
697
+ <Tooltip>
698
+ <TooltipTrigger asChild>
699
+ <div className="min-w-0 flex-1">
700
+ <Select
701
+ value={fetchConfig.apiCollectionEndpointName || undefined}
702
+ onValueChange={handleSelectCollectionEndpoint}
703
+ disabled={!selectedCollection}
704
+ >
705
+ <SelectTrigger className="h-8 w-full text-[11px]">
706
+ <SelectValue
707
+ placeholder={t(
708
+ "blueprint.config.apiCollectionEndpointPlaceholder"
709
+ )}
710
+ />
711
+ </SelectTrigger>
712
+ <SelectContent className="z-[10200]">
713
+ {collectionEndpoints.length === 0 ? (
714
+ <SelectItem value="__empty" disabled>
715
+ {t("blueprint.config.apiCollectionNoEndpoints")}
716
+ </SelectItem>
717
+ ) : (
718
+ collectionEndpoints.map((item) => (
719
+ <SelectItem
720
+ key={item.name}
721
+ value={item.name}
722
+ title={item.description?.trim() || undefined}
723
+ >
724
+ {item.name}
725
+ </SelectItem>
726
+ ))
727
+ )}
728
+ </SelectContent>
729
+ </Select>
730
+ </div>
731
+ </TooltipTrigger>
732
+ {selectedEndpointDescription ? (
733
+ <TooltipContent
734
+ side="top"
735
+ className="z-[10300] max-w-sm whitespace-pre-wrap text-[11px]"
736
+ >
737
+ {selectedEndpointDescription}
738
+ </TooltipContent>
739
+ ) : null}
740
+ </Tooltip>
741
+ </TooltipProvider>
742
+ <AsyncSendButton
743
+ loading={loadingFetchDebug}
744
+ disabled={!fetchConfig.url?.trim()}
745
+ onSend={handleSendFetchDebug}
746
+ onAbort={handleAbortFetchDebug}
747
+ sendTitle={t("blueprint.config.sendDebugRequest")}
748
+ abortTitle={t("blueprint.config.abortRequest")}
749
+ sendAriaLabel={t("blueprint.config.sendDebugRequest")}
750
+ abortAriaLabel={t("blueprint.config.abortDebugAria")}
751
+ />
752
+ </div>
753
+ </label>
754
+ {selectedCollection ? (
755
+ <p className="text-[10px] text-muted-foreground">
756
+ {t("blueprint.config.apiCollectionFilledHint")}
757
+ </p>
758
+ ) : null}
759
+ <label className="block space-y-1">
760
+ <span className="text-muted-foreground">{t("blueprint.config.requestUrl")}</span>
761
+ <Input
762
+ value={fetchConfig.url}
763
+ disabled={loadingFetchDebug}
764
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
765
+ onUpdateNode(node.id, patchFetchConfig(node, { url: e.target.value }))
766
+ }
767
+ className="h-8 font-mono text-[11px]"
768
+ />
769
+ </label>
770
+ {usesScopeTemplate && resolvedUrlPreview ? (
771
+ <p className="break-all font-mono text-[10px] text-muted-foreground">
772
+ {t("blueprint.config.fetchScopeResolvedUrl")}: {resolvedUrlPreview}
773
+ </p>
774
+ ) : null}
775
+ {loadingFetchDebug ? (
776
+ <p className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
777
+ <LoaderIcon className="h-3.5 w-3.5 shrink-0" />
778
+ {t("blueprint.config.sendingDebug")}
779
+ </p>
780
+ ) : null}
781
+ {fetchValidationError ? (
782
+ <p className="text-[11px] text-destructive">{fetchValidationError}</p>
783
+ ) : null}
784
+ <FetchDebugResponsePanel task={fetchDebugTask} />
785
+ </div>
786
+ ) : (
787
+ <>
480
788
  <label className="block space-y-1">
481
789
  <span className="text-muted-foreground">{t("blueprint.config.swaggerUrlOptional")}</span>
482
790
  <div className="flex gap-1.5">
@@ -667,6 +975,9 @@ export function FetchNodeConfigPanel({
667
975
  <FetchDebugResponsePanel task={fetchDebugTask} />
668
976
  </label>
669
977
 
978
+ </>
979
+ )}
980
+
670
981
  <label className="block space-y-1">
671
982
  <span className="text-muted-foreground">{t("blueprint.config.requestMethod")}</span>
672
983
  <Select
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { cn } from "@arronqzy/ui";
3
- import { useI18n } from "@arronqzy/i18n/react";
3
+ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
4
4
  import {
5
5
  buildEndpointSuggestions,
6
6
  filterEndpointSuggestions,
@@ -0,0 +1,97 @@
1
+ import { useMemo, useRef, type ChangeEvent } from "react";
2
+ import {
3
+ renderJsonWithErrorHighlights,
4
+ type JsonErrorRange,
5
+ } from "@arronqzy/blueprint-dsl";
6
+ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
7
+ import { cn } from "@arronqzy/ui";
8
+
9
+ export function JsonLintTextarea({
10
+ value,
11
+ onChange,
12
+ ranges,
13
+ placeholder,
14
+ rows = 12,
15
+ className,
16
+ }: {
17
+ value: string;
18
+ onChange: (value: string) => void;
19
+ ranges: JsonErrorRange[];
20
+ placeholder?: string;
21
+ rows?: number;
22
+ className?: string;
23
+ }) {
24
+ const { t } = useI18n();
25
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
26
+
27
+ const highlightedHtml = useMemo(
28
+ () => renderJsonWithErrorHighlights(value, ranges),
29
+ [ranges, value]
30
+ );
31
+
32
+ const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
33
+ onChange(e.target.value);
34
+ };
35
+
36
+ const jumpToFirstError = () => {
37
+ const ta = textareaRef.current;
38
+ const first = ranges[0];
39
+ if (!ta || !first) return;
40
+ ta.focus();
41
+ const start = Math.max(0, first.start);
42
+ const end = Math.max(start, Math.min(value.length, first.end));
43
+ ta.setSelectionRange(start, end);
44
+ };
45
+
46
+ return (
47
+ <div className={cn("space-y-1.5", className)}>
48
+ <textarea
49
+ ref={textareaRef}
50
+ value={value}
51
+ onChange={handleChange}
52
+ rows={rows}
53
+ spellCheck={false}
54
+ placeholder={placeholder}
55
+ className={cn(
56
+ "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",
57
+ ranges.length > 0 && "border-destructive/60"
58
+ )}
59
+ />
60
+ {ranges.length > 0 ? (
61
+ <div className="space-y-1">
62
+ <div className="flex items-center justify-between gap-2">
63
+ <span className="text-[10px] text-destructive">
64
+ {t("blueprint.config.jsonErrorPreviewHint")}
65
+ </span>
66
+ <button
67
+ type="button"
68
+ className="shrink-0 text-[10px] text-destructive underline-offset-2 hover:underline"
69
+ onClick={jumpToFirstError}
70
+ >
71
+ {t("blueprint.config.jsonJumpToError")}
72
+ </button>
73
+ </div>
74
+ <pre
75
+ aria-hidden
76
+ className="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"
77
+ dangerouslySetInnerHTML={{ __html: highlightedHtml || "\n" }}
78
+ />
79
+ <style>{`
80
+ .json-lint-error {
81
+ margin: 0;
82
+ padding: 0;
83
+ border: 0;
84
+ font: inherit;
85
+ line-height: inherit;
86
+ color: hsl(var(--destructive));
87
+ background: color-mix(in oklab, hsl(var(--destructive)) 22%, transparent);
88
+ border-radius: 2px;
89
+ text-decoration: underline wavy hsl(var(--destructive));
90
+ text-underline-offset: 2px;
91
+ }
92
+ `}</style>
93
+ </div>
94
+ ) : null}
95
+ </div>
96
+ );
97
+ }
@@ -1,6 +1,7 @@
1
- import { useCallback, useMemo, useState, type ChangeEvent } from "react";
1
+ import { useCallback, useMemo, useState } from "react";
2
2
  import type {
3
3
  ExecutionTraceEntry,
4
+ JsonErrorRange,
4
5
  JsonNodeConfig,
5
6
  } from "@arronqzy/blueprint-dsl";
6
7
  import {
@@ -9,11 +10,12 @@ import {
9
10
  resolveFetchScopeAutocompleteRoot,
10
11
  validateJsonStringAllowingScope,
11
12
  } from "@arronqzy/blueprint-dsl";
12
- import { useI18n } from "@arronqzy/i18n/react";
13
+ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
13
14
 
14
15
  import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
15
16
  import { resolveNodeJsonConfig } from "../graph/document";
16
17
  import { ConfigHintIcon, ConfigSectionTitle } from "./ConfigHintIcon";
18
+ import { JsonLintTextarea } from "./JsonLintTextarea";
17
19
  import { ScopeTemplateAutocompleteHost } from "./ScopeTemplateAutocompleteHost";
18
20
 
19
21
  export type JsonNodeConfigPanelProps = {
@@ -48,6 +50,7 @@ export function JsonNodeConfigPanel({
48
50
  const [formEl, setFormEl] = useState<HTMLDivElement | null>(null);
49
51
  const jsonConfig = resolveNodeJsonConfig(node);
50
52
  const [draftError, setDraftError] = useState<string | null>(null);
53
+ const [draftRanges, setDraftRanges] = useState<JsonErrorRange[] | null>(null);
51
54
 
52
55
  const incomingScope = useMemo(() => {
53
56
  const outputs = latestTraceOutputsByNode(traceEntries);
@@ -68,16 +71,21 @@ export function JsonNodeConfigPanel({
68
71
  const parseError =
69
72
  draftError ??
70
73
  (storedValidation.ok ? null : storedValidation.error);
74
+ const errorRanges: JsonErrorRange[] =
75
+ draftRanges ??
76
+ (!storedValidation.ok && "ranges" in storedValidation
77
+ ? storedValidation.ranges
78
+ : []);
71
79
  const deferredScope =
72
80
  storedValidation.ok && "deferred" in storedValidation
73
81
  ? storedValidation.deferred === true
74
82
  : false;
75
83
 
76
84
  const handleChange = useCallback(
77
- (e: ChangeEvent<HTMLTextAreaElement>) => {
78
- const jsonString = e.target.value;
85
+ (jsonString: string) => {
79
86
  const result = validateJsonStringAllowingScope(jsonString);
80
87
  setDraftError(result.ok ? null : result.error);
88
+ setDraftRanges(result.ok ? null : result.ranges);
81
89
  onUpdateNode(node.id, patchJsonConfig(node, { jsonString }));
82
90
  },
83
91
  [node, onUpdateNode]
@@ -104,12 +112,11 @@ export function JsonNodeConfigPanel({
104
112
  {t("blueprint.config.jsonScopeHint")}
105
113
  </ConfigHintIcon>
106
114
  </span>
107
- <textarea
115
+ <JsonLintTextarea
108
116
  value={jsonConfig.jsonString}
109
117
  onChange={handleChange}
118
+ ranges={errorRanges}
110
119
  rows={12}
111
- spellCheck={false}
112
- className="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"
113
120
  placeholder={'{\n "key": "{scope?.value}"\n}'}
114
121
  />
115
122
  {parseError ? (
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useMemo, useState, type ChangeEvent } from "react";
2
2
  import { validateLogicSourceCode } from "@arronqzy/blueprint-dsl";
3
3
  import type { LogicNodeConfig } from "@arronqzy/blueprint-dsl";
4
- import { useI18n } from "@arronqzy/i18n/react";
4
+ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
5
5
 
6
6
  import type { BlueprintGraphNode } from "../graph/document";
7
7
  import { resolveNodeLogicConfig } from "../graph/document";
@@ -76,17 +76,13 @@ export function ScopeTemplateAutocompleteHost({
76
76
 
77
77
  const refreshForInput = useCallback(
78
78
  (input: HTMLInputElement | HTMLTextAreaElement) => {
79
- if (scope === undefined) {
80
- closeDropdown();
81
- return;
82
- }
83
79
  const cursor = input.selectionStart ?? input.value.length;
84
- const parsed = parseScopeAutocomplete(input.value, cursor, scope);
80
+ const parsed = parseScopeAutocomplete(input.value, cursor, scope ?? {});
85
81
  if (!parsed) {
86
82
  closeDropdown();
87
83
  return;
88
84
  }
89
- const suggestions = getScopeAutocompleteSuggestions(scope, parsed);
85
+ const suggestions = getScopeAutocompleteSuggestions(scope ?? {}, parsed);
90
86
  if (suggestions.length === 0) {
91
87
  closeDropdown();
92
88
  return;
@@ -126,7 +122,7 @@ export function ScopeTemplateAutocompleteHost({
126
122
  );
127
123
 
128
124
  useEffect(() => {
129
- if (!root || scope === undefined) return;
125
+ if (!root) return;
130
126
 
131
127
  const onFocusIn = (event: FocusEvent) => {
132
128
  if (!isAutocompleteTarget(event.target)) return;
@@ -209,7 +205,7 @@ export function ScopeTemplateAutocompleteHost({
209
205
  };
210
206
  }, [applySuggestion, closeDropdown, refreshForInput, root, scope]);
211
207
 
212
- if (!dropdown || scope === undefined) return null;
208
+ if (!dropdown) return null;
213
209
 
214
210
  return createPortal(
215
211
  <div
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { cn } from "@arronqzy/ui";
3
- import { useI18n } from "@arronqzy/i18n/react";
3
+ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
4
4
  import {
5
5
  filterStorageKeySuggestions,
6
6
  listBrowserStorageKeys,
@@ -20,7 +20,7 @@ import {
20
20
  resolveFetchScopeAutocompleteRoot,
21
21
  uniqueStorageKinds,
22
22
  } from "@arronqzy/blueprint-dsl";
23
- import { useI18n } from "@arronqzy/i18n/react";
23
+ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
24
24
 
25
25
  import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
26
26
  import { resolveNodeStorageConfig } from "../graph/document";
@@ -12,7 +12,7 @@ import {
12
12
  PopoverContent,
13
13
  PopoverTrigger,
14
14
  } from "@arronqzy/ui";
15
- import { useI18n } from "@arronqzy/i18n/react";
15
+ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
16
16
 
17
17
  export type ViewElementMultiSelectOption = {
18
18
  id: string;
@@ -1,7 +1,7 @@
1
1
  import { BaseEdge, getSmoothStepPath, type EdgeProps } from "@xyflow/react";
2
2
 
3
- const EDGE_STROKE = "#2563eb";
4
- const EDGE_WIDTH = 2.5;
3
+ const EDGE_STROKE = "#94a3b8";
4
+ const EDGE_WIDTH = 1.75;
5
5
 
6
6
  export function BlueprintSmoothEdge({
7
7
  sourceX,
@@ -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
@@ -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 {