1
0
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:
llj
2025-02-17 14:58:30 +08:00
committed by GitHub
parent 06410d217d
commit 79413cb4fd
12 changed files with 1236 additions and 205 deletions

View File

@@ -0,0 +1,5 @@
import TreeHelper from './tree-helper';
import TreeView from './tree-view';
import TreeNode from './tree-node';
export { TreeHelper, TreeView, TreeNode };

View 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;

View 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;

View 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;

View 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;

View 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;