feat: fs tree component

This commit is contained in:
duanhongbo committed 2020-11-19 18:18:45 +08:00
1 parent 6c57df23f2
commit 278255efd7
18 files changed
+2425 -92

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
.container {
width: 250px;
height: 100vh;
overflow: hidden;
resize: inline;
}
.indent-root {
padding-left: 5px;
}
.indent {
padding-left: 25px;
}
.mb {
padding-bottom: 10px;
}
.item-wrapper {
cursor: pointer;
width: 100%;
white-space: nowrap;
}
.icon {
padding-right: 5px;
}
.icon.folder {
color: #fab019;
}
.tomato {
color: #e73934;
}
.red {
color: red;
}
.orange {
color: #dd5b26;
}
.blue {
color: #2372ba;
}
.green {
color: #08743b;
}
.purple {
color: #5b2d8d;
}
.grey {
color: #303c42;
}
.brown {
color: #574038;
}
.yellow {
color: #fab019;
}
.dragthis {
border: 1px solid blue;
width: 100px;
height: 100px;
}
.drophere {
border: 1px dashed #000;
width: 200px;
height: 200px;
}
+157
View File
@@ -0,0 +1,157 @@
import React from "react";
import icon from "../icon";
import request from "../../services/request";
import "./search.css";
interface SearchProp {
basePath: string;
onItemSelected?: Function;
}
interface SearchState {
query: string;
result: { name: string; path: string }[];
showSearchLoader: boolean;
showSearchBox: boolean;
}
class Search extends React.Component<SearchProp, SearchState> {
state: SearchState = {
query: "",
result: [],
showSearchLoader: false,
showSearchBox: false,
};
constructor(props: SearchProp) {
super(props);
}
runDebounce = this.debounce(this.doSearch, 1000);
debounce(fn: Function, delay: number) {
let timer: NodeJS.Timeout;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}
async doSearch(query: string) {
if (!query) {
this.setState({
query: "",
result: [],
showSearchBox: false,
showSearchLoader: false,
});
return;
}
const dirPath = this.props.basePath;
const result = await request.search(dirPath, query);
this.setState({
query,
result,
showSearchBox: true,
showSearchLoader: false,
});
}
handleChange = (query: string) => {
if (!query) {
this.setState({
query: "",
});
return;
}
this.setState({
query,
showSearchBox: true,
showSearchLoader: true,
});
};
clearSearch = (event: React.KeyboardEvent<HTMLInputElement>) => {
const keyCode = event.which || event.keyCode;
if (keyCode === 27) {
// Clear search
this.setState({
query: "",
result: [],
showSearchBox: false,
});
}
};
showResults = () => {
return (
<div className="item-wrapper search-results">
{this.state.result.map((item) => (
<div
onClick={() => this.props.onItemSelected(item)}
key={item.path}
className="indent mb search-item"
>
<div>
{icon.file(item)} <span>{item.name}</span>
</div>
<div className="search-path">{item.path}</div>
</div>
))}
</div>
);
};
emptyResults = () => {
return (
<div className="item-wrapper search-results empty-results">
<span className="tomato">{icon.close()}</span> No Matching Results
</div>
);
};
loadingResults = () => {
return <div className="item-wrapper search-results empty-results">{icon.loading()}</div>;
};
results = () => {
if (this.state.showSearchLoader) {
return this.loadingResults();
}
if (this.state.result.length > 0) {
return this.showResults();
}
return this.emptyResults();
};
render() {
return (
<div className="search-wrapper">
<span className="search-icon">{icon.search()}</span>
<input
type="text"
className="fa search"
placeholder="Search"
onChange={(event) => {
this.handleChange(event.target.value);
this.runDebounce();
}}
onKeyDown={this.clearSearch}
value={this.state.query}
/>
<div style={{ display: this.state.showSearchBox ? "block" : "none" }}>
{this.results()}
</div>
</div>
);
}
}
export default Search;
+53
View File
@@ -0,0 +1,53 @@
/* Search Styles */
.search-wrapper {
height: 35px;
width: 100%;
padding-bottom: 10px;
position: relative;
}
.search {
width: 100%;
height: 100%;
border: 1px solid rgb(180, 179, 179);
border-radius: 0;
padding-left: 30px;
color: grey;
font-size: 0.85em;
font-weight: 500;
/* border: 1px solid purple; */
}
.search-icon {
position: absolute;
padding-left: 8px;
padding-top: 12px;
color: grey;
font-size: 0.85em;
font-weight: 200;
}
.search-results {
background: #ffffff;
padding-top: 10px;
width: 100%;
min-height: 100vh;
height: auto;
}
.empty-results {
padding-top: 20px;
text-align: center;
font-size: 0.8em;
}
.search-item:hover > div {
background: rgba(0, 0, 0, 0.15);
}
.search-path {
font-size: 10px;
color: #6e6e6e;
padding: 5px 0;
padding-left: 20px;
}
+127
View File
@@ -0,0 +1,127 @@
import { Menu, Dropdown, Input } from "antd";
import React from "react";
import icon from "../icon";
export interface Node {
name: string;
path: string;
extension?: string;
type?: "directory" | "file";
//size?: number;
isOpen?: boolean;
isEditing?: boolean;
children?: NodeDict;
}
export type NodeDict = { [key: string]: Node };
export type NodeAction = "create" | "create_folder" | "rename" | "delete" | "reveal_in_explorer";
export interface TreeNodeProp {
node: Node;
currentFile: {
name: string;
path: string;
};
contextMenu: (node: Node) => JSX.Element;
handleClick?: (event: React.MouseEvent, selectedNode: Node) => void;
draggable?: boolean;
onDragStart?: (event: React.DragEvent<HTMLDivElement>, data: Node) => void;
onDrag?: (event: React.DragEvent<HTMLDivElement>) => void;
onDragOver?: (event: React.DragEvent<HTMLDivElement>) => void;
onDrop?: (event: React.DragEvent<HTMLDivElement>, data: Node) => void;
onRenameNode?:(node:Node,newFileName:string) => void;
disableContextMenu?: boolean;
}
let EditingText = "";
export default function TreeNode(props: TreeNodeProp) {
const {
node,
currentFile,
contextMenu,
handleClick,
onDragStart,
onDrag,
onDragOver,
onDrop,
onRenameNode,
disableContextMenu,
} = props;
return (
<div className="indent">
{node.children &&
Object.keys(node.children).map((path) => {
const child = node.children[path];
return (
<div
key={path}
onClick={(event) => handleClick(event, child)}
draggable={true}
onDragStart={(event) => onDragStart(event, child)}
onDrag={onDrag}
onDragOver={onDragOver}
onDrop={(event) => onDrop(event, child)}
>
{child.isEditing ? (
<Input
defaultValue={child.name}
onChange={(e) => {
EditingText = e.currentTarget.value;
}}
onFocus={(e)=>{
EditingText=child.name;
//e.currentTarget.select();
e.currentTarget.setSelectionRange(0,EditingText.lastIndexOf("."),"forward");
}}
onPressEnter={(e) => {
if(child.isEditing){
child.isEditing = false;
onRenameNode(child,EditingText);
}
}}
onBlur={(e)=>{
console.log("onBlur ", EditingText);
if(child.isEditing){
child.isEditing = false;
onRenameNode(child,EditingText);
}
}}
autoFocus={true}
/>
) : (
<Dropdown overlay={contextMenu(child)} trigger={["contextMenu"]}>
<div
className={
currentFile && currentFile.path === child.path
? "item-wrapper blue mb"
: "item-wrapper mb"
}
>
{icon.file(child)} {child.name}
</div>
</Dropdown>
)}
<TreeNode
node={child}
currentFile={currentFile}
contextMenu={contextMenu}
handleClick={handleClick}
draggable={true}
onDragStart={onDragStart}
onDrag={onDrag}
onDragOver={onDragOver}
onDrop={onDrop}
disableContextMenu={disableContextMenu}
/>
</div>
);
})}
</div>
);
}
+539
View File
@@ -0,0 +1,539 @@
import React, { ChangeEvent, Component } from "react";
import path from "path";
import request from "../../services/request";
import TreeNode, { Node, NodeAction, NodeDict } from "./TreeNode";
import icon from "../icon";
import Search from "../Search";
import { Modal, Menu, Dropdown } from "antd";
import { shell } from "electron";
export interface TreeProps {
className: string;
basePath: string;
disableContextMenu?: boolean;
filter: (path: string) => boolean;
onItemSelected?: (node: Node) => void;
onItemDeleted?: (node: Node) => void;
}
interface TreeState {
nodes?: NodeDict;
currentFile?: { name: string; path: string };
overwrite?: boolean;
dragdrop?: {
sourceNode: Node;
destinationNode?: Node;
};
}
export default class Tree extends Component<TreeProps, TreeState> {
state: TreeState = {
dragdrop: {
sourceNode: null,
destinationNode: null,
},
};
constructor(props: TreeProps) {
super(props);
}
_getRootNode() {
return {
[this.props.basePath]: {
name: path.basename(this.props.basePath),
path: this.props.basePath,
type: "directory",
isOpen: false,
children: null,
} as Node,
};
}
componentDidMount() {
this.setState({
nodes: this._getRootNode(),
});
// Open first level folders
this.openFolders();
console.log("componentDidMount", this.state);
}
openFolders = async () => {
let nodes = this._getRootNode();
const rootNode = nodes[this.props.basePath];
const children = await this.getChildNodes(rootNode, this.props.filter);
rootNode.children = children;
rootNode.isOpen = true;
for (const path in children) {
if (children.hasOwnProperty(path)) {
const node = children[path];
if (node.type === "directory") {
const subChildren = await this.getChildNodes(node, this.props.filter);
node.children = { ...subChildren };
node.isOpen = true;
}
}
}
this.setState({
nodes: nodes,
});
};
getChildNodes = async (node: Node, filter: (path: string) => boolean) => {
// if (!node.children) {
// return null;
// }
let children = (await request.tree(node.path)) as NodeDict;
let result: NodeDict = {};
for (const path in children) {
if (children.hasOwnProperty(path)) {
const node = children[path];
if (node.type == "directory" || filter(node.path)) {
result[node.path] = node;
}
}
}
return result;
};
handleClick = async (event: React.MouseEvent, selectedNode: Node) => {
event.stopPropagation();
// If folder is clicked and the folder is already open, we will close it
if (selectedNode.isOpen === true) {
this.emptyChildren(this.state.nodes, selectedNode);
selectedNode.isOpen = false;
return;
}
if (selectedNode.type === "file") {
this.setState({
currentFile: {
name: selectedNode.name,
path: selectedNode.path,
},
});
}
selectedNode.isOpen = !selectedNode.isOpen;
if(selectedNode.type === "directory"){
// Get children of a selected node
const newChildren = await this.getChildNodes(selectedNode, this.props.filter);
// Add the new children to the selected folder
this.addToChildren(this.state.nodes, selectedNode, newChildren);
}
// Propagate the selected item to the parent component
this.props.onItemSelected(selectedNode);
};
handleOnMove = async () => {
try {
const response = await request.dragDrop({
source: this.state.dragdrop.sourceNode.path,
destination: this.state.dragdrop.destinationNode.path,
overwrite: this.state.overwrite,
});
if (response.status === 201) {
this.removeItem(this.state.nodes, this.state.dragdrop.sourceNode);
this.emptyChildren(this.state.nodes, this.state.dragdrop.destinationNode);
}
} catch (error) {
Modal.error({
title: "Error",
content: "Item already exists in the destination",
centered: true,
});
}
this.setState({
overwrite: false,
});
};
renameNode = async (selectedNode: Node,newFileName:string) => {
try {
const response = await request.renameNode({
oldPath: selectedNode.path,
newFileName: newFileName,
});
if (response.status === 200) {
// Add the Item to state
let newNode = selectedNode;
newNode.name = newFileName;
newNode.path = response.data.newFilePath;
this.addItem(this.state.nodes, newNode);
}
} catch (error) {
Modal.error({
title: "Error",
content: `A ${
selectedNode.type === "file" ? "file" : "folder"
} with that name already exists`,
centered: true,
});
}
};
handleOnDelete = async (selectedNode: Node) => {
const response = await request.deleteNode(selectedNode.path);
if (response.status === 200) {
this.props.onItemDeleted && this.props.onItemDeleted(selectedNode);
this.removeItem(this.state.nodes, selectedNode);
}else{
Modal.error({
title: "Delete File/Folder Error",
content: `${response.statusText}`,
centered: true,
});
}
};
handleContextMenu = (data: { node: Node; action: NodeAction }) => {
const { node, action } = data;
switch (action) {
case "create":{
//TODO new file
const baseDir = path.dirname(node.path) ;
const defaultNewName="NewTree.json";
const newFilePath = `${baseDir}${path.sep}${defaultNewName}`;
this.addToChildren(this.state.nodes, node, {
[newFilePath]:{
name:defaultNewName,
path:newFilePath,
type:"file",
isEditing:true,
isOpen: false,
children: null,
}
});
}
break;
case "create_folder":{
//TODO: new folder
const baseDir = path.dirname(node.path) ;
const defaultNewName="NewTree.json";
const newFilePath = `${baseDir}${path.sep}${defaultNewName}`;
this.addToChildren(this.state.nodes, node, {
[newFilePath]:{
name:defaultNewName,
path:newFilePath,
type:"directory",
isEditing:true,
isOpen: false,
children: null,
}
});
}
break;
case "reveal_in_explorer":{
shell.showItemInFolder(node.path);
}
break;
case "rename":{
node.isEditing = true;
const nodes = this.state.nodes;
this.setState({nodes:nodes});
}
break;
case "delete":{
Modal.confirm({
title: "Delete Confirmation",
content: `You are about to delete ${node.path}`,
centered: true,
okText: "Delete",
onOk: () => this.handleOnDelete(node),
});
}
break;
}
};
// Adds children to a selected folder
addToChildren = (nodes: NodeDict, selectedNode: Node, newChildren: NodeDict) => {
for (const path in nodes) {
const iterator = nodes[path];
if (iterator.type === "file") {
continue;
}
if (selectedNode.path.includes(iterator.path)) {
// If its exactly the same path append the new node in its children array
if (selectedNode.path === iterator.path) {
iterator.children = { ...newChildren, ...iterator.children };
// Prepare a new version of the current state
const newNodes = this.state.nodes;
// Set the new state
this.setState({
nodes: newNodes,
});
break;
} else {
// If its a partial match, then get inside its children and perform iteration until exact match is not found
this.addToChildren(iterator.children, selectedNode, newChildren);
}
}
}
};
// Removes children from a selected folder
emptyChildren = (nodes: NodeDict, selectedNode: Node) => {
for (let path in nodes) {
const iterator = nodes[path];
if (iterator.type === "file") {
continue;
}
if (selectedNode.path.includes(iterator.path)) {
// If its exactly the same path append the new node in its children array
if (selectedNode.path === iterator.path) {
iterator.children = {};
iterator.isOpen = false;
// Prepare a new version of the current state
const newNodes = this.state.nodes;
// Set the new state
this.setState({
nodes: newNodes,
});
break;
} else {
// If its a partial match, then get inside its children and perform iteration until exact match is not found
this.emptyChildren(iterator.children, selectedNode);
}
}
}
};
// Add an item (file or folder) to the nodes array
addItem = (nodes: NodeDict, selectedNode: Node) => {
for (const path in nodes) {
const iterator = nodes[path];
if (selectedNode.path.includes(iterator.path)) {
// If its exactly the same path append the new node in its children object
if (selectedNode.path === iterator.path) {
// Add the element to the nodes object
nodes[path] = selectedNode;
// Prepare a new version of the current state
const newNodes = this.state.nodes;
// Set the new state
this.setState({
nodes: newNodes,
});
break;
} else {
// If its a partial match, then get inside its children and perform iteration until exact match is not found
this.addItem(iterator.children, selectedNode);
}
}
}
};
// Removes an item (file or folder) from the nodes array
removeItem = (nodes: NodeDict, selectedNode: Node) => {
for (const path in nodes) {
const iterator = nodes[path];
if (selectedNode.path.includes(iterator.path)) {
// If its exactly the same path append the new node in its children array
if (selectedNode.path === iterator.path) {
// Remove the element from the nodes array
// nodes.splice(path, 1);
delete nodes[path];
// Prepare a new version of the current state
const newNodes = this.state.nodes;
// Set the new state
this.setState({
nodes: newNodes,
});
break;
} else {
// If its a partial match, then get inside its children and perform iteration until exact match is not found
this.removeItem(iterator.children, selectedNode);
}
}
}
};
// Toggles the overwrite checkbox
toggleOverwrite = () => {
this.setState({
overwrite: !this.state.overwrite,
});
};
// Drag & Drop handlers
onDrop = (event: React.DragEvent<HTMLDivElement>, selectedNode: Node) => {
event.preventDefault();
event.stopPropagation();
if (selectedNode.type === "file") {
Modal.error({
title: "Error",
content: "Destination is not a directory",
centered: true,
});
return;
}
this.setState(
{
dragdrop: {
sourceNode: this.state.dragdrop.sourceNode,
destinationNode: selectedNode,
},
},
async () => {
Modal.confirm({
title: "Confirm Move",
content: `
<p>
You are about to move <strong>${this.state.dragdrop.sourceNode.name}</strong>
</p>
<p>to</p>
<p>
${selectedNode.path}
</p>`,
centered: true,
okText: "Delete",
onOk: () => this.handleOnMove(),
});
}
);
};
onDrag = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
};
onDragOver = (event: React.DragEvent<HTMLDivElement>) => event.preventDefault();
onDragStart = (event: React.DragEvent<HTMLDivElement>, selectedNode: Node) => {
event.stopPropagation();
this.setState({
dragdrop: {
sourceNode: selectedNode,
destinationNode: this.state.dragdrop.destinationNode,
},
});
const id = (event.target as HTMLDivElement).id;
event.dataTransfer.setData("text/plain", id);
};
renderContextMeun(node: Node) {
return (
<Menu>
<Menu.Item
key="create"
onClick={(info) => {
this.handleContextMenu({ node: node, action: "create" });
}}
>
New Tree
</Menu.Item>
<Menu.Item
key="create_folder"
onClick={(info) => {
this.handleContextMenu({ node: node, action: "create_folder" });
}}
>
New Folder
</Menu.Item>
<Menu.Item
key="reveal_in_explorer"
onClick={(info) => {
this.handleContextMenu({ node: node, action: "reveal_in_explorer" });
}}
>
Reveal In File Explorer
</Menu.Item>
<Menu.Divider/>
<Menu.Item
key="rename"
onClick={(info) => {
this.handleContextMenu({ node: node, action: "rename" });
}}
>
Rename
</Menu.Item>
<Menu.Item
key="delete"
onClick={(info) => {
this.handleContextMenu({ node: node, action: "delete" });
}}
>
Delete
</Menu.Item>
</Menu>
);
}
render() {
if (!this.state.nodes) {
return <p>Loading...</p>;
}
return (
<div className={`container ${this.props.className}`}>
<Search basePath={this.props.basePath} onItemSelected={this.props.onItemSelected} />
<div className="indent-root">
{Object.keys(this.state.nodes).map((path) => {
const node = this.state.nodes[path];
return (
<div key={path}>
<Dropdown
overlay={this.renderContextMeun(node)}
trigger={["contextMenu"]}
>
<div
onClick={(event) => {
this.handleClick(event, node);
}}
className="item-wrapper mb"
>
{icon.file(node)}{" "}{node.name}
</div>
</Dropdown>
<TreeNode
node={node}
currentFile={this.state.currentFile}
contextMenu={this.renderContextMeun.bind(this)}
handleClick={this.handleClick}
onDragStart={this.onDragStart}
onDrag={this.onDrag}
onDragOver={this.onDragOver}
onDrop={this.onDrop}
onRenameNode={this.renameNode}
disableContextMenu={this.props.disableContextMenu}
/>
</div>
);
})}
</div>
</div>
);
}
}
+34
View File
@@ -0,0 +1,34 @@
import React from "react";
import PropTypes from "prop-types";
import { FolderOutlined,FolderOpenOutlined,FileTextOutlined,SearchOutlined,CloseOutlined,LoadingOutlined} from '@ant-design/icons';
import { Node,NodeDict } from "./Tree/TreeNode";
const FileIcon = (node:Node) => {
if (node.type === "file") {
return (
<FileTextOutlined />
);
}
return node.isOpen ?(<FolderOpenOutlined />):(<FolderOutlined />)
};
const SearchIcon = () => {
return <SearchOutlined />;
};
const CloseIcon = () => {
return <CloseOutlined />;
};
const LoadingIcon = ()=>{
return <LoadingOutlined />
}
export default {
file: FileIcon,
search: SearchIcon,
close: CloseIcon,
loading: LoadingIcon
};
+3
View File
@@ -0,0 +1,3 @@
import "./App.css";
import Tree from "./components/Tree/index";
export default Tree;
+157
View File
@@ -0,0 +1,157 @@
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const app = express();
const fs = require("fs");
const fsx = require("fs-extra");
const path = require("path");
const rimraf = require("rimraf");
const glob = require("glob");
const sanitize = require("sanitize-filename");
function decodeUrlParam(s) {
return decodeURIComponent(s);
}
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// Tree
app.get("/", (req, res) => {
const dirPath = decodeUrlParam(req.query.path) || "";
fs.readdir(dirPath, (err, files) => {
if (!files) {
console.log("No Files", dirPath);
return res.json({});
}
let finalList = {};
files.forEach((file) => {
try {
const fullPath = path.join(dirPath, file);
const isDirectory = fs.lstatSync(fullPath).isDirectory();
let fileProperties = {
[fullPath]: {
name: file,
path: fullPath,
size: fs.statSync(fullPath).size,
extension: path.extname(file),
type: isDirectory ? "directory" : "file",
isOpen: false,
},
};
if (isDirectory) {
delete fileProperties[fullPath].extension;
fileProperties[fullPath].children = {};
// Directories should appear on top
finalList = { ...fileProperties, ...finalList };
} else {
// Files should appear at the end
finalList = { ...finalList, ...fileProperties };
}
} catch (error) {
console.log("Error:", error);
return res.json({});
}
});
return res.json(finalList);
});
});
// Search
app.get("/search", (req, res) => {
const dirPath = decodeUrlParam(req.query.path) || "";
let query = decodeUrlParam(req.query.query) || "";
query = sanitize(query);
const maxResults = 20;
glob(`${dirPath}/**/*.*`, {}, function (er, files) {
const final = [];
let counter = 0;
for (const file of files) {
// Dont show more than maxResults
if (counter >= maxResults) {
break;
}
let pattern;
try {
pattern = new RegExp(query, "gi");
} catch (error) {
query = query.replace(/[|&;$%@"<>()+,*]/g, "");
pattern = new RegExp(query, "gi");
}
if (pattern.test(path.basename(file))) {
final.push({
name: path.basename(file),
path: file,
type: "file",
extension: path.extname(file),
});
counter++;
}
}
return res.json(final);
});
});
// For Drag and Drop
app.post("/dragdrop", (req, res) => {
const { source, destination, overwrite } = req.body;
const filename = path.basename(source);
const destinationPath = `${destination}${path.sep}${filename}`;
fsx.move(source, destinationPath, { overwrite }, (error) => {
if (error) {
return res.status(400).send("NOT_OK");
}
return res.status(201).send("OK");
});
});
// Rename a file/folder
app.put("/rename", (req, res) => {
const oldPath = decodeUrlParam(req.body.oldPath);
const newFileName = decodeUrlParam(req.body.newFileName);
const newFilePath = `${path.dirname(oldPath)}${path.sep}${sanitize(newFileName)}`;
if (oldPath == newFilePath) {
return res.json({
newFilePath,
});
} else {
fsx.move(oldPath, newFilePath, { overwrite: false }, (error) => {
if (error) {
console.log("Error", error);
return res.status(400).send("NOT_OK");
}
return res.json({
newFilePath,
});
});
}
});
// Delete file/folder
app.delete("/", (req, res) => {
rimraf(decodeUrlParam(req.query.fullPath), (error) => {
if (error) {
return res.status(400).send(error);
} else {
return res.status(200).send("OK");
}
});
});
app.listen(5000, () => {
console.log("Server running on port 5000");
});
+53
View File
@@ -0,0 +1,53 @@
import * as axios from "axios";
function encodeUrlParam(s: string) {
return encodeURIComponent(s);
}
const BASE_URL = process.env.fsTreeViewUrl || "http://localhost:5000";
const tree = async (dirPath: string) => {
const response = await axios.default.get(`${BASE_URL}/?path=${encodeUrlParam(dirPath)}`);
return response.data;
};
const search = async (dirPath: string, query: string) => {
const response = await axios.default.get(
`${BASE_URL}/search?query=${encodeUrlParam(query)}&path=${encodeUrlParam(dirPath)}`
);
return response.data;
};
const dragDrop = async (args: { source: string; destination: string; overwrite: boolean }) => {
const response = await axios.default.post(`${BASE_URL}/dragdrop`, {
source : encodeUrlParam(args.source),
destination : encodeUrlParam(args.destination),
overwrite:args.overwrite
});
return response;
};
const renameNode = async (args: { oldPath: string; newFileName: string }) => {
const response = await axios.default.put(`${BASE_URL}/rename`, {
oldPath : encodeUrlParam(args.oldPath),
newFileName : encodeUrlParam(args.newFileName),
});
return response;
};
const deleteNode = async (fullPath : string) => {
const encodeFullPath = encodeUrlParam(fullPath);
const response = await axios.default.delete(`${BASE_URL}`, {
params: { fullPath:encodeFullPath },
});
return response;
};
export default {
tree,
search,
dragDrop,
renameNode,
deleteNode,
};
+4 -1
View File
@@ -3,7 +3,10 @@ import AppMenu from "./AppMenu";
import Settings from "./Settings";
import MainEventType from "../common/MainEventType";
const electronLocalshortcut = require("electron-localshortcut");
import electronLocalshortcut from "electron-localshortcut";
//launch fs server listen port 5000
require("../fs-tree-view/server/server");
// 一些暴露给render-process的全局变量
export interface Global {
+2 -2
View File
@@ -36,8 +36,8 @@ export default class Workspace {
const model = JSON.parse(str) as WorkspaceModel;
if (model.isRelative) {
const root = path.dirname(this.filepath);
this.nodeConfPath = root + "/" + model.nodeConfPath;
this.workdir = root + "/" + model.workdir;
this.nodeConfPath = path.join(root,model.nodeConfPath);
this.workdir = path.join(root,model.workdir);
} else {
this.nodeConfPath = model.nodeConfPath;
this.workdir = model.workdir;
+1152 -63
View File
File diff suppressed because it is too large. Load diff
+22 -7
View File
@@ -20,32 +20,47 @@
},
"build": {
"appId": "com.example.app",
"directories":{
"output":"build"
"directories": {
"output": "build"
}
},
"homepage": "https://github.com/zhandouxiaojiji/behavior3editor#readme",
"devDependencies": {
"@types/cors": "^2.8.8",
"@types/electron-localshortcut": "^3.1.0",
"@types/express": "^4.17.9",
"@types/node": "^14.14.8",
"@types/rimraf": "^3.0.0",
"copy-webpack-plugin": "^6.3.1",
"css-loader": "^4.3.0",
"electron": "^10.1.3",
"electron-builder": "^22.9.1",
"electron-localshortcut": "^3.2.1",
"electron-packager": "^15.1.0",
"file-loader": "^6.1.1",
"source-map-loader": "^1.1.0",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"electron": "^10.1.3",
"electron-localshortcut": "^3.2.1",
"electron-packager": "^15.1.0"
"webpack-cli": "^3.3.12"
},
"dependencies": {
"@ant-design/icons": "^4.3.0",
"@antv/g6": "^3.8.1",
"@types/react": "^16.9.51",
"@types/react-dom": "^16.9.8",
"antd": "^4.6.6",
"axios": "^0.19.0",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"fs-extra": "^9.0.1",
"react": "^16.13.1",
"react-confirm-alert": "^2.6.2",
"react-dom": "^16.13.1",
"react-markdown": "^5.0.2"
"react-markdown": "^5.0.2",
"rimraf": "^3.0.2",
"sanitize-filename": "^1.6.3"
}
}
+27 -12
View File
@@ -4,6 +4,7 @@ import * as fs from "fs";
import * as path from "path";
import { ipcRenderer } from "electron";
import MainEventType from "../common/MainEventType";
import Tree from "../fs-tree-view";
const { Search } = Input;
@@ -15,6 +16,7 @@ export interface TreeFile {
export interface PropertiesProps {
workdir: string;
onOpenTree: (path: string) => void;
onDeleteTree: (path: string) => void;
}
interface PropertiesState {
@@ -71,19 +73,32 @@ export default class Properties extends Component<PropertiesProps> {
render() {
console.log("render Properties");
const { onOpenTree, workdir } = this.props;
const treeList = this.getTreeList(workdir);
const { onOpenTree, onDeleteTree, workdir } = this.props;
//const treeList = this.getTreeList(workdir);
if (!fs.existsSync(workdir)) {
return <div></div>;
} else {
return (
<div>
<Search placeholder="Search" onChange={() => {}} />
<Menu mode="inline">
{treeList.map((tree) => (
<Menu.Item key={tree.name} onClick={() => onOpenTree(tree.path)}>
{tree.name}
</Menu.Item>
))}
</Menu>
</div>
<Tree
className={""}
basePath={workdir || "/"}
disableContextMenu={false}
filter={(path) => {
return path.endsWith(".json");
}}
onItemSelected={(item) => {
if (item.type == "file") {
onOpenTree(item.path);
}
}}
onItemDeleted={(item) => {
if (item.type == "file") {
onDeleteTree(item.path);
}
}}
></Tree>
);
}
}
}
+10 -3
View File
@@ -23,8 +23,6 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
editors: { [path: string]: Editor } = {};
componentWillMount() {}
componentDidMount() {
ipcRenderer.on(MainEventType.CREATE_NODE, (event: any, name: any) => {
const editor = this.getCurEditor();
@@ -74,7 +72,6 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
}
openFile(path: string) {
console.log("on open file", path);
if (this.state.filepaths.indexOf(path) < 0) {
const filepaths = this.state.filepaths;
filepaths.push(path);
@@ -84,6 +81,16 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
}
}
closeFile(path: string) {
const index = this.state.filepaths.indexOf(path);
if (index >= 0) {
const filepaths = this.state.filepaths;
filepaths.splice(index,1);
const length = filepaths.length;
this.setState({ filepaths, curPath: length>0?filepaths[length-1]:null });
}
}
render() {
console.log("render tabs");
const { filepaths, curPath } = this.state;
+4 -3
View File
@@ -34,12 +34,10 @@ export default class Main extends Component {
settings: Settings;
tabs: TreeTabs;
componentWillMount() {
componentDidMount() {
this.updateSettings();
RegisterNode(this.settings);
}
componentDidMount() {
ipcRenderer.on(MainEventType.OPEN_FILE, (event: any, path: any) => {
this.setState({ curPath: path });
});
@@ -73,6 +71,9 @@ export default class Main extends Component {
onOpenTree={(path) => {
this.tabs.openFile(path);
}}
onDeleteTree={(path) => {
this.tabs.closeFile(path);
}}
/>
</Sider>
<Content className="content">
+2 -1
View File
@@ -11,7 +11,8 @@
"include": [
"./common/**/*",
"./main-process/**/*",
"./render-process/**/*"
"./render-process/**/*",
"./fs-tree-view/**/*",
],
"exclude": [
"node_modules",
+8
View File
@@ -1,3 +1,5 @@
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
mode: "development",
entry: {
@@ -23,6 +25,12 @@ module.exports = {
{
test: /\.css$/,
loaders: ['style-loader', 'css-loader']
},
{//https://github.com/ashtuchkin/iconv-lite/issues/205
test: /node_modules[\/\\](iconv-lite)[\/\\].+/,
resolve: {
aliasFields: ['main']
}
}
]
},