From 8941ff329a0ca91cef839ef52ff89fd3f620e922 Mon Sep 17 00:00:00 2001 From: zxj96 <35951769+zxj96@users.noreply.github.com> Date: Tue, 30 Jul 2019 17:03:16 +0800 Subject: [PATCH 01/14] Solve the problem of having a dot in the dir name (#3930) --- frontend/src/components/file-chooser/tree-list-item.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/file-chooser/tree-list-item.js b/frontend/src/components/file-chooser/tree-list-item.js index b3e201c53d..97714b4b5d 100644 --- a/frontend/src/components/file-chooser/tree-list-item.js +++ b/frontend/src/components/file-chooser/tree-list-item.js @@ -88,9 +88,13 @@ class TreeViewItem extends React.Component { let isCurrentPath = this.props.selectedPath === this.state.filePath; const fileName = node.object.name; - if (this.props.fileSuffixes && fileName && fileName.indexOf('.') !== -1) { - const suffix = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); - if (!this.props.fileSuffixes.includes(suffix)) return null; + if (this.props.fileSuffixes && fileName && node.object.type === 'file') { + if ( fileName.indexOf('.') !== -1) { + const suffix = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); + if (!this.props.fileSuffixes.includes(suffix)) return null; + } else { + if (node.object.type === 'file') return null; + } } return( From 2b614b3324d913e06774817b606dd0036a6f2947 Mon Sep 17 00:00:00 2001 From: sniper-py <38058090+sniper-py@users.noreply.github.com> Date: Wed, 31 Jul 2019 10:10:56 +0800 Subject: [PATCH 02/14] fix is_repo_admin (#3929) --- seahub/share/utils.py | 44 ++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/seahub/share/utils.py b/seahub/share/utils.py index 68ad86f110..a88eecbafd 100644 --- a/seahub/share/utils.py +++ b/seahub/share/utils.py @@ -12,30 +12,40 @@ logger = logging.getLogger(__name__) def is_repo_admin(username, repo_id): - repo_owner = seafile_api.get_repo_owner(repo_id) - + # repo is shared to user with admin permission try: - if '@seafile_group' in repo_owner: - # is group owned repo - group_id = int(repo_owner.split('@')[0]) + user_share_permission = ExtraSharePermission.objects. \ + get_user_permission(repo_id, username) + if user_share_permission == PERMISSION_ADMIN: + return True + + # get all groups that repo is shared to with admin permission + group_ids = ExtraGroupsSharePermission.objects.get_admin_groups_by_repo(repo_id) + for group_id in group_ids: if is_group_admin(group_id, username): return True - else: - user_share_permission = ExtraSharePermission.objects.\ - get_user_permission(repo_id, username) - if user_share_permission == PERMISSION_ADMIN: - return True - - # get all groups that repo is shared to with admin permission - group_ids = ExtraGroupsSharePermission.objects.get_admin_groups_by_repo(repo_id) - for group_id in group_ids: - if is_group_admin(group_id, username): - return True - return False except Exception as e: logger.error(e) return False + repo_owner = seafile_api.get_repo_owner(repo_id) or seafile_api.get_org_repo_owner(repo_id) + if not repo_owner: + logger.error('repo %s owner is None' % repo_id) + return False + + # repo owner + if username == repo_owner: + return True + + # user is department admin + if '@seafile_group' in repo_owner: + # is group owned repo + group_id = int(repo_owner.split('@')[0]) + if is_group_admin(group_id, username): + return True + + return False + def share_dir_to_user(repo, path, owner, share_from, share_to, permission, org_id=None): # Share repo or subdir to user with permission(r, rw, admin). extra_share_permission = '' From 987f19bc007a47a021d5a24875326f94c5795edb Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Wed, 31 Jul 2019 14:43:11 +0800 Subject: [PATCH 03/14] reset password toast (#3935) --- frontend/src/pages/org-admin/org-user-item.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/org-admin/org-user-item.js b/frontend/src/pages/org-admin/org-user-item.js index 039c3dda56..305fa12030 100644 --- a/frontend/src/pages/org-admin/org-user-item.js +++ b/frontend/src/pages/org-admin/org-user-item.js @@ -56,12 +56,15 @@ class UserItem extends React.Component { toggleResetPW = () => { const email = this.props.user.email; + toaster.success(gettext('Resetting user\'s password, please wait for a moment.')); seafileAPI.resetOrgUserPassword(orgID, email).then(res => { let msg; msg = gettext('Successfully reset password to %(passwd)s for user %(user)s.'); msg = msg.replace('%(passwd)s', res.data.new_password); msg = msg.replace('%(user)s', email); - toaster.success(msg); + toaster.success(msg, { + duration: 15 + }); }).catch(error => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); From 1f78ae8ac5815f54515714213d2e842684404b54 Mon Sep 17 00:00:00 2001 From: llj Date: Thu, 1 Aug 2019 15:32:57 +0800 Subject: [PATCH 04/14] [org admin] user: rewrote it with react (#3936) --- .../dialog/set-org-user-contact-email.js | 75 +++++++ .../components/dialog/set-org-user-name.js | 77 +++++++ .../components/dialog/set-org-user-quota.js | 89 ++++++++ frontend/src/components/org-admin-user-nav.js | 36 ++++ frontend/src/css/org-admin-user.css | 3 + frontend/src/pages/org-admin/index.js | 6 + .../src/pages/org-admin/org-user-profile.js | 202 ++++++++++++++++++ .../src/pages/org-admin/org-user-repos.js | 195 +++++++++++++++++ .../pages/org-admin/org-user-shared-repos.js | 132 ++++++++++++ 9 files changed, 815 insertions(+) create mode 100644 frontend/src/components/dialog/set-org-user-contact-email.js create mode 100644 frontend/src/components/dialog/set-org-user-name.js create mode 100644 frontend/src/components/dialog/set-org-user-quota.js create mode 100644 frontend/src/components/org-admin-user-nav.js create mode 100644 frontend/src/css/org-admin-user.css create mode 100644 frontend/src/pages/org-admin/org-user-profile.js create mode 100644 frontend/src/pages/org-admin/org-user-repos.js create mode 100644 frontend/src/pages/org-admin/org-user-shared-repos.js diff --git a/frontend/src/components/dialog/set-org-user-contact-email.js b/frontend/src/components/dialog/set-org-user-contact-email.js new file mode 100644 index 0000000000..c2ae23fbdd --- /dev/null +++ b/frontend/src/components/dialog/set-org-user-contact-email.js @@ -0,0 +1,75 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; + +const propTypes = { + orgID: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + contactEmail: PropTypes.string.isRequired, + updateContactEmail: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +class SetOrgUserContactEmail extends React.Component { + + constructor(props) { + super(props); + this.state = { + inputValue: this.props.contactEmail, + submitBtnDisabled: false + }; + } + + handleInputChange = (e) => { + this.setState({ + inputValue: e.target.value + }); + } + + formSubmit = () => { + const { orgID, email } = this.props; + const contactEmail = this.state.inputValue.trim(); + + this.setState({ + submitBtnDisabled: true + }); + + seafileAPI.setOrgUserContactEmail(orgID, email, contactEmail).then((res) => { + const newContactEmail = contactEmail ? res.data.contact_email : ''; + this.props.updateContactEmail(newContactEmail); + this.props.toggleDialog(); + }).catch((error) => { + let errorMsg = Utils.getErrorMsg(error); + this.setState({ + formErrorMsg: errorMsg, + submitBtnDisabled: false + }); + }); + } + + render() { + const { inputValue, formErrorMsg, submitBtnDisabled } = this.state; + return ( + + {gettext('Set user contact email')} + + + + {formErrorMsg &&

{formErrorMsg}

} +
+
+ + + + +
+ ); + } +} + +SetOrgUserContactEmail.propTypes = propTypes; + +export default SetOrgUserContactEmail; diff --git a/frontend/src/components/dialog/set-org-user-name.js b/frontend/src/components/dialog/set-org-user-name.js new file mode 100644 index 0000000000..cb58081414 --- /dev/null +++ b/frontend/src/components/dialog/set-org-user-name.js @@ -0,0 +1,77 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; + +const propTypes = { + orgID: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + updateName: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +class SetOrgUserName extends React.Component { + + constructor(props) { + super(props); + this.state = { + inputValue: this.props.name, + submitBtnDisabled: false + }; + } + + handleInputChange = (e) => { + this.setState({ + inputValue: e.target.value + }); + } + + formSubmit = () => { + const { orgID, email } = this.props; + const name = this.state.inputValue.trim(); + + this.setState({ + submitBtnDisabled: true + }); + + // when name is '', api returns the previous name + // but newName needs to be '' + seafileAPI.setOrgUserName(orgID, email, name).then((res) => { + const newName = name ? res.data.name : ''; + this.props.updateName(newName); + this.props.toggleDialog(); + }).catch((error) => { + let errorMsg = Utils.getErrorMsg(error); + this.setState({ + formErrorMsg: errorMsg, + submitBtnDisabled: false + }); + }); + } + + render() { + const { inputValue, formErrorMsg, submitBtnDisabled } = this.state; + return ( + + {gettext('Set user name')} + + + + {formErrorMsg &&

{formErrorMsg}

} +
+
+ + + + +
+ ); + } +} + +SetOrgUserName.propTypes = propTypes; + +export default SetOrgUserName; diff --git a/frontend/src/components/dialog/set-org-user-quota.js b/frontend/src/components/dialog/set-org-user-quota.js new file mode 100644 index 0000000000..6d34cc6948 --- /dev/null +++ b/frontend/src/components/dialog/set-org-user-quota.js @@ -0,0 +1,89 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; + +const propTypes = { + orgID: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + quotaTotal: PropTypes.string.isRequired, + updateQuota: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +class SetOrgUserQuota extends React.Component { + + constructor(props) { + super(props); + const initialQuota = this.props.quotaTotal < 0 ? '' : + this.props.quotaTotal / (1000 * 1000); + this.state = { + inputValue: initialQuota, + submitBtnDisabled: false + }; + } + + handleInputChange = (e) => { + this.setState({ + inputValue: e.target.value + }); + } + + formSubmit = () => { + const { orgID, email } = this.props; + const quota = this.state.inputValue.trim(); + + if (!quota) { + this.setState({ + formErrorMsg: gettext('It is required.') + }); + return false; + } + + this.setState({ + submitBtnDisabled: true + }); + + seafileAPI.setOrgUserQuota(orgID, email, quota).then((res) => { + this.props.updateQuota(res.data.quota_total); + this.props.toggleDialog(); + }).catch((error) => { + let errorMsg = Utils.getErrorMsg(error); + this.setState({ + formErrorMsg: errorMsg, + submitBtnDisabled: false + }); + }); + } + + render() { + const { inputValue, formErrorMsg, submitBtnDisabled } = this.state; + return ( + + {gettext('Set user quota')} + + + + + + MB + + +

{gettext('Tip: 0 means default limit')}

+ {formErrorMsg &&

{formErrorMsg}

} +
+
+ + + + +
+ ); + } +} + +SetOrgUserQuota.propTypes = propTypes; + +export default SetOrgUserQuota; diff --git a/frontend/src/components/org-admin-user-nav.js b/frontend/src/components/org-admin-user-nav.js new file mode 100644 index 0000000000..3e15d5ab1d --- /dev/null +++ b/frontend/src/components/org-admin-user-nav.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@reach/router'; +import { siteRoot, gettext } from '../utils/constants'; + +const propTypes = { + email: PropTypes.string.isRequired, + currentItem: PropTypes.string.isRequired +}; + +class OrgAdminUserNav extends React.Component { + + render() { + const { email, currentItem } = this.props; + const urlBase = `${siteRoot}org/useradmin/info/${encodeURIComponent(email)}/`; + return ( +
+ +
+ ); + } +} + +OrgAdminUserNav.propTypes = propTypes; + +export default OrgAdminUserNav; diff --git a/frontend/src/css/org-admin-user.css b/frontend/src/css/org-admin-user.css new file mode 100644 index 0000000000..9ebdeb8ced --- /dev/null +++ b/frontend/src/css/org-admin-user.css @@ -0,0 +1,3 @@ +.cur-view-path.org-admin-user-nav { + padding: 0 16px 1px; +} diff --git a/frontend/src/pages/org-admin/index.js b/frontend/src/pages/org-admin/index.js index ae71320bc5..4d8c0c9ed2 100644 --- a/frontend/src/pages/org-admin/index.js +++ b/frontend/src/pages/org-admin/index.js @@ -5,6 +5,9 @@ import { Router } from '@reach/router'; import { siteRoot } from '../../utils/constants'; import SidePanel from './side-panel'; import OrgUsers from './org-users'; +import OrgUserProfile from './org-user-profile'; +import OrgUserRepos from './org-user-repos'; +import OrgUserSharedRepos from './org-user-shared-repos'; import OrgGroups from './org-groups'; import OrgLibraries from './org-libraries'; import OrgInfo from './org-info'; @@ -61,6 +64,9 @@ class Org extends React.Component { + + + diff --git a/frontend/src/pages/org-admin/org-user-profile.js b/frontend/src/pages/org-admin/org-user-profile.js new file mode 100644 index 0000000000..99c2e43a50 --- /dev/null +++ b/frontend/src/pages/org-admin/org-user-profile.js @@ -0,0 +1,202 @@ +import React, { Component, Fragment } from 'react'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, loginUrl } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import Loading from '../../components/loading'; +import OrgAdminUserNav from '../../components/org-admin-user-nav'; +import SetOrgUserName from '../../components/dialog/set-org-user-name'; +import SetOrgUserContactEmail from '../../components/dialog/set-org-user-contact-email'; +import SetOrgUserQuota from '../../components/dialog/set-org-user-quota'; +import MainPanelTopbar from './main-panel-topbar'; + +import '../../css/org-admin-user.css'; + +const { orgID, orgName } = window.org.pageOptions; + +class OrgUserProfile extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '' + }; + } + + componentDidMount() { + seafileAPI.getOrgUserInfo(orgID, this.props.email).then((res) => { + this.setState(Object.assign({ + loading: false + }, 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.') + }); + } + }); + } + + updateName = (name) => { + this.setState({ + name: name + }); + } + + updateContactEmail = (contactEmail) => { + this.setState({ + contact_email: contactEmail + }); + } + + updateQuota = (quota) => { + this.setState({ + quota_total: quota + }); + } + + render() { + return ( + + +
+
+ +
+ +
+
+
+
+ ); + } +} + +class Content extends Component { + + constructor(props) { + super(props); + this.state = { + isSetNameDialogOpen: false, + isSetContactEmailDialogOpen: false, + isSetQuotaDialogOpen: false + }; + } + + toggleSetNameDialog = () => { + this.setState({ + isSetNameDialogOpen: !this.state.isSetNameDialogOpen + }); + } + + toggleSetContactEmailDialog = () => { + this.setState({ + isSetContactEmailDialogOpen: !this.state.isSetContactEmailDialogOpen + }); + } + + toggleSetQuotaDialog = () => { + this.setState({ + isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen + }); + } + + render() { + const { + loading, errorMsg, + avatar_url, email, contact_email, + name, quota_total, quota_usage + } = this.props.data; + const { isSetNameDialogOpen, isSetContactEmailDialogOpen, isSetQuotaDialogOpen } = this.state; + + if (loading) { + return ; + } + if (errorMsg) { + return

{errorMsg}

; + } + + return ( + +
+
{gettext('Avatar')}
+
+ +
+ +
ID
+
{email}
+ +
{gettext('Name')}
+
+ {name || '--'} + +
+ +
{gettext('Contact Email')}
+
+ {contact_email || '--'} + +
+ +
{gettext('Organization')}
+
{orgName}
+ +
{gettext('Space Used / Quota')}
+
+ {`${Utils.bytesToSize(quota_usage)}${quota_total > 0 ? ' / ' + Utils.bytesToSize(quota_total) : ''}`} + +
+
+ {isSetNameDialogOpen && + + } + {isSetContactEmailDialogOpen && + + } + {isSetQuotaDialogOpen && + + } +
+ ); + } +} + +export default OrgUserProfile; diff --git a/frontend/src/pages/org-admin/org-user-repos.js b/frontend/src/pages/org-admin/org-user-repos.js new file mode 100644 index 0000000000..fb8f4ba87b --- /dev/null +++ b/frontend/src/pages/org-admin/org-user-repos.js @@ -0,0 +1,195 @@ +import React, { Component, Fragment } from 'react'; +import moment from 'moment'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, loginUrl } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import Loading from '../../components/loading'; +import toaster from '../../components/toast'; +import OrgAdminUserNav from '../../components/org-admin-user-nav'; +import DeleteRepoDialog from '../../components/dialog/delete-repo-dialog'; +import MainPanelTopbar from './main-panel-topbar'; + +import '../../css/org-admin-user.css'; + +const { orgID } = window.org.pageOptions; + +class OrgUserOwnedRepos extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '' + }; + } + + componentDidMount() { + seafileAPI.getOrgUserOwnedRepos(orgID, this.props.email).then((res) => { + this.setState(Object.assign({ + loading: false + }, 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 ( + + +
+
+ +
+ +
+
+
+
+ ); + } +} + +class Content extends Component { + + constructor(props) { + super(props); + } + + render() { + const { + loading, errorMsg, repo_list + } = this.props.data; + + if (loading) { + return ; + } + if (errorMsg) { + return

{errorMsg}

; + } + + return ( + + + + + + + + + + + + + {repo_list.map((item, index) => { + return ; + })} + +
{/*icon*/}{gettext('Name')}{gettext('Size')}{gettext('Last Update')}
+
+ ); + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + isOpIconShown: false, + deleted: false, + isDeleteRepoDialogOpen: false + }; + } + + handleMouseOver = () => { + this.setState({ + isOpIconShown: true + }); + } + + handleMouseOut = () => { + this.setState({ + isOpIconShown: false + }); + } + + handleDeleteIconClick = (e) => { + e.preventDefault(); + this.toggleDeleteRepoDialog(); + } + + toggleDeleteRepoDialog = () => { + this.setState({ + isDeleteRepoDialogOpen: !this.state.isDeleteRepoDialogOpen + }); + } + + deleteRepo = () => { + const repo = this.props.data; + seafileAPI.deleteOrgRepo(orgID, repo.repo_id).then((res) => { + this.setState({ + deleted: true + }); + const msg = gettext('Successfully deleted {name}.').replace('{name}', repo.repo_name); + toaster.success(msg); + }).catch((error) => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + } + + render() { + const { deleted, isOpIconShown, isDeleteRepoDialogOpen } = this.state; + const repo = this.props.data; + + if (deleted) { + return null; + } + + return ( + + + + {Utils.getLibIconTitle(repo)} + + {repo.repo_name} + {Utils.bytesToSize(repo.size)} + {moment(repo.last_modified).format('YYYY-MM-DD')} + + + + + {isDeleteRepoDialogOpen && + + } + + ); + } +} + +export default OrgUserOwnedRepos; diff --git a/frontend/src/pages/org-admin/org-user-shared-repos.js b/frontend/src/pages/org-admin/org-user-shared-repos.js new file mode 100644 index 0000000000..2c698d705b --- /dev/null +++ b/frontend/src/pages/org-admin/org-user-shared-repos.js @@ -0,0 +1,132 @@ +import React, { Component, Fragment } from 'react'; +import moment from 'moment'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, loginUrl } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import Loading from '../../components/loading'; +import OrgAdminUserNav from '../../components/org-admin-user-nav'; +import MainPanelTopbar from './main-panel-topbar'; + +import '../../css/org-admin-user.css'; + +const { orgID } = window.org.pageOptions; + +class OrgUserSharedRepos extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '' + }; + } + + componentDidMount() { + seafileAPI.getOrgUserBesharedRepos(orgID, this.props.email).then((res) => { + this.setState(Object.assign({ + loading: false + }, 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 ( + + +
+
+ +
+ +
+
+
+
+ ); + } +} + +class Content extends Component { + + constructor(props) { + super(props); + } + + render() { + const { + loading, errorMsg, repo_list + } = this.props.data; + + if (loading) { + return ; + } + if (errorMsg) { + return

{errorMsg}

; + } + + return ( + + + + + + + + + + + + {repo_list.map((item, index) => { + return ; + })} + +
{/*icon*/}{gettext('Name')}{gettext('Owner')}{gettext('Size')}{gettext('Last Update')}
+ ); + } +} + +class Item extends Component { + + constructor(props) { + super(props); + } + + render() { + const repo = this.props.data; + return ( + + + {Utils.getLibIconTitle(repo)} + + {repo.repo_name} + {repo.owner_name} + {Utils.bytesToSize(repo.size)} + {moment(repo.last_modified).format('YYYY-MM-DD')} + + ); + } +} + +export default OrgUserSharedRepos; From 1c6fa019d07cf10933026a5c67d75f082062aac6 Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Thu, 1 Aug 2019 18:40:26 +0800 Subject: [PATCH 05/14] change org department function name (#3938) * change org depatrment function name * change name * change name --- frontend/src/components/dialog/org-add-member-dialog.js | 2 +- frontend/src/components/dialog/org-add-repo-dialog.js | 2 +- frontend/src/components/dialog/org-delete-member-dialog.js | 2 +- frontend/src/components/dialog/org-delete-repo-dialog.js | 2 +- frontend/src/pages/org-admin/org-department-item.js | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/dialog/org-add-member-dialog.js b/frontend/src/components/dialog/org-add-member-dialog.js index 4ffcdbc9cc..133a730b64 100644 --- a/frontend/src/components/dialog/org-add-member-dialog.js +++ b/frontend/src/components/dialog/org-add-member-dialog.js @@ -34,7 +34,7 @@ class AddMemberDialog extends React.Component { const email = this.state.selectedOption.email; this.refs.orgSelect.clearSelect(); this.setState({ errMessage: [] }); - seafileAPI.orgAdminAddDepartGroupUser(orgID, this.props.groupID, email).then((res) => { + seafileAPI.orgAdminAddGroupMember(orgID, this.props.groupID, email).then((res) => { this.setState({ selectedOption: null }); if (res.data.failed.length > 0) { this.setState({ errMessage: res.data.failed[0].error_msg }); diff --git a/frontend/src/components/dialog/org-add-repo-dialog.js b/frontend/src/components/dialog/org-add-repo-dialog.js index 3f393dec01..ee9d9838f0 100644 --- a/frontend/src/components/dialog/org-add-repo-dialog.js +++ b/frontend/src/components/dialog/org-add-repo-dialog.js @@ -30,7 +30,7 @@ class AddRepoDialog extends React.Component { handleSubmit = () => { let isValid = this.validateName(); if (isValid) { - seafileAPI.orgAdminAddDepartGroupRepo(orgID, this.props.groupID, this.state.repoName.trim()).then((res) => { + seafileAPI.orgAdminAddDepartmentRepo(orgID, this.props.groupID, this.state.repoName.trim()).then((res) => { this.props.toggle(); this.props.onRepoChanged(); }).catch(error => { diff --git a/frontend/src/components/dialog/org-delete-member-dialog.js b/frontend/src/components/dialog/org-delete-member-dialog.js index 9e3cabc715..78c853bea6 100644 --- a/frontend/src/components/dialog/org-delete-member-dialog.js +++ b/frontend/src/components/dialog/org-delete-member-dialog.js @@ -21,7 +21,7 @@ class DeleteMemberDialog extends React.Component { deleteMember = () => { const userEmail = this.props.member.email; - seafileAPI.orgAdminDeleteDepartGroupUser(orgID, this.props.groupID, userEmail).then((res) => { + seafileAPI.orgAdminDeleteGroupMember(orgID, this.props.groupID, userEmail).then((res) => { if (res.data.success) { this.props.onMemberChanged(); this.props.toggle(); diff --git a/frontend/src/components/dialog/org-delete-repo-dialog.js b/frontend/src/components/dialog/org-delete-repo-dialog.js index 2d8fd93b78..5c958a3f85 100644 --- a/frontend/src/components/dialog/org-delete-repo-dialog.js +++ b/frontend/src/components/dialog/org-delete-repo-dialog.js @@ -13,7 +13,7 @@ class DeleteRepoDialog extends React.Component { } deleteRepo = () => { - seafileAPI.orgAdminDeleteDepartGroupRepo(orgID, this.props.groupID, this.props.repo.repo_id).then((res) => { + seafileAPI.orgAdminDeleteDepartmentRepo(orgID, this.props.groupID, this.props.repo.repo_id).then((res) => { if (res.data.success) { this.props.onRepoChanged(); this.props.toggle(); diff --git a/frontend/src/pages/org-admin/org-department-item.js b/frontend/src/pages/org-admin/org-department-item.js index 09766b82b8..9f81356140 100644 --- a/frontend/src/pages/org-admin/org-department-item.js +++ b/frontend/src/pages/org-admin/org-department-item.js @@ -59,7 +59,7 @@ class OrgDepartmentItem extends React.Component { } listOrgGroupRepo = (groupID) => { - seafileAPI.orgAdminListDepartGroupRepos(orgID, groupID).then(res => { + seafileAPI.orgAdminListGroupRepos(orgID, groupID).then(res => { this.setState({ repos: res.data.libraries }); }).catch(error => { let errMessage = Utils.getErrorMsg(error); @@ -392,7 +392,7 @@ class MemberItem extends React.Component { onChangeUserRole = (role) => { let isAdmin = role === 'Admin' ? true : false; - seafileAPI.orgAdminSetDepartGroupUserRole(orgID, this.props.groupID, this.props.member.email, isAdmin).then((res) => { + seafileAPI.orgAdminSetGroupMemberRole(orgID, this.props.groupID, this.props.member.email, isAdmin).then((res) => { this.props.onMemberChanged(); }).catch(error => { let errMessage = Utils.getErrorMsg(error); From aa4d7a8936151935e299255ee38099c1d3a03ffa Mon Sep 17 00:00:00 2001 From: Michael An <1822852997@qq.com> Date: Thu, 1 Aug 2019 19:33:52 +0800 Subject: [PATCH 06/14] update seafile-api --- frontend/package-lock.json | 6 +++--- frontend/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2c35b950ed..da32793735 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11325,9 +11325,9 @@ } }, "seafile-js": { - "version": "0.2.107", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.107.tgz", - "integrity": "sha512-Mv2JIs0dw7n+cXk532dKiPU1IenpwX8a7p+xs6evXJw63CX2dnYFc9xZUl27GGB4/uQ6V2UbEw7S+EqD0wfxSA==", + "version": "0.2.110", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.110.tgz", + "integrity": "sha512-h9Nc/cnUdg4u9TVpTSDMcBZY+xY1B/HRaruZlLqu9TluoCQeXD2lij3KG+yXS1aPgAfEg2gfCMWbDGw9QneE+g==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index d99f2eb7cb..0b85f7fc3f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "react-responsive": "^6.1.2", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.107", + "seafile-js": "^0.2.110", "socket.io-client": "^2.2.0", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", From d62a5584b727cf12eaf277ab7de5df51dc9b0c4f Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Fri, 2 Aug 2019 20:48:58 +0800 Subject: [PATCH 07/14] change published wiki tree (#3940) --- frontend/src/components/index-viewer.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/index-viewer.js b/frontend/src/components/index-viewer.js index 22f66226c3..7549326bd3 100644 --- a/frontend/src/components/index-viewer.js +++ b/frontend/src/components/index-viewer.js @@ -249,16 +249,12 @@ class FolderItem extends React.Component { }); } - expandChildNodes = () => { - this.setState({ expanded: true }); - } - renderLink = (node) => { const className = node.path === this.props.currentPath ? 'wiki-nav-content wiki-nav-content-highlight' : 'wiki-nav-content'; if (node.href && node.name) { - return ; + return ; } else if (node.name) { - return
{node.name}
; + return
{node.name}
; } else { return null; } From 067b17b01386eb5b19886acff758c778bc358fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E9=A1=BA=E5=BC=BA?= Date: Fri, 2 Aug 2019 20:53:39 +0800 Subject: [PATCH 08/14] improve upload interactive (#3941) * improve upload interactive * improve code --- .../src/components/cur-dir-path/dir-path.js | 17 ++++++-- .../components/file-uploader/file-uploader.js | 14 +++++++ frontend/src/components/main-side-nav.js | 41 +++++++++++-------- frontend/src/utils/utils.js | 7 ++++ 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/cur-dir-path/dir-path.js b/frontend/src/components/cur-dir-path/dir-path.js index fbb7648c5f..a0030e73a7 100644 --- a/frontend/src/components/cur-dir-path/dir-path.js +++ b/frontend/src/components/cur-dir-path/dir-path.js @@ -24,7 +24,16 @@ class DirPath extends React.Component { this.props.onPathClick(path); } - onTabNavClick = (tabName, id) => { + onTabNavClick = (e, tabName, id) => { + if (window.uploader && + window.uploader.isUploadProgressDialogShow && + window.uploader.totalProgress !== 100) { + if (!window.confirm(gettext('A file is being uploaded. Are you sure you want to leave this page?'))) { + e.preventDefault(); + return false; + } + window.uploader.isUploadProgressDialogShow = false; + } this.props.onTabNavClick(tabName, id); } @@ -72,20 +81,20 @@ class DirPath extends React.Component { {this.props.pathPrefix && this.props.pathPrefix.map((item, index) => { return ( - this.onTabNavClick(item.name, item.id)}>{gettext(item.showName)} + this.onTabNavClick(e, item.name, item.id)}>{gettext(item.showName)} / ); })} {this.props.pathPrefix && this.props.pathPrefix.length === 0 && ( - this.onTabNavClick('my-libs')}>{gettext('Libraries')} + this.onTabNavClick(e, 'my-libs')}>{gettext('Libraries')} / )} {!this.props.pathPrefix && ( - this.onTabNavClick('my-libs')}>{gettext('Libraries')} + this.onTabNavClick(e, 'my-libs')}>{gettext('Libraries')} / )} diff --git a/frontend/src/components/file-uploader/file-uploader.js b/frontend/src/components/file-uploader/file-uploader.js index 3d1d83b743..372547bb39 100644 --- a/frontend/src/components/file-uploader/file-uploader.js +++ b/frontend/src/components/file-uploader/file-uploader.js @@ -53,6 +53,7 @@ class FileUploader extends React.Component { this.timestamp = null; this.loaded = 0; this.bitrateInterval = 500; // Interval in milliseconds to calculate the bitrate + window.onbeforeunload = this.onbeforeunload; } componentDidMount() { @@ -86,11 +87,20 @@ class FileUploader extends React.Component { } componentWillUnmount = () => { + window.onbeforeunload = null; if (this.props.dragAndDrop === true) { this.resumable.disableDropOnDocument(); } } + onbeforeunload = () => { + if (window.uploader && + window.uploader.isUploadProgressDialogShow && + window.uploader.totalProgress !== 100) { + return ''; + } + } + bindCallbackHandler = () => { let {maxFilesErrorCallback, minFileSizeErrorCallback, maxFileSizeErrorCallback, fileTypeErrorCallback } = this.props; @@ -207,6 +217,7 @@ class FileUploader extends React.Component { isUploadProgressDialogShow: true, uploadFileList: uploadFileList, }); + Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', true); } buildCustomFileObj = (resumableFile) => { @@ -261,6 +272,7 @@ class FileUploader extends React.Component { totalProgress: progress, uploadBitrate: uploadBitrate }); + Utils.registerGlobalVariable('uploader', 'totalProgress', progress); } onFileUploadSuccess = (resumableFile, message) => { @@ -466,6 +478,7 @@ class FileUploader extends React.Component { onCloseUploadDialog = () => { this.resumable.files = []; this.setState({isUploadProgressDialogShow: false, uploadFileList: []}); + Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', false); } onUploadCancel = (uploadingItem) => { @@ -529,6 +542,7 @@ class FileUploader extends React.Component { }, () => { this.resumable.upload(); }); + Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', true); } cancelFileUpload = () => { diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js index 20e3c2547a..f3e229eccd 100644 --- a/frontend/src/components/main-side-nav.js +++ b/frontend/src/components/main-side-nav.js @@ -62,7 +62,16 @@ class MainSideNav extends React.Component { }); } - tabItemClick = (param, id) => { + tabItemClick = (e, param, id) => { + if (window.uploader && + window.uploader.isUploadProgressDialogShow && + window.uploader.totalProgress !== 100) { + if (!window.confirm(gettext('A file is being uploaded. Are you sure you want to leave this page?'))) { + e.preventDefault(); + return false; + } + window.uploader.isUploadProgressDialogShow = false; + } this.props.tabItemClick(param, id); } @@ -78,7 +87,7 @@ class MainSideNav extends React.Component { return (
  • - this.tabItemClick('groups')}> + this.tabItemClick(e, 'groups')}> {gettext('All Groups')} @@ -86,7 +95,7 @@ class MainSideNav extends React.Component { {this.state.groupItems.map(item => { return (
  • - this.tabItemClick(item.name, item.id)}> + this.tabItemClick(e, item.name, item.id)}> {item.name} @@ -111,7 +120,7 @@ class MainSideNav extends React.Component { if (canGenerateShareLink) { linksNavItem = (
  • - this.tabItemClick('share-admin-share-links')}> + this.tabItemClick(e, 'share-admin-share-links')}> {gettext('Links')} @@ -120,7 +129,7 @@ class MainSideNav extends React.Component { } else if (canGenerateUploadLink) { linksNavItem = (
  • - this.tabItemClick('share-admin-upload-links')}> + this.tabItemClick(e, 'share-admin-upload-links')}> {gettext('Links')} @@ -131,14 +140,14 @@ class MainSideNav extends React.Component {
      {canAddRepo && (
    • - this.tabItemClick('share-admin-libs')}> + this.tabItemClick(e, 'share-admin-libs')}> {gettext('Libraries')}
    • )}
    • - this.tabItemClick('share-admin-folders')}> + this.tabItemClick(e, 'share-admin-folders')}> {gettext('Folders')} @@ -157,20 +166,20 @@ class MainSideNav extends React.Component {
        {canAddRepo && (
      • - this.tabItemClick('my-libs')}> + this.tabItemClick(e, 'my-libs')}> {gettext('My Libraries')}
      • )}
      • - this.tabItemClick('shared-libs')}> + this.tabItemClick(e, 'shared-libs')}> {gettext('Shared with me')}
      • { canViewOrg && -
      • this.tabItemClick('org')}> +
      • this.tabItemClick(e, 'org')}> {gettext('Shared with all')} @@ -190,14 +199,14 @@ class MainSideNav extends React.Component {

        {gettext('Tools')}

        • - this.tabItemClick('starred')}> + this.tabItemClick(e, 'starred')}> {gettext('Favorites')}
        • {showActivity &&
        • - this.tabItemClick('dashboard')}> + this.tabItemClick(e, 'dashboard')}> {gettext('Activities')} @@ -205,14 +214,14 @@ class MainSideNav extends React.Component { } {enableWiki &&
        • - this.tabItemClick('published')}> + this.tabItemClick(e, 'published')}> {gettext('Published Libraries')}
        • } {isDocs && -
        • this.tabItemClick('drafts')}> +
        • this.tabItemClick(e, 'drafts')}> @@ -223,14 +232,14 @@ class MainSideNav extends React.Component {
        • }
        • - this.tabItemClick('linked-devices')}> + this.tabItemClick(e, 'linked-devices')}> {gettext('Linked Devices')}
        • {canInvitePeople &&
        • - this.tabItemClick('invitations')}> + this.tabItemClick(e, 'invitations')}> {gettext('Invite People')} diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index a44aa3389a..c60602255d 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -983,6 +983,13 @@ export const Utils = { } return false; + }, + + registerGlobalVariable(namespace, key, value) { + if (!window[namespace]) { + window[namespace] = {}; + } + window[namespace][key] = value; } }; From affa4f8d12342e4285082368aef763cacf7cdbc3 Mon Sep 17 00:00:00 2001 From: llj Date: Fri, 2 Aug 2019 20:58:36 +0800 Subject: [PATCH 09/14] [org admin] group: rewrote it with react (#3942) --- .../src/components/org-admin-group-nav.js | 36 ++++ frontend/src/pages/org-admin/index.js | 8 +- .../src/pages/org-admin/org-group-info.js | 105 ++++++++++ .../src/pages/org-admin/org-group-members.js | 146 +++++++++++++ .../src/pages/org-admin/org-group-repos.js | 195 ++++++++++++++++++ 5 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/org-admin-group-nav.js create mode 100644 frontend/src/pages/org-admin/org-group-info.js create mode 100644 frontend/src/pages/org-admin/org-group-members.js create mode 100644 frontend/src/pages/org-admin/org-group-repos.js diff --git a/frontend/src/components/org-admin-group-nav.js b/frontend/src/components/org-admin-group-nav.js new file mode 100644 index 0000000000..ae0d728b1a --- /dev/null +++ b/frontend/src/components/org-admin-group-nav.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@reach/router'; +import { siteRoot, gettext } from '../utils/constants'; + +const propTypes = { + groupID: PropTypes.string.isRequired, + currentItem: PropTypes.string.isRequired +}; + +class OrgAdminGroupNav extends React.Component { + + render() { + const { groupID, currentItem } = this.props; + const urlBase = `${siteRoot}org/groupadmin/${groupID}/`; + return ( +
          +
            +
          • + {gettext('Group Info')} +
          • +
          • + {gettext('Libraries')} +
          • +
          • + {gettext('Members')} +
          • +
          +
          + ); + } +} + +OrgAdminGroupNav.propTypes = propTypes; + +export default OrgAdminGroupNav; diff --git a/frontend/src/pages/org-admin/index.js b/frontend/src/pages/org-admin/index.js index 4d8c0c9ed2..1f7299af97 100644 --- a/frontend/src/pages/org-admin/index.js +++ b/frontend/src/pages/org-admin/index.js @@ -9,6 +9,9 @@ import OrgUserProfile from './org-user-profile'; import OrgUserRepos from './org-user-repos'; import OrgUserSharedRepos from './org-user-shared-repos'; import OrgGroups from './org-groups'; +import OrgGroupInfo from './org-group-info'; +import OrgGroupRepos from './org-group-repos'; +import OrgGroupMembers from './org-group-members'; import OrgLibraries from './org-libraries'; import OrgInfo from './org-info'; import OrgLinks from './org-links'; @@ -67,7 +70,10 @@ class Org extends React.Component { - + + + + diff --git a/frontend/src/pages/org-admin/org-group-info.js b/frontend/src/pages/org-admin/org-group-info.js new file mode 100644 index 0000000000..57209c43f1 --- /dev/null +++ b/frontend/src/pages/org-admin/org-group-info.js @@ -0,0 +1,105 @@ +import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, loginUrl, siteRoot } from '../../utils/constants'; +import Loading from '../../components/loading'; +import OrgAdminGroupNav from '../../components/org-admin-group-nav'; +import MainPanelTopbar from './main-panel-topbar'; + +import '../../css/org-admin-user.css'; + +const { orgID } = window.org.pageOptions; + +class OrgGroupInfo extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '' + }; + } + + componentDidMount() { + seafileAPI.orgAdminGetGroup(orgID, this.props.groupID).then((res) => { + this.setState(Object.assign({ + loading: false + }, 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 ( + + +
          +
          + +
          + +
          +
          +
          +
          + ); + } +} + +class Content extends Component { + + constructor(props) { + super(props); + this.state = { + }; + } + + render() { + const { + loading, errorMsg, + group_name, creator_email, creator_name + } = this.props.data; + + if (loading) { + return ; + } + if (errorMsg) { + return

          {errorMsg}

          ; + } + + return ( +
          +
          {gettext('Name')}
          +
          {group_name}
          + +
          {gettext('Creator')}
          +
          + {creator_name} +
          +
          + ); + } +} + +export default OrgGroupInfo; diff --git a/frontend/src/pages/org-admin/org-group-members.js b/frontend/src/pages/org-admin/org-group-members.js new file mode 100644 index 0000000000..342feb390b --- /dev/null +++ b/frontend/src/pages/org-admin/org-group-members.js @@ -0,0 +1,146 @@ +import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, loginUrl, siteRoot } from '../../utils/constants'; +import Loading from '../../components/loading'; +import OrgAdminGroupNav from '../../components/org-admin-group-nav'; +import MainPanelTopbar from './main-panel-topbar'; + +import '../../css/org-admin-user.css'; + +const { orgID } = window.org.pageOptions; + +class OrgGroupMembers extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '' + }; + } + + componentDidMount() { + seafileAPI.orgAdminListGroupMembers(orgID, this.props.groupID).then((res) => { + this.setState(Object.assign({ + loading: false + }, 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 ( + + +
          +
          + +
          + +
          +
          +
          +
          + ); + } +} + +class Content extends Component { + + constructor(props) { + super(props); + } + + render() { + const { + loading, errorMsg, members + } = this.props.data; + + if (loading) { + return ; + } + if (errorMsg) { + return

          {errorMsg}

          ; + } + + return ( + + + + + + + + + + + {members.map((item, index) => { + return ; + })} + +
          {gettext('Name')}{gettext('Role')}
          +
          + ); + } +} + +class Item extends Component { + + constructor(props) { + super(props); + } + + getRoleText() { + switch (this.props.data.role) { + case 'Owner': + return gettext('Owner'); + case 'Admin': + return gettext('Admin'); + case 'Member': + return gettext('Member'); + } + } + + render() { + const item = this.props.data; + return ( + + + + + + + {item.name} + + + {this.getRoleText()} + + + + ); + } +} + +export default OrgGroupMembers; diff --git a/frontend/src/pages/org-admin/org-group-repos.js b/frontend/src/pages/org-admin/org-group-repos.js new file mode 100644 index 0000000000..90f8d25132 --- /dev/null +++ b/frontend/src/pages/org-admin/org-group-repos.js @@ -0,0 +1,195 @@ +import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, loginUrl, siteRoot } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import Loading from '../../components/loading'; +import toaster from '../../components/toast'; +import OrgAdminGroupNav from '../../components/org-admin-group-nav'; +import DeleteRepoDialog from '../../components/dialog/delete-repo-dialog'; +import MainPanelTopbar from './main-panel-topbar'; + +import '../../css/org-admin-user.css'; + +const { orgID } = window.org.pageOptions; + +class OrgGroupRepos extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '' + }; + } + + componentDidMount() { + seafileAPI.orgAdminListGroupRepos(orgID, this.props.groupID).then((res) => { + this.setState(Object.assign({ + loading: false + }, 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 ( + + +
          +
          + +
          + +
          +
          +
          +
          + ); + } +} + +class Content extends Component { + + constructor(props) { + super(props); + } + + render() { + const { + loading, errorMsg, libraries + } = this.props.data; + + if (loading) { + return ; + } + if (errorMsg) { + return

          {errorMsg}

          ; + } + + return ( + + + + + + + + + + + + + {libraries.map((item, index) => { + return ; + })} + +
          {/*icon*/}{gettext('Name')}{gettext('Size')}{gettext('Shared By')}
          +
          + ); + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + isOpIconShown: false, + deleted: false, + isDeleteRepoDialogOpen: false + }; + } + + handleMouseOver = () => { + this.setState({ + isOpIconShown: true + }); + } + + handleMouseOut = () => { + this.setState({ + isOpIconShown: false + }); + } + + handleDeleteIconClick = (e) => { + e.preventDefault(); + this.toggleDeleteRepoDialog(); + } + + toggleDeleteRepoDialog = () => { + this.setState({ + isDeleteRepoDialogOpen: !this.state.isDeleteRepoDialogOpen + }); + } + + deleteRepo = () => { + const repo = this.props.data; + seafileAPI.deleteOrgRepo(orgID, repo.repo_id).then((res) => { + this.setState({ + deleted: true + }); + const msg = gettext('Successfully deleted {name}.').replace('{name}', repo.name); + toaster.success(msg); + }).catch((error) => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + } + + render() { + const { deleted, isOpIconShown, isDeleteRepoDialogOpen } = this.state; + const repo = this.props.data; + + if (deleted) { + return null; + } + + return ( + + + + {Utils.getLibIconTitle(repo)} + + {repo.name} + {Utils.bytesToSize(repo.size)} + {repo.shared_by_name} + + + + + {isDeleteRepoDialogOpen && + + } + + ); + } +} + +export default OrgGroupRepos; From c7933c7f63711cbe60104bce61bbe2937e1a394f Mon Sep 17 00:00:00 2001 From: zxj96 <35951769+zxj96@users.noreply.github.com> Date: Mon, 5 Aug 2019 17:31:15 +0800 Subject: [PATCH 10/14] Add internal links to the dir (#3947) * Add internal links to the dir * Indent style --- frontend/package-lock.json | 6 +++--- frontend/package.json | 2 +- frontend/src/components/dialog/internal-link.js | 6 +++--- frontend/src/components/dialog/share-dialog.js | 14 +++++++++++++- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index da32793735..77baf1bb11 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11325,9 +11325,9 @@ } }, "seafile-js": { - "version": "0.2.110", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.110.tgz", - "integrity": "sha512-h9Nc/cnUdg4u9TVpTSDMcBZY+xY1B/HRaruZlLqu9TluoCQeXD2lij3KG+yXS1aPgAfEg2gfCMWbDGw9QneE+g==", + "version": "0.2.111", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.111.tgz", + "integrity": "sha512-PsaU3VUledLrs8guKRm0nJ99s0g3bgQmpvIb00b1ESjdsgRIcyvQsgCrG3fTJQYn+fMdGJaSP1tiDbuOZLoqrA==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index 0b85f7fc3f..ed0d1488be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "react-responsive": "^6.1.2", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.110", + "seafile-js": "^0.2.111", "socket.io-client": "^2.2.0", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/components/dialog/internal-link.js b/frontend/src/components/dialog/internal-link.js index b6e1f675a9..075ae4b1f2 100644 --- a/frontend/src/components/dialog/internal-link.js +++ b/frontend/src/components/dialog/internal-link.js @@ -10,6 +10,7 @@ import { Utils } from '../../utils/utils'; const propTypes = { path: PropTypes.string.isRequired, repoID: PropTypes.string.isRequired, + direntType: PropTypes.string }; class InternalLink extends React.Component { @@ -21,9 +22,8 @@ class InternalLink extends React.Component { } componentDidMount() { - let repoID = this.props.repoID; - let path = this.props.path; - seafileAPI.getInternalLink(repoID, path).then(res => { + let { repoID, path, direntType } = this.props; + seafileAPI.getInternalLink(repoID, path, direntType).then(res => { this.setState({ smartLink: res.data.smart_link }); diff --git a/frontend/src/components/dialog/share-dialog.js b/frontend/src/components/dialog/share-dialog.js index 35c6195949..7dc8a423a4 100644 --- a/frontend/src/components/dialog/share-dialog.js +++ b/frontend/src/components/dialog/share-dialog.js @@ -76,7 +76,7 @@ class ShareDialog extends React.Component { } let activeTab = this.state.activeTab; - const {repoEncrypted, userPerm, enableDirPrivateShare} = this.props; + const {repoEncrypted, userPerm, enableDirPrivateShare, itemType} = this.props; const enableShareLink = !repoEncrypted && canGenerateShareLink; const enableUploadLink = !repoEncrypted && canGenerateUploadLink && userPerm == 'rw'; @@ -98,6 +98,11 @@ class ShareDialog extends React.Component { } + + + {gettext('Internal Link')} + + {enableDirPrivateShare && @@ -134,6 +139,13 @@ class ShareDialog extends React.Component { /> } + + + {enableDirPrivateShare && From c462709b4f4a1b089f60bb6b48d0498c53081ad1 Mon Sep 17 00:00:00 2001 From: sniper-py <38058090+sniper-py@users.noreply.github.com> Date: Mon, 5 Aug 2019 20:46:59 +0800 Subject: [PATCH 11/14] Revoke invitation (#3899) --- .../dialog/invitation-revoke-dialog.js | 66 +++++ .../components/dialog/invite-people-dialog.js | 133 ++++----- frontend/src/css/invitations.css | 6 + .../src/pages/invitations/invitations-view.js | 258 +++++++++--------- media/css/sf_font3/iconfont.css | 16 +- media/css/sf_font3/iconfont.eot | Bin 3264 -> 3560 bytes media/css/sf_font3/iconfont.js | 2 +- media/css/sf_font3/iconfont.svg | 3 + media/css/sf_font3/iconfont.ttf | Bin 3096 -> 3392 bytes media/css/sf_font3/iconfont.woff | Bin 1956 -> 2172 bytes media/css/sf_font3/iconfont.woff2 | Bin 1480 -> 1700 bytes seahub/api2/endpoints/invitation.py | 68 +++++ seahub/api2/endpoints/invitations.py | 47 ++-- seahub/invitations/models.py | 7 + .../invitations/invitation_revoke_email.html | 18 ++ seahub/invitations/views.py | 63 +++-- seahub/urls.py | 3 +- tests/api/endpoints/test_invitation.py | 98 +++++++ 18 files changed, 549 insertions(+), 239 deletions(-) create mode 100644 frontend/src/components/dialog/invitation-revoke-dialog.js create mode 100644 seahub/invitations/templates/invitations/invitation_revoke_email.html diff --git a/frontend/src/components/dialog/invitation-revoke-dialog.js b/frontend/src/components/dialog/invitation-revoke-dialog.js new file mode 100644 index 0000000000..c703c154b1 --- /dev/null +++ b/frontend/src/components/dialog/invitation-revoke-dialog.js @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; +import Loading from '../loading'; +import toaster from '../toast'; + +const propTypes = { + accepter: PropTypes.string.isRequired, + token: PropTypes.string.isRequired, + revokeInvitation: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +class InvitationRevokeDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + isSubmitting: false + }; + } + + onRevokeInvitation = () => { + this.setState({ + isSubmitting: true, + }); + + seafileAPI.revokeInvitation(this.props.token).then((res) => { + this.props.revokeInvitation(); + this.props.toggleDialog(); + const msg = gettext('Successfully revoked access of user {placeholder}.').replace('{placeholder}', this.props.accepter); + toaster.success(msg); + }).catch((error) => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + this.props.toggleDialog(); + }); + }; + + render() { + const { accepter, toggleDialog } = this.props; + const { isSubmitting } = this.state; + const email = '' + Utils.HTMLescape(this.props.accepter) + ''; + const content = gettext('Are you sure to revoke access of user {placeholder} ?').replace('{placeholder}', email); + + return ( + + {gettext('Revoke Access')} + +

          +
          + + + + +
          + ); + } +} + +InvitationRevokeDialog.propTypes = propTypes; + +export default InvitationRevokeDialog; diff --git a/frontend/src/components/dialog/invite-people-dialog.js b/frontend/src/components/dialog/invite-people-dialog.js index d8e54fa4e9..0c6deb4bcc 100644 --- a/frontend/src/components/dialog/invite-people-dialog.js +++ b/frontend/src/components/dialog/invite-people-dialog.js @@ -1,9 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {gettext} from '../../utils/constants'; -import {seafileAPI} from '../../utils/seafile-api'; -import {Modal, ModalHeader, ModalBody, ModalFooter, Input, Button} from 'reactstrap'; +import { Utils } from '../../utils/utils'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Input, Button } from 'reactstrap'; import toaster from '../toast'; +import Loading from '../loading'; + +const InvitePeopleDialogPropTypes = { + onInvitePeople: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired, +}; class InvitePeopleDialog extends React.Component { @@ -12,11 +19,12 @@ class InvitePeopleDialog extends React.Component { this.state = { emails: '', errorMsg: '', + isSubmitting: false }; } - handleEmailsChange = (event) => { - let emails = event.target.value; + handleInputChange = (e) => { + let emails = e.target.value; this.setState({ emails: emails }); @@ -36,91 +44,88 @@ class InvitePeopleDialog extends React.Component { handleSubmitInvite = () => { let emails = this.state.emails.trim(); + if (!emails) { + this.setState({ + errorMsg: gettext('It is required.') + }); + return false; + } + let emailsArray = []; - emails = emails.split(','); - for (let i = 0; i < emails.length; i++) { + emails = emails.split(','); + for (let i = 0, len = emails.length; i < len; i++) { let email = emails[i].trim(); if (email) { emailsArray.push(email); } } - if (emailsArray.length) { - seafileAPI.invitePeople(emailsArray).then((res) => { - this.setState({ - emails: '', - }); - this.props.toggleInvitePeopleDialog(); - // success messages - let successMsg = ''; - if (res.data.success.length === 1) { - successMsg = gettext('Successfully invited %(email).') - .replace('%(email)', res.data.success[0].accepter); - } else if(res.data.success.length > 1) { - successMsg = gettext('Successfully invited %(email) and %(num) other people.') - .replace('%(email)', res.data.success[0].accepter) - .replace('%(num)', res.data.success.length - 1); - } - if (successMsg) { - toaster.success(successMsg, {duration: 2}); - this.props.onInvitePeople(res.data.success); - } - // failed messages - if (res.data.failed.length) { - for (let i = 0; i< res.data.failed.length; i++){ - let failedMsg = res.data.failed[i].email + ': ' + res.data.failed[i].error_msg; - toaster.danger(failedMsg, {duration: 3});} - } - }).catch((error) => { - this.props.toggleInvitePeopleDialog(); - if (error.response){ - toaster.danger(error.response.data.detail || gettext('Error'), {duration: 3}); - } else { - toaster.danger(gettext('Please check the network.'), {duration: 3}); - } + + if (!emailsArray.length) { + this.setState({ + errorMsg: gettext('Email is invalid.') }); - } else { - if (this.state.emails){ - this.setState({ - errorMsg: gettext('Email is invalid.') - }); - } else { - this.setState({ - errorMsg: gettext('It is required.') - }); - } + return false; } + + this.setState({ + isSubmitting: true + }); + seafileAPI.invitePeople(emailsArray).then((res) => { + this.props.toggleDialog(); + const success = res.data.success; + if (success.length) { + let successMsg = ''; + if (success.length == 1) { + successMsg = gettext('Successfully invited %(email).') + .replace('%(email)', success[0].accepter); + } else { + successMsg = gettext('Successfully invited %(email) and %(num) other people.') + .replace('%(email)', success[0].accepter) + .replace('%(num)', success.length - 1); + } + toaster.success(successMsg); + this.props.onInvitePeople(success); + } + const failed = res.data.failed; + if (failed.length) { + for (let i = 0, len = failed.length; i < len; i++) { + let failedMsg = failed[i].email + ': ' + failed[i].error_msg; + toaster.danger(failedMsg); + } + } + }).catch((error) => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + this.props.toggleDialog(); + }); } render() { + const { isSubmitting } = this.state; return ( - - {gettext('Invite People')} + + {gettext('Invite People')} - {this.state.errorMsg} +

          {this.state.errorMsg}

          - - + +
          ); } } -const InvitePeopleDialogPropTypes = { - toggleInvitePeopleDialog: PropTypes.func.isRequired, - isInvitePeopleDialogOpen: PropTypes.bool.isRequired, - onInvitePeople: PropTypes.func.isRequired, -}; - InvitePeopleDialog.propTypes = InvitePeopleDialogPropTypes; -export default InvitePeopleDialog; \ No newline at end of file +export default InvitePeopleDialog; diff --git a/frontend/src/css/invitations.css b/frontend/src/css/invitations.css index c79a33abe8..34d67ad870 100644 --- a/frontend/src/css/invitations.css +++ b/frontend/src/css/invitations.css @@ -21,3 +21,9 @@ cursor: pointer; vertical-align: middle; } + +.submit-btn .loading-icon { + margin: 1px auto; + width: 21px; + height: 21px; +} diff --git a/frontend/src/pages/invitations/invitations-view.js b/frontend/src/pages/invitations/invitations-view.js index bc453082e0..fa225472ac 100644 --- a/frontend/src/pages/invitations/invitations-view.js +++ b/frontend/src/pages/invitations/invitations-view.js @@ -1,103 +1,149 @@ -import React, {Fragment} from 'react'; +import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; -import {gettext, siteRoot} from '../../utils/constants'; +import moment from 'moment'; +import { gettext, siteRoot, loginUrl, canInvitePeople } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import { seafileAPI } from '../../utils/seafile-api'; import InvitationsToolbar from '../../components/toolbar/invitations-toolbar'; import InvitePeopleDialog from '../../components/dialog/invite-people-dialog'; -import {seafileAPI} from '../../utils/seafile-api'; -import {Table} from 'reactstrap'; +import InvitationRevokeDialog from '../../components/dialog/invitation-revoke-dialog'; import Loading from '../../components/loading'; -import moment from 'moment'; import toaster from '../../components/toast'; import EmptyTip from '../../components/empty-tip'; import '../../css/invitations.css'; -class InvitationsListItem extends React.Component { +if (!canInvitePeople) { + location.href = siteRoot; +} + +class Item extends React.Component { constructor(props) { super(props); this.state = { - isOperationShow: false, + isOpIconShown: false, + isRevokeDialogOpen: false }; } - onMouseEnter = (event) => { - event.preventDefault(); + onMouseEnter = () => { this.setState({ - isOperationShow: true, - }); - } - - onMouseOver = () => { - this.setState({ - isOperationShow: true, + isOpIconShown: true }); } onMouseLeave = () => { this.setState({ - isOperationShow: false, + isOpIconShown: false + }); + } + + deleteItem = () => { + // make the icon avoid being clicked repeatedly + this.setState({ + isOpIconShown: false + }); + const token = this.props.invitation.token; + seafileAPI.deleteInvitation(token).then((res) => { + this.setState({deleted: true}); + toaster.success(gettext('Successfully deleted 1 item.')); + }).catch((error) => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + this.setState({ + isOpIconShown: true + }); + }); + } + + revokeItem = () => { + this.setState({deleted: true}); + } + + toggleRevokeDialog = () => { + this.setState({ + isRevokeDialogOpen: !this.state.isRevokeDialogOpen }); } render() { + const { isOpIconShown, deleted, isRevokeDialogOpen } = this.state; + + if (deleted) { + return null; + } + const invitationItem = this.props.invitation; - const acceptIcon = ; - const deleteOperation = ; + const operation = invitationItem.accept_time ? + + : + + ; + return ( - - {invitationItem.accepter} - {moment(invitationItem.invite_time).format('YYYY-MM-DD')} - {moment(invitationItem.expire_time).format('YYYY-MM-DD')} - {invitationItem.accept_time && acceptIcon} - {!invitationItem.accept_time && deleteOperation} - + + + {invitationItem.accepter} + {moment(invitationItem.invite_time).format('YYYY-MM-DD')} + {moment(invitationItem.expire_time).format('YYYY-MM-DD')} + {invitationItem.accept_time && } + {isOpIconShown && operation} + + {isRevokeDialogOpen && + + } + ); } } -const InvitationsListItemPropTypes = { +const ItemPropTypes = { invitation: PropTypes.object.isRequired, - onItemDelete: PropTypes.func.isRequired, }; -InvitationsListItem.propTypes = InvitationsListItemPropTypes; +Item.propTypes = ItemPropTypes; -class InvitationsListView extends React.Component { +class Content extends Component { constructor(props) { super(props); } - onItemDelete = (token, index) => { - seafileAPI.deleteInvitation(token).then((res) => { - this.props.onDeleteInvitation(index); - toaster.success(gettext('Successfully deleted 1 item.'), {duration: 1}); - }).catch((error) => { - if (error.response){ - toaster.danger(error.response.data.detail || gettext('Error'), {duration: 3}); - } else { - toaster.danger(gettext('Please check the network.'), {duration: 3}); - } - }); - } - render() { - const invitationsListItems = this.props.invitationsList.map((invitation, index) => { - return ( - ); - }); + const { + loading, errorMsg, invitationsList + } = this.props.data; + + if (loading) { + return ; + } + + if (errorMsg) { + return

          {errorMsg}

          ; + } + + if (!invitationsList.length) { + return ( + +

          {gettext('You have not invited any people.')}

          +
          + ); + } return ( - +
          @@ -108,100 +154,74 @@ class InvitationsListView extends React.Component { - {invitationsListItems} + {invitationsList.map((invitation, index) => { + return ( + + ); + })} -
          {gettext('Email')}
          + ); } } -const InvitationsListViewPropTypes = { - invitationsList: PropTypes.array.isRequired, - onDeleteInvitation: PropTypes.func.isRequired, -}; - -InvitationsListView.propTypes = InvitationsListViewPropTypes; - class InvitationsView extends React.Component { constructor(props) { super(props); this.state = { - isInvitePeopleDialogOpen: false, + loading: true, + errorMsg: '', invitationsList: [], - isLoading: true, - permissionDeniedMsg: '', - showEmptyTip: false, + isInvitePeopleDialogOpen: false }; } - listInvitations = () => { + componentDidMount() { seafileAPI.listInvitations().then((res) => { this.setState({ invitationsList: res.data, - showEmptyTip: true, - isLoading: false, + loading: false }); }).catch((error) => { - this.setState({ - isLoading: false, - }); - if (error.response){ - if (error.response.status === 403){ - let permissionDeniedMsg = gettext('Permission error'); + if (error.response) { + if (error.response.status == 403) { this.setState({ - permissionDeniedMsg: permissionDeniedMsg, - }); - } else{ - toaster.danger(error.response.data.detail || gettext('Error'), {duration: 3}); + loading: false, + errorMsg: gettext('Permission denied') + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext('Error') + }); } } else { - toaster.danger(gettext('Please check the network.'), {duration: 3}); + this.setState({ + loading: false, + errorMsg: gettext('Please check the network.') + }); } - }); + }); } onInvitePeople = (invitationsArray) => { - invitationsArray.push.apply(invitationsArray,this.state.invitationsList); + invitationsArray.push.apply(invitationsArray, this.state.invitationsList); this.setState({ invitationsList: invitationsArray, }); } - onDeleteInvitation = (index) => { - this.state.invitationsList.splice(index, 1); - this.setState({ - invitationsList: this.state.invitationsList, - }); - } - - componentDidMount() { - this.listInvitations(); - } - toggleInvitePeopleDialog = () => { this.setState({ isInvitePeopleDialogOpen: !this.state.isInvitePeopleDialogOpen }); } - emptyTip = () => { - return ( - -

          {gettext('You have not invited any people.')}

          -
          - ); - } - - handlePermissionDenied = () => { - window.location = siteRoot; - return( -
          - {this.state.permissionDeniedMsg} -
          - ); - } - render() { return ( @@ -216,22 +236,14 @@ class InvitationsView extends React.Component {

          {gettext('Invite People')}

          - {this.state.isLoading && } - {(!this.state.isLoading && this.state.permissionDeniedMsg !== '') && this.handlePermissionDenied() } - {(!this.state.isLoading && this.state.showEmptyTip && this.state.invitationsList.length === 0) && this.emptyTip()} - {(!this.state.isLoading && this.state.invitationsList.length !== 0) && - < InvitationsListView - invitationsList={this.state.invitationsList} - onDeleteInvitation={this.onDeleteInvitation} - />} +
          {this.state.isInvitePeopleDialogOpen && }
          diff --git a/media/css/sf_font3/iconfont.css b/media/css/sf_font3/iconfont.css index f858ebce99..fe3e68aaa4 100644 --- a/media/css/sf_font3/iconfont.css +++ b/media/css/sf_font3/iconfont.css @@ -1,10 +1,10 @@ @font-face {font-family: "sf3-font"; - src: url('iconfont.eot?t=1562919437059'); /* IE9 */ - src: url('iconfont.eot?t=1562919437059#iefix') format('embedded-opentype'), /* IE6-IE8 */ - url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAXIAAsAAAAADBgAAAV6AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDOAqLFIhzATYCJAMYCw4ABCAFhG0HXRseChGVpDuR/UzI5P6wvAd1auyv0yQR3c8g2tLZEzh2sRf8VS5JyxMxh5jo0Tq0jkfErQbAfftlu6bW3SBMhNnuztJ89b+87Pb8yYY2HqdRCiFBOBoggIDsH8de/nVe5/nuvn+wQs8IzbKmD7gbWERblPBmBUxTnIlt9giyeQWz4DN5lcuBAKBIgQxSo1YDNyQIkNsRAKRf756dIVX0EHqwBJJKxRwqgPwDERL3O3cYwN/J55NHcBIJ4CDykFdq0qNmN1S5S+625JVvCtrxCpTLWQH4RwE8ABmAAJB+rL0H+EwqJ3LKZQsoB0BSa3HAXe6u+27Lb9+IQO42F75KXAKFFv/yOPAQIAJEBSnXQtmAEiJwlxgZEgUHGaBoDi14KNzQQoCiJcogAgBUF9wkGSC3Ac4MqZRpGw1E8JAgGJGRkGgytdJBIkCiVh5qTNXh93Rk/gWAMp9S30JCvItaV9nRMWo7DO2rOffdBgZMbMEKW3TDumX22MKcuQcLQi0D1sjyvMACi2XRodx5y6ZuzPIFWkQUU5DNOZAfXGQ2L1xpj7W8fyDg3l5j9mJbMGha4HeEyaRVhWFzyNfj9Okmm+XTZzzRLfluBOeY/GHGclvO2cPOtZy70LH6dCk6NycUSKRu/U4hYrE4HOGYeWPxnWeLlpXfdq72upJ7zlQMLrFFZwDz1phM084WX1rp5NkWe3sSBgYKStTDsh6GNSeShcnHXfq1J1PEaac8szdbheiWbMO8bQ4xtjVnKAVrCUa6EYqXjS4ihPaoS7nyFykL2EjiXUj1vvlA20s8d3/fQrvoDsymc7favZMsLcPzmD/fE528eHVBCyE6f4yyhen827xbc7SzF7fWzcu8mxHdsqVd5l1Ktb7FKy+Z9m2uAQYzll5bGybJrT14pF8v9lfAtP8iUb757KQRNY90614lyM2mTveZpk9IvRkZ0YxIpqveSU+PQz1a0LXTQ8/lr9fiZ9szIxm/O6vNkYfX8vy0Ia3N8tOeHj3M/bpBvv45od70ot9+J7TpxXrqH8tznoGZ9emM4jV2/fmdtp2OlVjOd+H5w578D2dSG6CBpXSj+LaNJTfdtb/z82uhZn4G/51YVm7W3Val25RN89jEgybDb32d51bNutfb2evZD0PW7XVordUPZVh1jfmBngylUh31b7gwJ1A3HNacdKtA2cnb/6mh2a6p8Y+7Rd1K04uoQoumK8KDgc+uyzvXvXZ9w3Xu8a7ruwqiBqKftWFo/ubrm3W7Ns/SE8P+Uv9H0gXcov9N4qXeER+rWp+ypl2G+EqzQXFqref3Lff2tmJW57Z7uwwvOjsOd5ed8t2QB/YEa0Vrgit+UMV/Xpb9a+yg+JI5ly7lsHEJpbYNuum6Zd1meWW3vcQ4yDV5hQfw7bF7vCXRbKc6qyC5lX8p0y5tBrFdxUcdDRXeSGz1WrrQqKvO8X3bCjDjBRZCcBDYnMD2wOrSgzSoxjkhlAgNfxRFZQHNNhyAogA7kwbi/V1KaRoth/1RmgoOnGrJAJ4kCyFgZBAxLlCRVAaq0P3RTAJ1RKYBUOBLAGH0DRy9bcAz+kEImF8Qmf2DirEjUC26OiWT05BdK5whkv+kIcjKOseyjds8LX1A9zo7LYsS4gt0G0JUF1U9dY8Veok9treuMWNilYXuxNUwz0JJZUSwojdLl7Jk7g5FkGWXqU6BYyAi3mdEPwjEincymaFYK298/APovJo52lPzYH+BEfIBqye1QgXEvXwdVfNROMh+02kYRRlhv1gsyB1ZYVZxQRK/0QgCU+hnRJOL0unFY+XFSyqXf1kKNCDvpIojPBGISFREDXZzfkY+SydnvM37adJhQ/6O0Ds7uhhPLLqcNlHb7QAAAA==') format('woff2'), - url('iconfont.woff?t=1562919437059') format('woff'), - url('iconfont.ttf?t=1562919437059') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ - url('iconfont.svg?t=1562919437059#sf3-font') format('svg'); /* iOS 4.1- */ + src: url('iconfont.eot?t=1564546243284'); /* IE9 */ + src: url('iconfont.eot?t=1564546243284#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAakAAsAAAAADUAAAAZVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDTgqNDIpEATYCJAMcCxAABCAFhG0HcRsdC1GUTlKX7EeCbdtghVejLpYjGdqh2RIKfanCHUG19m/PHv69D+wAyLMlRYlNRREID2RsjIrREYaNeFdLtwmtC8WN/+8uitZUquTb3uXyZX82mzYepzECjZAgHJLhvzN7k9kUb6JFvKLqoEJ2CmfbHjRpndw5rrKigzvX+Dr//bHSzfG1xXwbVmCFXvH/38i6aMuwpgM+OoxoixQsOz8wOsDTkM5mYsGHbwAuJ5BNc3TOH96GsK/E7YFOT6xJYT/lVdawQuuoSw4txTdkbXqfZoGv7vvjP+zHPklV4LteP74v4OoX8itFiR5hp6Ewri+M3lEU2ARK4l1p9IWtYNt0KJftsu0cyFpJ+iV6Dh+JH+37v8d6HYPYckTlrN9guyUhKdp/eJWaqHHUD+ZdreZXR9k53IUd8ABuBfAQbiXwBG4VcArsrNKbnoOYA2kP5BOYh9U+leLuC5TDXNrWjsncesI4t7I2IpWHL7+Fit+FwqY47sEUVQAQlRDKqxFEVrMtrQlV2Y6pwLp+l323k0omVtXE1XV1NPD01Z7l075qsZKjbfRWVrHZNTNeFQ23ut3lSpGWYKqwsikfVQ2LVd3M0xe8v1IpGEwvreWqVMwqBV+DXG/x07DUctxo3NjrbzQJdX0+AqAqYyo0GOYlLhvDLOLyan6rMRKWe6qVdlAg30mNsNl8vkbP6g4cNoc1xA1YsjoixkwJqjqurgiAijYm87Y5sD5x2SwaP4hgAAMQQETcE4bT25YcrG4shtPalx3Jt1eEpb0cK12fB71igE/W93sWQoCJUUYkCARHW65BEIhnbaTkDxFV2AeIrBrS5JUA7FjHcx+XV/PIAmUpLO/nya6zxZoKTOEj1N2obfUVWekqPyb6MKpiQNbvSSmt3UatCOleoOsbijfI6iFFXnuWuk+MRoHJJLRYxGbzziCZGsJMGOiqlltTgwSqzY4llgKzWVSnI6xiNO7QyFhqO4FIlZUEUyjQKRVbLISuaGWRwnL7/3I6oGNGNEpmKvad/htP639r+y+A299rdeiWUvtzGXOSgmQVqRSGCkybXiK5Ra46V61beO6yEJ/BRbDdoH7j/98fNqU8N63rtdDUMv+zmcKLXc7bG41CHGcdkwD/O/+M5RrCrl5D4Kafc1dfiCMJT7rlwaLA9JGvgik7qVhQI7ofRWeFPn+anPJBPjtqvc2ODWsEcGRy35szqwwfVzSYHOO/uYCbLLnZU4F9Mc2kXz0aamkpeXw49NDr89KOcT6FkzbjyqFuQE8KXYnE7NVXwU9lyhyNRmr9VsqYG4Pfpq8dXJv+bWeLnERDGCRgmIFgKXDy9R1+55w/7nTdIb0YuTPiq6MjtJKuQp/eO73Ukd4SGkKfjPzB2KrKFvuBSV7XO4K/UjivsE37pfIo7JT1aorwWt/j8a0YJ3Tg8Qj97Z3QPWcFMTeDpU95tpwEjm24zamEb9/FfP3JKZuISNe+DZ9Lso0cOHUv/D5ngP2ex30H7Ev2SpBLlxCJYF+6KCF5Ll6M7+viJfcDf+c/KIMfkgwG0ociVGtTDV9S/va0y6SlMjasTRKSTkuRLWAXSreQkBS3PSXlUg4yJK9+z8nfkP/7K5xRkJOXk4NbS17+lpOfnW90FVrbbm1KGvgc8DVRAgUA/P8CfYyynWsQFaOTwe1H36FudQNqRPd+fTeTx8OLg23vZ/H/bfeRXvLTRHy10j1DCWgIvoMY2fhlzZUIH9FxcF9TlsWV4XorA5Hf+G7tBxKyjK9pctTv+sgEGfk/IBMxJI0pKLQWqSVzEyqdQ6i1jiDb8Hh0ZwzbEmUf1n0wCMM+IRn0AwrDvqkl8wcqk/6gNhxbkN2E7Sk7y2EP0xhTwUJlG2VzarRsJYeFOnZ95z6aRZVy0uw4Y+S5LagwMDjbeMQGeR17zJcmEtFKM9XqgV0Nq4pUyzTDXLypSNvzg/CyPb2cajh0hqGUGMmCktkoWjnS0DuJQ69pMTf39X3IWKikuKbfSXQMCchi+UrIE4D6iBpSv/uiRcslIyLtsKZUmZE6kIflwqgSVaK05RvNoJzwTJcot3r81niairyjJPXtbINB8Ds2kaKIMqqoo4lW90jSrEK3IkMXeDofkFu2c3RXmE9T2dpXFNs4cb1dSCwXedrkWLm2WVpJ5fJ5AAAAAAA=') format('woff2'), + url('iconfont.woff?t=1564546243284') format('woff'), + url('iconfont.ttf?t=1564546243284') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ + url('iconfont.svg?t=1564546243284#sf3-font') format('svg'); /* iOS 4.1- */ } .sf3-font { @@ -35,3 +35,7 @@ content: "\e657"; } +.sf3-font-cancel-invitation:before { + content: "\e661"; +} + diff --git a/media/css/sf_font3/iconfont.eot b/media/css/sf_font3/iconfont.eot index 3748273478e3240b64eb716d5a23170551fa9af8..7563a842365b1002cbc46bf4dc236f7ad346523c 100644 GIT binary patch delta 723 zcmYLFO=uHQ5T1E&cVC*OyJ>728%vFU$-(r8Hcc+l77?YP3ze2CQZY-iHlacfbu9~8tKk& zZ|{=eD`hN04bkBDiNB*foynC}dXO04K0(joQp%9;iEA`pr`nq{Rtgw^ZTf$d>R{f; znXg|4eq07{&jGMpVX0V(yx)EX;Q2^`IbZ$QO&SO^$GJEWv2^J0Pe5|oor z01K_@f84gEK+<_YrO)P!JS31op+b3u9DUUG*gClCAT6)zl}39kgWM=>r4u{2!x)=m zd;H?oob*(#(94>ee4i(w6;47wOp>LFq8I(681F|OkA}|Ljm};iJ~0#<3HM`o6w$#r zsZ=}?i-toQhC@Q>IF1DVt}*m?#beO{9E#zdxz5Z>)50Uy3`YeC*w zLAf2>v$q&4&CC=TTMh`Jsfj676C#mt_=KhkG0}3KCmkEk*GzN$_w$I`%WJE%9o?!Y zA}$RKPyMO$B6)BJvCZX`HBHwwO?Z{bglLylRo7LO+Pb}V3Li9J>sQMIrU_)UL&*wg24PZ{}?x!9M@m$N0KlwHa@Lw^BOYL1Nn delta 422 zcmYLFJxD@P7(MsC`>y{=)B`ODp+;LBD$21Sk|=U9^5IVozBF<$2x@2uT9RAP7J<;z zWP*c*Q$ur0OGLw?p`oFMmh5{v^uf8`Ip00!8@~6kZbWwdK-gcGagofVje6qbU=d*Z z0M@gXS!w>XLZm+k?XA*YVQ0oV1hiW~KP%?VoYCEAlS*x%JTJ#?#9o;sq{PpGN*9zcS0Jv2t?^bU<@7@4kn+zK& z$0p__oRtJ4ok?znX20PRGytBLvw{EKyBE+Bq}|}ol*-9T^}w!JN)>rbUGU*lsXqk@ z(E2P!-AmRI1g6RF9^`Fqui2?y53rfNI$+L9%DBl-HzdX>E1(mVYDoK4QB7ANAw5fd sCtXa9i@6|)HfwXv)BF)=*2KBq)P1KsAQ@*G6HI54xl|)CMVqMp0DityI{*Lx diff --git a/media/css/sf_font3/iconfont.js b/media/css/sf_font3/iconfont.js index 76f5196568..018147f11f 100644 --- a/media/css/sf_font3/iconfont.js +++ b/media/css/sf_font3/iconfont.js @@ -1 +1 @@ -!function(h){var c,e='',t=(c=document.getElementsByTagName("script"))[c.length-1].getAttribute("data-injectcss");if(t&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}!function(c){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(c,0);else{var t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()};document.addEventListener("DOMContentLoaded",t,!1)}else document.attachEvent&&(n=c,o=h.document,l=!1,(i=function(){try{o.documentElement.doScroll("left")}catch(c){return void setTimeout(i,50)}e()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,e())});function e(){l||(l=!0,n())}var n,o,l,i}(function(){var c,t;(c=document.createElement("div")).innerHTML=e,e=null,(t=c.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",function(c,t){t.firstChild?function(c,t){t.parentNode.insertBefore(c,t)}(c,t.firstChild):t.appendChild(c)}(t,document.body))})}(window); \ No newline at end of file +!function(h){var c,e='',t=(c=document.getElementsByTagName("script"))[c.length-1].getAttribute("data-injectcss");if(t&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}!function(c){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(c,0);else{var t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()};document.addEventListener("DOMContentLoaded",t,!1)}else document.attachEvent&&(n=c,o=h.document,l=!1,(i=function(){try{o.documentElement.doScroll("left")}catch(c){return void setTimeout(i,50)}e()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,e())});function e(){l||(l=!0,n())}var n,o,l,i}(function(){var c,t;(c=document.createElement("div")).innerHTML=e,e=null,(t=c.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",function(c,t){t.firstChild?function(c,t){t.parentNode.insertBefore(c,t)}(c,t.firstChild):t.appendChild(c)}(t,document.body))})}(window); \ No newline at end of file diff --git a/media/css/sf_font3/iconfont.svg b/media/css/sf_font3/iconfont.svg index 05920c8701..112d096f78 100644 --- a/media/css/sf_font3/iconfont.svg +++ b/media/css/sf_font3/iconfont.svg @@ -35,6 +35,9 @@ Created by iconfont + + + diff --git a/media/css/sf_font3/iconfont.ttf b/media/css/sf_font3/iconfont.ttf index 05c8fb395397d0b7e5c091d1b0fc222bf594b33c..916f7ac43d63b3c9483dfe08e4f18058b38879fe 100644 GIT binary patch delta 707 zcmYLFOK1~O6utMo%)De0XOh@9HI!JbCJWOP`bjpDf{4=4V5K2`sF)!gn_zx5A+#cN zp$#Z5ltFYM+O7mQE^48RA^~?r#6>r5imSnm;6lNT#ybPz9nN{@-Z}R$^PUZM!;ePR z<^k{y0OX5Ry|H-kf+T(?d|axm-Uwd1?E>JR0OW@yLtpIw_U0}bz7Ucn8i)gapY$%_ zM5)?Z8A4Kk`$R*PdQq3|iL12VrQToFR~i_GZF)aQ{cugM8n0f4e_RAm764eaQE#^5 z@3x-;_&$(f`SI?^v;aGia5L+fOMI?XvfT8Np4| z%`V2xuNczX&J>^l+);5l6|%O$1!u{xIQz_ER?fO)>AfC+h1UE(ZO2j}`Fx<#V{1kk zE|5T`MmBZ!%WN>O=5w5v9j41OOFTVf=@dt>txoOW4r6SA?eTM4Z}}5xjef1S!S{Iq zyl@;6FiV*#iU@|%m`Wf|O-4`IgZ>Ck92-wg#S$1xBRZTSmx_mzld-6Vv8a&yjv`TL zU>ZXMspRAsjwf-?=rGnXjCID=PY$cT?&>DTa1dWB+A!~5Ln(kt{uX1cT&~I3a##pW z&CIEq5SfglXEara8P6GBaAG)LH;m5j=W)f)H&^q0M^s;2oFAK*`_tu3%HR&;A-7-B zv}{(>gkPS`h=8Q3*{rJ4IBVZKRS*tf>w@P2(*!c)=t2o)6zDcB?`SAc)N4hfGFq;+ M%PqZCuGgHUzwoq+>;M1& delta 409 zcmYLDJ4gdT6r8trm;a|R5L1W{%h+1@FBL=(#WV?e#KaUi6ER2xK?@7ZNEGZu5Ui}u zK#C}-tt@P8L@ch+!a@r{h;vEd!t9%wH}CCs66f)H@?aj2eE_RD%dFTx%_#K;(b_ES z6}Bg=LqNL)jMHM?+=#UUF=k(hu_6uci*`)jCPs=@wH6mL$P=%4shl&7BYlALSJV;9 ztW{(XP4?r|!&|16zrIL(Edl;HKv|XYPW9&V?hOdG7|5QqlQSN;8;MwE>U}Lb`Hi4X z19)D{h5u{!UWYbI?Spb%9(T^u7?Mj#H#(9kP#T0rqzG=2QZED)=(_O~YoO=r7^n92 z*Ez~ranju`;E1!_XY9I9_?Qp%nBuGs!(LZm@h|k+e6X>rO-CqN(o#xg)B|8YYG;O> X4b)ZGBT6*U#So8p8^57Z_k+M6(8W(m diff --git a/media/css/sf_font3/iconfont.woff b/media/css/sf_font3/iconfont.woff index e8aa715e1dc6cf8dd86789cab5c5b6b03cdd71c0..14c28f5dd280d9d14c1ce0f25c1a3b92406a5b9e 100644 GIT binary patch delta 1715 zcmV;k22A;+5Bv}mcTYw}00961000Pl01E&B000d@krY3FQ)6vmZ~y=SfB*mhdH?_c z&Sc)60B3A@W&i*I`~Uz1NB{r^j1paX1ZZVpWB>pLL;wH)F8}}lHWlr!gJ@`FVE_OI zbN~PV9smFUBnRFDiD+$fcmMzflmGw#5&!@I92@}v0Bmn#VE_OIr~m)}5C8xG5Cwz> zT5Vx?Z~y={2DktK03QGV03ZPx0JUymZDjxe2G{@q0Zae@0&Su%}(z}WrD$NP~dhRL*O z)qb9!qWM?fDt>R`&^CylMP3^?kd$786Wj; zEXclyptT|tA6&uhli3Qz!K-c^-sbeepuXkNPIBg z`=9kTujz)N|C52~!`%BO{r7(uxsRuJ5BCo6d@cynoDh+EYDs)>6S#7-y_@t~qRs$5x5@%f4Kz@q&u|pD7YpQV z#x>g!1^LBef*3el6%0c)u{BAi@O}RA&_BBL=^1}(-grq>3c%EkEJ8WoFcnG2NfdG& z27;`bwrmLX?(x+Vesea==W_XQw&0oNq^d|ch0QLvkNV*#FA)^ly=}J5P7#GnlP)0; zg{83MmAuG{LTKT>8#ufc?zb@n2VDc97LLV02)_zB z?J9p=-T%V1D;rrRd~!dfll?Nn#l6)Bk~8_||K`L9CsTMbZBiovNsyga3X@V8LFAQC zjqyqFGRVNtvO|alzSnZa8kh%&(>Hc^7q6##yU_VaH?F52+_-VW(CKYM-+Mtf>;vrK z=UseKb7!ZhU4_nWdUxllR;01Th z6b5l7oUq>jTcbTNY!Q%es~->iprs+*A?$l7j{P9(0|WY7xdE?8)>PRAQM#$xQ)O#g zg=Wt>pR4@-t6PSXW!EjIJC){R@Xo|tE4#F1!B^!*qntkBG->m6W12p9aVigPS+RfR zJ6tmFSVvtj-`@M8Ql~#w+UqD$^%AtoMT{QU|?WpSjoW200K-v%msuD z4FAD=1^^mY0nh+=oMn$qP6D$N21fx_ZNV`PfFKM2z*bO*| delta 1476 zcmX9;c{J1u6#mUvGxkgkAu%$;vlhm_JWEu!LuJYH z9yMcMqezVGL-BYt%n2uj_kMc!obTLwzx$o@-GA=gldO=CiFdNI17H9YLKOhwFpHw- zG8UkNKbaT-0I+0;Aw#*jakoi=6q*nS)tdmocNYKzvLj8%$wAkN0Z>hWblMOMkW3L| z5JW;;71S$2Knut$i zC73MG6h0yFnU=1H7MavKAS_Fh3AM3i~?WF4;BH zJ_5)D@(;j(k9DFZl)}&S=_AQHqP9-K8p>IV{BJoW)`pvdEsg2R3#hrM*O#oZGk_Q> zW4ubqFm>%ktqn-XO1j>7^M#A$ufEy9YcBvd&2PgAYAS4Nd!JsVsqV;~ z`YG^SV(!U<;r-g#dQ8&+ma~|!dINKiD3;>FY`&G05yAT2=kM-i$rUbQi<7)4!s(!-o;Bxf?O}ehUSo)(7%NlDhrP* zrPwm+`}W?p!6VBrRX|ed*dO|yDvncuwNhs;{mb;w4|yYHk8hYm=a{?1f;eKWGDbwf z17E#br5@Zf;RH4-wy>yp{{h_*HqH9n8kGxHL~qx)954HPE~F{D2JLg&MuzrG_@SPg z7u!=GT+JMaNceU$)a=T;Opke`REJs2E@7>~8y`KIJ2+%so)JMF{UULX{;~8a<~vx; zdHMNSkC*$Xk4N684|q}9m9Axb7kwC5WlFaR8NMAk;9jbI;wF-&I})&^uHfma>Wl<0 z%S&dT?Fy93nd(BAoN$iER^Rngb+1YPGtV#U`=V$V~<%u&T)2T2T?$S#p z_cAS#5T-L*sqGqBaC*>apBfG^OT}2SJTxvD%YjRcA+Kym^zAL{GN-cN$hh+ zj3mYH;LA1l##Xj_Px-ll%*?&^wrZ&J!rd6O__xCFA)fE8XWiv2&Y5m`$PZ#D}e{->eTD#B;P={3j+44 zaOgupb0UF2fD!F5Vm$yX9*~1>{xD>kVa;$j+zbBthv?9h;Lqp~Jj!PZ8*ZEsbb_7t zC-@};T7v7znRFWc5uHlUp=Z*IQcRwpb+KxALp2LCUmg8AbW_!Dm_{7_FH?OZm4-AU YT~(^8W*a8&39hXwEd>TVT$eoiA37X`TL1t6 diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2 index f15e9bf5433fd78bfb729b31f77579771624c70f..a5639786791efe39150d7a9d65154a6b7bfc5470 100644 GIT binary patch delta 1691 zcmV;M24wlj3#1JgcTYw#00961000K001E&B000d@000J6kr*C-P6~|-ibMf60we<* z3lIPVAO(bN2XPx63sIC#QkU#Uf^FMig;%34mLnP3q1h4&eW}77LACa8&mR8059|QQ zvn54|O+^t14`ghNiqR2~JHk&pcr!fMJkRTkA z;lDF`li3t+qD8!aqUxYRb_!?P9yDp4+&QkYqJulv_&WdnvC_@)wZ(iJR$zr*@&Dh* zx@gO=rUO1YjA)A#EIU6iI>6JA&Sql4hi?EbCy-5Zbj}~%hOn>1?SW2DtVv-%yKZsxe z0}wz&+!Kb=+m;-xZY+k=+Ls;4w^xLvy&NlDp9j5SfKS%l4Qm8&WOCn|Gph_AS2UO$9A=3SEiB?A2GnPi4v+jYewy zR?3(ggWsmF9bB9KZD)&|L_-)PV3>h_U@36&^&NiaoImb#-66dgxijLcqa(DEu0o&Q zJH6DA-Vz!@pN#y$*s97F`+-Sb*E@p0QaG=$P5Y(tDD0H=YKrh$-{bME7$6-$9)Iv zOZG~TOP3IdU|+f@A$cxdjPL8>CHDh==RYWeACe3P(uX2cYf}whQvUX|%cP|;Hmpq& zl1@vJEnpW)w~&w&w>>50QVudCuYS+T-;n?NE1Xe~lb4f&TT5R4mXn{IZ*&z}+iq=2 z8u%RWHBkZu0Q~;~eT=f5YY-KGqm#kyN8h1tbpvWd_kMjhlgGo0gKh6;@&9f2kzVrC zM0~ZDJZlSsU>q0DxE{9wT>X$@OX4*4BHZAAJ%;^2*W-h8T11R!@jA1CG!U=lm4JK z99xjxgzYIi%V7`G7*oMQWgBI4YP4)6hlM(Jedj)!MU`^W>^NiOxh<$LFgV+I#0GgC zd(4+6646RC)#`y=4XcV&%QVAWys1dr^MmlRJ-wW2ICO@glo-i^lG!L)InvNOi9@ex zF*o=1eaKj-q`0QvNpuW)3CLpkO2`v{`iL4*zx$#^%OxWs?Xad)nUOk>hvmYk5>=wK zA6x%p)yn+a002R4GX($u delta 1470 zcmV;v1ws0x4af@^cTYw#00961000HZ01E&B000aa000Ggkr*C-I0}mth;sop0we<% z3l0DTAO(bN2VEN;3K5m0JCXfN$mIU8ya#n^?61=#5#7%q+S1t*ICilY{8cVV%M&q& znCQ_uw2nu_tp>n-`(?YP*4-dXgxT)S(tP#5yzKV;$%Zx_r%?(aK{y&9fPm~D$6o$* zUg!DlzJIVn&j`(bvZfEX8(2hJl<;N+OjDeRZT1MVc?GlJGkKNEfdHULfecckRs%OA z2$0(m0Hj~X{94dhP0}&GUJ9iks-^u5bM{p7e;2`plS8CFuW;ay1 zOYXMhm2V2#@d}j7SpoP*0XzU1fB=wwti1>LOe!aG%4G|GC<&e_AxwVb=qvHEl=IyWa2NThE4P9b7{u{a#zn0aIIoo9=dmEU%*_PUaL z&QuI8u|;P9^VXP5)6N!OTIuB37JGXV1_laB)WfnKhBYUWg~`WV`n4xh#A&BIvo~9X z=$355yloCKwl!xc1#1aLx)F+(jV>aD_NYs_@)s!!*hu1CNcFz?K-)_^cYohPyXX$g zrgOJ{+P#x3EyMGeKi?Cbym)m%3n4mxjIxF4{M)=+bF{M;x7N+eyqgi-vZY<-T}rjS z#VaqF_HC{K1~ZmkTN@_Htvz_8UoZAoz_kA&QNH=?q!Bfbbn9Lv$jzos_nD?oNWB>u z(Tqr@tKLaZk3&6L(6!S;&*iVz;%D14BN@McJFCr+hu8A_v>~m{^3$FkJOLB9Su zq2BbO-+m{w>BV~LAIo!|ftmH`jN&zR{rR1?osN}Q&UfMY!=C&PXHpxWfu(e#__mED zH{G?r^Yd$=W_||$omf_G*4!1loE(t|Xp#yKIB2n~M}MP&iaHXML|q5wOS z2IBo)N@*G`hy78S3J#oV$pD@t3n5@+5HT)LkyHk%(EZU&0(C^D0Z`ye0AchE9KCJ8 zGx|XYm|qZ?{ez0JBTy~6Iwh0SkX3A;`))$FhyvJS}}d_c}W*ixT1sbQ?lc z7garVj}>}}J+`;5F=Jwth3>?E)iBRONo7Q^qBl#IvSjWKMUZ8esZN1oAR^vpL_Y|` zigz-Zp;#+#eEb9GyqY=MQ}bYdff4cnt0%QW1;o949aZyDILLm}X&6NrVZT@`$Q@Z> zR&hZRztIQ+Q|Mn#bf2gODdOtSqd~D@1%+&JQ0XUR74GSbACo0K1(NOyv_S* Y(qTjXj?g 0: + return qs[0] + return None + + class Invitation(models.Model): INVITE_TYPE_CHOICES = ( (GUEST, _('Guest')), diff --git a/seahub/invitations/templates/invitations/invitation_revoke_email.html b/seahub/invitations/templates/invitations/invitation_revoke_email.html new file mode 100644 index 0000000000..6a9c2fb04e --- /dev/null +++ b/seahub/invitations/templates/invitations/invitation_revoke_email.html @@ -0,0 +1,18 @@ +{% extends 'email_base.html' %} + +{% load i18n %} + +{% block email_con %} + +{% autoescape off %} + +

          {% trans "Hi," %}

          + +

          +{% blocktrans %}{{ inviter }} revoke your access to {{ site_name }}.{% endblocktrans %} +

          + + +{% endautoescape %} + +{% endblock %} \ No newline at end of file diff --git a/seahub/invitations/views.py b/seahub/invitations/views.py index 273d13e941..8283ed526b 100644 --- a/seahub/invitations/views.py +++ b/seahub/invitations/views.py @@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404, render from django.utils.translation import ugettext as _ -from seahub.auth import login as auth_login +from seahub.auth import login as auth_login, authenticate from seahub.auth import get_backends from seahub.base.accounts import User from seahub.constants import GUEST_USER @@ -21,36 +21,55 @@ def token_view(request, token): if i.is_expired(): raise Http404 + if request.method == 'GET': + try: + user = User.objects.get(email=i.accepter) + if user.is_active is True: + # user is active return exist + messages.error(request, _('A user with this email already exists.')) + except User.DoesNotExist: + pass + + return render(request, 'invitations/token_view.html', {'iv': i, }) + if request.method == 'POST': passwd = request.POST.get('password', '') if not passwd: return HttpResponseRedirect(request.META.get('HTTP_REFERER')) try: - User.objects.get(email=i.accepter) - messages.error(request, _('A user with this email already exists.')) + user = User.objects.get(email=i.accepter) + if user.is_active is True: + # user is active return exist + messages.error(request, _('A user with this email already exists.')) + return render(request, 'invitations/token_view.html', {'iv': i, }) + else: + # user is inactive then set active and new password + user.set_password(passwd) + user.is_active = True + user.save() + user = authenticate(username=user.username, password=passwd) + except User.DoesNotExist: - # Create user, set that user as guest, and log user in. - u = User.objects.create_user(email=i.accepter, password=passwd, - is_active=True) - User.objects.update_role(u.username, GUEST_USER) - - i.accept() # Update invitaion accept time. - + # Create user, set that user as guest. + user = User.objects.create_user( + email=i.accepter, password=passwd, is_active=True) + User.objects.update_role(user.username, GUEST_USER) for backend in get_backends(): - u.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) - auth_login(request, u) + user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) - # send signal to notify inviter - accept_guest_invitation_successful.send( - sender=None, invitation_obj=i) + # Update invitation accept time. + i.accept() - # send email to notify admin - if NOTIFY_ADMIN_AFTER_REGISTRATION: - notify_admins_on_register_complete(u.email) + # login + auth_login(request, user) - return HttpResponseRedirect(SITE_ROOT) + # send signal to notify inviter + accept_guest_invitation_successful.send( + sender=None, invitation_obj=i) - return render(request, 'invitations/token_view.html', { - 'iv': i, - }) + # send email to notify admin + if NOTIFY_ADMIN_AFTER_REGISTRATION: + notify_admins_on_register_complete(user.email) + + return HttpResponseRedirect(SITE_ROOT) diff --git a/seahub/urls.py b/seahub/urls.py index 36ee196059..3067c024da 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -69,7 +69,7 @@ from seahub.api2.endpoints.copy_move_task import CopyMoveTaskView from seahub.api2.endpoints.query_copy_move_progress import QueryCopyMoveProgressView from seahub.api2.endpoints.move_folder_merge import MoveFolderMergeView from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchView -from seahub.api2.endpoints.invitation import InvitationView +from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView from seahub.api2.endpoints.notifications import NotificationsView, NotificationView from seahub.api2.endpoints.user_enabled_modules import UserEnabledModulesView from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView @@ -386,6 +386,7 @@ urlpatterns = [ url(r'^api/v2.1/invitations/$', InvitationsView.as_view()), url(r'^api/v2.1/invitations/batch/$', InvitationsBatchView.as_view()), url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/$', InvitationView.as_view()), + url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/revoke/$', InvitationRevokeView.as_view()), ## user::avatar url(r'^api/v2.1/user-avatar/$', UserAvatarView.as_view(), name='api-v2.1-user-avatar'), diff --git a/tests/api/endpoints/test_invitation.py b/tests/api/endpoints/test_invitation.py index f748e3f6be..a448c63b97 100644 --- a/tests/api/endpoints/test_invitation.py +++ b/tests/api/endpoints/test_invitation.py @@ -5,6 +5,9 @@ from seahub.base.accounts import UserPermissions from seahub.invitations.models import Invitation from seahub.test_utils import BaseTestCase from seahub.api2.permissions import CanInviteGuest +from tests.common.utils import randstring +from seahub.base.accounts import User +from django.core.urlresolvers import reverse class InvitationsTest(BaseTestCase): @@ -69,3 +72,98 @@ class InvitationsTest(BaseTestCase): self.assertEqual(204, resp.status_code) assert len(Invitation.objects.all()) == 0 + + +class InvitationRevokeTest(BaseTestCase): + def setUp(self): + self.login_as(self.user) + self.username = self.user.username + self.tmp_username = 'user_%s@test.com' % randstring(4) + + # add invitation + self.i = Invitation.objects.add(inviter=self.username, accepter=self.tmp_username) + self.endpoint = '/api/v2.1/invitations/' + self.i.token + '/revoke/' + assert len(Invitation.objects.all()) == 1 + + # accept invitation + self.i.accept() + self.tmp_user = self.create_user(self.tmp_username, is_staff=False) + assert self.tmp_user.is_active is True + + def tearDown(self): + self.remove_user(self.tmp_username) + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_post(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + resp = self.client.post(self.endpoint) + self.assertEqual(200, resp.status_code) + tmp_user = User.objects.get(self.tmp_username) + + assert len(Invitation.objects.all()) == 0 + assert tmp_user.is_active is False + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_invite_again_after_revoke(self, mock_can_invite_guest, mock_has_permission): + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + # revoke + resp = self.client.post(self.endpoint) + self.assertEqual(200, resp.status_code) + tmp_user = User.objects.get(self.tmp_username) + + assert len(Invitation.objects.all()) == 0 + assert tmp_user.is_active is False + + # invite again + invite_endpoint = '/api/v2.1/invitations/' + resp = self.client.post(invite_endpoint, { + 'type': 'guest', + 'accepter': self.tmp_username, + }) + self.assertEqual(201, resp.status_code) + assert len(Invitation.objects.all()) == 1 + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_invite_batch_again_and_accept_again_after_revoke(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + # revoke + resp = self.client.post(self.endpoint) + self.assertEqual(200, resp.status_code) + tmp_user = User.objects.get(self.tmp_username) + + assert len(Invitation.objects.all()) == 0 + assert tmp_user.is_active is False + + # invite again + invite_batch_endpoint = '/api/v2.1/invitations/batch/' + resp = self.client.post(invite_batch_endpoint, { + 'type': 'guest', + 'accepter': [self.tmp_username, ], + }) + self.assertEqual(200, resp.status_code) + assert len(Invitation.objects.all()) == 1 + + # accept again + self.logout() + + iv = Invitation.objects.all()[0] + token_endpoint = reverse('invitations:token_view', args=[iv.token]) + assert iv.accept_time is None + resp = self.client.post(token_endpoint, { + 'password': 'passwd' + }) + self.assertEqual(302, resp.status_code) + assert Invitation.objects.get(pk=iv.pk).accept_time is not None + tmp_user_accept = User.objects.get(self.tmp_username) + assert tmp_user_accept.is_active is True From ca4e3b4a08b5c598ab014b29bccc00a1b5e75146 Mon Sep 17 00:00:00 2001 From: llj Date: Tue, 6 Aug 2019 14:59:49 +0800 Subject: [PATCH 12/14] [org admin] side nav: fixup (#3950) --- frontend/src/pages/org-admin/index.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/org-admin/index.js b/frontend/src/pages/org-admin/index.js index 1f7299af97..a62a2ebad4 100644 --- a/frontend/src/pages/org-admin/index.js +++ b/frontend/src/pages/org-admin/index.js @@ -41,10 +41,14 @@ class Org extends React.Component { componentDidMount() { let href = window.location.href.split('/'); let currentTab = href[href.length - 2]; - if (currentTab == 'useradmin') { + + if (location.href.indexOf(`${siteRoot}org/useradmin`) != -1) { currentTab = 'users'; } - if (currentTab > 0) { + if (location.href.indexOf(`${siteRoot}org/groupadmin`) != -1) { + currentTab = 'groupadmin'; + } + if (location.href.indexOf(`${siteRoot}org/departmentadmin`) != -1) { currentTab = 'departmentadmin'; } this.setState({currentTab: currentTab}); @@ -81,9 +85,9 @@ class Org extends React.Component {
          - - - + + + From eb39b99e229387824f0647cb8fe6a154b0d41ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=81=A5=E8=BE=89?= <40563566+WangJianhui666@users.noreply.github.com> Date: Wed, 7 Aug 2019 02:29:02 -0500 Subject: [PATCH 13/14] fix delete tagged file (#3957) --- frontend/src/components/dialog/list-taggedfiles-dialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/dialog/list-taggedfiles-dialog.js b/frontend/src/components/dialog/list-taggedfiles-dialog.js index 4866900866..4f2fac1438 100644 --- a/frontend/src/components/dialog/list-taggedfiles-dialog.js +++ b/frontend/src/components/dialog/list-taggedfiles-dialog.js @@ -38,7 +38,7 @@ class ListTaggedFilesDialog extends React.Component { seafileAPI.deleteFileTag(repoID, fileTagID).then(res => { this.getTaggedFiles(); this.props.updateUsedRepoTags(); - if (this.props.onFileTagChanged) this.onFileTagChanged(taggedFile); + if ((this.props.onFileTagChanged) && !taggedFile.file_deleted) this.onFileTagChanged(taggedFile); }).catch(error => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); From 6f1ac70b820d3b34054000d496ad9e24dd325e36 Mon Sep 17 00:00:00 2001 From: sniper-py <38058090+sniper-py@users.noreply.github.com> Date: Wed, 7 Aug 2019 17:23:02 +0800 Subject: [PATCH 14/14] fix repo error return (#3959) --- seahub/api2/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/seahub/api2/views.py b/seahub/api2/views.py index e136bc3da5..7d5ab86e79 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -999,7 +999,7 @@ class Repos(APIView): storage_id = request.data.get("storage_id", None) if storage_id and storage_id not in [s['storage_id'] for s in storages]: error_msg = 'storage_id invalid.' - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + return None, api_error(status.HTTP_400_BAD_REQUEST, error_msg) repo_id = seafile_api.create_repo(repo_name, repo_desc, username, passwd, @@ -1043,7 +1043,7 @@ class Repos(APIView): salt = request.data.get('salt', '') if not salt: error_msg = 'salt invalid.' - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + return None, api_error(status.HTTP_400_BAD_REQUEST, error_msg) if len(magic) != 64 or len(random_key) != 96 or enc_version < 0: return None, api_error(status.HTTP_400_BAD_REQUEST,