diff --git a/packages/react-query-devtools/src/Explorer.tsx b/packages/react-query-devtools/src/Explorer.tsx index bc55515ade..04871143fd 100644 --- a/packages/react-query-devtools/src/Explorer.tsx +++ b/packages/react-query-devtools/src/Explorer.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { displayValue, styled } from './utils' +import superjson from 'superjson' export const Entry = styled('div', { fontFamily: 'Menlo, monospace', @@ -29,6 +30,55 @@ export const ExpandButton = styled('button', { padding: 0, }) +type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy' + +export const CopyButton = ({ value }: { value: unknown }) => { + const [copyState, setCopyState] = React.useState('NoCopy') + + return ( + + ) +} + export const Value = styled('span', (_props, theme) => ({ color: theme.danger, })) @@ -62,6 +112,76 @@ export const Expander = ({ expanded, style = {} }: ExpanderProps) => ( ) +const Copier = () => ( + + + + + + +) + +const ErrorCopier = () => ( + + + + + + See console + + +) + +const CopiedCopier = () => ( + + + + + +) + type Entry = { label: string } @@ -74,6 +194,7 @@ type RendererProps = { subEntryPages: Entry[][] type: string expanded: boolean + copyable: boolean toggleExpanded: () => void pageSize: number } @@ -108,6 +229,7 @@ export const DefaultRenderer: Renderer = ({ subEntryPages = [], type, expanded = false, + copyable = false, toggleExpanded, pageSize, }) => { @@ -124,6 +246,7 @@ export const DefaultRenderer: Renderer = ({ {subEntries.length} {subEntries.length > 1 ? `items` : `item`} + {copyable ? : null} {expanded ? ( subEntryPages.length === 1 ? ( {subEntries.map(handleEntry)} @@ -166,6 +289,7 @@ export const DefaultRenderer: Renderer = ({ type ExplorerProps = Partial & { renderer?: Renderer defaultExpanded?: true | Record + copyable?: boolean } type Property = { @@ -183,6 +307,7 @@ export default function Explorer({ defaultExpanded, renderer = DefaultRenderer, pageSize = 100, + copyable = false, ...rest }: ExplorerProps) { const [expanded, setExpanded] = React.useState(Boolean(defaultExpanded)) @@ -241,6 +366,7 @@ export default function Explorer({ key={entry.label} value={value} renderer={renderer} + copyable={copyable} {...rest} {...entry} /> @@ -250,6 +376,7 @@ export default function Explorer({ subEntryPages, value, expanded, + copyable, toggleExpanded, pageSize, ...rest, diff --git a/packages/react-query-devtools/src/__tests__/Explorer.test.tsx b/packages/react-query-devtools/src/__tests__/Explorer.test.tsx index 5cdccec1f6..7d4dd7e00e 100644 --- a/packages/react-query-devtools/src/__tests__/Explorer.test.tsx +++ b/packages/react-query-devtools/src/__tests__/Explorer.test.tsx @@ -1,7 +1,7 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, render, screen, act } from '@testing-library/react' import * as React from 'react' -import { chunkArray, DefaultRenderer } from '../Explorer' +import { chunkArray, CopyButton, DefaultRenderer } from '../Explorer' import { displayValue } from '../utils' describe('Explorer', () => { @@ -38,6 +38,7 @@ describe('Explorer', () => { toggleExpanded={toggleExpanded} pageSize={10} expanded={false} + copyable={false} subEntryPages={[[{ label: 'A lovely label' }]]} handleEntry={() => <>} value={undefined} @@ -54,6 +55,69 @@ describe('Explorer', () => { expect(toggleExpanded).toHaveBeenCalledTimes(1) }) + + it('when the copy button is clicked, update the clipboard value', async () => { + // Mock clipboard + let clipBoardContent = null + const value = 'someValue' + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: async () => { + return new Promise(() => (clipBoardContent = value)) + }, + }, + configurable: true, + }) + + act(() => { + render() + }) + + // After rendering the clipboard content should be null + expect(clipBoardContent).toBe(null) + + const copyButton = screen.getByRole('button') + + await screen.findByLabelText('Copy object to clipboard') + + // After clicking the content should be added to the clipboard + await act(async () => { + fireEvent.click(copyButton) + }) + + expect(clipBoardContent).toBe(value) + screen.findByLabelText('Object copied to clipboard') + }) + + it('when the copy button is clicked but there is an error, show error state', async () => { + // Mock clipboard with error state + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: async () => { + return new Promise(() => { + throw Error + }) + }, + }, + configurable: true, + }) + + act(() => { + render() + }) + + const copyButton = screen.getByRole('button') + + await screen.findByLabelText('Copy object to clipboard') + + // After clicking the content should NOT be added to the clipboard + await act(async () => { + fireEvent.click(copyButton) + }) + + // Check that it has failed + await screen.findByLabelText('Failed copying to clipboard') + }) }) describe('displayValue', () => { diff --git a/packages/react-query-devtools/src/devtools.tsx b/packages/react-query-devtools/src/devtools.tsx index 2172170322..0f846f2790 100644 --- a/packages/react-query-devtools/src/devtools.tsx +++ b/packages/react-query-devtools/src/devtools.tsx @@ -980,6 +980,7 @@ const ActiveQuery = ({ label="Data" value={activeQueryState.data} defaultExpanded={{}} + copyable />