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,738 @@
|
||||
import { FileModel, useWorkspace } from "@/contexts/workspace-context";
|
||||
import * as b3util from "@/misc/b3util";
|
||||
import { modal } from "@/misc/hooks";
|
||||
import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys";
|
||||
import Path from "@/misc/path";
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Button, Dropdown, Flex, FlexProps, Input, MenuProps, Space, Tree } from "antd";
|
||||
import { ItemType } from "antd/es/menu/hooks/useItems";
|
||||
import { ipcRenderer, shell } from "electron";
|
||||
import * as fs from "fs";
|
||||
import React, { FC, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaExclamationTriangle, FaSwatchbook } from "react-icons/fa";
|
||||
import { FiCommand, FiDelete } from "react-icons/fi";
|
||||
import { IoMdReturnLeft } from "react-icons/io";
|
||||
|
||||
const { DirectoryTree } = Tree;
|
||||
|
||||
type MenuInfo = Parameters<Exclude<MenuProps["onClick"], undefined>>[0];
|
||||
type MenuEvent =
|
||||
| "open"
|
||||
| "newFile"
|
||||
| "newFolder"
|
||||
| "revealFile"
|
||||
| "rename"
|
||||
| "delete"
|
||||
| "paste"
|
||||
| "copy"
|
||||
| "move"
|
||||
| "duplicate";
|
||||
|
||||
const findFile = (path: string | undefined, node: FileModel): FileModel | undefined => {
|
||||
if (!path) {
|
||||
return;
|
||||
} else if (node.path === path) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
const ret = findFile(path, child);
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const findParent = (node: FileModel, parent?: FileModel): FileModel | undefined => {
|
||||
if (parent && parent.children) {
|
||||
if (parent.children?.indexOf(node) >= 0) {
|
||||
return parent;
|
||||
}
|
||||
for (const child of parent.children) {
|
||||
const v = findParent(node, child);
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resolveKeys = (path: string, node: FileModel, keys: React.Key[]) => {
|
||||
if (node.path === path) {
|
||||
return true;
|
||||
}
|
||||
if (node.children) {
|
||||
keys.push(node.path);
|
||||
for (const child of node.children) {
|
||||
if (resolveKeys(path, child, keys)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
keys.pop();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// rename file
|
||||
const renameFile = (oldPath: string, newPath: string) => {
|
||||
const workspace = useWorkspace.getState();
|
||||
if (fs.existsSync(newPath)) {
|
||||
return false;
|
||||
} else if (oldPath !== newPath) {
|
||||
try {
|
||||
fs.renameSync(oldPath, newPath);
|
||||
const isDirectory = fs.statSync(newPath).isDirectory();
|
||||
for (const editor of workspace.editors) {
|
||||
if (isDirectory) {
|
||||
if (editor.path.startsWith(oldPath)) {
|
||||
editor.dispatch?.("rename", editor.path.replace(oldPath, newPath));
|
||||
}
|
||||
} else {
|
||||
if (editor.path === oldPath) {
|
||||
editor.dispatch?.("rename", newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const Explorer: FC = () => {
|
||||
const workspace = {
|
||||
close: useWorkspace((state) => state.close),
|
||||
editing: useWorkspace((state) => state.editing),
|
||||
editors: useWorkspace((state) => state.editors),
|
||||
open: useWorkspace((state) => state.open),
|
||||
fileTree: useWorkspace((state) => state.fileTree),
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>(
|
||||
workspace.fileTree?.path ? [workspace.fileTree.path] : []
|
||||
);
|
||||
const [copyFile, setCopyFile] = useState("");
|
||||
const [newName, setNewName] = useState<string | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<ItemType[]>([]);
|
||||
|
||||
if (workspace.fileTree) {
|
||||
workspace.fileTree.icon = <FaSwatchbook style={{ marginTop: "5px" }} />;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace.editing) {
|
||||
const keys: React.Key[] = [];
|
||||
resolveKeys(workspace.editing.path, workspace.fileTree!, keys);
|
||||
for (const k of expandedKeys) {
|
||||
if (keys.indexOf(k) === -1) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
setExpandedKeys(keys);
|
||||
setSelectedKeys([workspace.editing.path]);
|
||||
}
|
||||
}, [workspace.editing]);
|
||||
|
||||
const keysRef = useHotkeys<HTMLDivElement>(
|
||||
[
|
||||
Hotkey.F2,
|
||||
Hotkey.Enter,
|
||||
Hotkey.Copy,
|
||||
Hotkey.Paste,
|
||||
Hotkey.Delete,
|
||||
Hotkey.MacDelete,
|
||||
Hotkey.Escape,
|
||||
Hotkey.Duplicate,
|
||||
],
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const node = findFile(selectedKeys[0], workspace.fileTree!);
|
||||
if (isHotkeyPressed(Hotkey.F2) || (isMacos && isHotkeyPressed(Hotkey.Enter))) {
|
||||
if (node && node !== workspace.fileTree) {
|
||||
node.editing = true;
|
||||
setNewName("");
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Delete) || isHotkeyPressed(Hotkey.MacDelete)) {
|
||||
if (node && node !== workspace.fileTree) {
|
||||
dispatch("delete", node);
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Escape)) {
|
||||
if (node) {
|
||||
node.editing = false;
|
||||
setNewName(null);
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Duplicate)) {
|
||||
if (node && node.isLeaf) {
|
||||
dispatch("duplicate", node);
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Copy)) {
|
||||
if (node && node.isLeaf) {
|
||||
dispatch("copy", node);
|
||||
}
|
||||
} else if (isHotkeyPressed(Hotkey.Paste)) {
|
||||
if (node) {
|
||||
dispatch("paste", node);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const submitRename = (node: FileModel) => {
|
||||
if (!newName) {
|
||||
if (fs.existsSync(node.path)) {
|
||||
node.editing = false;
|
||||
} else {
|
||||
const parent = findParent(node, workspace.fileTree);
|
||||
if (parent && parent.children) {
|
||||
parent.children = parent.children.filter((v) => v !== node);
|
||||
}
|
||||
}
|
||||
setNewName(null);
|
||||
return;
|
||||
}
|
||||
if (node.path.endsWith("/:")) {
|
||||
node.path = Path.dirname(node.path) + "/" + newName.replace(/[^\w. _-]+/g, "");
|
||||
node.title = Path.basename(node.path);
|
||||
fs.mkdirSync(node.path);
|
||||
} else if (node.path.endsWith("/.json")) {
|
||||
node.path = Path.dirname(node.path) + "/" + newName.replace(/[^\w. _-]+/g, "") + ".json";
|
||||
node.title = Path.basename(node.path);
|
||||
fs.writeFileSync(node.path, JSON.stringify(b3util.createNewTree(node.title), null, 2));
|
||||
workspace.open(node.path);
|
||||
} else {
|
||||
const newpath = Path.dirname(node.path) + "/" + newName + Path.extname(node.path);
|
||||
if (renameFile(node.path, newpath)) {
|
||||
setSelectedKeys([newpath]);
|
||||
}
|
||||
}
|
||||
node.editing = false;
|
||||
setNewName(null);
|
||||
};
|
||||
|
||||
const dispatch = (event: MenuEvent, node: FileModel, dest?: FileModel) => {
|
||||
switch (event) {
|
||||
case "open": {
|
||||
workspace.open(node.path);
|
||||
break;
|
||||
}
|
||||
case "newFolder": {
|
||||
const folderNode: FileModel = {
|
||||
path: node.path + "/:",
|
||||
title: "",
|
||||
children: [],
|
||||
editing: true,
|
||||
};
|
||||
node.children?.unshift(folderNode);
|
||||
setNewName("");
|
||||
if (expandedKeys.indexOf(node.path) === -1) {
|
||||
setExpandedKeys([node.path, ...expandedKeys]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "newFile": {
|
||||
const folderNode: FileModel = {
|
||||
path: node.path + "/.json",
|
||||
title: "",
|
||||
isLeaf: true,
|
||||
editing: true,
|
||||
};
|
||||
node.children?.unshift(folderNode);
|
||||
setNewName("");
|
||||
if (expandedKeys.indexOf(node.path) === -1) {
|
||||
setExpandedKeys([node.path, ...expandedKeys]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "paste": {
|
||||
if (copyFile) {
|
||||
let folder = node.path;
|
||||
if (node.isLeaf) {
|
||||
folder = Path.dirname(node.path);
|
||||
}
|
||||
const newPath = folder + "/" + Path.basename(copyFile);
|
||||
if (fs.existsSync(newPath)) {
|
||||
if (node.path === newPath) {
|
||||
dispatch("duplicate", node);
|
||||
} else {
|
||||
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(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 {
|
||||
fs.copyFileSync(copyFile, newPath);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "copy": {
|
||||
setCopyFile(node.path);
|
||||
break;
|
||||
}
|
||||
case "duplicate": {
|
||||
for (let i = 1; ; i++) {
|
||||
const dupName = Path.basenameWithoutExt(node.path) + " " + i + ".json";
|
||||
const dupPath = Path.dirname(node.path) + "/" + dupName;
|
||||
if (!fs.existsSync(dupPath)) {
|
||||
fs.copyFileSync(node.path, dupPath);
|
||||
setSelectedKeys([dupPath]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "delete": {
|
||||
if (node === workspace.fileTree) {
|
||||
return;
|
||||
}
|
||||
if (node.isLeaf) {
|
||||
const alert = modal.confirm({
|
||||
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 {
|
||||
const alert = modal.confirm({
|
||||
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;
|
||||
}
|
||||
case "move": {
|
||||
try {
|
||||
const newPath = dest!.path + "/" + Path.basename(node.path);
|
||||
const doMove = () => {
|
||||
fs.renameSync(node.path, newPath);
|
||||
for (const editor of workspace.editors) {
|
||||
if (editor.path.startsWith(node.path)) {
|
||||
editor.dispatch?.("rename", dest!.path + "/" + 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?.("reload");
|
||||
}
|
||||
}
|
||||
};
|
||||
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) {
|
||||
console.error("move file:", error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "revealFile":
|
||||
shell.showItemInFolder(node.path.replaceAll("/", Path.sep));
|
||||
break;
|
||||
case "rename": {
|
||||
node.editing = true;
|
||||
setNewName("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// context menu
|
||||
const fileContextMenu = useMemo(() => {
|
||||
const MenuItem: FC<FlexProps> = (itemProps) => {
|
||||
return (
|
||||
<Flex
|
||||
gap="50px"
|
||||
style={{ minWidth: "200px", justifyContent: "space-between", alignItems: "center" }}
|
||||
{...itemProps}
|
||||
></Flex>
|
||||
);
|
||||
};
|
||||
const isMac = process.platform === "darwin";
|
||||
const arr: MenuProps["items"] = [
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("open")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "open",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("copy")}</div>
|
||||
<div>{isMac ? "⌘ C" : "Ctrl+C"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "copy",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("duplicate")}</div>
|
||||
<div>{isMac ? "⌘ D" : "Ctrl+D"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "duplicate",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{isMac ? t("revealFileOnMac") : t("revealFileOnWindows")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "revealFile",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("rename")}</div>
|
||||
{isMac && <IoMdReturnLeft />}
|
||||
{!isMac && <div>F2</div>}
|
||||
</MenuItem>
|
||||
),
|
||||
key: "rename",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("delete")}</div>
|
||||
{isMac && (
|
||||
<Space size={6}>
|
||||
<FiCommand />
|
||||
<FiDelete />
|
||||
</Space>
|
||||
)}
|
||||
</MenuItem>
|
||||
),
|
||||
key: "delete",
|
||||
},
|
||||
];
|
||||
return arr;
|
||||
}, [t]);
|
||||
|
||||
const directoryContextMenu = useMemo(() => {
|
||||
const MenuItem: FC<FlexProps> = (itemProps) => {
|
||||
return (
|
||||
<Flex
|
||||
gap="50px"
|
||||
style={{ minWidth: "200px", justifyContent: "space-between", alignItems: "center" }}
|
||||
{...itemProps}
|
||||
></Flex>
|
||||
);
|
||||
};
|
||||
const isMac = process.platform === "darwin";
|
||||
const arr: MenuProps["items"] = [
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("newFile")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "newFile",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("newFolder")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "newFolder",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{isMac ? t("revealFileOnMac") : t("revealFileOnWindows")}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "revealFile",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div style={{ color: copyFile ? "inherit" : "gray" }}>{t("paste")}</div>
|
||||
<div style={{ color: copyFile ? "inherit" : "gray" }}>{isMac ? "⌘ V" : "Ctrl+V"}</div>
|
||||
</MenuItem>
|
||||
),
|
||||
key: "paste",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("rename")}</div>
|
||||
{isMac && <IoMdReturnLeft />}
|
||||
{!isMac && <div>F2</div>}
|
||||
</MenuItem>
|
||||
),
|
||||
key: "rename",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<MenuItem>
|
||||
<div>{t("delete")}</div>
|
||||
{isMac && (
|
||||
<Space size={6}>
|
||||
<FiCommand />
|
||||
<FiDelete />
|
||||
</Space>
|
||||
)}
|
||||
</MenuItem>
|
||||
),
|
||||
key: "delete",
|
||||
},
|
||||
];
|
||||
return arr;
|
||||
}, [t, copyFile]);
|
||||
|
||||
const onClick = (info: MenuInfo) => {
|
||||
const node = findFile(selectedKeys[0], workspace.fileTree!) ?? workspace.fileTree;
|
||||
if (node) {
|
||||
dispatch(info.key as MenuEvent, node);
|
||||
}
|
||||
};
|
||||
|
||||
if (!workspace.fileTree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={keysRef}
|
||||
tabIndex={-1}
|
||||
style={{ height: "100%" }}
|
||||
onContextMenuCapture={() => {
|
||||
setContextMenu(directoryContextMenu);
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
if (workspace.fileTree) {
|
||||
// setSelectedKeys([workspace.fileTree.path]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "12px 24px" }}>
|
||||
<span style={{ fontSize: "18px", fontWeight: "600" }}>{t("explorer.title")}</span>
|
||||
</div>
|
||||
<Flex vertical style={{ overflow: "auto", height: "100%" }}>
|
||||
<Dropdown menu={{ items: contextMenu, onClick }} trigger={["contextMenu"]}>
|
||||
<div>
|
||||
<DirectoryTree
|
||||
tabIndex={-1}
|
||||
treeData={workspace.fileTree ? [workspace.fileTree] : []}
|
||||
fieldNames={{ key: "path" }}
|
||||
onExpand={(keys) => {
|
||||
setExpandedKeys(keys);
|
||||
}}
|
||||
onRightClick={(info) => {
|
||||
if (info.node.isLeaf) {
|
||||
setContextMenu(fileContextMenu);
|
||||
}
|
||||
setSelectedKeys([info.node.path]);
|
||||
}}
|
||||
onSelect={(_, info) => {
|
||||
const node = info.selectedNodes.at(0);
|
||||
if (node) {
|
||||
if (!node.children) {
|
||||
workspace.open(node.path);
|
||||
}
|
||||
setSelectedKeys([node.path]);
|
||||
}
|
||||
if (node && !node.children) {
|
||||
workspace.open(node.path);
|
||||
}
|
||||
}}
|
||||
onDrop={(info) => {
|
||||
dispatch("move", info.dragNode, info.node);
|
||||
}}
|
||||
titleRender={(node) => {
|
||||
if (node.editing) {
|
||||
return (
|
||||
<div style={{ display: "inline-flex" }}>
|
||||
<Input
|
||||
defaultValue={Path.basenameWithoutExt(node.title)}
|
||||
autoFocus
|
||||
style={{ padding: "0px 0px", borderRadius: "2px" }}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onPressEnter={() => {
|
||||
if (newName) {
|
||||
submitRename(node);
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyUp={(e) => {
|
||||
if (e.code === Hotkey.Escape) {
|
||||
if (!fs.existsSync(node.path)) {
|
||||
const parent = findParent(node, workspace.fileTree);
|
||||
if (parent && parent.children) {
|
||||
parent.children = parent.children.filter((v) => v !== node);
|
||||
}
|
||||
}
|
||||
node.editing = false;
|
||||
setNewName(null);
|
||||
}
|
||||
}}
|
||||
onBlur={() => submitRename(node)}
|
||||
></Input>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <span key={node.path}>{node.title}</span>;
|
||||
}
|
||||
}}
|
||||
allowDrop={(options) => {
|
||||
return !options.dropNode.isLeaf;
|
||||
}}
|
||||
defaultExpandedKeys={[workspace.fileTree.path]}
|
||||
draggable={newName !== null ? false : { icon: false }}
|
||||
expandedKeys={expandedKeys}
|
||||
switcherIcon={<DownOutlined />}
|
||||
selectedKeys={selectedKeys}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user