diff --git a/public/app/core/utils/shortLinks.test.ts b/public/app/core/utils/shortLinks.test.ts index 38d459b906a..406aa73e99f 100644 --- a/public/app/core/utils/shortLinks.test.ts +++ b/public/app/core/utils/shortLinks.test.ts @@ -15,6 +15,17 @@ jest.mock('@grafana/runtime', () => ({ }, })); +beforeEach(() => { + Object.assign(navigator, { + clipboard: { + write: jest.fn().mockResolvedValue(undefined), + writeText: jest.fn().mockResolvedValue(undefined), + }, + }); + + document.execCommand = jest.fn(); +}); + describe('createShortLink', () => { it('creates short link', async () => { const shortUrl = await createShortLink('www.verylonglinkwehavehere.com'); @@ -23,11 +34,34 @@ describe('createShortLink', () => { }); describe('createAndCopyShortLink', () => { - it('copies short link to clipboard', async () => { + it('copies short link to clipboard via document.execCommand when navigator.clipboard is undefined', async () => { + Object.assign(navigator, { + clipboard: { + write: undefined, + }, + }); document.execCommand = jest.fn(); await createAndCopyShortLink('www.verylonglinkwehavehere.com'); expect(document.execCommand).toHaveBeenCalledWith('copy'); }); + + it('copies short link to clipboard via navigator.clipboard.writeText when ClipboardItem is undefined', async () => { + window.isSecureContext = true; + await createAndCopyShortLink('www.verylonglinkwehavehere.com'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('www.short.com'); + }); + + it('copies short link to clipboard via navigator.clipboard.write and ClipboardItem when it is defined', async () => { + global.ClipboardItem = jest.fn().mockImplementation(() => ({ + type: 'text/plain', + size: 0, + slice: jest.fn(), + supports: jest.fn().mockReturnValue(true), + // eslint-disable-next-line + })) as any; + await createAndCopyShortLink('www.verylonglinkwehavehere.com'); + expect(navigator.clipboard.write).toHaveBeenCalled(); + }); }); describe('getLogsPermalinkRange', () => { diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index 3ab6556aa67..b6bfc95b006 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -35,13 +35,30 @@ export const createShortLink = memoizeOne(async function (path: string) { } }); +/** + * Creates a ClipboardItem for the shortened link. This is used due to clipboard issues in Safari after making async calls. + * See https://github.com/grafana/grafana/issues/106889 + * @param path - The long path to share. + * @returns A ClipboardItem for the shortened link. + */ +const createShortLinkClipboardItem = (path: string) => { + return new ClipboardItem({ + 'text/plain': createShortLink(path), + }); +}; + export const createAndCopyShortLink = async (path: string) => { - const shortLink = await createShortLink(path); - if (shortLink) { - copyStringToClipboard(shortLink); + if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write) { + navigator.clipboard.write([createShortLinkClipboardItem(path)]); dispatch(notifyApp(createSuccessNotification('Shortened link copied to clipboard'))); } else { - dispatch(notifyApp(createErrorNotification('Error generating shortened link'))); + const shortLink = await createShortLink(path); + if (shortLink) { + copyStringToClipboard(shortLink); + dispatch(notifyApp(createSuccessNotification('Shortened link copied to clipboard'))); + } else { + dispatch(notifyApp(createErrorNotification('Error generating shortened link'))); + } } }; diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx index 00c09366d37..66eec343ea3 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx @@ -15,7 +15,7 @@ const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButt export const ShareDashboardButton = ({ dashboard }: ToolbarActionProps) => { const [_, buildUrl] = useAsyncFn(async () => { DashboardInteractions.toolbarShareClick(); - return await buildShareUrl(dashboard); + await buildShareUrl(dashboard); }, [dashboard]); return ( diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx index 0dad98fb05e..00af6541807 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx @@ -22,8 +22,8 @@ export default function ShareButton({ dashboard, panel }: { dashboard: Dashboard const [_, buildUrl] = useAsyncFn(async () => { DashboardInteractions.toolbarShareClick(); - return await buildShareUrl(dashboard, panel); - }, [dashboard]); + await buildShareUrl(dashboard, panel); + }, [dashboard, panel]); const onMenuClick = useCallback((isOpen: boolean) => { if (isOpen) {