reveal file or node def in explorer

This commit is contained in:
codetypes committed 2025-03-22 11:13:15 +08:00
1 parent eba675b0ab
commit d595ce4a3a
2 files changed
+310 -239

No files matched your search

+304 -235
View File
@@ -22,6 +22,8 @@ import Path from "../misc/path";
const { DirectoryTree } = Tree; const { DirectoryTree } = Tree;
let timeout: NodeJS.Timeout;
type MenuInfo = Parameters<Exclude<MenuProps["onClick"], undefined>>[0]; type MenuInfo = Parameters<Exclude<MenuProps["onClick"], undefined>>[0];
type MenuEvent = type MenuEvent =
| "open" | "open"
@@ -273,6 +275,280 @@ const createFolderContextMenu = (copiedPath: string) => {
return arr; return arr;
}; };
const createNodeTree = (rootName: string, nodeDefs: b3util.NodeDefs) => {
const workspace = useWorkspace.getState();
const data: NodeTreeType = {
title: i18n.t("nodeDefinition"),
path: rootName,
icon: (
<Flex justify="center" align="center" style={{ height: "100%" }}>
<PiTreeStructureFill size={19} />
</Flex>
),
children: [],
style: {
fontWeight: "bold",
fontSize: "13px",
},
};
nodeDefs.forEach((nodeDef) => {
(nodeDef.group || [""]).forEach((g) => {
const typeGroup = !g ? nodeDef.type : `${nodeDef.type} (${g})`;
let catalog = data.children?.find((nt) => nt.title === typeGroup);
if (!catalog) {
const type = getNodeType(nodeDef);
catalog = {
title: typeGroup,
path: `nodeTree.catalog.${typeGroup}`,
children: [],
icon: (
<Flex justify="center" align="center" style={{ height: "100%" }}>
<img
className="b3-node-icon"
style={{ width: "13px", height: "13px", color: "white" }}
src={`./icons/${type}.svg`}
/>
</Flex>
),
};
data.children?.push(catalog);
}
catalog.children?.push({
title: `${nodeDef.name}(${nodeDef.desc})`,
isLeaf: true,
def: nodeDef,
path: `${nodeDef.name}(${g})`,
icon: (
<Flex justify="center" align="center" style={{ height: "100%" }}>
{nodeDef.icon ? (
<img
className="b3-node-icon"
style={{ width: "13px", height: "13px", color: "white" }}
src={`file:///${workspace.workdir}/${nodeDef.icon}`}
/>
) : (
<BsBoxFill style={{ width: "12px", height: "12px", color: "white" }} />
)}
</Flex>
),
});
});
});
data.children?.sort((a, b) => a.title.localeCompare(b.title));
data.children?.forEach((child) => child.children?.sort((a, b) => a.title.localeCompare(b.title)));
return data;
};
const isElementInViewport = (element: Element, container: Element) => {
const elementRect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
return elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom;
};
const scrollIntoView = (path: string) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
const element = document.querySelector(`[title="${path}"]`);
const container = element?.closest(".b3-overflow") || element?.closest(".b3-explorer");
if (element && container && !isElementInViewport(element, container)) {
element.scrollIntoView({
behavior: "auto",
block: "center",
});
}
}, 300);
};
const alertReplaceFile = (path: string, newPath: string) => {
const workspace = useWorkspace.getState();
const alert = modal.confirm({
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{i18n.t("explorer.replaceFile", { name: Path.basename(newPath) })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
danger
onClick={() => {
workspace.close(newPath);
renameFile(path, newPath);
alert.destroy();
}}
>
{i18n.t("replace")}
</Button>
<Button
type="primary"
onClick={() => {
alert.destroy();
}}
>
{i18n.t("cancel")}
</Button>
</Flex>
</Flex>
),
});
};
const doMoveFile = (path: string, newPath: string) => {
const workspace = useWorkspace.getState();
const title = Path.basename(path);
const destDir = Path.dirname(newPath);
const doMove = () => {
fs.renameSync(path, newPath);
for (const editor of workspace.editors) {
if (editor.path.startsWith(path)) {
editor.dispatch("rename", destDir + "/" + Path.basename(editor.path));
}
console.log("editor move", editor.path === newPath, editor.path, newPath);
if (editor.path.startsWith(newPath)) {
console.log("editor reload", editor.path === newPath, editor.path, newPath);
editor.dispatch("refresh");
}
}
};
if (!fs.existsSync(newPath)) {
doMove();
return;
}
const alert = modal.confirm({
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{i18n.t("explorer.replaceFile", { name: title })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
danger
onClick={() => {
console.log("close file", newPath);
workspace.close(path);
doMove();
alert.destroy();
}}
>
{i18n.t("replace")}
</Button>
<Button
type="primary"
onClick={() => {
alert.destroy();
}}
>
{i18n.t("cancel")}
</Button>
</Flex>
</Flex>
),
});
};
const alertDeleteFile = (path: string) => {
const workspace = useWorkspace.getState();
const title = Path.basename(path);
const alert = modal.confirm({
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{i18n.t("explorer.deleteFile", { name: title })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
onClick={() => {
alert.destroy();
if (path === workspace.editing?.path) {
workspace.close(path);
}
ipcRenderer.invoke("trashItem", path);
}}
>
{i18n.t("moveToTrash")}
</Button>
<Button
type="primary"
onClick={() => {
alert.destroy();
}}
>
{i18n.t("cancel")}
</Button>
</Flex>
<div style={{ fontSize: "11px", textAlign: "center" }}>
{i18n.t("explorer.restoreFileInfo")}
</div>
</Flex>
),
});
};
const alertDeleteFolder = (path: string) => {
const workspace = useWorkspace.getState();
const title = Path.basename(path);
const alert = modal.confirm({
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{i18n.t("explorer.deleteFolder", { name: title })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
onClick={() => {
workspace.editors.forEach((editor) => {
if (editor.path.startsWith(path + "/")) {
workspace.close(editor.path);
}
});
ipcRenderer.invoke("trashItem", path);
alert.destroy();
}}
>
{i18n.t("moveToTrash")}
</Button>
<Button
type="primary"
onClick={() => {
alert.destroy();
}}
>
{i18n.t("cancel")}
</Button>
</Flex>
<div style={{ fontSize: "11px", textAlign: "center" }}>
{i18n.t("explorer.restoreFileInfo")}
</div>
</Flex>
),
});
};
export const Explorer: FC = () => { export const Explorer: FC = () => {
const workspace = useWorkspace( const workspace = useWorkspace(
useShallow((state) => ({ useShallow((state) => ({
@@ -300,6 +576,7 @@ export const Explorer: FC = () => {
const [selectedNodedefKeys, setSelectedNodedefKeys] = useState<string[]>([]); const [selectedNodedefKeys, setSelectedNodedefKeys] = useState<string[]>([]);
const [expandedNodedefKeys, setExpandedNodedefKeys] = useState<React.Key[]>([rootNodedefName]); const [expandedNodedefKeys, setExpandedNodedefKeys] = useState<React.Key[]>([rootNodedefName]);
// set the icon of the root node
if (workspace.fileTree) { if (workspace.fileTree) {
workspace.fileTree.icon = ( workspace.fileTree.icon = (
<Flex justify="center" align="center" style={{ height: "100%" }}> <Flex justify="center" align="center" style={{ height: "100%" }}>
@@ -308,71 +585,10 @@ export const Explorer: FC = () => {
); );
} }
const nodeTree = useMemo(() => { const nodeTree = useMemo(
const data: NodeTreeType = { () => createNodeTree(rootNodedefName, workspace.nodeDefs),
title: t("nodeDefinition"), [t, workspace.nodeDefs]
path: rootNodedefName,
icon: (
<Flex justify="center" align="center" style={{ height: "100%" }}>
<PiTreeStructureFill size={19} />
</Flex>
),
children: [],
style: {
fontWeight: "bold",
fontSize: "13px",
},
};
workspace.nodeDefs.forEach((nodeDef) => {
(nodeDef.group || [""]).forEach((g) => {
const typeGroup = !g ? nodeDef.type : `${nodeDef.type} (${g})`;
let catalog = data.children?.find((nt) => nt.title === typeGroup);
if (!catalog) {
const type = getNodeType(nodeDef);
catalog = {
title: typeGroup,
path: `nodeTree.catalog.${typeGroup}`,
children: [],
icon: (
<Flex justify="center" align="center" style={{ height: "100%" }}>
<img
className="b3-node-icon"
style={{ width: "13px", height: "13px", color: "white" }}
src={`./icons/${type}.svg`}
/>
</Flex>
),
};
data.children?.push(catalog);
}
catalog.children?.push({
title: `${nodeDef.name}(${nodeDef.desc})`,
isLeaf: true,
def: nodeDef,
path: `${nodeDef.name}(${g})`,
icon: nodeDef.icon ? (
<Flex justify="center" align="center" style={{ height: "100%" }}>
<img
className="b3-node-icon"
key={catalog.title}
style={{ width: "13px", height: "13px", color: "white" }}
src={`file:///${workspace.workdir}/${nodeDef.icon}`}
/>
</Flex>
) : (
<Flex justify="center" align="center" style={{ height: "100%" }}>
<BsBoxFill style={{ width: "12px", height: "12px", color: "white" }} />{" "}
</Flex>
),
});
});
});
data.children?.sort((a, b) => a.title.localeCompare(b.title));
data.children?.forEach((child) =>
child.children?.sort((a, b) => a.title.localeCompare(b.title))
); );
return data;
}, [t, workspace.nodeDefs]);
// expand the selected tree // expand the selected tree
useEffect(() => { useEffect(() => {
@@ -384,6 +600,9 @@ export const Explorer: FC = () => {
keys.push(k); keys.push(k);
} }
} }
if (!selectedNodedefKeys.includes(workspace.editing.path)) {
scrollIntoView(workspace.editing.path);
}
setExpandedKeys(keys); setExpandedKeys(keys);
setSelectedKeys([workspace.editing.path]); setSelectedKeys([workspace.editing.path]);
} }
@@ -402,6 +621,9 @@ export const Explorer: FC = () => {
keys.push(k); keys.push(k);
} }
} }
if (!workspace.editingNodeDef.path) {
scrollIntoView(path);
}
setExpandedNodedefKeys(keys); setExpandedNodedefKeys(keys);
setSelectedNodedefKeys([path]); setSelectedNodedefKeys([path]);
} }
@@ -537,41 +759,7 @@ export const Explorer: FC = () => {
if (node.path === newPath) { if (node.path === newPath) {
dispatch("duplicate", node); dispatch("duplicate", node);
} else { } else {
const alert = modal.confirm({ alertReplaceFile(node.path, newPath);
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{t("explorer.replaceFile", { name: Path.basename(copyFile) })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
danger
onClick={() => {
workspace.close(newPath);
renameFile(node.path, newPath);
alert.destroy();
}}
>
{t("replace")}
</Button>
<Button
type="primary"
onClick={() => {
alert.destroy();
}}
>
{t("cancel")}
</Button>
</Flex>
</Flex>
),
});
} }
} else { } else {
fs.copyFileSync(copyFile, newPath); fs.copyFileSync(copyFile, newPath);
@@ -604,87 +792,9 @@ export const Explorer: FC = () => {
return; return;
} }
if (node.isLeaf) { if (node.isLeaf) {
const alert = modal.confirm({ alertDeleteFile(node.path);
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{t("explorer.deleteFile", { name: node.title })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
onClick={() => {
alert.destroy();
if (node.path === workspace.editing?.path) {
workspace.close(node.path);
}
ipcRenderer.invoke("trashItem", node.path);
}}
>
{t("moveToTrash")}
</Button>
<Button
type="primary"
onClick={() => {
alert.destroy();
}}
>
{t("cancel")}
</Button>
</Flex>
<div style={{ fontSize: "11px", textAlign: "center" }}>
{t("explorer.restoreFileInfo")}
</div>
</Flex>
),
});
} else { } else {
const alert = modal.confirm({ alertDeleteFolder(node.path);
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{t("explorer.deleteFolder", { name: node.title })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
onClick={() => {
workspace.editors.forEach((editor) => {
if (editor.path.startsWith(node.path + "/")) {
workspace.close(editor.path);
}
});
ipcRenderer.invoke("trashItem", node.path);
alert.destroy();
}}
>
{t("moveToTrash")}
</Button>
<Button
type="primary"
onClick={() => {
alert.destroy();
}}
>
{t("cancel")}
</Button>
</Flex>
<div style={{ fontSize: "11px", textAlign: "center" }}>
{t("explorer.restoreFileInfo")}
</div>
</Flex>
),
});
} }
break; break;
} }
@@ -695,59 +805,7 @@ export const Explorer: FC = () => {
return; return;
} }
const newPath = destDir + "/" + Path.basename(node.path); const newPath = destDir + "/" + Path.basename(node.path);
const doMove = () => { doMoveFile(node.path, newPath);
fs.renameSync(node.path, newPath);
for (const editor of workspace.editors) {
if (editor.path.startsWith(node.path)) {
editor.dispatch("rename", destDir + "/" + Path.basename(editor.path));
}
console.log("editor move", editor.path === newPath, editor.path, newPath);
if (editor.path.startsWith(newPath)) {
console.log("editor reload", editor.path === newPath, editor.path, newPath);
editor.dispatch("refresh");
}
}
};
if (fs.existsSync(newPath)) {
const alert = modal.confirm({
centered: true,
content: (
<Flex vertical gap="middle">
<div>
<FaExclamationTriangle style={{ fontSize: "60px", color: "#FADB14" }} />
</div>
<div>{t("explorer.replaceFile", { name: Path.basename(node.path) })}</div>
</Flex>
),
footer: (
<Flex vertical gap="middle" style={{ paddingTop: "30px" }}>
<Flex vertical gap="6px">
<Button
danger
onClick={() => {
console.log("close file", newPath);
workspace.close(node.path);
doMove();
alert.destroy();
}}
>
{t("replace")}
</Button>
<Button
type="primary"
onClick={() => {
alert.destroy();
}}
>
{t("cancel")}
</Button>
</Flex>
</Flex>
),
});
} else {
doMove();
}
} catch (error) { } catch (error) {
console.error("move file:", error); console.error("move file:", error);
} }
@@ -801,7 +859,7 @@ export const Explorer: FC = () => {
<DirectoryTree <DirectoryTree
tabIndex={-1} tabIndex={-1}
treeData={workspace.fileTree ? [workspace.fileTree] : []} treeData={workspace.fileTree ? [workspace.fileTree] : []}
fieldNames={{ key: "path" }} fieldNames={{ key: "path", title: "path" }}
expandedKeys={expandedKeys} expandedKeys={expandedKeys}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
onExpand={(keys) => { onExpand={(keys) => {
@@ -818,6 +876,18 @@ export const Explorer: FC = () => {
if (node && !node.editing) { if (node && !node.editing) {
dispatch("open", node); dispatch("open", node);
setSelectedKeys([node.path]); 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) => { onDrop={(info) => {
@@ -898,7 +968,7 @@ export const Explorer: FC = () => {
</Dropdown> </Dropdown>
<DirectoryTree <DirectoryTree
tabIndex={-1} tabIndex={-1}
fieldNames={{ key: "path" }} fieldNames={{ key: "path", title: "path" }}
treeData={[nodeTree]} treeData={[nodeTree]}
expandedKeys={expandedNodedefKeys} expandedKeys={expandedNodedefKeys}
selectedKeys={selectedNodedefKeys} selectedKeys={selectedNodedefKeys}
@@ -928,7 +998,6 @@ export const Explorer: FC = () => {
path: node.path, path: node.path,
}); });
} }
console.info("select node:", node.path, node.def);
setSelectedNodedefKeys([node.path]); setSelectedNodedefKeys([node.path]);
} }
}} }}
+6 -4
View File
@@ -634,10 +634,11 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
if (!fileMeta) { if (!fileMeta) {
fileMeta = { path: fileNode.path }; fileMeta = { path: fileNode.path };
allFiles.set(path, fileMeta); allFiles.set(path, fileMeta);
console.log("add file meta:", path);
} else { } else {
fileMeta.path = fileNode.path; fileMeta.path = fileNode.path;
} }
fileMeta.exists = true; fileMeta.exists = fs.existsSync(fileNode.path);
if (fileMeta.desc === undefined) { if (fileMeta.desc === undefined) {
const file = readJson(fileNode.path) as TreeModel; const file = readJson(fileNode.path) as TreeModel;
fileMeta.desc = file.desc ?? ""; fileMeta.desc = file.desc ?? "";
@@ -650,11 +651,12 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
allFiles.forEach((file, key) => { allFiles.forEach((file, key) => {
if (!file.exists) { if (!file.exists) {
allFiles.delete(key); allFiles.delete(key);
delete b3util.files[key];
updated = true; updated = true;
console.log("delete file meta:", key);
delete b3util.files[key];
} else {
b3util.files[key] = fs.statSync(file.path).mtimeMs;
} }
const modified = fs.statSync(file.path).mtimeMs;
b3util.files[key] = modified;
}); });
set({ allFiles }); set({ allFiles });
if (updated) { if (updated) {