feat: fs tree component
This commit is contained in:
1 parent
6c57df23f2
commit
278255efd7
18 files changed
+2427
-94
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import "./App.css";
|
||||
import Tree from "./components/Tree/index";
|
||||
export default Tree;
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+1152
-63
File diff suppressed because it is too large.
Load diff
+22
-7
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
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>
|
||||
);
|
||||
const { onOpenTree, onDeleteTree, workdir } = this.props;
|
||||
//const treeList = this.getTreeList(workdir);
|
||||
|
||||
if (!fs.existsSync(workdir)) {
|
||||
return <div></div>;
|
||||
} else {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -11,7 +11,8 @@
|
||||
"include": [
|
||||
"./common/**/*",
|
||||
"./main-process/**/*",
|
||||
"./render-process/**/*"
|
||||
"./render-process/**/*",
|
||||
"./fs-tree-view/**/*",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user