mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 23:48:47 +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:
@@ -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;
|
@@ -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 = (
|
||||
<Fragment>
|
||||
{groupID &&
|
||||
<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('Add Member')} onClick={this.toggleAddMemberDialog}>{gettext('Add Member')}</button>
|
||||
<button className={topBtn} onClick={this.toggleAddRepoDialog} title={gettext('New Library')}>{gettext('New Library')}</button>
|
||||
</Fragment>
|
||||
}
|
||||
{this.state.isShowRenameDepartmentDialog && (
|
||||
<ModalPortal>
|
||||
<RenameDepartmentDialog
|
||||
groupID={groupID}
|
||||
name={groupName}
|
||||
toggle={this.toggleRenameDepartmentDialog}
|
||||
onDepartmentNameChanged={this.onDepartmentNameChanged}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isShowAddMemberDialog && (
|
||||
<ModalPortal>
|
||||
<AddMemberDialog
|
||||
|
@@ -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 });
|
||||
@@ -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 {
|
||||
<Fragment key={group.id}>
|
||||
<GroupItem
|
||||
group={group}
|
||||
isItemFreezed={this.state.isItemFreezed}
|
||||
onFreezedItem={this.onFreezedItem}
|
||||
onUnfreezedItem={this.onUnfreezedItem}
|
||||
onDepartmentNameChanged={this.onDepartmentNameChanged}
|
||||
showDeleteDepartDialog={this.showDeleteDepartDialog}
|
||||
showSetGroupQuotaDialog={this.showSetGroupQuotaDialog}
|
||||
/>
|
||||
|
@@ -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 (
|
||||
<tr className={highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<Fragment>
|
||||
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
|
||||
<td><Link to={newHref}>{group.name}</Link></td>
|
||||
<td>{moment(group.created_at).fromNow()}</td>
|
||||
<td onClick={this.props.showSetGroupQuotaDialog.bind(this, group.id)}>
|
||||
{Utils.bytesToSize(group.quota)}{' '}
|
||||
<span title="Edit Quota" className={`fa fa-pencil-alt attr-action-icon ${highlight ? '' : 'vh'}`}></span>
|
||||
</td>
|
||||
<td className="cursor-pointer text-center" onClick={this.props.showDeleteDepartDialog.bind(this, group)}>
|
||||
<span className={`sf2-icon-delete action-icon ${highlight ? '' : 'vh'}`} title="Delete"></span>
|
||||
<td>
|
||||
{isOpIconShown &&
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
|
Reference in New Issue
Block a user