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 { 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, 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: `

You are about to move ${this.state.dragdrop.sourceNode.name}

to

${selectedNode.path}

`, centered: true, okText: "Delete", onOk: () => this.handleOnMove(), }); } ); }; onDrag = (event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); }; onDragOver = (event: React.DragEvent) => event.preventDefault(); onDragStart = (event: React.DragEvent, 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 ( { this.handleContextMenu({ node: node, action: "create" }); }} > New Tree { this.handleContextMenu({ node: node, action: "create_folder" }); }} > New Folder { this.handleContextMenu({ node: node, action: "reveal_in_explorer" }); }} > Reveal In File Explorer { this.handleContextMenu({ node: node, action: "rename" }); }} > Rename { this.handleContextMenu({ node: node, action: "delete" }); }} > Delete ); } render() { if (!this.state.nodes) { return

Loading...

; } return (
{Object.keys(this.state.nodes).map((path) => { const node = this.state.nodes[path]; return (
{ this.handleClick(event, node); }} className="item-wrapper mb" > {icon.file(node)}{" "}{node.name}
); })}
); } }