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.newInput = input;}} + value={this.state.groupName} + onChange={this.handleRepoNameChange} + /> + + + +
+ {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 = ( + + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{/* icon */}{gettext('Name')}{gettext('Role')}{/*Operations*/}
+
+ ); + 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 = ( + + + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{/* icon */}{gettext('Name')}{gettext('Size')}{gettext('Shared By')}{/*Operations*/}
+
+ ); + 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 ( + + + {iconTitle} + {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 = ( + + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{gettext('Name')}{gettext('Owner')}{gettext('Created At')}{/* operation */}
+ +
+ ); + 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"),