diff --git a/frontend/src/app.js b/frontend/src/app.js index 5e402e174f..614351222f 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -11,6 +11,10 @@ import FilesActivities from './pages/dashboard/files-activities'; import Starred from './pages/starred/starred'; import LinkedDevices from './pages/linked-devices/linked-devices'; import editUtilties from './utils/editor-utilties'; +import ShareAdminLibraries from './pages/share-admin/libraries'; +import ShareAdminFolders from './pages/share-admin/folders'; +import ShareAdminShareLinks from './pages/share-admin/share-links'; +import ShareAdminUploadLinks from './pages/share-admin/upload-links'; import 'seafile-ui'; import './assets/css/fa-solid.css'; @@ -88,6 +92,10 @@ class App extends Component { + + + + diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js index 674079c687..bf650d8cc1 100644 --- a/frontend/src/components/main-side-nav.js +++ b/frontend/src/components/main-side-nav.js @@ -95,22 +95,22 @@ class MainSideNav extends React.Component { return ( ); diff --git a/frontend/src/pages/share-admin/folders.js b/frontend/src/pages/share-admin/folders.js new file mode 100644 index 0000000000..6c2120cc08 --- /dev/null +++ b/frontend/src/pages/share-admin/folders.js @@ -0,0 +1,296 @@ +import React, { Component } from 'react'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; +import { gettext, siteRoot, loginUrl, isPro } from '../../utils/constants'; + +class Content extends Component { + + render() { + const {loading, errorMsg, items} = this.props.data; + + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( +
+

{gettext('You have not shared any folders')}

+

{gettext("You can share a single folder with a registered user if you don't want to share a whole library.")}

+
+ ); + + const table = ( + + + + + + + + + + + +
{/*icon*/}{gettext("Name")} {/* TODO: sort by name */}{gettext("Share To")}{gettext("Permission")}
+ ); + + return items.length ? table : emptyTip; + } + } +} + +class TableBody extends Component { + + constructor(props) { + super(props); + this.state = { + items: this.props.items + }; + } + + componentDidMount() { + document.addEventListener('click', this.clickDocument); + } + + clickDocument(e) { + // TODO: click 'outside' to hide ` + {permOption({perm: 'rw'})} + {permOption({perm: 'r'})} + {isPro ? permOption({perm: 'cloud-edit'}) : ''} + {isPro ? permOption({perm: 'preview'}) : ''} + + + ) : ( + + {data.cur_perm_text} + + + ) + } + + + ); + + return item; + } +} + +class ShareAdminFolders extends Component { + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + items: [] + }; + } + + componentDidMount() { + seafileAPI.listSharedFolders().then((res) => { + // res: {data: Array(2), status: 200, statusText: "OK", headers: {…}, config: {…}, …} + this.setState({ + loading: false, + items: res.data + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext("Permission denied") + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext("Error") + }); + } + + } else { + this.setState({ + loading: false, + errorMsg: gettext("Please check the network.") + }); + } + }); + } + + render() { + return ( +
+
+

{gettext("Folders")}

+
+
+ +
+
+ ); + } +} + +export default ShareAdminFolders; diff --git a/frontend/src/pages/share-admin/libraries.js b/frontend/src/pages/share-admin/libraries.js new file mode 100644 index 0000000000..86b89cc1df --- /dev/null +++ b/frontend/src/pages/share-admin/libraries.js @@ -0,0 +1,303 @@ +import React, { Component } from 'react'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; +import { gettext, siteRoot, loginUrl, isPro } from '../../utils/constants'; + +class Content extends Component { + + render() { + const {loading, errorMsg, items} = this.props.data; + + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( +
+

{gettext('You have not shared any libraries')}

+

{gettext("You can share libraries with your friends and colleagues by clicking the share icon of your own libraries in your home page or creating a new library in groups you are in.")}

+
+ ); + + const table = ( + + + + + + + + + + + +
{/*icon*/}{gettext("Name")} {/* TODO: sort by name */}{gettext("Share To")}{gettext("Permission")}
+ ); + + return items.length ? table : emptyTip; + } + } +} + +class TableBody extends Component { + + constructor(props) { + super(props); + this.state = { + items: this.props.items + }; + } + + componentDidMount() { + document.addEventListener('click', this.clickDocument); + } + + clickDocument(e) { + // TODO: click 'outside' to hide ` + {permOption({perm: 'rw'})} + {permOption({perm: 'r'})} + {data.show_admin ? permOption({perm: 'admin'}) : ''} + {isPro ? permOption({perm: 'cloud-edit'}) : ''} + {isPro ? permOption({perm: 'preview'}) : ''} + + + ) : ( + + {data.cur_perm_text} + + + ) + } + + + ); + + return item; + } +} + +class ShareAdminLibraries extends Component { + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + items: [] + }; + } + + componentDidMount() { + seafileAPI.listSharedLibraries().then((res) => { + // res: {data: Array(2), status: 200, statusText: "OK", headers: {…}, config: {…}, …} + this.setState({ + loading: false, + items: res.data + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext("Permission denied") + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext("Error") + }); + } + + } else { + this.setState({ + loading: false, + errorMsg: gettext("Please check the network.") + }); + } + }); + } + + render() { + return ( +
+
+

{gettext("Libraries")}

+
+
+ +
+
+ ); + } +} + +export default ShareAdminLibraries; diff --git a/frontend/src/pages/share-admin/share-links.js b/frontend/src/pages/share-admin/share-links.js new file mode 100644 index 0000000000..21be43370f --- /dev/null +++ b/frontend/src/pages/share-admin/share-links.js @@ -0,0 +1,259 @@ +import React, { Component } from 'react'; +import { Link } from '@reach/router'; +import moment from 'moment'; +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; +import { gettext, siteRoot, loginUrl, isPro, canGenerateUploadLink } from '../../utils/constants'; + +class Content extends Component { + constructor(props) { + super(props); + this.state = { + modalOpen: false, + modalContent: '' + }; + + this.toggleModal = this.toggleModal.bind(this); + this.showModal = this.showModal.bind(this); + } + + // required by `Modal`, and can only set the 'open' state + toggleModal() { + this.setState({ + modalOpen: !this.state.modalOpen + }); + } + + showModal(options) { + this.toggleModal(); + this.setState({ + modalContent: options.content + }); + } + + render() { + const {loading, errorMsg, items} = this.props.data; + + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( +
+

{gettext("You don't have any share links")}

+

{gettext("You can generate a share link for a folder or a file. Anyone who receives this link can view the folder or the file online.")}

+
+ ); + + const table = ( + + + + + + {/* TODO:sort */} + + + {/*TODO:sort*/} + + + + +
{/*icon*/}{gettext("Name")} {gettext("Library")}{gettext("Visits")}{gettext("Expiration")} {/*Operations*/}
+ + + {gettext('Link')} + + {this.state.modalContent} + + +
+ ); + + return items.length ? table : emptyTip; + } + } +} + +class TableBody extends Component { + + constructor(props) { + super(props); + this.state = { + //items: this.props.items + }; + } + + render() { + + let listItems = this.props.items.map(function(item, index) { + return ; + }, this); + + return ( + {listItems} + ); + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + showOpIcon: false, + deleted: false + }; + + this.handleMouseOver = this.handleMouseOver.bind(this); + this.handleMouseOut = this.handleMouseOut.bind(this); + + this.viewLink = this.viewLink.bind(this); + this.removeLink = this.removeLink.bind(this); + } + + handleMouseOver() { + this.setState({ + showOpIcon: true + }); + } + + handleMouseOut() { + this.setState({ + showOpIcon: false + }); + } + + viewLink(e) { + e.preventDefault(); + this.props.showModal({content: this.props.data.link}); + } + + removeLink(e) { + e.preventDefault(); + + const data = this.props.data; + seafileAPI.deleteShareLink(data.token) + .then((res) => { + this.setState({ + deleted: true + }); + // TODO: show feedback msg + // gettext("Successfully deleted 1 item") + }) + .catch((error) => { + // TODO: show feedback msg + }); + } + + render() { + + if (this.state.deleted) { + return null; + } + + const data = this.props.data; + + const icon_size = Utils.isHiDPI() ? 48 : 24; + if (data.is_dir) { + data.icon_url = Utils.getFolderIconUrl({ + is_readonly: false, + size: icon_size + }); + data.url = `${siteRoot}#my-libs/lib/${data.repo_id}${Utils.encodePath(data.path)}`; + } else { + data.icon_url = Utils.getFileIconUrl(data.obj_name, icon_size); + data.url = `${siteRoot}lib/${data.repo_id}/file${Utils.encodePath(data.path)}`; + } + let showDate = function(options) { + const date = moment(options.date).format('YYYY-MM-DD'); + return options.is_expired ? {date} : date; + } + + let iconVisibility = this.state.showOpIcon ? '' : ' invisible'; + let linkIconClassName = 'sf2-icon-link op-icon' + iconVisibility; + let deleteIconClassName = 'sf2-icon-delete op-icon' + iconVisibility; + + const item = ( + + + {data.obj_name} + {data.repo_name} + {data.view_cnt} + {data.expire_date ? showDate({date: data.expire_date, is_expired: data.is_expired}) : '--'} + + + + + + ); + + return item; + } +} + +class ShareAdminShareLinks extends Component { + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + items: [] + }; + } + + componentDidMount() { + seafileAPI.listShareLinks().then((res) => { + // res: {data: Array(2), status: 200, statusText: "OK", headers: {…}, config: {…}, …} + this.setState({ + loading: false, + items: res.data + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext("Permission denied") + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext("Error") + }); + } + + } else { + this.setState({ + loading: false, + errorMsg: gettext("Please check the network.") + }); + } + }); + } + + render() { + return ( +
+
+
    +
  • + {gettext('Share Links')} +
  • + { canGenerateUploadLink ? +
  • {gettext('Upload Links')}
  • + : '' } +
+
+
+ +
+
+ ); + } +} + +export default ShareAdminShareLinks; diff --git a/frontend/src/pages/share-admin/upload-links.js b/frontend/src/pages/share-admin/upload-links.js new file mode 100644 index 0000000000..dbce9e22a7 --- /dev/null +++ b/frontend/src/pages/share-admin/upload-links.js @@ -0,0 +1,246 @@ +import React, { Component } from 'react'; +import { Link } from '@reach/router'; +import moment from 'moment'; +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; +import { gettext, siteRoot, loginUrl, isPro, canGenerateShareLink } from '../../utils/constants'; + +class Content extends Component { + constructor(props) { + super(props); + this.state = { + modalOpen: false, + modalContent: '' + }; + + this.toggleModal = this.toggleModal.bind(this); + this.showModal = this.showModal.bind(this); + } + + // required by `Modal`, and can only set the 'open' state + toggleModal() { + this.setState({ + modalOpen: !this.state.modalOpen + }); + } + + showModal(options) { + this.toggleModal(); + this.setState({ + modalContent: options.content + }); + } + + render() { + const {loading, errorMsg, items} = this.props.data; + + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( +
+

{gettext("You don't have any upload links")}

+

{gettext("You can generate an upload link from any folder. Anyone who receives this link can upload files to this folder.")}

+
+ ); + + const table = ( + + + + + + + + + + + + +
{/*icon*/}{gettext("Name")}{gettext("Library")}{gettext("Visits")}{/*Operations*/}
+ + + {gettext('Link')} + + {this.state.modalContent} + + +
+ ); + + return items.length ? table : emptyTip; + } + } +} + +class TableBody extends Component { + + constructor(props) { + super(props); + this.state = { + //items: this.props.items + }; + } + + render() { + + let listItems = this.props.items.map(function(item, index) { + return ; + }, this); + + return ( + {listItems} + ); + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + showOpIcon: false, + deleted: false + }; + + this.handleMouseOver = this.handleMouseOver.bind(this); + this.handleMouseOut = this.handleMouseOut.bind(this); + + this.viewLink = this.viewLink.bind(this); + this.removeLink = this.removeLink.bind(this); + } + + handleMouseOver() { + this.setState({ + showOpIcon: true + }); + } + + handleMouseOut() { + this.setState({ + showOpIcon: false + }); + } + + viewLink(e) { + e.preventDefault(); + this.props.showModal({content: this.props.data.link}); + } + + removeLink(e) { + e.preventDefault(); + + const data = this.props.data; + seafileAPI.deleteUploadLink(data.token) + .then((res) => { + this.setState({ + deleted: true + }); + // TODO: show feedback msg + // gettext("Successfully deleted 1 item") + }) + .catch((error) => { + // TODO: show feedback msg + }); + } + + render() { + + if (this.state.deleted) { + return null; + } + + const data = this.props.data; + + const icon_size = Utils.isHiDPI() ? 48 : 24; + data.icon_url = Utils.getFolderIconUrl({ + is_readonly: false, + size: icon_size + }); + data.url = `${siteRoot}#my-libs/lib/${data.repo_id}${Utils.encodePath(data.path)}`; + + let iconVisibility = this.state.showOpIcon ? '' : ' invisible'; + let linkIconClassName = 'sf2-icon-link op-icon' + iconVisibility; + let deleteIconClassName = 'sf2-icon-delete op-icon' + iconVisibility; + + const item = ( + + + {data.obj_name} + {data.repo_name} + {data.view_cnt} + + + + + + ); + + return item; + } +} + +class ShareAdminUploadLinks extends Component { + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + items: [] + }; + } + + componentDidMount() { + seafileAPI.listUploadLinks().then((res) => { + // res: {data: Array(2), status: 200, statusText: "OK", headers: {…}, config: {…}, …} + this.setState({ + loading: false, + items: res.data + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext("Permission denied") + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext("Error") + }); + } + + } else { + this.setState({ + loading: false, + errorMsg: gettext("Please check the network.") + }); + } + }); + } + + render() { + return ( +
+
+
    + { canGenerateShareLink ? +
  • {gettext('Share Links')}
  • + : '' } +
  • {gettext('Upload Links')}
  • +
+
+
+ +
+
+ ); + } +} + +export default ShareAdminUploadLinks; diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index d5d8c4e9fe..f1560f3442 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -1,4 +1,4 @@ -import { mediaUrl } from './constants'; +import { mediaUrl, gettext } from './constants'; export const Utils = { @@ -171,5 +171,89 @@ export const Utils = { isSupportUploadFolder: function() { return navigator.userAgent.indexOf('Firefox')!=-1 || navigator.userAgent.indexOf('Chrome') > -1; + }, + + getLibIconUrl: function(options) { + /* + * param: {is_encrypted, is_readonly, size} + */ + // icon name + var icon_name = 'lib.png'; + if (options.is_encrypted) { + icon_name = 'lib-encrypted.png'; + } + if (options.is_readonly) { + icon_name = 'lib-readonly.png'; + } + + // icon size + var icon_size = options.size || 256; // 'size' can be 24, 48, or undefined. (2017.7.31) + + return mediaUrl + 'img/lib/' + icon_size + '/' + icon_name; + }, + + getFolderIconUrl: function(options) { + /* + * param: {is_readonly, size} + */ + const readonly = options.is_readonly; + const size = options.size; + return `${mediaUrl}img/folder${readonly ? '-read-only' : ''}${size > 24 ? '-192' : '-24'}.png`; + }, + + getLibIconTitle: function(options) { + /* + * param: {encrypted, is_admin, permission} + */ + var title; + if (options.encrypted) { + title = gettext("Encrypted library"); + } else if (options.is_admin) { // shared with 'admin' permission + title = gettext("Admin access"); + } else { + switch(options.permission) { + case 'rw': + title = gettext("Read-Write library"); + break; + case 'r': + title = gettext("Read-Only library"); + break; + case 'cloud-edit': + title = gettext("Preview-Edit-on-Cloud library"); + break; + case 'preview': + title = gettext("Preview-on-Cloud library"); + break; + } + } + return title; + }, + + getFolderIconTitle: function(options) { + var title; + switch(options.permission) { + case 'rw': + title = gettext("Read-Write folder"); + break; + case 'r': + title = gettext("Read-Only folder"); + break; + case 'cloud-edit': + title = gettext("Preview-Edit-on-Cloud folder"); + break; + case 'preview': + title = gettext("Preview-on-Cloud folder"); + break; + } + return title; + }, + + sharePerms: { + 'rw': gettext("Read-Write"), + 'r': gettext("Read-Only"), + 'admin': gettext("Admin"), + 'cloud-edit': gettext("Preview-Edit-on-Cloud"), + 'preview': gettext("Preview-on-Cloud") } + }; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index a6b3b814a8..2be9866a02 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -50,7 +50,6 @@ -webkit-font-smoothing: antialiased; } -.sf2-icon-delete:before { content:"\e006"; } .sf2-icon-menu:before { content: "\e031"; } .sf2-icon-more:before { content: "\e032"; } .sf2-icon-x1:before { content:"\e01d"; } @@ -72,6 +71,7 @@ .sf2-icon-trash:before { content:"\e016"; } .sf2-icon-download:before { content:"\e008"; } .sf2-icon-delete:before { content:"\e006"; } +.sf2-icon-link:before { content:"\e00e"; } .sf2-icon-caret-down:before { content:"\e01a"; } .sf2-icon-two-columns:before { content:"\e036"; } .sf2-icon-confirm:before {content:"\e01e"} @@ -1019,3 +1019,12 @@ a.op-icon:focus { display: flex; align-items: center; } + +.nav-link { + color: #8a948f; + padding: .3em .1em; +} +.nav-link.active { + color: #eb8205; + border-bottom: 2px solid #eb8205; +} diff --git a/seahub/templates/home_base.html b/seahub/templates/home_base.html index 403fb06a2b..2b9772138b 100644 --- a/seahub/templates/home_base.html +++ b/seahub/templates/home_base.html @@ -60,19 +60,19 @@ diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index cda8144b90..33024cdd3c 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -1595,19 +1595,19 @@ <% } %> <% if (can_add_repo) { %>
  • - {% trans "Libraries" %} + {% trans "Libraries" %}
  • - {% trans "Folders" %} + {% trans "Folders" %}
  • <% } %> <% if (can_generate_share_link) { %>
  • - {% trans "Links" %} + {% trans "Links" %}
  • <% } else if (can_generate_upload_link) { %>
  • - {% trans "Links" %} + {% trans "Links" %}
  • <% } %> diff --git a/seahub/urls.py b/seahub/urls.py index cfa188d588..b78ec01110 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -194,6 +194,10 @@ urlpatterns = [ url(r'^dashboard/$', react_fake_view, name="dashboard"), url(r'^starred/$', react_fake_view, name="starred"), url(r'^linked-devices/$', react_fake_view, name="linked_devices"), + url(r'^share-admin-libs/$', react_fake_view, name="share_admin_libs"), + url(r'^share-admin-folders/$', react_fake_view, name="share_admin_folders"), + url(r'^share-admin-share-links/$', react_fake_view, name="share_admin_share_links"), + url(r'^share-admin-upload-links/$', react_fake_view, name="share_admin_upload_links"), ### Ajax ### url(r'^ajax/repo/(?P[-0-9a-f]{36})/dirents/$', get_dirents, name="get_dirents"),