1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-04 08:28:11 +00:00

admin rename department (#4849)

* admin rename department

* update Code comment

* update group name validate msg

* [department] fixup: added 'add member' back

* [department] rename: fixup & improvements

* [system admin] departments: added 'rename' & 'op menu' for dept

Co-authored-by: lian <lian@seafile.com>
Co-authored-by: llj <lingjun.li1@gmail.com>
This commit is contained in:
lian
2021-03-25 21:41:55 +08:00
committed by GitHub
parent 1d108ae828
commit 855b457e5f
5 changed files with 318 additions and 67 deletions

View File

@@ -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 (
<Modal isOpen={true} toggle={this.props.toggle}>
<ModalHeader toggle={this.props.toggle}>{header}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="departmentName">{gettext('Name')}</Label>
<Input
id="departmentName"
onKeyPress={this.handleKeyPress}
value={this.state.departmentName}
onChange={this.handleChange}
innerRef={input => {this.newInput = input;}}
/>
</FormGroup>
</Form>
{this.state.errMessage && <p className="error">{this.state.errMessage}</p>}
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
RenameDepartmentDialog.propTypes = propTypes;
export default RenameDepartmentDialog;

View File

@@ -9,6 +9,7 @@ import toaster from '../../../components/toast';
import MainPanelTopbar from '../main-panel-topbar'; import MainPanelTopbar from '../main-panel-topbar';
import ModalPortal from '../../../components/modal-portal'; import ModalPortal from '../../../components/modal-portal';
import AddDepartDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-department-dialog'; 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 AddMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-member-dialog';
import DeleteMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-delete-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 AddRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-repo-dialog';
@@ -46,6 +47,7 @@ class DepartmentDetail extends React.Component {
showDeleteMemberDialog: false, showDeleteMemberDialog: false,
repos: [], repos: [],
deletedRepo: {}, deletedRepo: {},
isShowRenameDepartmentDialog: false,
isShowAddRepoDialog: false, isShowAddRepoDialog: false,
showDeleteRepoDialog: false, showDeleteRepoDialog: false,
groups: [], groups: [],
@@ -144,6 +146,12 @@ class DepartmentDetail extends React.Component {
this.listSubDepartGroups(this.props.groupID); this.listSubDepartGroups(this.props.groupID);
} }
onDepartmentNameChanged = (dept) => {
this.setState({
groupName: dept.name
});
}
onRepoChanged = () => { onRepoChanged = () => {
this.listGroupRepo(this.props.groupID); this.listGroupRepo(this.props.groupID);
} }
@@ -164,6 +172,10 @@ class DepartmentDetail extends React.Component {
this.setState({ showDeleteRepoDialog: true, deletedRepo: repo }); this.setState({ showDeleteRepoDialog: true, deletedRepo: repo });
} }
toggleRenameDepartmentDialog = () => {
this.setState({ isShowRenameDepartmentDialog: !this.state.isShowRenameDepartmentDialog });
}
toggleAddRepoDialog = () => { toggleAddRepoDialog = () => {
this.setState({ isShowAddRepoDialog: !this.state.isShowAddRepoDialog }); this.setState({ isShowAddRepoDialog: !this.state.isShowAddRepoDialog });
} }
@@ -192,18 +204,29 @@ class DepartmentDetail extends React.Component {
} }
render() { render() {
const { members, membersErrorMsg, repos, groups } = this.state; const { members, membersErrorMsg, repos, groups, groupName } = this.state;
const groupID = this.props.groupID; const groupID = this.props.groupID;
const topBtn = 'btn btn-secondary operation-item'; const topBtn = 'btn btn-secondary operation-item';
const topbarChildren = ( const topbarChildren = (
<Fragment> <Fragment>
{groupID && {groupID &&
<Fragment> <Fragment>
<button className={topBtn} title={gettext('Rename Department')} onClick={this.toggleRenameDepartmentDialog}>{gettext('Rename Department')}</button>
<button className={topBtn} title={gettext('New Sub-department')} onClick={this.toggleAddDepartDialog}>{gettext('New Sub-department')}</button> <button className={topBtn} title={gettext('New Sub-department')} onClick={this.toggleAddDepartDialog}>{gettext('New Sub-department')}</button>
<button className={topBtn} title={gettext('Add Member')} onClick={this.toggleAddMemberDialog}>{gettext('Add Member')}</button> <button className={topBtn} title={gettext('Add Member')} onClick={this.toggleAddMemberDialog}>{gettext('Add Member')}</button>
<button className={topBtn} onClick={this.toggleAddRepoDialog} title={gettext('New Library')}>{gettext('New Library')}</button> <button className={topBtn} onClick={this.toggleAddRepoDialog} title={gettext('New Library')}>{gettext('New Library')}</button>
</Fragment> </Fragment>
} }
{this.state.isShowRenameDepartmentDialog && (
<ModalPortal>
<RenameDepartmentDialog
groupID={groupID}
name={groupName}
toggle={this.toggleRenameDepartmentDialog}
onDepartmentNameChanged={this.onDepartmentNameChanged}
/>
</ModalPortal>
)}
{this.state.isShowAddMemberDialog && ( {this.state.isShowAddMemberDialog && (
<ModalPortal> <ModalPortal>
<AddMemberDialog <AddMemberDialog
@@ -296,45 +319,45 @@ class DepartmentDetail extends React.Component {
<div className="cur-view-content"> <div className="cur-view-content">
{membersErrorMsg ? <p className="error text-center">{membersErrorMsg}</p> : {membersErrorMsg ? <p className="error text-center">{membersErrorMsg}</p> :
members.length == 0 ? members.length == 0 ?
<p className="no-member">{gettext('No members')}</p> : <p className="no-member">{gettext('No members')}</p> :
<Fragment> <Fragment>
<table> <table>
<thead> <thead>
<tr> <tr>
<th width="5%"></th> <th width="5%"></th>
<th width="50%">{gettext('Name')}</th> <th width="50%">{gettext('Name')}</th>
<th width="15%">{gettext('Role')}</th> <th width="15%">{gettext('Role')}</th>
<th width="30%"></th> <th width="30%"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{members.map((member, index) => { {members.map((member, index) => {
return ( return (
<Fragment key={index}> <Fragment key={index}>
<MemberItem <MemberItem
member={member} member={member}
showDeleteMemberDialog={this.showDeleteMemberDialog} showDeleteMemberDialog={this.showDeleteMemberDialog}
isItemFreezed={this.state.isItemFreezed} isItemFreezed={this.state.isItemFreezed}
onMemberChanged={this.onMemberChanged} onMemberChanged={this.onMemberChanged}
toggleItemFreezed={this.toggleItemFreezed} toggleItemFreezed={this.toggleItemFreezed}
groupID={groupID} groupID={groupID}
/> />
</Fragment> </Fragment>
); );
})} })}
</tbody> </tbody>
</table> </table>
{this.state.membersPageInfo && {this.state.membersPageInfo &&
<Paginator <Paginator
gotoPreviousPage={this.getPreviousPageList} gotoPreviousPage={this.getPreviousPageList}
gotoNextPage={this.getNextPageList} gotoNextPage={this.getNextPageList}
currentPage={this.state.membersPageInfo.current_page} currentPage={this.state.membersPageInfo.current_page}
hasNextPage={this.state.membersPageInfo.has_next_page} hasNextPage={this.state.membersPageInfo.has_next_page}
curPerPage={this.state.membersPerPage} curPerPage={this.state.membersPerPage}
resetPerPage={this.resetPerPage} resetPerPage={this.resetPerPage}
/> />
} }
</Fragment> </Fragment>
} }
</div> </div>
</div> </div>

View File

@@ -1,5 +1,4 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import { seafileAPI } from '../../../utils/seafile-api'; import { seafileAPI } from '../../../utils/seafile-api';
import MainPanelTopbar from '../main-panel-topbar'; import MainPanelTopbar from '../main-panel-topbar';
@@ -24,6 +23,7 @@ class DepartmentsList extends React.Component {
showDeleteDepartDialog: false, showDeleteDepartDialog: false,
showSetGroupQuotaDialog: false, showSetGroupQuotaDialog: false,
isShowAddDepartDialog: false, isShowAddDepartDialog: false,
isItemFreezed: false
}; };
} }
@@ -31,6 +31,14 @@ class DepartmentsList extends React.Component {
this.listDepartGroups(); this.listDepartGroups();
} }
onFreezedItem = () => {
this.setState({isItemFreezed: true});
}
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
}
listDepartGroups = () => { listDepartGroups = () => {
seafileAPI.sysAdminListAllDepartments().then(res => { seafileAPI.sysAdminListAllDepartments().then(res => {
this.setState({ groups: res.data.data }); this.setState({ groups: res.data.data });
@@ -46,7 +54,7 @@ class DepartmentsList extends React.Component {
} }
toggleAddDepartDialog = () => { toggleAddDepartDialog = () => {
this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog}); this.setState({ isShowAddDepartDialog: !this.state.isShowAddDepartDialog });
} }
toggleCancel = () => { toggleCancel = () => {
@@ -60,6 +68,17 @@ class DepartmentsList extends React.Component {
this.listDepartGroups(); this.listDepartGroups();
} }
onDepartmentNameChanged = (dept) => {
this.setState({
groups: this.state.groups.map(item => {
if (item.id == dept.id) {
item.name = dept.name;
}
return item;
})
});
}
render() { render() {
const groups = this.state.groups; const groups = this.state.groups;
const topbarChildren = ( const topbarChildren = (
@@ -104,6 +123,10 @@ class DepartmentsList extends React.Component {
<Fragment key={group.id}> <Fragment key={group.id}>
<GroupItem <GroupItem
group={group} group={group}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
onDepartmentNameChanged={this.onDepartmentNameChanged}
showDeleteDepartDialog={this.showDeleteDepartDialog} showDeleteDepartDialog={this.showDeleteDepartDialog}
showSetGroupQuotaDialog={this.showSetGroupQuotaDialog} showSetGroupQuotaDialog={this.showSetGroupQuotaDialog}
/> />

View File

@@ -1,12 +1,18 @@
import React from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import { Link } from '@reach/router'; import { Link } from '@reach/router';
import { Utils } from '../../../utils/utils.js'; 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 = { const GroupItemPropTypes = {
isItemFreezed: PropTypes.bool.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired,
group: PropTypes.object.isRequired, group: PropTypes.object.isRequired,
onDepartmentNameChanged: PropTypes.func.isRequired,
showSetGroupQuotaDialog: PropTypes.func.isRequired, showSetGroupQuotaDialog: PropTypes.func.isRequired,
showDeleteDepartDialog: PropTypes.func.isRequired, showDeleteDepartDialog: PropTypes.func.isRequired,
}; };
@@ -16,34 +22,108 @@ class GroupItem extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
isOpIconShown: false,
highlight: false, highlight: false,
isRenameDialogOpen: false
}; };
} }
onMouseEnter = () => { handleMouseOver = () => {
this.setState({ highlight: true }); if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
} }
onMouseLeave = () => { handleMouseOut = () => {
this.setState({ highlight: false }); 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() { render() {
const group = this.props.group; const { group } = this.props;
const highlight = this.state.highlight; const { highlight, isOpIconShown, isRenameDialogOpen } = this.state;
const newHref = siteRoot+ 'sys/departments/' + group.id + '/'; const newHref = siteRoot+ 'sys/departments/' + group.id + '/';
return ( return (
<tr className={highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <Fragment>
<td><Link to={newHref}>{group.name}</Link></td> <tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td>{moment(group.created_at).fromNow()}</td> <td><Link to={newHref}>{group.name}</Link></td>
<td onClick={this.props.showSetGroupQuotaDialog.bind(this, group.id)}> <td>{moment(group.created_at).fromNow()}</td>
{Utils.bytesToSize(group.quota)}{' '} <td onClick={this.props.showSetGroupQuotaDialog.bind(this, group.id)}>
<span title="Edit Quota" className={`fa fa-pencil-alt attr-action-icon ${highlight ? '' : 'vh'}`}></span> {Utils.bytesToSize(group.quota)}{' '}
</td> <span title="Edit Quota" className={`fa fa-pencil-alt attr-action-icon ${highlight ? '' : 'vh'}`}></span>
<td className="cursor-pointer text-center" onClick={this.props.showDeleteDepartDialog.bind(this, group)}> </td>
<span className={`sf2-icon-delete action-icon ${highlight ? '' : 'vh'}`} title="Delete"></span> <td>
</td> {isOpIconShown &&
</tr> <OpMenu
operations={['Rename', 'Delete']}
translateOperations={this.translateOperations}
onMenuItemClick={this.onMenuItemClick}
onFreezedItem={this.props.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
}
</td>
</tr>
{isRenameDialogOpen && (
<RenameDepartmentDialog
groupID={group.id}
name={group.name}
toggle={this.toggleRenameDialog}
onDepartmentNameChanged={this.props.onDepartmentNameChanged}
/>
)}
</Fragment>
); );
} }
} }

View File

@@ -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 import is_valid_username, is_pro_version
from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.group.utils import is_group_member, is_group_admin, \ 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.signals import admin_operation
from seahub.admin_log.models import GROUP_CREATE, GROUP_DELETE, GROUP_TRANSFER from seahub.admin_log.models import GROUP_CREATE, GROUP_DELETE, GROUP_TRANSFER
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
@@ -26,6 +26,7 @@ from seahub.share.models import ExtraGroupsSharePermission
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_group_info(group_id): def get_group_info(group_id):
group = ccnet_api.get_group(group_id) group = ccnet_api.get_group(group_id)
isoformat_timestr = timestamp_to_isoformat_timestr(group.timestamp) isoformat_timestr = timestamp_to_isoformat_timestr(group.timestamp)
@@ -41,6 +42,7 @@ def get_group_info(group_id):
return group_info return group_info
class AdminGroups(APIView): class AdminGroups(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
@@ -160,7 +162,7 @@ class AdminGroups(APIView):
"owner": new_owner, "owner": new_owner,
} }
admin_operation.send(sender=None, admin_name=username, 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 # get info of new group
group_info = get_group_info(group_id) group_info = get_group_info(group_id)
@@ -178,7 +180,8 @@ class AdminGroup(APIView):
""" Admin update a group """ Admin update a group
1. transfer a group. 1. transfer a group.
2. set group quota 2. set group quota.
3. rename group.
Permission checking: Permission checking:
1. Admin user; 1. Admin user;
@@ -188,7 +191,7 @@ class AdminGroup(APIView):
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
# recourse check # 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) group = ccnet_api.get_group(group_id)
if not group: if not group:
error_msg = 'Group %d not found.' % group_id error_msg = 'Group %d not found.' % group_id
@@ -236,7 +239,7 @@ class AdminGroup(APIView):
"to": new_owner, "to": new_owner,
} }
admin_operation.send(sender=None, admin_name=request.user.username, 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 # set group quota
group_quota = request.data.get('quota', '') group_quota = request.data.get('quota', '')
@@ -258,6 +261,25 @@ class AdminGroup(APIView):
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) 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) group_info = get_group_info(group_id)
return Response(group_info) return Response(group_info)
@@ -296,7 +318,7 @@ class AdminGroup(APIView):
"owner": group_owner, "owner": group_owner,
} }
admin_operation.send(sender=None, admin_name=request.user.username, 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}) return Response({'success': True})