implement expression validation and build script setup in workspace

This commit is contained in:
codetypes committed 2025-03-23 12:30:32 +08:00
1 parent d595ce4a3a
commit 2483606ef4
24 files changed
+573 -318

No files matched your search

+6 -16
View File
@@ -1253,32 +1253,22 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
editor.size.height = editorSize.height;
}
if (typeof editor.editNode === "number") {
const id = editor.editNode.toString();
selectNode(null);
if (editor.selectedId) {
const id = editor.selectedId;
setTimeout(() => {
editor.graph.focusItem(id);
selectNode(id);
}, 100);
}, 50);
}
});
}, [editorSize, workspace.editing]);
// check should subtree
// check should repaint node
useEffect(() => {
if (workspace.editing === editor) {
checkSubtree();
editor.graph && refresh();
}
}, [workspace.editing]);
// check should repaint node
useEffect(() => {
if (editor.graph) {
editor.graph.changeData(editor.root);
editor.graph.layout();
restoreViewport();
}
}, [t]);
}, [workspace.editing, t]);
const nextResult = () => {
const { results, index } = filterOption;
-12
View File
@@ -876,18 +876,6 @@ export const Explorer: FC = () => {
if (node && !node.editing) {
dispatch("open", node);
setSelectedKeys([node.path]);
setTimeout(() => {
const element = document.querySelector(`[title="${node.title}"]`);
const container =
element?.closest(".b3-overflow") || element?.closest(".b3-explorer");
if (element && container && !isElementInViewport(element, container)) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}, 100);
}
}}
onDrop={(info) => {
+75 -79
View File
@@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";
import { useDebounceCallback } from "usehooks-ts";
import { useShallow } from "zustand/react/shallow";
import { ExpressionEvaluator } from "../behavior3/src/behavior3";
import { EditNode, EditTree, useWorkspace } from "../contexts/workspace-context";
import {
ImportDef,
@@ -36,6 +37,7 @@ import {
isIntType,
isJsonType,
isStringType,
NodeArg,
NodeModel,
TreeGraphData,
VarDef,
@@ -53,6 +55,7 @@ import {
usingGroups,
usingVars,
} from "../misc/b3util";
import i18n from "../misc/i18n";
import { Hotkey, isMacos } from "../misc/keys";
import { mergeClassNames } from "../misc/util";
@@ -550,6 +553,43 @@ const TreeInspector: FC = () => {
);
};
const validateArg = (node: NodeModel, arg: NodeArg, value: unknown) => {
const type = getNodeArgRawType(arg);
const required = !isNodeArgOptional(arg);
if (isExprType(type) && value) {
for (const v of parseExpr(value as string)) {
if (usingVars && !usingVars[v]) {
return Promise.reject(new Error(i18n.t("node.undefinedVariable", { variable: v })));
}
}
if (useWorkspace.getState().settings.checkExpr) {
try {
if (!new ExpressionEvaluator(value as string).dryRun()) {
return Promise.reject(new Error(i18n.t("node.invalidExpression")));
}
} catch (e) {
console.error(e);
return Promise.reject(new Error(i18n.t("node.invalidExpression")));
}
}
}
if (value && isJsonType(type)) {
try {
if (value !== "null") {
JSON.parse(value as string);
}
} catch (e) {
return Promise.reject(new Error(i18n.t("node.invalidValue")));
}
} else if (value === null && !required) {
value = undefined;
}
if (!checkNodeArgValue(node, arg, value, true)) {
return Promise.reject(new Error(i18n.t("node.invalidValue")));
}
return Promise.resolve(value);
};
const NodeInspector: FC = () => {
const workspace = useWorkspace(
useShallow((state) => ({
@@ -842,7 +882,9 @@ const NodeInspector: FC = () => {
{
validator() {
if (def.group && !def.group.some((g) => usingGroups[g])) {
return Promise.reject(new Error(t("node.invalidGroup", { group: def.group })));
return Promise.reject(
new Error(t("node.groupNotEnabled", { group: def.group }))
);
}
return Promise.resolve();
},
@@ -1105,33 +1147,7 @@ const NodeInspector: FC = () => {
},
() => ({
validator(_, value) {
if (usingVars && isExprType(type) && value) {
for (const v of parseExpr(value)) {
if (!usingVars![v]) {
return Promise.reject(
new Error(
t("node.undefinedVariable", { variable: v })
)
);
}
}
}
if (isJsonType(type)) {
try {
if (value !== "null") {
JSON.parse(value);
}
} catch (e) {
return Promise.reject(
new Error(t("node.invalidValue"))
);
}
} else if (
!checkNodeArgValue(editingNode.data, arg, value, true)
) {
return Promise.reject(new Error(t("node.invalidValue")));
}
return Promise.resolve();
return validateArg(editingNode.data, arg, value);
},
}),
]}
@@ -1228,59 +1244,39 @@ const NodeInspector: FC = () => {
rules={[
{ required, message: t("fieldRequired", { field: arg.desc }) },
({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({
validator(_, value) {
if (usingVars && isExprType(type) && value) {
for (const v of parseExpr(value)) {
if (!usingVars![v]) {
return Promise.reject(
new Error(t("node.undefinedVariable", { variable: v }))
);
}
async validator(_, value) {
return validateArg(editingNode.data, arg, value).then((result) => {
value = result;
if (!arg.oneof) {
return Promise.resolve();
}
}
if (value && isJsonType(type)) {
try {
if (value !== "null") {
JSON.parse(value);
}
} catch (e) {
return Promise.reject(new Error(t("node.invalidValue")));
const idx = def.input?.findIndex(
(input) => input.replace("?", "") === arg.oneof
);
if (idx === undefined || idx < 0) {
return Promise.reject(
new Error(t("node.oneof.inputNotfound", { input: arg.oneof }))
);
}
} else if (value === null && !required) {
value = undefined;
}
if (!checkNodeArgValue(editingNode.data, arg, value)) {
return Promise.reject(new Error(t("node.invalidValue")));
}
if (!arg.oneof) {
return Promise.resolve();
}
const idx = def.input?.findIndex(
(input) => input.replace("?", "") === arg.oneof
);
if (idx === undefined || idx < 0) {
return Promise.reject(
new Error(t("node.oneof.inputNotfound", { input: arg.oneof }))
);
}
const inputName = `input.${idx}`;
if (!isFieldValidating(inputName)) {
setFieldValue(`args.${arg.name}`, value);
validateFields([inputName]);
}
if (!checkOneof(getFieldValue(inputName) ?? "", value)) {
return Promise.reject(
new Error(
t("node.oneof.error", {
input: def.input![idx],
arg: arg.name,
desc: arg.desc ?? "",
})
)
);
} else {
return Promise.resolve();
}
const inputName = `input.${idx}`;
if (!isFieldValidating(inputName)) {
setFieldValue(`args.${arg.name}`, value);
validateFields([inputName]);
}
if (!checkOneof(getFieldValue(inputName) ?? "", value)) {
return Promise.reject(
new Error(
t("node.oneof.error", {
input: def.input![idx],
arg: arg.name,
desc: arg.desc ?? "",
})
)
);
} else {
return Promise.resolve();
}
});
},
}),
]}
+24 -2
View File
@@ -54,6 +54,9 @@ export const Menu: FC<LayoutProps> = () => {
save: state.save,
saveAll: state.saveAll,
workdir: state.workdir,
settings: state.settings,
setCheckExpr: state.setCheckExpr,
setupBuildScript: state.setupBuildScript,
}))
);
const enabled = !!workspace.workdir;
@@ -165,6 +168,14 @@ export const Menu: FC<LayoutProps> = () => {
workspace.buildProject();
},
},
{
id: "menu.file.buildScript",
label: t("setupBuildScript"),
enabled: enabled,
click: () => {
workspace.setupBuildScript();
},
},
{
id: "menu.file.batch",
label: t("batch"),
@@ -255,6 +266,17 @@ export const Menu: FC<LayoutProps> = () => {
// workspace.editing?.dispatch("delete");
},
},
{ type: "separator" },
{
id: "menu.edit.checkExpr",
label: t("checkExpr"),
enabled: enabled,
type: "checkbox",
checked: workspace.settings.checkExpr,
click: () => {
workspace.setCheckExpr(!workspace.settings.checkExpr);
},
},
],
},
// { role: 'viewMenu' }
@@ -394,7 +416,7 @@ export const Menu: FC<LayoutProps> = () => {
],
},
] as MenuItemConstructorOptions[];
}, [t, workspace.workdir, settings.recent, workspace.editing, settings]);
}, [t, workspace.workdir, settings.recent, workspace.editing, workspace.settings, settings]);
if (isMacos) {
const menu = AppMenu.buildFromTemplate(menuTemplate);
@@ -446,7 +468,7 @@ export const Menu: FC<LayoutProps> = () => {
label: (
<MenuItemLabel>
<div>
{option.type === "radio" && option.checked && (
{(option.type === "radio" || option.type === "checkbox") && option.checked && (
<CheckOutlined style={{ paddingRight: "5px" }} />
)}
{option.type === "radio" && !option.checked && (
+40 -15
View File
@@ -1,5 +1,6 @@
import G6 from "@antv/g6";
import { NodeDef } from "../behavior3/src/behavior3";
import { ExpressionEvaluator, NodeDef } from "../behavior3/src/behavior3";
import { useSetting } from "../contexts/setting-context";
import { useWorkspace } from "../contexts/workspace-context";
import { TreeGraphData, getNodeType, isExprType } from "../misc/b3type";
import { checkTreeData, nodeDefs, parseExpr, usingGroups, usingVars } from "../misc/b3util";
@@ -11,9 +12,6 @@ let defaultFontSize = "";
const textWidthMap = new Map<string, number>();
const textLines: Record<string, string[]> = {};
let layoutWidth = 220;
let layoutStyle: "compact" | "normal" = "compact";
const calcTextWith = (text: string, fontSize?: string) => {
fontSize = fontSize ?? defaultFontSize;
let b3Workspace: HTMLDivElement | null;
@@ -133,7 +131,40 @@ const foundUndefinedInArgs = (def: NodeDef, data: TreeGraphData) => {
return false;
};
const hasErrorInArgExpr = (def: NodeDef, data: TreeGraphData) => {
const checkExpr = useWorkspace.getState().settings.checkExpr;
if (!checkExpr || !def.args || !data.args) {
return false;
}
for (const arg of def.args) {
if (!isExprType(arg.type)) {
continue;
}
const expr = data.args[arg.name] as string | string[] | undefined;
if (!expr) {
continue;
}
try {
if (typeof expr === "string") {
if (!new ExpressionEvaluator(expr).dryRun()) {
return true;
}
} else if (Array.isArray(expr)) {
for (const str of expr) {
if (!new ExpressionEvaluator(str).dryRun()) {
return true;
}
}
}
} catch (e) {
return true;
}
}
return false;
};
export const calcTreeDataSize = (data: TreeGraphData) => {
const width = useSetting.getState().data.layout === "compact" ? 220 : 260;
let height = 50 + 2;
const updateHeight = (obj: unknown) => {
if ((Array.isArray(obj) && obj.length) || (obj && Object.keys(obj).length > 0)) {
@@ -147,16 +178,7 @@ export const calcTreeDataSize = (data: TreeGraphData) => {
updateHeight(data.args);
updateHeight(data.input);
updateHeight(data.output);
return [layoutWidth, height];
};
export const setLayoutStyle = (style: "compact" | "normal") => {
layoutStyle = style;
if (style === "compact") {
layoutWidth = 220;
} else {
layoutWidth = 260;
}
return [width, height];
};
const NODE_COLORS = {
@@ -217,6 +239,8 @@ G6.registerNode(
let classify = getNodeType(nodeDef);
let color = nodeDef.color || NODE_COLORS[classify] || NODE_COLORS["Other"];
const layoutStyle = useSetting.getState().data.layout;
if (
!nodeDefs.has(data.name) ||
(data.path && !data.children?.length) ||
@@ -224,7 +248,8 @@ G6.registerNode(
!checkTreeData(data) ||
foundUndefined(data.input) ||
foundUndefined(data.output) ||
foundUndefinedInArgs(nodeDef, data)
foundUndefinedInArgs(nodeDef, data) ||
hasErrorInArgExpr(nodeDef, data)
) {
classify = "Error";
color = NODE_COLORS[classify];