mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-20 19:08:21 +00:00
Shared dir view redesign (#7468)
* [shared dir view] redesigned it - changed to a full-screen 'side panel & main panel' layout - added a 'folder tree' panel, a current path bar with an operation dropdown menu, a toolbar for selected items, a 'view mode' menu, and a 'sort' menu * [shared dir view] added a resizing bar (enable users to resize the width of side panel and main panel) * [shared dir view] path bar: added a 'plus' icon to the dropdown menu toggle when 'upload' is offered in the menu * [shared dir view] folder tree: cleaned up the code * [shared dir view] improved 'visit a folder' * [shared dir view] improvements & cleanup
This commit is contained in:
5
frontend/src/components/shared-dir-tree-view/index.js
Normal file
5
frontend/src/components/shared-dir-tree-view/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import TreeHelper from './tree-helper';
|
||||
import TreeView from './tree-view';
|
||||
import TreeNode from './tree-node';
|
||||
|
||||
export { TreeHelper, TreeView, TreeNode };
|
150
frontend/src/components/shared-dir-tree-view/tree-helper.js
Normal file
150
frontend/src/components/shared-dir-tree-view/tree-helper.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Utils } from '../../utils/utils';
|
||||
import Tree from './tree';
|
||||
import TreeNode from './tree-node';
|
||||
|
||||
class TreeHelper {
|
||||
|
||||
expandNode(tree, node) { // This tree has been cloned
|
||||
tree.expandNode(node);
|
||||
}
|
||||
|
||||
collapseNode(tree, node) {
|
||||
let treeCopy = tree.clone();
|
||||
node = treeCopy.getNodeByPath(node.path);
|
||||
treeCopy.collapseNode(node);
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
findNodeByPath(tree, nodePath) {
|
||||
let treeCopy = tree.clone();
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
return node;
|
||||
}
|
||||
|
||||
getNodeChildrenObject(tree, node, sortType = 'name', order = 'asc') {
|
||||
let objects = tree.getNodeChildrenObject(node);
|
||||
objects = Utils.sortDirents(objects, sortType, order);
|
||||
return objects;
|
||||
}
|
||||
|
||||
addNodeToParent(tree, node, parentNode) {
|
||||
tree.addNodeToParentNode(node, parentNode);
|
||||
return tree;
|
||||
}
|
||||
|
||||
addNodeListToParent(tree, nodeList, parentNode) {
|
||||
tree.addNodeListToParent(nodeList, parentNode);
|
||||
return tree;
|
||||
}
|
||||
|
||||
addNodeToParentByPath(tree, node, parentPath) {
|
||||
let treeCopy = tree.clone();
|
||||
let parentNode = treeCopy.getNodeByPath(parentPath);
|
||||
treeCopy.addNodeToParent(node, parentNode);
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
deleteNodeByPath(tree, nodePath) {
|
||||
let treeCopy = tree.clone();
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
if (node) {
|
||||
treeCopy.deleteNode(node);
|
||||
}
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
deleteNodeListByPaths(tree, nodePaths) {
|
||||
let treeCopy = tree.clone();
|
||||
nodePaths.forEach(nodePath => {
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
if (node) {
|
||||
treeCopy.deleteNode(node);
|
||||
}
|
||||
});
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
renameNodeByPath(tree, nodePath, newName) {
|
||||
let treeCopy = tree.clone();
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
if (!node) {
|
||||
return treeCopy;
|
||||
}
|
||||
treeCopy.renameNode(node, newName);
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
updateNodeByPath(tree, nodePath, keys, newValues) {
|
||||
let treeCopy = tree.clone();
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
treeCopy.updateNode(node, keys, newValues);
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
moveNodeByPath(tree, nodePath, destPath, nodeName) {
|
||||
let treeCopy = tree.clone();
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
let destNode = treeCopy.getNodeByPath(destPath);
|
||||
if (destNode && node) { // node has loaded
|
||||
node.object.name = nodeName; // need not update path
|
||||
treeCopy.moveNode(node, destNode);
|
||||
}
|
||||
if (!destNode && node) {
|
||||
treeCopy.deleteNode(node);
|
||||
}
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
moveNodeListByPaths(tree, nodePaths, destPath) {
|
||||
let treeCopy = tree.clone();
|
||||
let destNode = treeCopy.getNodeByPath(destPath);
|
||||
if (destNode) {
|
||||
nodePaths.forEach(nodePath => {
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
treeCopy.moveNode(node, destNode);
|
||||
});
|
||||
} else {
|
||||
nodePaths.forEach(nodePath => {
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
treeCopy.delete(node);
|
||||
});
|
||||
}
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
copyNodeByPath(tree, nodePath, destPath, nodeName) {
|
||||
let treeCopy = tree.clone();
|
||||
let destNode = treeCopy.getNodeByPath(destPath);
|
||||
let treeNode = treeCopy.getNodeByPath(nodePath);
|
||||
if (destNode) {
|
||||
let node = treeNode.clone(); // need a dup
|
||||
node.object.name = nodeName; // need not update path
|
||||
treeCopy.copyNode(node, destNode);
|
||||
}
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
copyNodeListByPaths(tree, nodePaths, destPath) {
|
||||
let treeCopy = tree.clone();
|
||||
let destNode = treeCopy.getNodeByPath(destPath);
|
||||
if (destNode) {
|
||||
nodePaths.forEach(nodePath => {
|
||||
let node = treeCopy.getNodeByPath(nodePath);
|
||||
treeCopy.copyNode(node, destNode);
|
||||
});
|
||||
}
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
buildTree() {
|
||||
let tree = new Tree();
|
||||
let object = { folder_path: '/', is_dir: true };
|
||||
let root = new TreeNode({ object, isLoaded: false, isExpanded: true });
|
||||
tree.setRoot(root);
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
|
||||
let treeHelper = new TreeHelper();
|
||||
|
||||
export default treeHelper;
|
158
frontend/src/components/shared-dir-tree-view/tree-node-view.js
Normal file
158
frontend/src/components/shared-dir-tree-view/tree-node-view.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const LEFT_INDENT = 20;
|
||||
|
||||
const propTypes = {
|
||||
node: PropTypes.object.isRequired,
|
||||
currentPath: PropTypes.string.isRequired,
|
||||
leftIndent: PropTypes.number.isRequired,
|
||||
onNodeClick: PropTypes.func.isRequired,
|
||||
onNodeExpanded: PropTypes.func.isRequired,
|
||||
onNodeCollapse: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class TreeNodeView extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHighlight: false,
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({
|
||||
isHighlight: true,
|
||||
});
|
||||
};
|
||||
|
||||
onMouseOver = () => {
|
||||
this.setState({
|
||||
isHighlight: true,
|
||||
});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({
|
||||
isHighlight: false,
|
||||
});
|
||||
};
|
||||
|
||||
onNodeClick = () => {
|
||||
const { node } = this.props;
|
||||
this.props.onNodeClick(node);
|
||||
};
|
||||
|
||||
onLoadToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
const { node } = this.props;
|
||||
if (node.isExpanded) {
|
||||
this.props.onNodeCollapse(node);
|
||||
} else {
|
||||
this.props.onNodeExpanded(node);
|
||||
}
|
||||
};
|
||||
|
||||
getNodeTypeAndIcon = () => {
|
||||
let { node } = this.props;
|
||||
let icon = '';
|
||||
let type = '';
|
||||
if (node.object.is_dir) {
|
||||
icon = <i className="sf3-font sf3-font-folder"></i>;
|
||||
type = 'dir';
|
||||
} else {
|
||||
let index = node.object.file_name.lastIndexOf('.');
|
||||
if (index === -1) {
|
||||
icon = <i className="sf3-font sf3-font-file"></i>;
|
||||
type = 'file';
|
||||
} else {
|
||||
let suffix = node.object.file_name.slice(index).toLowerCase();
|
||||
if (suffix === '.png' || suffix === '.jpg' || suffix === '.jpeg' || suffix === '.gif' || suffix === '.bmp') {
|
||||
icon = <i className="sf3-font sf3-font-image"></i>;
|
||||
type = 'image';
|
||||
}
|
||||
else if (suffix === '.md' || suffix === '.markdown') {
|
||||
icon = <i className="sf3-font sf3-font-files2"></i>;
|
||||
type = 'file';
|
||||
}
|
||||
else {
|
||||
icon = <i className="sf3-font sf3-font-file"></i>;
|
||||
type = 'file';
|
||||
}
|
||||
}
|
||||
}
|
||||
return { icon, type };
|
||||
};
|
||||
|
||||
renderChildren = () => {
|
||||
let { node } = this.props;
|
||||
if (!node.hasChildren()) {
|
||||
return '';
|
||||
}
|
||||
return (
|
||||
<div className="children">
|
||||
{node.children.map((item, index) => {
|
||||
return (
|
||||
<TreeNodeView
|
||||
key={index}
|
||||
node={item}
|
||||
leftIndent={this.props.leftIndent + LEFT_INDENT}
|
||||
currentPath={this.props.currentPath}
|
||||
onNodeClick={this.props.onNodeClick}
|
||||
onNodeCollapse={this.props.onNodeCollapse}
|
||||
onNodeExpanded={this.props.onNodeExpanded}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
let { currentPath, node, leftIndent } = this.props;
|
||||
let { type, icon } = this.getNodeTypeAndIcon();
|
||||
let hlClass = this.state.isHighlight ? 'tree-node-inner-hover' : '';
|
||||
if (node.path === currentPath) {
|
||||
hlClass = 'tree-node-hight-light';
|
||||
}
|
||||
|
||||
const nodeName = node.object.folder_name || node.object.file_name;
|
||||
return (
|
||||
<div className="tree-node">
|
||||
<div
|
||||
type={type}
|
||||
className={`tree-node-inner text-nowrap ${hlClass} ${node.path === '/' ? 'hide' : ''}`}
|
||||
title={nodeName}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onClick={this.onNodeClick}
|
||||
>
|
||||
<div
|
||||
className="tree-node-text"
|
||||
style={{ paddingLeft: leftIndent + 5 }}
|
||||
>
|
||||
{nodeName}
|
||||
</div>
|
||||
<div className="left-icon" style={{ left: leftIndent - 40 }}>
|
||||
{type === 'dir' && (!node.isLoaded || (node.isLoaded && node.hasChildren())) && (
|
||||
<i
|
||||
className={`folder-toggle-icon sf3-font sf3-font-down ${node.isExpanded ? '' : 'rotate-270'}`}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={this.onLoadToggle}
|
||||
>
|
||||
</i>
|
||||
)}
|
||||
<i className="tree-node-icon">{icon}</i>
|
||||
</div>
|
||||
</div>
|
||||
{node.isExpanded && this.renderChildren()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TreeNodeView.propTypes = propTypes;
|
||||
|
||||
export default TreeNodeView;
|
152
frontend/src/components/shared-dir-tree-view/tree-node.js
Normal file
152
frontend/src/components/shared-dir-tree-view/tree-node.js
Normal file
@@ -0,0 +1,152 @@
|
||||
class TreeNode {
|
||||
|
||||
constructor({ path, object, isLoaded, isPreload, isExpanded, parentNode }) {
|
||||
// this.path = path || object.name; // The default setting is the object name, which is set to a relative path when the father is set.
|
||||
this.path = object.folder_path;
|
||||
this.object = object;
|
||||
this.isLoaded = isLoaded || false;
|
||||
this.isPreload = isPreload || false;
|
||||
this.isExpanded = isExpanded || false;
|
||||
this.children = [];
|
||||
this.parentNode = parentNode || null;
|
||||
}
|
||||
|
||||
clone(parentNode) {
|
||||
let treeNode = new TreeNode({
|
||||
path: this.path,
|
||||
// object: this.object.clone(),
|
||||
object: this.object,
|
||||
isLoaded: this.isLoaded,
|
||||
isPreload: this.isPreload,
|
||||
isExpanded: this.isExpanded,
|
||||
parentNode: parentNode || null,
|
||||
});
|
||||
treeNode.children = this.children.map(child => {
|
||||
let newChild = child.clone(treeNode);
|
||||
return newChild;
|
||||
});
|
||||
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
setParent(parentNode) {
|
||||
this.path = this.generatePath(parentNode);
|
||||
this.parentNode = parentNode;
|
||||
this.isLoaded = false; // update parentNode need loaded data again;
|
||||
}
|
||||
|
||||
hasChildren() {
|
||||
return this.children.length !== 0;
|
||||
}
|
||||
|
||||
addChild(node) {
|
||||
node.setParent(this);
|
||||
let children = this.children;
|
||||
if (node.object.isDir()) {
|
||||
this.children.unshift(node);
|
||||
} else {
|
||||
let index = -1;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (!children[i].object.isDir()) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1) { // -1: all the node object is dir;
|
||||
this.children.push(node);
|
||||
} else if (index === 0) { // 0: all the node object is file
|
||||
this.children.unshift(node);
|
||||
} else {
|
||||
this.children.splice(index, 0, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addChildren(nodeList) {
|
||||
nodeList.forEach(node => {
|
||||
node.setParent(this);
|
||||
});
|
||||
this.children = nodeList;
|
||||
}
|
||||
|
||||
deleteChild(node) {
|
||||
let children = this.children.filter(item => {
|
||||
return item !== node;
|
||||
});
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
rename(newName) {
|
||||
this.object.name = newName;
|
||||
this.path = this.generatePath(this.parentNode);
|
||||
if (this.isExpanded) {
|
||||
this.updateChildrenPath(this);
|
||||
} else {
|
||||
this.isLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateChildrenPath(node) {
|
||||
let children = node.children;
|
||||
children.forEach(child => {
|
||||
child.path = child.generatePath(child.parentNode);
|
||||
if (child.isExpanded) {
|
||||
child.updateChildrenPath(child);
|
||||
} else {
|
||||
child.isLoaded = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateObjectProperties(keys, newValues) {
|
||||
if (Array.isArray(keys) && Array.isArray(newValues)) {
|
||||
keys.forEach((key, index) => {
|
||||
this.object[key] = newValues[index];
|
||||
});
|
||||
} else {
|
||||
this.object[keys] = newValues;
|
||||
}
|
||||
}
|
||||
|
||||
generatePath(parentNode) {
|
||||
// return parentNode.path === '/' ? parentNode.path + this.object.name : parentNode.path + '/' + this.object.name;
|
||||
return this.path;
|
||||
}
|
||||
|
||||
serializeToJson() {
|
||||
let children = [];
|
||||
if (this.hasChildren) {
|
||||
children = this.children.map(m => m.serializeToJson());
|
||||
}
|
||||
|
||||
const treeNode = {
|
||||
path: this.path,
|
||||
object: this.object.clone(),
|
||||
isLoaded: this.isLoaded,
|
||||
isPreload: this.isPreload,
|
||||
isExpanded: this.isExpanded,
|
||||
parentNode: this.parentNode,
|
||||
children: children,
|
||||
};
|
||||
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
static deserializefromJson(json) {
|
||||
let { path, object, isLoaded, isPreload, isExpanded, parentNode, children = [] } = json;
|
||||
object = object.clone();
|
||||
|
||||
const treeNode = new TreeNode({
|
||||
path,
|
||||
object,
|
||||
isLoaded,
|
||||
isPreload,
|
||||
isExpanded,
|
||||
parentNode,
|
||||
children: children.map(item => TreeNode.deserializefromJson(item))
|
||||
});
|
||||
return treeNode;
|
||||
}
|
||||
}
|
||||
|
||||
export default TreeNode;
|
43
frontend/src/components/shared-dir-tree-view/tree-view.js
Normal file
43
frontend/src/components/shared-dir-tree-view/tree-view.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TreeNodeView from './tree-node-view';
|
||||
|
||||
const propTypes = {
|
||||
treeData: PropTypes.object.isRequired,
|
||||
currentPath: PropTypes.string.isRequired,
|
||||
onNodeClick: PropTypes.func.isRequired,
|
||||
onNodeExpanded: PropTypes.func.isRequired,
|
||||
onNodeCollapse: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const LEFT_INDENT = 20;
|
||||
|
||||
class TreeView extends React.Component {
|
||||
|
||||
/*
|
||||
onNodeClick = (node) => {
|
||||
this.props.onNodeClick(node);
|
||||
};
|
||||
*/
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={'tree-view tree'}
|
||||
>
|
||||
<TreeNodeView
|
||||
leftIndent={LEFT_INDENT}
|
||||
node={this.props.treeData.root}
|
||||
currentPath={this.props.currentPath}
|
||||
onNodeClick={this.props.onNodeClick}
|
||||
onNodeExpanded={this.props.onNodeExpanded}
|
||||
onNodeCollapse={this.props.onNodeCollapse}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TreeView.propTypes = propTypes;
|
||||
|
||||
export default TreeView;
|
139
frontend/src/components/shared-dir-tree-view/tree.js
Normal file
139
frontend/src/components/shared-dir-tree-view/tree.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import TreeNode from './tree-node';
|
||||
|
||||
class Tree {
|
||||
|
||||
constructor() {
|
||||
this.root = null;
|
||||
}
|
||||
|
||||
clone() {
|
||||
let tree = new Tree();
|
||||
if (this.root) {
|
||||
tree.root = this.root.clone();
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
setRoot(node) {
|
||||
this.root = node;
|
||||
}
|
||||
|
||||
getNodeByPath(path) {
|
||||
let findNode = null;
|
||||
function callback(currentNode) {
|
||||
if (currentNode.path === path) {
|
||||
findNode = currentNode;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
this.traverseDF(callback);
|
||||
return findNode;
|
||||
}
|
||||
|
||||
getNodeChildrenObject(node) {
|
||||
let objects = node.children.map(item => {
|
||||
let object = item.object;
|
||||
return object;
|
||||
});
|
||||
return objects;
|
||||
}
|
||||
|
||||
addNodeToParent(node, parentNode) {
|
||||
parentNode.addChild(node);
|
||||
}
|
||||
|
||||
addNodeListToParent(nodeList, parentNode) {
|
||||
nodeList.forEach(node => {
|
||||
parentNode.addChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
deleteNode(node) {
|
||||
let parentNode = this.getNodeByPath(node.parentNode.path);
|
||||
parentNode.deleteChild(node);
|
||||
}
|
||||
|
||||
deleteNodeList(nodeList) {
|
||||
nodeList.forEach(node => {
|
||||
this.deleteNode(node);
|
||||
});
|
||||
}
|
||||
|
||||
renameNode(node, newName) {
|
||||
node.rename(newName);
|
||||
}
|
||||
|
||||
updateNode(node, keys, newValues) {
|
||||
node.updateObjectParam(keys, newValues);
|
||||
}
|
||||
|
||||
moveNode(node, destNode) {
|
||||
this.deleteNode(node);
|
||||
destNode.addChild(node);
|
||||
}
|
||||
|
||||
copyNode(node, destNode) {
|
||||
destNode.addChild(node);
|
||||
}
|
||||
|
||||
traverseDF(callback) {
|
||||
let stack = [];
|
||||
let found = false;
|
||||
stack.unshift(this.root);
|
||||
let currentNode = stack.shift();
|
||||
while (!found && currentNode) {
|
||||
found = callback(currentNode) == true ? true : false;
|
||||
if (!found) {
|
||||
stack.unshift(...currentNode.children);
|
||||
currentNode = stack.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseBF(callback) {
|
||||
let queue = [];
|
||||
let found = false;
|
||||
queue.push(this.root);
|
||||
let currentNode = queue.shift();
|
||||
while (!found && currentNode) {
|
||||
found = callback(currentNode) === true ? true : false;
|
||||
if (!found) {
|
||||
queue.push(...currentNode.children);
|
||||
currentNode = queue.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expandNode(node) {
|
||||
node.isExpanded = true;
|
||||
while (node.parentNode) {
|
||||
node.parentNode.isExpanded = true;
|
||||
node = node.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
collapseNode(node) {
|
||||
node.isExpanded = false;
|
||||
}
|
||||
|
||||
isNodeChild(node, parentNode) {
|
||||
return parentNode.children.some(item => {
|
||||
return item.path === node.path;
|
||||
});
|
||||
}
|
||||
|
||||
serializeToJson() {
|
||||
return this.root.serializeToJson();
|
||||
}
|
||||
|
||||
deserializefromJson(json) {
|
||||
let root = TreeNode.deserializefromJson(json);
|
||||
let tree = new Tree();
|
||||
tree.setRoot(root);
|
||||
return tree;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Tree;
|
Reference in New Issue
Block a user