@arronqzy/react-blueprint 1.0.19 → 1.0.22
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 +24 -0
- package/package.json +4 -4
- package/src/BlueprintDslLocaleSync.tsx +13 -0
- package/src/BlueprintNodeConfigSidebar.tsx +14 -8
- package/src/blueprint.css +336 -54
- package/src/components/ApiCollectionManagerDialog.tsx +448 -0
- package/src/components/ClockNodeConfigPanel.tsx +7 -4
- package/src/components/ConfigHintIcon.tsx +15 -2
- package/src/components/FetchNodeConfigPanel.tsx +312 -5
- package/src/components/JsonLintTextarea.tsx +97 -0
- package/src/components/JsonNodeConfigPanel.tsx +13 -6
- package/src/components/LogicNodeConfigPanel.tsx +2 -2
- package/src/components/ScopeTemplateAutocompleteHost.tsx +4 -8
- package/src/edges/BlueprintSmoothEdge.tsx +2 -2
- package/src/graph/blueprint-graph.ts +9 -0
- package/src/graph/document.ts +20 -1
- package/src/graph/sync-edges.ts +2 -2
- package/src/index.tsx +42 -29
- package/src/library/api-collection-db.ts +98 -0
- package/src/library/api-collection-store.ts +139 -0
- package/src/nodes/AndFlowNode.tsx +3 -2
- package/src/nodes/BlueprintFlowNode.tsx +7 -2
- package/src/nodes/BlueprintNodeCard.tsx +138 -120
- package/src/nodes/BlueprintNodeShell.tsx +1 -0
- package/src/nodes/LifecycleFlowNode.tsx +1 -0
|
@@ -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,6 +51,7 @@ import {
|
|
|
45
51
|
useFetchDebugTask,
|
|
46
52
|
useSwaggerLoadTask,
|
|
47
53
|
} from "../fetch-config-task-store";
|
|
54
|
+
import { useApiCollections } from "../library/api-collection-store";
|
|
48
55
|
import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
|
|
49
56
|
|
|
50
57
|
import { ConfigFieldLabel, ConfigHintIcon, ConfigSectionTitle } from "./ConfigHintIcon";
|
|
@@ -272,12 +279,24 @@ export function FetchNodeConfigPanel({
|
|
|
272
279
|
traceEntries = [],
|
|
273
280
|
onUpdateNode,
|
|
274
281
|
}: FetchNodeConfigPanelProps) {
|
|
275
|
-
const { t } = useI18n();
|
|
282
|
+
const { t, locale } = 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";
|
|
@@ -334,11 +353,11 @@ export function FetchNodeConfigPanel({
|
|
|
334
353
|
}, [resolvedFetchConfig.body, usesScopeTemplate]);
|
|
335
354
|
const headersJsonError = useMemo(
|
|
336
355
|
() => getFetchHeadersValidationError(fetchConfig, incomingScope),
|
|
337
|
-
[fetchConfig, incomingScope]
|
|
356
|
+
[fetchConfig, incomingScope, locale]
|
|
338
357
|
);
|
|
339
358
|
const bodyJsonError = useMemo(
|
|
340
359
|
() => getFetchBodyValidationError(fetchConfig.body, resolvedFetchConfig.body),
|
|
341
|
-
[fetchConfig.body, resolvedFetchConfig.body]
|
|
360
|
+
[fetchConfig.body, resolvedFetchConfig.body, locale]
|
|
342
361
|
);
|
|
343
362
|
const incomingHasPendingValue = useMemo(() => {
|
|
344
363
|
if (!incomingScope || typeof incomingScope !== "object") return false;
|
|
@@ -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="inline-flex items-center gap-1 text-[11px] text-muted-foreground">
|
|
603
|
+
{t("blueprint.config.fetchSourceMode")}
|
|
604
|
+
<ConfigHintIcon label={t("blueprint.config.fetchSourceMode")}>
|
|
605
|
+
{usingCollection
|
|
606
|
+
? t("blueprint.config.fetchSourceCollectionHint")
|
|
607
|
+
: t("blueprint.config.fetchSourceDirectHint")}
|
|
608
|
+
</ConfigHintIcon>
|
|
609
|
+
</span>
|
|
610
|
+
<div className="flex rounded-md border border-border p-0.5">
|
|
611
|
+
<button
|
|
612
|
+
type="button"
|
|
613
|
+
className={cn(
|
|
614
|
+
"flex-1 rounded px-2 py-1 text-[11px] transition-colors",
|
|
615
|
+
!usingCollection
|
|
616
|
+
? "bg-primary text-primary-foreground"
|
|
617
|
+
: "text-muted-foreground hover:text-foreground"
|
|
618
|
+
)}
|
|
619
|
+
onClick={() => setFetchSourceMode("direct")}
|
|
620
|
+
>
|
|
621
|
+
{t("blueprint.config.fetchSourceDirect")}
|
|
622
|
+
</button>
|
|
623
|
+
<button
|
|
624
|
+
type="button"
|
|
625
|
+
className={cn(
|
|
626
|
+
"flex-1 rounded px-2 py-1 text-[11px] transition-colors",
|
|
627
|
+
usingCollection
|
|
628
|
+
? "bg-primary text-primary-foreground"
|
|
629
|
+
: "text-muted-foreground hover:text-foreground"
|
|
630
|
+
)}
|
|
631
|
+
onClick={() => setFetchSourceMode("collection")}
|
|
632
|
+
>
|
|
633
|
+
{t("blueprint.config.fetchSourceCollection")}
|
|
634
|
+
</button>
|
|
635
|
+
</div>
|
|
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,129 @@ 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
|
+
<ConfigFieldLabel
|
|
693
|
+
label={t("blueprint.config.apiCollectionEndpoint")}
|
|
694
|
+
hint={t("blueprint.config.apiCollectionFilledHint")}
|
|
695
|
+
/>
|
|
696
|
+
<div className="flex gap-1.5">
|
|
697
|
+
<TooltipProvider delayDuration={200}>
|
|
698
|
+
<Tooltip>
|
|
699
|
+
<TooltipTrigger asChild>
|
|
700
|
+
<div className="min-w-0 flex-1">
|
|
701
|
+
<Select
|
|
702
|
+
value={fetchConfig.apiCollectionEndpointName || undefined}
|
|
703
|
+
onValueChange={handleSelectCollectionEndpoint}
|
|
704
|
+
disabled={!selectedCollection}
|
|
705
|
+
>
|
|
706
|
+
<SelectTrigger className="h-8 w-full text-[11px]">
|
|
707
|
+
<SelectValue
|
|
708
|
+
placeholder={t(
|
|
709
|
+
"blueprint.config.apiCollectionEndpointPlaceholder"
|
|
710
|
+
)}
|
|
711
|
+
/>
|
|
712
|
+
</SelectTrigger>
|
|
713
|
+
<SelectContent className="z-[10200]">
|
|
714
|
+
{collectionEndpoints.length === 0 ? (
|
|
715
|
+
<SelectItem value="__empty" disabled>
|
|
716
|
+
{t("blueprint.config.apiCollectionNoEndpoints")}
|
|
717
|
+
</SelectItem>
|
|
718
|
+
) : (
|
|
719
|
+
collectionEndpoints.map((item) => (
|
|
720
|
+
<SelectItem
|
|
721
|
+
key={item.name}
|
|
722
|
+
value={item.name}
|
|
723
|
+
title={item.description?.trim() || undefined}
|
|
724
|
+
>
|
|
725
|
+
{item.name}
|
|
726
|
+
</SelectItem>
|
|
727
|
+
))
|
|
728
|
+
)}
|
|
729
|
+
</SelectContent>
|
|
730
|
+
</Select>
|
|
731
|
+
</div>
|
|
732
|
+
</TooltipTrigger>
|
|
733
|
+
{selectedEndpointDescription ? (
|
|
734
|
+
<TooltipContent
|
|
735
|
+
side="top"
|
|
736
|
+
className="z-[10300] max-w-sm whitespace-pre-wrap text-[11px]"
|
|
737
|
+
>
|
|
738
|
+
{selectedEndpointDescription}
|
|
739
|
+
</TooltipContent>
|
|
740
|
+
) : null}
|
|
741
|
+
</Tooltip>
|
|
742
|
+
</TooltipProvider>
|
|
743
|
+
<AsyncSendButton
|
|
744
|
+
loading={loadingFetchDebug}
|
|
745
|
+
disabled={!fetchConfig.url?.trim()}
|
|
746
|
+
onSend={handleSendFetchDebug}
|
|
747
|
+
onAbort={handleAbortFetchDebug}
|
|
748
|
+
sendTitle={t("blueprint.config.sendDebugRequest")}
|
|
749
|
+
abortTitle={t("blueprint.config.abortRequest")}
|
|
750
|
+
sendAriaLabel={t("blueprint.config.sendDebugRequest")}
|
|
751
|
+
abortAriaLabel={t("blueprint.config.abortDebugAria")}
|
|
752
|
+
/>
|
|
753
|
+
</div>
|
|
754
|
+
</label>
|
|
755
|
+
<label className="block space-y-1">
|
|
756
|
+
<span className="text-muted-foreground">{t("blueprint.config.requestUrl")}</span>
|
|
757
|
+
<Input
|
|
758
|
+
value={fetchConfig.url}
|
|
759
|
+
disabled={loadingFetchDebug}
|
|
760
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
761
|
+
onUpdateNode(node.id, patchFetchConfig(node, { url: e.target.value }))
|
|
762
|
+
}
|
|
763
|
+
className="h-8 font-mono text-[11px]"
|
|
764
|
+
/>
|
|
765
|
+
</label>
|
|
766
|
+
{usesScopeTemplate && resolvedUrlPreview ? (
|
|
767
|
+
<p className="break-all font-mono text-[10px] text-muted-foreground">
|
|
768
|
+
{t("blueprint.config.fetchScopeResolvedUrl")}: {resolvedUrlPreview}
|
|
769
|
+
</p>
|
|
770
|
+
) : null}
|
|
771
|
+
{loadingFetchDebug ? (
|
|
772
|
+
<p className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
773
|
+
<LoaderIcon className="h-3.5 w-3.5 shrink-0" />
|
|
774
|
+
{t("blueprint.config.sendingDebug")}
|
|
775
|
+
</p>
|
|
776
|
+
) : null}
|
|
777
|
+
{fetchValidationError ? (
|
|
778
|
+
<p className="text-[11px] text-destructive">{fetchValidationError}</p>
|
|
779
|
+
) : null}
|
|
780
|
+
<FetchDebugResponsePanel task={fetchDebugTask} />
|
|
781
|
+
</div>
|
|
782
|
+
) : (
|
|
783
|
+
<>
|
|
480
784
|
<label className="block space-y-1">
|
|
481
785
|
<span className="text-muted-foreground">{t("blueprint.config.swaggerUrlOptional")}</span>
|
|
482
786
|
<div className="flex gap-1.5">
|
|
@@ -667,6 +971,9 @@ export function FetchNodeConfigPanel({
|
|
|
667
971
|
<FetchDebugResponsePanel task={fetchDebugTask} />
|
|
668
972
|
</label>
|
|
669
973
|
|
|
974
|
+
</>
|
|
975
|
+
)}
|
|
976
|
+
|
|
670
977
|
<label className="block space-y-1">
|
|
671
978
|
<span className="text-muted-foreground">{t("blueprint.config.requestMethod")}</span>
|
|
672
979
|
<Select
|
|
@@ -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 {
|
|
@@ -14,6 +15,7 @@ import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
|
|
|
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 ? (
|
|
@@ -29,13 +29,13 @@ export function LogicNodeConfigPanel({
|
|
|
29
29
|
node,
|
|
30
30
|
onUpdateNode,
|
|
31
31
|
}: LogicNodeConfigPanelProps) {
|
|
32
|
-
const { t } = useI18n();
|
|
32
|
+
const { t, locale } = useI18n();
|
|
33
33
|
const logicConfig = resolveNodeLogicConfig(node);
|
|
34
34
|
const [draftError, setDraftError] = useState<string | null>(null);
|
|
35
35
|
|
|
36
36
|
const storedValidation = useMemo(
|
|
37
37
|
() => validateLogicSourceCode(logicConfig.sourceCode),
|
|
38
|
-
[logicConfig.sourceCode]
|
|
38
|
+
[logicConfig.sourceCode, locale]
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
const parseError =
|
|
@@ -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
|
|
@@ -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
|
@@ -207,7 +207,7 @@ export function resolveRunnableNodeType(
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
import type { TranslateFn } from "@arronqzy/i18n";
|
|
210
|
-
import { resolveLocale, tForLocale } from "@arronqzy/i18n";
|
|
210
|
+
import { matchMessageInAnyLocale, relocalizeMessage, resolveLocale, tForLocale } from "@arronqzy/i18n";
|
|
211
211
|
|
|
212
212
|
/** 画布节点顶部(拖拽区)配置类型对应的 i18n key */
|
|
213
213
|
export const BLUEPRINT_CONFIG_TYPE_LABEL_KEYS: Record<
|
|
@@ -289,6 +289,25 @@ export function defaultBlueprintNodeLabel(
|
|
|
289
289
|
return getBlueprintNodeTypeLabel(t, configSource);
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
const BLUEPRINT_TYPE_LABEL_KEY_LIST = Object.values(BLUEPRINT_CONFIG_TYPE_LABEL_KEYS);
|
|
293
|
+
|
|
294
|
+
/** 当前名称是否仍是「类型默认名」(空或等于任一语言下的任一类型文案) */
|
|
295
|
+
export function isDefaultBlueprintNodeLabel(
|
|
296
|
+
label: string | undefined | null,
|
|
297
|
+
t: TranslateFn = defaultTranslate()
|
|
298
|
+
): boolean {
|
|
299
|
+
const trimmed = (label ?? "").trim();
|
|
300
|
+
if (!trimmed) return true;
|
|
301
|
+
return BLUEPRINT_TYPE_LABEL_KEY_LIST.some(
|
|
302
|
+
(key) => t(key) === trimmed || matchMessageInAnyLocale(trimmed, key) !== null
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** 类型默认名按当前语言显示;用户自定义名称保持不变 */
|
|
307
|
+
export function localizeBlueprintNodeLabel(label: string, t: TranslateFn): string {
|
|
308
|
+
return relocalizeMessage(label, BLUEPRINT_TYPE_LABEL_KEY_LIST, t);
|
|
309
|
+
}
|
|
310
|
+
|
|
292
311
|
export function resolveNodeFetchConfig(
|
|
293
312
|
node: Pick<BlueprintGraphNode, "fetchConfig">
|
|
294
313
|
): FetchRequestConfig {
|
package/src/graph/sync-edges.ts
CHANGED
|
@@ -6,8 +6,8 @@ import type { BlueprintGraph } from "./blueprint-graph";
|
|
|
6
6
|
|
|
7
7
|
/** 边线描边(写入 edge.style;使用 hex 避免 SVG style 解析 hsl 异常) */
|
|
8
8
|
export const BP_EDGE_STYLE = {
|
|
9
|
-
stroke: "#
|
|
10
|
-
strokeWidth:
|
|
9
|
+
stroke: "#94a3b8",
|
|
10
|
+
strokeWidth: 1.75,
|
|
11
11
|
} as const;
|
|
12
12
|
|
|
13
13
|
export const BP_FLOW_EDGE_TYPE = "blueprintSmooth" as const;
|