mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-01 23:38:37 +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:
parent
06410d217d
commit
79413cb4fd
@ -19,7 +19,7 @@ const propTypes = {
|
|||||||
repoID: PropTypes.string,
|
repoID: PropTypes.string,
|
||||||
onDirentItemClick: PropTypes.func,
|
onDirentItemClick: PropTypes.func,
|
||||||
onRepoItemClick: PropTypes.func,
|
onRepoItemClick: PropTypes.func,
|
||||||
mode: PropTypes.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
fileSuffixes: PropTypes.arrayOf(PropTypes.string),
|
fileSuffixes: PropTypes.arrayOf(PropTypes.string),
|
||||||
currentPath: PropTypes.string,
|
currentPath: PropTypes.string,
|
||||||
searchResults: PropTypes.array,
|
searchResults: PropTypes.array,
|
||||||
|
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;
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
|||||||
import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
|
import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||||
import { gettext } from '../utils/constants';
|
import { gettext } from '../utils/constants';
|
||||||
|
|
||||||
|
import '../css/item-dropdown-menu.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
sortBy: PropTypes.string,
|
sortBy: PropTypes.string,
|
||||||
sortOrder: PropTypes.string,
|
sortOrder: PropTypes.string,
|
||||||
|
@ -6,60 +6,131 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-header {
|
#shared-dir-view .side-panel {
|
||||||
background-color: #f8fafd;
|
border-right: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#shared-dir-view .meta-info {
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
padding: 8px 16px 4px;
|
}
|
||||||
height: 53px;
|
|
||||||
|
/* tree view */
|
||||||
|
.tree-node-hight-light {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-hight-light::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
left: -8px;
|
||||||
|
top: 2px;
|
||||||
|
background-color: #ff9800;
|
||||||
|
border-radius: 2px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-inner {
|
||||||
|
position: relative;
|
||||||
|
height: 28px;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.625;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-inner-hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-inner .tree-node-text {
|
||||||
|
padding-left: 2.8rem;
|
||||||
|
width: calc(100% - 1.5rem);
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-inner .left-icon {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
top: 1px;
|
||||||
|
left: 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-toggle-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.625;
|
||||||
|
width: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* toolbar */
|
||||||
|
#shared-dir-view .path-container .path-item {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#shared-dir-view .path-container .path-item:hover {
|
||||||
|
background: #efefef;
|
||||||
|
}
|
||||||
|
|
||||||
|
#shared-dir-view .path-container .path-split {
|
||||||
|
padding: 0 2px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-dir-view-main {
|
#shared-dir-view .path-item-dropdown-toggle .sf3-font {
|
||||||
width: calc(100% - 40px);
|
color: #666;
|
||||||
max-width: 950px;
|
|
||||||
padding: 15px 0 40px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-bar {
|
#shared-dir-view .path-item-dropdown-toggle .main-icon {
|
||||||
padding: 9px 10px;
|
margin-right: 2px;
|
||||||
background: #f2f2f2;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-view-mode-btn {
|
@keyframes displaySelectedToolbar {
|
||||||
padding: 0;
|
from {
|
||||||
height: 30px;
|
top: 24px;
|
||||||
min-width: 2rem;
|
}
|
||||||
color: #aaa;
|
to {
|
||||||
background-color: #fff;
|
top: 0;
|
||||||
border: 1px solid #ccc;
|
}
|
||||||
line-height: 29px;
|
|
||||||
font-size: 18px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-view-mode-btn.current-mode {
|
.cur-view-path .selected-items-toolbar {
|
||||||
background-color: #ccc;
|
height: 24px;
|
||||||
color: #fff;
|
position: relative;
|
||||||
box-shadow: none;
|
animation: displaySelectedToolbar .3s ease-in-out 1;
|
||||||
}
|
|
||||||
|
|
||||||
.shared-dir-op-btn {
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shared-dir-upload-btn {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-item .action-icon {
|
.grid-item .action-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
padding: 3px 5px;
|
padding: 5px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MD5 from 'MD5';
|
import MD5 from 'MD5';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Button, Dropdown, DropdownToggle, DropdownItem, UncontrolledTooltip } from 'reactstrap';
|
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, UncontrolledTooltip } from 'reactstrap';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import Account from './components/common/account';
|
import Account from './components/common/account';
|
||||||
@ -20,9 +20,18 @@ import SaveSharedDirDialog from './components/dialog/save-shared-dir-dialog';
|
|||||||
import CopyMoveDirentProgressDialog from './components/dialog/copy-move-dirent-progress-dialog';
|
import CopyMoveDirentProgressDialog from './components/dialog/copy-move-dirent-progress-dialog';
|
||||||
import RepoInfoBar from './components/repo-info-bar';
|
import RepoInfoBar from './components/repo-info-bar';
|
||||||
import RepoTag from './models/repo-tag';
|
import RepoTag from './models/repo-tag';
|
||||||
import { GRID_MODE, LIST_MODE } from './components/dir-view-mode/constants';
|
import { LIST_MODE } from './components/dir-view-mode/constants';
|
||||||
import { MetadataAIOperationsProvider } from './hooks/metadata-ai-operation';
|
import { MetadataAIOperationsProvider } from './hooks/metadata-ai-operation';
|
||||||
|
import ViewModes from './components/view-modes';
|
||||||
|
import SortMenu from './components/sort-menu';
|
||||||
|
import { TreeHelper, TreeNode, TreeView } from './components/shared-dir-tree-view';
|
||||||
|
import ResizeBar from './components/resize-bar';
|
||||||
|
import {
|
||||||
|
DRAG_HANDLER_HEIGHT, INIT_SIDE_PANEL_RATE, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE
|
||||||
|
} from './components/resize-bar/constants';
|
||||||
|
|
||||||
|
import './css/layout.css';
|
||||||
|
import './css/header.css';
|
||||||
import './css/shared-dir-view.css';
|
import './css/shared-dir-view.css';
|
||||||
import './css/grid-view.css';
|
import './css/grid-view.css';
|
||||||
|
|
||||||
@ -33,7 +42,7 @@ let loginUser = window.app.pageOptions.name;
|
|||||||
let {
|
let {
|
||||||
token, dirName, dirPath, sharedBy,
|
token, dirName, dirPath, sharedBy,
|
||||||
repoID, relativePath,
|
repoID, relativePath,
|
||||||
mode, thumbnailSize, zipped,
|
mode, thumbnailSize,
|
||||||
trafficOverLimit, canDownload,
|
trafficOverLimit, canDownload,
|
||||||
noQuota, canUpload, enableVideoThumbnail, enablePDFThumbnail
|
noQuota, canUpload, enableVideoThumbnail, enablePDFThumbnail
|
||||||
} = window.shared.pageOptions;
|
} = window.shared.pageOptions;
|
||||||
@ -45,10 +54,19 @@ class SharedDirView extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isTreeDataLoading: true,
|
||||||
|
treeData: TreeHelper.buildTree(),
|
||||||
|
|
||||||
|
// for resizing side/main panels
|
||||||
|
inResizing: false,
|
||||||
|
sidePanelRate: parseFloat(localStorage.getItem('sf_side_panel_rate') || INIT_SIDE_PANEL_RATE),
|
||||||
|
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
items: [],
|
items: [],
|
||||||
|
path: relativePath,
|
||||||
|
|
||||||
|
isDropdownMenuOpen: false,
|
||||||
currentMode: mode,
|
currentMode: mode,
|
||||||
|
|
||||||
isAllItemsSelected: false,
|
isAllItemsSelected: false,
|
||||||
@ -61,7 +79,6 @@ class SharedDirView extends React.Component {
|
|||||||
zipFolderPath: '',
|
zipFolderPath: '',
|
||||||
|
|
||||||
usedRepoTags: [],
|
usedRepoTags: [],
|
||||||
isRepoInfoBarShow: false,
|
|
||||||
|
|
||||||
isSaveSharedDirDialogShow: false,
|
isSaveSharedDirDialogShow: false,
|
||||||
itemsForSave: [],
|
itemsForSave: [],
|
||||||
@ -75,6 +92,9 @@ class SharedDirView extends React.Component {
|
|||||||
imageItems: [],
|
imageItems: [],
|
||||||
imageIndex: 0
|
imageIndex: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.resizeBarRef = React.createRef();
|
||||||
|
this.dragHandlerRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -84,12 +104,54 @@ class SharedDirView extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listItems(thumbnailSize);
|
this.loadTreePanel();
|
||||||
|
this.listItems();
|
||||||
this.getShareLinkRepoTags();
|
this.getShareLinkRepoTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
listItems = (thumbnailSize) => {
|
loadTreePanel = () => {
|
||||||
seafileAPI.listSharedDir(token, relativePath, thumbnailSize).then((res) => {
|
seafileAPI.listSharedDir(token, '/', thumbnailSize).then((res) => {
|
||||||
|
const { dirent_list } = res.data;
|
||||||
|
let tree = this.state.treeData;
|
||||||
|
this.addResponseListToNode(dirent_list, tree.root);
|
||||||
|
this.setState({
|
||||||
|
isTreeDataLoading: false,
|
||||||
|
treeData: tree
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.setState({ isTreeDataLoading: false });
|
||||||
|
});
|
||||||
|
/* keep it for now
|
||||||
|
if (relativePath == '/') {
|
||||||
|
} else {
|
||||||
|
this.loadNodeAndParentsByPath(relativePath);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
loadNodeAndParentsByPath = (path) => {
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
addResponseListToNode = (list, node) => {
|
||||||
|
node.isLoaded = true;
|
||||||
|
node.isExpanded = true;
|
||||||
|
|
||||||
|
// only display folders in the tree
|
||||||
|
const dirList = list.filter(item => item.is_dir);
|
||||||
|
const direntList = Utils.sortDirentsInSharedDir(dirList, this.state.sortBy, this.state.sortOrder);
|
||||||
|
|
||||||
|
const nodeList = direntList.map(object => {
|
||||||
|
return new TreeNode({ object });
|
||||||
|
});
|
||||||
|
node.addChildren(nodeList);
|
||||||
|
};
|
||||||
|
|
||||||
|
listItems = () => {
|
||||||
|
const { path, currentMode } = this.state;
|
||||||
|
const thumbnailSize = currentMode == LIST_MODE ? thumbnailDefaultSize : thumbnailSizeForGrid;
|
||||||
|
seafileAPI.listSharedDir(token, path, thumbnailSize).then((res) => {
|
||||||
const items = res.data['dirent_list'].map(item => {
|
const items = res.data['dirent_list'].map(item => {
|
||||||
item.isSelected = false;
|
item.isSelected = false;
|
||||||
return item;
|
return item;
|
||||||
@ -107,6 +169,21 @@ class SharedDirView extends React.Component {
|
|||||||
errorMsg: errorMsg
|
errorMsg: errorMsg
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update the URL
|
||||||
|
let normalizedPath = '';
|
||||||
|
if (path == '/') {
|
||||||
|
normalizedPath = path;
|
||||||
|
} else {
|
||||||
|
normalizedPath = path[path.length - 1] === '/' ? path.slice(0, path.length - 1) : path;
|
||||||
|
}
|
||||||
|
let url = new URL(location.href);
|
||||||
|
let searchParams = new URLSearchParams(url.search);
|
||||||
|
searchParams.set('p', normalizedPath);
|
||||||
|
searchParams.set('mode', currentMode);
|
||||||
|
url.search = searchParams.toString();
|
||||||
|
url = url.toString();
|
||||||
|
window.history.pushState({ url: url, path: path }, path, url);
|
||||||
};
|
};
|
||||||
|
|
||||||
sortItems = (sortBy, sortOrder) => {
|
sortItems = (sortBy, sortOrder) => {
|
||||||
@ -151,22 +228,136 @@ class SharedDirView extends React.Component {
|
|||||||
getThumbnail(0);
|
getThumbnail(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleDropdownMenu = () => {
|
||||||
|
this.setState({
|
||||||
|
isDropdownMenuOpen: !this.state.isDropdownMenuOpen
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onDropdownToggleKeyDown = (e) => {
|
||||||
|
if (e.key == 'Enter' || e.key == 'Space') {
|
||||||
|
this.toggleDropdownMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMenuItemKeyDown = (item, e) => {
|
||||||
|
if (e.key == 'Enter' || e.key == 'Space') {
|
||||||
|
item.onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
visitFolder = (folderPath) => {
|
||||||
|
this.setState({
|
||||||
|
path: folderPath
|
||||||
|
}, () => {
|
||||||
|
this.listItems();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
renderPath = () => {
|
renderPath = () => {
|
||||||
|
let opList = [];
|
||||||
|
if (showDownloadIcon) {
|
||||||
|
opList.push({
|
||||||
|
'icon': 'download1',
|
||||||
|
'text': gettext('ZIP'),
|
||||||
|
'onClick': this.zipDownloadFolder.bind(this, this.state.path)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canDownload && loginUser && (loginUser !== sharedBy)) {
|
||||||
|
opList.push({
|
||||||
|
'icon': 'save',
|
||||||
|
'text': gettext('Save'),
|
||||||
|
'onClick': this.saveAllItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUpload) {
|
||||||
|
opList.push({
|
||||||
|
'icon': 'upload-files',
|
||||||
|
'disabled': noQuota,
|
||||||
|
'title': noQuota ? gettext('The owner of this library has run out of space.') : '',
|
||||||
|
'text': gettext('Upload'),
|
||||||
|
'onClick': this.onUploadFile
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipped = []; // be compatible with the old code
|
||||||
|
const rootItem = {
|
||||||
|
path: '/',
|
||||||
|
name: dirName
|
||||||
|
};
|
||||||
|
zipped.push(rootItem);
|
||||||
|
const { path } = this.state;
|
||||||
|
if (path != '/') {
|
||||||
|
const normalizedPath = path[path.length - 1] === '/' ? path.slice(0, path.length - 1) : path;
|
||||||
|
const pathList = normalizedPath.split('/');
|
||||||
|
pathList.shift();
|
||||||
|
let itemPath = '';
|
||||||
|
const subItems = pathList.map((item, index) => {
|
||||||
|
itemPath += '/' + item;
|
||||||
|
return {
|
||||||
|
path: itemPath + '/', // the ending '/' is necessary
|
||||||
|
name: item
|
||||||
|
};
|
||||||
|
});
|
||||||
|
zipped.push(...subItems);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{zipped.map((item, index) => {
|
{zipped.map((item, index) => {
|
||||||
if (index != zipped.length - 1) {
|
if (index != zipped.length - 1) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<a href={`?p=${encodeURIComponent(item.path)}&mode=${mode}`} className="mx-1 ellipsis" title={item.name}>{item.name}</a>
|
<span className="path-item" title={item.name} role="button" onClick={this.visitFolder.bind(this, item.path)}>{item.name}</span>
|
||||||
<span> / </span>
|
<span className="path-split"> / </span>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})}
|
||||||
|
{(!showDownloadIcon && !canUpload)
|
||||||
|
? <span className="path-item" title={zipped[zipped.length - 1].name}>{zipped[zipped.length - 1].name}</span>
|
||||||
|
: (
|
||||||
|
<Dropdown isOpen={this.state.isDropdownMenuOpen} toggle={this.toggleDropdownMenu}>
|
||||||
|
<DropdownToggle
|
||||||
|
tag="div"
|
||||||
|
role="button"
|
||||||
|
className="path-item path-item-dropdown-toggle"
|
||||||
|
onClick={this.toggleDropdownMenu}
|
||||||
|
onKeyDown={this.onDropdownToggleKeyDown}
|
||||||
|
data-toggle="dropdown"
|
||||||
|
>
|
||||||
|
<span title={zipped[zipped.length - 1].name}>{zipped[zipped.length - 1].name}</span>
|
||||||
|
{canUpload
|
||||||
|
? <><i className="sf3-font-new sf3-font main-icon ml-2"></i><i className="sf3-font-down sf3-font"></i></>
|
||||||
|
: <i className="sf3-font-down sf3-font ml-1"></i>
|
||||||
|
}
|
||||||
|
</DropdownToggle>
|
||||||
|
<DropdownMenu positionFixed={true}>
|
||||||
|
{opList.map((item, index) => {
|
||||||
|
if (item == 'Divider') {
|
||||||
|
return <DropdownItem key={index} divider />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={index}
|
||||||
|
onClick={item.onClick}
|
||||||
|
onKeyDown={this.onMenuItemKeyDown.bind(this, item)}
|
||||||
|
disabled={item.disabled || false}
|
||||||
|
title={item.title || ''}
|
||||||
|
>
|
||||||
|
<i className={`sf3-font-${item.icon} sf3-font mr-2 dropdown-item-icon`}></i>
|
||||||
|
{item.text}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
<span className="ml-1 ellipsis" title={zipped[zipped.length - 1].name}>{zipped[zipped.length - 1].name}</span>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -193,17 +384,18 @@ class SharedDirView extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
zipDownloadSelectedItems = () => {
|
zipDownloadSelectedItems = () => {
|
||||||
|
const { path } = this.state;
|
||||||
if (!useGoFileserver) {
|
if (!useGoFileserver) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isZipDialogOpen: true,
|
isZipDialogOpen: true,
|
||||||
zipFolderPath: relativePath,
|
zipFolderPath: path,
|
||||||
selectedItems: this.state.items.filter(item => item.isSelected)
|
selectedItems: this.state.items.filter(item => item.isSelected)
|
||||||
.map(item => item.file_name || item.folder_name)
|
.map(item => item.file_name || item.folder_name)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let target = this.state.items.filter(item => item.isSelected).map(item => item.file_name || item.folder_name);
|
let target = this.state.items.filter(item => item.isSelected).map(item => item.file_name || item.folder_name);
|
||||||
seafileAPI.getShareLinkDirentsZipTask(token, relativePath, target).then((res) => {
|
seafileAPI.getShareLinkDirentsZipTask(token, path, target).then((res) => {
|
||||||
const zipToken = res.data['zip_token'];
|
const zipToken = res.data['zip_token'];
|
||||||
location.href = `${fileServerRoot}zip/${zipToken}`;
|
location.href = `${fileServerRoot}zip/${zipToken}`;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@ -277,10 +469,8 @@ class SharedDirView extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleSaveSharedDir = (destRepoID, dstPath) => {
|
handleSaveSharedDir = (destRepoID, dstPath) => {
|
||||||
|
const { path, itemsForSave } = this.state;
|
||||||
const itemsForSave = this.state.itemsForSave;
|
seafileAPI.saveSharedDir(destRepoID, dstPath, token, path, itemsForSave).then((res) => {
|
||||||
|
|
||||||
seafileAPI.saveSharedDir(destRepoID, dstPath, token, relativePath, itemsForSave).then((res) => {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isSaveSharedDirDialogShow: false,
|
isSaveSharedDirDialogShow: false,
|
||||||
itemsForSave: [],
|
itemsForSave: [],
|
||||||
@ -376,6 +566,16 @@ class SharedDirView extends React.Component {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
unselectItems = () => {
|
||||||
|
this.setState({
|
||||||
|
isAllItemsSelected: false,
|
||||||
|
items: this.state.items.map((item) => {
|
||||||
|
item.isSelected = false;
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
toggleAllSelected = () => {
|
toggleAllSelected = () => {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
isAllItemsSelected: !prevState.isAllItemsSelected,
|
isAllItemsSelected: !prevState.isAllItemsSelected,
|
||||||
@ -407,11 +607,12 @@ class SharedDirView extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onFileUploadSuccess = (direntObject) => {
|
onFileUploadSuccess = (direntObject) => {
|
||||||
|
const { path } = this.state;
|
||||||
const { name, size } = direntObject;
|
const { name, size } = direntObject;
|
||||||
const newItem = {
|
const newItem = {
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
file_name: name,
|
file_name: name,
|
||||||
file_path: Utils.joinPath(relativePath, name),
|
file_path: Utils.joinPath(path, name),
|
||||||
is_dir: false,
|
is_dir: false,
|
||||||
last_modified: dayjs().format(),
|
last_modified: dayjs().format(),
|
||||||
size: size
|
size: size
|
||||||
@ -434,9 +635,6 @@ class SharedDirView extends React.Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.setState({ usedRepoTags: usedRepoTags });
|
this.setState({ usedRepoTags: usedRepoTags });
|
||||||
if (Utils.isDesktop() && usedRepoTags.length != 0 && relativePath == '/') {
|
|
||||||
this.setState({ isRepoInfoBarShow: true });
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
toaster.danger(errMessage);
|
toaster.danger(errMessage);
|
||||||
@ -452,76 +650,203 @@ class SharedDirView extends React.Component {
|
|||||||
currentMode: mode,
|
currentMode: mode,
|
||||||
isLoading: true
|
isLoading: true
|
||||||
}, () => {
|
}, () => {
|
||||||
const thumbnailSize = mode == LIST_MODE ? thumbnailDefaultSize : thumbnailSizeForGrid;
|
this.listItems();
|
||||||
this.listItems(thumbnailSize);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onSelectSortOption = (item) => {
|
||||||
|
const [sortBy, sortOrder] = item.value.split('-');
|
||||||
|
this.sortItems(sortBy, sortOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
onTreeNodeCollapse = (node) => {
|
||||||
|
const tree = TreeHelper.collapseNode(this.state.treeData, node);
|
||||||
|
this.setState({ treeData: tree });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTreeNodeExpanded = (node) => {
|
||||||
|
let tree = this.state.treeData.clone();
|
||||||
|
node = tree.getNodeByPath(node.path);
|
||||||
|
if (!node.isLoaded) {
|
||||||
|
seafileAPI.listSharedDir(token, node.path, thumbnailSize).then((res) => {
|
||||||
|
const { dirent_list } = res.data;
|
||||||
|
this.addResponseListToNode(dirent_list, node);
|
||||||
|
this.setState({ treeData: tree });
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tree.expandNode(node);
|
||||||
|
this.setState({ treeData: tree });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onTreeNodeClick = (node) => {
|
||||||
|
if (node.object.is_dir) {
|
||||||
|
if (node.isLoaded && node.path === this.state.path) {
|
||||||
|
if (node.isExpanded) {
|
||||||
|
let tree = TreeHelper.collapseNode(this.state.treeData, node);
|
||||||
|
this.setState({ treeData: tree });
|
||||||
|
} else {
|
||||||
|
let tree = this.state.treeData.clone();
|
||||||
|
node = tree.getNodeByPath(node.path);
|
||||||
|
tree.expandNode(node);
|
||||||
|
this.setState({ treeData: tree });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.isLoaded) {
|
||||||
|
let tree = this.state.treeData.clone();
|
||||||
|
node = tree.getNodeByPath(node.path);
|
||||||
|
seafileAPI.listSharedDir(token, node.path, thumbnailSize).then((res) => {
|
||||||
|
const { dirent_list } = res.data;
|
||||||
|
this.addResponseListToNode(dirent_list, node);
|
||||||
|
tree.collapseNode(node);
|
||||||
|
this.setState({ treeData: tree });
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.path === this.state.path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.visitFolder(node.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onResizeMouseUp = () => {
|
||||||
|
if (this.state.inResizing) {
|
||||||
|
this.setState({
|
||||||
|
inResizing: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
localStorage.setItem('sf_side_panel_rate', this.state.sidePanelRate);
|
||||||
|
};
|
||||||
|
|
||||||
|
onResizeMouseDown = () => {
|
||||||
|
this.setState({
|
||||||
|
inResizing: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onResizeMouseMove = (e) => {
|
||||||
|
let rate = e.nativeEvent.clientX / window.innerWidth;
|
||||||
|
this.setState({
|
||||||
|
sidePanelRate: Math.max(Math.min(rate, MAX_SIDE_PANEL_RATE), MIN_SIDE_PANEL_RATE),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onResizeMouseOver = (event) => {
|
||||||
|
if (!this.dragHandlerRef.current) return;
|
||||||
|
const { top } = this.resizeBarRef.current.getBoundingClientRect();
|
||||||
|
const dragHandlerRefTop = event.pageY - top - DRAG_HANDLER_HEIGHT / 2;
|
||||||
|
this.setDragHandlerTop(dragHandlerRefTop);
|
||||||
|
};
|
||||||
|
|
||||||
|
setDragHandlerTop = (top) => {
|
||||||
|
this.dragHandlerRef.current.style.top = top + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentMode: mode } = this.state;
|
const {
|
||||||
|
usedRepoTags, currentMode: mode,
|
||||||
|
sortBy, sortOrder, isTreeDataLoading, treeData, path,
|
||||||
|
sidePanelRate, inResizing
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const mainPanelStyle = {
|
||||||
|
userSelect: inResizing ? 'none' : '',
|
||||||
|
flex: sidePanelRate ? `1 0 ${(1 - sidePanelRate) * 100}%` : `0 0 ${100 - INIT_SIDE_PANEL_RATE * 100}%`,
|
||||||
|
};
|
||||||
|
const sidePanelStyle = {
|
||||||
|
userSelect: inResizing ? 'none' : '',
|
||||||
|
flex: sidePanelRate ? `0 0 ${sidePanelRate * 100}%` : `0 0 ${INIT_SIDE_PANEL_RATE * 100}%`,
|
||||||
|
};
|
||||||
|
|
||||||
const isDesktop = Utils.isDesktop();
|
const isDesktop = Utils.isDesktop();
|
||||||
const modeBaseClass = 'btn btn-secondary btn-icon sf-view-mode-btn';
|
const selectedItemsLength = this.state.items.filter(item => item.isSelected).length;
|
||||||
|
const isRepoInfoBarShown = isDesktop && path == '/' && usedRepoTags.length != 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetadataAIOperationsProvider repoID={repoID} enableMetadata={false} enableOCR={false} repoInfo={{ permission: 'r' }} >
|
<MetadataAIOperationsProvider repoID={repoID} enableMetadata={false} enableOCR={false} repoInfo={{ permission: 'r' }} >
|
||||||
<div className="h-100 d-flex flex-column">
|
<div id="shared-dir-view" className="h-100 d-flex flex-column">
|
||||||
<div className="top-header d-flex justify-content-between">
|
<div className="top-header d-flex justify-content-between flex-shrink-0">
|
||||||
<a href={siteRoot}>
|
<a href={siteRoot}>
|
||||||
<img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
|
<img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
|
||||||
</a>
|
</a>
|
||||||
{loginUser && <Account />}
|
{loginUser && <Account />}
|
||||||
</div>
|
</div>
|
||||||
<div className="o-auto">
|
<div
|
||||||
<div className="shared-dir-view-main">
|
className="flex-fill d-flex o-hidden position-relative"
|
||||||
<h2 className="h3 text-truncate" title={dirName}>{dirName}</h2>
|
onMouseMove={inResizing ? this.onResizeMouseMove : null}
|
||||||
<p>{gettext('Shared by: ')}{sharedBy}</p>
|
onMouseUp={this.onResizeMouseUp}
|
||||||
<div className="d-flex justify-content-between align-items-center op-bar">
|
>
|
||||||
<p className="m-0 mr-4 ellipsis d-flex align-items-center">{gettext('Current path: ')}{this.renderPath()}</p>
|
<div className="side-panel" style={sidePanelStyle}>
|
||||||
<div className="flex-none">
|
<div className="meta-info py-4 mx-4">
|
||||||
{isDesktop &&
|
<h2 className="h3 text-truncate mb-4" title={dirName}>{dirName}</h2>
|
||||||
<div className="view-mode btn-group">
|
<p className="m-0">{gettext('Shared by: ')}{sharedBy}</p>
|
||||||
<button
|
</div>
|
||||||
className={`${modeBaseClass} sf2-icon-list-view ${mode == LIST_MODE ? 'current-mode' : ''}`}
|
<div className="p-4 flex-fill o-auto">
|
||||||
title={gettext('List')}
|
{isTreeDataLoading ? <Loading /> : (
|
||||||
aria-label={gettext('List')}
|
<TreeView
|
||||||
onClick={this.switchMode.bind(this, LIST_MODE)}
|
currentPath={path}
|
||||||
>
|
treeData={treeData}
|
||||||
</button>
|
onNodeExpanded={this.onTreeNodeExpanded}
|
||||||
<button
|
onNodeCollapse={this.onTreeNodeCollapse}
|
||||||
className={`${modeBaseClass} sf2-icon-grid-view ${mode == GRID_MODE ? 'current-mode' : ''}`}
|
onNodeClick={this.onTreeNodeClick}
|
||||||
title={gettext('Grid')}
|
/>
|
||||||
aria-label={gettext('Grid')}
|
)}
|
||||||
onClick={this.switchMode.bind(this, GRID_MODE)}
|
</div>
|
||||||
>
|
</div>
|
||||||
</button>
|
{isDesktop &&
|
||||||
</div>
|
<ResizeBar
|
||||||
|
resizeBarRef={this.resizeBarRef}
|
||||||
|
dragHandlerRef={this.dragHandlerRef}
|
||||||
|
resizeBarStyle={{ left: `calc(${sidePanelRate ? sidePanelRate * 100 + '%' : `${INIT_SIDE_PANEL_RATE * 100}%`} - 1px)` }}
|
||||||
|
dragHandlerStyle={{ height: DRAG_HANDLER_HEIGHT }}
|
||||||
|
onResizeMouseDown={this.onResizeMouseDown}
|
||||||
|
onResizeMouseOver={this.onResizeMouseOver}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<div className="main-panel cur-view-container" style={mainPanelStyle}>
|
||||||
|
<div className="cur-view-path d-flex justify-content-between align-items-center">
|
||||||
|
<div className="cur-view-path-left flex-fill o-hidden">
|
||||||
|
{(showDownloadIcon && this.state.items.some(item => item.isSelected))
|
||||||
|
? (
|
||||||
|
<div className="selected-items-toolbar">
|
||||||
|
<span className="cur-view-path-btn px-1" onClick={this.unselectItems}>
|
||||||
|
<span className="sf3-font-x-01 sf3-font mr-2" aria-label={gettext('Unselect')} title={gettext('Unselect')}></span>
|
||||||
|
<span>{`${selectedItemsLength} ${gettext('selected')}`}</span>
|
||||||
|
</span>
|
||||||
|
<span className="cur-view-path-btn ml-4" onClick={this.zipDownloadSelectedItems}>
|
||||||
|
<span className="sf3-font-download1 sf3-font" aria-label={gettext('Download')} title={gettext('Download')}></span>
|
||||||
|
</span>
|
||||||
|
{(canDownload && loginUser && (loginUser !== sharedBy)) &&
|
||||||
|
<span className="cur-view-path-btn ml-4" onClick={this.saveSelectedItems}>
|
||||||
|
<span className="sf3-font-save sf3-font" aria-label={gettext('Save')} title={gettext('Save')}></span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className="path-container">
|
||||||
|
<span className="mr-2">{gettext('Current path: ')}</span>
|
||||||
|
{this.renderPath()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
{canUpload && (
|
</div>
|
||||||
<Button disabled={noQuota}
|
<div className="cur-view-path-right">
|
||||||
title={noQuota ? gettext('The owner of this library has run out of space.') : ''}
|
{isDesktop && (
|
||||||
onClick={this.onUploadFile} className="ml-2 shared-dir-op-btn shared-dir-upload-btn"
|
<>
|
||||||
>{gettext('Upload')}
|
<ViewModes currentViewMode={mode} switchViewMode={this.switchMode} />
|
||||||
</Button>
|
<SortMenu sortBy={sortBy} sortOrder={sortOrder} onSelectSortOption={this.onSelectSortOption} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{showDownloadIcon &&
|
|
||||||
<Fragment>
|
|
||||||
{this.state.items.some(item => item.isSelected) ?
|
|
||||||
<Fragment>
|
|
||||||
<Button color="success" onClick={this.zipDownloadSelectedItems} className="ml-2 shared-dir-op-btn">{gettext('ZIP Selected Items')}</Button>
|
|
||||||
{(canDownload && loginUser && (loginUser !== sharedBy)) &&
|
|
||||||
<Button color="success" onClick={this.saveSelectedItems} className="ml-2 shared-dir-op-btn">{gettext('Save Selected Items')}</Button>
|
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
:
|
|
||||||
<Fragment>
|
|
||||||
<Button color="success" onClick={this.zipDownloadFolder.bind(this, relativePath)} className="ml-2 shared-dir-op-btn">{gettext('ZIP')}</Button>
|
|
||||||
{(canDownload && loginUser && (loginUser !== sharedBy)) &&
|
|
||||||
<Button color="success" onClick={this.saveAllItems} className="ml-2 shared-dir-op-btn">{gettext('Save')}</Button>
|
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!noQuota && canUpload && (
|
{!noQuota && canUpload && (
|
||||||
@ -530,38 +855,40 @@ class SharedDirView extends React.Component {
|
|||||||
dragAndDrop={false}
|
dragAndDrop={false}
|
||||||
token={token}
|
token={token}
|
||||||
path={dirPath === '/' ? dirPath : dirPath.replace(/\/+$/, '')}
|
path={dirPath === '/' ? dirPath : dirPath.replace(/\/+$/, '')}
|
||||||
relativePath={relativePath === '/' ? relativePath : relativePath.replace(/\/+$/, '')}
|
relativePath={path === '/' ? path : path.replace(/\/+$/, '')}
|
||||||
repoID={repoID}
|
repoID={repoID}
|
||||||
onFileUploadSuccess={this.onFileUploadSuccess}
|
onFileUploadSuccess={this.onFileUploadSuccess}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.state.isRepoInfoBarShow && (
|
<div className="cur-view-content p-0">
|
||||||
<RepoInfoBar
|
{isRepoInfoBarShown && (
|
||||||
repoID={repoID}
|
<RepoInfoBar
|
||||||
currentPath={'/'}
|
repoID={repoID}
|
||||||
usedRepoTags={this.state.usedRepoTags}
|
currentPath={'/'}
|
||||||
shareLinkToken={token}
|
usedRepoTags={this.state.usedRepoTags}
|
||||||
enableFileDownload={showDownloadIcon}
|
shareLinkToken={token}
|
||||||
className="mx-0"
|
enableFileDownload={showDownloadIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Content
|
<Content
|
||||||
isDesktop={isDesktop}
|
isDesktop={isDesktop}
|
||||||
isLoading={this.state.isLoading}
|
isLoading={this.state.isLoading}
|
||||||
errorMsg={this.state.errorMsg}
|
errorMsg={this.state.errorMsg}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
items={this.state.items}
|
items={this.state.items}
|
||||||
sortBy={this.state.sortBy}
|
sortBy={this.state.sortBy}
|
||||||
sortOrder={this.state.sortOrder}
|
sortOrder={this.state.sortOrder}
|
||||||
sortItems={this.sortItems}
|
sortItems={this.sortItems}
|
||||||
isAllItemsSelected={this.state.isAllItemsSelected}
|
isAllItemsSelected={this.state.isAllItemsSelected}
|
||||||
toggleAllSelected={this.toggleAllSelected}
|
toggleAllSelected={this.toggleAllSelected}
|
||||||
toggleItemSelected={this.toggleItemSelected}
|
toggleItemSelected={this.toggleItemSelected}
|
||||||
zipDownloadFolder={this.zipDownloadFolder}
|
visitFolder={this.visitFolder}
|
||||||
showImagePopup={this.showImagePopup}
|
zipDownloadFolder={this.zipDownloadFolder}
|
||||||
/>
|
showImagePopup={this.showImagePopup}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -578,7 +905,7 @@ class SharedDirView extends React.Component {
|
|||||||
{this.state.isSaveSharedDirDialogShow &&
|
{this.state.isSaveSharedDirDialogShow &&
|
||||||
<SaveSharedDirDialog
|
<SaveSharedDirDialog
|
||||||
sharedToken={token}
|
sharedToken={token}
|
||||||
parentDir={relativePath}
|
parentDir={path}
|
||||||
items={this.state.itemsForSave}
|
items={this.state.itemsForSave}
|
||||||
toggleCancel={this.toggleSaveSharedDirCancel}
|
toggleCancel={this.toggleSaveSharedDirCancel}
|
||||||
handleSaveSharedDir={this.handleSaveSharedDir}
|
handleSaveSharedDir={this.handleSaveSharedDir}
|
||||||
@ -660,6 +987,7 @@ class Content extends React.Component {
|
|||||||
isDesktop={isDesktop}
|
isDesktop={isDesktop}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
item={item}
|
item={item}
|
||||||
|
visitFolder={this.props.visitFolder}
|
||||||
zipDownloadFolder={this.props.zipDownloadFolder}
|
zipDownloadFolder={this.props.zipDownloadFolder}
|
||||||
showImagePopup={this.props.showImagePopup}
|
showImagePopup={this.props.showImagePopup}
|
||||||
toggleItemSelected={this.props.toggleItemSelected}
|
toggleItemSelected={this.props.toggleItemSelected}
|
||||||
@ -685,24 +1013,26 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
const sortIcon = <span className={`sf3-font ${sortOrder == 'asc' ? 'sf3-font-down rotate-180 d-inline-block' : 'sf3-font-down'}`}></span>;
|
const sortIcon = <span className={`sf3-font ${sortOrder == 'asc' ? 'sf3-font-down rotate-180 d-inline-block' : 'sf3-font-down'}`}></span>;
|
||||||
return mode == LIST_MODE ? (
|
return mode == LIST_MODE ? (
|
||||||
<table className="table-hover">
|
<div className="table-container">
|
||||||
<thead>
|
<table className="table-hover">
|
||||||
<tr>
|
<thead>
|
||||||
{showDownloadIcon &&
|
<tr>
|
||||||
<th width="3%" className="text-center">
|
{showDownloadIcon &&
|
||||||
<input type="checkbox" checked={isAllItemsSelected} onChange={this.props.toggleAllSelected} />
|
<th width="3%" className="text-center">
|
||||||
</th>
|
<input type="checkbox" checked={isAllItemsSelected} onChange={this.props.toggleAllSelected} />
|
||||||
}
|
</th>
|
||||||
<th width="5%"></th>
|
}
|
||||||
<th width={showDownloadIcon ? '50%' : '53%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortBy == 'name' && sortIcon}</a></th>
|
<th width="5%"></th>
|
||||||
<th width="8%"></th>
|
<th width={showDownloadIcon ? '50%' : '53%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortBy == 'name' && sortIcon}</a></th>
|
||||||
<th width="14%"><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBy == 'size' && sortIcon}</a></th>
|
<th width="8%"></th>
|
||||||
<th width="13%"><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortBy == 'time' && sortIcon}</a></th>
|
<th width="14%"><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBy == 'size' && sortIcon}</a></th>
|
||||||
<th width="7%"></th>
|
<th width="13%"><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortBy == 'time' && sortIcon}</a></th>
|
||||||
</tr>
|
<th width="7%"></th>
|
||||||
</thead>
|
</tr>
|
||||||
{tbody}
|
</thead>
|
||||||
</table>
|
{tbody}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="grid-view">
|
<ul className="grid-view">
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
@ -710,6 +1040,7 @@ class Content extends React.Component {
|
|||||||
key={index}
|
key={index}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
item={item}
|
item={item}
|
||||||
|
visitFolder={this.props.visitFolder}
|
||||||
zipDownloadFolder={this.props.zipDownloadFolder}
|
zipDownloadFolder={this.props.zipDownloadFolder}
|
||||||
showImagePopup={this.props.showImagePopup}
|
showImagePopup={this.props.showImagePopup}
|
||||||
/>;
|
/>;
|
||||||
@ -733,6 +1064,7 @@ Content.propTypes = {
|
|||||||
toggleItemSelected: PropTypes.func,
|
toggleItemSelected: PropTypes.func,
|
||||||
zipDownloadFolder: PropTypes.func,
|
zipDownloadFolder: PropTypes.func,
|
||||||
showImagePopup: PropTypes.func,
|
showImagePopup: PropTypes.func,
|
||||||
|
visitFolder: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
class Item extends React.Component {
|
class Item extends React.Component {
|
||||||
@ -776,6 +1108,13 @@ class Item extends React.Component {
|
|||||||
this.props.toggleItemSelected(this.props.item, e.target.checked);
|
this.props.toggleItemSelected(this.props.item, e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onFolderItemClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { item } = this.props;
|
||||||
|
const { folder_path } = item;
|
||||||
|
this.props.visitFolder(folder_path);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { item, isDesktop, mode } = this.props;
|
const { item, isDesktop, mode } = this.props;
|
||||||
const { isIconShown } = this.state;
|
const { isIconShown } = this.state;
|
||||||
@ -797,14 +1136,14 @@ class Item extends React.Component {
|
|||||||
}
|
}
|
||||||
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt="" width="24" /></td>
|
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt="" width="24" /></td>
|
||||||
<td>
|
<td>
|
||||||
<a href={`?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`}>{item.folder_name}</a>
|
<a href={`?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`} onClick={this.onFolderItemClick}>{item.folder_name}</a>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td title={dayjs(item.last_modified).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.last_modified).fromNow()}</td>
|
<td title={dayjs(item.last_modified).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.last_modified).fromNow()}</td>
|
||||||
<td>
|
<td>
|
||||||
{showDownloadIcon &&
|
{showDownloadIcon &&
|
||||||
<a role="button" className={`action-icon sf2-icon-download${isIconShown ? '' : ' invisible'}`} href="#" onClick={this.zipDownloadFolder} title={gettext('Download')} aria-label={gettext('Download')}>
|
<a role="button" className={`op-icon sf3-font sf3-font-download1${isIconShown ? '' : ' invisible'}`} href="#" onClick={this.zipDownloadFolder} title={gettext('Download')} aria-label={gettext('Download')}>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
@ -813,7 +1152,7 @@ class Item extends React.Component {
|
|||||||
<tr>
|
<tr>
|
||||||
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt="" width="24" /></td>
|
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt="" width="24" /></td>
|
||||||
<td>
|
<td>
|
||||||
<a href={`?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`}>{item.folder_name}</a>
|
<a href={`?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`} onClick={this.onFolderItemClick}>{item.folder_name}</a>
|
||||||
<br />
|
<br />
|
||||||
<span className="item-meta-info">{dayjs(item.last_modified).fromNow()}</span>
|
<span className="item-meta-info">{dayjs(item.last_modified).fromNow()}</span>
|
||||||
</td>
|
</td>
|
||||||
@ -879,7 +1218,7 @@ class Item extends React.Component {
|
|||||||
<td title={dayjs(item.last_modified).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.last_modified).fromNow()}</td>
|
<td title={dayjs(item.last_modified).format('dddd, MMMM D, YYYY h:mm:ss A')}>{dayjs(item.last_modified).fromNow()}</td>
|
||||||
<td>
|
<td>
|
||||||
{showDownloadIcon &&
|
{showDownloadIcon &&
|
||||||
<a className={`action-icon sf2-icon-download${isIconShown ? '' : ' invisible'}`} href={`${fileURL}&dl=1`} title={gettext('Download')} aria-label={gettext('Download')}></a>
|
<a className={`op-icon sf3-font sf3-font-download1${isIconShown ? '' : ' invisible'}`} href={`${fileURL}&dl=1`} title={gettext('Download')} aria-label={gettext('Download')}></a>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -934,6 +1273,7 @@ Item.propTypes = {
|
|||||||
toggleItemSelected: PropTypes.func,
|
toggleItemSelected: PropTypes.func,
|
||||||
zipDownloadFolder: PropTypes.func,
|
zipDownloadFolder: PropTypes.func,
|
||||||
showImagePopup: PropTypes.func,
|
showImagePopup: PropTypes.func,
|
||||||
|
visitFolder: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
class GridItem extends React.Component {
|
class GridItem extends React.Component {
|
||||||
@ -968,6 +1308,13 @@ class GridItem extends React.Component {
|
|||||||
this.props.showImagePopup(item);
|
this.props.showImagePopup(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onFolderItemClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { item } = this.props;
|
||||||
|
const { folder_path } = item;
|
||||||
|
this.props.visitFolder(folder_path);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { item, mode } = this.props;
|
const { item, mode } = this.props;
|
||||||
const { isIconShown } = this.state;
|
const { isIconShown } = this.state;
|
||||||
@ -976,12 +1323,12 @@ class GridItem extends React.Component {
|
|||||||
const folderURL = `?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`;
|
const folderURL = `?p=${encodeURIComponent(item.folder_path.substr(0, item.folder_path.length - 1))}&mode=${mode}`;
|
||||||
return (
|
return (
|
||||||
<li className="grid-item" onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
<li className="grid-item" onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||||
<a href={folderURL} className="grid-file-img-link d-block">
|
<a href={folderURL} className="grid-file-img-link d-block" onClick={this.onFolderItemClick}>
|
||||||
<img src={Utils.getFolderIconUrl(false, 192)} alt="" width="80" height="80" />
|
<img src={Utils.getFolderIconUrl(false, 192)} alt="" width="80" height="80" />
|
||||||
</a>
|
</a>
|
||||||
<a href={folderURL} className="grid-file-name grid-file-name-link">{item.folder_name}</a>
|
<a href={folderURL} className="grid-file-name grid-file-name-link" onClick={this.onFolderItemClick}>{item.folder_name}</a>
|
||||||
{showDownloadIcon &&
|
{showDownloadIcon &&
|
||||||
<a role="button" className={`action-icon sf2-icon-download${isIconShown ? '' : ' invisible'}`} href="#" onClick={this.zipDownloadFolder} title={gettext('Download')} aria-label={gettext('Download')}>
|
<a role="button" className={`action-icon sf3-font sf3-font-download1${isIconShown ? '' : ' invisible'}`} href="#" onClick={this.zipDownloadFolder} title={gettext('Download')} aria-label={gettext('Download')}>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
@ -999,7 +1346,7 @@ class GridItem extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
<a href={fileURL} className="grid-file-name grid-file-name-link" onClick={this.handleFileClick}>{item.file_name}</a>
|
<a href={fileURL} className="grid-file-name grid-file-name-link" onClick={this.handleFileClick}>{item.file_name}</a>
|
||||||
{showDownloadIcon &&
|
{showDownloadIcon &&
|
||||||
<a className={`action-icon sf2-icon-download${isIconShown ? '' : ' invisible'}`} href={`${fileURL}&dl=1`} title={gettext('Download')} aria-label={gettext('Download')}>
|
<a className={`action-icon sf3-font sf3-font-download1${isIconShown ? '' : ' invisible'}`} href={`${fileURL}&dl=1`} title={gettext('Download')} aria-label={gettext('Download')}>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
@ -1013,6 +1360,7 @@ GridItem.propTypes = {
|
|||||||
item: PropTypes.object,
|
item: PropTypes.object,
|
||||||
zipDownloadFolder: PropTypes.func,
|
zipDownloadFolder: PropTypes.func,
|
||||||
showImagePopup: PropTypes.func,
|
showImagePopup: PropTypes.func,
|
||||||
|
visitFolder: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const root = createRoot(document.getElementById('wrapper'));
|
const root = createRoot(document.getElementById('wrapper'));
|
||||||
|
@ -24,16 +24,6 @@
|
|||||||
sharedBy: '{{ username|email2nickname|escapejs }}',
|
sharedBy: '{{ username|email2nickname|escapejs }}',
|
||||||
repoID: '{{repo.id}}',
|
repoID: '{{repo.id}}',
|
||||||
relativePath: '{{ path|escapejs }}',
|
relativePath: '{{ path|escapejs }}',
|
||||||
zipped: (function() {
|
|
||||||
var list = [];
|
|
||||||
{% for name, path in zipped %}
|
|
||||||
list.push({
|
|
||||||
'name': '{{ name|escapejs }}',
|
|
||||||
'path': '{{ path|escapejs }}'
|
|
||||||
});
|
|
||||||
{% endfor %}
|
|
||||||
return list;
|
|
||||||
})(),
|
|
||||||
token: '{{ token }}',
|
token: '{{ token }}',
|
||||||
mode: '{{ mode }}',
|
mode: '{{ mode }}',
|
||||||
thumbnailSize: {{ thumbnail_size }},
|
thumbnailSize: {{ thumbnail_size }},
|
||||||
|
@ -296,16 +296,6 @@ def view_shared_dir(request, fileshare):
|
|||||||
else:
|
else:
|
||||||
dir_name = os.path.basename(real_path[:-1])
|
dir_name = os.path.basename(real_path[:-1])
|
||||||
|
|
||||||
current_commit = seaserv.get_commits(repo_id, 0, 1)[0]
|
|
||||||
file_list, dir_list, dirent_more = get_repo_dirents(request, repo,
|
|
||||||
current_commit, real_path)
|
|
||||||
|
|
||||||
# generate dir navigator
|
|
||||||
if fileshare.path == '/':
|
|
||||||
zipped = gen_path_link(req_path, repo.name)
|
|
||||||
else:
|
|
||||||
zipped = gen_path_link(req_path, os.path.basename(fileshare.path[:-1]))
|
|
||||||
|
|
||||||
if req_path == '/': # When user view the root of shared dir..
|
if req_path == '/': # When user view the root of shared dir..
|
||||||
# increase shared link view_cnt,
|
# increase shared link view_cnt,
|
||||||
fileshare = FileShare.objects.get(token=token)
|
fileshare = FileShare.objects.get(token=token)
|
||||||
@ -320,20 +310,6 @@ def view_shared_dir(request, fileshare):
|
|||||||
mode = 'grid'
|
mode = 'grid'
|
||||||
thumbnail_size = THUMBNAIL_DEFAULT_SIZE if mode == 'list' else THUMBNAIL_SIZE_FOR_GRID
|
thumbnail_size = THUMBNAIL_DEFAULT_SIZE if mode == 'list' else THUMBNAIL_SIZE_FOR_GRID
|
||||||
|
|
||||||
for f in file_list:
|
|
||||||
file_type, file_ext = get_file_type_and_ext(f.obj_name)
|
|
||||||
if file_type == IMAGE:
|
|
||||||
f.is_img = True
|
|
||||||
if file_type == VIDEO:
|
|
||||||
f.is_video = True
|
|
||||||
|
|
||||||
if file_type in (IMAGE, XMIND) or \
|
|
||||||
(file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL):
|
|
||||||
if os.path.exists(os.path.join(THUMBNAIL_ROOT, str(thumbnail_size), f.obj_id)):
|
|
||||||
req_image_path = posixpath.join(req_path, f.obj_name)
|
|
||||||
src = get_share_link_thumbnail_src(token, thumbnail_size, req_image_path)
|
|
||||||
f.encoded_thumbnail_src = quote(src)
|
|
||||||
|
|
||||||
# for 'upload file'
|
# for 'upload file'
|
||||||
no_quota = True if seaserv.check_quota(repo_id) < 0 else False
|
no_quota = True if seaserv.check_quota(repo_id) < 0 else False
|
||||||
|
|
||||||
@ -355,9 +331,6 @@ def view_shared_dir(request, fileshare):
|
|||||||
'username': username,
|
'username': username,
|
||||||
'dir_name': dir_name,
|
'dir_name': dir_name,
|
||||||
'dir_path': real_path,
|
'dir_path': real_path,
|
||||||
'file_list': file_list,
|
|
||||||
'dir_list': dir_list,
|
|
||||||
'zipped': zipped,
|
|
||||||
'traffic_over_limit': False,
|
'traffic_over_limit': False,
|
||||||
'max_upload_file_size': max_upload_file_size,
|
'max_upload_file_size': max_upload_file_size,
|
||||||
'no_quota': no_quota,
|
'no_quota': no_quota,
|
||||||
|
Loading…
Reference in New Issue
Block a user