diff --git a/frontend/src/pages/share-admin/share-links.js b/frontend/src/pages/share-admin/share-links.js index ade3653dd2..401ec4d31f 100644 --- a/frontend/src/pages/share-admin/share-links.js +++ b/frontend/src/pages/share-admin/share-links.js @@ -19,11 +19,15 @@ import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-tool import FixedWidthTable from '../../components/common/fixed-width-table'; import MobileItemMenu from '../../components/mobile-item-menu'; +import '../../css/share-admin-links.css'; + const contentPropTypes = { loading: PropTypes.bool.isRequired, isLoadingMore: PropTypes.bool.isRequired, errorMsg: PropTypes.string.isRequired, items: PropTypes.array.isRequired, + toggleSelectAllLinks: PropTypes.func.isRequired, + toggleSelectLink: PropTypes.func.isRequired, onRemoveLink: PropTypes.func.isRequired }; @@ -40,6 +44,10 @@ class Content extends Component { this.setState({ isItemFreezed: isFreezed }); }; + toggleSelectAllLinks = (e) => { + this.props.toggleSelectAllLinks(e.target.checked); + }; + render() { const { loading, errorMsg, items } = this.props; @@ -58,6 +66,9 @@ class Content extends Component { ); } + const selectedItems = items.filter(item => item.isSelected); + const isAllLinksSelected = selectedItems.length == items.length; + const isDesktop = Utils.isDesktop(); // only for some columns const columnWidths = isPro ? [0.14, 0.07, 0.14] : [0.21, 0.14, 0.2]; @@ -67,6 +78,7 @@ class Content extends Component { ) }, // checkbox { isFixed: true, width: 40 }, // icon { isFixed: false, width: 0.35, children: gettext('Name') }, { isFixed: false, width: columnWidths[0], children: gettext('Library') }, @@ -88,6 +100,7 @@ class Content extends Component { onRemoveLink={this.props.onRemoveLink} isItemFreezed={this.state.isItemFreezed} toggleItemFreezed={this.toggleItemFreezed} + toggleSelectLink={this.props.toggleSelectLink} />); })} @@ -104,7 +117,8 @@ const itemPropTypes = { isDesktop: PropTypes.bool.isRequired, onRemoveLink: PropTypes.func.isRequired, isItemFreezed: PropTypes.bool.isRequired, - toggleItemFreezed: PropTypes.func.isRequired + toggleItemFreezed: PropTypes.func.isRequired, + toggleSelectLink: PropTypes.func.isRequired }; class Item extends Component { @@ -210,9 +224,19 @@ class Item extends Component { }); }; + onCheckboxClicked = (e) => { + e.stopPropagation(); + }; + + toggleSelectLink = (e) => { + const { item } = this.props; + this.props.toggleSelectLink(item, e.target.checked); + }; + render() { - const item = this.props.item; - const { currentPermission, permissionOptions, isOpIconShown, isPermSelectDialogOpen, isLinkDialogOpen } = this.state; + const { item } = this.props; + const { isSelected = false } = item; + const { highlight, currentPermission, permissionOptions, isOpIconShown, isPermSelectDialogOpen, isLinkDialogOpen } = this.state; this.permOptions = permissionOptions.map(item => { return { value: item, @@ -222,7 +246,8 @@ class Item extends Component { }); const currentSelectedPermOption = this.permOptions.filter(item => item.isSelected)[0] || {}; - let iconUrl; let objUrl; + let iconUrl; + let objUrl; if (item.is_dir) { let path = item.path === '/' ? '/' : item.path.slice(0, item.path.length - 1); iconUrl = Utils.getFolderIconUrl(false); @@ -236,11 +261,23 @@ class Item extends Component { {this.props.isDesktop ? + + + {item.is_dir ? @@ -345,13 +382,14 @@ class ShareAdminShareLinks extends Component { constructor(props) { super(props); this.state = { - isCleanInvalidShareLinksDialogOpen: false, loading: true, hasMore: false, isLoadingMore: false, page: 1, errorMsg: '', items: [], + isCleanInvalidShareLinksDialogOpen: false, + isDeleteShareLinksDialogOpen: false }; } @@ -441,25 +479,103 @@ class ShareAdminShareLinks extends Component { }); }; + toggleDeleteShareLinksDialog = () => { + this.setState({ isDeleteShareLinksDialogOpen: !this.state.isDeleteShareLinksDialogOpen }); + }; + + 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: 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({ + 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); + }); + }; + render() { + const { items } = this.state; + const selectedLinksLen = items.filter(item => item.isSelected).length; return (
-
-
    -
  • - - {gettext('Share Links')} - - -
  • - {canGenerateUploadLink && ( -
  • {gettext('Upload Links')}
  • - )} -
+
0 })}> + {selectedLinksLen > 0 + ? ( +
+ + + {`${selectedLinksLen} ${gettext('selected')}`} + + + + +
+ ) + : ( +
    +
  • + + {gettext('Share Links')} + + +
  • + {canGenerateUploadLink && ( +
  • {gettext('Upload Links')}
  • + )} +
+ ) + }
@@ -481,6 +599,15 @@ class ShareAdminShareLinks extends Component { toggleDialog={this.toggleCleanInvalidShareLinksDialog} /> } + {this.state.isDeleteShareLinksDialogOpen && ( + + )} ); } diff --git a/frontend/src/pages/share-admin/upload-links.js b/frontend/src/pages/share-admin/upload-links.js index 7baaf23c1b..e67ae7e364 100644 --- a/frontend/src/pages/share-admin/upload-links.js +++ b/frontend/src/pages/share-admin/upload-links.js @@ -6,6 +6,7 @@ import { DropdownItem } from 'reactstrap'; import classnames from 'classnames'; import { gettext, siteRoot, canGenerateShareLink } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; +import { repoShareAdminAPI } from '../../utils/repo-share-admin-api'; import { Utils } from '../../utils/utils'; import toaster from '../../components/toast'; import Loading from '../../components/loading'; @@ -17,15 +18,23 @@ import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-tool import FixedWidthTable from '../../components/common/fixed-width-table'; import MobileItemMenu from '../../components/mobile-item-menu'; +import '../../css/share-admin-links.css'; + const contentPropTypes = { loading: PropTypes.bool.isRequired, errorMsg: PropTypes.string.isRequired, items: PropTypes.array.isRequired, + toggleSelectAllLinks: PropTypes.func.isRequired, + toggleSelectLink: PropTypes.func.isRequired, onRemoveLink: PropTypes.func.isRequired }; class Content extends Component { + toggleSelectAllLinks = (e) => { + this.props.toggleSelectAllLinks(e.target.checked); + }; + render() { const { loading, errorMsg, items } = this.props; @@ -44,11 +53,20 @@ class Content extends Component { ); } + const selectedItems = items.filter(item => item.isSelected); + const isAllLinksSelected = selectedItems.length == items.length; + const isDesktop = Utils.isDesktop(); return ( ) + }, // checkbox { isFixed: true, width: 40 }, // icon { isFixed: false, width: 0.33, children: gettext('Name') }, { isFixed: false, width: 0.25, children: gettext('Library') }, @@ -62,7 +80,7 @@ class Content extends Component { ]} > {items.map((item, index) => { - return (); + return (); })} ); @@ -74,7 +92,8 @@ Content.propTypes = contentPropTypes; const itemPropTypes = { isDesktop: PropTypes.bool.isRequired, item: PropTypes.object.isRequired, - onRemoveLink: PropTypes.func.isRequired + onRemoveLink: PropTypes.func.isRequired, + toggleSelectLink: PropTypes.func.isRequired }; class Item extends Component { @@ -82,6 +101,7 @@ class Item extends Component { constructor(props) { super(props); this.state = { + highlight: false, isOpIconShown: false, isLinkDialogOpen: false }; @@ -94,11 +114,17 @@ class Item extends Component { }; handleMouseOver = () => { - this.setState({ isOpIconShown: true }); + this.setState({ + highlight: true, + isOpIconShown: true + }); }; handleMouseOut = () => { - this.setState({ isOpIconShown: false }); + this.setState({ + highlight: false, + isOpIconShown: false + }); }; viewLink = (e) => { @@ -121,9 +147,19 @@ class Item extends Component { return ({expire_date}); }; + onCheckboxClicked = (e) => { + e.stopPropagation(); + }; + + toggleSelectLink = (e) => { + const { item } = this.props; + this.props.toggleSelectLink(item, e.target.checked); + }; + render() { - let item = this.props.item; - const { isOpIconShown, isLinkDialogOpen } = this.state; + const { item } = this.props; + const { isSelected = false } = item; + const { highlight, isOpIconShown, isLinkDialogOpen } = this.state; const iconUrl = Utils.getFolderIconUrl(false); const repoUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}`; @@ -132,7 +168,24 @@ class Item extends Component { return ( {this.props.isDesktop ? - + + + + {item.obj_name} @@ -184,10 +237,11 @@ class ShareAdminUploadLinks extends Component { constructor(props) { super(props); this.state = { - isCleanInvalidUploadLinksDialogOpen: false, loading: true, errorMsg: '', - items: [] + items: [], + isCleanInvalidUploadLinksDialogOpen: false, + isDeleteLinksDialogOpen: false }; } @@ -241,25 +295,103 @@ class ShareAdminUploadLinks extends Component { }); }; + toggleDeleteLinksDialog = () => { + this.setState({ isDeleteLinksDialogOpen: !this.state.isDeleteLinksDialogOpen }); + }; + + 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; + }) + }); + }; + + deleteLinks = () => { + 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 newItems = items.filter(item => { + return !success.some(deletedItem => { + return deletedItem.token == item.token; + }); + }); + this.setState({ + items: newItems + }); + 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); + }); + }; + render() { + const { items } = this.state; + const selectedLinksLen = items.filter(item => item.isSelected).length; return (
-
-
    - {canGenerateShareLink && ( -
  • {gettext('Share Links')}
  • - )} -
  • - - {gettext('Upload Links')} - - -
  • -
+
0 })}> + {selectedLinksLen > 0 + ? ( +
+ + + {`${selectedLinksLen} ${gettext('selected')}`} + + + + +
+ ) + : ( +
    + {canGenerateShareLink && ( +
  • {gettext('Share Links')}
  • + )} +
  • + + {gettext('Upload Links')} + + +
  • +
+ ) + }
@@ -280,6 +414,15 @@ class ShareAdminUploadLinks extends Component { toggleDialog={this.toggleCleanInvalidUploadLinksDialog} /> } + {this.state.isDeleteLinksDialogOpen && ( + + )} ); }