1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-21 03:18:23 +00:00

delete share links (#5572)

* delete share links

* [share dialog] 'Share Link' panel: fixup & improvements for 'visit link details & delete link/links'

---------

Co-authored-by: llj <lingjun.li1@gmail.com>
This commit is contained in:
lian
2023-08-07 12:17:21 +08:00
committed by GitHub
parent 2366af08db
commit 14569059b3
5 changed files with 207 additions and 50 deletions

View File

@@ -95,15 +95,46 @@ class ShareLinkPanel extends React.Component {
});
}
deleteLink = () => {
const { sharedLinkInfo, shareLinks } = this.state;
seafileAPI.deleteShareLink(sharedLinkInfo.token).then(() => {
deleteLink = (token) => {
const { shareLinks } = this.state;
seafileAPI.deleteShareLink(token).then(() => {
this.setState({
mode: '',
sharedLinkInfo: null,
shareLinks: shareLinks.filter(item => item.token !== sharedLinkInfo.token)
shareLinks: shareLinks.filter(item => item.token !== token)
});
toaster.success(gettext('Successfully deleted 1 share link'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
deleteShareLinks = () => {
const { shareLinks } = this.state;
const tokens = shareLinks.filter(item => item.isSelected).map(link => link.token);
seafileAPI.deleteShareLinks(tokens).then(res => {
const { success, failed } = res.data;
if (success.length) {
let newShareLinkList = shareLinks.filter(shareLink => {
return !success.some(deletedShareLink => {
return deletedShareLink.token == shareLink.token;
});
});
this.setState({
shareLinks: 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);
});
toaster.success(gettext('Link deleted'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@@ -209,6 +240,8 @@ class ShareLinkPanel extends React.Component {
showLinkDetails={this.showLinkDetails}
toggleSelectAllLinks={this.toggleSelectAllLinks}
toggleSelectLink={this.toggleSelectLink}
deleteShareLinks={this.deleteShareLinks}
deleteLink={this.deleteLink}
/>
);
}

View File

@@ -5,6 +5,7 @@ import copy from 'copy-to-clipboard';
import { Button } from 'reactstrap';
import { isPro, gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax, shareLinkExpireDaysDefault, canSendShareLinkEmail } from '../../utils/constants';
import ShareLinkPermissionEditor from '../../components/select-editor/share-link-permission-editor';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import ShareLink from '../../models/share-link';
@@ -38,7 +39,7 @@ class LinkDetails extends React.Component {
expireDays: this.props.defaultExpireDays,
expDate: null,
isOpIconShown: false,
isNoticeMessageShow: false,
isLinkDeleteDialogOpen: false,
isSendLinkShown: false
};
}
@@ -129,14 +130,20 @@ class LinkDetails extends React.Component {
});
}
onNoticeMessageToggle = () => {
this.setState({isNoticeMessageShow: !this.state.isNoticeMessageShow});
toggleLinkDeleteDialog = () => {
this.setState({isLinkDeleteDialogOpen: !this.state.isLinkDeleteDialogOpen});
}
toggleSendLink = () => {
this.setState({ isSendLinkShown: !this.state.isSendLinkShown });
}
deleteLink = () => {
const { sharedLinkInfo } = this.props;
const { token } = sharedLinkInfo;
this.props.deleteLink(token);
}
goBack = () => {
this.props.showLinkDetails(null);
}
@@ -235,7 +242,7 @@ class LinkDetails extends React.Component {
</>
)}
</dl>
{(canSendShareLinkEmail && !this.state.isSendLinkShown && !this.state.isNoticeMessageShow) &&
{(canSendShareLinkEmail && !this.state.isSendLinkShown) &&
<Button onClick={this.toggleSendLink} className='mr-2'>{gettext('Send')}</Button>
}
{this.state.isSendLinkShown &&
@@ -246,16 +253,17 @@ class LinkDetails extends React.Component {
closeShareDialog={this.props.closeShareDialog}
/>
}
{(!this.state.isSendLinkShown && !this.state.isNoticeMessageShow) &&
<Button onClick={this.onNoticeMessageToggle}>{gettext('Delete')}</Button>
{(!this.state.isSendLinkShown) &&
<Button onClick={this.toggleLinkDeleteDialog}>{gettext('Delete')}</Button>
}
{this.state.isNoticeMessageShow &&
<div className="alert alert-warning">
<h4 className="alert-heading">{gettext('Are you sure you want to delete the share link?')}</h4>
<p className="mb-4">{gettext('If the share link is deleted, no one will be able to access it any more.')}</p>
<button className="btn btn-primary" onClick={this.props.deleteLink}>{gettext('Delete')}</button>{' '}
<button className="btn btn-secondary" onClick={this.onNoticeMessageToggle}>{gettext('Cancel')}</button>
</div>
{this.state.isLinkDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete share link')}
message={gettext('Are you sure you want to delete the share link?')}
executeOperation={this.deleteLink}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleLinkDeleteDialog}
/>
}
</div>
);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import copy from 'copy-to-clipboard';
@@ -6,12 +6,14 @@ import toaster from '../toast';
import { isPro, gettext } from '../../utils/constants';
import ShareLinkPermissionEditor from '../../components/select-editor/share-link-permission-editor';
import { Utils } from '../../utils/utils';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
const propTypes = {
item: PropTypes.object.isRequired,
permissionOptions: PropTypes.array,
showLinkDetails : PropTypes.func.isRequired,
toggleSelectLink: PropTypes.func.isRequired
toggleSelectLink: PropTypes.func.isRequired,
deleteLink: PropTypes.func.isRequired
};
class LinkItem extends React.Component {
@@ -19,7 +21,8 @@ class LinkItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isItemOpVisible: false
isItemOpVisible: false,
isDeleteShareLinkDialogOpen: false
};
}
@@ -40,6 +43,10 @@ class LinkItem extends React.Component {
return link.slice(0, 9) + '...' + link.slice(length-5);
}
toggleDeleteShareLinkDialog = () => {
this.setState({isDeleteShareLinkDialogOpen: !this.state.isDeleteShareLinkDialogOpen});
}
copyLink = (e) => {
e.preventDefault();
const { item } = this.props;
@@ -57,17 +64,25 @@ class LinkItem extends React.Component {
this.props.toggleSelectLink(item, e.target.checked);
}
deleteLink = () => {
const { item } = this.props;
this.props.deleteLink(item.token);
}
render() {
const { isItemOpVisible } = this.state;
const { item, permissionOptions } = this.props;
const { isSelected = false, permissions, link, expire_date } = item;
const currentPermission = Utils.getShareLinkPermissionStr(permissions);
return (
<Fragment>
<tr onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={isSelected ? 'tr-highlight' : ''}>
<td className="text-center">
<input type="checkbox" checked={isSelected} onChange={this.toggleSelectLink} className="vam" />
</td>
<td>{this.cutLink(link)}</td>
<td>
<a href="#" onClick={this.viewDetails} className="text-inherit">{this.cutLink(link)}</a>
</td>
<td>
{(isPro && permissions) && (
<ShareLinkPermissionEditor
@@ -84,9 +99,20 @@ class LinkItem extends React.Component {
</td>
<td>
<a href="#" role="button" onClick={this.copyLink} className={`sf2-icon-copy action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Copy')} aria-label={gettext('Copy')}></a>
<a href="#" role="button" onClick={this.viewDetails} className={`fas fa-info-circle font-weight-bold action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Details')} aria-label={gettext('Details')}></a>
<a href="#" role="button" onClick={this.viewDetails} className={`fa fa-pencil-alt attr-action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Edit')} aria-label={gettext('Edit')}></a>
<a href="#" role="button" onClick={this.toggleDeleteShareLinkDialog} className={`sf2-icon-delete action-icon ${isItemOpVisible ? '' : 'invisible'}`} title={gettext('Delete')} aria-label={gettext('Delete')}></a>
</td>
</tr>
{this.state.isDeleteShareLinkDialogOpen && (
<CommonOperationConfirmationDialog
title={gettext('Delete share link')}
message={gettext('Are you sure you want to delete the share link?')}
executeOperation={this.deleteLink}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteShareLinkDialog}
/>
)}
</Fragment>
);
}
}

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { gettext, siteRoot } from '../../utils/constants';
import EmptyTip from '../empty-tip';
import LinkItem from './link-item';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
const propTypes = {
shareLinks: PropTypes.array.isRequired,
@@ -10,11 +11,24 @@ const propTypes = {
setMode: PropTypes.func.isRequired,
showLinkDetails: PropTypes.func.isRequired,
toggleSelectAllLinks: PropTypes.func.isRequired,
toggleSelectLink: PropTypes.func.isRequired
toggleSelectLink: PropTypes.func.isRequired,
deleteLink: PropTypes.func.isRequired,
deleteShareLinks: PropTypes.func.isRequired
};
class LinkList extends React.Component {
constructor(props) {
super(props);
this.state = {
isDeleteShareLinksDialogOpen: false
};
}
toggleDeleteShareLinksDialog = () => {
this.setState({isDeleteShareLinksDialogOpen: !this.state.isDeleteShareLinksDialogOpen});
}
toggleSelectAllLinks = (e) => {
this.props.toggleSelectAllLinks(e.target.checked);
}
@@ -40,7 +54,7 @@ class LinkList extends React.Component {
<Fragment>
<div className="d-flex justify-content-between align-items-center pb-2 border-bottom">
<h6 className="font-weight-normal m-0">{gettext('Share Link')}</h6>
<div>
<div className="d-flex">
{selectedLinks.length == 0 ? (
<>
<button className="btn btn-sm btn-outline-primary mr-2" onClick={this.props.setMode.bind(this, 'singleLinkCreation')}>{gettext('Generate Link')}</button>
@@ -49,7 +63,8 @@ class LinkList extends React.Component {
) : (
<>
<button className="btn btn-sm btn-secondary mr-2" onClick={this.cancelSelectAllLinks}>{gettext('Cancel')}</button>
<button className="btn btn-sm btn-primary" onClick={this.exportSelectedLinks}>{gettext('Export')}</button>
<button className="btn btn-sm btn-secondary mr-2" onClick={this.toggleDeleteShareLinksDialog}>{gettext('Delete')}</button>
<button className="btn btn-sm btn-secondary" onClick={this.exportSelectedLinks}>{gettext('Export')}</button>
</>
)}
</div>
@@ -67,8 +82,8 @@ class LinkList extends React.Component {
</th>
<th width="23%">{gettext('Link')}</th>
<th width="30%">{gettext('Permission')}</th>
<th width="28%">{gettext('Expiration')}</th>
<th width="14%"></th>
<th width="24%">{gettext('Expiration')}</th>
<th width="18%"></th>
</tr>
</thead>
<tbody>
@@ -80,12 +95,22 @@ class LinkList extends React.Component {
permissionOptions={permissionOptions}
showLinkDetails={this.props.showLinkDetails}
toggleSelectLink={this.props.toggleSelectLink}
deleteLink={this.props.deleteLink}
/>
);
})}
</tbody>
</table>
)}
{this.state.isDeleteShareLinksDialogOpen && (
<CommonOperationConfirmationDialog
title={gettext('Delete share links')}
message={gettext('Are you sure you want to delete the selected share link(s) ?')}
executeOperation={this.props.deleteShareLinks}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteShareLinksDialog}
/>
)}
</Fragment>
);
}

View File

@@ -459,6 +459,71 @@ class ShareLinks(APIView):
link_info = get_share_link_info(fs)
return Response(link_info)
def delete(self, request):
""" Delete share links.
Permission checking:
1. default(NOT guest) user;
2. link owner;
"""
token_list = request.data.get('tokens')
if not token_list:
error_msg = 'token invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
result = {}
result['failed'] = []
result['success'] = []
username = request.user.username
for token in token_list:
try:
fs = FileShare.objects.get(token=token)
except FileShare.DoesNotExist:
result['success'].append({
'token': token,
})
continue
has_published_library = False
if fs.path == '/':
try:
Wiki.objects.get(repo_id=fs.repo_id)
has_published_library = True
except Wiki.DoesNotExist:
pass
if not fs.is_owner(username):
result['failed'].append({
'token': token,
'error_msg': 'Permission denied.'
})
continue
if has_published_library:
result['failed'].append({
'token': token,
'error_msg': _('There is an associated published library.')
})
continue
try:
fs.delete()
result['success'].append({
'token': token,
})
except Exception as e:
logger.error(e)
result['failed'].append({
'token': token,
'error_msg': 'Internal Server Error'
})
continue
return Response(result)
class ShareLink(APIView):