From 1c305d5f69e4e6a1bfc620386bb74359b03f2937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=81=A5=E8=BE=89?= <40563566+mrwangjianhui@users.noreply.github.com> Date: Fri, 11 Feb 2022 11:43:17 +0800 Subject: [PATCH] improve org admin manage department --- .../dialog/org-rename-department-dialog.js | 108 ++++++++++++++ .../pages/org-admin/org-department-item.js | 133 ++++++++++++++++-- .../pages/org-admin/org-departments-list.js | 133 ++++++++++++++++-- .../organizations/api/address_book/groups.py | 55 +++++++- 4 files changed, 400 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/dialog/org-rename-department-dialog.js diff --git a/frontend/src/components/dialog/org-rename-department-dialog.js b/frontend/src/components/dialog/org-rename-department-dialog.js new file mode 100644 index 0000000000..8aa03773d7 --- /dev/null +++ b/frontend/src/components/dialog/org-rename-department-dialog.js @@ -0,0 +1,108 @@ +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 '../toast'; + +const propTypes = { + groupID: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]).isRequired, + orgID: 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(); + const { orgID, groupID } = this.props; + if (isValid) { + seafileAPI.orgAdminUpdateDepartGroup(orgID, 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} + + + + {gettext('Name')} + {this.newInput = input;}} + /> + + + {this.state.errMessage && {this.state.errMessage}} + + + {gettext('Submit')} + + + ); + } +} + +RenameDepartmentDialog.propTypes = propTypes; + +export default RenameDepartmentDialog; diff --git a/frontend/src/pages/org-admin/org-department-item.js b/frontend/src/pages/org-admin/org-department-item.js index e76e3fc11e..cb759c2ef1 100644 --- a/frontend/src/pages/org-admin/org-department-item.js +++ b/frontend/src/pages/org-admin/org-department-item.js @@ -15,6 +15,8 @@ import AddRepoDialog from '../../components/dialog/org-add-repo-dialog'; import DeleteRepoDialog from '../../components/dialog/org-delete-repo-dialog'; import DeleteDepartDialog from '../../components/dialog/org-delete-department-dialog'; import SetGroupQuotaDialog from '../../components/dialog/org-set-group-quota-dialog'; +import RenameDepartmentDialog from '../../components/dialog/org-rename-department-dialog'; +import OpMenu from '../../components/dialog/op-menu'; import { serviceURL, siteRoot, gettext, orgID, lang } from '../../utils/constants'; import '../../css/org-department-item.css'; @@ -27,6 +29,7 @@ class OrgDepartmentItem extends React.Component { this.state = { groupName: '', isItemFreezed: false, + isDepartFreezed: false, ancestorGroups: [], members: [], deletedMember: {}, @@ -99,6 +102,25 @@ class OrgDepartmentItem extends React.Component { }); } + onFreezedDepart = () => { + this.setState({isDepartFreezed: true}); + } + + onUnfreezedDepart = () => { + this.setState({isDepartFreezed: false}); + } + + onDepartmentNameChanged = (dept) => { + this.setState({ + groups: this.state.groups.map(item => { + if (item.id == dept.id) { + item.name = dept.name; + } + return item; + }) + }); + } + onSubDepartChanged = () => { this.listSubDepartGroups(this.props.groupID); } @@ -237,6 +259,10 @@ class OrgDepartmentItem extends React.Component { @@ -490,33 +516,108 @@ class GroupItem extends React.Component { super(props); this.state = { highlight: false, + isOpIconShown: false, + isRenameDialogOpen: false, }; } onMouseEnter = () => { - this.setState({ highlight: true }); + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: true, + highlight: true + }); + } } onMouseLeave = () => { - this.setState({ highlight: false }); + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: false, + highlight: false + }); + } + } + + 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; + } + } + + onUnfreezedItem = () => { + this.setState({ + highlight: false, + isOpIconShow: false + }); + this.props.onUnfreezedItem(); + } + + toggleRenameDialog = () => { + this.setState({ + isRenameDialogOpen: !this.state.isRenameDialogOpen + }); } render() { const group = this.props.group; - const highlight = this.state.highlight; + const { highlight, isOpIconShown, isRenameDialogOpen } = this.state; const newHref = siteRoot+ 'org/departmentadmin/groups/' + 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 && ( + + )} + ); } } @@ -524,6 +625,10 @@ class GroupItem extends React.Component { const GroupItemPropTypes = { group: PropTypes.object.isRequired, groupID: PropTypes.string, + isItemFreezed: PropTypes.bool.isRequired, + onFreezedItem: PropTypes.func.isRequired, + onUnfreezedItem: PropTypes.func.isRequired, + onDepartmentNameChanged: PropTypes.func.isRequired, showSetGroupQuotaDialog: PropTypes.func.isRequired, showDeleteDepartDialog: PropTypes.func.isRequired, isSubdepartChanged: PropTypes.bool, diff --git a/frontend/src/pages/org-admin/org-departments-list.js b/frontend/src/pages/org-admin/org-departments-list.js index 1ff99a651a..9647b60186 100644 --- a/frontend/src/pages/org-admin/org-departments-list.js +++ b/frontend/src/pages/org-admin/org-departments-list.js @@ -9,6 +9,8 @@ import ModalPortal from '../../components/modal-portal'; import AddDepartDialog from '../../components/dialog/org-add-department-dialog'; import DeleteDepartDialog from '../../components/dialog/org-delete-department-dialog'; import SetGroupQuotaDialog from '../../components/dialog/org-set-group-quota-dialog'; +import RenameDepartmentDialog from '../../components/dialog/org-rename-department-dialog'; +import OpMenu from '../../components/dialog/op-menu'; import { siteRoot, gettext, orgID, lang } from '../../utils/constants'; import '../../css/org-department-item.css'; @@ -25,6 +27,7 @@ class OrgDepartmentsList extends React.Component { showDeleteDepartDialog: false, showSetGroupQuotaDialog: false, isShowAddDepartDialog: false, + isItemFreezed: false, }; } @@ -38,6 +41,25 @@ class OrgDepartmentsList extends React.Component { }); } + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); + } + + onDepartmentNameChanged = (dept) => { + this.setState({ + groups: this.state.groups.map(item => { + if (item.id == dept.id) { + item.name = dept.name; + } + return item; + }) + }); + } + showDeleteDepartDialog = (group) => { this.setState({ showDeleteDepartDialog: true, groupID: group.id, groupName: group.name }); } @@ -105,6 +127,10 @@ class OrgDepartmentsList extends React.Component { @@ -151,39 +177,118 @@ class GroupItem extends React.Component { super(props); this.state = { highlight: false, + isOpIconShown: false, + isRenameDialogOpen: false, }; } onMouseEnter = () => { - this.setState({ highlight: true }); + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: true, + highlight: true + }); + } } onMouseLeave = () => { - this.setState({ highlight: false }); + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: false, + highlight: false + }); + } + } + + 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; + } + } + + onUnfreezedItem = () => { + this.setState({ + highlight: false, + isOpIconShow: false + }); + this.props.onUnfreezedItem(); + } + + toggleRenameDialog = () => { + this.setState({ + isRenameDialogOpen: !this.state.isRenameDialogOpen + }); } render() { const group = this.props.group; - const highlight = this.state.highlight; + const { highlight, isOpIconShown, isRenameDialogOpen } = this.state; const newHref = siteRoot+ 'org/departmentadmin/groups/' + 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 && ( + + )} + ); } } const GroupItemPropTypes = { group: PropTypes.object.isRequired, + isItemFreezed: PropTypes.bool.isRequired, + onFreezedItem: PropTypes.func.isRequired, + onUnfreezedItem: PropTypes.func.isRequired, + onDepartmentNameChanged: PropTypes.func.isRequired, showSetGroupQuotaDialog: PropTypes.func.isRequired, showDeleteDepartDialog: PropTypes.func.isRequired, }; diff --git a/seahub/organizations/api/address_book/groups.py b/seahub/organizations/api/address_book/groups.py index 0f1c7c9129..3009bdf561 100644 --- a/seahub/organizations/api/address_book/groups.py +++ b/seahub/organizations/api/address_book/groups.py @@ -1,10 +1,11 @@ import logging from rest_framework.authentication import SessionAuthentication +from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status -from seaserv import ccnet_api +from seaserv import ccnet_api, seafile_api from seahub.api2.utils import api_error from seahub.organizations.views import get_org_id_by_group @@ -17,6 +18,10 @@ from seahub.api2.endpoints.admin.address_book.groups import ( ) from seahub.organizations.api.permissions import IsOrgAdmin from seahub.organizations.api.utils import check_org_admin +from seahub.utils import is_pro_version +from seahub.base.templatetags.seahub_tags import email2nickname +from seahub.utils.timeutils import timestamp_to_isoformat_timestr +from seahub.group.utils import validate_group_name, set_group_name_cache logger = logging.getLogger(__name__) @@ -83,3 +88,51 @@ class AdminAddressBookGroup(APIView): return api_error(status.HTTP_404_NOT_FOUND, error_msg) return SysAdminAddressBookGroup().delete(request, group_id) + + @check_org_admin + def put(self, request, org_id, group_id): + """ Update an org address book group. + """ + # resource check + org_id = int(org_id) + if not ccnet_api.get_org_by_id(org_id): + error_msg = 'Organization %s not found.' % org_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + group_id = int(group_id) + if get_org_id_by_group(group_id) != org_id: + error_msg = 'Group %s not found.' % group_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + new_group_name = request.data.get('group_name', '').strip() + if not new_group_name: + error_msg = 'name %s invalid.' % new_group_name + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # Check whether group name is validate. + if not validate_group_name(new_group_name): + error_msg = 'Group name can only contain letters, numbers, blank, hyphen, dot, single quote or underscore' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + ccnet_api.set_group_name(group_id, new_group_name) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + set_group_name_cache(group_id, new_group_name) + + group = ccnet_api.get_group(group_id) + isoformat_timestr = timestamp_to_isoformat_timestr(group.timestamp) + group_info = { + "id": group.id, + "name": group.group_name, + "owner": group.creator_name, + "owner_name": email2nickname(group.creator_name), + "created_at": isoformat_timestr, + "quota": seafile_api.get_group_quota(group_id) if is_pro_version() else 0, + "parent_group_id": group.parent_group_id if is_pro_version() else 0 + } + + return Response(group_info)
{this.state.errMessage}