diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-group-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-group-dialog.js
new file mode 100644
index 0000000000..a3fa81545c
--- /dev/null
+++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-group-dialog.js
@@ -0,0 +1,104 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
+import { gettext } from '../../../utils/constants';
+import UserSelect from '../../user-select';
+
+
+const propTypes = {
+ createGroup: PropTypes.func.isRequired,
+ toggleDialog: PropTypes.func.isRequired
+};
+
+class SysAdminCreateGroupDialog extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ groupName: '',
+ ownerEmail: '',
+ disabled: true,
+ errMessage: '',
+ isSubmitBtnActive: false
+ };
+ this.newInput = React.createRef();
+ }
+
+ handleRepoNameChange = (e) => {
+ if (!e.target.value.trim()) {
+ this.setState({isSubmitBtnActive: false});
+ } else {
+ this.setState({isSubmitBtnActive: true});
+ }
+
+ this.setState({groupName: e.target.value});
+ }
+
+ handleSubmit = () => {
+ let groupName = this.state.groupName.trim();
+ this.props.createGroup(groupName, this.state.ownerEmail);
+ }
+
+ handleSelectChange = (option) => {
+ // option can be null
+ this.setState({
+ ownerEmail: option ? option.email : ''
+ });
+ }
+
+ handleKeyPress = (e) => {
+ if (e.key === 'Enter') {
+ this.handleSubmit();
+ e.preventDefault();
+ }
+ }
+
+ toggle = () => {
+ this.props.toggleDialog();
+ }
+
+ componentDidMount() {
+ this.newInput.focus();
+ }
+
+ render() {
+ return (
+
+ {gettext('New Group')}
+
+
+ {this.state.errMessage && {this.state.errMessage}}
+
+
+
+
+
+
+ );
+ }
+}
+
+SysAdminCreateGroupDialog.propTypes = propTypes;
+
+export default SysAdminCreateGroupDialog;
diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog.js
new file mode 100644
index 0000000000..4c6444a96b
--- /dev/null
+++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+import { gettext } from '../../../utils/constants';
+import UserSelect from '../../user-select';
+
+const propTypes = {
+ toggle: PropTypes.func.isRequired,
+ addMembers: PropTypes.func.isRequired
+};
+
+class SysAdminGroupAddMemberDialog extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selectedOptions: null,
+ isSubmitBtnDisabled: true
+ };
+ }
+
+ handleSelectChange = (options) => {
+ this.setState({
+ selectedOptions: options,
+ isSubmitBtnDisabled: !options.length
+ });
+ }
+
+ addMembers = () => {
+ let emails = this.state.selectedOptions.map(item => item.email);
+ this.props.addMembers(emails);
+ this.props.toggle();
+ }
+
+ render() {
+ const { isSubmitBtnDisabled } = this.state;
+ return (
+
+ {gettext('Add Member')}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+SysAdminGroupAddMemberDialog.propTypes = propTypes;
+
+export default SysAdminGroupAddMemberDialog;
diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-transfer-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-transfer-dialog.js
new file mode 100644
index 0000000000..52f92c3e89
--- /dev/null
+++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-transfer-dialog.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+import { Utils } from '../../../utils/utils';
+import { gettext } from '../../../utils/constants';
+import UserSelect from '../../user-select';
+
+const propTypes = {
+ groupName: PropTypes.string.isRequired,
+ transferGroup: PropTypes.func.isRequired,
+ toggleDialog: PropTypes.func.isRequired
+};
+
+class SysAdminTransferGroupDialog extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selectedOption: null,
+ submitBtnDisabled: true
+ };
+ }
+
+ handleSelectChange = (option) => {
+ this.setState({
+ selectedOption: option,
+ submitBtnDisabled: option == null
+ });
+ }
+
+ submit = () => {
+ const receiver = this.state.selectedOption.email;
+ this.props.transferGroup(receiver);
+ this.props.toggleDialog();
+ }
+
+ render() {
+ const { submitBtnDisabled } = this.state;
+ const groupName = Utils.HTMLescape(this.props.groupName);
+ const innerSpan = '' + groupName +'';
+ const msg = gettext('Transfer Group {library_name} to').replace('{library_name}', innerSpan);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+SysAdminTransferGroupDialog.propTypes = propTypes;
+
+export default SysAdminTransferGroupDialog;
diff --git a/frontend/src/components/select-editor/sysadmin-group-role-editor.js b/frontend/src/components/select-editor/sysadmin-group-role-editor.js
new file mode 100644
index 0000000000..7b0073ac3b
--- /dev/null
+++ b/frontend/src/components/select-editor/sysadmin-group-role-editor.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { gettext } from '../../utils/constants';
+import SelectEditor from './select-editor';
+
+const propTypes = {
+ isTextMode: PropTypes.bool.isRequired,
+ isEditIconShow: PropTypes.bool.isRequired,
+ roleOptions: PropTypes.array.isRequired,
+ currentRole: PropTypes.string.isRequired,
+ onRoleChanged: PropTypes.func.isRequired
+};
+
+class SysAdminGroupRoleEditor extends React.Component {
+
+ translateRoles = (role) => {
+ switch (role) {
+ case 'Member':
+ return gettext('Member');
+ case 'Admin':
+ return gettext('Admin');
+ default:
+ return role;
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+SysAdminGroupRoleEditor.propTypes = propTypes;
+
+export default SysAdminGroupRoleEditor;
\ No newline at end of file
diff --git a/frontend/src/pages/sys-admin/groups/group-members.js b/frontend/src/pages/sys-admin/groups/group-members.js
new file mode 100644
index 0000000000..3d2060199e
--- /dev/null
+++ b/frontend/src/pages/sys-admin/groups/group-members.js
@@ -0,0 +1,283 @@
+import React, { Component, Fragment } from 'react';
+import { Button } from 'reactstrap';
+import { Utils } from '../../../utils/utils';
+import { seafileAPI } from '../../../utils/seafile-api';
+import { siteRoot, loginUrl, gettext } from '../../../utils/constants';
+import toaster from '../../../components/toast';
+import EmptyTip from '../../../components/empty-tip';
+import Loading from '../../../components/loading';
+import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
+import SysAdminGroupAddMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog';
+import SysAdminGroupRoleEditor from '../../../components/select-editor/sysadmin-group-role-editor';
+import MainPanelTopbar from '../main-panel-topbar';
+import GroupNav from './group-nav';
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { loading, errorMsg, items } = this.props;
+ if (loading) {
+ return ;
+ } else if (errorMsg) {
+ return
{errorMsg}
;
+ } else {
+ const emptyTip = (
+
+ {gettext('No members')}
+
+ );
+ const table = (
+
+
+
+
+ {/* icon */} |
+ {gettext('Name')} |
+ {gettext('Role')} |
+ {/*Operations*/} |
+
+
+
+ {items.map((item, index) => {
+ return ( );
+ })}
+
+
+
+ );
+ return items.length ? table : emptyTip;
+ }
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpIconShown: false,
+ isDeleteDialogOpen: false
+ };
+ }
+
+ handleMouseEnter = () => {
+ this.setState({isOpIconShown: true});
+ }
+
+ handleMouseLeave = () => {
+ this.setState({isOpIconShown: false});
+ }
+
+ toggleDeleteDialog = (e) => {
+ if (e) {
+ e.preventDefault();
+ }
+ this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
+ }
+
+ removeMember = () => {
+ const { item } = this.props;
+ this.props.removeMember(item.email, item.name);
+ this.toggleDeleteDialog();
+ }
+
+ updateMemberRole = (role) => {
+ this.props.updateMemberRole(this.props.item.email, role);
+ }
+
+ render() {
+ let { isOpIconShown, isDeleteDialogOpen } = this.state;
+ let { item } = this.props;
+
+ let itemName = '' + Utils.HTMLescape(item.name) + '';
+ let dialogMsg = gettext('Are you sure you want to remove {placeholder} ?').replace('{placeholder}', itemName);
+
+ return (
+
+
+  |
+ {item.name} |
+
+ {item.role == 'Owner' ?
+ gettext('Owner') :
+
+ }
+ |
+
+ {item.role != 'Owner' &&
+
+ }
+ |
+
+ {isDeleteDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+class GroupMembers extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ groupName: '',
+ memberList: [],
+ isAddMemberDialogOpen: false
+ };
+ }
+
+ componentDidMount () {
+ seafileAPI.sysAdminListGroupMembers(this.props.groupID).then((res) => {
+ this.setState({
+ loading: false,
+ memberList: res.data.members,
+ groupName: res.data.group_name
+ });
+ }).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.')
+ });
+ }
+ });
+ }
+
+ toggleAddMemgerDialog = () => {
+ this.setState({isAddMemberDialogOpen: !this.state.isAddMemberDialogOpen});
+ }
+
+ addMembers = (emails) => {
+ seafileAPI.sysAdminAddGroupMember(this.props.groupID, emails).then(res => {
+ let newMemberList = res.data.success;
+ if (newMemberList.length) {
+ newMemberList = newMemberList.concat(this.state.memberList);
+ this.setState({
+ memberList: newMemberList
+ });
+ newMemberList.map(item => {
+ const msg = gettext('Successfully added {email_placeholder}')
+ .replace('{email_placeholder}', item.email);
+ toaster.success(msg);
+ });
+ }
+ res.data.failed.map(item => {
+ const msg = gettext('Failed to add {email_placeholder}: {error_msg_placeholder}')
+ .replace('{email_placeholder}', item.email)
+ .replace('{error_msg_placeholder}', item.error_msg);
+ toaster.danger(msg, {duration: 3});
+ });
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ removeMember = (email, name) => {
+ seafileAPI.sysAdminDeleteGroupMember(this.props.groupID, email).then(res => {
+ let newRepoList = this.state.memberList.filter(item => {
+ return item.email != email;
+ });
+ this.setState({
+ memberList: newRepoList
+ });
+ toaster.success(gettext('Successfully removed {placeholder}.').replace('{placeholder}', name));
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ updateMemberRole = (email, role) => {
+ let isAdmin = role == 'Admin';
+ seafileAPI.sysAdminUpdateGroupMemberRole(this.props.groupID, email, isAdmin).then(res => {
+ let newRepoList = this.state.memberList.map(item => {
+ if (item.email == email) {
+ item.role = role;
+ }
+ return item;
+ });
+ this.setState({
+ memberList: newRepoList
+ });
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ render() {
+ let { isAddMemberDialogOpen } = this.state;
+ return (
+
+
+
+
+
+ {isAddMemberDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+export default GroupMembers;
diff --git a/frontend/src/pages/sys-admin/groups/group-nav.js b/frontend/src/pages/sys-admin/groups/group-nav.js
new file mode 100644
index 0000000000..6b047b43a8
--- /dev/null
+++ b/frontend/src/pages/sys-admin/groups/group-nav.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@reach/router';
+import { siteRoot, gettext } from '../../../utils/constants';
+
+const propTypes = {
+ currentItem: PropTypes.string.isRequired,
+ groupID: PropTypes.string.isRequired
+};
+
+class Nav extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.navItems = [
+ {name: 'repos', urlPart: 'groups/' + this.props.groupID + '/libraries', text: gettext('Libraries')},
+ {name: 'members', urlPart: 'groups/' + this.props.groupID + '/members', text: gettext('Members')}
+ ];
+ }
+
+ render() {
+ const { groupName, currentItem } = this.props;
+ return (
+
+
+
{gettext('Groups')} / {groupName}
+
+
+ {this.navItems.map((item, index) => {
+ return (
+ -
+ {item.text}
+
+ );
+ })}
+
+
+ );
+ }
+}
+
+Nav.propTypes = propTypes;
+
+export default Nav;
diff --git a/frontend/src/pages/sys-admin/groups/group-repos.js b/frontend/src/pages/sys-admin/groups/group-repos.js
new file mode 100644
index 0000000000..8ecf9aa592
--- /dev/null
+++ b/frontend/src/pages/sys-admin/groups/group-repos.js
@@ -0,0 +1,229 @@
+import React, { Component, Fragment } from 'react';
+import { Utils } from '../../../utils/utils';
+import { seafileAPI } from '../../../utils/seafile-api';
+import { siteRoot, loginUrl, gettext, isPro } from '../../../utils/constants';
+import toaster from '../../../components/toast';
+import EmptyTip from '../../../components/empty-tip';
+import Loading from '../../../components/loading';
+import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
+import MainPanelTopbar from '../main-panel-topbar';
+import GroupNav from './group-nav';
+
+const { enableSysAdminViewRepo } = window.sysadmin.pageOptions;
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { loading, errorMsg, items } = this.props;
+ if (loading) {
+ return ;
+ } else if (errorMsg) {
+ return {errorMsg}
;
+ } else {
+ const emptyTip = (
+
+ {gettext('No libraries')}
+
+ );
+ const table = (
+
+
+
+
+ {/* icon */} |
+ {gettext('Name')} |
+ {gettext('Size')} |
+ {gettext('Shared By')} |
+ {/*Operations*/} |
+
+
+
+ {items.map((item, index) => {
+ return ( );
+ })}
+
+
+
+ );
+ return items.length ? table : emptyTip;
+ }
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpIconShown: false,
+ isUnshareRepoDialogOpen: false
+ };
+ }
+
+ handleMouseEnter = () => {
+ this.setState({isOpIconShown: true});
+ }
+
+ handleMouseLeave = () => {
+ this.setState({isOpIconShown: false});
+ }
+
+ toggleUnshareRepoDialog = (e) => {
+ if (e) {
+ e.preventDefault();
+ }
+ this.setState({isUnshareRepoDialogOpen: !this.state.isUnshareRepoDialogOpen});
+ }
+
+ unshareRepo = () => {
+ const { item } = this.props;
+ this.props.unshareRepo(item.repo_id, item.name);
+ this.toggleUnshareRepoDialog();
+ }
+
+ renderRepoName = () => {
+ const { item } = this.props;
+ const repo = item;
+ repo.id = item.repo_id;
+ if (repo.name) {
+ if (isPro && enableSysAdminViewRepo && !repo.encrypted) {
+ return {repo.name};
+ } else {
+ return repo.name;
+ }
+ } else {
+ return '--';
+ }
+ }
+
+ render() {
+ let { isOpIconShown, isUnshareRepoDialogOpen } = this.state;
+ let { item } = this.props;
+
+ let iconUrl = Utils.getLibIconUrl(item);
+ let iconTitle = Utils.getLibIconTitle(item);
+
+ let repoName = '' + Utils.HTMLescape(item.name) + '';
+ let dialogMsg = gettext('Are you sure you want to unshare {placeholder} ?').replace('{placeholder}', repoName);
+
+ return (
+
+
+  |
+ {this.renderRepoName()} |
+ {Utils.bytesToSize(item.size)} |
+
+ {item.shared_by_name}
+ |
+
+
+ |
+
+ {isUnshareRepoDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+class GroupRepos extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ groupName: '',
+ repoList: []
+ };
+ }
+
+ componentDidMount () {
+ seafileAPI.sysAdminListGroupRepos(this.props.groupID).then((res) => {
+ this.setState({
+ loading: false,
+ repoList: res.data.libraries,
+ groupName: res.data.group_name
+ });
+ }).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.')
+ });
+ }
+ });
+ }
+
+ unshareRepo = (repoID, repoName) => {
+ seafileAPI.sysAdminUnshareRepoFromGroup(this.props.groupID, repoID).then(res => {
+ let newRepoList = this.state.repoList.filter(item => {
+ return item.repo_id != repoID;
+ });
+ this.setState({
+ repoList: newRepoList
+ });
+ const msg = gettext('Successfully unshared library {placeholder}')
+ .replace('{placeholder}', repoName);
+ toaster.success(msg);
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default GroupRepos;
diff --git a/frontend/src/pages/sys-admin/groups/groups.js b/frontend/src/pages/sys-admin/groups/groups.js
new file mode 100644
index 0000000000..305b60aba9
--- /dev/null
+++ b/frontend/src/pages/sys-admin/groups/groups.js
@@ -0,0 +1,359 @@
+import React, { Component, Fragment } from 'react';
+import { Button } from 'reactstrap';
+import moment from 'moment';
+import { Utils } from '../../../utils/utils';
+import { seafileAPI } from '../../../utils/seafile-api';
+import { siteRoot, loginUrl, gettext } from '../../../utils/constants';
+import toaster from '../../../components/toast';
+import Loading from '../../../components/loading';
+import EmptyTip from '../../../components/empty-tip';
+import Paginator from '../../../components/paginator';
+import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
+import SysAdminCreateGroupDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-create-group-dialog';
+import SysAdminTransferGroupDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-group-transfer-dialog';
+import MainPanelTopbar from '../main-panel-topbar';
+import OpMenu from './op-menu';
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isItemFreezed: false
+ };
+ }
+
+ onFreezedItem = () => {
+ this.setState({isItemFreezed: true});
+ }
+
+ onUnfreezedItem = () => {
+ this.setState({isItemFreezed: false});
+ }
+
+ getPreviousPage = () => {
+ this.props.getListByPage(this.props.pageInfo.current_page - 1);
+ }
+
+ getNextPage = () => {
+ this.props.getListByPage(this.props.pageInfo.current_page + 1);
+ }
+
+ render() {
+ const { loading, errorMsg, items, pageInfo } = this.props;
+ if (loading) {
+ return ;
+ } else if (errorMsg) {
+ return {errorMsg}
;
+ } else {
+ const emptyTip = (
+
+ {gettext('No groups')}
+
+ );
+ const table = (
+
+
+
+
+ {gettext('Name')} |
+ {gettext('Owner')} |
+ {gettext('Created At')} |
+ {/* operation */} |
+
+
+
+ {items.map((item, index) => {
+ return ( );
+ })}
+
+
+
+
+ );
+ return items.length ? table : emptyTip;
+ }
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpIconShown: false,
+ highlight: false,
+ isDeleteDialogOpen: false,
+ isTransferDialogOpen: false
+ };
+ }
+
+ handleMouseEnter = () => {
+ if (!this.props.isItemFreezed) {
+ this.setState({
+ isOpIconShown: true,
+ highlight: true
+ });
+ }
+ }
+
+ handleMouseLeave = () => {
+ if (!this.props.isItemFreezed) {
+ this.setState({
+ isOpIconShown: false,
+ highlight: false
+ });
+ }
+ }
+
+ onUnfreezedItem = () => {
+ this.setState({
+ highlight: false,
+ isOpIconShow: false
+ });
+ this.props.onUnfreezedItem();
+ }
+
+ onMenuItemClick = (operation) => {
+ switch(operation) {
+ case 'Delete':
+ this.toggleDeleteDialog();
+ break;
+ case 'Transfer':
+ this.toggleTransferDialog();
+ break;
+ default:
+ break;
+ }
+ }
+
+ toggleDeleteDialog = (e) => {
+ if (e) {
+ e.preventDefault();
+ }
+ this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
+ }
+
+ toggleTransferDialog = (e) => {
+ if (e) {
+ e.preventDefault();
+ }
+ this.setState({isTransferDialogOpen: !this.state.isTransferDialogOpen});
+ }
+
+ deleteGroup = () => {
+ this.props.deleteGroup(this.props.item.id);
+ }
+
+ transferGroup = (receiver) => {
+ this.props.transferGroup(this.props.item.id, receiver);
+ }
+
+ render() {
+ const { isOpIconShown, isDeleteDialogOpen, isTransferDialogOpen } = this.state;
+ const { item } = this.props;
+
+ let groupName = '' + Utils.HTMLescape(item.name) + '';
+ let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', groupName);
+
+ const libUrl = item.parent_group_id == 0 ?
+ `${siteRoot}sys/groups/${item.id}/libraries/` :
+ `${siteRoot}sysadmin/#address-book/groups/${item.id}/`;
+
+ return (
+
+
+ {item.name} |
+
+ {item.owner == 'system admin' ?
+ '--' :
+ {item.owner_name}
+ }
+ |
+
+ {moment(item.created_at).fromNow()}
+ |
+
+ {(isOpIconShown && item.owner != 'system admin') &&
+
+ }
+ |
+
+ {isDeleteDialogOpen &&
+
+ }
+ {isTransferDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+class Groups extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ groupList: [],
+ pageInfo: {},
+ perPage: 100,
+ isCreateGroupDialogOpen: false
+ };
+ }
+
+ componentDidMount () {
+ this.getGroupListByPage(1);
+ }
+
+ toggleCreateGroupDialog = () => {
+ this.setState({isCreateGroupDialogOpen: !this.state.isCreateGroupDialogOpen});
+ }
+
+ getGroupListByPage = (page) => {
+ seafileAPI.sysAdminListAllGroups(page, this.state.perPage).then((res) => {
+ this.setState({
+ loading: false,
+ groupList: res.data.groups,
+ pageInfo: res.data.page_info
+ });
+ }).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.')
+ });
+ }
+ });
+ }
+
+ createGroup = (groupName, OnwerEmail) => {
+ seafileAPI.sysAdminCreateNewGroup(groupName, OnwerEmail).then(res => {
+ let newGroupList = this.state.groupList;
+ newGroupList.unshift(res.data);
+ this.setState({
+ groupList: newGroupList
+ });
+ this.toggleCreateGroupDialog();
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ deleteGroup = (groupID) => {
+ seafileAPI.sysAdminDismissGroupByID(groupID).then(res => {
+ let newGroupList = this.state.groupList.filter(item => {
+ return item.id != groupID;
+ });
+ this.setState({
+ groupList: newGroupList
+ });
+ toaster.success(gettext('Successfully deleted 1 item.'));
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ transferGroup = (groupID, receiverEmail) => {
+ seafileAPI.sysAdminTransferGroup(receiverEmail, groupID).then(res => {
+ let newGroupList = this.state.groupList.map(item => {
+ if (item.id == groupID) {
+ item = res.data;
+ }
+ return item;
+ });
+ this.setState({
+ groupList: newGroupList
+ });
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+
+ render() {
+ let { isCreateGroupDialogOpen } = this.state;
+
+ return (
+
+
+
+
+ {gettext('Export Excel')}
+
+
+
+
+
+
{gettext('Groups')}
+
+
+
+
+
+
+ {isCreateGroupDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+export default Groups;
diff --git a/frontend/src/pages/sys-admin/groups/op-menu.js b/frontend/src/pages/sys-admin/groups/op-menu.js
new file mode 100644
index 0000000000..7d7f1c8384
--- /dev/null
+++ b/frontend/src/pages/sys-admin/groups/op-menu.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
+import { gettext } from '../../../utils/constants';
+import { Utils } from '../../../utils/utils';
+
+const propTypes = {
+ onFreezedItem: PropTypes.func.isRequired,
+ onUnfreezedItem: PropTypes.func.isRequired,
+ onMenuItemClick: PropTypes.func.isRequired,
+};
+
+class OpMenu extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isItemMenuShow: false
+ };
+ }
+
+ onMenuItemClick = (e) => {
+ let operation = Utils.getEventData(e, 'op');
+ this.props.onMenuItemClick(operation);
+ }
+
+ onDropdownToggleClick = (e) => {
+ this.toggleOperationMenu(e);
+ }
+
+ toggleOperationMenu = (e) => {
+ this.setState(
+ {isItemMenuShow: !this.state.isItemMenuShow},
+ () => {
+ if (this.state.isItemMenuShow) {
+ this.props.onFreezedItem();
+ } else {
+ this.props.onUnfreezedItem();
+ }
+ }
+ );
+ }
+
+ translateOperations = (item) => {
+ let translateResult = '';
+ switch(item) {
+ case 'Delete':
+ translateResult = gettext('Delete');
+ break;
+ case 'Transfer':
+ translateResult = gettext('Transfer');
+ break;
+ }
+
+ return translateResult;
+ }
+
+ render() {
+ const operations = ['Delete', 'Transfer'];
+ return (
+
+
+
+ {operations.map((item, index )=> {
+ return ({this.translateOperations(item)});
+ })}
+
+
+ );
+ }
+}
+
+OpMenu.propTypes = propTypes;
+
+export default OpMenu;
diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js
index e0968faeab..1852977bf7 100644
--- a/frontend/src/pages/sys-admin/index.js
+++ b/frontend/src/pages/sys-admin/index.js
@@ -16,6 +16,10 @@ import SystemRepo from './repos/system-repo';
import TrashRepos from './repos/trash-repos';
import DirView from './repos/dir-view';
+import Groups from './groups/groups';
+import GroupRepos from './groups/group-repos';
+import GroupMembers from './groups/group-members';
+
import WebSettings from './web-settings/web-settings';
import Notifications from './notifications/notifications';
import FileScanRecords from './file-scan-records';
@@ -49,6 +53,10 @@ class SysAdmin extends React.Component {
tab: 'libraries',
urlPartList: ['all-libraries', 'system-library', 'trash-libraries', 'libraries/']
},
+ {
+ tab: 'groups',
+ urlPartList: ['groups/']
+ },
];
const tmpTab = this.getCurrentTabForPageList(pageList);
currentTab = tmpTab ? tmpTab : currentTab;
@@ -101,6 +109,9 @@ class SysAdmin extends React.Component {
+
+
+
-
+ this.props.tabItemClick('groups')}
+ >
{gettext('Groups')}
-
+
}
{isPro && canManageGroup &&
diff --git a/seahub/urls.py b/seahub/urls.py
index 39805b55a1..f6f45ec575 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -646,7 +646,9 @@ urlpatterns = [
url(r'^sys/trash-libraries/$', sysadmin_react_fake_view, name="sys_trash_libraries"),
url(r'^sys/libraries/(?P[-0-9a-f]{36})/$', sysadmin_react_fake_view, name="sys_libraries_template"),
url(r'^sys/libraries/(?P[-0-9a-f]{36})/(?P[^/]+)/(?P.*)$', sysadmin_react_fake_view, name="sys_libraries_template_dirent"),
-
+ 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/work-weixin/$', sysadmin_react_fake_view, name="sys_work_weixin"),
url(r'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"),