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.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.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 ?
+
+
+
+ {gettext('Name')} |
+ {gettext('Created At')} |
+ {gettext('Quota')} |
+ |
+
+
+
+ {groups.map((group, index) => {
+ return(
+
+
+
+ );
+ })}
+
+
+ :
{gettext('No sub-departments')}
+ }
+
+
+
+
+
+
+ {(members && members.length === 1 && members[0].role === 'Owner') ?
+
{gettext('No members')}
:
+
+
+
+ |
+ {gettext('Name')} |
+ {gettext('Role')} |
+ |
+
+
+
+ {members.map((member, index) => {
+ return (
+
+
+
+ );
+ })}
+
+
+ }
+
+
+
+
+
+
{gettext('Libraries')}
+
+ { repos.length > 0 ?
+
+
+
+
+ |
+ {gettext('Name')} |
+ {gettext('Size')} |
+ |
+
+
+
+ {repos.map((repo, index) => {
+ return(
+
+
+
+ );
+ })}
+
+
+
+ :
{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 ?
+
+
+
+ {gettext('Name')} |
+ {gettext('Created At')} |
+ {gettext('Quota')} |
+ |
+
+
+
+ {groups.map((group, index) => {
+ return(
+
+
+
+ );
+ })}
+
+
+ :
+
{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.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 (
+
+  |
+ {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"),