@tanstack/query-devtools 5.0.0-rc.1 → 5.0.0-rc.13

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/src/Explorer.tsx CHANGED
@@ -4,8 +4,14 @@ import { clsx as cx } from 'clsx'
4
4
  import { Index, Match, Show, Switch, createMemo, createSignal } from 'solid-js'
5
5
  import { Key } from '@solid-primitives/keyed'
6
6
  import { tokens } from './theme'
7
- import { displayValue } from './utils'
8
- import { CopiedCopier, Copier, ErrorCopier } from './icons'
7
+ import {
8
+ deleteNestedDataByPath,
9
+ displayValue,
10
+ updateNestedDataByPath,
11
+ } from './utils'
12
+ import { Check, CopiedCopier, Copier, ErrorCopier, List, Trash } from './icons'
13
+ import { useQueryDevtoolsContext, useTheme } from './Context'
14
+ import type { Query } from '@tanstack/query-core'
9
15
 
10
16
  /**
11
17
  * Chunk elements in the array by size
@@ -31,12 +37,15 @@ export function chunkArray<T extends { label: string; value: unknown }>(
31
37
  }
32
38
 
33
39
  const Expander = (props: { expanded: boolean }) => {
34
- const styles = getStyles()
40
+ const theme = useTheme()
41
+ const styles = createMemo(() => {
42
+ return theme() === 'dark' ? darkStyles : lightStyles
43
+ })
35
44
 
36
45
  return (
37
46
  <span
38
47
  class={cx(
39
- styles.expander,
48
+ styles().expander,
40
49
  css`
41
50
  transform: rotate(${props.expanded ? 90 : 0}deg);
42
51
  `,
@@ -68,12 +77,16 @@ const Expander = (props: { expanded: boolean }) => {
68
77
 
69
78
  type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
70
79
  const CopyButton = (props: { value: unknown }) => {
71
- const styles = getStyles()
80
+ const theme = useTheme()
81
+ const styles = createMemo(() => {
82
+ return theme() === 'dark' ? darkStyles : lightStyles
83
+ })
72
84
  const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
73
85
 
74
86
  return (
75
87
  <button
76
- class={styles.copyButton}
88
+ class={styles().actionButton}
89
+ title="Copy object to clipboard"
77
90
  aria-label={`${
78
91
  copyState() === 'NoCopy'
79
92
  ? 'Copy object to clipboard'
@@ -108,7 +121,7 @@ const CopyButton = (props: { value: unknown }) => {
108
121
  <Copier />
109
122
  </Match>
110
123
  <Match when={copyState() === 'SuccessCopy'}>
111
- <CopiedCopier />
124
+ <CopiedCopier theme={theme()} />
112
125
  </Match>
113
126
  <Match when={copyState() === 'ErrorCopy'}>
114
127
  <ErrorCopier />
@@ -118,11 +131,103 @@ const CopyButton = (props: { value: unknown }) => {
118
131
  )
119
132
  }
120
133
 
134
+ const ClearArrayButton = (props: {
135
+ dataPath: Array<string>
136
+ activeQuery: Query
137
+ }) => {
138
+ const theme = useTheme()
139
+ const styles = createMemo(() => {
140
+ return theme() === 'dark' ? darkStyles : lightStyles
141
+ })
142
+ const queryClient = useQueryDevtoolsContext().client
143
+
144
+ return (
145
+ <button
146
+ class={styles().actionButton}
147
+ title={'Remove all items'}
148
+ aria-label={'Remove all items'}
149
+ onClick={() => {
150
+ const oldData = props.activeQuery.state.data
151
+ const newData = updateNestedDataByPath(oldData, props.dataPath, [])
152
+ queryClient.setQueryData(props.activeQuery.queryKey, newData)
153
+ }}
154
+ >
155
+ <List />
156
+ </button>
157
+ )
158
+ }
159
+
160
+ const DeleteItemButton = (props: {
161
+ dataPath: Array<string>
162
+ activeQuery: Query
163
+ }) => {
164
+ const theme = useTheme()
165
+ const styles = createMemo(() => {
166
+ return theme() === 'dark' ? darkStyles : lightStyles
167
+ })
168
+ const queryClient = useQueryDevtoolsContext().client
169
+
170
+ return (
171
+ <button
172
+ class={cx(styles().actionButton)}
173
+ title={'Delete item'}
174
+ aria-label={'Delete item'}
175
+ onClick={() => {
176
+ const oldData = props.activeQuery.state.data
177
+ const newData = deleteNestedDataByPath(oldData, props.dataPath)
178
+ queryClient.setQueryData(props.activeQuery.queryKey, newData)
179
+ }}
180
+ >
181
+ <Trash />
182
+ </button>
183
+ )
184
+ }
185
+
186
+ const ToggleValueButton = (props: {
187
+ dataPath: Array<string>
188
+ activeQuery: Query
189
+ value: boolean
190
+ }) => {
191
+ const theme = useTheme()
192
+ const styles = createMemo(() => {
193
+ return theme() === 'dark' ? darkStyles : lightStyles
194
+ })
195
+ const queryClient = useQueryDevtoolsContext().client
196
+
197
+ return (
198
+ <button
199
+ class={cx(
200
+ styles().actionButton,
201
+ css`
202
+ width: ${tokens.size[3.5]};
203
+ height: ${tokens.size[3.5]};
204
+ `,
205
+ )}
206
+ title={'Toggle value'}
207
+ aria-label={'Toggle value'}
208
+ onClick={() => {
209
+ const oldData = props.activeQuery.state.data
210
+ const newData = updateNestedDataByPath(
211
+ oldData,
212
+ props.dataPath,
213
+ !props.value,
214
+ )
215
+ queryClient.setQueryData(props.activeQuery.queryKey, newData)
216
+ }}
217
+ >
218
+ <Check theme={theme()} checked={props.value} />
219
+ </button>
220
+ )
221
+ }
222
+
121
223
  type ExplorerProps = {
122
- copyable?: boolean
224
+ editable?: boolean
123
225
  label: string
124
226
  value: unknown
125
227
  defaultExpanded?: Array<string>
228
+ dataPath?: Array<string>
229
+ activeQuery?: Query
230
+ itemsDeletable?: boolean
126
231
  }
127
232
 
128
233
  function isIterable(x: any): x is Iterable<unknown> {
@@ -130,7 +235,11 @@ function isIterable(x: any): x is Iterable<unknown> {
130
235
  }
131
236
 
132
237
  export default function Explorer(props: ExplorerProps) {
133
- const styles = getStyles()
238
+ const theme = useTheme()
239
+ const styles = createMemo(() => {
240
+ return theme() === 'dark' ? darkStyles : lightStyles
241
+ })
242
+ const queryClient = useQueryDevtoolsContext().client
134
243
 
135
244
  const [expanded, setExpanded] = createSignal(
136
245
  (props.defaultExpanded || []).includes(props.label),
@@ -187,27 +296,49 @@ export default function Explorer(props: ExplorerProps) {
187
296
 
188
297
  const subEntryPages = createMemo(() => chunkArray(subEntries(), 100))
189
298
 
299
+ const currentDataPath = props.dataPath ?? []
300
+
190
301
  return (
191
- <div class={styles.entry}>
302
+ <div class={styles().entry}>
192
303
  <Show when={subEntryPages().length}>
193
- <div class={styles.expanderButtonContainer}>
304
+ <div class={styles().expanderButtonContainer}>
194
305
  <button
195
- class={styles.expanderButton}
306
+ class={styles().expanderButton}
196
307
  onClick={() => toggleExpanded()}
197
308
  >
198
309
  <Expander expanded={expanded()} /> <span>{props.label}</span>{' '}
199
- <span class={styles.info}>
310
+ <span class={styles().info}>
200
311
  {String(type()).toLowerCase() === 'iterable' ? '(Iterable) ' : ''}
201
312
  {subEntries().length} {subEntries().length > 1 ? `items` : `item`}
202
313
  </span>
203
314
  </button>
204
- <Show when={props.copyable}>
205
- <CopyButton value={props.value} />
315
+ <Show when={props.editable}>
316
+ <div class={styles().actions}>
317
+ <CopyButton value={props.value} />
318
+
319
+ <Show
320
+ when={props.itemsDeletable && props.activeQuery !== undefined}
321
+ >
322
+ <DeleteItemButton
323
+ activeQuery={props.activeQuery!}
324
+ dataPath={currentDataPath}
325
+ />
326
+ </Show>
327
+
328
+ <Show
329
+ when={type() === 'array' && props.activeQuery !== undefined}
330
+ >
331
+ <ClearArrayButton
332
+ activeQuery={props.activeQuery!}
333
+ dataPath={currentDataPath}
334
+ />
335
+ </Show>
336
+ </div>
206
337
  </Show>
207
338
  </div>
208
339
  <Show when={expanded()}>
209
340
  <Show when={subEntryPages().length === 1}>
210
- <div class={styles.subEntry}>
341
+ <div class={styles().subEntry}>
211
342
  <Key each={subEntries()} by={(item) => item.label}>
212
343
  {(entry) => {
213
344
  return (
@@ -215,7 +346,14 @@ export default function Explorer(props: ExplorerProps) {
215
346
  defaultExpanded={props.defaultExpanded}
216
347
  label={entry().label}
217
348
  value={entry().value}
218
- copyable={props.copyable}
349
+ editable={props.editable}
350
+ dataPath={[...currentDataPath, entry().label]}
351
+ activeQuery={props.activeQuery}
352
+ itemsDeletable={
353
+ type() === 'array' ||
354
+ type() === 'Iterable' ||
355
+ type() === 'object'
356
+ }
219
357
  />
220
358
  )
221
359
  }}
@@ -223,11 +361,11 @@ export default function Explorer(props: ExplorerProps) {
223
361
  </div>
224
362
  </Show>
225
363
  <Show when={subEntryPages().length > 1}>
226
- <div class={styles.subEntry}>
364
+ <div class={styles().subEntry}>
227
365
  <Index each={subEntryPages()}>
228
366
  {(entries, index) => (
229
367
  <div>
230
- <div class={styles.entry}>
368
+ <div class={styles().entry}>
231
369
  <button
232
370
  onClick={() =>
233
371
  setExpandedPages((old) =>
@@ -236,21 +374,23 @@ export default function Explorer(props: ExplorerProps) {
236
374
  : [...old, index],
237
375
  )
238
376
  }
239
- class={styles.expanderButton}
377
+ class={styles().expanderButton}
240
378
  >
241
379
  <Expander expanded={expandedPages().includes(index)} />{' '}
242
380
  [{index * 100}...
243
381
  {index * 100 + 100 - 1}]
244
382
  </button>
245
383
  <Show when={expandedPages().includes(index)}>
246
- <div class={styles.subEntry}>
384
+ <div class={styles().subEntry}>
247
385
  <Key each={entries()} by={(entry) => entry.label}>
248
386
  {(entry) => (
249
387
  <Explorer
250
388
  defaultExpanded={props.defaultExpanded}
251
389
  label={entry().label}
252
390
  value={entry().value}
253
- copyable={props.copyable}
391
+ editable={props.editable}
392
+ dataPath={[...currentDataPath, entry().label]}
393
+ activeQuery={props.activeQuery}
254
394
  />
255
395
  )}
256
396
  </Key>
@@ -265,22 +405,86 @@ export default function Explorer(props: ExplorerProps) {
265
405
  </Show>
266
406
  </Show>
267
407
  <Show when={subEntryPages().length === 0}>
268
- <div
269
- style={{
270
- 'line-height': '1.125rem',
271
- }}
272
- >
273
- <span class={styles.label}>{props.label}:</span>{' '}
274
- <span class={styles.value}>{displayValue(props.value)}</span>
408
+ <div class={styles().row}>
409
+ <span class={styles().label}>{props.label}:</span>
410
+ <Show
411
+ when={
412
+ props.editable &&
413
+ props.activeQuery !== undefined &&
414
+ (type() === 'string' ||
415
+ type() === 'number' ||
416
+ type() === 'boolean')
417
+ }
418
+ fallback={
419
+ <span class={styles().value}>{displayValue(props.value)}</span>
420
+ }
421
+ >
422
+ <Show
423
+ when={
424
+ props.editable &&
425
+ props.activeQuery !== undefined &&
426
+ (type() === 'string' || type() === 'number')
427
+ }
428
+ >
429
+ <input
430
+ type={type() === 'number' ? 'number' : 'text'}
431
+ class={cx(styles().value, styles().editableInput)}
432
+ value={props.value as string | number}
433
+ onChange={(changeEvent) => {
434
+ const oldData = props.activeQuery!.state.data
435
+
436
+ const newData = updateNestedDataByPath(
437
+ oldData,
438
+ currentDataPath,
439
+ type() === 'number'
440
+ ? changeEvent.target.valueAsNumber
441
+ : changeEvent.target.value,
442
+ )
443
+
444
+ queryClient.setQueryData(props.activeQuery!.queryKey, newData)
445
+ }}
446
+ />
447
+ </Show>
448
+
449
+ <Show when={type() === 'boolean'}>
450
+ <span
451
+ class={cx(
452
+ styles().value,
453
+ styles().actions,
454
+ styles().editableInput,
455
+ )}
456
+ >
457
+ <ToggleValueButton
458
+ activeQuery={props.activeQuery!}
459
+ dataPath={currentDataPath}
460
+ value={props.value as boolean}
461
+ />
462
+ {displayValue(props.value)}
463
+ </span>
464
+ </Show>
465
+ </Show>
466
+
467
+ <Show
468
+ when={
469
+ props.editable &&
470
+ props.itemsDeletable &&
471
+ props.activeQuery !== undefined
472
+ }
473
+ >
474
+ <DeleteItemButton
475
+ activeQuery={props.activeQuery!}
476
+ dataPath={currentDataPath}
477
+ />
478
+ </Show>
275
479
  </div>
276
480
  </Show>
277
481
  </div>
278
482
  )
279
483
  }
280
484
 
281
- const getStyles = () => {
485
+ const stylesFactory = (theme: 'light' | 'dark') => {
282
486
  const { colors, font, size, border } = tokens
283
-
487
+ const t = (light: string, dark: string) => (theme === 'light' ? light : dark)
284
488
  return {
285
489
  entry: css`
286
490
  & * {
@@ -294,7 +498,7 @@ const getStyles = () => {
294
498
  subEntry: css`
295
499
  margin: 0 0 0 0.5em;
296
500
  padding-left: 0.75em;
297
- border-left: 2px solid ${colors.darkGray[400]};
501
+ border-left: 2px solid ${t(colors.gray[300], colors.darkGray[400])};
298
502
  /* outline: 1px solid ${colors.teal[400]}; */
299
503
  `,
300
504
  expander: css`
@@ -315,6 +519,7 @@ const getStyles = () => {
315
519
  align-items: center;
316
520
  line-height: 1.125rem;
317
521
  min-height: 1.125rem;
522
+ gap: ${size[2]};
318
523
  `,
319
524
  expanderButton: css`
320
525
  cursor: pointer;
@@ -342,19 +547,45 @@ const getStyles = () => {
342
547
  }
343
548
  `,
344
549
  info: css`
345
- color: ${colors.gray[500]};
550
+ color: ${t(colors.gray[500], colors.gray[500])};
346
551
  font-size: ${font.size.xs};
347
552
  margin-left: ${size[1]};
348
553
  /* outline: 1px solid ${colors.yellow[400]}; */
349
554
  `,
350
555
  label: css`
351
- color: ${colors.gray[300]};
556
+ color: ${t(colors.gray[700], colors.gray[300])};
352
557
  `,
353
558
  value: css`
354
- color: ${colors.purple[400]};
559
+ color: ${t(colors.purple[600], colors.purple[400])};
560
+ flex-grow: 1;
561
+ `,
562
+ actions: css`
563
+ display: inline-flex;
564
+ gap: ${size[2]};
565
+ align-items: center;
566
+ `,
567
+ row: css`
568
+ display: inline-flex;
569
+ gap: ${size[2]};
570
+ width: 100%;
571
+ margin-bottom: ${size[0.5]};
572
+ line-height: 1.125rem;
573
+ align-items: center;
574
+ `,
575
+ editableInput: css`
576
+ border: none;
577
+ padding: ${size[0.5]} ${size[1]} ${size[0.5]} ${size[1.5]};
578
+ flex-grow: 1;
579
+ border-radius: ${border.radius.xs};
580
+ background-color: ${t(colors.gray[200], colors.darkGray[500])};
581
+
582
+ &:hover {
583
+ background-color: ${t(colors.gray[300], colors.darkGray[600])};
584
+ }
355
585
  `,
356
- copyButton: css`
586
+ actionButton: css`
357
587
  background-color: transparent;
588
+ color: ${t(colors.gray[500], colors.gray[500])};
358
589
  border: none;
359
590
  display: inline-flex;
360
591
  padding: 0px;
@@ -364,11 +595,10 @@ const getStyles = () => {
364
595
  width: ${size[3]};
365
596
  height: ${size[3]};
366
597
  position: relative;
367
- left: ${size[2]};
368
598
  z-index: 1;
369
599
 
370
- &:hover svg .copier {
371
- stroke: ${colors.gray[500]} !important;
600
+ &:hover svg {
601
+ color: ${t(colors.gray[600], colors.gray[400])};
372
602
  }
373
603
 
374
604
  &:focus-visible {
@@ -379,3 +609,6 @@ const getStyles = () => {
379
609
  `,
380
610
  }
381
611
  }
612
+
613
+ const lightStyles = stylesFactory('light')
614
+ const darkStyles = stylesFactory('dark')