diff --git a/frontend/src/components/constants.js b/frontend/src/components/constants.js index 375033cb22..86a553837b 100644 --- a/frontend/src/components/constants.js +++ b/frontend/src/components/constants.js @@ -10,6 +10,7 @@ export const logoWidth = window.app.config.logoWidth; export const logoHeight = window.app.config.logoHeight; export const isPro = window.app.config.isPro === "True"; export const lang = window.app.config.lang; +export const fileServerRoot = window.app.config.fileServerRoot; // wiki export const slug = window.wiki ? window.wiki.config.slug : ''; diff --git a/frontend/src/components/dialog/zip-download-dialog.js b/frontend/src/components/dialog/zip-download-dialog.js new file mode 100644 index 0000000000..d64abdac6b --- /dev/null +++ b/frontend/src/components/dialog/zip-download-dialog.js @@ -0,0 +1,22 @@ +import React from 'react' +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; + +class ZipDownloadDialog extends React.Component { + + toggle = () => { + this.props.onCancelDownload(); + } + + render() { + return ( + + + +
{this.props.progress}
+
+
+ ) + } +} + +export default ZipDownloadDialog; diff --git a/frontend/src/components/dirent-operation/operation-group.js b/frontend/src/components/dirent-operation/operation-group.js new file mode 100644 index 0000000000..5b8fbb71bf --- /dev/null +++ b/frontend/src/components/dirent-operation/operation-group.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { gettext } from '../constants'; +import OperationMenu from './operation-menu'; + +class OperationGroup extends React.Component { + + constructor(props) { + super(props); + this.state = { + isItemMenuShow: false, + menuPosition: {top: 0, left: 0 }, + } + } + + componentDidMount() { + document.addEventListener('click', this.onItemMenuHide); + } + + componentWillUnmount() { + document.removeEventListener('click', this.onItemMenuHide); + } + + onDownload = (e) => { + e.nativeEvent.stopImmediatePropagation(); + this.props.onDownload(); + } + + onShare = (e) => { + //todos:: + } + + onDelete = (e) => { + e.nativeEvent.stopImmediatePropagation(); //for document event + this.props.onDelete(); + } + + onItemMenuShow = (e) => { + if (!this.state.isItemMenuShow) { + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + + let left = e.clientX - 8*16; + let top = e.clientY + 15; + let position = Object.assign({},this.state.menuPosition, {left: left, top: top}); + this.setState({ + menuPosition: position, + isItemMenuShow: true, + }); + this.props.onItemMenuShow(); + } else { + this.onItemMenuHide(); + } + } + + onItemMenuHide = () => { + this.setState({ + isItemMenuShow: false, + }); + this.props.onItemMenuHide(); + } + + onRename = () => { + //todos: + } + + onCopy = () => { + //todos + } + + render() { + return ( +
+ + { + this.state.isItemMenuShow && + + } +
+ ); + } +} + +export default OperationGroup; diff --git a/frontend/src/components/dirent-operation/operation-menu.js b/frontend/src/components/dirent-operation/operation-menu.js new file mode 100644 index 0000000000..7d3ca79cdd --- /dev/null +++ b/frontend/src/components/dirent-operation/operation-menu.js @@ -0,0 +1,142 @@ +import React from 'react'; +import { gettext } from '../constants'; + +class OperationMenu extends React.Component { + + getItemType() { + return this.props.currentItem.type; + } + + renderDirentDirMenu() { + let position = this.props.menuPosition; + let style = {position: 'fixed', left: position.left, top: position.top, display: 'block'}; + if (this.props.currentItem.permission === 'rw') { + return ( + + ) + } + + if (this.props.currentItem.permission === 'r') { + return ( + + ) + } + + } + + renderDirentFileMenu() { + let position = this.props.menuPosition; + let style = {position: 'fixed', left: position.left, top: position.top, display: 'block'}; + if (this.props.currentItem.permission === 'rw') { + return ( + + ) + } + + if (this.props.currentItem.permission === "r") { + return ( + + ) + } + + } + + render() { + let type = this.getItemType(); + let menu = null; + switch(type) { + case 'file': + menu = this.renderDirentFileMenu(); + break; + case 'dir': + menu = this.renderDirentDirMenu(); + break; + default: + break; + } + return menu; + } +} + +export default OperationMenu; diff --git a/frontend/src/components/draft-list-view/draft-list-item.js b/frontend/src/components/draft-list-view/draft-list-item.js index 3ff4fb1f31..c23f0e2463 100644 --- a/frontend/src/components/draft-list-view/draft-list-item.js +++ b/frontend/src/components/draft-list-view/draft-list-item.js @@ -62,11 +62,11 @@ class DraftListItem extends React.Component { localTime = moment(localTime).fromNow(); return ( - - {fileName} - {draft.owner} - {localTime} - + + {fileName} + {draft.owner} + {localTime} + -
  • - - ); - } else if (permission) { - trashUrl = siteRoot + 'dir/recycle/' + repoID + '/?dir_path=' + encodeURIComponent(initialFilePath); - return ( - - ); +class PathToolbar extends React.Component { + + isMarkdownFile(filePath) { + let lastIndex = filePath.lastIndexOf('/'); + let name = filePath.slice(lastIndex + 1); + return name.indexOf('.md') > -1 ? true : false; + } + + render() { + let isFile = this.isMarkdownFile(this.props.filePath); + let index = this.props.filePath.lastIndexOf('/'); + let name = this.props.filePath.slice(index + 1); + let trashUrl = siteRoot + 'repo/recycle/' + repoID + '/?referer=' + encodeURIComponent(location.href); + let historyUrl = siteRoot + 'repo/history/' + repoID + '/?referer=' + encodeURIComponent(location.href); + if ( (name === slug || name === '') && !isFile && permission) { + return ( + + ); + } else if ( !isFile && permission) { + return ( + + ); + } else if (permission) { + historyUrl = siteRoot + 'repo/file_revisions/' + repoID + '/?p=' + encodePath(this.props.filePath) + '&referer=' + encodeURIComponent(location.href); + return ( + + ) + } + return ''; } - return ''; - } +PathToolbar.propTypes = propTypes; + export default PathToolbar; diff --git a/frontend/src/components/tree-dir-view/tree-dir-list.js b/frontend/src/components/tree-dir-view/tree-dir-list.js index 00a31051f2..72c237f897 100644 --- a/frontend/src/components/tree-dir-view/tree-dir-list.js +++ b/frontend/src/components/tree-dir-view/tree-dir-list.js @@ -1,41 +1,93 @@ import React, { Component } from 'react'; import { serviceUrl } from '../constants'; +import OperationGroup from '../dirent-operation/operation-group'; + class TreeDirList extends React.Component { constructor(props) { super(props); this.state = { - isMourseEnter: false, - highlight: '', - } + highlight: false, + isOperationShow: false, + }; } onMouseEnter = () => { - this.setState({ - highlight: 'tr-highlight' - }); + if (!this.props.isItemFreezed) { + this.setState({ + highlight: true, + isOperationShow: true, + }); + } + } + + onMouseOver = () => { + if (!this.props.isItemFreezed) { + this.setState({ + highlight: true, + isOperationShow: true, + }); + } } onMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + highlight: false, + isOperationShow: false + }); + } + } + + onItemMenuShow = () => { + this.props.onItemMenuShow(); + } + + onItemMenuHide = () => { this.setState({ - highlight: '', + isOperationShow: false, + highlight: '' }); + this.props.onItemMenuHide(); } onMainNodeClick = () => { this.props.onMainNodeClick(this.props.node); } + onDownload = () => { + this.props.onDownload(this.props.node); + } + + onDelete = () => { + this.props.onDelete(this.props.node); + } + render() { let node = this.props.node; return ( - - + + - {node.name} - {node.size} - {node.last_update_time} + {node.name} + { + this.props.needOperationGroup && + + { + this.state.isOperationShow && + + } + + } + {node.size} + {node.last_update_time} ) } diff --git a/frontend/src/components/tree-dir-view/tree-dir-view.js b/frontend/src/components/tree-dir-view/tree-dir-view.js index b25a031aa6..62262f8e78 100644 --- a/frontend/src/components/tree-dir-view/tree-dir-view.js +++ b/frontend/src/components/tree-dir-view/tree-dir-view.js @@ -1,9 +1,81 @@ -import React, { Component } from "react"; +import React from "react"; +import { gettext, repoID } from '../constants'; +import editorUtilities from '../../utils/editor-utilties'; +import URLDecorator from '../../utils/url-decorator'; +import ZipDownloadDialog from '../dialog/zip-download-dialog'; import TreeDirList from './tree-dir-list' import "../../css/common.css"; -const gettext = window.gettext; class TreeDirView extends React.Component { + + constructor(props) { + super(props); + this.state = { + isProgressDialogShow: false, + progress: '0%', + isItemFreezed: false + }; + this.zip_token = null; + this.interval = null; + } + + onDownload = (item) => { + if (item.isDir()) { + this.setState({isProgressDialogShow: true, progress: '0%'}); + editorUtilities.zipDownload(item.parent_path, item.name).then(res => { + this.zip_token = res.data['zip_token']; + this.addDownloadAnimation(); + this.interval = setInterval(this.addDownloadAnimation, 1000); + }); + } else { + let url = URLDecorator.getUrl({type:'download_file_url', repoID: repoID, filePath: item.path}); + location.href = url; + } + } + + addDownloadAnimation = () => { + let _this = this; + let token = this.zip_token; + editorUtilities.queryZipProgress(token).then(res => { + let data = res.data; + let progress = data.total === 0 ? '100%' : (data.zipped / data.total * 100).toFixed(0) + '%'; + this.setState({progress: progress}); + + if (data['total'] === data['zipped']) { + this.setState({ + progress: '100%' + }); + clearInterval(this.interval); + location.href = URLDecorator.getUrl({type: 'download_dir_zip_url', token: token}); + setTimeout(function() { + _this.setState({isProgressDialogShow: false}); + }, 500); + } + + }); + } + + onCancelDownload = () => { + let zip_token = this.zip_token; + editorUtilities.cancelZipTask(zip_token).then(res => { + this.setState({ + isProgressDialogShow: false, + }); + }) + } + + onItemMenuShow = () => { + this.setState({ + isItemFreezed: true, + }) + } + + onItemMenuHide = () => { + this.setState({ + isItemFreezed: false, + }); + } + render() { let node = this.props.node; let children = node.hasChildren() ? node.children : null; @@ -12,21 +84,46 @@ class TreeDirView extends React.Component {
    - - - - - - + { + this.props.needOperationGroup ? + + + + + + + + : + + + + + + + } {children && children.map((node, index) => { return ( - + ) })}
    {gettext('Name')}{gettext('Size')}{gettext('Last Update')}
    {gettext('Name')}{gettext('Size')}{gettext('Last Update')}
    {gettext('Name')}{gettext('Size')}{gettext('Last Update')}
    + { + this.state.isProgressDialogShow && + + }
    ) } diff --git a/frontend/src/components/tree-view/node.js b/frontend/src/components/tree-view/node.js index ebf1289217..8871445989 100644 --- a/frontend/src/components/tree-view/node.js +++ b/frontend/src/components/tree-view/node.js @@ -1,13 +1,15 @@ class Node { static deserializefromJson(object) { - const {name, type, size, last_update_time, isExpanded = true, children = []} = object; + const {name, type, size, last_update_time, permission, parent_path, isExpanded = true, children = []} = object; const node = new Node({ name, type, size, last_update_time, + permission, + parent_path, isExpanded, children: children.map(item => Node.deserializefromJson(item)), }); @@ -15,11 +17,13 @@ class Node { return node; } - constructor({name, type, size, last_update_time, isExpanded, children}) { + constructor({name, type, size, last_update_time, permission, parent_path, isExpanded, children}) { this.name = name; this.type = type; this.size = size; this.last_update_time = last_update_time; + this.permission = permission; + this.parent_path = parent_path; this.isExpanded = isExpanded !== undefined ? isExpanded : true; this.children = children ? children : []; this.parent = null; @@ -31,6 +35,8 @@ class Node { type: this.type, size: this.size, last_update_time: this.last_update_time, + permission: this.permission, + parent_path: this.parent_path, isExpanded: this.isExpanded }); n.children = this.children.map(child => { @@ -101,6 +107,8 @@ class Node { type: this.type, size: this.size, last_update_time: this.last_update_time, + permission: this.permission, + parent_path: this.parent_path, isExpanded: this.isExpanded, children: children } diff --git a/frontend/src/components/tree-view/tree.js b/frontend/src/components/tree-view/tree.js index 4a870621e3..62d22f61c9 100644 --- a/frontend/src/components/tree-view/tree.js +++ b/frontend/src/components/tree-view/tree.js @@ -186,6 +186,8 @@ class Tree { type: model.type, size: bytesToSize(model.size), last_update_time: moment.unix(model.last_update_time).fromNow(), + permission: model.permission, + parent_path: model.parent_path, isExpanded: false }); if (model.children instanceof Array) { @@ -214,6 +216,8 @@ class Tree { type: nodeObj.type, size: bytesToSize(nodeObj.size), last_update_time: moment.unix(nodeObj.last_update_time).fromNow(), + permission: nodeObj.permission, + parent_path: nodeObj.parent_path, isExpanded: false }); node.parent_path = nodeObj.parent_path; @@ -240,6 +244,8 @@ class Tree { type: node.type, size: bytesToSize(node.size), last_update_time: moment.unix(node.last_update_time).fromNow(), + permission: node.permission, + parent_path: node.parent_path, isExpanded: false }); if (node.children instanceof Array) { diff --git a/frontend/src/components/utils.js b/frontend/src/components/utils.js index a9b6b58aaf..b5f8d81626 100644 --- a/frontend/src/components/utils.js +++ b/frontend/src/components/utils.js @@ -18,3 +18,12 @@ export function bytesToSize(bytes) { if (i === 0) return bytes + ' ' + sizes[i]; return (bytes / (1000 ** i)).toFixed(1) + ' ' + sizes[i]; } + +export function encodePath(path) { + let path_arr = path.split('/'); + let path_arr_ = []; + for (let i = 0, len = path_arr.length; i < len; i++) { + path_arr_.push(encodeURIComponent(path_arr[i])); + } + return path_arr_.join('/'); +} \ No newline at end of file diff --git a/frontend/src/css/common.css b/frontend/src/css/common.css index 9d39abbe07..7868570705 100644 --- a/frontend/src/css/common.css +++ b/frontend/src/css/common.css @@ -99,17 +99,12 @@ } .table-container table .icon { - position: relative; + text-align: center; } .table-container table .icon img { - position: absolute; - display: block; - width: 24px; - height: 24px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + width: 1.5rem; + height: 1.5rem; } /* specific handler */ .table-container table .menu-toggle { @@ -131,6 +126,12 @@ .dropdown-item { cursor: pointer; } + +.dropdown-item.menu-inner-divider { + margin: 0.25rem 0; + border-bottom: 1px solid #ddd; +} + /* end dropdown-menu style */ /* begin tip */ @@ -172,3 +173,39 @@ text-decoration: underline; } /* end more component */ + +/* begin operation menu */ +.operation { + display: flex; +} + +.operation .operation-group { + list-style: none; +} + +.operation-group .operation-group-item { + display: inline-block; + color: #f89a68; + margin-right: 0.5rem; +} + +.operation-group-item i { + font-style: normal; + font-size: 1.25rem; + line-height: 1; + cursor: pointer; + vertical-align: middle; +} +.operation-group-item i:hover { + text-decoration: underline; +} + +.operation-group-item .sf-dropdown-toggle { + font-size: 0.85rem; + color: #888; +} + +.operation-group-item .sf-dropdown-toggle:hover { + text-decoration: none; +} +/* end operaton menu */ diff --git a/frontend/src/globals.js b/frontend/src/globals.js deleted file mode 100644 index 17caff3c15..0000000000 --- a/frontend/src/globals.js +++ /dev/null @@ -1,15 +0,0 @@ -export const gettext = window.gettext; -export const siteRoot = window.app.config.siteRoot; -export const lang = window.app.config.lang; - -export const getUrl = (options) => { - switch (options.name) { - case 'user_profile': return siteRoot + 'profile/' + options.username + '/'; - case 'common_lib': return siteRoot + '#common/lib/' + options.repoID + options.path; - case 'view_lib_file': return `${siteRoot}lib/${options.repoID}/file${options.filePath}`; - case 'download_historic_file': return `${siteRoot}repo/${options.repoID}/${options.objID}/download/?p=${options.filePath}`; - case 'view_historic_file': return `${siteRoot}repo/${options.repoID}/history/files/?obj_id=${options.objID}&commit_id=${options.commitID}&p=${options.filePath}`; - case 'diff_historic_file': return `${siteRoot}repo/text_diff/${options.repoID}/?commit=${options.commitID}&p=${options.filePath}`; - - } -} diff --git a/frontend/src/pages/repo-wiki-mode/main-panel.js b/frontend/src/pages/repo-wiki-mode/main-panel.js index fe23f32fe8..76f3f50c25 100644 --- a/frontend/src/pages/repo-wiki-mode/main-panel.js +++ b/frontend/src/pages/repo-wiki-mode/main-panel.js @@ -10,8 +10,9 @@ class MainPanel extends Component { constructor(props) { super(props); this.state = { - isWikiMode: true - } + isWikiMode: true, + needOperationGroup: true, + }; } onMenuClick = () => { @@ -90,7 +91,7 @@ class MainPanel extends Component { {slug} {pathElem} - +
    { this.props.isViewFileState && @@ -106,6 +107,9 @@ class MainPanel extends Component { } diff --git a/frontend/src/pages/wiki/main-panel.js b/frontend/src/pages/wiki/main-panel.js index e65c30bc19..248cb5bbcc 100644 --- a/frontend/src/pages/wiki/main-panel.js +++ b/frontend/src/pages/wiki/main-panel.js @@ -6,6 +6,13 @@ import TreeDirView from '../../components/tree-dir-view/tree-dir-view'; class MainPanel extends Component { + constructor(props) { + super(props); + this.state = { + needOperationGroupo: false + }; + } + onMenuClick = () => { this.props.onMenuClick(); } @@ -82,7 +89,10 @@ class MainPanel extends Component { { !this.props.isViewFileState && } diff --git a/frontend/src/repo-wiki-mode.js b/frontend/src/repo-wiki-mode.js index b8e6dfbc9f..ac64fbf6b3 100644 --- a/frontend/src/repo-wiki-mode.js +++ b/frontend/src/repo-wiki-mode.js @@ -352,13 +352,12 @@ class Wiki extends Component { onDeleteNode = (node) => { let filePath = node.path; - if (node.isMarkdown()) { - editorUtilities.deleteFile(filePath); - } else if (node.isDir()) { + if (node.isDir()) { editorUtilities.deleteDir(filePath); } else { - return false; + editorUtilities.deleteFile(filePath); } + let isCurrentFile = false; if (node.isDir()) { @@ -533,9 +532,11 @@ class Wiki extends Component { onMainNavBarClick={this.onMainNavBarClick} onMainNodeClick={this.onMainNodeClick} switchViewMode={this.switchViewMode} + onDeleteNode={this.onDeleteNode} + onRenameNode={this.onRenameNode} />
    - ) + ); } } diff --git a/frontend/src/utils/editor-utilties.js b/frontend/src/utils/editor-utilties.js index 1a416e179a..254bcb76fc 100644 --- a/frontend/src/utils/editor-utilties.js +++ b/frontend/src/utils/editor-utilties.js @@ -12,6 +12,7 @@ class EditorUtilities { isExpanded: item.type === 'dir' ? true : false, parent_path: item.parent_dir, last_update_time: item.last_update_time, + permission: item.permission, size: item.size }; }); @@ -28,6 +29,7 @@ class EditorUtilities { isExpanded: item.type === 'dir' ? true : false, parent_path: item.parent_dir, last_update_time: item.mtime, + permission: item.permission, size: item.size }; }); @@ -104,6 +106,19 @@ class EditorUtilities { publishDraft(id) { return seafileAPI.publishDraft(id); } + + zipDownload(parent_dir, dirents) { + return seafileAPI.zipDownload(repoID, parent_dir, dirents); + } + + queryZipProgress(zip_token) { + return seafileAPI.queryZipProgress(zip_token); + } + + cancelZipTask(zip_token) { + return seafileAPI.cancelZipTask(zip_token) + } + } const editorUtilities = new EditorUtilities(); diff --git a/frontend/src/utils/url-decorator.js b/frontend/src/utils/url-decorator.js index 9d9cf48dd7..88492fffa5 100644 --- a/frontend/src/utils/url-decorator.js +++ b/frontend/src/utils/url-decorator.js @@ -1,32 +1,20 @@ -const siteRoot = window.app.config.siteRoot; -const repoID = window.fileHistory.pageOptions.repoID; - +import {siteRoot, historyRepoID, fileServerRoot } from '../components/constants'; +import { encodePath } from '../components/utils'; class URLDecorator { static getUrl(options) { let url = ''; let params = ''; switch (options.type) { - case 'user_profile': - url = siteRoot + 'profile/' + options.username + '/'; - break; - case 'common_lib': - url = siteRoot + '#common/lib/' + repoID + options.path; - break; - case 'view_lib_file': - url = siteRoot + 'lib/' + repoID + '/file' + options.filePath; - break; case 'download_historic_file': params = 'p=' + options.filePath; - url = siteRoot + 'repo/' + repoID + '/' + options.objID + '/download?' + params; + url = siteRoot + 'repo/' + historyRepoID + '/' + options.objID + '/download?' + params; break; - case 'view_historic_file': - params = 'obj_id=' + options.objID + '&commit_id=' + options.commitID + '&p=' + options.filePath; - url = siteRoot + 'repo/' + options.repoID + 'history/files/?' + params; + case 'download_dir_zip_url': + url = fileServerRoot + 'zip/' + options.token; break; - case 'diff_historic_file': - params = 'commit_id=' + options.commitID + '&p=' + options.filePath; - url = siteRoot + 'repo/text_diff/' + repoID + '/?' + params; + case 'download_file_url': + url = siteRoot + 'lib/' + options.repoID + "/file" + encodePath(options.filePath) + "?dl=1"; break; default: url = ''; diff --git a/frontend/src/wiki.js b/frontend/src/wiki.js index 536d31df47..669b7c0faf 100644 --- a/frontend/src/wiki.js +++ b/frontend/src/wiki.js @@ -314,14 +314,13 @@ class Wiki extends Component { onDeleteNode = (node) => { let filePath = node.path; - if (node.isMarkdown()) { - editorUtilities.deleteFile(filePath); - } else if (node.isDir()) { + if (node.isDir()) { editorUtilities.deleteDir(filePath); } else { - return false; + editorUtilities.deleteFile(filePath); } + let isCurrentFile = false; if (node.isDir()) { isCurrentFile = this.isModifyContainsCurrentFile(node); @@ -494,6 +493,8 @@ class Wiki extends Component { onSearchedClick={this.onSearchedClick} onMainNavBarClick={this.onMainNavBarClick} onMainNodeClick={this.onMainNodeClick} + onDeleteNode={this.onDeleteNode} + onRenameNode={this.onRenameNode} /> ); diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 7e1a6e5e83..0719b0ccd7 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -65,6 +65,9 @@ .sf2-icon-edit:before { content:"\e018"; } .sf2-icon-history:before { content:"\e014"; } .sf2-icon-trash:before { content:"\e016"; } +.sf2-icon-download:before { content:"\e008"; } +.sf2-icon-delete:before { content:"\e006"; } +.sf2-icon-caret-down:before { content:"\e01a"; } /* common class and element style*/ a { color:#eb8205; } diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index c5d9c19010..43da0c53af 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -30,7 +30,8 @@ siteTitle: '{{ site_title }}', siteRoot: '{{ SITE_ROOT }}', isPro: '{{ is_pro }}', - lang: '{{ LANGUAGE_CODE }}' + lang: '{{ LANGUAGE_CODE }}', + fileServerRoot: '{{ FILE_SERVER_ROOT }}' } };