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:
codetypes committed 2024-04-17 10:35:51 +08:00
1 parent 91da6f0071
commit 2e5c08b2ad
84 files changed
+14250 -11038

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
import { TreeGraphData as G6TreeGraphData } from "@antv/g6";
export interface NodeArgOption {
name: string;
value: string | number;
}
export interface NodeArg {
name: string;
type: string;
desc: string;
default?: string;
options?: NodeArgOption[];
}
export interface NodeDef {
name: string;
type?: string;
desc?: string;
args?: NodeArg[];
input?: string[];
output?: string[];
doc?: string;
color?: string;
icon?: string;
}
export interface NodeModel {
id: number;
name: string;
desc?: string;
args?: { [key: string]: any };
input?: string[];
output?: string[];
children?: NodeModel[];
debug?: boolean;
path?: string;
}
export interface TreeModel {
name: string;
desc?: string;
root: NodeModel;
}
export interface TreeGraphData extends G6TreeGraphData {
name: string;
desc?: string;
args?: { [key: string]: any };
input?: string[];
output?: string[];
children?: TreeGraphData[];
def: NodeDef;
debug?: boolean;
parent?: string;
path?: string;
lastModified?: number;
size?: number[];
highlightInput?: boolean;
highlightOutput?: boolean;
}
export const unknownNodeDef: NodeDef = {
name: "unknown",
desc: "新建节点",
type: "Action",
};
+439
View File
@@ -0,0 +1,439 @@
import { useWorkspace } from "@/contexts/workspace-context";
import { NodeModel, TreeGraphData, TreeModel } from "@/misc/b3type";
import * as fs from "fs";
import { message } from "./hooks";
import Path from "./path";
import i18n from "./i18n";
export const isSubtreeRoot = (data: TreeGraphData) => {
return data.path && data.id.toString() !== "1";
};
export const isSubtreeUpdated = (data: TreeGraphData) => {
if (data.path) {
try {
const subtreePath = useWorkspace.getState().workdir + "/" + data.path;
if (fs.statSync(subtreePath).mtimeMs !== data.lastModified) {
return true;
}
} catch (error) {
return true;
}
}
if (data.children) {
for (const child of data.children) {
if (isSubtreeUpdated(child)) {
return true;
}
}
}
return false;
};
export const isNodeEqual = (node1: NodeModel, node2: NodeModel) => {
if (
node1.name === node2.name &&
node1.desc === node2.desc &&
node1.path === node2.path &&
node1.debug === node2.debug
) {
const def = useWorkspace.getState().getNodeDef(node1.name);
for (const arg of def.args ?? []) {
if (node1.args?.[arg.name] !== node2.args?.[arg.name]) {
return false;
}
}
if (def.input?.length) {
for (let i = 0; i < def.input.length; i++) {
if (node1.input?.[i] !== node2.input?.[i]) {
return false;
}
}
}
if (def.output?.length) {
for (let i = 0; i < def.output.length; i++) {
if (node1.output?.[i] !== node2.output?.[i]) {
return false;
}
}
}
return true;
}
return false;
};
export const copyFromNode = (data: TreeGraphData, node: NodeModel) => {
data.name = node.name;
data.debug = node.debug;
data.desc = node.desc;
data.path = node.path;
data.args = node.args;
data.input = node.input;
data.output = node.output;
};
const parsingStack: string[] = [];
export const createNode = (data: TreeGraphData, includeChildren: boolean = true) => {
const node: NodeModel = {
id: Number(data.id),
name: data.name,
desc: data.desc,
path: data.path,
debug: data.debug,
};
if (data.input) {
node.input = [];
for (const v of data.input) {
node.input.push(v ?? "");
}
}
if (data.output) {
node.output = [];
for (const v of data.output) {
node.output.push(v ?? "");
}
}
if (data.args) {
node.args = {};
for (const k in data.args) {
const v = data.args[k];
if (v !== null && v !== undefined) {
node.args[k] = v;
}
}
}
if (data.children && !isSubtreeRoot(data) && includeChildren) {
node.children = [];
for (const child of data.children) {
node.children.push(createNode(child));
}
}
return node;
};
export const refreshTreeDataId = (data: TreeGraphData, id?: number) => {
if (!id) {
id = 1;
}
data.id = (id++).toString();
if (data.children) {
data.children.forEach((child) => {
child.parent = data.id;
id = refreshTreeDataId(child, id);
});
}
return id;
};
export const calcTreeDataSize = (data: TreeGraphData) => {
let height = 40 + 2;
const updateHeight = (obj: any) => {
if (Array.isArray(obj) || (obj && Object.keys(obj).length > 0)) {
const { str, line } = toBreakWord(`参数:${JSON.stringify(obj)}`, 35);
height += 20 * line;
}
};
if (data.path) {
height += 20;
}
updateHeight(data.args);
updateHeight(data.input);
updateHeight(data.output);
return [200, height];
};
export const createTreeData = (node: NodeModel, parent?: string) => {
const workspace = useWorkspace.getState();
let treeData: TreeGraphData = {
id: node.id.toFixed(),
name: node.name,
desc: node.desc,
args: node.args,
input: node.input,
output: node.output,
debug: node.debug,
def: workspace.getNodeDef(node.name),
parent: parent,
};
treeData.size = calcTreeDataSize(treeData);
if (!parent) {
parsingStack.length = 0;
}
if (node.children) {
treeData.children = [];
node.children.forEach((child) => {
treeData.children!.push(createTreeData(child, treeData.id));
});
} else if (node.path) {
if (parsingStack.indexOf(node.path) >= 0) {
treeData.path = node.path;
treeData.size = calcTreeDataSize(treeData);
message.error(`循环引用节点:${node.path}`, 4);
return treeData;
}
parsingStack.push(node.path);
try {
const subtreePath = workspace.workdir + "/" + node.path;
const str = fs.readFileSync(subtreePath, "utf8");
treeData = createTreeData(JSON.parse(str).root, treeData.id);
treeData.lastModified = fs.statSync(subtreePath).mtimeMs;
treeData.path = node.path;
treeData.debug = node.debug;
treeData.size = calcTreeDataSize(treeData);
} catch (error) {
message.error(`解析子树失败:${node.path}`);
console.log("parse subtree:", error);
}
parsingStack.pop();
}
calcTreeDataSize(treeData);
return treeData;
};
export const createBuildData = (path: string) => {
try {
const str = fs.readFileSync(path, "utf8");
const treeModel = JSON.parse(str);
const data = createTreeData(treeModel.root);
refreshTreeDataId(data);
treeModel.root = createFileData(data, true);
return treeModel as NodeModel;
} catch (error) {
console.log("build error:", path, error);
}
return null;
};
export const createFileData = (data: TreeGraphData, includeSubtree?: boolean) => {
const nodeData: NodeModel = {
id: Number(data.id),
name: data.name,
desc: data.desc || undefined,
args: data.args || undefined,
input: data.input || undefined,
output: data.output || undefined,
debug: data.debug || undefined,
path: data.path || undefined,
};
if (data.children?.length && (includeSubtree || !isSubtreeRoot(data))) {
nodeData.children = [];
data.children.forEach((child) => {
nodeData.children!.push(createFileData(child, includeSubtree));
});
}
return nodeData;
};
export const createNewTree = (filename: string) => {
const tree: TreeModel = {
name: Path.basenameWithoutExt(filename),
root: {
id: 1,
name: "Sequence",
},
};
return tree;
};
export const toBreakWord = (str: string, len: number, char = "\n") => {
let strTemp = "";
let line = 1;
if (str.length <= len) {
return { str, line };
}
while (str.length > len) {
strTemp += str.slice(0, len) + char;
str = str.slice(len, str.length);
line++;
}
strTemp += str;
return {
str: strTemp,
line,
};
};
export const createProject = (path: string) => {
fs.writeFileSync(
Path.dirname(path) + "/node-config.b3-setting",
JSON.stringify(
[
{
name: "AlwaysFail",
type: "Decorator",
desc: i18n.t("node.alwaysFail.desc"),
doc: i18n.t("node.alwaysFail.doc"),
},
{
name: "AlwaysSuccess",
type: "Decorator",
desc: i18n.t("node.alwaysSuccess.desc"),
doc: i18n.t("node.alwaysSuccess.doc"),
},
{
name: "Check",
type: "Condition",
desc: i18n.t("node.check.desc"),
args: [
{
name: "value",
type: "code",
desc: i18n.t("node.check.arg1.desc"),
},
],
doc: i18n.t("node.check.doc"),
},
{
name: "Clear",
type: "Action",
desc: i18n.t("node.clear.desc"),
output: [i18n.t("node.clear.output1")],
},
{
name: "ForEach",
type: "Composite",
desc: i18n.t("node.forEach.desc"),
input: [i18n.t("node.forEach.input1")],
output: [i18n.t("node.forEach.output1")],
doc: i18n.t("node.forEach.doc"),
},
{
name: "GetTime",
type: "Action",
desc: i18n.t("node.getTime.desc"),
output: [i18n.t("node.getTime.output1")],
},
{
name: "IsNull",
type: "Condition",
desc: i18n.t("node.isNull.desc"),
input: [i18n.t("node.isNull.input1")],
},
{
name: "Log",
type: "Action",
desc: i18n.t("node.log.desc"),
args: [
{
name: "message",
type: "string",
desc: i18n.t("node.log.arg1.desc"),
},
],
},
{
name: "Loop",
type: "Composite",
desc: i18n.t("node.loop.desc"),
args: [
{
name: "count",
type: "int?",
desc: i18n.t("node.loop.arg1.desc"),
},
],
input: [i18n.t("node.loop.input1") + "?"],
},
{
name: "Not",
type: "Decorator",
desc: i18n.t("node.not.desc"),
doc: i18n.t("node.not.doc"),
},
{
name: "NotNull",
type: "Condition",
desc: i18n.t("node.notNull.desc"),
input: [i18n.t("node.notNull.input")],
},
{
name: "Once",
type: "Composite",
desc: i18n.t("node.once.desc"),
doc: i18n.t("node.once.doc"),
},
{
name: "Parallel",
type: "Composite",
desc: i18n.t("node.parallel.desc"),
doc: i18n.t("node.parallel.doc"),
},
{
name: "Selector",
type: "Composite",
desc: i18n.t("node.selector.desc"),
doc: i18n.t("node.selector.doc"),
},
{
name: "Sequence",
type: "Composite",
desc: i18n.t("node.sequence.desc"),
doc: i18n.t("node.sequence.doc"),
},
{
name: "Wait",
type: "Action",
desc: i18n.t("node.wait.desc"),
args: [
{
name: "time",
type: "float",
desc: i18n.t("node.wait.arg1.desc"),
},
],
},
],
null,
2
)
);
fs.writeFileSync(
Path.dirname(path) + "/example.json",
JSON.stringify(
{
name: "example",
root: {
id: 1,
name: "Sequence",
children: [
{
id: 2,
name: "Log",
args: {
str: "hello",
},
},
{
id: 3,
name: "Wait",
args: {
time: 1,
},
},
],
},
},
null,
2
)
);
fs.writeFileSync(
path,
JSON.stringify(
{
nodeConf: "node-config.b3-setting",
metadata: [],
},
null,
2
)
);
};
+17
View File
@@ -0,0 +1,17 @@
import { App } from "antd";
import type { MessageInstance } from "antd/es/message/interface";
import type { ModalStaticFunctions } from "antd/es/modal/confirm";
import type { NotificationInstance } from "antd/es/notification/interface";
let message: MessageInstance;
let notification: NotificationInstance;
let modal: Omit<ModalStaticFunctions, "warn">;
const setGlobalHooks = () => {
const staticFunction = App.useApp();
message = staticFunction.message;
modal = staticFunction.modal;
notification = staticFunction.notification;
};
export { message, modal, notification, setGlobalHooks };
+45
View File
@@ -0,0 +1,45 @@
import dayjs from "dayjs";
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import HttpApi from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
declare module "i18next" {
interface CustomTypeOptions {
returnNull: false;
}
}
i18n
.use(LanguageDetector)
.use(initReactI18next)
.use(HttpApi)
.init(
{
debug: import.meta.env.NODE_ENV === "development",
returnNull: false,
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
react: {
useSuspense: false,
},
load: "languageOnly",
detection: {
caches: ["localStorage", "sessionStorage", "cookie"],
},
backend: {
loadPath: `locales/{{lng}}.json`,
},
},
function (err, t) {
dayjs.locale(i18n.language);
}
);
i18n.on("languageChanged", (lng) => {
dayjs.locale(lng);
});
export default i18n;
+11
View File
@@ -0,0 +1,11 @@
import * as fs from "fs";
export const readJson = (path: string) => {
const str = fs.readFileSync(path, "utf-8");
return JSON.parse(str);
};
export const writeJson = (path: string, data: any) => {
const str = JSON.stringify(data, undefined, 2);
fs.writeFileSync(path, str, "utf-8");
};
+105
View File
@@ -0,0 +1,105 @@
import { BrowserWindow } from "@electron/remote";
import { MutableRefObject } from "react";
import {
HotkeyCallback,
Keys,
isHotkeyPressed as _isHotkeyPressed,
useHotkeys as _useHotkeys,
} from "react-hotkeys-hook";
import { OptionsOrDependencyArray, RefType } from "react-hotkeys-hook/dist/types";
import { Key } from "ts-key-enum";
export const isMacos = process.platform === "darwin";
let forceTarget: HTMLElement | undefined;
const hotkey = (key: string) => {
if (key.indexOf("ctrl") >= 0 && isMacos) {
key = key.replace("ctrl", Key.Meta);
}
return key.toLowerCase();
};
export const isHotkeyPressed = (key: string) => {
if (!key) {
return false;
} else if (key.indexOf("ctrl") >= 0 && isMacos) {
key = key.replace("ctrl", Key.Meta);
}
return _isHotkeyPressed(key.toLowerCase(), "+");
};
export function useHotkeys<T extends HTMLElement>(
keys: Keys,
callback: HotkeyCallback,
options?: OptionsOrDependencyArray,
dependencies?: OptionsOrDependencyArray
): MutableRefObject<RefType<T>> {
if (keys instanceof Array) {
keys = keys.filter((v) => !!v);
}
return _useHotkeys(keys, callback, options, dependencies);
}
export const Hotkey = {
Backspace: Key.Backspace,
Build: hotkey("ctrl+b"),
CloseEditor: hotkey("ctrl+w"),
Copy: hotkey("ctrl+c"),
Delete: Key.Delete,
Duplicate: hotkey("ctrl+d"),
Enter: Key.Enter,
Escape: Key.Escape,
F2: Key.F2,
Insert: Key.Insert,
MacDelete: isMacos ? hotkey("ctrl+backspace") : "",
Paste: hotkey("ctrl+v"),
Redo: isMacos ? hotkey("shift+ctrl+z") : hotkey("ctrl+y"),
Replace: hotkey("shift+ctrl+v"),
Save: hotkey("ctrl+s"),
Search: hotkey("ctrl+p"),
Undo: hotkey("ctrl+z"),
};
export const setInputFocus = (target: HTMLElement) => {
forceTarget = target;
};
export const sendInputEvent = (key: string) => {
const webContents = BrowserWindow.getFocusedWindow()?.webContents;
if (webContents) {
let keyCode: string | undefined;
const modifiers: Electron.InputEvent["modifiers"] = [];
key.split("+").forEach((v) => {
if (v.indexOf("ctrl") >= 0) {
modifiers.push("ctrl");
} else if (v.indexOf("shift") >= 0) {
modifiers.push("shift");
} else if (v.indexOf("alt") >= 0) {
modifiers.push("alt");
} else if (v.indexOf("meta") >= 0) {
modifiers.push("meta");
} else {
keyCode = v;
}
});
if (!keyCode) {
return;
}
forceTarget?.focus();
modifiers.forEach((mod) => {
webContents.sendInputEvent({
type: "keyDown",
keyCode: mod,
});
});
webContents.sendInputEvent({ type: "keyDown", keyCode: keyCode, modifiers: modifiers });
webContents.sendInputEvent({ type: "keyUp", keyCode: keyCode, modifiers: modifiers });
modifiers.forEach((mod) => {
webContents.sendInputEvent({
type: "keyUp",
keyCode: mod,
});
});
}
};
+16
View File
@@ -0,0 +1,16 @@
import path from "path";
const Path = path;
declare module "path" {
interface PlatformPath {
basenameWithoutExt(path: string): string;
}
}
// eslint-disable-next-line @typescript-eslint/no-shadow
path.basenameWithoutExt = (path: string) => {
return Path.basename(path, Path.extname(path));
};
export default Path;
+31
View File
@@ -0,0 +1,31 @@
import { ThemeConfig, theme } from "antd";
import "../index.scss";
export const themeConfig: ThemeConfig = {
cssVar: true,
algorithm: theme.darkAlgorithm,
token: {
colorBgBase: "#0d1117",
colorBgContainer: "#0d1117",
colorBgElevated: "#161b22",
colorBorderSecondary: "#30363d",
borderRadius: 4,
},
components: {
Tree: {
// directoryNodeSelectedBg: "#010409",
borderRadius: 0,
colorBgContainer: "#010409",
},
Tabs: {
horizontalMargin: "0",
},
Layout: {
headerBg: "#0d1117",
siderBg: "#010409",
},
Dropdown: {
motionDurationMid: "0.1s",
},
},
};