diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-rename-department-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-rename-department-dialog.js new file mode 100644 index 0000000000..42f84b075d --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-rename-department-dialog.js @@ -0,0 +1,103 @@ +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'; +import toaster from '../../../components/toast'; + +const propTypes = { + groupID: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]).isRequired, + name: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, + onDepartmentNameChanged: PropTypes.func.isRequired +}; + +class RenameDepartmentDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + departmentName: this.props.name, + errMessage: '' + }; + this.newInput = React.createRef(); + } + + componentDidMount() { + this.newInput.select(); + this.newInput.focus(); + } + + handleSubmit = () => { + let isValid = this.validateName(); + if (isValid) { + seafileAPI.sysAdminRenameDepartment(this.props.groupID, this.state.departmentName.trim()).then((res) => { + this.props.toggle(); + this.props.onDepartmentNameChanged(res.data); + toaster.success(gettext('Success')); + }).catch(error => { + let errorMsg = Utils.getErrorMsg(error); + this.setState({ errMessage: errorMsg }); + }); + } + } + + validateName = () => { + let errMessage = ''; + const name = this.state.departmentName.trim(); + if (!name.length) { + errMessage = gettext('Name is required'); + this.setState({ errMessage: errMessage }); + return false; + } + return true; + } + + handleChange = (e) => { + this.setState({ + departmentName: e.target.value + }); + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + e.preventDefault(); + } + } + + render() { + let header = gettext('Rename Department'); + return ( + + {header} + +
+ + + {this.newInput = input;}} + /> + +
+ {this.state.errMessage &&

{this.state.errMessage}

} +
+ + + +
+ ); + } +} + +RenameDepartmentDialog.propTypes = propTypes; + +export default RenameDepartmentDialog; diff --git a/frontend/src/pages/sys-admin/departments/department-detail.js b/frontend/src/pages/sys-admin/departments/department-detail.js index 87d1fe9f5d..31f2e928e8 100644 --- a/frontend/src/pages/sys-admin/departments/department-detail.js +++ b/frontend/src/pages/sys-admin/departments/department-detail.js @@ -9,6 +9,7 @@ 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 RenameDepartmentDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-rename-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'; @@ -46,6 +47,7 @@ class DepartmentDetail extends React.Component { showDeleteMemberDialog: false, repos: [], deletedRepo: {}, + isShowRenameDepartmentDialog: false, isShowAddRepoDialog: false, showDeleteRepoDialog: false, groups: [], @@ -144,6 +146,12 @@ class DepartmentDetail extends React.Component { this.listSubDepartGroups(this.props.groupID); } + onDepartmentNameChanged = (dept) => { + this.setState({ + groupName: dept.name + }); + } + onRepoChanged = () => { this.listGroupRepo(this.props.groupID); } @@ -164,6 +172,10 @@ class DepartmentDetail extends React.Component { this.setState({ showDeleteRepoDialog: true, deletedRepo: repo }); } + toggleRenameDepartmentDialog = () => { + this.setState({ isShowRenameDepartmentDialog: !this.state.isShowRenameDepartmentDialog }); + } + toggleAddRepoDialog = () => { this.setState({ isShowAddRepoDialog: !this.state.isShowAddRepoDialog }); } @@ -192,18 +204,29 @@ class DepartmentDetail extends React.Component { } render() { - const { members, membersErrorMsg, repos, groups } = this.state; + const { members, membersErrorMsg, repos, groups, groupName } = this.state; const groupID = this.props.groupID; const topBtn = 'btn btn-secondary operation-item'; const topbarChildren = ( {groupID && + } + {this.state.isShowRenameDepartmentDialog && ( + + + + )} {this.state.isShowAddMemberDialog && ( {membersErrorMsg ?

{membersErrorMsg}

: members.length == 0 ? -

{gettext('No members')}

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

{gettext('No members')}

: + + + + + + + + + + + + {members.map((member, index) => { + return ( + + + + ); + })} + +
{gettext('Name')}{gettext('Role')}
+ {this.state.membersPageInfo && + + } +
} diff --git a/frontend/src/pages/sys-admin/departments/departments-list.js b/frontend/src/pages/sys-admin/departments/departments-list.js index 8a208b7460..0884e6e68a 100644 --- a/frontend/src/pages/sys-admin/departments/departments-list.js +++ b/frontend/src/pages/sys-admin/departments/departments-list.js @@ -1,5 +1,4 @@ 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'; @@ -24,6 +23,7 @@ class DepartmentsList extends React.Component { showDeleteDepartDialog: false, showSetGroupQuotaDialog: false, isShowAddDepartDialog: false, + isItemFreezed: false }; } @@ -31,6 +31,14 @@ class DepartmentsList extends React.Component { this.listDepartGroups(); } + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); + } + listDepartGroups = () => { seafileAPI.sysAdminListAllDepartments().then(res => { this.setState({ groups: res.data.data }); @@ -46,7 +54,7 @@ class DepartmentsList extends React.Component { } toggleAddDepartDialog = () => { - this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog}); + this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog }); } toggleCancel = () => { @@ -60,6 +68,17 @@ class DepartmentsList extends React.Component { this.listDepartGroups(); } + onDepartmentNameChanged = (dept) => { + this.setState({ + groups: this.state.groups.map(item => { + if (item.id == dept.id) { + item.name = dept.name; + } + return item; + }) + }); + } + render() { const groups = this.state.groups; const topbarChildren = ( @@ -104,6 +123,10 @@ class DepartmentsList extends React.Component { diff --git a/frontend/src/pages/sys-admin/departments/group-item.js b/frontend/src/pages/sys-admin/departments/group-item.js index ac6eb77c41..f69d902f39 100644 --- a/frontend/src/pages/sys-admin/departments/group-item.js +++ b/frontend/src/pages/sys-admin/departments/group-item.js @@ -1,12 +1,18 @@ -import React from 'react'; +import React, { Fragment } 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'; +import { siteRoot, gettext } from '../../../utils/constants'; +import OpMenu from '../../../components/dialog/op-menu'; +import RenameDepartmentDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-rename-department-dialog'; const GroupItemPropTypes = { + isItemFreezed: PropTypes.bool.isRequired, + onFreezedItem: PropTypes.func.isRequired, + onUnfreezedItem: PropTypes.func.isRequired, group: PropTypes.object.isRequired, + onDepartmentNameChanged: PropTypes.func.isRequired, showSetGroupQuotaDialog: PropTypes.func.isRequired, showDeleteDepartDialog: PropTypes.func.isRequired, }; @@ -16,34 +22,108 @@ class GroupItem extends React.Component { constructor(props) { super(props); this.state = { + isOpIconShown: false, highlight: false, + isRenameDialogOpen: false }; } - onMouseEnter = () => { - this.setState({ highlight: true }); + handleMouseOver = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: true, + highlight: true + }); + } } - onMouseLeave = () => { - this.setState({ highlight: false }); + handleMouseOut = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: false, + highlight: false + }); + } + } + + onUnfreezedItem = () => { + this.setState({ + highlight: false, + isOpIconShow: false + }); + this.props.onUnfreezedItem(); + } + + translateOperations = (item) => { + let translateResult = ''; + switch(item) { + case 'Rename': + translateResult = gettext('Rename'); + break; + case 'Delete': + translateResult = gettext('Delete'); + break; + default: + break; + } + + return translateResult; + } + + onMenuItemClick = (operation) => { + const { group } = this.props; + switch(operation) { + case 'Rename': + this.toggleRenameDialog(); + break; + case 'Delete': + this.props.showDeleteDepartDialog(group); + break; + default: + break; + } + } + + toggleRenameDialog = () => { + this.setState({ + isRenameDialogOpen: !this.state.isRenameDialogOpen + }); } render() { - const group = this.props.group; - const highlight = this.state.highlight; + const { group } = this.props; + const { highlight, isOpIconShown, isRenameDialogOpen } = this.state; const newHref = siteRoot+ 'sys/departments/' + group.id + '/'; return ( - - {group.name} - {moment(group.created_at).fromNow()} - - {Utils.bytesToSize(group.quota)}{' '} - - - - - - + + + {group.name} + {moment(group.created_at).fromNow()} + + {Utils.bytesToSize(group.quota)}{' '} + + + + {isOpIconShown && + + } + + + {isRenameDialogOpen && ( + + )} + ); } } diff --git a/seahub/api2/endpoints/admin/groups.py b/seahub/api2/endpoints/admin/groups.py index ab5dfa2a2f..7654357541 100644 --- a/seahub/api2/endpoints/admin/groups.py +++ b/seahub/api2/endpoints/admin/groups.py @@ -16,7 +16,7 @@ from seahub.base.templatetags.seahub_tags import email2nickname from seahub.utils import is_valid_username, is_pro_version from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seahub.group.utils import is_group_member, is_group_admin, \ - validate_group_name + validate_group_name, check_group_name_conflict, set_group_name_cache from seahub.admin_log.signals import admin_operation from seahub.admin_log.models import GROUP_CREATE, GROUP_DELETE, GROUP_TRANSFER from seahub.api2.utils import api_error @@ -26,6 +26,7 @@ from seahub.share.models import ExtraGroupsSharePermission logger = logging.getLogger(__name__) + def get_group_info(group_id): group = ccnet_api.get_group(group_id) isoformat_timestr = timestamp_to_isoformat_timestr(group.timestamp) @@ -41,6 +42,7 @@ def get_group_info(group_id): return group_info + class AdminGroups(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) @@ -160,7 +162,7 @@ class AdminGroups(APIView): "owner": new_owner, } admin_operation.send(sender=None, admin_name=username, - operation=GROUP_CREATE, detail=admin_op_detail) + operation=GROUP_CREATE, detail=admin_op_detail) # get info of new group group_info = get_group_info(group_id) @@ -178,7 +180,8 @@ class AdminGroup(APIView): """ Admin update a group 1. transfer a group. - 2. set group quota + 2. set group quota. + 3. rename group. Permission checking: 1. Admin user; @@ -188,7 +191,7 @@ class AdminGroup(APIView): return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') # recourse check - group_id = int(group_id) # Checked by URL Conf + group_id = int(group_id) # Checked by URL Conf group = ccnet_api.get_group(group_id) if not group: error_msg = 'Group %d not found.' % group_id @@ -236,7 +239,7 @@ class AdminGroup(APIView): "to": new_owner, } admin_operation.send(sender=None, admin_name=request.user.username, - operation=GROUP_TRANSFER, detail=admin_op_detail) + operation=GROUP_TRANSFER, detail=admin_op_detail) # set group quota group_quota = request.data.get('quota', '') @@ -258,6 +261,25 @@ class AdminGroup(APIView): error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + new_name = request.data.get('name', '') + if new_name: + if not validate_group_name(new_name): + + error_msg = _('Name can only contain letters, numbers, spaces, hyphen, dot, single quote, brackets or underscore.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if check_group_name_conflict(request, new_name): + error_msg = _('There is already a group with that name.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + ccnet_api.set_group_name(group_id, new_name) + set_group_name_cache(group_id, new_name) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + group_info = get_group_info(group_id) return Response(group_info) @@ -296,7 +318,7 @@ class AdminGroup(APIView): "owner": group_owner, } admin_operation.send(sender=None, admin_name=request.user.username, - operation=GROUP_DELETE, detail=admin_op_detail) + operation=GROUP_DELETE, detail=admin_op_detail) return Response({'success': True})