diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js index bf48d405bb..e55c48c1c0 100644 --- a/frontend/config/webpack.config.dev.js +++ b/frontend/config/webpack.config.dev.js @@ -84,6 +84,11 @@ module.exports = { require.resolve('react-dev-utils/webpackHotDevClient'), paths.appSrc + "/draw/draw.js", ], + sharedDirView: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/shared-dir-view.js", + ], sharedFileViewMarkdown: [ require.resolve('./polyfills'), require.resolve('react-dev-utils/webpackHotDevClient'), diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js index 38fe5aaffb..26e95ab6fe 100644 --- a/frontend/config/webpack.config.prod.js +++ b/frontend/config/webpack.config.prod.js @@ -65,6 +65,7 @@ module.exports = { app: [require.resolve('./polyfills'), paths.appSrc + "/app.js"], draft: [require.resolve('./polyfills'), paths.appSrc + "/draft.js"], draw: [require.resolve('./polyfills'), paths.appSrc + "/draw/draw.js"], + sharedDirView: [require.resolve('./polyfills'), paths.appSrc + "/shared-dir-view.js"], sharedFileViewMarkdown: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-markdown.js"], sharedFileViewText: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-text.js"], sharedFileViewImage: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-image.js"], diff --git a/frontend/src/components/dialog/share-link-zip-download-dialog.js b/frontend/src/components/dialog/share-link-zip-download-dialog.js new file mode 100644 index 0000000000..5fe1100008 --- /dev/null +++ b/frontend/src/components/dialog/share-link-zip-download-dialog.js @@ -0,0 +1,127 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; +import { gettext, fileServerRoot } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import Loading from '../loading'; + +const propTypes = { + token: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +let interval; + +class ShareLinkZipDownloadDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + errorMsg: '', + zipProgress: null + }; + } + + componentDidMount() { + const { token, path } = this.props; + seafileAPI.getShareLinkZipTask(token, path).then((res) => { + const zipToken = res.data['zip_token']; + this.setState({ + isLoading: false, + errorMsg: '', + zipToken: zipToken + }); + this.queryZipProgress(); + interval = setInterval(this.queryZipProgress, 1000); + }).catch((error) => { + let errorMsg = ''; + if (error.response) { + errorMsg = gettext('Error'); + } else { + errorMsg = gettext('Please check the network.'); + } + this.setState({ + isLoading: false, + errorMsg: errorMsg + }); + }); + } + + queryZipProgress = () => { + const zipToken = this.state.zipToken; + seafileAPI.queryZipProgress(zipToken).then((res) => { + const data = res.data; + this.setState({ + zipProgress: data.total == 0 ? '100%' : (data.zipped/data.total*100).toFixed(2) + '%' + }); + if (data['total'] == data['zipped']) { + clearInterval(interval); + this.props.toggleDialog(); + location.href = `${fileServerRoot}zip/${zipToken}`; + } + }).catch((error) => { + clearInterval(interval); + let errorMsg = ''; + if (error.response) { + errorMsg = gettext('Error'); + } else { + errorMsg = gettext('Please check the network.'); + } + this.setState({ + isLoading: false, + errorMsg: errorMsg + }); + }); + } + + cancelZipTask = () => { + const zipToken = this.state.zipToken; + seafileAPI.cancelZipTask(zipToken).then((res) => { + // do nothing + }).catch((error) => { + // do nothing + }); + } + + toggleDialog = () => { + const zipProgress = this.state.zipProgress; + if (zipProgress && zipProgress != '100%') { + clearInterval(interval); + this.cancelZipTask(); + } + this.props.toggleDialog(); + } + + render() { + return ( + + {gettext('Download')} + + + + + ); + } +} + +class Content extends React.Component { + + render() { + const {isLoading, errorMsg, zipProgress} = this.props.data; + + if (isLoading) { + return ; + } + + if (errorMsg) { + return

{errorMsg}

; + } + + return

{`${gettext('Packaging...')} ${zipProgress}`}

; + } +} + +ShareLinkZipDownloadDialog.propTypes = propTypes; + +export default ShareLinkZipDownloadDialog; diff --git a/frontend/src/css/shared-dir-view.css b/frontend/src/css/shared-dir-view.css new file mode 100644 index 0000000000..d3386bb0e0 --- /dev/null +++ b/frontend/src/css/shared-dir-view.css @@ -0,0 +1,28 @@ +body { + overflow: hidden; +} +#wrapper { + height: 100%; +} +.top-header { + background: #f4f4f7; + border-bottom: 1px solid #e8e8e8; + padding: 8px 16px 4px; + height: 53px; + flex-shrink: 0; +} +.title { + font-size: 1.4rem; + margin-bottom: .5rem; +} +.shared-dir-view-main { + width: calc(100% - 40px); + max-width: 950px; + padding: 15px 0; + margin: 0 auto; +} +.op-bar { + padding: 9px 10px; + background: #f2f2f2; + border-radius: 2px; +} diff --git a/frontend/src/shared-dir-view.js b/frontend/src/shared-dir-view.js new file mode 100644 index 0000000000..fe40a7f5e1 --- /dev/null +++ b/frontend/src/shared-dir-view.js @@ -0,0 +1,255 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Button } from 'reactstrap'; +import moment from 'moment'; +import Account from './components/common/account'; +import { gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants'; +import { Utils } from './utils/utils'; +import { seafileAPI } from './utils/seafile-api'; +import Loading from './components/loading'; +import toaster from './components/toast'; +import ModalPortal from './components/modal-portal'; +import ShareLinkZipDownloadDialog from './components/dialog/share-link-zip-download-dialog'; + +import './css/shared-dir-view.css'; + +let loginUser = window.app.pageOptions.name; +const { token, trafficOverLimit, dirName, sharedBy, path, canDownload } = window.shared.pageOptions; + +const showDownloadIcon = !trafficOverLimit && canDownload; + +class SharedDirView extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: true, + errorMsg: '', + items: [], + isZipDialogOpen: false, + zipFolderPath: '' + }; + } + + componentDidMount() { + if (trafficOverLimit) { + toaster.danger(gettext('File download is disabled: the share link traffic of owner is used up.'), { + duration: 3 + }); + } + + seafileAPI.listSharedDir(token, path).then((res) => { + const items = res.data['dirent_list']; + this.setState({ + isLoading: false, + errorMsg: '', + items: items + }); + }).catch((error) => { + let errorMsg = ''; + if (error.response) { + if (error.response.data && error.response.data['error_msg']) { + errorMsg = error.response.data['error_msg']; + } else { + errorMsg = gettext('Error'); + } + } else { + errorMsg = gettext('Please check the network.'); + } + this.setState({ + isLoading: false, + errorMsg: errorMsg + }); + }); + } + + renderPath = () => { + // path: '/', or '/g/' + if (path == '/') { + return dirName; + } + + let pathList = path.substr(0, path.length -1).split('/'); + return ( + + {dirName} + / + {pathList.map((item, index) => { + if (index > 0 && index != pathList.length - 1) { + return ( + + {pathList[index]} + / + + ); + } + } + )} + {pathList[pathList.length - 1]} + + ); + } + + zipDownloadFolder = (folderPath) => { + this.setState({ + isZipDialogOpen: true, + zipFolderPath: folderPath + }); + } + + closeZipDialog = () => { + this.setState({ + isZipDialogOpen: false, + zipFolderPath: '' + }); + } + + render() { + return ( + +
+
+ + logo + + {loginUser && } +
+
+
+

{dirName}

+

{gettext('Shared by: ')}{sharedBy}

+
+

{gettext('Current path: ')}{this.renderPath()}

+ {showDownloadIcon && + + } +
+ +
+
+
+ {this.state.isZipDialogOpen && + + + + } +
+ ); + } +} + +class Content extends React.Component { + render() { + const { isLoading, errorMsg, items } = this.props.data; + + if (isLoading) { + return ; + } + + if (errorMsg) { + return

{errorMsg}

; + } + + return ( + + + + + + + + + + + + {items.map((item, index) => { + return ; + })} + +
{gettext('Name')}{gettext('Size')}{gettext('Last Update')}{gettext('Operations')}
+ ); + } +} + +class Item extends React.Component { + + constructor(props) { + super(props); + this.state = { + isIconShown: false + }; + } + + handleMouseOver = () => { + this.setState({isIconShown: true}); + } + + handleMouseOut = () => { + this.setState({isIconShown: false}); + } + + zipDownloadFolder = (e) => { + e.preventDefault(); + this.props.zipDownloadFolder.bind(this, this.props.item.folder_path)(); + } + + render() { + const item = this.props.item; + const { isIconShown } = this.state; + + if (item.is_dir) { + return ( + + + + {item.folder_name} + + + {moment(item.last_modified).format('YYYY-MM-DD')} + + {showDownloadIcon && + + {gettext('Download')} + + } + + + ); + } else { + const fileURL = `${siteRoot}d/${token}/files/?p=${encodeURIComponent(item.file_path)}`; + return ( + + + + {item.file_name} + + {Utils.bytesToSize(item.size)} + {moment(item.last_modified).format('YYYY-MM-DD')} + + {showDownloadIcon && + + {gettext('Download')} + + } + + + ); + } + } +} + +ReactDOM.render( + , + document.getElementById('wrapper') +); diff --git a/seahub/templates/view_shared_dir_react.html b/seahub/templates/view_shared_dir_react.html new file mode 100644 index 0000000000..5bc69fdf35 --- /dev/null +++ b/seahub/templates/view_shared_dir_react.html @@ -0,0 +1,23 @@ +{% extends "base_for_react.html" %} +{% load seahub_tags i18n %} +{% load render_bundle from webpack_loader %} + +{% block extra_style %} +{% render_bundle 'sharedDirView' 'css' %} +{% endblock %} + +{% block extra_script %} + + {% render_bundle 'sharedDirView' 'js' %} +{% endblock %} diff --git a/seahub/views/repo.py b/seahub/views/repo.py index e1dfa5af8a..127c2698f7 100644 --- a/seahub/views/repo.py +++ b/seahub/views/repo.py @@ -275,8 +275,11 @@ def view_shared_dir(request, fileshare): req_image_path = posixpath.join(req_path, f.obj_name) src = get_share_link_thumbnail_src(token, thumbnail_size, req_image_path) f.encoded_thumbnail_src = urlquote(src) + + #template = 'view_shared_dir.html' + template = 'view_shared_dir_react.html' - return render(request, 'view_shared_dir.html', { + return render(request, template, { 'repo': repo, 'token': token, 'path': req_path,