From 3549a33c6152660ed44601eb2e03312d694e6167 Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Mon, 25 Sep 2023 15:34:48 -0400 Subject: [PATCH] fix: Copy did not work from embedded iframes (#1528) - embedded iframes require the 'clipboard-write' permission to copy in Chrome. Added note to the README - Display an error correctly if the copy doesn't succeed, and added unit test - Fixed context menu as well - default menu handler did not return an array as it should've - Tested in Firefox and Chrome on Linux - Fixes #1527 --------- Co-authored-by: Matthew Runyon --- packages/embed-grid/README.md | 18 +++++ packages/iris-grid/src/IrisGrid.tsx | 6 +- .../src/IrisGridCopyHandler.test.tsx | 32 ++++++++ .../iris-grid/src/IrisGridCopyHandler.tsx | 75 +++++++++++-------- 4 files changed, 96 insertions(+), 35 deletions(-) diff --git a/packages/embed-grid/README.md b/packages/embed-grid/README.md index d73b0e44a6..590041bc73 100644 --- a/packages/embed-grid/README.md +++ b/packages/embed-grid/README.md @@ -12,6 +12,24 @@ This project uses [Vite](https://vitejs.dev/). It is to provide an example React - `name`: Required. The name of the table to load +## Usage + +You simply need to provide the URL to embed the iframe. Also add the `clipboard-write` permission to allow copying when embedded, e.g.: + +``` + + +

Dev

+ + + +``` + ## API The iframe provides an API to perform some basic actions with the table loaded. Use by posting the command/value as a [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) to the `contentWindow` of the iframe element, e.g. `document.getElementById('my-iframe').contentWindow.postMessage({ command, value }, 'http://localhost:4010')` diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 3c7e5ec407..2b778cceff 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -319,7 +319,9 @@ export interface IrisGridProps { invertSearchColumns: boolean; // eslint-disable-next-line react/no-unused-prop-types - onContextMenu: (data: IrisGridContextMenuData) => ResolvableContextAction[]; + onContextMenu: ( + data: IrisGridContextMenuData + ) => readonly ResolvableContextAction[]; pendingDataMap?: PendingDataMap; getDownloadWorker: () => Promise; @@ -486,7 +488,7 @@ export class IrisGrid extends Component { searchValue: '', selectedSearchColumns: null, invertSearchColumns: true, - onContextMenu: (): void => undefined, + onContextMenu: (): readonly ResolvableContextAction[] => EMPTY_ARRAY, pendingDataMap: EMPTY_MAP, getDownloadWorker: DownloadServiceWorkerUtils.getServiceWorker, settings: { diff --git a/packages/iris-grid/src/IrisGridCopyHandler.test.tsx b/packages/iris-grid/src/IrisGridCopyHandler.test.tsx index ded2ca69cd..8be7d23b1b 100644 --- a/packages/iris-grid/src/IrisGridCopyHandler.test.tsx +++ b/packages/iris-grid/src/IrisGridCopyHandler.test.tsx @@ -160,3 +160,35 @@ it('retry option available if fetching fails', async () => { expect(copyToClipboard).toHaveBeenCalled(); expect(screen.getByText('Copied to Clipboard!')).toBeTruthy(); }); + +it('shows an error if the copy fails permissions', async () => { + const user = userEvent.setup({ delay: null }); + const error = new Error('Test copy error'); + mockedCopyToClipboard.mockReturnValueOnce(Promise.reject(error)); + + const ranges = GridTestUtils.makeRanges(); + const copyOperation = makeCopyOperation(ranges); + mountCopySelection({ copyOperation }); + + await waitFor(() => + expect(copyToClipboard).toHaveBeenCalledWith(DEFAULT_EXPECTED_TEXT) + ); + + expect(screen.getByText('Fetched 50 rows!')).toBeTruthy(); + + mockedCopyToClipboard.mockClear(); + mockedCopyToClipboard.mockReturnValueOnce(Promise.reject(error)); + + const btn = screen.getByText('Click to Copy'); + expect(btn).toBeInTheDocument(); + + await user.click(btn); + + await waitFor(() => + expect(copyToClipboard).toHaveBeenCalledWith(DEFAULT_EXPECTED_TEXT) + ); + + expect( + screen.getByText('Unable to copy. Verify your browser permissions.') + ).toBeInTheDocument(); +}); diff --git a/packages/iris-grid/src/IrisGridCopyHandler.tsx b/packages/iris-grid/src/IrisGridCopyHandler.tsx index 54470f0e83..7b54071f9d 100644 --- a/packages/iris-grid/src/IrisGridCopyHandler.tsx +++ b/packages/iris-grid/src/IrisGridCopyHandler.tsx @@ -234,11 +234,19 @@ class IrisGridCopyHandler extends Component< this.setState({ isShown: false }); } - handleCopyClick(): void { + async handleCopyClick(): Promise { log.debug2('handleCopyClick'); if (this.textData != null) { - this.copyText(this.textData); + try { + await this.copyText(this.textData); + this.showCopyDone(); + } catch (e) { + log.error('Error copying text', e); + this.setState({ + error: 'Unable to copy. Verify your browser permissions.', + }); + } } else { this.startFetch(); } @@ -252,27 +260,20 @@ class IrisGridCopyHandler extends Component< this.setState({ isShown: false }); } - copyText(text: string): void { + async copyText(text: string): Promise { log.debug2('copyText', text); this.textData = text; - copyToClipboard(text).then( - () => { - this.setState({ copyState: IrisGridCopyHandler.COPY_STATES.DONE }); - this.startHideTimer(); - }, - error => { - log.error('copyText error', error); - this.setState({ - buttonState: IrisGridCopyHandler.BUTTON_STATES.CLICK_TO_COPY, - copyState: IrisGridCopyHandler.COPY_STATES.CLICK_REQUIRED, - }); - } - ); + await copyToClipboard(text); + } + + showCopyDone(): void { + this.setState({ copyState: IrisGridCopyHandler.COPY_STATES.DONE }); + this.startHideTimer(); } - startFetch(): void { + async startFetch(): Promise { this.stopFetch(); this.setState({ @@ -310,23 +311,31 @@ class IrisGridCopyHandler extends Component< this.fetchPromise = PromiseUtils.makeCancelable( model.textSnapshot(modelRanges, includeHeaders, formatValue) ); - this.fetchPromise - .then((text: string) => { + try { + const text = await this.fetchPromise; + this.fetchPromise = undefined; + try { + await this.copyText(text); + this.showCopyDone(); + } catch (e) { + log.error('Error copying text', e); + this.setState({ + buttonState: IrisGridCopyHandler.BUTTON_STATES.CLICK_TO_COPY, + copyState: IrisGridCopyHandler.COPY_STATES.CLICK_REQUIRED, + }); + } + } catch (e) { + if (e instanceof CanceledPromiseError) { + log.debug('User cancelled copy.'); + } else { + log.error('Error fetching contents', e); this.fetchPromise = undefined; - this.copyText(text); - }) - .catch((error: unknown) => { - if (error instanceof CanceledPromiseError) { - log.debug('User cancelled copy.'); - } else { - log.error('Error fetching contents', error); - this.fetchPromise = undefined; - this.setState({ - buttonState: IrisGridCopyHandler.BUTTON_STATES.RETRY, - copyState: IrisGridCopyHandler.COPY_STATES.FETCH_ERROR, - }); - } - }); + this.setState({ + buttonState: IrisGridCopyHandler.BUTTON_STATES.RETRY, + copyState: IrisGridCopyHandler.COPY_STATES.FETCH_ERROR, + }); + } + } } stopFetch(): void {