1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-21 11:27:18 +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 = () => { deleteLink = (token) => {
const { sharedLinkInfo, shareLinks } = this.state; const { shareLinks } = this.state;
seafileAPI.deleteShareLink(sharedLinkInfo.token).then(() => { seafileAPI.deleteShareLink(token).then(() => {
this.setState({ this.setState({
mode: '', mode: '',
sharedLinkInfo: null, 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) => { }).catch((error) => {
let errMessage = Utils.getErrorMsg(error); let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage); toaster.danger(errMessage);
@@ -209,6 +240,8 @@ class ShareLinkPanel extends React.Component {
showLinkDetails={this.showLinkDetails} showLinkDetails={this.showLinkDetails}
toggleSelectAllLinks={this.toggleSelectAllLinks} toggleSelectAllLinks={this.toggleSelectAllLinks}
toggleSelectLink={this.toggleSelectLink} 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 { Button } from 'reactstrap';
import { isPro, gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax, shareLinkExpireDaysDefault, canSendShareLinkEmail } from '../../utils/constants'; import { isPro, gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax, shareLinkExpireDaysDefault, canSendShareLinkEmail } from '../../utils/constants';
import ShareLinkPermissionEditor from '../../components/select-editor/share-link-permission-editor'; 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 { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import ShareLink from '../../models/share-link'; import ShareLink from '../../models/share-link';
@@ -38,7 +39,7 @@ class LinkDetails extends React.Component {
expireDays: this.props.defaultExpireDays, expireDays: this.props.defaultExpireDays,
expDate: null, expDate: null,
isOpIconShown: false, isOpIconShown: false,
isNoticeMessageShow: false, isLinkDeleteDialogOpen: false,
isSendLinkShown: false isSendLinkShown: false
}; };
} }
@@ -129,14 +130,20 @@ class LinkDetails extends React.Component {
}); });
} }
onNoticeMessageToggle = () => { toggleLinkDeleteDialog = () => {
this.setState({isNoticeMessageShow: !this.state.isNoticeMessageShow}); this.setState({isLinkDeleteDialogOpen: !this.state.isLinkDeleteDialogOpen});
} }
toggleSendLink = () => { toggleSendLink = () => {
this.setState({ isSendLinkShown: !this.state.isSendLinkShown }); this.setState({ isSendLinkShown: !this.state.isSendLinkShown });
} }
deleteLink = () => {
const { sharedLinkInfo } = this.props;
const { token } = sharedLinkInfo;
this.props.deleteLink(token);
}
goBack = () => { goBack = () => {
this.props.showLinkDetails(null); this.props.showLinkDetails(null);
} }
@@ -235,7 +242,7 @@ class LinkDetails extends React.Component {
</> </>
)} )}
</dl> </dl>
{(canSendShareLinkEmail && !this.state.isSendLinkShown && !this.state.isNoticeMessageShow) && {(canSendShareLinkEmail && !this.state.isSendLinkShown) &&
<Button onClick={this.toggleSendLink} className='mr-2'>{gettext('Send')}</Button> <Button onClick={this.toggleSendLink} className='mr-2'>{gettext('Send')}</Button>
} }
{this.state.isSendLinkShown && {this.state.isSendLinkShown &&
@@ -246,16 +253,17 @@ class LinkDetails extends React.Component {
closeShareDialog={this.props.closeShareDialog} closeShareDialog={this.props.closeShareDialog}
/> />
} }
{(!this.state.isSendLinkShown && !this.state.isNoticeMessageShow) && {(!this.state.isSendLinkShown) &&
<Button onClick={this.onNoticeMessageToggle}>{gettext('Delete')}</Button> <Button onClick={this.toggleLinkDeleteDialog}>{gettext('Delete')}</Button>
} }
{this.state.isNoticeMessageShow && {this.state.isLinkDeleteDialogOpen &&
<div className="alert alert-warning"> <CommonOperationConfirmationDialog
<h4 className="alert-heading">{gettext('Are you sure you want to delete the share link?')}</h4> title={gettext('Delete share link')}
<p className="mb-4">{gettext('If the share link is deleted, no one will be able to access it any more.')}</p> message={gettext('Are you sure you want to delete the share link?')}
<button className="btn btn-primary" onClick={this.props.deleteLink}>{gettext('Delete')}</button>{' '} executeOperation={this.deleteLink}
<button className="btn btn-secondary" onClick={this.onNoticeMessageToggle}>{gettext('Cancel')}</button> confirmBtnText={gettext('Delete')}
</div> toggleDialog={this.toggleLinkDeleteDialog}
/>
} }
</div> </div>
); );

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
@@ -6,12 +6,14 @@ import toaster from '../toast';
import { isPro, gettext } from '../../utils/constants'; import { isPro, gettext } from '../../utils/constants';
import ShareLinkPermissionEditor from '../../components/select-editor/share-link-permission-editor'; import ShareLinkPermissionEditor from '../../components/select-editor/share-link-permission-editor';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
const propTypes = { const propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
permissionOptions: PropTypes.array, permissionOptions: PropTypes.array,
showLinkDetails : PropTypes.func.isRequired, showLinkDetails : PropTypes.func.isRequired,
toggleSelectLink: PropTypes.func.isRequired toggleSelectLink: PropTypes.func.isRequired,
deleteLink: PropTypes.func.isRequired
}; };
class LinkItem extends React.Component { class LinkItem extends React.Component {
@@ -19,7 +21,8 @@ class LinkItem extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { 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); return link.slice(0, 9) + '...' + link.slice(length-5);
} }
toggleDeleteShareLinkDialog = () => {
this.setState({isDeleteShareLinkDialogOpen: !this.state.isDeleteShareLinkDialogOpen});
}
copyLink = (e) => { copyLink = (e) => {
e.preventDefault(); e.preventDefault();
const { item } = this.props; const { item } = this.props;
@@ -57,17 +64,25 @@ class LinkItem extends React.Component {
this.props.toggleSelectLink(item, e.target.checked); this.props.toggleSelectLink(item, e.target.checked);
} }
deleteLink = () => {
const { item } = this.props;
this.props.deleteLink(item.token);
}
render() { render() {
const { isItemOpVisible } = this.state; const { isItemOpVisible } = this.state;
const { item, permissionOptions } = this.props; const { item, permissionOptions } = this.props;
const { isSelected = false, permissions, link, expire_date } = item; const { isSelected = false, permissions, link, expire_date } = item;
const currentPermission = Utils.getShareLinkPermissionStr(permissions); const currentPermission = Utils.getShareLinkPermissionStr(permissions);
return ( return (
<Fragment>
<tr onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={isSelected ? 'tr-highlight' : ''}> <tr onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={isSelected ? 'tr-highlight' : ''}>
<td className="text-center"> <td className="text-center">
<input type="checkbox" checked={isSelected} onChange={this.toggleSelectLink} className="vam" /> <input type="checkbox" checked={isSelected} onChange={this.toggleSelectLink} className="vam" />
</td> </td>
<td>{this.cutLink(link)}</td> <td>
<a href="#" onClick={this.viewDetails} className="text-inherit">{this.cutLink(link)}</a>
</td>
<td> <td>
{(isPro && permissions) && ( {(isPro && permissions) && (
<ShareLinkPermissionEditor <ShareLinkPermissionEditor
@@ -84,9 +99,20 @@ class LinkItem extends React.Component {
</td> </td>
<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.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> </td>
</tr> </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 { gettext, siteRoot } from '../../utils/constants';
import EmptyTip from '../empty-tip'; import EmptyTip from '../empty-tip';
import LinkItem from './link-item'; import LinkItem from './link-item';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
const propTypes = { const propTypes = {
shareLinks: PropTypes.array.isRequired, shareLinks: PropTypes.array.isRequired,
@@ -10,11 +11,24 @@ const propTypes = {
setMode: PropTypes.func.isRequired, setMode: PropTypes.func.isRequired,
showLinkDetails: PropTypes.func.isRequired, showLinkDetails: PropTypes.func.isRequired,
toggleSelectAllLinks: 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 { class LinkList extends React.Component {
constructor(props) {
super(props);
this.state = {
isDeleteShareLinksDialogOpen: false
};
}
toggleDeleteShareLinksDialog = () => {
this.setState({isDeleteShareLinksDialogOpen: !this.state.isDeleteShareLinksDialogOpen});
}
toggleSelectAllLinks = (e) => { toggleSelectAllLinks = (e) => {
this.props.toggleSelectAllLinks(e.target.checked); this.props.toggleSelectAllLinks(e.target.checked);
} }
@@ -40,7 +54,7 @@ class LinkList extends React.Component {
<Fragment> <Fragment>
<div className="d-flex justify-content-between align-items-center pb-2 border-bottom"> <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> <h6 className="font-weight-normal m-0">{gettext('Share Link')}</h6>
<div> <div className="d-flex">
{selectedLinks.length == 0 ? ( {selectedLinks.length == 0 ? (
<> <>
<button className="btn btn-sm btn-outline-primary mr-2" onClick={this.props.setMode.bind(this, 'singleLinkCreation')}>{gettext('Generate Link')}</button> <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-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> </div>
@@ -67,8 +82,8 @@ class LinkList extends React.Component {
</th> </th>
<th width="23%">{gettext('Link')}</th> <th width="23%">{gettext('Link')}</th>
<th width="30%">{gettext('Permission')}</th> <th width="30%">{gettext('Permission')}</th>
<th width="28%">{gettext('Expiration')}</th> <th width="24%">{gettext('Expiration')}</th>
<th width="14%"></th> <th width="18%"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -80,12 +95,22 @@ class LinkList extends React.Component {
permissionOptions={permissionOptions} permissionOptions={permissionOptions}
showLinkDetails={this.props.showLinkDetails} showLinkDetails={this.props.showLinkDetails}
toggleSelectLink={this.props.toggleSelectLink} toggleSelectLink={this.props.toggleSelectLink}
deleteLink={this.props.deleteLink}
/> />
); );
})} })}
</tbody> </tbody>
</table> </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> </Fragment>
); );
} }

View File

@@ -459,6 +459,71 @@ class ShareLinks(APIView):
link_info = get_share_link_info(fs) link_info = get_share_link_info(fs)
return Response(link_info) 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): class ShareLink(APIView):