implement expression validation and build script setup in workspace
This commit is contained in:
1 parent
d595ce4a3a
commit
2483606ef4
24 files changed
+573
-318
No files matched your search
+1
-1
Submodule src/behavior3 updated: 14563ec920...f45acd9d30.
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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 && (
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { app } from "@electron/remote";
|
||||
import * as fs from "fs";
|
||||
import { create } from "zustand";
|
||||
import { setLayoutStyle } from "../components/register-node";
|
||||
import { readJson, writeJson } from "../misc/util";
|
||||
import { useWorkspace } from "./workspace-context";
|
||||
|
||||
@@ -29,6 +28,7 @@ export const useSetting = create<SettingStore>((set, get) => ({
|
||||
recent: [],
|
||||
buildDir: "",
|
||||
layout: "compact",
|
||||
checkExpr: true,
|
||||
},
|
||||
load: () => {
|
||||
try {
|
||||
@@ -36,7 +36,6 @@ export const useSetting = create<SettingStore>((set, get) => ({
|
||||
const settings = readJson(settingPath) as SettingModel;
|
||||
settings.layout = settings.layout || "compact";
|
||||
set({ data: settings });
|
||||
setLayoutStyle(settings.layout);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -67,7 +66,6 @@ export const useSetting = create<SettingStore>((set, get) => ({
|
||||
const { data, save } = get();
|
||||
set({ data: { ...data, layout } });
|
||||
save();
|
||||
setLayoutStyle(layout);
|
||||
useWorkspace.getState().editing?.dispatch("refresh");
|
||||
},
|
||||
}));
|
||||
@@ -18,17 +18,11 @@ import { message } from "../misc/hooks";
|
||||
import i18n from "../misc/i18n";
|
||||
import Path from "../misc/path";
|
||||
import { zhNodeDef } from "../misc/template";
|
||||
import { readJson, readTree, writeJson } from "../misc/util";
|
||||
import { readJson, readTree, readWorkspace, writeJson } from "../misc/util";
|
||||
import { useSetting } from "./setting-context";
|
||||
|
||||
let buildDir: string | undefined;
|
||||
|
||||
interface BatchScript {
|
||||
processTree?(tree: TreeModel, path: string): TreeModel;
|
||||
|
||||
processNode?(node: NodeModel, tree: TreeModel): NodeModel;
|
||||
}
|
||||
|
||||
export type EditEvent =
|
||||
| "save"
|
||||
| "copy"
|
||||
@@ -72,8 +66,6 @@ export class EditorStore {
|
||||
graphMatrix?: Matrix;
|
||||
graph!: TreeGraph;
|
||||
|
||||
editNode: EditNode | number | null = null;
|
||||
|
||||
dispatch!: (event: EditEvent, data?: unknown) => void;
|
||||
|
||||
constructor(path: string) {
|
||||
@@ -142,8 +134,12 @@ export type FileMeta = {
|
||||
exists?: boolean;
|
||||
};
|
||||
|
||||
interface WorkspaceModel {
|
||||
export interface WorkspaceModel {
|
||||
files?: { path: string; desc: string }[];
|
||||
settings: {
|
||||
checkExpr?: boolean;
|
||||
buildScript?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type WorkspaceStore = {
|
||||
@@ -153,9 +149,14 @@ export type WorkspaceStore = {
|
||||
batchProject: () => void;
|
||||
buildProject: () => void;
|
||||
|
||||
settings: WorkspaceModel["settings"];
|
||||
workdir: string;
|
||||
path: string;
|
||||
|
||||
// settings
|
||||
setCheckExpr: (checkExpr: boolean) => void;
|
||||
setupBuildScript: () => void;
|
||||
|
||||
loadWorkspace: () => void;
|
||||
saveWorkspace: () => void;
|
||||
updateFileMeta: (editor: EditorStore) => void;
|
||||
@@ -170,8 +171,8 @@ export type WorkspaceStore = {
|
||||
isShowingSearch: boolean;
|
||||
onShowingSearch: (isShowingSearch: boolean) => void;
|
||||
|
||||
open: (path: string, selectedNode?: number) => void;
|
||||
edit: (path: string, selectedNode?: number) => void;
|
||||
open: (path: string, selectedId?: number) => void;
|
||||
edit: (path: string, selectedId?: number) => void;
|
||||
close: (path: string) => void;
|
||||
find: (path: string) => EditorStore | undefined;
|
||||
relative: (path: string) => string;
|
||||
@@ -250,6 +251,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
modifiedEditors: [],
|
||||
workdir: "",
|
||||
path: "",
|
||||
settings: {},
|
||||
|
||||
init: (path) => {
|
||||
const workspace = get();
|
||||
@@ -272,6 +274,40 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
// setting
|
||||
setCheckExpr: (checkExpr: boolean) => {
|
||||
const { settings, saveWorkspace } = get();
|
||||
set({
|
||||
settings: {
|
||||
...settings,
|
||||
checkExpr,
|
||||
},
|
||||
});
|
||||
b3util.setCheckExpr(checkExpr);
|
||||
saveWorkspace();
|
||||
useWorkspace.getState().editing?.dispatch("refresh");
|
||||
},
|
||||
|
||||
setupBuildScript: () => {
|
||||
const workspace = get();
|
||||
let buildScript = dialog.showOpenDialogSync({
|
||||
properties: ["openFile"],
|
||||
defaultPath: workspace.workdir,
|
||||
filters: [{ name: "Javascript", extensions: ["js"] }],
|
||||
})?.[0];
|
||||
if (buildScript) {
|
||||
buildScript = Path.posixPath(buildScript);
|
||||
const { settings, saveWorkspace, relative } = get();
|
||||
set({
|
||||
settings: {
|
||||
...settings,
|
||||
buildScript: relative(buildScript),
|
||||
},
|
||||
});
|
||||
saveWorkspace();
|
||||
}
|
||||
},
|
||||
|
||||
createProject: () => {
|
||||
const path = dialog.showSaveDialogSync({
|
||||
properties: ["showOverwriteConfirmation", "createDirectory"],
|
||||
@@ -355,30 +391,46 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
for (const editor of workspace.editors) {
|
||||
editor.dispatch("save");
|
||||
}
|
||||
const debug = console.debug;
|
||||
console.debug = () => {};
|
||||
try {
|
||||
let hasError = false;
|
||||
|
||||
let buildScript: b3util.BuildScript | undefined;
|
||||
if (workspace.settings.buildScript) {
|
||||
const scriptPath = workspace.workdir + "/" + workspace.settings.buildScript;
|
||||
if (fs.existsSync(scriptPath)) {
|
||||
buildScript = eval(fs.readFileSync(scriptPath, "utf8"));
|
||||
}
|
||||
}
|
||||
|
||||
workspace.allFiles.forEach((file) => {
|
||||
const buildpath = buildDir + "/" + workspace.relative(file.path);
|
||||
const treeModel = b3util.createBuildData(file.path);
|
||||
if (!treeModel) {
|
||||
let tree = b3util.createBuildData(file.path);
|
||||
if (buildScript) {
|
||||
tree = b3util.processBatch(tree, file.path, buildScript);
|
||||
}
|
||||
|
||||
if (!tree) {
|
||||
return;
|
||||
}
|
||||
if (treeModel.export === false) {
|
||||
|
||||
if (tree.export === false) {
|
||||
console.log("skip:", buildpath);
|
||||
return;
|
||||
}
|
||||
console.log("build:", buildpath);
|
||||
const declare: FileVarDecl = {
|
||||
import: treeModel.import.map((v) => ({ path: v, vars: [], depends: [] })),
|
||||
declvar: treeModel.declvar.map((v) => ({ name: v.name, desc: v.desc })),
|
||||
import: tree.import.map((v) => ({ path: v, vars: [], depends: [] })),
|
||||
declvar: tree.declvar.map((v) => ({ name: v.name, desc: v.desc })),
|
||||
subtree: [],
|
||||
};
|
||||
b3util.refreshDeclare(treeModel.root, treeModel.group, declare);
|
||||
if (!b3util.checkNodeData(treeModel?.root)) {
|
||||
b3util.refreshDeclare(tree.root, tree.group, declare);
|
||||
if (!b3util.checkNodeData(tree?.root)) {
|
||||
hasError = true;
|
||||
}
|
||||
fs.mkdirSync(Path.dirname(buildpath), { recursive: true });
|
||||
fs.writeFileSync(buildpath, JSON.stringify(treeModel, null, 2));
|
||||
fs.writeFileSync(buildpath, JSON.stringify(tree, null, 2));
|
||||
});
|
||||
if (hasError) {
|
||||
message.error(i18n.t("buildFailed"));
|
||||
@@ -390,6 +442,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
console.error(error);
|
||||
message.error(i18n.t("buildFailed"));
|
||||
}
|
||||
console.debug = debug;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -405,28 +458,10 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
try {
|
||||
console.log("run script", scriptPath);
|
||||
const str = fs.readFileSync(scriptPath, "utf8");
|
||||
const batch = eval(str) as BatchScript;
|
||||
const batch = eval(str) as b3util.BuildScript;
|
||||
workspace.allFiles.forEach((file) => {
|
||||
let tree: TreeModel | undefined = readTree(file.path);
|
||||
if (batch.processTree && tree) {
|
||||
tree = batch.processTree(tree, file.path);
|
||||
}
|
||||
if (tree && batch.processNode) {
|
||||
const processNode = (node: NodeModel) => {
|
||||
if (node.children) {
|
||||
const children: NodeModel[] = [];
|
||||
node.children?.forEach((child) => {
|
||||
const newChild = processNode(child);
|
||||
if (newChild) {
|
||||
children.push(newChild);
|
||||
}
|
||||
});
|
||||
node.children = children;
|
||||
}
|
||||
return batch.processNode?.(node, tree!);
|
||||
};
|
||||
tree.root = processNode(tree.root) ?? ({} as NodeModel);
|
||||
}
|
||||
let tree: TreeModel | null = readTree(file.path);
|
||||
tree = b3util.processBatch(tree, file.path, batch);
|
||||
if (tree) {
|
||||
fs.writeFileSync(file.path, JSON.stringify(tree, null, 2));
|
||||
}
|
||||
@@ -445,7 +480,9 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
|
||||
loadWorkspace: () => {
|
||||
const workspace = get();
|
||||
const data = readJson(workspace.path) as WorkspaceModel;
|
||||
const data = readWorkspace(workspace.path);
|
||||
set({ settings: data.settings });
|
||||
b3util.setCheckExpr(data.settings?.checkExpr ?? true);
|
||||
data.files?.forEach((file) => {
|
||||
workspace.allFiles.set(file.path, { path: file.path, desc: file.desc, exists: false });
|
||||
});
|
||||
@@ -455,6 +492,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
const workspace = get();
|
||||
const data: WorkspaceModel = {
|
||||
files: [],
|
||||
settings: workspace.settings,
|
||||
};
|
||||
workspace.allFiles.forEach((file) => {
|
||||
data.files?.push({
|
||||
@@ -484,7 +522,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
set({ isShowingSearch });
|
||||
},
|
||||
|
||||
open: (path, selectedNode) => {
|
||||
open: (path, selectedId) => {
|
||||
path = Path.posixPath(path);
|
||||
const workspace = get();
|
||||
let editor = workspace.editors.find((v) => v.path === path);
|
||||
@@ -494,7 +532,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
workspace.editors.push(editor);
|
||||
set({ editors: workspace.editors });
|
||||
workspace.updateFileMeta(editor);
|
||||
workspace.edit(editor.path, selectedNode);
|
||||
workspace.edit(editor.path, selectedId);
|
||||
|
||||
if (b3util.isNewVersion(editor.data.version)) {
|
||||
message.warning(i18n.t("alertNewVersion", { version: editor.data.version }));
|
||||
@@ -504,26 +542,20 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
message.error(`invalid file: ${path}`);
|
||||
}
|
||||
} else if (workspace.editing !== editor) {
|
||||
workspace.edit(editor.path, selectedNode);
|
||||
workspace.edit(editor.path, selectedId);
|
||||
}
|
||||
},
|
||||
|
||||
edit: (path, selectedNode) => {
|
||||
edit: (path, selectedId) => {
|
||||
const workspace = get();
|
||||
const editor = workspace.editors.find((v) => v.path === path);
|
||||
if (editor && selectedNode) {
|
||||
editor.editNode = selectedNode;
|
||||
if (editor && selectedId) {
|
||||
editor.selectedId = selectedId.toString();
|
||||
}
|
||||
set({ editing: editor, editingNode: null, editingTree: null });
|
||||
if (editor) {
|
||||
workspace.refresh(editor.path);
|
||||
if (!editor.editNode) {
|
||||
workspace.onEditingTree(editor);
|
||||
} else if (typeof editor.editNode === "object") {
|
||||
workspace.onEditingNode(editor.editNode);
|
||||
}
|
||||
} else {
|
||||
set({ editingNode: null, editingTree: null });
|
||||
workspace.onEditingTree(editor);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -675,27 +707,16 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
|
||||
// node edit
|
||||
onEditingNode: (node) => {
|
||||
const workspace = get();
|
||||
if (workspace.editing) {
|
||||
workspace.editing.editNode = node;
|
||||
}
|
||||
set({ editingNode: node, editingNodeDef: null, editingTree: null });
|
||||
},
|
||||
|
||||
onEditingNodeDef: (nodeDef) => {
|
||||
const workspace = get();
|
||||
if (workspace.editing) {
|
||||
workspace.editing.editNode = null;
|
||||
}
|
||||
set({ editingNodeDef: nodeDef, editingNode: null, editingTree: null });
|
||||
},
|
||||
|
||||
// tree edit
|
||||
onEditingTree: (editor) => {
|
||||
const workspace = get();
|
||||
if (workspace.editing) {
|
||||
workspace.editing.editNode = null;
|
||||
}
|
||||
workspace.refresh(editor.path);
|
||||
set({
|
||||
editingTree: {
|
||||
|
||||
+73
-5
@@ -1,5 +1,5 @@
|
||||
import * as fs from "fs";
|
||||
import { NodeDef } from "../behavior3/src/behavior3";
|
||||
import { ExpressionEvaluator, NodeDef } from "../behavior3/src/behavior3";
|
||||
import {
|
||||
FileVarDecl,
|
||||
ImportDef,
|
||||
@@ -20,6 +20,16 @@ import {
|
||||
import Path from "./path";
|
||||
import { readJson, readTree } from "./util";
|
||||
|
||||
export interface BatchScript {
|
||||
processTree?(tree: TreeModel, path: string): TreeModel | null;
|
||||
|
||||
processNode?(node: NodeModel, tree: TreeModel): NodeModel | null;
|
||||
}
|
||||
|
||||
export interface BuildScript extends BatchScript {
|
||||
writeFile?(path: string, tree: TreeModel): void;
|
||||
}
|
||||
|
||||
export class NodeDefs extends Map<string, NodeDef> {
|
||||
get(key: string): NodeDef {
|
||||
return super.get(key) ?? unknownNodeDef;
|
||||
@@ -34,6 +44,7 @@ export const files: Record<string, number> = {};
|
||||
|
||||
const parsedVarDefs: Record<string, ImportDef> = {};
|
||||
const parsedExprs: Record<string, string[]> = {};
|
||||
let checkExpr: boolean = false;
|
||||
let workdir: string = "";
|
||||
let alertError: (msg: string, duration?: number) => void = () => {};
|
||||
|
||||
@@ -78,6 +89,10 @@ export const updateUsingVars = (vars: VarDef[]) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setCheckExpr = (check: boolean) => {
|
||||
checkExpr = check;
|
||||
};
|
||||
|
||||
export const parseExpr = (expr: string) => {
|
||||
if (parsedExprs[expr]) {
|
||||
return parsedExprs[expr];
|
||||
@@ -323,11 +338,13 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.args && conf.args) {
|
||||
for (const arg of conf.args) {
|
||||
const value = data.args?.[arg.name] as string | string[] | undefined;
|
||||
if (isExprType(arg.type)) {
|
||||
if (data.args && conf.args) {
|
||||
for (const arg of conf.args) {
|
||||
const value = data.args?.[arg.name] as string | string[] | undefined;
|
||||
if (isExprType(arg.type) && value) {
|
||||
if (usingVars) {
|
||||
const vars: string[] = [];
|
||||
if (typeof value === "string") {
|
||||
vars.push(...parseExpr(value));
|
||||
@@ -343,6 +360,27 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (checkExpr) {
|
||||
const exprs: string[] = [];
|
||||
if (typeof value === "string") {
|
||||
exprs.push(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
exprs.push(v);
|
||||
}
|
||||
}
|
||||
for (const expr of exprs) {
|
||||
try {
|
||||
if (!new ExpressionEvaluator(expr).dryRun()) {
|
||||
error(data, `expr '${expr}' is not valid`);
|
||||
hasError = true;
|
||||
}
|
||||
} catch (e) {
|
||||
error(data, `expr '${expr}' is not valid`);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -740,6 +778,35 @@ export const createBuildData = (path: string) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const processBatch = (tree: TreeModel | null, path: string, batch: BatchScript) => {
|
||||
if (!tree) {
|
||||
return null;
|
||||
}
|
||||
if (batch.processTree) {
|
||||
tree = batch.processTree(tree, path);
|
||||
}
|
||||
if (!tree) {
|
||||
return null;
|
||||
}
|
||||
if (batch.processNode) {
|
||||
const processNode = (node: NodeModel) => {
|
||||
if (node.children) {
|
||||
const children: NodeModel[] = [];
|
||||
node.children?.forEach((child) => {
|
||||
const newChild = processNode(child);
|
||||
if (newChild) {
|
||||
children.push(newChild);
|
||||
}
|
||||
});
|
||||
node.children = children;
|
||||
}
|
||||
return batch.processNode?.(node, tree);
|
||||
};
|
||||
tree.root = processNode(tree.root) ?? ({} as NodeModel);
|
||||
}
|
||||
return tree;
|
||||
};
|
||||
|
||||
export const createFileData = (data: TreeGraphData, includeSubtree?: boolean) => {
|
||||
const nodeData: NodeModel = {
|
||||
id: Number(data.id),
|
||||
@@ -887,4 +954,5 @@ export const refreshDeclare = (
|
||||
});
|
||||
updateUsingGroups(group);
|
||||
updateUsingVars(Array.from(vars));
|
||||
console.debug("refresh declare", group, vars);
|
||||
};
|
||||
+9
-2
@@ -1,13 +1,20 @@
|
||||
import * as fs from "fs";
|
||||
import { type WorkspaceModel } from "../contexts/workspace-context";
|
||||
import { VERSION, type TreeModel } from "./b3type";
|
||||
|
||||
export const readJson = (path: string) => {
|
||||
export const readJson = <T>(path: string): T => {
|
||||
const str = fs.readFileSync(path, "utf-8");
|
||||
return JSON.parse(str);
|
||||
};
|
||||
|
||||
export const readWorkspace = (path: string) => {
|
||||
const data = readJson(path) as WorkspaceModel;
|
||||
data.settings = data.settings ?? {};
|
||||
return data;
|
||||
};
|
||||
|
||||
export const readTree = (path: string) => {
|
||||
const data = readJson(path);
|
||||
const data = readJson(path) as TreeModel;
|
||||
data.version = data.version ?? VERSION;
|
||||
data.firstid = data.firstid ?? 1;
|
||||
data.group = data.group || [];
|
||||
|
||||
Reference in new issue
Block a user