diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-department-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-department-dialog.js new file mode 100644 index 0000000000..f65c6a8853 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-department-dialog.js @@ -0,0 +1,101 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Form, FormGroup, Label } from 'reactstrap'; +import { gettext, orgID } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; + +const propTypes = { + groupID: PropTypes.string, + parentGroupID: PropTypes.string, + toggle: PropTypes.func.isRequired, + onDepartChanged: PropTypes.func.isRequired, +}; + +class AddDepartDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + departName: '', + errMessage: '', + }; + this.newInput = React.createRef(); + } + + componentDidMount() { + this.newInput.focus(); + this.newInput.setSelectionRange(0, 0); + } + + handleSubmit = () => { + let isValid = this.validateName(); + if (isValid) { + let parentGroup = -1; + if (this.props.parentGroupID) { + parentGroup = this.props.parentGroupID; + } + seafileAPI.sysAdminAddNewDepartment(parentGroup, this.state.departName.trim()).then((res) => { + this.props.toggle(); + this.props.onDepartChanged(); + }).catch(error => { + let errorMsg = gettext(error.response.data.error_msg); + this.setState({ errMessage: errorMsg }); + }); + } + } + + validateName = () => { + let errMessage = ''; + const name = this.state.departName.trim(); + if (!name.length) { + errMessage = gettext('Name is required'); + this.setState({ errMessage: errMessage }); + return false; + } + return true; + } + + handleChange = (e) => { + this.setState({ + departName: e.target.value, + }); + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + e.preventDefault(); + } + } + + render() { + let header = this.props.parentGroupID ? gettext('New Sub-department') : gettext('New Department'); + return ( + + {header} + +
+ + + {this.newInput = input;}} + /> + +
+ { this.state.errMessage &&

{this.state.errMessage}

} +
+ + + +
+ ); + } +} + +AddDepartDialog.propTypes = propTypes; + +export default AddDepartDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-member-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-member-dialog.js new file mode 100644 index 0000000000..be51c52a8d --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-member-dialog.js @@ -0,0 +1,75 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext, orgID } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; +import toaster from '../../toast'; +import UserSelect from '../../user-select.js'; + +const propTypes = { + toggle: PropTypes.func.isRequired, + groupID: PropTypes.string.isRequired, + onMemberChanged: PropTypes.func.isRequired +}; + +class AddMemberDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + selectedOption: null, + errMessage: '', + }; + } + + handleSelectChange = (option) => { + this.setState({ selectedOption: option }); + } + + handleSubmit = () => { + if (!this.state.selectedOption) return; + const emails = this.state.selectedOption.map(item => item.email); + this.refs.orgSelect.clearSelect(); + this.setState({ errMessage: [] }); + seafileAPI.sysAdminAddGroupMember(this.props.groupID, emails).then((res) => { + this.setState({ selectedOption: null }); + if (res.data.failed.length > 0) { + this.setState({ errMessage: res.data.failed[0].error_msg }); + } + if (res.data.success.length > 0) { + this.props.onMemberChanged(); + this.props.toggle(); + } + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + render() { + return ( + + {gettext('Add Member')} + + + { this.state.errMessage &&

{this.state.errMessage}

} +
+ + + + +
+ ); + } +} + +AddMemberDialog.propTypes = propTypes; + +export default AddMemberDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog.js new file mode 100644 index 0000000000..3cefd3018f --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Form, FormGroup, Label } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; + +const propTypes = { + toggle: PropTypes.func.isRequired, + groupID: PropTypes.string.isRequired, + onRepoChanged: PropTypes.func.isRequired, +}; + +class AddRepoDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + repoName: '', + errMessage: '', + }; + this.newInput = React.createRef(); + } + + componentDidMount() { + this.newInput.focus(); + this.newInput.setSelectionRange(0, 0); + } + + handleSubmit = () => { + let isValid = this.validateName(); + if (isValid) { + seafileAPI.sysAdminAddRepoInDepartment(this.props.groupID, this.state.repoName.trim()).then((res) => { + this.props.toggle(); + this.props.onRepoChanged(); + }).catch(error => { + let errorMsg = Utils.getErrorMsg(error); + this.setState({ errMessage: errorMsg }); + }); + } + } + + validateName = () => { + let errMessage = ''; + const name = this.state.repoName.trim(); + if (!name.length) { + errMessage = gettext('Name is required'); + this.setState({ errMessage: errMessage }); + return false; + } + return true; + } + + handleChange = (e) => { + this.setState({ + repoName: e.target.value, + }); + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + e.preventDefault(); + } + } + + render() { + return ( + + {gettext('New Library')} + +
+ + + {this.newInput = input;}} + /> + +
+ { this.state.errMessage &&

{this.state.errMessage}

} +
+ + + +
+ ); + } +} + +AddRepoDialog.propTypes = propTypes; + +export default AddRepoDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-department-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-department-dialog.js new file mode 100644 index 0000000000..6a0302efd2 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-department-dialog.js @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; + +const propTypes = { + groupName: PropTypes.string, + groupID: PropTypes.number.isRequired, + toggle: PropTypes.func.isRequired, + onDepartChanged: PropTypes.func.isRequired +}; + +class DeleteDepartDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + errMessage: null + }; + } + + deleteDepart = () => { + seafileAPI.sysAdminDeleteDepartment(this.props.groupID).then((res) => { + if (res.data.success) { + this.props.onDepartChanged(); + this.props.toggle(); + } + }).catch(err => { + this.setState({ errMessage: 'There are sub-departments in this department.' }); + }); + } + + render() { + let tipMessage = gettext('Are you sure you want to delete {placeholder} ?'); + tipMessage = tipMessage.replace('{placeholder}', '' + Utils.HTMLescape(this.props.groupName) + ''); + return ( + + {gettext('Delete Department')} + +
+ { this.state.errMessage &&

{this.state.errMessage}

} +
+ + {!this.state.errMessage && } + + +
+ ); + } +} + +DeleteDepartDialog.propTypes = propTypes; + +export default DeleteDepartDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-member-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-member-dialog.js new file mode 100644 index 0000000000..e7ff3afe05 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-member-dialog.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; +import toaster from '../../toast'; + +const propTypes = { + member: PropTypes.object.isRequired, + groupID: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, + onMemberChanged: PropTypes.func.isRequired +}; + +class DeleteMemberDialog extends React.Component { + + constructor(props) { + super(props); + } + + deleteMember = () => { + const userEmail = this.props.member.email; + seafileAPI.sysAdminDeleteGroupMember(this.props.groupID, userEmail).then((res) => { + if (res.data.success) { + this.props.onMemberChanged(); + this.props.toggle(); + } + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + render() { + let tipMessage = gettext('Are you sure you want to delete {placeholder} ?'); + tipMessage = tipMessage.replace('{placeholder}', '' + Utils.HTMLescape(this.props.member.name) + ''); + return ( + + {gettext('Delete Member')} + +
+
+ + + + +
+ ); + } +} + +DeleteMemberDialog.propTypes = propTypes; + +export default DeleteMemberDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-repo-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-repo-dialog.js new file mode 100644 index 0000000000..cc76b24e66 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-delete-repo-dialog.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; +import toaster from '../../toast'; + +class DeleteRepoDialog extends React.Component { + + constructor(props) { + super(props); + } + + deleteRepo = () => { + seafileAPI.sysAdminDeleteRepoInDepartment(this.props.groupID, this.props.repo.repo_id).then((res) => { + if (res.data.success) { + this.props.onRepoChanged(); + this.props.toggle(); + } + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + render() { + let tipMessage = gettext('Are you sure you want to delete {placeholder} ?'); + tipMessage = tipMessage.replace('{placeholder}', '' + Utils.HTMLescape(this.props.repo.name) + ''); + return ( + + {gettext('Delete Library')} + +
+
+ + + + +
+ ); + } +} + +const propTypes = { + repo: PropTypes.object.isRequired, + toggle: PropTypes.func.isRequired, + groupID: PropTypes.string.isRequired, + onRepoChanged: PropTypes.func.isRequired +}; + +DeleteRepoDialog.propTypes = propTypes; + +export default DeleteRepoDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-set-group-quota-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-set-group-quota-dialog.js new file mode 100644 index 0000000000..b5bdb4e755 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-set-group-quota-dialog.js @@ -0,0 +1,92 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, InputGroupAddon, InputGroup } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; +import toaster from '../../toast'; + +const propTypes = { + toggle: PropTypes.func.isRequired, + groupID: PropTypes.number.isRequired, + onDepartChanged: PropTypes.func.isRequired, +}; + +class SetGroupQuotaDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + quota: '', + errMessage: '', + }; + this.newInput = React.createRef(); + } + + componentDidMount() { + this.newInput.focus(); + this.newInput.setSelectionRange(0, 0); + } + + setGroupQuota = () => { + const numberReg = /^[1-9]\d*$/im; + let quota = this.state.quota; + if ((quota.length && numberReg.test(quota)) || quota == -2) { + this.setState({ errMessage: '' }); + let newQuota = this.state.quota == -2 ? this.state.quota : this.state.quota * 1000000; + seafileAPI.sysAdminUpdateDepartmentQuota(this.props.groupID, newQuota).then((res) => { + this.props.toggle(); + this.props.onDepartChanged(); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } else { + const err = gettext('Quota is invalid.'); + this.setState({ errMessage: err }); + } + } + + handleChange = (e) => { + const quota = e.target.value.trim(); + this.setState({ quota: quota }); + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.setGroupQuota(); + e.preventDefault(); + } + } + + render() { + return ( + + {gettext('Set Quota')} + + + {this.newInput = input;}} + /> + {'MB'} + +

+
{gettext('An integer that is greater than 0 or equal to -2.')}
+ {gettext('Tip: -2 means no limit.')} +

+ { this.state.errMessage &&

{this.state.errMessage}

} +
+ + + +
+ ); + } +} + +SetGroupQuotaDialog.propTypes = propTypes; + +export default SetGroupQuotaDialog; diff --git a/frontend/src/pages/sys-admin/departments/department-detail.js b/frontend/src/pages/sys-admin/departments/department-detail.js new file mode 100644 index 0000000000..d0062f3579 --- /dev/null +++ b/frontend/src/pages/sys-admin/departments/department-detail.js @@ -0,0 +1,372 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { Link } from '@reach/router'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils.js'; +import toaster from '../../../components/toast'; +import MainPanelTopbar from '../main-panel-topbar'; +import ModalPortal from '../../../components/modal-portal'; +import AddDepartDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-department-dialog'; +import AddMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-member-dialog'; +import DeleteMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-member-dialog'; +import AddRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog'; +import DeleteRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-repo-dialog'; +import DeleteDepartDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-department-dialog'; +import SetGroupQuotaDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-set-group-quota-dialog'; +import { siteRoot, gettext, lang } from '../../../utils/constants'; +import GroupItem from './group-item'; +import MemberItem from './member-item'; +import RepoItem from './repo-item'; +import '../../../css/org-department-item.css'; + +moment.locale(lang); + +const DepartmentDetailPropTypes = { + groupID: PropTypes.string, +}; + +class DepartmentDetail extends React.Component { + + constructor(props) { + super(props); + this.state = { + groupName: '', + isItemFreezed: false, + ancestorGroups: [], + members: [], + deletedMember: {}, + isShowAddMemberDialog: false, + showDeleteMemberDialog: false, + repos: [], + deletedRepo: {}, + isShowAddRepoDialog: false, + showDeleteRepoDialog: false, + groups: [], + subGroupID: '', + subGroupName: '', + isShowAddDepartDialog: false, + showDeleteDepartDialog: false, + showSetGroupQuotaDialog: false, + }; + } + + componentDidMount() { + const groupID = this.props.groupID; + this.listGroupRepo(groupID); + this.listMembers(groupID); + } + + componentWillReceiveProps(nextProps) { + if (this.props.groupID !== nextProps.groupID) { + this.listGroupRepo(nextProps.groupID); + this.listMembers(nextProps.groupID); + } + } + + listGroupRepo = (groupID) => { + seafileAPI.sysAdminListGroupRepos(groupID).then(res => { + this.setState({ repos: res.data.libraries }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + listMembers = (groupID) => { + seafileAPI.sysAdminGetDepartmentInfo(groupID, true).then(res => { + this.setState({ + members: res.data.members, + groups: res.data.groups, + ancestorGroups: res.data.ancestor_groups, + groupName: res.data.name, + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + listSubDepartGroups = (groupID) => { + seafileAPI.sysAdminGetDepartmentInfo(groupID, true).then(res => { + this.setState({ groups: res.data.groups }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + toggleCancel = () => { + this.setState({ + showDeleteMemberDialog: false, + showDeleteRepoDialog: false, + showDeleteDepartDialog: false, + showSetGroupQuotaDialog: false, + }); + } + + onSubDepartChanged = () => { + this.listSubDepartGroups(this.props.groupID); + } + + onRepoChanged = () => { + this.listGroupRepo(this.props.groupID); + } + + onMemberChanged = () => { + this.listMembers(this.props.groupID); + } + + toggleItemFreezed = (isFreezed) => { + this.setState({ isItemFreezed: isFreezed }); + } + + showDeleteMemberDialog = (member) => { + this.setState({ showDeleteMemberDialog: true, deletedMember: member }); + } + + showDeleteRepoDialog = (repo) => { + this.setState({ showDeleteRepoDialog: true, deletedRepo: repo }); + } + + toggleAddRepoDialog = () => { + this.setState({ isShowAddRepoDialog: !this.state.isShowAddRepoDialog }); + } + + toggleAddMemberDialog = () => { + this.setState({ isShowAddMemberDialog: !this.state.isShowAddMemberDialog }); + } + + toggleAddDepartDialog = () => { + this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog}); + } + + showDeleteDepartDialog = (subGroup) => { + this.setState({ + showDeleteDepartDialog: true, + subGroupID: subGroup.id, + subGroupName: subGroup.name + }); + } + + showSetGroupQuotaDialog = (subGroupID) => { + this.setState({ + showSetGroupQuotaDialog: true, + subGroupID: subGroupID + }); + } + + render() { + const { members, repos, groups } = this.state; + const groupID = this.props.groupID; + const topBtn = 'btn btn-secondary operation-item'; + const topbarChildren = ( + + {groupID && + + + + + + } + {this.state.isShowAddMemberDialog && ( + + + + )} + {this.state.isShowAddRepoDialog && ( + + + + )} + {this.state.isShowAddDepartDialog && ( + + + + )} + + ); + + return ( + + +
+
+
+
+

+ {groupID ? + {gettext('Departments')} + : {gettext('Departments')} + } + {this.state.ancestorGroups.map(ancestor => { + let newHref = siteRoot + 'sys/departments/' + ancestor.id + '/'; + return {' / '}{ancestor.name}; + })} + {groupID && {' / '}{this.state.groupName}} +

+
+
+ +
+
+

{gettext('Sub-departments')}

+
+
+ {groups && groups.length > 0 ? + + + + + + + + + + + {groups.map((group, index) => { + return( + + + + ); + })} + +
{gettext('Name')}{gettext('Created At')}{gettext('Quota')}
+ :

{gettext('No sub-departments')}

+ } +
+
+ +
+
+

{gettext('Members')}

+
+
+ {(members && members.length === 1 && members[0].role === 'Owner') ? +

{gettext('No members')}

: + + + + + + + + + + + {members.map((member, index) => { + return ( + + + + ); + })} + +
{gettext('Name')}{gettext('Role')}
+ } +
+
+ +
+
+

{gettext('Libraries')}

+
+ { repos.length > 0 ? +
+ + + + + + + + + + + {repos.map((repo, index) => { + return( + + + + ); + })} + +
{gettext('Name')}{gettext('Size')}
+
+ :

{gettext('No libraries')}

+ } +
+ +
+ {this.state.showDeleteMemberDialog && ( + + + + )} + {this.state.showDeleteRepoDialog && ( + + + + )} + {this.state.showDeleteDepartDialog && ( + + + + )} + {this.state.showSetGroupQuotaDialog && ( + + + + )} +
+
+ ); + } +} + +DepartmentDetail.propTypes = DepartmentDetailPropTypes; + +export default DepartmentDetail; \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/departments/departments-list.js b/frontend/src/pages/sys-admin/departments/departments-list.js new file mode 100644 index 0000000000..221a8a544e --- /dev/null +++ b/frontend/src/pages/sys-admin/departments/departments-list.js @@ -0,0 +1,145 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { seafileAPI } from '../../../utils/seafile-api'; +import MainPanelTopbar from '../main-panel-topbar'; +import ModalPortal from '../../../components/modal-portal'; +import AddDepartDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-department-dialog'; +import DeleteDepartDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-department-dialog'; +import SetGroupQuotaDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-set-group-quota-dialog'; +import { gettext, lang } from '../../../utils/constants'; +import GroupItem from './group-item'; +import '../../../css/org-department-item.css'; + +moment.locale(lang); + +class DepartmentsList extends React.Component { + + constructor(props) { + super(props); + this.state = { + groups: null, + groupID: '', + groupName: '', + showDeleteDepartDialog: false, + showSetGroupQuotaDialog: false, + isShowAddDepartDialog: false, + }; + } + + componentDidMount() { + this.listDepartGroups(); + } + + listDepartGroups = () => { + seafileAPI.sysAdminListAllDepartments().then(res => { + this.setState({ groups: res.data.data }); + }); + } + + showDeleteDepartDialog = (group) => { + this.setState({ showDeleteDepartDialog: true, groupID: group.id, groupName: group.name }); + } + + showSetGroupQuotaDialog = (groupID) => { + this.setState({ showSetGroupQuotaDialog: true, groupID: groupID }); + } + + toggleAddDepartDialog = () => { + this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog}); + } + + toggleCancel = () => { + this.setState({ + showDeleteDepartDialog: false, + showSetGroupQuotaDialog: false, + }); + } + + onDepartChanged = () => { + this.listDepartGroups(); + } + + render() { + const groups = this.state.groups; + const topbarChildren = ( + + + {this.state.isShowAddDepartDialog && ( + + + + )} + + ); + return ( + + +
+
+
+
+

{gettext('Departments')}

+
+
+
+ {groups && groups.length > 0 ? + + + + + + + + + + + {groups.map((group, index) => { + return( + + + + ); + })} + +
{gettext('Name')}{gettext('Created At')}{gettext('Quota')}
+ : +

{gettext('No departments')}

+ } +
+ {this.state.showDeleteDepartDialog && ( + + + + )} + {this.state.showSetGroupQuotaDialog && ( + + + + )} +
+
+
+ ); + } +} + +export default DepartmentsList; \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/departments/departments.js b/frontend/src/pages/sys-admin/departments/departments.js new file mode 100644 index 0000000000..70d71aa16f --- /dev/null +++ b/frontend/src/pages/sys-admin/departments/departments.js @@ -0,0 +1,19 @@ +import React from 'react'; +import '../../../css/org-department-item.css'; + +class Departments extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +export default Departments; diff --git a/frontend/src/pages/sys-admin/departments/group-item.js b/frontend/src/pages/sys-admin/departments/group-item.js new file mode 100644 index 0000000000..a127f1b198 --- /dev/null +++ b/frontend/src/pages/sys-admin/departments/group-item.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { Link } from '@reach/router'; +import { Utils } from '../../../utils/utils.js'; +import { siteRoot } from '../../../utils/constants'; + +const GroupItemPropTypes = { + group: PropTypes.object.isRequired, + showSetGroupQuotaDialog: PropTypes.func.isRequired, + showDeleteDepartDialog: PropTypes.func.isRequired, +}; + +class GroupItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + }; + } + + onMouseEnter = () => { + this.setState({ highlight: true }); + } + + onMouseLeave = () => { + this.setState({ highlight: false }); + } + + render() { + const group = this.props.group; + const highlight = this.state.highlight; + const newHref = siteRoot+ 'sys/departments/' + group.id + '/'; + return ( + + {group.name} + {moment(group.created_at).fromNow()} + + {Utils.bytesToSize(group.quota)}{' '} + + + + + + + ); + } +} + +GroupItem.propTypes = GroupItemPropTypes; + +export default GroupItem; \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/departments/member-item.js b/frontend/src/pages/sys-admin/departments/member-item.js new file mode 100644 index 0000000000..b9f3a04882 --- /dev/null +++ b/frontend/src/pages/sys-admin/departments/member-item.js @@ -0,0 +1,87 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils.js'; +import toaster from '../../../components/toast'; +import RoleEditor from '../../../components/select-editor/role-editor'; +import { serviceURL } from '../../../utils/constants'; + +const MemberItemPropTypes = { + groupID: PropTypes.string.isRequired, + member: PropTypes.object.isRequired, + isItemFreezed: PropTypes.bool.isRequired, + onMemberChanged: PropTypes.func.isRequired, + showDeleteMemberDialog: PropTypes.func.isRequired, + toggleItemFreezed: PropTypes.func.isRequired, +}; + +class MemberItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + showRoleMenu: false, + }; + this.roles = ['Admin', 'Member']; + } + + onMouseEnter = () => { + if (this.props.isItemFreezed) return; + this.setState({ highlight: true }); + } + + onMouseLeave = () => { + if (this.props.isItemFreezed) return; + this.setState({ highlight: false }); + } + + toggleMemberRoleMenu = () => { + this.setState({ showRoleMenu: !this.state.showRoleMenu }); + } + + onChangeUserRole = (role) => { + let isAdmin = role === 'Admin' ? true : false; + seafileAPI.sysAdminUpdateGroupMemberRole(this.props.groupID, this.props.member.email, isAdmin).then((res) => { + this.props.onMemberChanged(); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + this.setState({ + highlight: false, + }); + } + + render() { + const member = this.props.member; + const highlight = this.state.highlight; + let memberLink = serviceURL + '/useradmin/info/' + member.email + '/'; + if (member.role === 'Owner') return null; + return ( + + member-header + {member.name} + + + + {!this.props.isItemFreezed ? + + + : + } + + ); + } +} + +MemberItem.propTypes = MemberItemPropTypes; + +export default MemberItem; \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/departments/repo-item.js b/frontend/src/pages/sys-admin/departments/repo-item.js new file mode 100644 index 0000000000..e3204c2e81 --- /dev/null +++ b/frontend/src/pages/sys-admin/departments/repo-item.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Utils } from '../../../utils/utils.js'; +import { siteRoot, gettext } from '../../../utils/constants'; + +const RepoItemPropTypes = { + repo: PropTypes.object.isRequired, + showDeleteRepoDialog: PropTypes.func.isRequired, +}; + +class RepoItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + }; + } + + onMouseEnter = () => { + this.setState({ highlight: true }); + } + + onMouseLeave = () => { + this.setState({ highlight: false }); + } + + render() { + const repo = this.props.repo; + const highlight = this.state.highlight; + let iconUrl = Utils.getLibIconUrl(repo); + return ( + + {gettext('icon')}/ + {repo.name} + {Utils.bytesToSize(repo.size)}{' '} + + + + + ); + } +} + +RepoItem.propTypes = RepoItemPropTypes; + +export default RepoItem; \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js index f0389721a4..e417af7556 100644 --- a/frontend/src/pages/sys-admin/index.js +++ b/frontend/src/pages/sys-admin/index.js @@ -20,6 +20,10 @@ import Groups from './groups/groups'; import GroupRepos from './groups/group-repos'; import GroupMembers from './groups/group-members'; +import Departments from './departments/departments'; +import DepartmentsList from './departments/departments-list'; +import DepartmentDetail from './departments/department-detail'; + import ShareLinks from './links/share-links'; import UploadLinks from './links/upload-links'; @@ -115,6 +119,10 @@ class SysAdmin extends React.Component { + + + + - - + this.props.tabItemClick('departments')} + > + {gettext('Departments')} - + } {multiTenancy && isDefaultAdmin && diff --git a/seahub/urls.py b/seahub/urls.py index 83f5b671ff..f316f62ca1 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -666,6 +666,8 @@ urlpatterns = [ url(r'^sys/groups/$', sysadmin_react_fake_view, name="sys_groups"), url(r'^sys/groups/(?P\d+)/libraries/$', sysadmin_react_fake_view, name="sys_group_libraries"), url(r'^sys/groups/(?P\d+)/members/$', sysadmin_react_fake_view, name="sys_group_members"), + url(r'^sys/departments/$', sysadmin_react_fake_view, name="sys_departments"), + url(r'^sys/departments/(?P\d+)/$', sysadmin_react_fake_view, name="sys_department"), url(r'^sys/users/$', sysadmin_react_fake_view, name="sys_users"), url(r'^sys/users-all/$', sysadmin_react_fake_view, name="sys_users_all"), url(r'^sys/users-admin/$', sysadmin_react_fake_view, name="sys_users_admin"),