import * as React from 'react'; import { Modal, Button } from 'react-bootstrap'; import { ThreeBounce } from 'better-react-spinkit'; import { If } from 'react-if'; import fileDownload from 'react-file-download'; import { action, observable, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; const Clipboard = require('clipboard'); import copyDownloadStyles from './copyDownloadControls.module.scss'; import { CopyDownloadButtons } from './CopyDownloadButtons'; import { ICopyDownloadControlsProps } from './ICopyDownloadControls'; import { AppContext } from 'cbioportal-frontend-commons'; export interface IAsyncCopyDownloadControlsProps extends ICopyDownloadControlsProps { downloadData?: () => Promise; } export interface ICopyDownloadData { status: 'complete' | 'incomplete'; text: string; } /** * @author Selcuk Onur Sumer * @author Aaron Lisman */ @observer export class CopyDownloadControls extends React.Component< IAsyncCopyDownloadControlsProps, {} > { @observable downloadingData = false; @observable copyingData = false; @observable showErrorMessage = false; @observable showTooltipCopyMessage = false; private _copyButton: HTMLButtonElement | null = null; private _modalCopyButton: HTMLButtonElement | null = null; private _modalCopyButtonContainer: HTMLElement | null = null; private _copyText: string | null = null; public static defaultProps: IAsyncCopyDownloadControlsProps = { className: '', copyMessageDuration: 3000, showCopy: true, showDownload: true, downloadFilename: 'data.tsv', }; constructor(props: IAsyncCopyDownloadControlsProps) { super(props); makeObservable(this); this.handleDownload = this.handleDownload.bind(this); this.handleCopy = this.handleCopy.bind(this); this.handleModalClose = this.handleModalClose.bind(this); } componentDidMount() { // this is necessary because the clipboard wrapper library // doesn't work with tooltips :( if (this.props.showCopy) { this.bindCopyButton(this._copyButton); } } public bindCopyButton( button: HTMLButtonElement | null, container?: HTMLElement | null ) { if (button) { new Clipboard(button, { text: this.getText.bind(this), // we need to pass a container to the clipboard when we use it in a Modal element // see https://stackoverflow.com/questions/38398070/bootstrap-modal-does-not-work-with-clipboard-js-on-firefox container: container, }); } } public render() { return this.context.showDownloadControls === true ? ( { this._copyButton = el; }} /> {this.downloadIndicatorModal()} {this.copyIndicatorModal()} {this.downloadErrorModal()} ) : null; } public downloadIndicatorModal(): JSX.Element { return ( undefined} bsSize="sm" className={`${copyDownloadStyles['centered-modal-dialog']}`} > Downloading Table Data... ); } public copyIndicatorModal(): JSX.Element { return ( undefined} onEntered={() => { this.bindCopyButton( this._modalCopyButton, this._modalCopyButtonContainer ); }} bsSize="sm" className={`${copyDownloadStyles['centered-modal-dialog']}`} > {this.showErrorMessage ? 'Copy Error!' : 'Copy Ready!'} {this.showErrorMessage && 'An error occurred while copying the data. Data may be incomplete.'} Please click on Copy to copy the data to clipboard. { this._modalCopyButtonContainer = el; }} > ); } public downloadErrorModal(): JSX.Element { return ( Download Error! An error occurred while downloading the data. Downloaded file may contain incomplete data. ); } public getText(): string { return this._copyText || ''; } public handleCopy() { this.initCopyProcess(); } public initCopyProcess() { // this makes sure that copy data and the download data are the same/consistent this.initDownloadProcess(text => { // do not update if the copy text is not updated since the last copy request // (also do not update the observable "copyingData" otherwise prompting unnecessary copy modal) if (this._copyText !== text) { this._copyText = text; this.copyingData = true; } else { this.showSimpleCopyMessage(); } }); } public handleDownload() { this.initDownloadProcess(text => { // save the text so that we won't prompt it again for copy action this._copyText = text; // init file download this.download(text); }); } public initDownloadProcess(callback: (text: string) => void) { if (this.props.downloadData) { // mark downloading data true, so that we can show a loading message this.downloadingData = true; this.props .downloadData() .then(copyDownloadData => { if (copyDownloadData.status === 'complete') { // promise is resolved, we need to hide the download indicator this.downloadingData = false; } else { this.triggerDownloadError(); } callback(copyDownloadData.text); }) .catch(() => { this.triggerDownloadError(); }); } } public download(text: string) { fileDownload(text, this.props.downloadFilename); } @action private handleModalClose() { // need to set both flags to false, // in order to not show multiple modals in case of a download error during copy action this.copyingData = false; this.showErrorMessage = false; } @action private triggerDownloadError() { // promise is rejected: we need to hide the download indicator and show an error message this.downloadingData = false; this.showErrorMessage = true; } @action private showSimpleCopyMessage() { this.showTooltipCopyMessage = true; // we only want to show the notification for a limited time setTimeout(() => { this.showTooltipCopyMessage = false; }, this.props.copyMessageDuration); } } CopyDownloadControls.contextType = AppContext;