@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.
- package/README.md +36 -26
- package/dist/blueprint.css +336 -54
- package/index.ts +23 -0
- package/package.json +5 -5
- package/src/BlueprintContextMenu.tsx +1 -1
- package/src/BlueprintNodeConfigSidebar.tsx +10 -2
- package/src/blueprint.css +336 -54
- package/src/components/ApiCollectionManagerDialog.tsx +448 -0
- package/src/components/BlueprintExecutionLogPanel.tsx +10 -2
- package/src/components/BlueprintLibrarySelect.tsx +1 -1
- package/src/components/BlueprintMetaDialog.tsx +1 -1
- package/src/components/BlueprintNodeSwitchTaskDialog.tsx +1 -1
- package/src/components/BlueprintPanelToolbar.tsx +10 -2
- package/src/components/ClockNodeConfigPanel.tsx +1 -1
- package/src/components/ConfigHintIcon.tsx +1 -1
- package/src/components/FetchNodeConfigPanel.tsx +314 -3
- package/src/components/FetchUrlAutocomplete.tsx +1 -1
- package/src/components/JsonLintTextarea.tsx +97 -0
- package/src/components/JsonNodeConfigPanel.tsx +14 -7
- package/src/components/LogicNodeConfigPanel.tsx +1 -1
- package/src/components/ScopeTemplateAutocompleteHost.tsx +4 -8
- package/src/components/StorageKeyAutocomplete.tsx +1 -1
- package/src/components/StorageNodeConfigPanel.tsx +1 -1
- package/src/components/ViewElementMultiSelect.tsx +1 -1
- package/src/edges/BlueprintSmoothEdge.tsx +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/hooks/useBlueprintFlowState.ts +1 -1
- package/src/index.tsx +41 -28
- package/src/library/api-collection-db.ts +98 -0
- package/src/library/api-collection-store.ts +139 -0
- package/src/nodes/AndFlowNode.tsx +4 -3
- package/src/nodes/BlueprintFlowNode.tsx +8 -3
- package/src/nodes/BlueprintNodeCard.tsx +137 -121
- package/src/nodes/BlueprintNodeShell.tsx +2 -1
- package/src/nodes/ClockFlowNode.tsx +1 -1
- package/src/nodes/EventFlowNode.tsx +1 -1
- package/src/nodes/FetchFlowNode.tsx +1 -1
- package/src/nodes/JsonFlowNode.tsx +1 -1
- package/src/nodes/LifecycleFlowNode.tsx +2 -1
- package/src/nodes/LogicFlowNode.tsx +1 -1
- 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 {
|
|
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:
|
|
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
|
|
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
|
-
(
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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";
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createNodeId,
|
|
4
4
|
defaultBlueprintNodeLabel,
|
|
5
5
|
filterInvalidBlueprintEdges,
|
|
6
|
+
isDefaultBlueprintNodeLabel,
|
|
6
7
|
patchNodeConfigSource,
|
|
7
8
|
resolveNodeClockConfig,
|
|
8
9
|
resolveNodeEventConfig,
|
|
@@ -218,6 +219,14 @@ export class BlueprintGraph {
|
|
|
218
219
|
nodes: this.document.nodes.map((n) => {
|
|
219
220
|
if (n.id !== nodeId) return n;
|
|
220
221
|
const nextNode = { ...n, ...configPatch, ...patch };
|
|
222
|
+
if (
|
|
223
|
+
patch.configSource &&
|
|
224
|
+
patch.configSource !== exists.configSource &&
|
|
225
|
+
patch.label === undefined &&
|
|
226
|
+
isDefaultBlueprintNodeLabel(exists.label)
|
|
227
|
+
) {
|
|
228
|
+
nextNode.label = defaultBlueprintNodeLabel(patch.configSource);
|
|
229
|
+
}
|
|
221
230
|
if (
|
|
222
231
|
patch.viewElementIds !== undefined ||
|
|
223
232
|
patch.viewElementId !== undefined
|
package/src/graph/document.ts
CHANGED
|
@@ -289,6 +289,18 @@ export function defaultBlueprintNodeLabel(
|
|
|
289
289
|
return getBlueprintNodeTypeLabel(t, configSource);
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
/** 当前名称是否仍是「类型默认名」(空或等于任一类型文案) */
|
|
293
|
+
export function isDefaultBlueprintNodeLabel(
|
|
294
|
+
label: string | undefined | null,
|
|
295
|
+
t: TranslateFn = defaultTranslate()
|
|
296
|
+
): boolean {
|
|
297
|
+
const trimmed = (label ?? "").trim();
|
|
298
|
+
if (!trimmed) return true;
|
|
299
|
+
return (
|
|
300
|
+
Object.keys(BLUEPRINT_CONFIG_TYPE_LABEL_KEYS) as BlueprintConfigSource[]
|
|
301
|
+
).some((src) => getBlueprintNodeTypeLabel(t, src) === trimmed);
|
|
302
|
+
}
|
|
303
|
+
|
|
292
304
|
export function resolveNodeFetchConfig(
|
|
293
305
|
node: Pick<BlueprintGraphNode, "fetchConfig">
|
|
294
306
|
): FetchRequestConfig {
|