From 1f78ae8ac5815f54515714213d2e842684404b54 Mon Sep 17 00:00:00 2001 From: llj Date: Thu, 1 Aug 2019 15:32:57 +0800 Subject: [PATCH] [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;