reimplement the editor use lastest antd and other libs, feature of this commit:
1. file manger 2. highlight for click input or output 3. selector of subtress 4. autocomplete for input or output
This commit is contained in:
1 parent
91da6f0071
commit
2e5c08b2ad
84 files changed
+14250
-11038
No files matched your search
@@ -0,0 +1,450 @@
|
||||
import { NodeDef, NodeModel, TreeGraphData, TreeModel, unknownNodeDef } from "@/misc/b3type";
|
||||
import * as b3util from "@/misc/b3util";
|
||||
import { message } from "@/misc/hooks";
|
||||
import i18n from "@/misc/i18n";
|
||||
import { readJson } from "@/misc/json";
|
||||
import Path from "@/misc/path";
|
||||
import { Matrix, TreeGraph } from "@antv/g6";
|
||||
import { BrowserWindow, dialog } from "@electron/remote";
|
||||
import * as fs from "fs";
|
||||
import React from "react";
|
||||
import { create } from "zustand";
|
||||
import { useSetting } from "./setting-context";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
let buildDir: string | undefined;
|
||||
|
||||
interface BatchScript {
|
||||
processTree?(tree: TreeModel): TreeModel;
|
||||
processNode?(node: NodeModel, tree: TreeModel): NodeModel;
|
||||
}
|
||||
|
||||
export type EditEvent =
|
||||
| "save"
|
||||
| "copy"
|
||||
| "paste"
|
||||
| "replace"
|
||||
| "delete"
|
||||
| "insert"
|
||||
| "undo"
|
||||
| "redo"
|
||||
| "reload"
|
||||
| "rename"
|
||||
| "updateTree"
|
||||
| "updateNode"
|
||||
| "editSubtree"
|
||||
| "saveAsSubtree";
|
||||
|
||||
export class EditorStore {
|
||||
path: string;
|
||||
data: TreeGraphData;
|
||||
desc: string;
|
||||
name: string;
|
||||
|
||||
autoId: number = 1;
|
||||
dragSrcId?: string;
|
||||
dragDstId?: string;
|
||||
unsave: boolean = false;
|
||||
historyStack: NodeModel[] = [];
|
||||
historyIndex: number = 0;
|
||||
selectedId?: string | null;
|
||||
|
||||
size = { width: 0, height: 0 };
|
||||
graphMatrix?: Matrix;
|
||||
graph!: TreeGraph;
|
||||
|
||||
dispatch?: (event: EditEvent, data?: unknown) => void;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
|
||||
const file = readJson(path);
|
||||
this.data = b3util.createTreeData(file.root);
|
||||
this.desc = file.desc;
|
||||
this.name = file.name || path.slice(0, -5);
|
||||
this.autoId = b3util.refreshTreeDataId(this.data);
|
||||
this.historyStack.push(b3util.createNode(this.data));
|
||||
this.historyIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export type FileModel = {
|
||||
path: string;
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
desc?: string;
|
||||
isLeaf?: boolean;
|
||||
children?: FileModel[];
|
||||
editing?: boolean;
|
||||
};
|
||||
|
||||
export type EditNode = {
|
||||
data: NodeModel;
|
||||
editable: boolean;
|
||||
};
|
||||
|
||||
export type EditTree = {
|
||||
data: TreeModel;
|
||||
};
|
||||
|
||||
export type WorkspaceStore = {
|
||||
init: (project: string) => void;
|
||||
createProject: () => void;
|
||||
openProject: (project?: string) => void;
|
||||
batchProject: () => void;
|
||||
buildProject: () => void;
|
||||
|
||||
workdir: string;
|
||||
path: string;
|
||||
|
||||
allFiles: string[];
|
||||
fileTree?: FileModel;
|
||||
editors: EditorStore[];
|
||||
editing?: EditorStore;
|
||||
|
||||
isShowingSearch: boolean;
|
||||
onShowingSearch: (isShowingSearch: boolean) => void;
|
||||
|
||||
open: (path: string) => void;
|
||||
edit: (path: string) => void;
|
||||
close: (path: string) => void;
|
||||
find: (path: string) => EditorStore | undefined;
|
||||
|
||||
save: () => void;
|
||||
saveAs: () => void;
|
||||
saveAll: () => void;
|
||||
|
||||
watch(): void;
|
||||
loadTrees: () => void;
|
||||
loadNodeDefs: () => void;
|
||||
|
||||
// edit node
|
||||
editingNode?: EditNode | null;
|
||||
onEditingNode: (node: EditNode) => void;
|
||||
|
||||
// edit tree
|
||||
editingTree?: EditTree | null;
|
||||
onEditingTree: (tree: EditTree | null) => void;
|
||||
|
||||
// node setting
|
||||
nodeDefs: Map<string, NodeDef>;
|
||||
getNodeDef: (name: string) => NodeDef;
|
||||
hasNodeDef: (name: string) => boolean;
|
||||
};
|
||||
|
||||
const loadFileTree = (workdir: string, filename: string) => {
|
||||
const fullpath = fs.realpathSync(`${workdir}/${filename}`);
|
||||
if (!fs.existsSync(fullpath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = fs.statSync(fullpath);
|
||||
if (!(stat.isDirectory() || fullpath.endsWith(".json"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: FileModel = {
|
||||
path: fullpath.replaceAll(Path.sep, "/"),
|
||||
title: Path.basename(filename),
|
||||
};
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
data.children = [];
|
||||
const files = fs.readdirSync(data.path);
|
||||
files.forEach((v) => {
|
||||
const child = loadFileTree(workdir, `${filename}/${v}`);
|
||||
if (child) {
|
||||
data.children?.push(child);
|
||||
}
|
||||
});
|
||||
data.children.sort((a, b) => {
|
||||
if ((a.children && b.children) || (!a.children && !b.children)) {
|
||||
return a.title.localeCompare(b.title);
|
||||
} else {
|
||||
return a.children ? -1 : 1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
data.isLeaf = true;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const saveFile = (editor?: EditorStore) => {
|
||||
if (editor?.unsave) {
|
||||
editor.dispatch?.("save");
|
||||
}
|
||||
};
|
||||
|
||||
export const useWorkspace = create<WorkspaceStore>((set, get) => ({
|
||||
allFiles: [],
|
||||
fileTree: undefined,
|
||||
editors: [],
|
||||
workdir: "",
|
||||
path: "",
|
||||
|
||||
init: (path) => {
|
||||
const workspace = get();
|
||||
if (!workspace.workdir) {
|
||||
try {
|
||||
workspace.workdir = Path.dirname(path).replaceAll(Path.sep, "/");
|
||||
workspace.path = path;
|
||||
workspace.loadTrees();
|
||||
workspace.loadNodeDefs();
|
||||
workspace.watch();
|
||||
useSetting.getState().appendRecent(path);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
useSetting.getState().removeRecent(path);
|
||||
message.error(`load workspace error: ${path}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createProject: () => {
|
||||
const path = dialog.showSaveDialogSync({
|
||||
properties: ["showOverwriteConfirmation", "createDirectory"],
|
||||
filters: [{ name: "Behavior3 Workspace", extensions: ["b3-workspace"] }],
|
||||
});
|
||||
if (path) {
|
||||
const workspace = get();
|
||||
b3util.createProject(path);
|
||||
workspace.init(path);
|
||||
}
|
||||
},
|
||||
|
||||
openProject: (project?: string) => {
|
||||
if (project) {
|
||||
ipcRenderer.invoke("open-win", project);
|
||||
} else {
|
||||
const win = BrowserWindow.getFocusedWindow();
|
||||
if (win) {
|
||||
const path = dialog.showOpenDialogSync(win, {
|
||||
filters: [{ name: "workspace", extensions: ["b3-workspace"] }],
|
||||
});
|
||||
if (path?.length) {
|
||||
ipcRenderer.invoke("open-win", path[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildProject: () => {
|
||||
const workspace = get();
|
||||
if (!buildDir && workspace.path) {
|
||||
buildDir = dialog.showOpenDialogSync({
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
})?.[0];
|
||||
if (buildDir) {
|
||||
for (const editor of workspace.editors) {
|
||||
editor.dispatch?.("save");
|
||||
}
|
||||
try {
|
||||
for (const path of workspace.allFiles) {
|
||||
const buildpath = buildDir + "/" + Path.relative(workspace.workdir, path);
|
||||
console.log("build:", buildpath);
|
||||
const treeModel = b3util.createBuildData(path);
|
||||
fs.mkdirSync(Path.dirname(buildpath), { recursive: true });
|
||||
fs.writeFileSync(buildpath, JSON.stringify(treeModel, null, 2));
|
||||
}
|
||||
message.success(i18n.t("buildCompleted"));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
batchProject: () => {
|
||||
const workspace = get();
|
||||
const scriptPath = dialog.showOpenDialogSync({
|
||||
properties: ["openFile"],
|
||||
defaultPath: workspace.workdir,
|
||||
filters: [{ name: "Javascript", extensions: ["js"] }],
|
||||
})?.[0];
|
||||
if (scriptPath) {
|
||||
try {
|
||||
console.log("run script", scriptPath);
|
||||
const str = fs.readFileSync(scriptPath, "utf8");
|
||||
const batch = eval(str) as BatchScript;
|
||||
workspace.allFiles.forEach((path) => {
|
||||
const treeStr = fs.readFileSync(path, "utf8");
|
||||
let tree: TreeModel | undefined = JSON.parse(treeStr);
|
||||
if (batch.processTree && tree) {
|
||||
tree = batch.processTree(tree);
|
||||
}
|
||||
if (tree && batch.processNode) {
|
||||
const processNode = (node: NodeModel) => {
|
||||
batch.processNode?.(node, tree!);
|
||||
node.children?.forEach((child) => processNode(child));
|
||||
};
|
||||
processNode(tree.root);
|
||||
}
|
||||
if (tree) {
|
||||
fs.writeFileSync(path, JSON.stringify(tree));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isShowingSearch: false,
|
||||
onShowingSearch: (isShowingSearch) => {
|
||||
set({ isShowingSearch });
|
||||
},
|
||||
|
||||
open: (path) => {
|
||||
const workspace = get();
|
||||
let editor = workspace.editors.find((v) => v.path === path);
|
||||
if (!editor) {
|
||||
try {
|
||||
editor = new EditorStore(path);
|
||||
workspace.editors.push(editor);
|
||||
set({ editors: workspace.editors });
|
||||
workspace.edit(editor.path);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error(`invalid file: ${path}`);
|
||||
}
|
||||
} else if (workspace.editing !== editor) {
|
||||
workspace.edit(editor.path);
|
||||
}
|
||||
},
|
||||
|
||||
edit: (path) => {
|
||||
const workspace = get();
|
||||
const editor = workspace.editors.find((v) => v.path === path);
|
||||
set({ editing: editor, editingNode: null });
|
||||
if (editor) {
|
||||
set({
|
||||
editingTree: {
|
||||
data: {
|
||||
name: editor.name,
|
||||
desc: editor.desc,
|
||||
root: null!,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
set({ editingNode: null, editingTree: null });
|
||||
}
|
||||
},
|
||||
|
||||
close: (path) => {
|
||||
const workspace = get();
|
||||
const idx = workspace.editors.findIndex((v) => v.path === path);
|
||||
const editors = workspace.editors.filter((v) => v.path !== path);
|
||||
let editting = workspace.editing;
|
||||
if (editors.length && path === editting?.path) {
|
||||
editting = editors[idx === editors.length ? idx - 1 : idx];
|
||||
set({
|
||||
editingTree: {
|
||||
data: {
|
||||
name: editting.name,
|
||||
desc: editting.desc,
|
||||
root: null!,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (editors.length === 0) {
|
||||
editting = undefined;
|
||||
set({ editingNode: undefined });
|
||||
}
|
||||
set({ editing: editting, editors: editors });
|
||||
},
|
||||
|
||||
find: (path) => {
|
||||
const workspace = get();
|
||||
return workspace.editors.find((v) => v.path === path);
|
||||
},
|
||||
|
||||
save: () => {
|
||||
const workspace = get();
|
||||
saveFile(workspace.editing);
|
||||
},
|
||||
|
||||
saveAs: () => {},
|
||||
|
||||
saveAll: () => {
|
||||
const workspace = get();
|
||||
for (const editor of workspace.editors) {
|
||||
saveFile(editor);
|
||||
}
|
||||
},
|
||||
|
||||
watch: () => {
|
||||
try {
|
||||
const workspace = get();
|
||||
let hasEvent = false;
|
||||
fs.watch(workspace.workdir, { recursive: true }, (event, filename) => {
|
||||
console.log("fileanme", event, filename);
|
||||
if (event === "rename") {
|
||||
if (!hasEvent) {
|
||||
setTimeout(() => {
|
||||
workspace.loadTrees();
|
||||
hasEvent = false;
|
||||
}, 200);
|
||||
hasEvent = true;
|
||||
}
|
||||
}
|
||||
if (event === "change" && filename === "node-config.b3-setting") {
|
||||
workspace.loadNodeDefs();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
loadTrees: () => {
|
||||
const workspace = get();
|
||||
const data = loadFileTree(workspace.workdir, ".")!;
|
||||
data.title = Path.basename(workspace.workdir);
|
||||
set({ fileTree: data });
|
||||
|
||||
const allFiles: string[] = [];
|
||||
const collect = (fileNode?: FileModel) => {
|
||||
if (fileNode?.isLeaf) {
|
||||
allFiles.push(fileNode.path);
|
||||
}
|
||||
fileNode?.children?.forEach((child) => collect(child));
|
||||
};
|
||||
collect(data);
|
||||
set({ allFiles });
|
||||
},
|
||||
|
||||
loadNodeDefs: () => {
|
||||
const workspace = get();
|
||||
const nodeDefData = readJson(`${workspace.workdir}/node-config.b3-setting`) as NodeDef[];
|
||||
for (const v of nodeDefData) {
|
||||
workspace.nodeDefs.set(v.name, v);
|
||||
}
|
||||
set({ nodeDefs: workspace.nodeDefs });
|
||||
workspace.editing?.dispatch?.("reload");
|
||||
},
|
||||
|
||||
// node edit
|
||||
onEditingNode: (node) => {
|
||||
set({ editingNode: node });
|
||||
},
|
||||
|
||||
// tree edit
|
||||
onEditingTree: (tree) => {
|
||||
set({ editingTree: tree });
|
||||
},
|
||||
|
||||
// node def
|
||||
nodeDefs: new Map(),
|
||||
getNodeDef: (name) => {
|
||||
const workspace = get();
|
||||
unknownNodeDef.desc = i18n.t("node.unknown.desc");
|
||||
return workspace.nodeDefs.get(name) || unknownNodeDef;
|
||||
},
|
||||
hasNodeDef: (name) => {
|
||||
const workspace = get();
|
||||
return workspace.nodeDefs.has(name);
|
||||
},
|
||||
}));
|
||||
Reference in new issue
Block a user