mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-15 14:49:09 +00:00
Repo share admin (#6745)
* [repo 'share admin'] redesigned the 'Share Links' panel * [repo 'share admin'] 'Share Links' panel: display user avatar, add 'delete selected links', add 'scroll to the bottom to load the next page of data' * [repo 'share admin'] 'Upload Links' panel: redesigned it
This commit is contained in:
@@ -112,7 +112,7 @@ class FileAccessLog extends React.Component {
|
|||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td className="pl10">
|
<td className="pl10">
|
||||||
<img src={item.avatar_url} alt='' width="24" className="rounded-circle mr-2" />
|
<img src={item.avatar_url} alt='' width="24" className="rounded-circle mr-2" />
|
||||||
{item.email ? <a href={`${siteRoot}profile/${encodeURIComponent(item.email)}/`} target="_blank" rel="noreferrer">{item.name}</a> : <span>{gettext('Anonymous User')}</span>}
|
{item.email ? <a href={`${siteRoot}profile/${encodeURIComponent(item.email)}/`} target="_blank" rel="noreferrer" className="align-middle">{item.name}</a> : <span>{gettext('Anonymous User')}</span>}
|
||||||
</td>
|
</td>
|
||||||
<td>{item.etype}</td>
|
<td>{item.etype}</td>
|
||||||
<td className="pr-4">
|
<td className="pr-4">
|
||||||
|
@@ -8,6 +8,8 @@ import RepoShareAdminUploadLinks from './repo-share-admin/upload-links';
|
|||||||
import RepoShareAdminUserShares from './repo-share-admin/user-shares';
|
import RepoShareAdminUserShares from './repo-share-admin/user-shares';
|
||||||
import RepoShareAdminGroupShares from './repo-share-admin/group-shares';
|
import RepoShareAdminGroupShares from './repo-share-admin/group-shares';
|
||||||
|
|
||||||
|
import '../../css/repo-share-admin.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
repo: PropTypes.object.isRequired,
|
repo: PropTypes.object.isRequired,
|
||||||
toggleDialog: PropTypes.func.isRequired,
|
toggleDialog: PropTypes.func.isRequired,
|
||||||
@@ -53,11 +55,11 @@ class RepoShareAdminDialog extends React.Component {
|
|||||||
title = title.replace('{placeholder}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(repoName) + '</span>');
|
title = title.replace('{placeholder}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(repoName) + '</span>');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Modal isOpen={true} style={{ maxWidth: '760px' }} className="share-dialog" toggle={this.props.toggleDialog}>
|
<Modal isOpen={true} className="repo-share-admin-container share-dialog" toggle={this.props.toggleDialog}>
|
||||||
<ModalHeader toggle={this.props.toggleDialog}>
|
<ModalHeader toggle={this.props.toggleDialog}>
|
||||||
<span dangerouslySetInnerHTML={{ __html: title }} className="d-flex mw-100"></span>
|
<span dangerouslySetInnerHTML={{ __html: title }} className="d-flex mw-100"></span>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody className="dialog-list-container share-dialog-content" role="tablist">
|
<ModalBody className="repo-share-admin-content-container share-dialog-content" role="tablist">
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="share-dialog-side">
|
<div className="share-dialog-side">
|
||||||
<Nav pills>
|
<Nav pills>
|
||||||
|
@@ -1,16 +1,21 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import moment from 'moment';
|
||||||
|
import classnames from 'classnames';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
import { Link } from '@gatsbyjs/reach-router';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { seafileAPI } from '../../../utils/seafile-api';
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import { repoShareAdminAPI } from '../../../utils/repo-share-admin-api';
|
||||||
import { gettext, siteRoot } from '../../../utils/constants';
|
import { gettext, siteRoot } from '../../../utils/constants';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import toaster from '../../../components/toast';
|
import toaster from '../../../components/toast';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
|
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
||||||
|
|
||||||
const itemPropTypes = {
|
const itemPropTypes = {
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
deleteItem: PropTypes.func.isRequired
|
deleteItem: PropTypes.func.isRequired,
|
||||||
|
toggleSelectLink: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
class Item extends Component {
|
class Item extends Component {
|
||||||
@@ -30,11 +35,20 @@ class Item extends Component {
|
|||||||
this.setState({ isOperationShow: false });
|
this.setState({ isOperationShow: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeleteLink = (e) => {
|
onDeleteLink = () => {
|
||||||
e.preventDefault();
|
|
||||||
this.props.deleteItem(this.props.item);
|
this.props.deleteItem(this.props.item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cutLink = (link) => {
|
||||||
|
let length = link.length;
|
||||||
|
return link.slice(0, 9) + '...' + link.slice(length - 5);
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleSelectLink = (e) => {
|
||||||
|
const { item } = this.props;
|
||||||
|
this.props.toggleSelectLink(item, e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let objUrl;
|
let objUrl;
|
||||||
let item = this.props.item;
|
let item = this.props.item;
|
||||||
@@ -47,8 +61,24 @@ class Item extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
<tr
|
||||||
<td className="name">{item.creator_name}</td>
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
onFocus={this.onMouseEnter}
|
||||||
|
className={classnames({ 'tr-highlight': item.isSelected })}
|
||||||
|
>
|
||||||
|
<td className="text-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={item.isSelected || false}
|
||||||
|
className="vam"
|
||||||
|
onChange={this.toggleSelectLink}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<img src={item.creator_avatar} alt={item.creator_name} width="24" className="rounded-circle mr-2" />
|
||||||
|
<a href={`${siteRoot}profile/${encodeURIComponent(item.creator_email)}/`} target="_blank" className="align-middle" rel="noreferrer">{item.creator_name}</a>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{item.is_dir ?
|
{item.is_dir ?
|
||||||
<Link to={objUrl}>{item.obj_name}</Link>
|
<Link to={objUrl}>{item.obj_name}</Link>
|
||||||
@@ -57,19 +87,25 @@ class Item extends Component {
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href={item.link} target="_blank" rel="noreferrer">{item.link}</a>
|
<a href={item.link} target="_blank" rel="noreferrer">
|
||||||
|
{this.cutLink(item.link)}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
{item.expire_date ? moment(item.expire_date).format('YYYY-MM-DD HH:mm') : '--'}
|
||||||
|
</td>
|
||||||
|
<td>{item.view_cnt}</td>
|
||||||
|
<td>
|
||||||
|
<i
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
role="button"
|
role="button"
|
||||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
className={`sf3-font-delete1 sf3-font op-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
||||||
onClick={this.onDeleteLink}
|
onClick={this.onDeleteLink}
|
||||||
onKeyDown={Utils.onKeyDown}
|
onKeyDown={Utils.onKeyDown}
|
||||||
title={gettext('Delete')}
|
title={gettext('Delete')}
|
||||||
aria-label={gettext('Delete')}
|
aria-label={gettext('Delete')}
|
||||||
>
|
>
|
||||||
</span>
|
</i>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -82,21 +118,30 @@ const propTypes = {
|
|||||||
repo: PropTypes.object.isRequired,
|
repo: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PER_PAGE = 25;
|
||||||
|
|
||||||
class RepoShareAdminShareLinks extends Component {
|
class RepoShareAdminShareLinks extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
|
hasMore: false,
|
||||||
|
isLoadingMore: false,
|
||||||
|
page: 1,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
items: []
|
items: [],
|
||||||
|
isDeleteShareLinksDialogOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
seafileAPI.listRepoShareLinks(this.props.repo.repo_id).then((res) => {
|
const { repo } = this.props;
|
||||||
|
const { page } = this.state;
|
||||||
|
repoShareAdminAPI.listRepoShareLinks(repo.repo_id, page).then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
hasMore: res.data.length == PER_PAGE,
|
||||||
items: res.data,
|
items: res.data,
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -121,38 +166,179 @@ class RepoShareAdminShareLinks extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleDeleteShareLinksDialog = () => {
|
||||||
|
this.setState({ isDeleteShareLinksDialogOpen: !this.state.isDeleteShareLinksDialogOpen });
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleSelectAllLinks = (e) => {
|
||||||
|
this._toggleSelectAllLinks(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelSelectAllLinks = () => {
|
||||||
|
this._toggleSelectAllLinks(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
_toggleSelectAllLinks = (isSelected) => {
|
||||||
|
const { items: links } = this.state;
|
||||||
|
this.setState({
|
||||||
|
items: links.map(item => {
|
||||||
|
item.isSelected = isSelected;
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleSelectLink = (link, isSelected) => {
|
||||||
|
const { items: links } = this.state;
|
||||||
|
this.setState({
|
||||||
|
items: links.map(item => {
|
||||||
|
if (item.token == link.token) {
|
||||||
|
item.isSelected = isSelected;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteShareLinks = () => {
|
||||||
|
const { items } = this.state;
|
||||||
|
const tokens = items.filter(item => item.isSelected).map(link => link.token);
|
||||||
|
seafileAPI.deleteShareLinks(tokens).then(res => {
|
||||||
|
const { success, failed } = res.data;
|
||||||
|
if (success.length) {
|
||||||
|
let newShareLinkList = items.filter(shareLink => {
|
||||||
|
return !success.some(deletedShareLink => {
|
||||||
|
return deletedShareLink.token == shareLink.token;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
items: newShareLinkList
|
||||||
|
});
|
||||||
|
const length = success.length;
|
||||||
|
const msg = length == 1 ?
|
||||||
|
gettext('Successfully deleted 1 share link') :
|
||||||
|
gettext('Successfully deleted {number_placeholder} share links')
|
||||||
|
.replace('{number_placeholder}', length);
|
||||||
|
toaster.success(msg);
|
||||||
|
}
|
||||||
|
failed.forEach(item => {
|
||||||
|
const msg = `${item.token}: ${item.error_msg}`;
|
||||||
|
toaster.danger(msg);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getTheadContent = (withCheckbox, isAllLinksSelected) => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<th width="5%" className="text-center">
|
||||||
|
{withCheckbox && <input type="checkbox" checked={isAllLinksSelected} className="vam" onChange={this.toggleSelectAllLinks} />}
|
||||||
|
</th>
|
||||||
|
<th width="20%">{gettext('Creator')}</th>
|
||||||
|
<th width="24%">{gettext('Name')}</th>
|
||||||
|
<th width="19%">{gettext('Link')}</th>
|
||||||
|
<th width="17%">{gettext('Expiration')}</th>
|
||||||
|
<th width="8%">{gettext('Visits')}</th>
|
||||||
|
<th width="7%"></th>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleScroll = (event) => {
|
||||||
|
if (!this.state.isLoadingMore && this.state.hasMore) {
|
||||||
|
const clientHeight = event.target.clientHeight;
|
||||||
|
const scrollHeight = event.target.scrollHeight;
|
||||||
|
const scrollTop = event.target.scrollTop;
|
||||||
|
const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight);
|
||||||
|
if (isBottom) { // scroll to the bottom
|
||||||
|
this.setState({ isLoadingMore: true }, () => {
|
||||||
|
this.getMore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getMore = () => {
|
||||||
|
const { page, items } = this.state;
|
||||||
|
const { repo } = this.props;
|
||||||
|
|
||||||
|
repoShareAdminAPI.listRepoShareLinks(repo.repo_id, page + 1).then((res) => {
|
||||||
|
this.setState({
|
||||||
|
isLoadingMore: false,
|
||||||
|
hasMore: res.data.length == PER_PAGE,
|
||||||
|
page: page + 1,
|
||||||
|
items: items.concat(res.data)
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
this.setState({
|
||||||
|
isLoadingMore: false
|
||||||
|
});
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, errorMsg, items } = this.state;
|
const { loading, isLoadingMore, errorMsg, items, isDeleteShareLinksDialogOpen } = this.state;
|
||||||
|
const selectedLinks = items.filter(item => item.isSelected);
|
||||||
|
const isAllLinksSelected = items.length == selectedLinks.length;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
<div className="d-flex justify-content-between align-items-center pb-2 mt-1 pr-1 border-bottom">
|
||||||
|
<h6 className="font-weight-normal m-0">{gettext('Share Links')}</h6>
|
||||||
|
<div className="d-flex">
|
||||||
|
{selectedLinks.length > 0 && (
|
||||||
|
<>
|
||||||
|
<button className="btn btn-sm btn-secondary mr-2" onClick={this.cancelSelectAllLinks}>{gettext('Cancel')}</button>
|
||||||
|
<button className="btn btn-sm btn-secondary mr-2" onClick={this.toggleDeleteShareLinksDialog}>{gettext('Delete')}</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{loading && <Loading />}
|
{loading && <Loading />}
|
||||||
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
||||||
{!loading && !errorMsg && !items.length &&
|
{!loading && !errorMsg && !items.length &&
|
||||||
<EmptyTip text={gettext('No share links')}/>
|
<EmptyTip text={gettext('No share links')}/>
|
||||||
}
|
}
|
||||||
{!loading && !errorMsg && items.length > 0 &&
|
{!loading && !errorMsg && items.length > 0 && (
|
||||||
<table className="table-hover">
|
<>
|
||||||
<thead>
|
<table>
|
||||||
<tr>
|
<thead>{this.getTheadContent(true, isAllLinksSelected)}</thead>
|
||||||
<th width="22%">{gettext('Creator')}</th>
|
<tbody></tbody>
|
||||||
<th width="20%">{gettext('Name')}</th>
|
</table>
|
||||||
<th width="50%">{gettext('Link')}</th>
|
<div className='table-real-container' onScroll={this.handleScroll}>
|
||||||
<th width="8%"></th>
|
<table className="table-hover table-thead-hidden">
|
||||||
</tr>
|
<thead>{this.getTheadContent(false)}</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
{items.map((item, index) => {
|
||||||
{items.map((item, index) => {
|
return (
|
||||||
return (
|
<Item
|
||||||
<Item
|
key={index}
|
||||||
key={index}
|
item={item}
|
||||||
item={item}
|
deleteItem={this.deleteItem}
|
||||||
deleteItem={this.deleteItem}
|
toggleSelectLink={this.toggleSelectLink}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
}
|
{isLoadingMore && <Loading />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isDeleteShareLinksDialogOpen && (
|
||||||
|
<CommonOperationConfirmationDialog
|
||||||
|
title={gettext('Delete share links')}
|
||||||
|
message={gettext('Are you sure you want to delete the selected share link(s) ?')}
|
||||||
|
executeOperation={this.deleteShareLinks}
|
||||||
|
confirmBtnText={gettext('Delete')}
|
||||||
|
toggleDialog={this.toggleDeleteShareLinksDialog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,21 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import moment from 'moment';
|
||||||
|
import classnames from 'classnames';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
import { Link } from '@gatsbyjs/reach-router';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { seafileAPI } from '../../../utils/seafile-api';
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import { repoShareAdminAPI } from '../../../utils/repo-share-admin-api';
|
||||||
import { gettext, siteRoot } from '../../../utils/constants';
|
import { gettext, siteRoot } from '../../../utils/constants';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import toaster from '../../../components/toast';
|
import toaster from '../../../components/toast';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
|
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
||||||
|
|
||||||
const itemPropTypes = {
|
const itemPropTypes = {
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
deleteItem: PropTypes.func.isRequired
|
deleteItem: PropTypes.func.isRequired,
|
||||||
|
toggleSelectLink: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
class Item extends Component {
|
class Item extends Component {
|
||||||
@@ -30,36 +35,67 @@ class Item extends Component {
|
|||||||
this.setState({ isOperationShow: false });
|
this.setState({ isOperationShow: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeleteLink = (e) => {
|
onDeleteLink = () => {
|
||||||
e.preventDefault();
|
|
||||||
this.props.deleteItem(this.props.item);
|
this.props.deleteItem(this.props.item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cutLink = (link) => {
|
||||||
|
let length = link.length;
|
||||||
|
return link.slice(0, 9) + '...' + link.slice(length - 5);
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleSelectLink = (e) => {
|
||||||
|
const { item } = this.props;
|
||||||
|
this.props.toggleSelectLink(item, e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let item = this.props.item;
|
let item = this.props.item;
|
||||||
let path = item.path === '/' ? '/' : item.path.slice(0, item.path.length - 1);
|
let path = item.path === '/' ? '/' : item.path.slice(0, item.path.length - 1);
|
||||||
let objUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}${Utils.encodePath(path)}`;
|
let objUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}${Utils.encodePath(path)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
<tr
|
||||||
<td className="name">{item.creator_name}</td>
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
onFocus={this.onMouseEnter}
|
||||||
|
className={classnames({ 'tr-highlight': item.isSelected })}
|
||||||
|
>
|
||||||
|
<td className="text-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={item.isSelected || false}
|
||||||
|
className="vam"
|
||||||
|
onChange={this.toggleSelectLink}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<img src={item.creator_avatar} alt={item.creator_name} width="24" className="rounded-circle mr-2" />
|
||||||
|
<a href={`${siteRoot}profile/${encodeURIComponent(item.creator_email)}/`} target="_blank" className="align-middle" rel="noreferrer">{item.creator_name}</a>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Link to={objUrl}>{item.obj_name}</Link>
|
<Link to={objUrl}>{item.obj_name}</Link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href={item.link} target="_blank" rel="noreferrer">{item.link}</a>
|
<a href={item.link} target="_blank" rel="noreferrer">
|
||||||
|
{this.cutLink(item.link)}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
{item.expire_date ? moment(item.expire_date).format('YYYY-MM-DD HH:mm') : '--'}
|
||||||
|
</td>
|
||||||
|
<td>{item.view_cnt}</td>
|
||||||
|
<td>
|
||||||
|
<i
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
role="button"
|
role="button"
|
||||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
className={`sf3-font-delete1 sf3-font op-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
||||||
onClick={this.onDeleteLink}
|
onClick={this.onDeleteLink}
|
||||||
onKeyDown={Utils.onKeyDown}
|
onKeyDown={Utils.onKeyDown}
|
||||||
title={gettext('Delete')}
|
title={gettext('Delete')}
|
||||||
aria-label={gettext('Delete')}
|
aria-label={gettext('Delete')}
|
||||||
>
|
>
|
||||||
</span>
|
</i>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -72,21 +108,30 @@ const propTypes = {
|
|||||||
repo: PropTypes.object.isRequired,
|
repo: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PER_PAGE = 25;
|
||||||
|
|
||||||
class RepoShareAdminUploadLinks extends Component {
|
class RepoShareAdminUploadLinks extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
|
hasMore: false,
|
||||||
|
isLoadingMore: false,
|
||||||
|
page: 1,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
items: [],
|
items: [],
|
||||||
|
isDeleteUploadLinksDialogOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
seafileAPI.listRepoUploadLinks(this.props.repo.repo_id).then((res) => {
|
const { repo } = this.props;
|
||||||
|
const { page } = this.state;
|
||||||
|
repoShareAdminAPI.listRepoUploadLinks(repo.repo_id, page).then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
hasMore: res.data.length == PER_PAGE,
|
||||||
items: res.data,
|
items: res.data,
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -111,38 +156,178 @@ class RepoShareAdminUploadLinks extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleDeleteUploadLinksDialog = () => {
|
||||||
|
this.setState({ isDeleteUploadLinksDialogOpen: !this.state.isDeleteUploadLinksDialogOpen });
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleSelectAllLinks = (e) => {
|
||||||
|
this._toggleSelectAllLinks(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelSelectAllLinks = () => {
|
||||||
|
this._toggleSelectAllLinks(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
_toggleSelectAllLinks = (isSelected) => {
|
||||||
|
const { items: links } = this.state;
|
||||||
|
this.setState({
|
||||||
|
items: links.map(item => {
|
||||||
|
item.isSelected = isSelected;
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleSelectLink = (link, isSelected) => {
|
||||||
|
const { items: links } = this.state;
|
||||||
|
this.setState({
|
||||||
|
items: links.map(item => {
|
||||||
|
if (item.token == link.token) {
|
||||||
|
item.isSelected = isSelected;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteUploadLinks = () => {
|
||||||
|
const { items } = this.state;
|
||||||
|
const tokens = items.filter(item => item.isSelected).map(link => link.token);
|
||||||
|
repoShareAdminAPI.deleteUploadLinks(tokens).then(res => {
|
||||||
|
const { success, failed } = res.data;
|
||||||
|
if (success.length) {
|
||||||
|
let newLinkList = items.filter(link => {
|
||||||
|
return !success.some(deletedLink => {
|
||||||
|
return deletedLink.token == link.token;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
items: newLinkList
|
||||||
|
});
|
||||||
|
const length = success.length;
|
||||||
|
const msg = length == 1 ?
|
||||||
|
gettext('Successfully deleted 1 upload link') :
|
||||||
|
gettext('Successfully deleted {number_placeholder} upload links')
|
||||||
|
.replace('{number_placeholder}', length);
|
||||||
|
toaster.success(msg);
|
||||||
|
}
|
||||||
|
failed.forEach(item => {
|
||||||
|
const msg = `${item.token}: ${item.error_msg}`;
|
||||||
|
toaster.danger(msg);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getTheadContent = (withCheckbox, isAllLinksSelected) => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<th width="5%" className="text-center">
|
||||||
|
{withCheckbox && <input type="checkbox" checked={isAllLinksSelected} className="vam" onChange={this.toggleSelectAllLinks} />}
|
||||||
|
</th>
|
||||||
|
<th width="20%">{gettext('Creator')}</th>
|
||||||
|
<th width="24%">{gettext('Name')}</th>
|
||||||
|
<th width="19%">{gettext('Link')}</th>
|
||||||
|
<th width="17%">{gettext('Expiration')}</th>
|
||||||
|
<th width="8%">{gettext('Visits')}</th>
|
||||||
|
<th width="7%"></th>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleScroll = (event) => {
|
||||||
|
if (!this.state.isLoadingMore && this.state.hasMore) {
|
||||||
|
const clientHeight = event.target.clientHeight;
|
||||||
|
const scrollHeight = event.target.scrollHeight;
|
||||||
|
const scrollTop = event.target.scrollTop;
|
||||||
|
const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight);
|
||||||
|
if (isBottom) { // scroll to the bottom
|
||||||
|
this.setState({ isLoadingMore: true }, () => {
|
||||||
|
this.getMore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getMore = () => {
|
||||||
|
const { page, items } = this.state;
|
||||||
|
const { repo } = this.props;
|
||||||
|
|
||||||
|
repoShareAdminAPI.listRepoUploadLinks(repo.repo_id, page + 1).then((res) => {
|
||||||
|
this.setState({
|
||||||
|
isLoadingMore: false,
|
||||||
|
hasMore: res.data.length == PER_PAGE,
|
||||||
|
page: page + 1,
|
||||||
|
items: items.concat(res.data)
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
this.setState({
|
||||||
|
isLoadingMore: false
|
||||||
|
});
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, errorMsg, items } = this.state;
|
const { loading, isLoadingMore, errorMsg, items, isDeleteUploadLinksDialogOpen } = this.state;
|
||||||
|
const selectedLinks = items.filter(item => item.isSelected);
|
||||||
|
const isAllLinksSelected = items.length == selectedLinks.length;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
<div className="d-flex justify-content-between align-items-center pb-2 mt-1 pr-1 border-bottom">
|
||||||
|
<h6 className="font-weight-normal m-0">{gettext('Upload Links')}</h6>
|
||||||
|
<div className="d-flex">
|
||||||
|
{selectedLinks.length > 0 && (
|
||||||
|
<>
|
||||||
|
<button className="btn btn-sm btn-secondary mr-2" onClick={this.cancelSelectAllLinks}>{gettext('Cancel')}</button>
|
||||||
|
<button className="btn btn-sm btn-secondary mr-2" onClick={this.toggleDeleteUploadLinksDialog}>{gettext('Delete')}</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{loading && <Loading />}
|
{loading && <Loading />}
|
||||||
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
||||||
{!loading && !errorMsg && !items.length &&
|
{!loading && !errorMsg && !items.length &&
|
||||||
<EmptyTip text={gettext('No upload links')}/>
|
<EmptyTip text={gettext('No upload links')}/>
|
||||||
}
|
}
|
||||||
{!loading && !errorMsg && items.length > 0 &&
|
{!loading && !errorMsg && items.length > 0 && (
|
||||||
<table className="table-hover">
|
<>
|
||||||
<thead>
|
<table>
|
||||||
<tr>
|
<thead>{this.getTheadContent(true, isAllLinksSelected)}</thead>
|
||||||
<th width="22%">{gettext('Creator')}</th>
|
<tbody></tbody>
|
||||||
<th width="20%">{gettext('Name')}</th>
|
</table>
|
||||||
<th width="50%">{gettext('Link')}</th>
|
<div className='table-real-container' onScroll={this.handleScroll}>
|
||||||
<th width="8%"></th>
|
<table className="table-hover table-thead-hidden">
|
||||||
</tr>
|
<thead>{this.getTheadContent(false)}</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
{items.map((item, index) => {
|
||||||
{items.map((item, index) => {
|
return (
|
||||||
return (
|
<Item
|
||||||
<Item
|
key={index}
|
||||||
key={index}
|
item={item}
|
||||||
item={item}
|
deleteItem={this.deleteItem}
|
||||||
deleteItem={this.deleteItem}
|
toggleSelectLink={this.toggleSelectLink}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
}
|
{isLoadingMore && <Loading />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isDeleteUploadLinksDialogOpen && (
|
||||||
|
<CommonOperationConfirmationDialog
|
||||||
|
title={gettext('Delete upload links')}
|
||||||
|
message={gettext('Are you sure you want to delete the selected upload link(s) ?')}
|
||||||
|
executeOperation={this.deleteUploadLinks}
|
||||||
|
confirmBtnText={gettext('Delete')}
|
||||||
|
toggleDialog={this.toggleDeleteUploadLinksDialog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
14
frontend/src/css/repo-share-admin.css
Normal file
14
frontend/src/css/repo-share-admin.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@media(min-width:768px) {
|
||||||
|
.repo-share-admin-container {
|
||||||
|
max-width: 1100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-share-admin-content-container {
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-share-admin-content-container .table-real-container {
|
||||||
|
height: 20rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
74
frontend/src/utils/repo-share-admin-api.js
Normal file
74
frontend/src/utils/repo-share-admin-api.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import cookie from 'react-cookies';
|
||||||
|
import { siteRoot } from './constants';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
class RepoShareAdminAPI {
|
||||||
|
init({ server, username, password, token }) {
|
||||||
|
this.server = server;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.token = token; // none
|
||||||
|
if (this.token && this.server) {
|
||||||
|
this.req = axios.create({
|
||||||
|
baseURL: this.server,
|
||||||
|
headers: { 'Authorization': 'Token ' + this.token },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
|
||||||
|
if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') {
|
||||||
|
let server = siteRoot.substring(0, siteRoot.length - 1);
|
||||||
|
this.server = server;
|
||||||
|
} else {
|
||||||
|
this.server = siteRoot;
|
||||||
|
}
|
||||||
|
this.req = axios.create({
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': xcsrfHeaders,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendPostRequest(url, form) {
|
||||||
|
if (form.getHeaders) {
|
||||||
|
return this.req.post(url, form, {
|
||||||
|
headers: form.getHeaders()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.req.post(url, form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listRepoShareLinks(repoID, page, perPage) {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/share-links/';
|
||||||
|
const params = {
|
||||||
|
page: page || 1,
|
||||||
|
per_page: perPage || 25
|
||||||
|
};
|
||||||
|
return this.req.get(url, { params: params });
|
||||||
|
}
|
||||||
|
|
||||||
|
listRepoUploadLinks(repoID, page, perPage) {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/upload-links/';
|
||||||
|
const params = {
|
||||||
|
page: page || 1,
|
||||||
|
per_page: perPage || 25
|
||||||
|
};
|
||||||
|
return this.req.get(url, { params: params });
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUploadLinks(tokens) {
|
||||||
|
const url = this.server + '/api/v2.1/upload-links/';
|
||||||
|
let param = { tokens: tokens };
|
||||||
|
return this.req.delete(url, { data: param });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let repoShareAdminAPI = new RepoShareAdminAPI();
|
||||||
|
let xcsrfHeaders = cookie.load('sfcsrftoken');
|
||||||
|
repoShareAdminAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
|
||||||
|
export { repoShareAdminAPI };
|
Reference in New Issue
Block a user