From f3e0284751b272748081ca293d59b36a52e5e3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E6=B0=B4=E4=BA=BA=E5=AE=B6?= Date: Thu, 25 Oct 2018 13:36:06 +0800 Subject: [PATCH] Implement wiki mode menu function (#2461) --- frontend/package-lock.json | 6 +- frontend/package.json | 2 +- .../components/dialog/copy-dirent-dialog.js | 114 +++++++ .../components/dialog/create-file-dialog.js | 16 +- .../components/dialog/move-dirent-dialog.js | 113 +++++++ .../components/dialog/zip-download-dialog.js | 4 +- .../dirent-detail/detail-list-view.js | 64 ++++ .../dirent-detail/dirent-details.js | 88 ++++++ .../dirent-list-view/dirent-list-item.js | 297 ++++++++++++++++-- .../dirent-list-view/dirent-list-view.js | 131 +++++--- .../dirent-list-view/dirent-menu-item.js | 36 +++ .../dirent-list-view/dirent-menu.js | 127 ++++++++ .../dirent-list-view/dirent-rename.js | 71 +++++ .../dirent-operation/operation-group.js | 116 ------- .../dirent-operation/operation-menu.js | 173 ---------- .../file-chooser/dirent-list-item.js | 100 ++++++ .../file-chooser/dirent-list-view.js | 61 ++++ .../components/file-chooser/file-chooser.js | 125 ++++++++ .../components/file-chooser/repo-list-item.js | 64 ++++ .../components/file-chooser/repo-list-view.js | 45 +++ frontend/src/components/main-side-nav.js | 2 +- .../components/tree-dir-view/tree-dir-list.js | 66 +--- .../components/tree-dir-view/tree-dir-view.js | 113 +------ frontend/src/css/dirent-detail.css | 84 +++++ frontend/src/css/file-chooser.css | 70 +++++ frontend/src/css/layout.css | 7 + frontend/src/css/toolbar.css | 2 +- frontend/src/models/repo.js | 2 +- frontend/src/pages/drafts/draft-content.js | 2 +- .../src/pages/repo-wiki-mode/main-panel.js | 113 ++++--- frontend/src/pages/wiki/main-panel.js | 9 +- frontend/src/repo-wiki-mode.js | 67 +++- frontend/src/utils/constants.js | 6 + frontend/src/utils/url-decorator.js | 10 + frontend/src/utils/utils.js | 9 +- media/css/seahub_react.css | 83 ++++- seahub/api2/endpoints/repos.py | 195 +++++++++++- seahub/templates/base_for_react.html | 9 +- seahub/urls.py | 3 +- 39 files changed, 1995 insertions(+), 610 deletions(-) create mode 100644 frontend/src/components/dialog/copy-dirent-dialog.js create mode 100644 frontend/src/components/dialog/move-dirent-dialog.js create mode 100644 frontend/src/components/dirent-detail/detail-list-view.js create mode 100644 frontend/src/components/dirent-detail/dirent-details.js create mode 100644 frontend/src/components/dirent-list-view/dirent-menu-item.js create mode 100644 frontend/src/components/dirent-list-view/dirent-menu.js create mode 100644 frontend/src/components/dirent-list-view/dirent-rename.js delete mode 100644 frontend/src/components/dirent-operation/operation-group.js delete mode 100644 frontend/src/components/dirent-operation/operation-menu.js create mode 100644 frontend/src/components/file-chooser/dirent-list-item.js create mode 100644 frontend/src/components/file-chooser/dirent-list-view.js create mode 100644 frontend/src/components/file-chooser/file-chooser.js create mode 100644 frontend/src/components/file-chooser/repo-list-item.js create mode 100644 frontend/src/components/file-chooser/repo-list-view.js create mode 100644 frontend/src/css/dirent-detail.css create mode 100644 frontend/src/css/file-chooser.css diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6b8d1f9303..a241e7a1ce 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10225,9 +10225,9 @@ } }, "seafile-js": { - "version": "0.2.28", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.28.tgz", - "integrity": "sha512-+QA16BLpNVrvYIHfvrU/0D0x90bb1gCzW31U2BeFdL1CT3vgdLXaVc+j0XAtDahBYTyKtu7YKGPR6UO0ZdlXdw==", + "version": "0.2.29", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.29.tgz", + "integrity": "sha512-fsWHcTWZk2iV/Ah3lRmnO5vyB0AFoxVyV3Z7FQ7k8aAYQtWcf07hxbFIhFpGU6cpmaq2mtnJQBUNU9zQdKtGRA==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2" diff --git a/frontend/package.json b/frontend/package.json index 6402212fa6..58f052cada 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,7 @@ "react-dom": "^16.5.2", "react-moment": "^0.7.9", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.28", + "seafile-js": "^0.2.29", "seafile-ui": "^0.1.10", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/components/dialog/copy-dirent-dialog.js b/frontend/src/components/dialog/copy-dirent-dialog.js new file mode 100644 index 0000000000..953b84bcff --- /dev/null +++ b/frontend/src/components/dialog/copy-dirent-dialog.js @@ -0,0 +1,114 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert } from 'reactstrap'; +import { gettext, repoID } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import FileChooser from '../file-chooser/file-chooser'; + +const propTypes = { + direntPath: PropTypes.string, + dirent: PropTypes.object.isRequired, + onItemCopy: PropTypes.func.isRequired, + onCancelCopy: PropTypes.func.isRequired, +}; + +// need dirent file Path; +class CopyDirent extends React.Component { + + constructor(props) { + super(props); + this.state = { + repo: null, + filePath: '', + errMessage: '', + }; + } + + shouldComponentUpdate(nextProps, nextState) { + if (this.state.errMessage === nextState.errMessage) { + return false; + } + return true; + } + + handleSubmit = () => { + let { direntPath } = this.props; + let { repo, filePath } = this.state; + let message = 'Invalid destination path'; + + if (!repo || (repo.repo_id === repoID && filePath === '')) { + this.setState({errMessage: message}); + return; + } + + if (filePath && direntPath === filePath) { + this.setState({errMessage: message}); + return; + } + + + if (filePath && direntPath.length > filePath.length && direntPath.indexOf(filePath) > -1) { + this.setState({errMessage: message}); + return; + } + + if ( filePath && filePath.length > direntPath.length && filePath.indexOf(direntPath) > -1) { + message = gettext('Can not copy directory %(src)s to its subdirectory %(des)s') + message = message.replace('%(src)s', direntPath); + message = message.replace('%(des)s', filePath); + this.setState({errMessage: message}); + return; + } + + if (filePath === '') { + filePath = '/'; + } + this.props.onItemCopy(repo, direntPath, filePath); + this.toggle(); + } + + toggle = () => { + this.props.onCancelCopy(); + } + + onDirentItemClick = (repo, filePath) => { + this.setState({ + repo: repo, + filePath: filePath, + errMessage: '', + }); + } + + onRepoItemClick = (repo) => { + this.setState({ + repo: repo, + filePath: '', + errMessage: '' + }); + } + + render() { + let title = gettext("Copy {placeholder} to:"); + title = title.replace('{placeholder}', '' + Utils.HTMLescape(this.props.dirent.name) + ''); + return ( + +
+ + + {this.state.errMessage && {this.state.errMessage}} + + + + + +
+ ); + } +} + +CopyDirent.propTypes = propTypes; + +export default CopyDirent; diff --git a/frontend/src/components/dialog/create-file-dialog.js b/frontend/src/components/dialog/create-file-dialog.js index 225539dc1b..4841faaab8 100644 --- a/frontend/src/components/dialog/create-file-dialog.js +++ b/frontend/src/components/dialog/create-file-dialog.js @@ -40,7 +40,7 @@ class CreateFile extends React.Component { } handleCheck = () => { - let pos = this.state.childName.lastIndexOf("."); + let pos = this.state.childName.lastIndexOf('.'); if (this.state.isDraft) { // from draft to not draft @@ -54,12 +54,12 @@ class CreateFile extends React.Component { this.setState({ childName: fileName + fileType, isDraft: !this.state.isDraft - }) + }); } else { // don't change file name this.setState({ isDraft: !this.state.isDraft - }) + }); } } @@ -74,16 +74,16 @@ class CreateFile extends React.Component { this.setState({ childName: fileName + '(draft)' + fileType, isDraft: !this.state.isDraft - }) + }); } else if (pos === 0 ) { this.setState({ childName: '(draft)' + this.state.childname, isDraft: !this.state.isdraft - }) + }); } else { - this.setState({ + this.setState({ isDraft: !this.state.isdraft - }) + }); } } } @@ -121,7 +121,7 @@ class CreateFile extends React.Component { diff --git a/frontend/src/components/dialog/move-dirent-dialog.js b/frontend/src/components/dialog/move-dirent-dialog.js new file mode 100644 index 0000000000..e2882f6b0f --- /dev/null +++ b/frontend/src/components/dialog/move-dirent-dialog.js @@ -0,0 +1,113 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert } from 'reactstrap'; +import { gettext, repoID } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import FileChooser from '../file-chooser/file-chooser'; + +const propTypes = { + direntPath: PropTypes.string, + dirent: PropTypes.object.isRequired, + onItemMove: PropTypes.func.isRequired, + onCancelMove: PropTypes.func.isRequired, +}; + +// need dirent file Path; +class MoveDirent extends React.Component { + + constructor(props) { + super(props); + this.state = { + repo: null, + filePath: '', + errMessage: '', + }; + } + + shouldComponentUpdate(nextProps, nextState) { + if (this.state.errMessage === nextState.errMessage) { + return false; + } + return true; + } + + handleSubmit = () => { + let { direntPath } = this.props; + let { repo, filePath } = this.state; + let message = gettext('Invalid destination path'); + + if (!repo || (repo.repo_id === repoID && filePath === '')) { + this.setState({errMessage: message}); + return; + } + + if (filePath && direntPath === filePath) { + this.setState({errMessage: message}); + return; + } + + + if (filePath && direntPath.length > filePath.length && direntPath.indexOf(filePath) > -1) { + this.setState({errMessage: message}); + return; + } + + if ( filePath && filePath.length > direntPath.length && filePath.indexOf(direntPath) > -1) { + message = gettext('Can not move directory %(src)s to its subdirectory %(des)s') + message = message.replace('%(src)s', direntPath); + message = message.replace('%(des)s', filePath); + this.setState({errMessage: message}); + return; + } + if (filePath === '') { + filePath = '/'; + } + this.props.onItemMove(repo, direntPath, filePath); + this.toggle(); + } + + toggle = () => { + this.props.onCancelMove(); + } + + onDirentItemClick = (repo, filePath) => { + this.setState({ + repo: repo, + filePath: filePath, + errMessage: '', + }); + } + + onRepoItemClick = (repo) => { + this.setState({ + repo: repo, + filePath: '', + errMessage: '' + }); + } + + render() { + let title = gettext("Move {placeholder} to:"); + title = title.replace('{placeholder}', '' + Utils.HTMLescape(this.props.dirent.name) + ''); + return ( + +
+ + + {this.state.errMessage && {this.state.errMessage}} + + + + + +
+ ); + } +} + +MoveDirent.propTypes = propTypes; + +export default MoveDirent; diff --git a/frontend/src/components/dialog/zip-download-dialog.js b/frontend/src/components/dialog/zip-download-dialog.js index a64d1d1878..ff8ccf26e6 100644 --- a/frontend/src/components/dialog/zip-download-dialog.js +++ b/frontend/src/components/dialog/zip-download-dialog.js @@ -4,7 +4,7 @@ import { Modal, ModalHeader, ModalBody } from 'reactstrap'; const propTypes = { onCancelDownload: PropTypes.func.isRequired, - progress: PropTypes.string.isRequired, + progress: PropTypes.number.isRequired, }; class ZipDownloadDialog extends React.Component { @@ -18,7 +18,7 @@ class ZipDownloadDialog extends React.Component { -
{this.props.progress}
+
{this.props.progress + '%'}
); diff --git a/frontend/src/components/dirent-detail/detail-list-view.js b/frontend/src/components/dirent-detail/detail-list-view.js new file mode 100644 index 0000000000..39273cd72e --- /dev/null +++ b/frontend/src/components/dirent-detail/detail-list-view.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; + +const propTypes = { + repo: PropTypes.object.isRequired, + direntType: PropTypes.string.isRequired, + direntDetail: PropTypes.object.isRequired, + direntPath: PropTypes.string.isRequired, +}; + +class DetailListView extends React.Component { + + getDirentPostion = () => { + let { repo, direntPath } = this.props; + let position = repo.repo_name + '/'; + if (direntPath !== '/') { + let index = direntPath.lastIndexOf('/'); + let path = direntPath.slice(0, index); + position = position + path; + } + return position; + } + + render() { + let { direntType, direntDetail } = this.props; + let position = this.getDirentPostion(); + if (direntType === 'dir') { + return ( +
+ + + + + + + + + +
{gettext('Folder')}{direntDetail.dir_count}
{gettext('File')}{direntDetail.file_count}
{gettext('Size')}{Utils.bytesToSize(direntDetail.size)}
{gettext('Position')}{position}
{gettext('Last Update')}{moment(direntDetail.mtime).format('YYYY-MM-DD')}
+
+ ); + } else { + return ( +
+ + + + + + + +
{gettext('Size')}{direntDetail.size}
{gettext('Position')}{position}
{gettext('Last Update')}{moment(direntDetail.mtime).format('YYYY-MM-DD')}
+
+ ); + } + } +} + +DetailListView.propTypes = propTypes; + +export default DetailListView; diff --git a/frontend/src/components/dirent-detail/dirent-details.js b/frontend/src/components/dirent-detail/dirent-details.js new file mode 100644 index 0000000000..977b9e4008 --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details.js @@ -0,0 +1,88 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../utils/seafile-api'; +import { serviceUrl, repoID } from '../../utils/constants'; +import DetailListView from './detail-list-view'; +import Repo from '../../models/repo'; +import '../../css/dirent-detail.css'; + +const propTypes = { + dirent: PropTypes.object.isRequired, + direntPath: PropTypes.string.isRequired, + onItemDetailsClose: PropTypes.func.isRequired, +}; + +class DirentDetail extends React.Component { + + constructor(props) { + super(props); + this.state = { + direntType: '', + direntDetail: '', + repo: null, + }; + } + + componentDidMount() { + let { dirent, direntPath } = this.props; + seafileAPI.getRepoInfo(repoID).then(res => { + let repo = new Repo(res.data); + this.setState({repo: repo}); + this.updateDetailView(dirent, direntPath); + }); + } + + componentWillReceiveProps(nextProps) { + this.updateDetailView(nextProps.dirent, nextProps.direntPath); + } + + updateDetailView = (dirent, direntPath) => { + if (dirent.type === 'file') { + seafileAPI.getFileInfo(repoID, direntPath).then(res => { + this.setState({ + direntType: 'file', + direntDetail: res.data, + }); + }); + } else { + seafileAPI.getDirInfo(repoID, direntPath).then(res => { + this.setState({ + direntType: 'dir', + direntDetail: res.data + }); + }); + } + } + + render() { + let { dirent } = this.props; + return ( +
+
+
+
+ icon + {dirent.name} +
+
+
+
+ icon +
+ {this.state.direntDetail && + + } +
+
+ ); + } +} + +DirentDetail.propTypes = propTypes; + +export default DirentDetail; diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index 7bc7b75f5b..a5abd93aa2 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -1,17 +1,27 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { serviceUrl, gettext } from '../../utils/constants'; -import OperationGroup from '../dirent-operation/operation-group'; +import { serviceUrl, gettext, repoID } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import URLDecorator from '../../utils/url-decorator'; +import Toast from '../toast'; +import DirentMenu from './dirent-menu'; +import DirentRename from './dirent-rename'; const propTypes = { + filePath: PropTypes.string.isRequired, isItemFreezed: PropTypes.bool.isRequired, dirent: PropTypes.object.isRequired, onItemClick: PropTypes.func.isRequired, - onItemMenuShow: PropTypes.func.isRequired, - onItemMenuHide: PropTypes.func.isRequired, + onFreezedItem: PropTypes.func.isRequired, + onUnfreezedItem: PropTypes.func.isRequired, + onRenameMenuItemClick: PropTypes.func.isRequired, onItemDelete: PropTypes.func.isRequired, - onItemStarred: PropTypes.func.isRequired, + onItemRename: PropTypes.func.isRequired, onItemDownload: PropTypes.func.isRequired, + onDirentItemMove: PropTypes.func.isRequired, + onDirentItemCopy: PropTypes.func.isRequired, + onItemDetails: PropTypes.func.isRequired, + updateViewList: PropTypes.func.isRequired, }; class DirentListItem extends React.Component { @@ -20,10 +30,20 @@ class DirentListItem extends React.Component { super(props); this.state = { isOperationShow: false, - highlight: false + highlight: false, + isItemMenuShow: false, + menuPosition: {top: 0, left: 0 }, }; } + componentDidMount() { + document.addEventListener('click', this.onItemMenuHide); + } + + componentWillUnmount() { + document.removeEventListener('click', this.onItemMenuHide); + } + //UI Interactive onMouseEnter = () => { if (!this.props.isItemFreezed) { @@ -52,38 +72,234 @@ class DirentListItem extends React.Component { } } - onItemMenuShow = () => { - this.props.onItemMenuShow(); + onItemMenuToggle = (e) => { + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + + if (!this.state.isItemMenuShow) { + this.onItemMenuShow(e); + } else { + this.onItemMenuHide(); + } + } + + onItemMenuShow = (e) => { + 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.onFreezedItem(); } onItemMenuHide = () => { this.setState({ isOperationShow: false, - highlight: '' + highlight: '', + isItemMenuShow: false, + isRenameing: false, }); - this.props.onItemMenuHide(); + this.props.onUnfreezedItem(); } //buiness handler onItemSelected = () => { //todos; } - + onItemStarred = () => { - this.props.onItemStarred(this.props.dirent); + let dirent = this.props.dirent; + let filePath = this.getDirentPath(dirent); + if (dirent.starred) { + seafileAPI.unStarFile(repoID, filePath).then(() => { + this.props.updateViewList(this.props.filePath); + }); + } else { + seafileAPI.starFile(repoID, filePath).then(() => { + this.props.updateViewList(this.props.filePath); + }); + } } - + onItemClick = () => { - this.props.onItemClick(this.props.dirent); + let direntPath = this.getDirentPath(this.props.dirent); + this.props.onItemClick(direntPath); } - - onItemDownload = () => { - this.props.onItemDownload(this.props.dirent); + onItemDownload = (e) => { + e.nativeEvent.stopImmediatePropagation(); + let direntPath = this.getDirentPath(this.props.dirent); + this.props.onItemDownload(this.props.dirent, direntPath); } - onItemDelete = () => { - this.props.onItemDelete(this.props.dirent); + onItemDelete = (e) => { + e.nativeEvent.stopImmediatePropagation(); //for document event + let direntPath = this.getDirentPath(this.props.dirent); + this.props.onItemDelete(direntPath); + } + + onItemMenuItemClick = (operation) => { + switch(operation) { + case 'Rename': + this.onRenameMenuItemClick(); + break; + case 'Move': + this.onDirentItemMove(); + break; + case 'Copy': + this.onDirentItemCopy(); + break; + case 'Permission': + this.onPermissionItem(); + break; + case 'Details': + this.onDetailsItem(); + break; + case 'Unlock': + this.onUnlockItem(); + break; + case 'Lock': + this.onLockItem(); + break; + case 'New Draft': + this.onNewDraft(); + break; + case 'Comment': + this.onComnentItem(); + break; + case 'History': + this.onHistory(); + break; + case 'Access Log': + this.onAccessLog(); + break; + case 'Open via Client': + this.onOpenViaClient(); + break; + default: + break; + } + } + + onRenameMenuItemClick = () => { + this.setState({ + isOperationShow: false, + isItemMenuShow: false, + isRenameing: true, + }); + this.props.onRenameMenuItemClick(this.props.dirent); + } + + onRenameConfirm = (newName) => { + if (newName === this.props.dirent.name) { + this.onRenameCancel(); + return false; + } + + if (!newName) { + let errMessage = 'It is required.'; + Toast.error(gettext(errMessage)); + return false; + } + + if (newName.indexOf('/') > -1) { + let errMessage = 'Name should not include "/".'; + Toast.error(gettext(errMessage)); + return false; + } + + let direntPath = this.getDirentPath(this.props.dirent); + this.props.onItemRename(direntPath, newName); + this.onRenameCancel(); + } + + onRenameCancel = () => { + this.setState({ + isRenameing: false, + }); + this.props.onUnfreezedItem(); + } + + onDirentItemMove = () => { + let direntPath = this.getDirentPath(this.props.dirent); + this.props.onDirentItemMove(this.props.dirent, direntPath); + this.onItemMenuHide(); + } + + onDirentItemCopy = () => { + let direntPath = this.getDirentPath(this.props.dirent); + this.props.onDirentItemCopy(this.props.dirent, direntPath); + this.onItemMenuHide(); + } + + onPermissionItem = () => { + + } + + onDetailsItem = () => { + let direntPath = this.getDirentPath(this.props.dirent); + this.props.onItemDetails(this.props.dirent, direntPath); + this.onItemMenuHide(); + } + + onLockItem = () => { + let filePath = this.getDirentPath(this.props.dirent); + seafileAPI.lockfile(repoID, filePath).then(() => { + this.props.updateViewList(this.props.filePath); + }); + this.onItemMenuHide(); + } + + onUnlockItem = () => { + let filePath = this.getDirentPath(this.props.dirent); + seafileAPI.unlockfile(repoID, filePath).then(() => { + this.props.updateViewList(this.props.filePath); + }); + this.onItemMenuHide(); + } + + onNewDraft = () => { + let filePath = this.getDirentPath(this.props.dirent); + seafileAPI.createDraft(repoID,filePath).then(res => { + let draft_file_Path = res.data.draft_file_path; + let draftId = res.data.id; + let url = URLDecorator.getUrl({type: 'draft_view', repoID: repoID, filePath: draft_file_Path, draftId: draftId}); + let newWindow = window.open('draft'); + newWindow.location.href = url; + }).catch(() => { + Toast.error('Create draft failed.'); + }); + this.onItemMenuHide(); + } + + onComnentItem = () => { + + } + + onHistory = () => { + let filePath = this.getDirentPath(this.props.dirent); + let referer = location.href; + let url = URLDecorator.getUrl({type: 'file_revisions', repoID: repoID, filePath: filePath, referer: referer}); + location.href = url; + this.onItemMenuHide(); + } + + onAccessLog = () => { + + } + + onOpenViaClient = () => { + let filePath = this.getDirentPath(this.props.dirent); + let url = URLDecorator.getUrl({type: 'open_via_client', repoID: repoID, filePath: filePath}); + location.href = url; + this.onItemMenuHide(); + } + + getDirentPath = (dirent) => { + let path = this.props.filePath; + return path === '/' ? path + dirent.name : path + '/' + dirent.name; } render() { @@ -98,19 +314,44 @@ class DirentListItem extends React.Component { {dirent.starred !== undefined && dirent.starred && } - {gettext('file +
+ {gettext('file + {dirent.is_locked && {gettext('locked')}} +
+ + + {this.state.isRenameing ? + : + {dirent.name} + } - {dirent.name} { this.state.isOperationShow && - +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ { + this.state.isItemMenuShow && + + } +
} {dirent.size && dirent.size} diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js index 6520db99ca..1836c9b32a 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-view.js +++ b/frontend/src/components/dirent-list-view/dirent-list-view.js @@ -3,16 +3,23 @@ import PropTypes from 'prop-types'; import { gettext, repoID } from '../../utils/constants'; import URLDecorator from '../../utils/url-decorator'; import editorUtilities from '../../utils/editor-utilties'; -import { seafileAPI } from '../../utils/seafile-api'; +import Loading from '../loading'; import DirentListItem from './dirent-list-item'; import ZipDownloadDialog from '../dialog/zip-download-dialog'; +import MoveDirentDialog from '../dialog/move-dirent-dialog'; +import CopyDirentDialog from '../dialog/copy-dirent-dialog'; const propTypes = { filePath: PropTypes.string.isRequired, direntList: PropTypes.array.isRequired, onItemDelete: PropTypes.func.isRequired, + onItemRename: PropTypes.func.isRequired, onItemClick: PropTypes.func.isRequired, + onItemMove: PropTypes.func.isRequired, + onItemCopy: PropTypes.func.isRequired, + onItemDetails: PropTypes.func.isRequired, updateViewList: PropTypes.func.isRequired, + isDirentListLoading: PropTypes.bool.isRequired, }; class DirentListView extends React.Component { @@ -20,54 +27,74 @@ class DirentListView extends React.Component { constructor(props) { super(props); this.state = { + progress: 0, isItemFreezed: false, isProgressDialogShow: false, - progress: '0%', + isMoveDialogShow: false, + isCopyDialogShow: false, + currentDirent: false, + direntPath: '', }; } - onItemMenuShow = () => { + onFreezedItem = () => { this.setState({isItemFreezed: true}); } - onItemMenuHide = () => { + onUnfreezedItem = () => { this.setState({isItemFreezed: false}); } - onItemClick = (dirent) => { - let direntPath = this.getDirentPath(dirent); - this.props.onItemClick(direntPath); + onRenameMenuItemClick = () => { + this.onFreezedItem(); } - onItemDelete = (dirent) => { - let direntPath = this.getDirentPath(dirent); - this.props.onItemDelete(direntPath); + onDirentItemMove = (dirent, direntPath) => { + this.setState({ + isMoveDialogShow: true, + currentDirent: dirent, + direntPath: direntPath + }); } - onItemStarred = (dirent) => { - let filePath = this.getDirentPath(dirent); - if (dirent.starred) { - seafileAPI.unStarFile(repoID, filePath).then(() => { - this.props.updateViewList(this.props.filePath); - }); - } else { - seafileAPI.starFile(repoID, filePath).then(() => { - this.props.updateViewList(this.props.filePath); - }); - } + onDirentItemCopy = (dirent, direntPath) => { + this.setState({ + isCopyDialogShow: true, + currentDirent: dirent, + direntPath: direntPath + }); } - onItemDownload = (dirent) => { + onItemMove = (repo, direntPath, moveToDirentPath) => { + this.props.onItemMove(repo, direntPath, moveToDirentPath); + } + + onCancelMove = () => { + this.setState({isMoveDialogShow: false}); + } + + onItemCopy = (repo, direntPath, copyToDirentPath) => { + this.props.onItemCopy(repo, direntPath, copyToDirentPath); + } + + onCancelCopy = () => { + this.setState({isCopyDialogShow: false}); + } + + onItemDetails = (dirent, direntPath) => { + this.props.onItemDetails(dirent, direntPath); + } + + onItemDownload = (dirent, direntPath) => { if (dirent.type === 'dir') { - this.setState({isProgressDialogShow: true, progress: '0%'}); + this.setState({isProgressDialogShow: true, progress: 0}); editorUtilities.zipDownload(this.props.filePath, dirent.name).then(res => { this.zip_token = res.data['zip_token']; this.addDownloadAnimation(); this.interval = setInterval(this.addDownloadAnimation, 1000); }); } else { - let path = this.getDirentPath(dirent); - let url = URLDecorator.getUrl({type: 'download_file_url', repoID: repoID, filePath: path}); + let url = URLDecorator.getUrl({type: 'download_file_url', repoID: repoID, filePath: direntPath}); location.href = url; } } @@ -77,12 +104,12 @@ class DirentListView extends React.Component { 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}); + let progress = data.total === 0 ? 100 : (data.zipped / data.total * 100).toFixed(0); + this.setState({progress: parseInt(progress)}); if (data['total'] === data['zipped']) { this.setState({ - progress: '100%' + progress: 100 }); clearInterval(this.interval); location.href = URLDecorator.getUrl({type: 'download_dir_zip_url', token: token}); @@ -103,13 +130,13 @@ class DirentListView extends React.Component { }); } - getDirentPath = (dirent) => { - let path = this.props.filePath; - return path === '/' ? path + dirent.name : path + '/' + dirent.name; - } - render() { const { direntList } = this.props; + + if (this.props.isDirentListLoading) { + return (); + } + return (
@@ -131,22 +158,44 @@ class DirentListView extends React.Component { ); }) }
- { - this.state.isProgressDialogShow && - + {this.state.isProgressDialogShow && + + } + {this.state.isMoveDialogShow && + + } + {this.state.isCopyDialogShow && + }
); diff --git a/frontend/src/components/dirent-list-view/dirent-menu-item.js b/frontend/src/components/dirent-list-view/dirent-menu-item.js new file mode 100644 index 0000000000..ca49725b1a --- /dev/null +++ b/frontend/src/components/dirent-list-view/dirent-menu-item.js @@ -0,0 +1,36 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; + +const propTypes = { + item: PropTypes.string.isRequired, + onItemClick: PropTypes.func.isRequired, +}; + +class DirentMenuItem extends React.Component { + + onClick = (e) => { + e.nativeEvent.stopImmediatePropagation(); + let operation = e.target.dataset.type; + this.props.onItemClick(operation); + } + + render() { + let operationName = gettext(this.props.item); + return ( + + { + operationName !== 'Divider' ? +
  • + {operationName} +
  • : +
  • + } +
    + ); + } +} + +DirentMenuItem.propTypes = propTypes; + +export default DirentMenuItem; diff --git a/frontend/src/components/dirent-list-view/dirent-menu.js b/frontend/src/components/dirent-list-view/dirent-menu.js new file mode 100644 index 0000000000..2f80d3d71d --- /dev/null +++ b/frontend/src/components/dirent-list-view/dirent-menu.js @@ -0,0 +1,127 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../utils/seafile-api'; +import { repoID, isPro, enableFileComment, fileAuditEnabled, folderPermEnabled} from '../../utils/constants'; +import Repo from '../../models/repo'; +import DirentMenuItem from './dirent-menu-item'; + +const propTypes = { + dirent: PropTypes.object.isRequired, + menuPosition: PropTypes.object.isRequired, + onMenuItemClick: PropTypes.func.isRequired, +}; + +class DirentMenu extends React.Component { + + constructor(props) { + super(props); + this.state = { + repo: null, + menuList: [], + }; + this.is_repo_owner = false; + } + + componentDidMount() { + seafileAPI.getRepoInfo(repoID).then(res => { + let repo = new Repo(res.data); + seafileAPI.getAccountInfo().then(res => { + let user_email = res.data.email; + this.is_repo_owner = repo.owner_email === user_email; + let menuList = this.calculateMenuList(repo); + this.setState({ + repo: repo, + menuList: menuList + }); + }); + }); + } + + calculateMenuList(repoInfo) { + let dirent = this.props.dirent; + let type = dirent.type; + let permission = dirent.permission; + let can_set_folder_perm = folderPermEnabled && ((this.is_repo_owner && repoInfo.has_been_shared_out) || repoInfo.is_admin); + if (type === 'dir' && permission === 'rw') { + let menuList = []; + if (can_set_folder_perm) { + menuList = ['Rename', 'Move', 'Copy', 'Divider', 'Permission', 'Details', 'Divider', 'Open via Client']; + } else { + menuList = ['Rename', 'Move', 'Copy', 'Divider', 'Details', 'Divider', 'Open via Client']; + } + return menuList; + } + + if (type === 'dir' && permission === 'r') { + let menuList = repoInfo.encrypted ? ['Copy', 'Details'] : ['Details']; + return menuList; + } + + if (type === 'file' && permission === 'rw') { + let menuList = []; + if (!dirent.is_locked || (dirent.is_locked && dirent.locked_by_me)) { + menuList.push('Rename'); + menuList.push('Move'); + } + menuList.push('Copy'); + if (isPro) { + if (dirent.is_locked && dirent.locked_by_me) { + menuList.push('Unlock'); + } else { + menuList.push('Lock'); + } + } + menuList.push('New Draft'); + menuList.push('Divider'); + if (enableFileComment) { + menuList.push('Comment'); + } + menuList.push('History'); + if (fileAuditEnabled) { + menuList.push('Access Log'); + } + menuList.push('Details'); + menuList.push('Divider'); + menuList.push('Open via Client'); + return menuList; + } + + if (type === 'file' && permission === 'r') { + let menuList = []; + if (!repoInfo.encrypted) { + menuList.push('Copy'); + } + if (enableFileComment) { + menuList.push('Comment'); + } + menuList.push('History'); + menuList.push('Details'); + return menuList; + } + } + + onMenuItemClick = (operation) => { + this.props.onMenuItemClick(operation); + } + + render() { + let position = this.props.menuPosition; + let style = {position: 'fixed', left: position.left, top: position.top, display: 'block'}; + if (this.state.menuList.length) { + return ( +
      + {this.state.menuList.map((item, index) => { + return ( + + ); + })} +
    + ); + } + return ''; + } +} + +DirentMenu.propTypes = propTypes; + +export default DirentMenu; diff --git a/frontend/src/components/dirent-list-view/dirent-rename.js b/frontend/src/components/dirent-list-view/dirent-rename.js new file mode 100644 index 0000000000..b941edaa39 --- /dev/null +++ b/frontend/src/components/dirent-list-view/dirent-rename.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + dirent: PropTypes.object.isRequired, + onRenameConfirm: PropTypes.func.isRequired, + onRenameCancel: PropTypes.func.isRequired, +}; +class DirentRename extends React.Component { + + constructor(props) { + super(props); + this.state = { + name: props.dirent.name + }; + } + + componentDidMount() { + this.refs.renameInput.focus(); + if (this.props.dirent.type === 'file') { + var endIndex = this.props.dirent.name.lastIndexOf('.'); + this.refs.renameInput.setSelectionRange(0, endIndex, 'forward'); + } else { + this.refs.renameInput.setSelectionRange(0, -1); + } + } + + onChange = (e) => { + this.setState({name: e.target.value}); + } + + onClick = (e) => { + e.nativeEvent.stopImmediatePropagation(); + } + + onKeyPress = (e) => { + if (e.key === 'Enter') { + this.onRenameConfirm(e); + } + } + + onRenameConfirm = (e) => { + e.nativeEvent.stopImmediatePropagation(); + this.props.onRenameConfirm(this.state.name); + } + + onRenameCancel = (e) => { + e.nativeEvent.stopImmediatePropagation(); + this.props.onRenameCancel(); + } + + render() { + return ( +
    + + + +
    + ); + } +} + +DirentRename.propTypes = propTypes; + +export default DirentRename; diff --git a/frontend/src/components/dirent-operation/operation-group.js b/frontend/src/components/dirent-operation/operation-group.js deleted file mode 100644 index eee0d41477..0000000000 --- a/frontend/src/components/dirent-operation/operation-group.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { gettext } from '../../utils/constants'; -import OperationMenu from './operation-menu'; - -const propTypes = { - dirent: PropTypes.object.isRequired, - onItemMenuShow: PropTypes.func.isRequired, - onItemMenuHide: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - onDownload: PropTypes.func.isRequired, -}; - -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(); - } - - onItemMenuToggle = (e) => { - e.stopPropagation(); - e.nativeEvent.stopImmediatePropagation(); - - if (!this.state.isItemMenuShow) { - this.onItemMenuShow(e); - } else { - this.onItemMenuHide(); - } - } - - onItemMenuShow = (e) => { - 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(); - } - - onItemMenuHide = () => { - this.setState({ - isItemMenuShow: false, - }); - this.props.onItemMenuHide(); - } - - onRename = () => { - //todos: - } - - onCopy = () => { - //todos - } - - render() { - return ( -
    -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    - { - this.state.isItemMenuShow && - - } -
    - ); - } -} - -OperationGroup.propTypes = propTypes; - -export default OperationGroup; diff --git a/frontend/src/components/dirent-operation/operation-menu.js b/frontend/src/components/dirent-operation/operation-menu.js deleted file mode 100644 index 7a028deaff..0000000000 --- a/frontend/src/components/dirent-operation/operation-menu.js +++ /dev/null @@ -1,173 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { gettext, repoID } from '../../utils/constants'; -import { seafileAPI } from '../../utils/seafile-api'; -import Repo from '../../models/repo'; - -const propTypes = { - dirent: PropTypes.object.isRequired, - menuPosition: PropTypes.object.isRequired, -}; - -class OperationMenu extends React.Component { - - constructor(props) { - super(props); - this.state = { - repo: null, - is_repo_owner: false, - }; - } - - componentDidMount() { - seafileAPI.getRepoInfo(repoID).then(res => { - let repo = new Repo(res.data); - seafileAPI.getAccountInfo().then(res => { - let user_email = res.data.email; - let is_repo_owner = repo.owner_email === user_email; - this.setState({ - repo: repo, - is_repo_owner: is_repo_owner - }); - }); - }); - } - - getItemType() { - let type = this.props.dirent.is_dir ? 'dir' : 'file'; - return type; - } - - renderDirentDirMenu() { - let position = this.props.menuPosition; - let style = {position: 'fixed', left: position.left, top: position.top, display: 'block'}; - if (this.props.dirent.permission === 'rw') { - return ( -
      -
    • - {gettext('Rename')} -
    • -
    • - {gettext('Move')} -
    • -
    • - {gettext('Copy')} -
    • -
    • - -
    • - {gettext('Permission')} -
    • -
    • - {gettext('Details')} -
    • -
    • -
    • - {gettext('Open via Client')} -
    • -
    - ); - } - - if (this.props.dirent.permission === 'r') { - return ( -
      -
    • - {gettext('Copy')} -
    • -
    • - {gettext('Details')} -
    • -
    - ); - } - - } - - renderDirentFileMenu() { - let position = this.props.menuPosition; - let style = {position: 'fixed', left: position.left, top: position.top, display: 'block'}; - if (this.props.dirent.permission === 'rw') { - return ( -
      -
    • - {gettext('Rename')} -
    • -
    • - {gettext('Move')} -
    • -
    • - {gettext('Copy')} -
    • -
    • - {gettext('Lock')} -
    • -
    • - {gettext('Unlock')} -
    • -
    • - {gettext('New Draft')} -
    • -
    • -
    • - {gettext('Comment')} -
    • -
    • - {gettext('History')} -
    • -
    • - {gettext('Access Log')} -
    • -
    • - {gettext('Details')} -
    • -
    • - -
    • - {gettext('Open via Client')} -
    • -
    - ); - } - - if (this.props.dirent.permission === 'r') { - return ( -
      -
    • - {gettext('Copy')} -
    • -
    • - {gettext('Comment')} -
    • -
    • - {gettext('History')} -
    • -
    • - {gettext('Details')} -
    • -
    - ); - } - - } - - 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; - } -} - -OperationMenu.propTypes = propTypes; - -export default OperationMenu; diff --git a/frontend/src/components/file-chooser/dirent-list-item.js b/frontend/src/components/file-chooser/dirent-list-item.js new file mode 100644 index 0000000000..9baaea3203 --- /dev/null +++ b/frontend/src/components/file-chooser/dirent-list-item.js @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../utils/seafile-api'; +import Dirent from '../../models/dirent'; + +const propTypes = { + filePath: PropTypes.string, + selectedPath: PropTypes.string, + dirent: PropTypes.object.isRequired, + repo: PropTypes.object.isRequired, + onDirentItemClick: PropTypes.func.isRequired, +}; + +class DirentListItem extends React.Component { + + constructor(props) { + super(props); + + let filePath = this.props.filePath ? this.props.filePath + '/' + this.props.dirent.name : '/' + this.props.dirent.name; + + this.state = { + isShowChildren: false, + hasRequest: false, + hasChildren: true, + filePath: filePath, + direntList: [], + }; + } + + onItemClick = () => { + this.props.onDirentItemClick(this.state.filePath); + } + + onToggleClick = () => { + if (!this.state.hasRequest) { + seafileAPI.listDir(this.props.repo.repo_id, this.state.filePath).then(res => { + let direntList = []; + res.data.forEach(item => { + if (item.type === 'dir') { + let dirent = new Dirent(item); + direntList.push(dirent); + } + this.setState({ + hasRequest: true, + direntList: direntList, + }); + }); + if (res.data.length === 0 || direntList.length === 0) { + this.setState({ + hasRequest: true, + direntList: [], + hasChildren: false + }); + } + }); + } + + this.setState({isShowChildren: !this.state.isShowChildren}); + } + + renderChildren = () => { + return ( +
      + {this.state.direntList.map((dirent, index) => { + return ( + + ); + })} +
    + ); + } + + render() { + return ( +
  • + { + this.state.hasChildren && + + } + + + {this.props.dirent && this.props.dirent.name} + + {this.state.isShowChildren && this.renderChildren()} +
  • + ); + } +} + +DirentListItem.propTypes = propTypes; + +export default DirentListItem; diff --git a/frontend/src/components/file-chooser/dirent-list-view.js b/frontend/src/components/file-chooser/dirent-list-view.js new file mode 100644 index 0000000000..5a3d63059a --- /dev/null +++ b/frontend/src/components/file-chooser/dirent-list-view.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../utils/seafile-api'; +import Dirent from '../../models/dirent'; +import DirentListItem from './dirent-list-item'; + +const propTypes = { + selectedPath: PropTypes.string, + repo: PropTypes.object.isRequired, + isShowChildren: PropTypes.bool.isRequired, + onDirentItemClick: PropTypes.func.isRequired +}; + +class DirentListView extends React.Component { + + constructor(props) { + super(props); + this.state = { + direntList: [], + }; + } + + componentDidMount() { + let repo = this.props.repo; + seafileAPI.listDir(repo.repo_id, '/').then(res => { + let direntList = []; + res.data.forEach(item => { + if (item.type === 'dir') { + let dirent = new Dirent(item); + direntList.push(dirent); + } + this.setState({ + direntList: direntList, + }); + }); + }); + } + + render() { + let { direntList } = this.state; + return ( +
      + { direntList.map((dirent, index) => { + return ( + + ); + })} +
    + ); + } +} + +DirentListView.propTypes = propTypes; + +export default DirentListView; diff --git a/frontend/src/components/file-chooser/file-chooser.js b/frontend/src/components/file-chooser/file-chooser.js new file mode 100644 index 0000000000..1c8a400ef7 --- /dev/null +++ b/frontend/src/components/file-chooser/file-chooser.js @@ -0,0 +1,125 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import RepoListView from './repo-list-view'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, repoID } from '../../utils/constants'; +import Repo from '../../models/repo'; + +import '../../css/file-chooser.css'; + +const propTypes = { + onDirentItemClick: PropTypes.func, + onRepoItemClick: PropTypes.func, +}; + +class FileChooser extends React.Component { + + constructor(props) { + super(props); + this.state = { + hasRequest: false, + isOtherRepoShow: false, + repoList: [], + currentRepo: null, + selectedRepo: null, + selectedPath: '', + }; + } + + componentDidMount() { + seafileAPI.getRepoInfo(repoID).then(res => { + let repo = new Repo(res.data); + this.setState({ + currentRepo: repo, + }); + }); + } + + onOtherRepoToggle = () => { + if (!this.state.hasRequest) { + let { currentRepo } = this.state; + seafileAPI.listRepos().then(res => { + let repos = res.data.repos; + let repoList = []; + let repoIdList = []; + for(let i = 0; i < repos.length; i++) { + if (repos[i].repo_name === currentRepo.repo_name || repos[i].permission !== 'rw') { + continue; + } + if (repoIdList.indexOf(repos[i].repo_id) > -1) { + continue; + } + repoList.push(repos[i]); + repoIdList.push(repos[i].repo_id); + } + this.setState({ + repoList: repoList, + isOtherRepoShow: !this.state.isOtherRepoShow, + }); + }); + } else { + this.setState({isOtherRepoShow: !this.state.isOtherRepoShow}); + } + } + + onDirentItemClick = (repo, filePath) => { + this.props.onDirentItemClick(repo, filePath); + this.setState({ + selectedRepo: repo, + selectedPath: filePath + }); + } + + onRepoItemClick = (repo) => { + this.props.onRepoItemClick(repo); + this.setState({ + selectedRepo: repo, + selectedPath: '', + }); + } + + render() { + return ( +
    +
    +
    + + {gettext('Current Library')} +
    + { + this.state.currentRepo && + + } +
    +
    +
    + + {gettext('Other Libraries')} +
    + { + this.state.isOtherRepoShow && + + } +
    +
    + ); + } +} + +FileChooser.propTypes = propTypes; + +export default FileChooser; diff --git a/frontend/src/components/file-chooser/repo-list-item.js b/frontend/src/components/file-chooser/repo-list-item.js new file mode 100644 index 0000000000..34e70527b0 --- /dev/null +++ b/frontend/src/components/file-chooser/repo-list-item.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DirentListView from './dirent-list-view'; + +const propTypes = { + selectedPath: PropTypes.string, + selectedRepo: PropTypes.object, + repo: PropTypes.object.isRequired, + initToShowChildren: PropTypes.bool.isRequired, + onDirentItemClick: PropTypes.func.isRequired, + onRepoItemClick: PropTypes.func.isRequired, +}; + +class RepoListItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + isShowChildren: this.props.initToShowChildren, + }; + } + + onToggleClick = () => { + this.setState({isShowChildren: !this.state.isShowChildren}); + } + + onDirentItemClick = (filePath) => { + let repo = this.props.repo; + this.props.onDirentItemClick(repo, filePath); + } + + onRepoItemClick = () => { + this.props.onRepoItemClick(this.props.repo); + } + + render() { + let repoActive = false; + let isCurrentRepo = this.props.selectedRepo && (this.props.repo.repo_id === this.props.selectedRepo.repo_id); + if (isCurrentRepo && !this.props.selectedPath) { + repoActive = true; + } + return ( +
  • + + + + {this.props.repo.repo_name} + + { + + } +
  • + ); + } +} + +RepoListItem.propTypes = propTypes; + +export default RepoListItem; diff --git a/frontend/src/components/file-chooser/repo-list-view.js b/frontend/src/components/file-chooser/repo-list-view.js new file mode 100644 index 0000000000..1e70d654aa --- /dev/null +++ b/frontend/src/components/file-chooser/repo-list-view.js @@ -0,0 +1,45 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import RepoListItem from './repo-list-item'; + +const propTypes = { + repo: PropTypes.object, + repoList: PropTypes.array, + selectedRepo: PropTypes.object, + initToShowChildren: PropTypes.bool.isRequired, + selectedPath: PropTypes.string, + onDirentItemClick: PropTypes.func.isRequired, + onRepoItemClick: PropTypes.func.isRequired, +}; + +class RepoListView extends React.Component { + + render() { + let { repo, repoList } = this.props; + if (repo) { + repoList = []; + repoList.push(repo); + } + return ( +
      + {repoList.length > 0 && repoList.map((repoItem, index) => { + return ( + + ); + })} +
    + ); + } +} + +RepoListView.propTypes = propTypes; + +export default RepoListView; diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js index 06a2ab2461..8a2d2eba2d 100644 --- a/frontend/src/components/main-side-nav.js +++ b/frontend/src/components/main-side-nav.js @@ -152,7 +152,7 @@ class MainSideNav extends React.Component {

    Tools

    • - this.tabItemClick('favorites')}> + this.tabItemClick('starred')}> {gettext('Favorites')} 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 989e0ac0b7..eb986370d6 100644 --- a/frontend/src/components/tree-dir-view/tree-dir-list.js +++ b/frontend/src/components/tree-dir-view/tree-dir-list.js @@ -1,17 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { serviceUrl } from '../../utils/constants'; -import OperationGroup from '../dirent-operation/operation-group'; const propTypes = { - isItemFreezed: PropTypes.bool.isRequired, node: PropTypes.object.isRequired, - needOperationGroup: PropTypes.bool.isRequired, - onItemMenuHide: PropTypes.func.isRequired, - onItemMenuShow: PropTypes.func.isRequired, onMainNodeClick: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - onDownload: PropTypes.func.isRequired, }; class TreeDirList extends React.Component { @@ -25,79 +18,26 @@ class TreeDirList extends React.Component { } onMouseEnter = () => { - if (!this.props.isItemFreezed) { - this.setState({ - highlight: true, - isOperationShow: true, - }); - } + this.setState({highlight: 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({ - isOperationShow: false, - highlight: '' - }); - this.props.onItemMenuHide(); + this.setState({highlight: false}); } 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 ( - + icon {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 e5dc96b8b1..c0dca364b7 100644 --- a/frontend/src/components/tree-dir-view/tree-dir-view.js +++ b/frontend/src/components/tree-dir-view/tree-dir-view.js @@ -1,88 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { gettext, repoID } from '../../utils/constants'; -import editorUtilities from '../../utils/editor-utilties'; -import URLDecorator from '../../utils/url-decorator'; -import ZipDownloadDialog from '../dialog/zip-download-dialog'; +import { gettext } from '../../utils/constants'; import TreeDirList from './tree-dir-list'; const propTypes = { - needOperationGroup: PropTypes.bool.isRequired, node: PropTypes.object.isRequired, onMainNodeClick: PropTypes.func.isRequired, - onDeleteItem: PropTypes.func.isRequired, }; 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; @@ -91,45 +18,21 @@ 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/css/dirent-detail.css b/frontend/src/css/dirent-detail.css new file mode 100644 index 0000000000..411af948a9 --- /dev/null +++ b/frontend/src/css/dirent-detail.css @@ -0,0 +1,84 @@ +.detail-container { + flex: 1; + display: flex; + flex-direction: column; + border-left: 1px solid #e8e8e8; +} + +.detail-header { + position: relative; + display: flex; + align-items: center; + justify-content: center; + line-height: 2.5rem; + background-color: #f9f9f9; + border-bottom: 1px solid #e8e8e8; +} + +.detail-header .detail-control { + position: absolute; + font-size: 16px; + color: #b9b9b9; + left: 0.5rem; +} + +.detail-header .detail-control:hover { + color: #888; +} + +.detail-header .detail-title img{ + width: 1.5rem; + height: 1.5rem; +} + +.detail-header .detail-title .name { + font-size: 1rem; + color: #322; +} + +.detail-body { + flex: 1; + display: flex; + flex-direction: column; +} + +.dirent-info .img { + height: 10rem; + padding: 0.5rem 0; + display: flex; + justify-content: center; + align-items: center; +} + +.dirent-info .img img { + width: 6rem; + height: 6rem; +} + +.dirent-table-container { + padding: 10px 20px; + display: flex; +} + +.dirent-table-container table { + flex: 1; +} + +.dirent-table-container th, +.dirent-table-container td { + padding: 5px 3px; + border: none; +} + +.dirent-table-container th { + font-size: 13px; + text-align: left; + font-weight: normal; + color: #9c9c9c; +} + +.dirent-table-container td { + font-size: 14px; + color: #333; + word-break: break-all; +} \ No newline at end of file diff --git a/frontend/src/css/file-chooser.css b/frontend/src/css/file-chooser.css new file mode 100644 index 0000000000..b8b67af58f --- /dev/null +++ b/frontend/src/css/file-chooser.css @@ -0,0 +1,70 @@ +.file-chooser-container { + padding: 0.5rem; + height: 20rem; + border: 1px solid #eee; + overflow: auto; + font-size: 1rem; +} + +.item-toggle{ + position: absolute; + height: 1.5rem; + width: 1.5rem; + left: 0; + top: 0; + line-height: 1.5rem !important; + text-align: center; + cursor: pointer; + color: #c0c0c0; +} + +.file-chooser-container .list-view { + margin-top: 0.25rem; +} + +.list-view-header { + position: relative; + padding-left: 1.5rem; +} + +.list-view-header .name { + color: #eb8205; +} + +.list-view-content { + margin: 0; + padding: 0; + list-style: none; +} + +.file-chooser-item { + position: relative; + padding-left: 1.5rem; +} + +.file-chooser-item .item-info { + display: inline-block; + height: 1.5rem; + cursor: pointer; +} + +.file-chooser-item .item-info:hover { + background: #e7f4f9; + border-radius: 2px; + box-shadow: inset 0 0 1px #999; +} + +.file-chooser-item .item-active { + background: #beebff !important; + border-radius: 2px; + box-shadow: inset 0 0 1px #999; +} + +.file-chooser-item .item-info .icon { + color: #b0b0b0; + width: 1.5rem; + height: 1.5rem; +} + + + diff --git a/frontend/src/css/layout.css b/frontend/src/css/layout.css index 8ae639105f..82db90f6f5 100644 --- a/frontend/src/css/layout.css +++ b/frontend/src/css/layout.css @@ -58,6 +58,7 @@ display: flex; flex-direction: column; flex: 1; + min-height: 0; } .side-panel-center, @@ -108,6 +109,12 @@ border-radius: 2px; } +.cur-view-detail { + display: flex; + width: 20rem; +} + +/* for reach/router */ [role=group] { display: flex; flex: 1; diff --git a/frontend/src/css/toolbar.css b/frontend/src/css/toolbar.css index c392f4573d..5ca4e909ba 100644 --- a/frontend/src/css/toolbar.css +++ b/frontend/src/css/toolbar.css @@ -19,7 +19,7 @@ /* file-operation toolbar eg: edit, upload, new, share*/ .operation-item { - padding: 0 0.25rem; + padding: 0 0.5rem; margin-right: 0.25rem; height: 30px; min-width: 55px; diff --git a/frontend/src/models/repo.js b/frontend/src/models/repo.js index 8506608efe..5a8cd58d19 100644 --- a/frontend/src/models/repo.js +++ b/frontend/src/models/repo.js @@ -3,7 +3,7 @@ import { Utils } from '../utils/utils'; class Repo { constructor(object) { this.repo_id = object.repo_id; - this.repo_name = object.name; + this.repo_name = object.repo_name; this.permission = object.permission; this.size = Utils.bytesToSize(object.size); this.file_count = object.file_count; diff --git a/frontend/src/pages/drafts/draft-content.js b/frontend/src/pages/drafts/draft-content.js index 9771d4ce7b..5fb9c726b9 100644 --- a/frontend/src/pages/drafts/draft-content.js +++ b/frontend/src/pages/drafts/draft-content.js @@ -64,7 +64,7 @@ class DraftContent extends React.Component { let draft = this.state.currentDraft; editUtilties.createDraftReview(draft.id).then(res => { - const w = window.open() + const w = window.open(); w.location = siteRoot + 'drafts/review/' + res.data.id; }).catch((error) => { if (error.response.status == '409') { diff --git a/frontend/src/pages/repo-wiki-mode/main-panel.js b/frontend/src/pages/repo-wiki-mode/main-panel.js index 48b2dd7208..37106c99ac 100644 --- a/frontend/src/pages/repo-wiki-mode/main-panel.js +++ b/frontend/src/pages/repo-wiki-mode/main-panel.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { gettext, repoID, serviceUrl, slug, siteRoot } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; @@ -7,6 +7,7 @@ import CommonToolbar from '../../components/toolbar/common-toolbar'; import PathToolbar from '../../components/toolbar/path-toolbar'; import MarkdownViewer from '../../components/markdown-viewer'; import DirentListView from '../../components/dirent-list-view/dirent-list-view'; +import DirentDetail from '../../components/dirent-detail/dirent-details'; import CreateFolder from '../../components/dialog/create-folder-dialog'; import CreateFile from '../../components/dialog/create-file-dialog'; @@ -25,6 +26,9 @@ const propTypes = { onLinkClick: PropTypes.func.isRequired, onMainItemClick: PropTypes.func.isRequired, onMainItemDelete: PropTypes.func.isRequired, + onMainItemRename: PropTypes.func.isRequired, + onMainItemMove: PropTypes.func.isRequired, + onMainItemCopy: PropTypes.func.isRequired, onMainAddFile: PropTypes.func.isRequired, onMainAddFolder: PropTypes.func.isRequired, switchViewMode: PropTypes.func.isRequired, @@ -42,6 +46,10 @@ class MainPanel extends Component { showFileDialog: false, showFolderDialog: false, createFileType: '', + isDirentDetailShow: false, + currentDirent: null, + currentFilePath: '', + isDirentListLoading: true, }; } @@ -62,7 +70,8 @@ class MainPanel extends Component { } updateViewList = (filePath) => { - seafileAPI.listDir(repoID, filePath, 48).then(res => { + this.setState({isDirentListLoading: true}); + seafileAPI.listDir(repoID, filePath).then(res => { let direntList = []; res.data.forEach(item => { let dirent = new Dirent(item); @@ -70,6 +79,7 @@ class MainPanel extends Component { }); this.setState({ direntList: direntList, + isDirentListLoading: false, }); }); } @@ -168,6 +178,18 @@ class MainPanel extends Component { this.props.onMainAddFolder(dirPath); } + onItemDetails = (dirent, direntPath) => { + this.setState({ + currentDirent: dirent, + currentFilePath: direntPath, + isDirentDetailShow: true, + }); + } + + onItemDetailsClose = () => { + this.setState({isDirentDetailShow: false}); + } + render() { let filePathList = this.props.filePath.split('/'); let nodePath = ''; @@ -202,13 +224,18 @@ class MainPanel extends Component {
      - { + { this.props.permission === 'rw' && } - - - + { + !this.props.isViewFileState && + + + + + + }
      { this.state.uploadMenuShow && @@ -222,7 +249,7 @@ class MainPanel extends Component {
      • {gettext('New Folder')}
      • {gettext('New File')}
      • -
      • +
      • {gettext('New Markdown File')}
      } @@ -235,37 +262,53 @@ class MainPanel extends Component {
      -
      -
      -
      - {gettext('Libraries')} - / - {this.props.filePath === '/' ? - {slug} : - {slug} - } - {pathElem} +
      +
      +
      +
      + {gettext('Libraries')} + / + {this.props.filePath === '/' ? + {slug} : + {slug} + } + {pathElem} +
      + +
      +
      + { this.props.isViewFileState ? + : + + }
      -
      -
      - { this.props.isViewFileState ? - : - + - } -
      +
      + }
      {this.state.showFileDialog && } { !this.props.isViewFileState && - - + }
      diff --git a/frontend/src/repo-wiki-mode.js b/frontend/src/repo-wiki-mode.js index a6c7a20ec1..f71d7ddec2 100644 --- a/frontend/src/repo-wiki-mode.js +++ b/frontend/src/repo-wiki-mode.js @@ -1,14 +1,15 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; +import moment from 'moment'; +import cookie from 'react-cookies'; +import { gettext, repoID, serviceUrl, initialFilePath } from './utils/constants'; +import { seafileAPI } from './utils/seafile-api'; +import editorUtilities from './utils/editor-utilties'; import SidePanel from './pages/repo-wiki-mode/side-panel'; import MainPanel from './pages/repo-wiki-mode/main-panel'; -import moment from 'moment'; -import { repoID, serviceUrl, initialFilePath } from './utils/constants'; -import editorUtilities from './utils/editor-utilties'; -import { seafileAPI } from './utils/seafile-api'; import Node from './components/tree-view/node'; import Tree from './components/tree-view/tree'; -import cookie from 'react-cookies'; +import Toast from './components/toast'; import 'seafile-ui'; import './assets/css/fa-solid.css'; import './assets/css/fa-regular.css'; @@ -182,9 +183,58 @@ class Wiki extends Component { let node = this.state.tree_data.getNodeByPath(direntPath); this.onDeleteNode(node); } + + onMainItemRename = (direntPath, newName) => { + let node = this.state.tree_data.getNodeByPath(direntPath); + this.onRenameNode(node, newName); + } - onMainItemRename = () => { - //todos: + onMainItemMove = (repo, direntPath, moveToDirentPath) => { + let index = direntPath.lastIndexOf('/'); + let dirPath = direntPath.slice(0, index + 1); + let dirName = direntPath.slice(index + 1); + seafileAPI.moveDir(repoID, repo.repo_id, moveToDirentPath, dirPath, dirName).then(() => { + let tree = this.state.tree_data.clone(); + let moveNode = tree.getNodeByPath(direntPath); + let moveNodeParent = tree.findNodeParentFromTree(moveNode); + if (repoID === repo.repo_id) { + let moveToNode = tree.getNodeByPath(moveToDirentPath); + tree.addNodeToParent(moveNode, moveToNode); + } + tree.removeNodeFromParent(moveNode, moveNodeParent); + + this.exitViewFileState(tree, moveNodeParent); + let message = gettext('Successfully moved %(name)s.'); + message = message.replace('%(name)s', dirName); + Toast.success(message); + }).catch(() => { + let message = gettext('Failed to move %(name)s'); + message = message.replace('%(name)s', dirName); + Toast.error(message); + }); + } + + onMainItemCopy = (repo, direntPath, copyToDirentPath) => { + let index = direntPath.lastIndexOf('/'); + let dirPath = direntPath.slice(0, index + 1); + let dirName = direntPath.slice(index + 1); + seafileAPI.copyDir(repoID, repo.repo_id, copyToDirentPath, dirPath, dirName).then(() => { + if (repoID === repo.repo_id) { + let tree = this.state.tree_data.clone(); + let copyNode = tree.getNodeByPath(direntPath); + let copyToNode = tree.getNodeByPath(copyToDirentPath); + tree.addNodeToParent(copyNode, copyToNode); + this.exitViewFileState(tree, this.state.changedNode); + } + + let message = gettext('Successfully copied %(name)s.'); + message = message.replace('%(name)s', dirName); + Toast.success(message); + }).catch(() => { + let message = gettext('Failed to copy %(name)s'); + message = message.replace('%(name)s', dirName); + Toast.error(message); + }); } onNodeClick = (e, node) => { @@ -537,6 +587,9 @@ class Wiki extends Component { onMainNavBarClick={this.onMainNavBarClick} onMainItemClick={this.onMainItemClick} onMainItemDelete={this.onMainItemDelete} + onMainItemRename={this.onMainItemRename} + onMainItemMove={this.onMainItemMove} + onMainItemCopy={this.onMainItemCopy} onMainAddFile={this.onAddFileNode} onMainAddFolder={this.onAddFolderNode} /> diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 9636706125..a5fdfacaa1 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -13,6 +13,12 @@ export const isPro = window.app.config.isPro === 'True'; export const lang = window.app.config.lang; export const fileServerRoot = window.app.config.fileServerRoot; +//pageOptions +export const canGenerateShareLink = window.app.pageOptions.canGenerateShareLink === 'True'; +export const canGenerateUploadLink = window.app.pageOptions.canGenerateUploadLink === 'True'; +export const fileAuditEnabled = window.app.pageOptions.fileAuditEnabled ? true : false; +export const enableFileComment = window.app.pageOptions.enableFileComment ? true : false; +export const folderPermEnabled = window.app.pageOptions.folderPermEnabled === 'True'; // wiki export const slug = window.wiki ? window.wiki.config.slug : ''; export const repoID = window.wiki ? window.wiki.config.repoId : ''; diff --git a/frontend/src/utils/url-decorator.js b/frontend/src/utils/url-decorator.js index 6a5f88ba3f..7d20bf856c 100644 --- a/frontend/src/utils/url-decorator.js +++ b/frontend/src/utils/url-decorator.js @@ -16,6 +16,16 @@ class URLDecorator { case 'download_file_url': url = siteRoot + 'lib/' + options.repoID + '/file' + Utils.encodePath(options.filePath) + '?dl=1'; break; + case 'file_revisions': + params = 'p=' + Utils.encodePath(options.filePath) + '&referer=' + Utils.encodePath(options.referer); + url = siteRoot + 'repo/file_revisions/' + options.repoID + '/?' + params; + break; + case 'open_via_client': + url = 'seafile://openfile?repo_id=' + options.repoID + '&path=' + Utils.encodePath(options.filePath); + break; + case 'draft_view': + url = siteRoot + 'lib/' + options.repoID + '/file' + options.filePath + '?mode=edit&draft_id=' + options.draftId; + break; default: url = ''; break; diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index be0f7d71d4..27ad0869bc 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -154,5 +154,12 @@ export const Utils = { path_arr_.push(encodeURIComponent(path_arr[i])); } return path_arr_.join('/'); - } + }, + + HTMLescape: function(html) { + return document.createElement('div') + .appendChild(document.createTextNode(html)) + .parentNode + .innerHTML; + }, }; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 6ba3b89e91..4904ed45ce 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -74,6 +74,8 @@ .sf2-icon-delete:before { content:"\e006"; } .sf2-icon-caret-down:before { content:"\e01a"; } .sf2-icon-two-columns:before { content:"\e036"; } +.sf2-icon-confirm:before {content:"\e01e"} +.sf2-icon-cancel:before {content:"\e01f"} /* common class and element style*/ a { color:#eb8205; } @@ -109,26 +111,36 @@ ul,ol,li { } .text-left { -text-align: left; + text-align: left; } .text-right { -text-align: right; + text-align: right; } .a-simulate { -color: #eb8205 !important; -text-decoration: none; -font-weight: normal; -cursor: pointer; + color: #eb8205 !important; + text-decoration: none; + font-weight: normal; + cursor: pointer; } .a-simulate:hover { -text-decoration: underline; + text-decoration: underline; } .flex-right { -justify-content: flex-end; + justify-content: flex-end; +} + +.flex-direction-row { + flex-direction: row; +} + +.sf-font { + color: #eb8205 !important; + text-decoration: none; + font-weight: normal; } /* UI Widget */ @@ -783,9 +795,47 @@ a.op-icon:focus { .table-container table .select, .table-container table .star, .table-container table .icon { + position: relative; text-align: center; } +.table-container table .star { + cursor: pointer; +} + +.table-container table .rename-container input { + box-sizing: content-box; + padding: 2px 3px; + width: 16.25rem; + height: 22px; + line-height: 19px; + border-radius: 2px; + word-wrap: break-word; + vertical-align: middle; + border: 1px solid #ccc; +} + +.table-container table .rename-container input:focus { + background-color: #fff; + border-color: #1991eb; + outline: 0; + box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25); +} + +.table-container table .rename-container button { + margin-left: 0.25rem; + padding: 5px 6px; + font-size: 1rem; + line-height: 1; + color: #666; + min-width: 0; +} + +.table-container table .rename-container .confirm { + color: green; +} + + .table-container table .star .empty { color: #d0d0d0; } @@ -794,6 +844,19 @@ a.op-icon:focus { width: 1.5rem; height: 1.5rem; } + +.table-container table .dir-icon { + position: relative; +} + +.table-container table .dir-icon .locked { + position: absolute; + width: 1rem; + height: 1rem; + top: 50%; + left: 50%; +} + .table-container table .menu-toggle { text-align: center; cursor: pointer; @@ -811,10 +874,6 @@ a.op-icon:focus { .dropdown-item { cursor: pointer; } -.dropdown-item.menu-inner-divider { - margin: 0.25rem 0; - border-bottom: 1px solid #ddd; -} /* end dropdown-menu style */ diff --git a/seahub/api2/endpoints/repos.py b/seahub/api2/endpoints/repos.py index 5085edf631..d3b4bfd395 100644 --- a/seahub/api2/endpoints/repos.py +++ b/seahub/api2/endpoints/repos.py @@ -15,12 +15,205 @@ from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email from seahub.utils.repo import get_repo_owner, is_repo_admin, \ repo_has_been_shared_out -from seahub.views import check_folder_permission +from seahub.views import check_folder_permission, list_inner_pub_repos +from seahub.share.models import ExtraSharePermission +from seahub.utils import is_org_context +from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seaserv import seafile_api logger = logging.getLogger(__name__) + +class ReposView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle,) + + def get(self, request): + """ Return repos user can access. + + Permission checking: + 1. all authenticated user can perform this action. + """ + + filter_by = { + 'mine': False, + 'shared': False, + 'group': False, + 'public': False, + } + + request_type_list = request.GET.getlist('type', "") + if not request_type_list: + # set all to True, no filter applied + filter_by = filter_by.fromkeys(filter_by.iterkeys(), True) + + for request_type in request_type_list: + request_type = request_type.strip() + filter_by[request_type] = True + + email = request.user.username + + # Use dict to reduce memcache fetch cost in large for-loop. + contact_email_dict = {} + nickname_dict = {} + + org_id = None + if is_org_context(request): + org_id = request.user.org.org_id + + repo_info_list = [] + if filter_by['mine']: + if org_id: + owned_repos = seafile_api.get_org_owned_repo_list(org_id, + email, ret_corrupted=True) + else: + owned_repos = seafile_api.get_owned_repo_list(email, + ret_corrupted=True) + + # Reduce memcache fetch ops. + modifiers_set = set([x.last_modifier for x in owned_repos]) + for e in modifiers_set: + if e not in contact_email_dict: + contact_email_dict[e] = email2contact_email(e) + if e not in nickname_dict: + nickname_dict[e] = email2nickname(e) + + owned_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify)) + for r in owned_repos: + + # do not return virtual repos + if r.is_virtual: + continue + + repo_info = { + "type": "mine", + "repo_id": r.id, + "repo_name": r.name, + "owner_email": email, + "owner_name": email2nickname(email), + "owner_contact_email": email2contact_email(email), + "last_modified": timestamp_to_isoformat_timestr(r.last_modify), + "modifier_email": r.last_modifier, + "modifier_name": nickname_dict.get(r.last_modifier, ''), + "modifier_contact_email": contact_email_dict.get(r.last_modifier, ''), + "size": r.size, + "encrypted": r.encrypted, + "permission": 'rw', # Always have read-write permission to owned repo + } + repo_info_list.append(repo_info) + + if filter_by['shared']: + + if org_id: + shared_repos = seafile_api.get_org_share_in_repo_list(org_id, + email, -1, -1) + else: + shared_repos = seafile_api.get_share_in_repo_list( + email, -1, -1) + + repos_with_admin_share_to = ExtraSharePermission.objects.\ + get_repos_with_admin_permission(email) + + # Reduce memcache fetch ops. + owners_set = set([x.user for x in shared_repos]) + modifiers_set = set([x.last_modifier for x in shared_repos]) + for e in owners_set | modifiers_set: + if e not in contact_email_dict: + contact_email_dict[e] = email2contact_email(e) + if e not in nickname_dict: + nickname_dict[e] = email2nickname(e) + + shared_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify)) + for r in shared_repos: + + repo_info = { + "type": "shared", + "repo_id": r.repo_id, + "repo_name": r.repo_name, + "last_modified": timestamp_to_isoformat_timestr(r.last_modify), + "modifier_email": r.last_modifier, + "modifier_name": nickname_dict.get(r.last_modifier, ''), + "modifier_contact_email": contact_email_dict.get(r.last_modifier, ''), + "size": r.size, + "encrypted": r.encrypted, + "permission": r.permission, + } + + if r.repo_id in repos_with_admin_share_to: + repo_info['is_admin'] = True + else: + repo_info['is_admin'] = False + + repo_info_list.append(repo_info) + + if filter_by['group']: + + if org_id: + group_repos = seafile_api.get_org_group_repos_by_user(email, org_id) + else: + group_repos = seafile_api.get_group_repos_by_user(email) + + group_repos.sort(lambda x, y: cmp(y.last_modify, x.last_modify)) + + # Reduce memcache fetch ops. + share_from_set = set([x.user for x in group_repos]) + modifiers_set = set([x.last_modifier for x in group_repos]) + for e in modifiers_set | share_from_set: + if e not in contact_email_dict: + contact_email_dict[e] = email2contact_email(e) + if e not in nickname_dict: + nickname_dict[e] = email2nickname(e) + + for r in group_repos: + repo_info = { + "type": "group", + "group_id": r.group_id, + "group_name": r.group_name, + "repo_id": r.repo_id, + "repo_name": r.repo_name, + "last_modified": timestamp_to_isoformat_timestr(r.last_modify), + "modifier_email": r.last_modifier, + "modifier_name": nickname_dict.get(r.last_modifier, ''), + "modifier_contact_email": contact_email_dict.get(r.last_modifier, ''), + "size": r.size, + "encrypted": r.encrypted, + "permission": r.permission, + } + repo_info_list.append(repo_info) + + if filter_by['public'] and request.user.permissions.can_view_org(): + public_repos = list_inner_pub_repos(request) + + # Reduce memcache fetch ops. + share_from_set = set([x.user for x in public_repos]) + modifiers_set = set([x.last_modifier for x in public_repos]) + for e in modifiers_set | share_from_set: + if e not in contact_email_dict: + contact_email_dict[e] = email2contact_email(e) + if e not in nickname_dict: + nickname_dict[e] = email2nickname(e) + + for r in public_repos: + repo_info = { + "type": "public", + "repo_id": r.repo_id, + "repo_name": r.repo_name, + "last_modified": timestamp_to_isoformat_timestr(r.last_modify), + "modifier_email": r.last_modifier, + "modifier_name": nickname_dict.get(r.last_modifier, ''), + "modifier_contact_email": contact_email_dict.get(r.last_modifier, ''), + "size": r.size, + "encrypted": r.encrypted, + "permission": r.permission, + } + repo_info_list.append(repo_info) + + return Response({'repos': repo_info_list}) + + class RepoView(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 38660c2604..a8f89d53e8 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -33,7 +33,14 @@ isPro: '{{ is_pro }}', lang: '{{ LANGUAGE_CODE }}', fileServerRoot: '{{ FILE_SERVER_ROOT }}' - } + }, + pageOptions: { + canGenerateShareLink: '{{ user.permissions.can_generate_share_link }}', + canGenerateUploadLink: '{{ user.permissions.can_generate_upload_link }}', + fileAuditEnabled: '{{ file_audit_enabled }}', + enableFileComment: '{{ enable_file_comment }}', + folderPermEnabled: '{{ folder_perm_enabled }}' + } }; diff --git a/seahub/urls.py b/seahub/urls.py index d6e61c45c7..812f8bbac1 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -42,7 +42,7 @@ from seahub.api2.endpoints.upload_links import UploadLinks, UploadLink, \ from seahub.api2.endpoints.repos_batch import ReposBatchView, \ ReposBatchCopyDirView, ReposBatchCreateDirView, \ ReposBatchCopyItemView, ReposBatchMoveItemView -from seahub.api2.endpoints.repos import RepoView +from seahub.api2.endpoints.repos import RepoView, ReposView from seahub.api2.endpoints.file import FileView from seahub.api2.endpoints.file_history import FileHistoryView, NewFileHistoryView from seahub.api2.endpoints.dir import DirView, DirDetailView @@ -280,6 +280,7 @@ urlpatterns = [ url(r'^api/v2.1/deleted-repos/$', DeletedRepos.as_view(), name='api2-v2.1-deleted-repos'), ## user::repos + url(r'^api/v2.1/repos/$', ReposView.as_view(), name='api-v2.1-repos-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/$', RepoView.as_view(), name='api-v2.1-repo-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/tags/$', FileTagsView.as_view(), name="api-v2.1-filetags-view"), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/tags/(?P.*?)/$',FileTagView.as_view(), name="api-v2.1-filetag-view"),