@arronqzy/react-blueprint 1.0.0
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/.eslintrc.cjs +5 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
- package/.turbo/turbo-lint.log +4 -0
- package/dist/assets/index-CneNgzWb.css +1 -0
- package/dist/assets/index-DsVhHV0-.js +226 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/index.ts +70 -0
- package/package.json +43 -0
- package/postcss.config.cjs +6 -0
- package/src/BlueprintCanvasContext.tsx +61 -0
- package/src/BlueprintContextMenu.tsx +108 -0
- package/src/BlueprintNodeConfigSidebar.tsx +358 -0
- package/src/blueprint.css +321 -0
- package/src/blueprintNodeTypes.ts +20 -0
- package/src/components/BlueprintExecutionLogPanel.tsx +427 -0
- package/src/components/BlueprintLibrarySelect.tsx +68 -0
- package/src/components/BlueprintMetaDialog.tsx +96 -0
- package/src/components/BlueprintNodeSwitchTaskDialog.tsx +58 -0
- package/src/components/BlueprintPanelToolbar.tsx +664 -0
- package/src/components/ClockNodeConfigPanel.tsx +139 -0
- package/src/components/FetchNodeConfigPanel.tsx +716 -0
- package/src/components/FetchUrlAutocomplete.tsx +190 -0
- package/src/components/JsonNodeConfigPanel.tsx +79 -0
- package/src/components/LogicNodeConfigPanel.tsx +80 -0
- package/src/components/ViewElementMultiSelect.tsx +140 -0
- package/src/createBlueprintEdgeTypes.tsx +10 -0
- package/src/dev-seed.ts +34 -0
- package/src/dev.css +38 -0
- package/src/edges/BlueprintSmoothEdge.tsx +35 -0
- package/src/fetch-config-task-store.ts +197 -0
- package/src/flowCoordinates.ts +19 -0
- package/src/flowDefaults.ts +9 -0
- package/src/graph/blueprint-graph.ts +261 -0
- package/src/graph/document.ts +384 -0
- package/src/graph/index.ts +7 -0
- package/src/graph/node-summary.ts +88 -0
- package/src/graph/node-types.ts +9 -0
- package/src/graph/react-flow-adapter.ts +127 -0
- package/src/graph/sync-edges.ts +69 -0
- package/src/graph/sync-nodes.ts +104 -0
- package/src/hooks/useBlueprintDebugSession.ts +515 -0
- package/src/hooks/useBlueprintFlowColorMode.ts +8 -0
- package/src/hooks/useBlueprintFlowState.ts +486 -0
- package/src/hooks/useBlueprintFlowViewport.ts +54 -0
- package/src/hooks/useBlueprintNodeSelectionGuard.ts +63 -0
- package/src/hooks/useBlueprintPageLifecycle.ts +245 -0
- package/src/index.tsx +271 -0
- package/src/library/blueprint-io.ts +108 -0
- package/src/library/blueprint-library-db.ts +112 -0
- package/src/library/execution-log-db.ts +171 -0
- package/src/library/execution-log-settings.ts +50 -0
- package/src/library/swagger-docs.ts +56 -0
- package/src/library/types.ts +35 -0
- package/src/main.tsx +43 -0
- package/src/nodes/AndFlowNode.tsx +59 -0
- package/src/nodes/BlueprintFlowNode.tsx +26 -0
- package/src/nodes/BlueprintNodeCard.tsx +169 -0
- package/src/nodes/BlueprintNodeShell.tsx +45 -0
- package/src/nodes/ClockFlowNode.tsx +67 -0
- package/src/nodes/FetchFlowNode.tsx +26 -0
- package/src/nodes/JsonFlowNode.tsx +26 -0
- package/src/nodes/LifecycleFlowNode.tsx +44 -0
- package/src/nodes/LogicFlowNode.tsx +26 -0
- package/src/runtime/document-to-runnable-graph.ts +48 -0
- package/src/runtime/execution-overlay.ts +169 -0
- package/src/types.ts +1 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.ts +6 -0
- package/tsconfig.json +12 -0
- package/vite.config.ts +16 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { cn } from "@arronqzy/ui";
|
|
3
|
+
import {
|
|
4
|
+
buildEndpointSuggestions,
|
|
5
|
+
filterEndpointSuggestions,
|
|
6
|
+
resolveFetchRequestUrl,
|
|
7
|
+
type FetchHttpMethod,
|
|
8
|
+
type SwaggerApiEndpoint,
|
|
9
|
+
} from "@arronqzy/blueprint-dsl";
|
|
10
|
+
|
|
11
|
+
export type FetchUrlAutocompleteProps = {
|
|
12
|
+
value: string;
|
|
13
|
+
apiBaseUrl: string;
|
|
14
|
+
endpoints: SwaggerApiEndpoint[];
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
/** 为 true 时仅能从列表选择,不可自由输入 URL */
|
|
17
|
+
selectOnly?: boolean;
|
|
18
|
+
onChange: (value: string) => void;
|
|
19
|
+
onSelectEndpoint?: (endpoint: SwaggerApiEndpoint, fullUrl: string) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function FetchUrlAutocomplete({
|
|
23
|
+
value,
|
|
24
|
+
apiBaseUrl,
|
|
25
|
+
endpoints,
|
|
26
|
+
placeholder,
|
|
27
|
+
selectOnly = false,
|
|
28
|
+
onChange,
|
|
29
|
+
onSelectEndpoint,
|
|
30
|
+
}: FetchUrlAutocompleteProps) {
|
|
31
|
+
const [open, setOpen] = useState(false);
|
|
32
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
33
|
+
const [filterQuery, setFilterQuery] = useState("");
|
|
34
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
|
|
36
|
+
const suggestions = useMemo(
|
|
37
|
+
() => buildEndpointSuggestions(apiBaseUrl, endpoints),
|
|
38
|
+
[apiBaseUrl, endpoints]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const query = selectOnly ? filterQuery : value;
|
|
42
|
+
|
|
43
|
+
const filtered = useMemo(
|
|
44
|
+
() => filterEndpointSuggestions(suggestions, query),
|
|
45
|
+
[suggestions, query]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const selectedSuggestion = useMemo(
|
|
49
|
+
() => suggestions.find((item) => item.path === value),
|
|
50
|
+
[suggestions, value]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const inputValue = selectOnly
|
|
54
|
+
? open
|
|
55
|
+
? filterQuery
|
|
56
|
+
: selectedSuggestion?.label ?? value
|
|
57
|
+
: value;
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setActiveIndex(0);
|
|
61
|
+
}, [query, filtered.length]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!open) return;
|
|
65
|
+
const onPointerDown = (event: MouseEvent) => {
|
|
66
|
+
if (!containerRef.current?.contains(event.target as globalThis.Node)) {
|
|
67
|
+
setOpen(false);
|
|
68
|
+
setFilterQuery("");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener("pointerdown", onPointerDown);
|
|
72
|
+
return () => window.removeEventListener("pointerdown", onPointerDown);
|
|
73
|
+
}, [open]);
|
|
74
|
+
|
|
75
|
+
const showSuggestions = open && endpoints.length > 0;
|
|
76
|
+
|
|
77
|
+
const selectSuggestion = (index: number) => {
|
|
78
|
+
const item = filtered[index];
|
|
79
|
+
if (!item) return;
|
|
80
|
+
const nextValue = apiBaseUrl.trim() ? item.path : item.fullUrl;
|
|
81
|
+
onChange(nextValue);
|
|
82
|
+
onSelectEndpoint?.(item, item.fullUrl);
|
|
83
|
+
setFilterQuery("");
|
|
84
|
+
setOpen(false);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const openDropdown = () => {
|
|
88
|
+
setOpen(true);
|
|
89
|
+
if (selectOnly) {
|
|
90
|
+
setFilterQuery("");
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const closeDropdown = () => {
|
|
95
|
+
setOpen(false);
|
|
96
|
+
setFilterQuery("");
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div ref={containerRef} className="relative">
|
|
101
|
+
<input
|
|
102
|
+
value={inputValue}
|
|
103
|
+
readOnly={selectOnly && !open}
|
|
104
|
+
placeholder={placeholder}
|
|
105
|
+
onChange={(e) => {
|
|
106
|
+
if (selectOnly) {
|
|
107
|
+
setFilterQuery(e.target.value);
|
|
108
|
+
setOpen(true);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
onChange(e.target.value);
|
|
112
|
+
setOpen(true);
|
|
113
|
+
}}
|
|
114
|
+
onFocus={openDropdown}
|
|
115
|
+
onBlur={() => {
|
|
116
|
+
if (selectOnly) {
|
|
117
|
+
closeDropdown();
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
120
|
+
onKeyDown={(e) => {
|
|
121
|
+
if (!showSuggestions || filtered.length === 0) {
|
|
122
|
+
if (selectOnly && e.key === "Escape") {
|
|
123
|
+
closeDropdown();
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (e.key === "ArrowDown") {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1));
|
|
130
|
+
} else if (e.key === "ArrowUp") {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
setActiveIndex((prev) => Math.max(prev - 1, 0));
|
|
133
|
+
} else if (e.key === "Enter") {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
selectSuggestion(activeIndex);
|
|
136
|
+
} else if (e.key === "Escape") {
|
|
137
|
+
closeDropdown();
|
|
138
|
+
}
|
|
139
|
+
}}
|
|
140
|
+
className={cn(
|
|
141
|
+
"flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 text-[11px] text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary",
|
|
142
|
+
selectOnly && !open ? "cursor-pointer font-medium" : "font-mono"
|
|
143
|
+
)}
|
|
144
|
+
/>
|
|
145
|
+
{showSuggestions ? (
|
|
146
|
+
<div className="absolute z-[10150] mt-1 max-h-52 w-full overflow-auto rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-md">
|
|
147
|
+
{filtered.length === 0 ? (
|
|
148
|
+
<div className="px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
149
|
+
无匹配接口
|
|
150
|
+
</div>
|
|
151
|
+
) : (
|
|
152
|
+
filtered.map((item, index) => (
|
|
153
|
+
<button
|
|
154
|
+
key={`${item.method}:${item.path}:${item.operationId ?? index}`}
|
|
155
|
+
type="button"
|
|
156
|
+
className={cn(
|
|
157
|
+
"flex w-full flex-col items-start gap-0.5 px-2 py-1.5 text-left text-[11px] hover:bg-accent",
|
|
158
|
+
index === activeIndex && "bg-accent"
|
|
159
|
+
)}
|
|
160
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
161
|
+
onClick={() => selectSuggestion(index)}
|
|
162
|
+
>
|
|
163
|
+
<span className="font-medium text-foreground">{item.label}</span>
|
|
164
|
+
<span className="truncate font-mono text-muted-foreground">
|
|
165
|
+
{item.fullUrl}
|
|
166
|
+
</span>
|
|
167
|
+
</button>
|
|
168
|
+
))
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
) : null}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function resolveFetchUrlPreview(
|
|
177
|
+
apiBaseUrl: string,
|
|
178
|
+
url: string,
|
|
179
|
+
endpoints: SwaggerApiEndpoint[]
|
|
180
|
+
): string {
|
|
181
|
+
const matched = endpoints.find((item) => item.path === url);
|
|
182
|
+
const resolvedPath = matched?.path ?? url;
|
|
183
|
+
try {
|
|
184
|
+
return resolveFetchRequestUrl({ url: resolvedPath, apiBaseUrl });
|
|
185
|
+
} catch {
|
|
186
|
+
return url;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export type { FetchHttpMethod };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState, type ChangeEvent } from "react";
|
|
2
|
+
import { validateJsonString } from "@arronqzy/blueprint-dsl";
|
|
3
|
+
import type { JsonNodeConfig } from "@arronqzy/blueprint-dsl";
|
|
4
|
+
|
|
5
|
+
import type { BlueprintGraphNode } from "../graph/document";
|
|
6
|
+
import { resolveNodeJsonConfig } from "../graph/document";
|
|
7
|
+
|
|
8
|
+
export type JsonNodeConfigPanelProps = {
|
|
9
|
+
node: BlueprintGraphNode;
|
|
10
|
+
onUpdateNode: (
|
|
11
|
+
nodeId: string,
|
|
12
|
+
patch: Partial<Pick<BlueprintGraphNode, "jsonConfig" | "configSource">>
|
|
13
|
+
) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function patchJsonConfig(
|
|
17
|
+
node: BlueprintGraphNode,
|
|
18
|
+
patch: Partial<JsonNodeConfig>
|
|
19
|
+
) {
|
|
20
|
+
return {
|
|
21
|
+
jsonConfig: { ...resolveNodeJsonConfig(node), ...patch },
|
|
22
|
+
configSource: "json" as const,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function JsonNodeConfigPanel({
|
|
27
|
+
node,
|
|
28
|
+
onUpdateNode,
|
|
29
|
+
}: JsonNodeConfigPanelProps) {
|
|
30
|
+
const jsonConfig = resolveNodeJsonConfig(node);
|
|
31
|
+
const [draftError, setDraftError] = useState<string | null>(null);
|
|
32
|
+
|
|
33
|
+
const storedValidation = useMemo(
|
|
34
|
+
() => validateJsonString(jsonConfig.jsonString),
|
|
35
|
+
[jsonConfig.jsonString]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const parseError =
|
|
39
|
+
draftError ??
|
|
40
|
+
(storedValidation.ok ? null : storedValidation.error);
|
|
41
|
+
|
|
42
|
+
const handleChange = useCallback(
|
|
43
|
+
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
44
|
+
const jsonString = e.target.value;
|
|
45
|
+
const result = validateJsonString(jsonString);
|
|
46
|
+
setDraftError(result.ok ? null : result.error);
|
|
47
|
+
onUpdateNode(node.id, patchJsonConfig(node, { jsonString }));
|
|
48
|
+
},
|
|
49
|
+
[node, onUpdateNode]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
|
|
54
|
+
<div className="font-medium text-foreground">JSON 节点</div>
|
|
55
|
+
<p className="text-[11px] text-muted-foreground">
|
|
56
|
+
收到<strong>真信号</strong>后,将下方 JSON 解析为 JavaScript object(或
|
|
57
|
+
array)并从输出口发出<strong>真信号</strong>;解析失败则发出
|
|
58
|
+
<strong>假信号</strong>。
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
<label className="block space-y-1">
|
|
62
|
+
<span className="text-muted-foreground">JSON 内容</span>
|
|
63
|
+
<textarea
|
|
64
|
+
value={jsonConfig.jsonString}
|
|
65
|
+
onChange={handleChange}
|
|
66
|
+
rows={12}
|
|
67
|
+
spellCheck={false}
|
|
68
|
+
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"
|
|
69
|
+
placeholder={'{\n "key": "value"\n}'}
|
|
70
|
+
/>
|
|
71
|
+
{parseError ? (
|
|
72
|
+
<p className="text-[11px] text-destructive">JSON 格式错误:{parseError}</p>
|
|
73
|
+
) : (
|
|
74
|
+
<p className="text-[11px] text-muted-foreground">JSON 格式正确</p>
|
|
75
|
+
)}
|
|
76
|
+
</label>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState, type ChangeEvent } from "react";
|
|
2
|
+
import { validateLogicSourceCode } from "@arronqzy/blueprint-dsl";
|
|
3
|
+
import type { LogicNodeConfig } from "@arronqzy/blueprint-dsl";
|
|
4
|
+
|
|
5
|
+
import type { BlueprintGraphNode } from "../graph/document";
|
|
6
|
+
import { resolveNodeLogicConfig } from "../graph/document";
|
|
7
|
+
|
|
8
|
+
export type LogicNodeConfigPanelProps = {
|
|
9
|
+
node: BlueprintGraphNode;
|
|
10
|
+
onUpdateNode: (
|
|
11
|
+
nodeId: string,
|
|
12
|
+
patch: Partial<Pick<BlueprintGraphNode, "logicConfig" | "configSource">>
|
|
13
|
+
) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function patchLogicConfig(
|
|
17
|
+
node: BlueprintGraphNode,
|
|
18
|
+
patch: Partial<LogicNodeConfig>
|
|
19
|
+
) {
|
|
20
|
+
return {
|
|
21
|
+
logicConfig: { ...resolveNodeLogicConfig(node), ...patch },
|
|
22
|
+
configSource: "logic" as const,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function LogicNodeConfigPanel({
|
|
27
|
+
node,
|
|
28
|
+
onUpdateNode,
|
|
29
|
+
}: LogicNodeConfigPanelProps) {
|
|
30
|
+
const logicConfig = resolveNodeLogicConfig(node);
|
|
31
|
+
const [draftError, setDraftError] = useState<string | null>(null);
|
|
32
|
+
|
|
33
|
+
const storedValidation = useMemo(
|
|
34
|
+
() => validateLogicSourceCode(logicConfig.sourceCode),
|
|
35
|
+
[logicConfig.sourceCode]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const parseError =
|
|
39
|
+
draftError ?? (storedValidation.ok ? null : storedValidation.error);
|
|
40
|
+
|
|
41
|
+
const handleChange = useCallback(
|
|
42
|
+
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
43
|
+
const sourceCode = e.target.value;
|
|
44
|
+
const result = validateLogicSourceCode(sourceCode);
|
|
45
|
+
setDraftError(result.ok ? null : result.error);
|
|
46
|
+
onUpdateNode(node.id, patchLogicConfig(node, { sourceCode }));
|
|
47
|
+
},
|
|
48
|
+
[node, onUpdateNode]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
|
|
53
|
+
<div className="font-medium text-foreground">逻辑节点</div>
|
|
54
|
+
<p className="text-[11px] text-muted-foreground">
|
|
55
|
+
收到<strong>真信号</strong>后,将输入数据传入下方 <code>update</code>{" "}
|
|
56
|
+
函数并执行;返回值作为<strong>真信号</strong>输出。代码语法错误或运行时报错则发出
|
|
57
|
+
<strong>假信号</strong>(错误信息为输出值)。
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
<label className="block space-y-1">
|
|
61
|
+
<span className="text-muted-foreground">JavaScript 代码</span>
|
|
62
|
+
<textarea
|
|
63
|
+
value={logicConfig.sourceCode}
|
|
64
|
+
onChange={handleChange}
|
|
65
|
+
rows={14}
|
|
66
|
+
spellCheck={false}
|
|
67
|
+
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"
|
|
68
|
+
placeholder={`function update(input) {\n return input;\n}`}
|
|
69
|
+
/>
|
|
70
|
+
{parseError ? (
|
|
71
|
+
<p className="text-[11px] text-destructive">
|
|
72
|
+
JavaScript 错误:{parseError}
|
|
73
|
+
</p>
|
|
74
|
+
) : (
|
|
75
|
+
<p className="text-[11px] text-muted-foreground">JavaScript 语法正确</p>
|
|
76
|
+
)}
|
|
77
|
+
</label>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
cn,
|
|
5
|
+
Command,
|
|
6
|
+
CommandEmpty,
|
|
7
|
+
CommandGroup,
|
|
8
|
+
CommandInput,
|
|
9
|
+
CommandItem,
|
|
10
|
+
CommandList,
|
|
11
|
+
Popover,
|
|
12
|
+
PopoverContent,
|
|
13
|
+
PopoverTrigger,
|
|
14
|
+
} from "@arronqzy/ui";
|
|
15
|
+
|
|
16
|
+
export type ViewElementMultiSelectOption = {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ViewElementMultiSelectProps = {
|
|
22
|
+
options: ViewElementMultiSelectOption[];
|
|
23
|
+
value: string[];
|
|
24
|
+
onChange: (next: string[]) => void;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function CheckIcon({ className }: { className?: string }) {
|
|
30
|
+
return (
|
|
31
|
+
<svg viewBox="0 0 24 24" className={className} aria-hidden>
|
|
32
|
+
<path
|
|
33
|
+
fill="currentColor"
|
|
34
|
+
d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ChevronDownIcon({ className }: { className?: string }) {
|
|
41
|
+
return (
|
|
42
|
+
<svg viewBox="0 0 24 24" className={className} aria-hidden>
|
|
43
|
+
<path fill="currentColor" d="M7 10l5 5 5-5z" />
|
|
44
|
+
</svg>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveTriggerText(
|
|
49
|
+
value: string[],
|
|
50
|
+
labelById: Map<string, string>,
|
|
51
|
+
placeholder: string
|
|
52
|
+
): string {
|
|
53
|
+
if (value.length === 0) return placeholder;
|
|
54
|
+
const labels = value.map((id) => labelById.get(id) ?? id);
|
|
55
|
+
if (labels.length === 1) return labels[0]!;
|
|
56
|
+
const joined = labels.join("、");
|
|
57
|
+
if (joined.length <= 26) return joined;
|
|
58
|
+
return `已选 ${value.length} 项`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ViewElementMultiSelect({
|
|
62
|
+
options,
|
|
63
|
+
value,
|
|
64
|
+
onChange,
|
|
65
|
+
placeholder = "选择视图节点",
|
|
66
|
+
disabled = false,
|
|
67
|
+
}: ViewElementMultiSelectProps) {
|
|
68
|
+
const [open, setOpen] = useState(false);
|
|
69
|
+
const labelById = useMemo(
|
|
70
|
+
() => new Map(options.map((opt) => [opt.id, opt.label])),
|
|
71
|
+
[options]
|
|
72
|
+
);
|
|
73
|
+
const selectedSet = useMemo(() => new Set(value), [value]);
|
|
74
|
+
const triggerText = resolveTriggerText(value, labelById, placeholder);
|
|
75
|
+
const emptyOptions = options.length === 0;
|
|
76
|
+
|
|
77
|
+
const toggle = (id: string) => {
|
|
78
|
+
onChange(
|
|
79
|
+
selectedSet.has(id)
|
|
80
|
+
? value.filter((item) => item !== id)
|
|
81
|
+
: [...value, id]
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
87
|
+
<PopoverTrigger asChild>
|
|
88
|
+
<Button
|
|
89
|
+
type="button"
|
|
90
|
+
variant="outline"
|
|
91
|
+
role="combobox"
|
|
92
|
+
aria-expanded={open}
|
|
93
|
+
disabled={disabled || emptyOptions}
|
|
94
|
+
className={cn(
|
|
95
|
+
"h-8 w-full justify-between px-3 text-xs font-normal",
|
|
96
|
+
value.length === 0 && "text-muted-foreground"
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<span className="min-w-0 flex-1 truncate text-left">
|
|
100
|
+
{emptyOptions ? "视图画布暂无节点" : triggerText}
|
|
101
|
+
</span>
|
|
102
|
+
<ChevronDownIcon className="h-4 w-4 shrink-0 opacity-50" />
|
|
103
|
+
</Button>
|
|
104
|
+
</PopoverTrigger>
|
|
105
|
+
<PopoverContent
|
|
106
|
+
className="z-[10100] w-[var(--radix-popover-trigger-width)] p-0"
|
|
107
|
+
align="start"
|
|
108
|
+
>
|
|
109
|
+
<Command>
|
|
110
|
+
<CommandInput placeholder="搜索视图节点…" className="h-9 text-xs" />
|
|
111
|
+
<CommandList>
|
|
112
|
+
<CommandEmpty>无匹配节点</CommandEmpty>
|
|
113
|
+
<CommandGroup>
|
|
114
|
+
{options.map((opt) => {
|
|
115
|
+
const selected = selectedSet.has(opt.id);
|
|
116
|
+
return (
|
|
117
|
+
<CommandItem
|
|
118
|
+
key={opt.id}
|
|
119
|
+
value={`${opt.label} ${opt.id}`}
|
|
120
|
+
className="text-xs"
|
|
121
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
122
|
+
onSelect={() => toggle(opt.id)}
|
|
123
|
+
>
|
|
124
|
+
<CheckIcon
|
|
125
|
+
className={cn(
|
|
126
|
+
"h-4 w-4 shrink-0",
|
|
127
|
+
selected ? "opacity-100" : "opacity-0"
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
<span className="min-w-0 truncate">{opt.label}</span>
|
|
131
|
+
</CommandItem>
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</CommandGroup>
|
|
135
|
+
</CommandList>
|
|
136
|
+
</Command>
|
|
137
|
+
</PopoverContent>
|
|
138
|
+
</Popover>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EdgeTypes } from "@xyflow/react";
|
|
2
|
+
|
|
3
|
+
import { BlueprintSmoothEdge } from "./edges/BlueprintSmoothEdge";
|
|
4
|
+
import { BP_FLOW_EDGE_TYPE } from "./graph/sync-edges";
|
|
5
|
+
|
|
6
|
+
export function createBlueprintEdgeTypes(): EdgeTypes {
|
|
7
|
+
return {
|
|
8
|
+
[BP_FLOW_EDGE_TYPE]: BlueprintSmoothEdge,
|
|
9
|
+
};
|
|
10
|
+
}
|
package/src/dev-seed.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { addEdge } from "@xyflow/react";
|
|
2
|
+
|
|
3
|
+
import { BlueprintGraph } from "./graph";
|
|
4
|
+
import {
|
|
5
|
+
BP_EDGE_STYLE,
|
|
6
|
+
BP_FLOW_EDGE_TYPE,
|
|
7
|
+
flowEdgesToGraphEdges,
|
|
8
|
+
graphToFlowEdges,
|
|
9
|
+
} from "./graph/sync-edges";
|
|
10
|
+
|
|
11
|
+
/** DevApp 初始图:两个节点 + 一条连线,便于直接验证边是否渲染 */
|
|
12
|
+
export function createDevBlueprintGraph(): BlueprintGraph {
|
|
13
|
+
let graph = BlueprintGraph.empty();
|
|
14
|
+
graph = graph.addBlueprintNode({ x: 40, y: 80 }, "节点 A");
|
|
15
|
+
const nodeA = graph.document.nodes[0]!;
|
|
16
|
+
graph = graph.addBlueprintNode({ x: 300, y: 80 }, "节点 B");
|
|
17
|
+
const nodeB = graph.document.nodes[1]!;
|
|
18
|
+
|
|
19
|
+
const withEdge = addEdge(
|
|
20
|
+
{
|
|
21
|
+
id: "dev-edge-1",
|
|
22
|
+
source: nodeA.id,
|
|
23
|
+
target: nodeB.id,
|
|
24
|
+
sourceHandle: "out",
|
|
25
|
+
targetHandle: "in",
|
|
26
|
+
type: BP_FLOW_EDGE_TYPE,
|
|
27
|
+
zIndex: 1000,
|
|
28
|
+
style: { ...BP_EDGE_STYLE },
|
|
29
|
+
},
|
|
30
|
+
graphToFlowEdges(graph)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return graph.replaceEdges(flowEdgesToGraphEdges(withEdge));
|
|
34
|
+
}
|
package/src/dev.css
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
html,
|
|
6
|
+
body,
|
|
7
|
+
#app {
|
|
8
|
+
height: 100%;
|
|
9
|
+
margin: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#app {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
background: hsl(var(--background));
|
|
17
|
+
color: hsl(var(--foreground));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.bp-dev-toolbar {
|
|
21
|
+
flex-shrink: 0;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 12px;
|
|
25
|
+
padding: 8px 12px;
|
|
26
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
27
|
+
background: hsl(var(--card));
|
|
28
|
+
font-size: 12px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.bp-dev-toolbar strong {
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.bp-dev-canvas {
|
|
36
|
+
flex: 1;
|
|
37
|
+
min-height: 0;
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseEdge, getSmoothStepPath, type EdgeProps } from "@xyflow/react";
|
|
2
|
+
|
|
3
|
+
const EDGE_STROKE = "#2563eb";
|
|
4
|
+
const EDGE_WIDTH = 2.5;
|
|
5
|
+
|
|
6
|
+
export function BlueprintSmoothEdge({
|
|
7
|
+
sourceX,
|
|
8
|
+
sourceY,
|
|
9
|
+
targetX,
|
|
10
|
+
targetY,
|
|
11
|
+
sourcePosition,
|
|
12
|
+
targetPosition,
|
|
13
|
+
style,
|
|
14
|
+
}: EdgeProps) {
|
|
15
|
+
const [path] = getSmoothStepPath({
|
|
16
|
+
sourceX,
|
|
17
|
+
sourceY,
|
|
18
|
+
sourcePosition,
|
|
19
|
+
targetX,
|
|
20
|
+
targetY,
|
|
21
|
+
targetPosition,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<BaseEdge
|
|
26
|
+
path={path}
|
|
27
|
+
className="bp-edge-visible"
|
|
28
|
+
style={{
|
|
29
|
+
stroke: EDGE_STROKE,
|
|
30
|
+
strokeWidth: EDGE_WIDTH,
|
|
31
|
+
...style,
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|