1
0
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:
llj
2024-09-11 18:20:33 +08:00
committed by GitHub
parent 699df10f7c
commit 8d7092f776
6 changed files with 533 additions and 72 deletions

View File

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

View File

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

View File

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

View File

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

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

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