mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-02 07:47:32 +00:00
sysadmin reconstruct groups page (#4036)
* sysadmin reconstruct groups page * [system admin] groups: fixup & improvement * [system admin] groups: fixup & improvement
This commit is contained in:
parent
140e26ff22
commit
6df11d6402
@ -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 (
|
||||
<Modal isOpen={true} toggle={this.toggle}>
|
||||
<ModalHeader toggle={this.toggle}>{gettext('New Group')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label for="groupName">{gettext('Name')}</Label>
|
||||
<Input
|
||||
id="groupName"
|
||||
onKeyPress={this.handleKeyPress}
|
||||
innerRef={input => {this.newInput = input;}}
|
||||
value={this.state.groupName}
|
||||
onChange={this.handleRepoNameChange}
|
||||
/>
|
||||
<Label className="mt-2">
|
||||
{gettext('Owner')}
|
||||
<span className="small text-secondary">{gettext('(If left blank, owner will be admin)')}</span>
|
||||
</Label>
|
||||
<UserSelect
|
||||
isMulti={false}
|
||||
className="reviewer-select"
|
||||
placeholder={gettext('Select a user')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
{this.state.errMessage && <Alert color="danger">{this.state.errMessage}</Alert>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.handleSubmit} disabled={!this.state.isSubmitBtnActive}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SysAdminCreateGroupDialog.propTypes = propTypes;
|
||||
|
||||
export default SysAdminCreateGroupDialog;
|
@ -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 (
|
||||
<Modal isOpen={true}>
|
||||
<ModalHeader toggle={this.props.toggle}>{gettext('Add Member')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
isMulti={true}
|
||||
className="reviewer-select"
|
||||
placeholder={gettext('Search users')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.addMembers} disabled={isSubmitBtnDisabled}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SysAdminGroupAddMemberDialog.propTypes = propTypes;
|
||||
|
||||
export default SysAdminGroupAddMemberDialog;
|
@ -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 = '<span class="op-target" title=' + groupName + '>' + groupName +'</span>';
|
||||
const msg = gettext('Transfer Group {library_name} to').replace('{library_name}', innerSpan);
|
||||
return (
|
||||
<Modal isOpen={true}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>
|
||||
<span dangerouslySetInnerHTML={{__html: msg}}></span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
isMulti={false}
|
||||
className="reviewer-select"
|
||||
placeholder={gettext('Select a user')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.submit} disabled={submitBtnDisabled}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SysAdminTransferGroupDialog.propTypes = propTypes;
|
||||
|
||||
export default SysAdminTransferGroupDialog;
|
@ -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 (
|
||||
<SelectEditor
|
||||
isTextMode={this.props.isTextMode}
|
||||
isEditIconShow={this.props.isEditIconShow}
|
||||
options={this.props.roleOptions}
|
||||
currentOption={this.props.currentRole}
|
||||
onOptionChanged={this.props.onRoleChanged}
|
||||
translateOption={this.translateRoles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SysAdminGroupRoleEditor.propTypes = propTypes;
|
||||
|
||||
export default SysAdminGroupRoleEditor;
|
283
frontend/src/pages/sys-admin/groups/group-members.js
Normal file
283
frontend/src/pages/sys-admin/groups/group-members.js
Normal file
@ -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 <Loading />;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
<EmptyTip>
|
||||
<h2>{gettext('No members')}</h2>
|
||||
</EmptyTip>
|
||||
);
|
||||
const table = (
|
||||
<Fragment>
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">{/* icon */}</th>
|
||||
<th width="55%">{gettext('Name')}</th>
|
||||
<th width="30%">{gettext('Role')}</th>
|
||||
<th width="10%">{/*Operations*/}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
item={item}
|
||||
removeMember={this.props.removeMember}
|
||||
updateMemberRole={this.props.updateMemberRole}
|
||||
/>);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</Fragment>
|
||||
);
|
||||
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 = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
|
||||
let dialogMsg = gettext('Are you sure you want to remove {placeholder} ?').replace('{placeholder}', itemName);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<tr onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<td><img src={item.avatar_url} alt="" className="rounded-circle" width="24" /></td>
|
||||
<td><a href={`${siteRoot}useradmin/info/${encodeURIComponent(item.email)}/`}>{item.name}</a></td>
|
||||
<td>
|
||||
{item.role == 'Owner' ?
|
||||
gettext('Owner') :
|
||||
<SysAdminGroupRoleEditor
|
||||
isTextMode={true}
|
||||
isEditIconShow={isOpIconShown}
|
||||
roleOptions={['Member', 'Admin']}
|
||||
currentRole={item.role}
|
||||
onRoleChanged={this.updateMemberRole}
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{item.role != 'Owner' &&
|
||||
<a href="#" className={`action-icon sf2-icon-x3 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Remove')} onClick={this.toggleDeleteDialog}></a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
{isDeleteDialogOpen &&
|
||||
<CommonOperationConfirmationDialog
|
||||
title={gettext('Remove Member')}
|
||||
message={dialogMsg}
|
||||
executeOperation={this.removeMember}
|
||||
confirmBtnText={gettext('Remove')}
|
||||
toggleDialog={this.toggleDeleteDialog}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Fragment>
|
||||
<MainPanelTopbar>
|
||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleAddMemgerDialog}>{gettext('Add Member')}</Button>
|
||||
</MainPanelTopbar>
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<GroupNav
|
||||
currentItem="members"
|
||||
groupID={this.props.groupID}
|
||||
groupName={this.state.groupName}
|
||||
/>
|
||||
<div className="cur-view-content">
|
||||
<Content
|
||||
loading={this.state.loading}
|
||||
errorMsg={this.state.errorMsg}
|
||||
items={this.state.memberList}
|
||||
removeMember={this.removeMember}
|
||||
updateMemberRole={this.updateMemberRole}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isAddMemberDialogOpen &&
|
||||
<SysAdminGroupAddMemberDialog
|
||||
addMembers={this.addMembers}
|
||||
toggle={this.toggleAddMemgerDialog}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupMembers;
|
44
frontend/src/pages/sys-admin/groups/group-nav.js
Normal file
44
frontend/src/pages/sys-admin/groups/group-nav.js
Normal file
@ -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 (
|
||||
<div>
|
||||
<div className="cur-view-path">
|
||||
<h3 className="sf-heading"><Link to={`${siteRoot}sys/groups/`}>{gettext('Groups')}</Link> / {groupName}</h3>
|
||||
</div>
|
||||
<ul className="nav border-bottom mx-4">
|
||||
{this.navItems.map((item, index) => {
|
||||
return (
|
||||
<li className="nav-item mr-2" key={index}>
|
||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link ${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Nav.propTypes = propTypes;
|
||||
|
||||
export default Nav;
|
229
frontend/src/pages/sys-admin/groups/group-repos.js
Normal file
229
frontend/src/pages/sys-admin/groups/group-repos.js
Normal file
@ -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 <Loading />;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
<EmptyTip>
|
||||
<h2>{gettext('No libraries')}</h2>
|
||||
</EmptyTip>
|
||||
);
|
||||
const table = (
|
||||
<Fragment>
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">{/* icon */}</th>
|
||||
<th width="30%">{gettext('Name')}</th>
|
||||
<th width="30%">{gettext('Size')}</th>
|
||||
<th width="25%">{gettext('Shared By')}</th>
|
||||
<th width="10%">{/*Operations*/}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
item={item}
|
||||
unshareRepo={this.props.unshareRepo}
|
||||
/>);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</Fragment>
|
||||
);
|
||||
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 <a href={`${siteRoot}sys/libraries/${repo.id}/`}>{repo.name}</a>;
|
||||
} 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 = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
|
||||
let dialogMsg = gettext('Are you sure you want to unshare {placeholder} ?').replace('{placeholder}', repoName);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<tr onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
|
||||
<td>{this.renderRepoName()}</td>
|
||||
<td>{Utils.bytesToSize(item.size)}</td>
|
||||
<td>
|
||||
<a href={`${siteRoot}useradmin/info/${encodeURIComponent(item.shared_by)}/`}>{item.shared_by_name}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" className={`action-icon sf2-icon-x3 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Unshare')} onClick={this.toggleUnshareRepoDialog}></a>
|
||||
</td>
|
||||
</tr>
|
||||
{isUnshareRepoDialogOpen &&
|
||||
<CommonOperationConfirmationDialog
|
||||
title={gettext('Unshare Library')}
|
||||
message={dialogMsg}
|
||||
executeOperation={this.unshareRepo}
|
||||
confirmBtnText={gettext('Unshare')}
|
||||
toggleDialog={this.toggleUnshareRepoDialog}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Fragment>
|
||||
<MainPanelTopbar />
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<GroupNav
|
||||
groupID={this.props.groupID}
|
||||
groupName={this.state.groupName}
|
||||
currentItem="repos"
|
||||
/>
|
||||
<div className="cur-view-content">
|
||||
<Content
|
||||
loading={this.state.loading}
|
||||
errorMsg={this.state.errorMsg}
|
||||
items={this.state.repoList}
|
||||
unshareRepo={this.unshareRepo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupRepos;
|
359
frontend/src/pages/sys-admin/groups/groups.js
Normal file
359
frontend/src/pages/sys-admin/groups/groups.js
Normal file
@ -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 <Loading />;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
<EmptyTip>
|
||||
<h2>{gettext('No groups')}</h2>
|
||||
</EmptyTip>
|
||||
);
|
||||
const table = (
|
||||
<Fragment>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="35%">{gettext('Name')}</th>
|
||||
<th width="40%">{gettext('Owner')}</th>
|
||||
<th width="20%">{gettext('Created At')}</th>
|
||||
<th width="5%">{/* operation */}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
item={item}
|
||||
isItemFreezed={this.state.isItemFreezed}
|
||||
onFreezedItem={this.onFreezedItem}
|
||||
onUnfreezedItem={this.onUnfreezedItem}
|
||||
deleteGroup={this.props.deleteGroup}
|
||||
transferGroup={this.props.transferGroup}
|
||||
/>);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<Paginator
|
||||
gotoPreviousPage={this.getPreviousPage}
|
||||
gotoNextPage={this.getNextPage}
|
||||
currentPage={pageInfo.current_page}
|
||||
hasNextPage={pageInfo.has_next_page}
|
||||
canResetPerPage={false}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
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 = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
|
||||
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 (
|
||||
<Fragment>
|
||||
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<td><a href={libUrl}>{item.name}</a></td>
|
||||
<td>
|
||||
{item.owner == 'system admin' ?
|
||||
'--' :
|
||||
<a href={`${siteRoot}useradmin/info/${encodeURIComponent(item.owner)}/`}>{item.owner_name}</a>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span title={moment(item.created_at).format('llll')}>{moment(item.created_at).fromNow()}</span>
|
||||
</td>
|
||||
<td>
|
||||
{(isOpIconShown && item.owner != 'system admin') &&
|
||||
<OpMenu
|
||||
onMenuItemClick={this.onMenuItemClick}
|
||||
onFreezedItem={this.props.onFreezedItem}
|
||||
onUnfreezedItem={this.onUnfreezedItem}
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
{isDeleteDialogOpen &&
|
||||
<CommonOperationConfirmationDialog
|
||||
title={gettext('Delete Group')}
|
||||
message={deleteDialogMsg}
|
||||
executeOperation={this.deleteGroup}
|
||||
confirmBtnText={gettext('Delete')}
|
||||
toggleDialog={this.toggleDeleteDialog}
|
||||
/>
|
||||
}
|
||||
{isTransferDialogOpen &&
|
||||
<SysAdminTransferGroupDialog
|
||||
groupName={item.name}
|
||||
transferGroup={this.transferGroup}
|
||||
toggleDialog={this.toggleTransferDialog}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Fragment>
|
||||
<MainPanelTopbar>
|
||||
<Fragment>
|
||||
<Button className="operation-item" onClick={this.toggleCreateGroupDialog}>{gettext('New Group')}</Button>
|
||||
<a className="btn btn-secondary operation-item" href={`${siteRoot}sys/groupadmin/export-excel/`}>{gettext('Export Excel')}</a>
|
||||
</Fragment>
|
||||
</MainPanelTopbar>
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<div className="cur-view-path">
|
||||
<h3 className="sf-heading">{gettext('Groups')}</h3>
|
||||
</div>
|
||||
<div className="cur-view-content">
|
||||
<Content
|
||||
loading={this.state.loading}
|
||||
errorMsg={this.state.errorMsg}
|
||||
items={this.state.groupList}
|
||||
pageInfo={this.state.pageInfo}
|
||||
deleteGroup={this.deleteGroup}
|
||||
transferGroup={this.transferGroup}
|
||||
getListByPage={this.getGroupListByPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isCreateGroupDialogOpen &&
|
||||
<SysAdminCreateGroupDialog
|
||||
createGroup={this.createGroup}
|
||||
toggleDialog={this.toggleCreateGroupDialog}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Groups;
|
81
frontend/src/pages/sys-admin/groups/op-menu.js
Normal file
81
frontend/src/pages/sys-admin/groups/op-menu.js
Normal file
@ -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 (
|
||||
<Dropdown isOpen={this.state.isItemMenuShow} toggle={this.toggleOperationMenu}>
|
||||
<DropdownToggle
|
||||
tag="i"
|
||||
className="sf-dropdown-toggle fa fa-ellipsis-v"
|
||||
title={gettext('More Operations')}
|
||||
data-toggle="dropdown"
|
||||
aria-expanded={this.state.isItemMenuShow}
|
||||
/>
|
||||
<DropdownMenu className="mt-2 mr-2">
|
||||
{operations.map((item, index )=> {
|
||||
return (<DropdownItem key={index} data-op={item} onClick={this.onMenuItemClick}>{this.translateOperations(item)}</DropdownItem>);
|
||||
})}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OpMenu.propTypes = propTypes;
|
||||
|
||||
export default OpMenu;
|
@ -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 {
|
||||
<DirView path={siteRoot + 'sys/libraries/:repoID/*'} />
|
||||
<WebSettings path={siteRoot + 'sys/web-settings'} />
|
||||
<Notifications path={siteRoot + 'sys/notifications'} />
|
||||
<Groups path={siteRoot + 'sys/groups'} />
|
||||
<GroupRepos path={siteRoot + 'sys/groups/:groupID/libraries'} />
|
||||
<GroupMembers path={siteRoot + 'sys/groups/:groupID/members'} />
|
||||
<FileScanRecords
|
||||
path={siteRoot + 'sys/file-scan-records'}
|
||||
currentTab={currentTab}
|
||||
|
@ -97,10 +97,14 @@ class SidePanel extends React.Component {
|
||||
}
|
||||
{canManageGroup &&
|
||||
<li className="nav-item">
|
||||
<a className='nav-link ellipsis' href={siteRoot + 'sysadmin/#groups/'}>
|
||||
<Link
|
||||
className={`nav-link ellipsis ${this.getActiveClass('groups')}`}
|
||||
to={siteRoot + 'sys/groups/'}
|
||||
onClick={() => this.props.tabItemClick('groups')}
|
||||
>
|
||||
<span className="sf2-icon-group" aria-hidden="true"></span>
|
||||
<span className="nav-text">{gettext('Groups')}</span>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
}
|
||||
{isPro && canManageGroup &&
|
||||
|
@ -646,7 +646,9 @@ urlpatterns = [
|
||||
url(r'^sys/trash-libraries/$', sysadmin_react_fake_view, name="sys_trash_libraries"),
|
||||
url(r'^sys/libraries/(?P<repo_id>[-0-9a-f]{36})/$', sysadmin_react_fake_view, name="sys_libraries_template"),
|
||||
url(r'^sys/libraries/(?P<repo_id>[-0-9a-f]{36})/(?P<repo_name>[^/]+)/(?P<path>.*)$', 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<group_id>\d+)/libraries/$', sysadmin_react_fake_view, name="sys_group_libraries"),
|
||||
url(r'^sys/groups/(?P<group_id>\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"),
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user