@tanstack/react-query-devtools 4.15.1 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-query-devtools",
3
- "version": "4.15.1",
3
+ "version": "4.16.0",
4
4
  "description": "Developer tools to interact with and visualize the TanStack/react-query cache",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
package/src/Explorer.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react'
2
2
 
3
3
  import { displayValue, styled } from './utils'
4
+ import superjson from 'superjson'
4
5
 
5
6
  export const Entry = styled('div', {
6
7
  fontFamily: 'Menlo, monospace',
@@ -29,6 +30,55 @@ export const ExpandButton = styled('button', {
29
30
  padding: 0,
30
31
  })
31
32
 
33
+ type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
34
+
35
+ export const CopyButton = ({ value }: { value: unknown }) => {
36
+ const [copyState, setCopyState] = React.useState<CopyState>('NoCopy')
37
+
38
+ return (
39
+ <button
40
+ onClick={
41
+ copyState === 'NoCopy'
42
+ ? () => {
43
+ navigator.clipboard.writeText(superjson.stringify(value)).then(
44
+ () => {
45
+ setCopyState('SuccessCopy')
46
+ setTimeout(() => {
47
+ setCopyState('NoCopy')
48
+ }, 1500)
49
+ },
50
+ (err) => {
51
+ console.error('Failed to copy: ', err)
52
+ setCopyState('ErrorCopy')
53
+ setTimeout(() => {
54
+ setCopyState('NoCopy')
55
+ }, 1500)
56
+ },
57
+ )
58
+ }
59
+ : undefined
60
+ }
61
+ style={{
62
+ cursor: 'pointer',
63
+ color: 'inherit',
64
+ font: 'inherit',
65
+ outline: 'inherit',
66
+ background: 'transparent',
67
+ border: 'none',
68
+ padding: 0,
69
+ }}
70
+ >
71
+ {copyState === 'NoCopy' ? (
72
+ <Copier />
73
+ ) : copyState === 'SuccessCopy' ? (
74
+ <CopiedCopier />
75
+ ) : (
76
+ <ErrorCopier />
77
+ )}
78
+ </button>
79
+ )
80
+ }
81
+
32
82
  export const Value = styled('span', (_props, theme) => ({
33
83
  color: theme.danger,
34
84
  }))
@@ -62,6 +112,76 @@ export const Expander = ({ expanded, style = {} }: ExpanderProps) => (
62
112
  </span>
63
113
  )
64
114
 
115
+ const Copier = () => (
116
+ <span
117
+ aria-label="Copy object to clipboard"
118
+ title="Copy object to clipboard"
119
+ style={{
120
+ paddingLeft: '1em',
121
+ }}
122
+ >
123
+ <svg height="12" viewBox="0 0 16 12" width="10">
124
+ <path
125
+ fill="currentColor"
126
+ d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
127
+ ></path>
128
+ <path
129
+ fill="currentColor"
130
+ d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
131
+ ></path>
132
+ </svg>
133
+ </span>
134
+ )
135
+
136
+ const ErrorCopier = () => (
137
+ <span
138
+ aria-label="Failed copying to clipboard"
139
+ title="Failed copying to clipboard"
140
+ style={{
141
+ paddingLeft: '1em',
142
+ display: 'flex',
143
+ alignItems: 'center',
144
+ }}
145
+ >
146
+ <svg height="12" viewBox="0 0 16 12" width="10" display="block">
147
+ <path
148
+ fill="red"
149
+ d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"
150
+ ></path>
151
+ </svg>
152
+ <span
153
+ style={{
154
+ color: 'red',
155
+ fontSize: '12px',
156
+ paddingLeft: '4px',
157
+ position: 'relative',
158
+ top: '2px',
159
+ }}
160
+ >
161
+ See console
162
+ </span>
163
+ </span>
164
+ )
165
+
166
+ const CopiedCopier = () => (
167
+ <span
168
+ aria-label="Object copied to clipboard"
169
+ title="Object copied to clipboard"
170
+ style={{
171
+ paddingLeft: '1em',
172
+ display: 'inline-block',
173
+ verticalAlign: 'middle',
174
+ }}
175
+ >
176
+ <svg height="16" viewBox="0 0 16 16" width="16" display="block">
177
+ <path
178
+ fill="green"
179
+ d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
180
+ ></path>
181
+ </svg>
182
+ </span>
183
+ )
184
+
65
185
  type Entry = {
66
186
  label: string
67
187
  }
@@ -74,6 +194,7 @@ type RendererProps = {
74
194
  subEntryPages: Entry[][]
75
195
  type: string
76
196
  expanded: boolean
197
+ copyable: boolean
77
198
  toggleExpanded: () => void
78
199
  pageSize: number
79
200
  }
@@ -108,6 +229,7 @@ export const DefaultRenderer: Renderer = ({
108
229
  subEntryPages = [],
109
230
  type,
110
231
  expanded = false,
232
+ copyable = false,
111
233
  toggleExpanded,
112
234
  pageSize,
113
235
  }) => {
@@ -124,6 +246,7 @@ export const DefaultRenderer: Renderer = ({
124
246
  {subEntries.length} {subEntries.length > 1 ? `items` : `item`}
125
247
  </Info>
126
248
  </ExpandButton>
249
+ {copyable ? <CopyButton value={value} /> : null}
127
250
  {expanded ? (
128
251
  subEntryPages.length === 1 ? (
129
252
  <SubEntries>{subEntries.map(handleEntry)}</SubEntries>
@@ -166,6 +289,7 @@ export const DefaultRenderer: Renderer = ({
166
289
  type ExplorerProps = Partial<RendererProps> & {
167
290
  renderer?: Renderer
168
291
  defaultExpanded?: true | Record<string, boolean>
292
+ copyable?: boolean
169
293
  }
170
294
 
171
295
  type Property = {
@@ -183,6 +307,7 @@ export default function Explorer({
183
307
  defaultExpanded,
184
308
  renderer = DefaultRenderer,
185
309
  pageSize = 100,
310
+ copyable = false,
186
311
  ...rest
187
312
  }: ExplorerProps) {
188
313
  const [expanded, setExpanded] = React.useState(Boolean(defaultExpanded))
@@ -241,6 +366,7 @@ export default function Explorer({
241
366
  key={entry.label}
242
367
  value={value}
243
368
  renderer={renderer}
369
+ copyable={copyable}
244
370
  {...rest}
245
371
  {...entry}
246
372
  />
@@ -250,6 +376,7 @@ export default function Explorer({
250
376
  subEntryPages,
251
377
  value,
252
378
  expanded,
379
+ copyable,
253
380
  toggleExpanded,
254
381
  pageSize,
255
382
  ...rest,
@@ -1,7 +1,7 @@
1
- import { fireEvent, render, screen } from '@testing-library/react'
1
+ import { fireEvent, render, screen, act } from '@testing-library/react'
2
2
  import * as React from 'react'
3
3
 
4
- import { chunkArray, DefaultRenderer } from '../Explorer'
4
+ import { chunkArray, CopyButton, DefaultRenderer } from '../Explorer'
5
5
  import { displayValue } from '../utils'
6
6
 
7
7
  describe('Explorer', () => {
@@ -38,6 +38,7 @@ describe('Explorer', () => {
38
38
  toggleExpanded={toggleExpanded}
39
39
  pageSize={10}
40
40
  expanded={false}
41
+ copyable={false}
41
42
  subEntryPages={[[{ label: 'A lovely label' }]]}
42
43
  handleEntry={() => <></>}
43
44
  value={undefined}
@@ -54,6 +55,69 @@ describe('Explorer', () => {
54
55
 
55
56
  expect(toggleExpanded).toHaveBeenCalledTimes(1)
56
57
  })
58
+
59
+ it('when the copy button is clicked, update the clipboard value', async () => {
60
+ // Mock clipboard
61
+ let clipBoardContent = null
62
+ const value = 'someValue'
63
+ Object.defineProperty(navigator, 'clipboard', {
64
+ value: {
65
+ writeText: async () => {
66
+ return new Promise(() => (clipBoardContent = value))
67
+ },
68
+ },
69
+ configurable: true,
70
+ })
71
+
72
+ act(() => {
73
+ render(<CopyButton value={value} />)
74
+ })
75
+
76
+ // After rendering the clipboard content should be null
77
+ expect(clipBoardContent).toBe(null)
78
+
79
+ const copyButton = screen.getByRole('button')
80
+
81
+ await screen.findByLabelText('Copy object to clipboard')
82
+
83
+ // After clicking the content should be added to the clipboard
84
+ await act(async () => {
85
+ fireEvent.click(copyButton)
86
+ })
87
+
88
+ expect(clipBoardContent).toBe(value)
89
+ screen.findByLabelText('Object copied to clipboard')
90
+ })
91
+
92
+ it('when the copy button is clicked but there is an error, show error state', async () => {
93
+ // Mock clipboard with error state
94
+ Object.defineProperty(navigator, 'clipboard', {
95
+ value: {
96
+ writeText: async () => {
97
+ return new Promise(() => {
98
+ throw Error
99
+ })
100
+ },
101
+ },
102
+ configurable: true,
103
+ })
104
+
105
+ act(() => {
106
+ render(<CopyButton value={'someValue'} />)
107
+ })
108
+
109
+ const copyButton = screen.getByRole('button')
110
+
111
+ await screen.findByLabelText('Copy object to clipboard')
112
+
113
+ // After clicking the content should NOT be added to the clipboard
114
+ await act(async () => {
115
+ fireEvent.click(copyButton)
116
+ })
117
+
118
+ // Check that it has failed
119
+ await screen.findByLabelText('Failed copying to clipboard')
120
+ })
57
121
  })
58
122
 
59
123
  describe('displayValue', () => {
package/src/devtools.tsx CHANGED
@@ -980,6 +980,7 @@ const ActiveQuery = ({
980
980
  label="Data"
981
981
  value={activeQueryState.data}
982
982
  defaultExpanded={{}}
983
+ copyable
983
984
  />
984
985
  </div>
985
986
  <div