Skip to content

Commit

Permalink
fix: Copy did not work from embedded iframes (#1528)
Browse files Browse the repository at this point in the history
- 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 <mattrunyonstuff@gmail.com>
  • Loading branch information
mofojed and mattrunyon authored Sep 25, 2023
1 parent 1442ace commit 3549a33
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 35 deletions.
18 changes: 18 additions & 0 deletions packages/embed-grid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.:

```
<html>
<body>
<h1>Dev</h1>
<iframe
src="http://localhost:4010/?name=t"
width="800"
height="500"
allow="clipboard-write"
></iframe>
</body>
</html>
```

## 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')`
Expand Down
6 changes: 4 additions & 2 deletions packages/iris-grid/src/IrisGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServiceWorker>;
Expand Down Expand Up @@ -486,7 +488,7 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
searchValue: '',
selectedSearchColumns: null,
invertSearchColumns: true,
onContextMenu: (): void => undefined,
onContextMenu: (): readonly ResolvableContextAction[] => EMPTY_ARRAY,
pendingDataMap: EMPTY_MAP,
getDownloadWorker: DownloadServiceWorkerUtils.getServiceWorker,
settings: {
Expand Down
32 changes: 32 additions & 0 deletions packages/iris-grid/src/IrisGridCopyHandler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
75 changes: 42 additions & 33 deletions packages/iris-grid/src/IrisGridCopyHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,19 @@ class IrisGridCopyHandler extends Component<
this.setState({ isShown: false });
}

handleCopyClick(): void {
async handleCopyClick(): Promise<void> {
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();
}
Expand All @@ -252,27 +260,20 @@ class IrisGridCopyHandler extends Component<
this.setState({ isShown: false });
}

copyText(text: string): void {
async copyText(text: string): Promise<void> {
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<void> {
this.stopFetch();

this.setState({
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 3549a33

Please sign in to comment.