Files
behavior3editor/src/components/graph.ts
T
2025-04-13 14:33:06 +08:00

1003 lines
28 KiB
TypeScript

import {
CanvasEvent as G6CanvasEvent,
Graph as G6Graph,
GraphOptions as G6GraphOptions,
NodeData as G6NodeData,
NodeEvent as G6NodeEvent,
Rect as G6Rect,
IPointerEvent as IG6PointerEvent,
treeToGraphData,
} from "@antv/g6";
import { dialog } from "@electron/remote";
import assert from "assert";
import { clipboard } from "electron";
import * as fs from "fs";
import { ObjectType } from "../behavior3/src/behavior3";
import { EditNode, EditorStore, EditTree, useWorkspace } from "../contexts/workspace-context";
import { ImportDecl, isExprType, NodeData, TreeData, VarDecl } from "../misc/b3type";
import * as b3util from "../misc/b3util";
import { message } from "../misc/hooks";
import i18n from "../misc/i18n";
import Path from "../misc/path";
import { readTree, writeTree } from "../misc/util";
import { TreeNodeState, TreeNodeStyle } from "./register-node";
type G6NodeState = Exclude<G6GraphOptions["node"], undefined>["state"];
type IGraph = {
context: {
behavior?: {
currentTarget: unknown;
};
};
};
const workspace = useWorkspace.getState();
export interface FilterOption {
results: string[];
index: number;
filterStr: string;
filterCase: boolean;
filterFocus: boolean;
filterType: "content" | "id";
placeholder: string;
}
export class Graph {
onChange?: () => void;
onUpdateSearch?: () => void;
private _graph: G6Graph;
private _historyStack: string[] = [];
private _historyIndex: number = 0;
private _dragId?: string;
private _dropId?: string;
private _selectedId: string | null = null;
constructor(readonly editor: EditorStore, ref: React.RefObject<HTMLDivElement>) {
this._graph = new G6Graph({
container: ref.current!,
behaviors: ["drag-canvas", "zoom-canvas", "hover-activate"],
animation: false,
zoomRange: [0.25, 2],
node: {
type: "TreeNode",
style: {
radius: 4,
fill: "white",
size: [260, 50],
ports: [{ placement: "right" }, { placement: "left" }],
},
state: TreeNodeStyle as G6NodeState,
},
edge: {
type: "cubic-horizontal",
style: {
lineWidth: 2,
stroke: "#A3B1BF",
},
animation: {
enter: false,
},
},
layout: {
type: "compact-box",
direction: "LR",
sortBy: (nodeA: G6NodeData, nodeB: G6NodeData) => {
const dataA = nodeA.data as unknown as NodeData;
const dataB = nodeB.data as unknown as NodeData;
return Number(dataA.id) - Number(dataB.id);
},
getHeight: ({ data }: { data: NodeData }) => data.size![1],
getWidth: ({ data }: { data: NodeData }) => data.size![0],
getVGap: () => 20,
getHGap: () => 30,
},
});
this._graph.on(G6CanvasEvent.CLICK, this._onCanvasClick.bind(this));
this._graph.on(G6NodeEvent.CONTEXT_MENU, this._onContextMenu.bind(this));
this._graph.on(G6NodeEvent.CLICK, this._onClick.bind(this));
this._graph.on(G6NodeEvent.DRAG_START, this._onDragStart.bind(this));
this._graph.on(G6NodeEvent.DRAG_END, this._onDragEnd.bind(this));
this._graph.on(G6NodeEvent.DRAG_ENTER, this._onDragEnter.bind(this));
this._graph.on(G6NodeEvent.DRAG_LEAVE, this._onDragLeave.bind(this));
this._graph.on(G6NodeEvent.DRAG, this._onDrag.bind(this));
this._graph.on(G6NodeEvent.DROP, this._onDrop.bind(this));
this._update(editor.data);
this._historyIndex = -1;
this._storeHistory(false);
}
destroy() {
this._graph.destroy();
}
get data() {
return this.editor.data;
}
private _storeHistory(changed: boolean = true) {
if (this._graph.rendered) {
this.editor.data.root = this._nodeToData("1");
}
const str = JSON.stringify(this.data, null, 2);
if (this._historyStack[this._historyIndex] !== str) {
this._historyStack.length = ++this._historyIndex;
this._historyStack.push(str);
if (changed) {
this.onChange?.();
}
}
}
private async _applyHistory(str: string) {
// clear node inspector
this.selectNode(null);
const data = JSON.parse(str) as TreeData;
this.editor.declare.import = data.import.map((v) => ({ path: v, vars: [], depends: [] }));
this.editor.declare.vars = data.vars.map((v) => ({ ...v }));
await this._update(data);
// update tree inspector
this.selectNode(null);
this.onChange?.();
}
private async _update(data: TreeData, refreshId: boolean = true) {
this.editor.data = data;
if (refreshId) {
b3util.refreshNodeData(this.data.root, 1);
}
workspace.refresh(this.editor.path);
// clear current target avoid hover error
const graph = this._graph as unknown as IGraph;
if (graph.context.behavior) {
graph.context.behavior.currentTarget = null;
}
this._graph.clear();
this._graph.setData(
treeToGraphData(data.root, {
getNodeData: (node) => {
return {
id: node.id,
prefix: this.data.prefix,
data: node as unknown as Record<string, unknown>,
children: node.children?.map((child) => child.id),
};
},
})
);
await this._render();
}
setSize(width: number, height: number) {
const [w, h] = this._graph.getSize();
if (w !== width || h !== height) {
this._graph.setSize(width, height);
}
}
async undo() {
if (this._historyIndex > 0) {
await this._applyHistory(this._historyStack[--this._historyIndex]);
}
}
async redo() {
if (this._historyIndex < this._historyStack.length - 1) {
await this._applyHistory(this._historyStack[++this._historyIndex]);
}
}
private _nodeToData(id: string) {
const node = this._graph.getElementData(id) as G6NodeData;
const data = { ...node.data } as unknown as NodeData;
if (node.children) {
data.children = [];
for (const child of node.children) {
data.children.push(this._nodeToData(child));
}
} else {
data.children = undefined;
}
return data;
}
private async _render() {
if (!this._graph.rendered) {
await this._graph.render();
}
const zoom = this._graph.getZoom();
await this._graph.zoomTo(1, false);
const [x, y] = this._graph.getPosition();
await this._graph.translateTo([0, 0], false);
await this._graph.render();
await this._graph.translateTo([x, y], false);
await this._graph.zoomTo(zoom, false);
}
private _getAncestors(id: string): G6NodeData[] {
return this._graph.getAncestorsData(id, "tree") as G6NodeData[];
}
private _findSubtreeRoot(id: string): G6NodeData | null {
const node = this._graph.getNodeData(id);
const data = node.data as unknown as NodeData | undefined;
if (data?.path) {
return node;
} else {
return this._getAncestors(id).find((v) => (v.data as unknown as NodeData)?.path) ?? null;
}
}
private _isSubtreeNode(id: string | null | undefined): boolean {
return !!(id && this._findSubtreeRoot(id));
}
private _findParent(id: string) {
return this._graph.getParentData(id, "tree") as G6NodeData | null;
}
private _findHightlight(
node: NodeData,
highlight: string[],
changed?: [NodeData, TreeNodeState[]][]
) {
changed ||= [];
if (highlight.length > 0) {
const states: TreeNodeState[] = [];
for (const v of node.input ?? []) {
if (highlight.includes(v)) {
states.push("highlightinput");
break;
}
}
for (const v of node.output ?? []) {
if (highlight.includes(v)) {
states.push("highlightoutput");
break;
}
}
const def = b3util.nodeDefs.get(node.name);
loop: for (const arg of def.args ?? []) {
if (isExprType(arg.type)) {
const expr = node.args?.[arg.name] as string | string[] | undefined;
if (typeof expr === "string") {
for (const v of b3util.parseExpr(expr)) {
if (highlight.includes(v)) {
states.push("highlightargs");
break loop;
}
}
} else if (expr instanceof Array) {
for (const str of expr) {
for (const v of b3util.parseExpr(str)) {
if (highlight.includes(v)) {
states.push("highlightargs");
break loop;
}
}
}
}
}
}
if (states.length > 0) {
changed.push([node, states]);
} else {
changed.push([node, ["highlightgray"]]);
}
} else {
changed.push([node, []]);
}
node.children?.forEach((child) => this._findHightlight(child, highlight, changed));
return changed;
}
async expandElement() {
for (const node of this._graph.getNodeData()) {
await this._graph.expandElement(node.id, false);
}
}
clickVar(...names: string[]) {
console.debug("click variable:", names);
const nodes = this._findHightlight(this.data.root, names);
for (const [node, states] of nodes) {
const oldStates = this._getState(node.id).sort();
const newStates = [...oldStates.filter((v) => !this._isHighlightState(v)), ...states].sort();
if (oldStates.length !== newStates.length || oldStates.some((v, i) => v !== newStates[i])) {
this._setState(node.id, newStates);
}
}
if (names.length === 0) {
this.onUpdateSearch?.();
}
}
private _includeString(content: string | undefined, option: FilterOption) {
if (!content || typeof content !== "string") {
return false;
} else if (option.filterCase) {
return content.includes(option.filterStr);
} else {
return content.toLowerCase().includes(option.filterStr.toLowerCase());
}
}
hightlightSearch(option: FilterOption, node: NodeData | null) {
if (!node) {
return;
}
let highlightGray = option.filterFocus && !!option.filterStr;
if (option.filterStr) {
const def = b3util.nodeDefs.get(node.name);
let found = false;
if (option.filterType === "id") {
if (option.filterStr === node.id) {
found = true;
}
} else {
if (
this._includeString(node.name, option) ||
this._includeString(node.desc || def.desc, option)
) {
found = true;
}
if (!found && node.input) {
for (const str of node.input) {
if (this._includeString(str, option)) {
found = true;
break;
}
}
}
if (!found && node.args) {
loop: for (const str in node.args) {
const value = node.args[str];
if (typeof value === "string") {
if (this._includeString(value, option)) {
found = true;
break loop;
}
} else if (value instanceof Array) {
for (const v of value) {
if (this._includeString(v, option)) {
found = true;
break loop;
}
}
}
}
}
if (!found && node.output) {
for (const str of node.output) {
if (this._includeString(str, option)) {
found = true;
break;
}
}
}
if (!found && node.path) {
if (this._includeString(node.path, option)) {
found = true;
}
}
}
if (found) {
option.results.push(node.id);
highlightGray = false;
}
}
const states = this._getState(node.id).filter((v) => !this._isHighlightState(v));
if (highlightGray) {
states.push("highlightgray");
}
this._setState(node.id, states);
node.children?.forEach((child) => this.hightlightSearch(option, child));
}
private _isTreeUpdated(editTree: EditTree) {
if (
this.data.prefix !== editTree.prefix ||
this.data.export !== editTree.export ||
this.data.name !== editTree.name ||
this.data.desc !== editTree.desc
) {
return true;
}
let max = Math.max(this.editor.declare.vars.length, editTree.vars.length);
for (let i = 0; i < max; i++) {
const v1: VarDecl | undefined = this.editor.declare.vars[i];
const v2: VarDecl | undefined = editTree.vars[i];
if (v1?.name !== v2?.name || v1?.desc !== v2?.desc) {
return true;
}
}
max = Math.max(this.data.group.length, editTree.group.length);
for (let i = 0; i < max; i++) {
if (this.data.group[i] !== editTree.group[i]) {
return true;
}
}
max = Math.max(this.editor.declare.import.length, editTree.import.length);
for (let i = 0; i < max; i++) {
const v1: ImportDecl | undefined = this.editor.declare.import[i];
const v2: ImportDecl | undefined = editTree.import[i];
if (v1?.path !== v2?.path) {
return true;
}
}
return false;
}
async updateTree(editTree: EditTree) {
if (this._isTreeUpdated(editTree)) {
this.data.desc = editTree.desc || "";
this.data.export = editTree.export !== false;
this.data.group = editTree.group;
this.data.prefix = editTree.prefix ?? "";
this.data.import = editTree.import.map((v) => v.path).sort();
this.data.vars = editTree.vars
.map((v) => ({ ...v }))
.sort((a, b) => a.name.localeCompare(b.name));
this.editor.declare.vars = editTree.vars || [];
this.editor.declare.import = editTree.import || [];
workspace.refresh(this.editor.path);
await this.refresh();
this._storeHistory();
}
}
async updateNode(editNode: EditNode) {
const node = this._graph.getNodeData(editNode.data.id);
const data = node.data as unknown as NodeData;
if (b3util.isNodeEqual(data, editNode.data)) {
return;
}
// update node
node.data = { ...editNode.data, size: b3util.calcSize(editNode.data) };
this._graph.updateNodeData([node]);
await this._graph.draw();
// update subtree
if (data.path !== editNode.data.path) {
this.editor.data.root = this._nodeToData("1");
await this.refresh();
}
this._storeHistory();
}
async refresh() {
this.selectNode(null);
await this._update(this.data);
this.selectNode(null);
}
async reload() {
this.selectNode(null);
await this._update(readTree(this.editor.path));
this._storeHistory(false);
this.selectNode(null);
}
async focusNode(id: string) {
this.selectNode(id);
await this._graph.translateTo([0, 0], false);
await this._graph.focusElement(id, true);
}
get selectedId() {
return this._selectedId;
}
selectNode(id: string | null) {
if (this._selectedId && id !== this._selectedId) {
this._setState(
this._selectedId,
this._getState(this._selectedId).filter((v) => v !== "selected")
);
}
this._selectedId = id;
if (this._selectedId) {
const node = this._graph.getNodeData(this._selectedId);
const data = node.data as unknown as NodeData;
workspace.onEditingNode({
data: { ...data },
prefix: this.data.prefix,
disabled: this._isSubtreeNode(node.id),
subtreeEditable: !this._isSubtreeNode(this._findParent(node.id)?.id),
});
const states = this._getState(this._selectedId);
this._setState(this._selectedId, [...states, "selected"]);
} else {
workspace.onEditingTree(this.editor);
}
}
private _onContextMenu(e: IG6PointerEvent<G6Rect>) {
this.selectNode(e.target.id);
}
private _onCanvasClick(e: IG6PointerEvent<G6Rect>) {
this.selectNode(null);
}
private _onClick(e: IG6PointerEvent<G6Rect>) {
const names: string[] = [];
const originalTarget = e.originalTarget;
if (originalTarget.className === "input-text") {
const node = this._graph.getNodeData(e.target.id);
const data = node.data as unknown as NodeData;
data.input?.forEach((v) => v && names.push(v));
} else if (originalTarget.className === "output-text") {
const node = this._graph.getNodeData(e.target.id);
const data = node.data as unknown as NodeData;
data.output?.forEach((v) => v && names.push(v));
}
this.clickVar(...names);
this.selectNode(e.target.id);
}
private _isDragState(state: string): boolean {
return (
state === "dragsrc" || state === "dragup" || state === "dragdown" || state === "dragright"
);
}
private _isHighlightState(state: string): boolean {
return (
state === "highlightgray" ||
state === "highlightinput" ||
state === "highlightoutput" ||
state === "highlightargs"
);
}
private _getState(id: string) {
return this._graph.getElementState(id) as TreeNodeState[];
}
private _setState(id: string, states: TreeNodeState[]) {
this._graph.setElementState(id, states);
}
private _clearDragState(id: string) {
const states = this._getState(id).filter((v) => !this._isDragState(v));
this._setState(id, states);
}
private _onDragStart(e: IG6PointerEvent<G6Rect>) {
const { target } = e;
const states = this._getState(target.id);
this._setState(target.id, ["dragsrc", ...states]);
this._dragId = target.id;
console.log("drag start", target.id);
}
private _onDragEnd(e: IG6PointerEvent<G6Rect>) {
if (!this._dragId) {
return;
}
if (e.target.id !== this._dragId) {
console.warn("cancel drag", this._dragId, e.target.id);
this._clearDragState(this._dragId);
}
const { target } = e;
this._clearDragState(target.id);
this._dragId = undefined;
console.log("drag end", target.id);
}
private _onDragEnter(e: IG6PointerEvent<G6Rect>) {
const { target } = e;
if (target.id !== this._dragId) {
this._dropId = target.id;
}
}
private _onDragLeave(e: IG6PointerEvent<G6Rect>) {
const { target } = e;
if (target.id !== this._dragId) {
this._clearDragState(target.id);
this._dropId = undefined;
}
}
private _onDrag(e: IG6PointerEvent<G6Rect>) {
if (!this._dropId) {
return;
}
const id = this._dropId;
const pos = this._graph.getElementPosition(id);
const data = this._graph.getNodeData(id).data as unknown as NodeData;
const [w, h] = data.size!;
const x = e.canvas.x - pos[0] + w / 2;
const y = e.canvas.y - pos[1] + h / 2;
const states = this._getState(id);
let dragto: TreeNodeState | undefined;
if (x > w / 2) {
dragto = "dragright";
} else if (y > h / 2) {
dragto = "dragdown";
} else if (y < h / 2) {
dragto = "dragup";
}
if (dragto && !states.includes(dragto)) {
// console.log(`drag node: drop=${id} dropto=${dragto}`, states);
this._setState(id, [dragto, ...states.filter((v) => !this._isDragState(v))]);
}
}
private async _onDrop(e: IG6PointerEvent<G6Rect>) {
const srcId = this._dragId!;
const dstId = e.target.id;
this._dragId = undefined;
this._dropId = undefined;
const dstStates = this._getState(e.target.id);
const dragto: TreeNodeState = dstStates.find((v) => this._isDragState(v))!;
this._clearDragState(srcId);
this._clearDragState(dstId);
if (srcId === e.target.id) {
console.log("drop same node");
return;
}
const ancestors = this._getAncestors(dstId);
if (
ancestors.some((v) => v.id === srcId) ||
((dragto === "dragdown" || dragto === "dragup") && dstId === "1")
) {
message.error(i18n.t("node.dropDenied"));
return;
}
if (this._isSubtreeNode(dstId)) {
message.error(i18n.t("node.editSubtreeDenied"));
return;
}
console.log(`drop node: drag=${srcId} target=${dstId} dropto=${dragto}`);
if (e.originalEvent instanceof DragEvent) {
// const dragEvent = e.originalEvent as DragEvent;
// const exploreFile = dragEvent.dataTransfer?.getData("explore-file");
// const exploreNode = dragEvent.dataTransfer?.getData("explore-node");
// const dstData = findDataById(destNode.getID());
// if (exploreNode) {
// const newTreeData: TreeGraphData = createTreeData(
// {
// id: editor.autoId++,
// name: exploreNode,
// },
// dstData.id
// );
// dstData.children ||= [];
// dstData.children.push(newTreeData);
// refreshItem(dstData);
// srcNodeId = newTreeData.id;
// } else if (exploreFile && exploreFile !== editor.path) {
// const newTreeData: TreeGraphData = createTreeData(
// {
// id: editor.autoId++,
// name: "unknow",
// path: workspace.relative(exploreFile),
// },
// dstData.id
// );
// dstData.children ||= [];
// dstData.children.push(newTreeData);
// refreshItem(dstData);
// srcNodeId = newTreeData.id;
// editor.autoId = b3util.refreshTreeDataId(newTreeData, Number(srcNodeId));
// }
}
const root = this._nodeToData("1");
const srcParentId = this._graph.getParentData(srcId, "tree")?.id;
const dstParentId = this._graph.getParentData(dstId, "tree")?.id;
let srcData: NodeData | undefined;
let dstData: NodeData | undefined;
let srcParentData: NodeData | undefined;
let dstParentData: NodeData | undefined;
b3util.dfs(root, (node) => {
if (node.id === srcId) {
srcData = node;
}
if (node.id === dstId) {
dstData = node;
}
if (node.id === srcParentId) {
srcParentData = node;
}
if (node.id === dstParentId) {
dstParentData = node;
}
});
assert(srcData, srcId);
assert(dstData, dstId);
assert(srcParentData, srcParentId);
if (dragto === "dragright") {
srcParentData.children?.remove(srcData);
dstData.children ||= [];
dstData.children.push(srcData);
} else if (dragto === "dragup") {
assert(dstParentData, dstParentId);
srcParentData.children?.remove(srcData);
const idx = dstParentData.children!.findIndex((v) => v.id === dstId);
dstParentData.children?.insertAt(idx, srcData);
} else if (dragto === "dragdown") {
assert(dstParentData, dstParentId);
srcParentData.children?.remove(srcData);
const idx = dstParentData.children!.findIndex((v) => v.id === dstId);
dstParentData.children?.insertAt(idx + 1, srcData);
}
await this._update({ ...this.data, root }, false);
this._storeHistory();
}
copyNode() {
if (this._selectedId) {
const node = this._graph.getNodeData(this._selectedId);
if (node) {
const data = node.data as unknown as NodeData;
const str = JSON.stringify(b3util.createNode(data));
clipboard.writeText(str);
}
}
}
async pasteNode() {
if (!this._selectedId) {
message.error(i18n.t("node.noNodeSelected"));
return;
}
if (this._isSubtreeNode(this._selectedId)) {
message.error(i18n.t("node.editSubtreeDenied"));
return;
}
try {
const str = clipboard.readText();
if (!str || str === "") {
return;
}
console.debug("parse node:", str);
const root = this._nodeToData("1");
let dstData: NodeData | undefined;
b3util.dfs(root, (node) => {
if (node.id === this._selectedId) {
dstData = node;
}
});
assert(dstData, this._selectedId);
dstData.children ||= [];
dstData.children.push(JSON.parse(str) as NodeData);
this.selectNode(null);
await this._update({ ...this.data, root });
this._storeHistory();
} catch (error) {
message.error(i18n.t("node.pasteDataError"));
console.log(error);
}
}
async replaceNode() {
if (!this._selectedId) {
message.error(i18n.t("node.noNodeSelected"));
return;
}
if (this._isSubtreeNode(this._selectedId)) {
message.error(i18n.t("node.editSubtreeDenied"));
return;
}
try {
const str = clipboard.readText();
if (!str || str === "") {
return;
}
const root = this._nodeToData("1");
let dstData: NodeData | undefined;
b3util.dfs(root, (node) => {
if (node.id === this._selectedId) {
dstData = node;
}
});
assert(dstData, this._selectedId);
Object.keys(dstData).forEach((k) => delete (dstData as unknown as ObjectType)[k]);
Object.assign(dstData, JSON.parse(str));
this.selectNode(null);
await this._update({ ...this.data, root });
this._storeHistory();
} catch (error) {
message.error(i18n.t("node.pasteDataError"));
console.log(error);
}
}
async createNode() {
if (!this._selectedId) {
message.error(i18n.t("node.noNodeSelected"));
return;
}
if (this._isSubtreeNode(this._selectedId)) {
message.error(i18n.t("node.editSubtreeDenied"));
return;
}
const root = this._nodeToData("1");
let dstData: NodeData | undefined;
b3util.dfs(root, (node) => {
if (node.id === this._selectedId) {
dstData = node;
}
});
assert(dstData, this._selectedId);
dstData.children ||= [];
dstData.children.push({ id: "", name: "unknow" });
await this._update({ ...this.data, root });
this._storeHistory();
}
async deleteNode() {
if (!this._selectedId) {
return;
}
if (this._selectedId === "1") {
message.error(i18n.t("node.deleteRootNodeDenied"));
return;
}
const subtreeRoot = this._getAncestors(this._selectedId)
.reverse()
.find((v) => (v.data as unknown as NodeData)?.path);
if (subtreeRoot && subtreeRoot.id !== this._selectedId) {
message.error(i18n.t("node.editSubtreeDenied"));
return;
}
const root = this._nodeToData("1");
const parentId = this._findParent(this._selectedId)?.id;
b3util.dfs(root, (n) => {
if (n.id === parentId) {
n.children = n.children?.filter((v) => v.id !== this._selectedId);
}
});
this.selectNode(null);
await this._update({ ...this.data, root });
this._storeHistory();
}
private _isSubtreeUpdated(data: NodeData) {
let updated = false;
b3util.dfs(data, (node) => {
if (node.path && b3util.files[node.path] !== node.mtime) {
updated = true;
}
});
return updated;
}
async refreshSubtree() {
if (this._isSubtreeUpdated(this.data.root)) {
this.selectNode(null);
await this.refresh();
this._storeHistory();
}
}
async save() {
if (b3util.isNewVersion(this.data.version)) {
message.error(i18n.t("alertNewVersion", { version: this.data.version }), 6);
return;
}
await this._update({ ...this.data, root: this._nodeToData("1") });
writeTree(this.editor.path, {
...this.data,
root: b3util.createNode(this.data.root),
});
workspace.updateFileMeta(this.editor);
}
editSubtree() {
if (!this._selectedId) {
message.error(i18n.t("node.noNodeSelected"));
return;
}
const node = this._findSubtreeRoot(this._selectedId);
const data = node?.data as unknown as NodeData | undefined;
if (data?.path) {
const path = `${workspace.workdir}/${data.path}`;
workspace.open(path, (Number(this._selectedId) - Number(data.id) + 1).toString());
}
}
async saveAsSubtree() {
if (!this._selectedId) {
message.error(i18n.t("node.noNodeSelected"));
return;
}
if (this._selectedId === "1") {
message.error(i18n.t("node.subtreeSaveRootError"));
return;
}
if (this._isSubtreeNode(this._selectedId)) {
message.error(i18n.t("node.editSubtreeDenied"));
return;
}
const ret = await dialog.showSaveDialog({
defaultPath: workspace.workdir,
properties: ["showOverwriteConfirmation"],
filters: [{ name: "Json", extensions: ["json"] }],
});
if (ret.canceled) {
return;
}
const subpath = ret.filePath.replaceAll(Path.sep, "/");
if (subpath.indexOf(workspace.workdir) === -1) {
message.error(i18n.t("node.subtreePathError"));
return;
}
const node = this._graph.getNodeData(this._selectedId);
const data = node.data as unknown as NodeData;
const subroot = b3util.createFileData(data);
const subtreeModel = {
name: Path.basenameWithoutExt(subpath),
root: subroot,
desc: data.desc,
} as TreeData;
fs.writeFileSync(subpath, JSON.stringify(subtreeModel, null, 2));
data.path = workspace.relative(subpath);
await this.refresh();
this._storeHistory();
}
}