1
0
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:
Leo 2019-10-15 13:41:41 +08:00 committed by Daniel Pan
parent 140e26ff22
commit 6df11d6402
12 changed files with 1290 additions and 3 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -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}

View File

@ -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 &&

View File

@ -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"),