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:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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):
|
||||
|
||||
|
Reference in New Issue
Block a user