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 {