@@ -4,8 +4,14 @@ import { clsx as cx } from 'clsx'
44import  {  Index ,  Match ,  Show ,  Switch ,  createMemo ,  createSignal  }  from  'solid-js' 
55import  {  Key  }  from  '@solid-primitives/keyed' 
66import  {  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  {  CopiedCopier ,  Copier ,  ErrorCopier ,  List ,  Trash  }  from  './icons' 
13+ import  {  useQueryDevtoolsContext  }  from  './Context' 
14+ import  type  {  Query  }  from  '@tanstack/query-core' 
915
1016/** 
1117 * Chunk elements in the array by size 
@@ -73,7 +79,8 @@ const CopyButton = (props: { value: unknown }) => {
7379
7480  return  ( 
7581    < button 
76-       class = { styles . copyButton } 
82+       class = { styles . actionButton } 
83+       title = "Copy object to clipboard" 
7784      aria-label = { `${  
7885        copyState ( )  ===  'NoCopy'  
7986          ? 'Copy object to clipboard'  
@@ -118,11 +125,60 @@ const CopyButton = (props: { value: unknown }) => {
118125  ) 
119126} 
120127
128+ const  ClearArrayButton  =  ( props : { 
129+   dataPath : Array < string > 
130+   activeQuery : Query 
131+ } )  =>  { 
132+   const  styles  =  getStyles ( ) 
133+   const  queryClient  =  useQueryDevtoolsContext ( ) . client 
134+ 
135+   return  ( 
136+     < button 
137+       class = { styles . actionButton } 
138+       title = { 'Remove all items' } 
139+       aria-label = { 'Remove all items' } 
140+       onClick = { ( )  =>  { 
141+         const  oldData  =  props . activeQuery . state . data 
142+         const  newData  =  updateNestedDataByPath ( oldData ,  props . dataPath ,  [ ] ) 
143+         queryClient . setQueryData ( props . activeQuery . queryKey ,  newData ) 
144+       } } 
145+     > 
146+       < List  /> 
147+     </ button > 
148+   ) 
149+ } 
150+ 
151+ const  DeleteItemButton  =  ( props : { 
152+   dataPath : Array < string > 
153+   activeQuery : Query 
154+ } )  =>  { 
155+   const  styles  =  getStyles ( ) 
156+   const  queryClient  =  useQueryDevtoolsContext ( ) . client 
157+ 
158+   return  ( 
159+     < button 
160+       class = { cx ( styles . actionButton ) } 
161+       title = { 'Delete item' } 
162+       aria-label = { 'Delete item' } 
163+       onClick = { ( )  =>  { 
164+         const  oldData  =  props . activeQuery . state . data 
165+         const  newData  =  deleteNestedDataByPath ( oldData ,  props . dataPath ) 
166+         queryClient . setQueryData ( props . activeQuery . queryKey ,  newData ) 
167+       } } 
168+     > 
169+       < Trash  /> 
170+     </ button > 
171+   ) 
172+ } 
173+ 
121174type  ExplorerProps  =  { 
122-   copyable ?: boolean 
175+   editable ?: boolean 
123176  label : string 
124177  value : unknown 
125178  defaultExpanded ?: Array < string > 
179+   dataPath ?: Array < string > 
180+   activeQuery ?: Query 
181+   itemsDeletable ?: boolean 
126182} 
127183
128184function  isIterable ( x : any ) : x  is Iterable < unknown >  { 
@@ -131,6 +187,7 @@ function isIterable(x: any): x is Iterable<unknown> {
131187
132188export  default  function  Explorer ( props : ExplorerProps )  { 
133189  const  styles  =  getStyles ( ) 
190+   const  queryClient  =  useQueryDevtoolsContext ( ) . client 
134191
135192  const  [ expanded ,  setExpanded ]  =  createSignal ( 
136193    ( props . defaultExpanded  ||  [ ] ) . includes ( props . label ) , 
@@ -187,6 +244,8 @@ export default function Explorer(props: ExplorerProps) {
187244
188245  const  subEntryPages  =  createMemo ( ( )  =>  chunkArray ( subEntries ( ) ,  100 ) ) 
189246
247+   const  currentDataPath  =  props . dataPath  ??  [ ] 
248+ 
190249  return  ( 
191250    < div  class = { styles . entry } > 
192251      < Show  when = { subEntryPages ( ) . length } > 
@@ -201,8 +260,28 @@ export default function Explorer(props: ExplorerProps) {
201260              { subEntries ( ) . length }  { subEntries ( ) . length  >  1  ? `items`  : `item` } 
202261            </ span > 
203262          </ button > 
204-           < Show  when = { props . copyable } > 
205-             < CopyButton  value = { props . value }  /> 
263+           < Show  when = { props . editable } > 
264+             < div  class = { styles . actions } > 
265+               < CopyButton  value = { props . value }  /> 
266+ 
267+               < Show 
268+                 when = { props . itemsDeletable  &&  props . activeQuery  !==  undefined } 
269+               > 
270+                 < DeleteItemButton 
271+                   activeQuery = { props . activeQuery ! } 
272+                   dataPath = { currentDataPath } 
273+                 /> 
274+               </ Show > 
275+ 
276+               < Show 
277+                 when = { type ( )  ===  'array'  &&  props . activeQuery  !==  undefined } 
278+               > 
279+                 < ClearArrayButton 
280+                   activeQuery = { props . activeQuery ! } 
281+                   dataPath = { currentDataPath } 
282+                 /> 
283+               </ Show > 
284+             </ div > 
206285          </ Show > 
207286        </ div > 
208287        < Show  when = { expanded ( ) } > 
@@ -215,7 +294,14 @@ export default function Explorer(props: ExplorerProps) {
215294                      defaultExpanded = { props . defaultExpanded } 
216295                      label = { entry ( ) . label } 
217296                      value = { entry ( ) . value } 
218-                       copyable = { props . copyable } 
297+                       editable = { props . editable } 
298+                       dataPath = { [ ...currentDataPath ,  entry ( ) . label ] } 
299+                       activeQuery = { props . activeQuery } 
300+                       itemsDeletable = { 
301+                         type ( )  ===  'array'  || 
302+                         type ( )  ===  'Iterable'  || 
303+                         type ( )  ===  'object' 
304+                       } 
219305                    /> 
220306                  ) 
221307                } } 
@@ -250,7 +336,9 @@ export default function Explorer(props: ExplorerProps) {
250336                                defaultExpanded = { props . defaultExpanded } 
251337                                label = { entry ( ) . label } 
252338                                value = { entry ( ) . value } 
253-                                 copyable = { props . copyable } 
339+                                 editable = { props . editable } 
340+                                 dataPath = { [ ...currentDataPath ,  entry ( ) . label ] } 
341+                                 activeQuery = { props . activeQuery } 
254342                              /> 
255343                            ) } 
256344                          </ Key > 
@@ -265,13 +353,50 @@ export default function Explorer(props: ExplorerProps) {
265353        </ Show > 
266354      </ Show > 
267355      < 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 > 
356+         < div  class = { styles . row } > 
357+           < span  class = { styles . label } > { props . label } :</ span > 
358+           < Show 
359+             when = { 
360+               props . editable  && 
361+               props . activeQuery  !==  undefined  && 
362+               ( type ( )  ===  'string'  ||  type ( )  ===  'number' ) 
363+             } 
364+             fallback = { 
365+               < span  class = { styles . value } > { displayValue ( props . value ) } </ span > 
366+             } 
367+           > 
368+             < input 
369+               type = { type ( )  ===  'number'  ? 'number'  : 'text' } 
370+               class = { cx ( styles . value ,  styles . editableInput ) } 
371+               value = { props . value  as  string  |  number } 
372+               onChange = { ( changeEvent )  =>  { 
373+                 const  oldData  =  props . activeQuery ! . state . data 
374+ 
375+                 const  newData  =  updateNestedDataByPath ( 
376+                   oldData , 
377+                   currentDataPath , 
378+                   type ( )  ===  'number' 
379+                     ? changeEvent . target . valueAsNumber 
380+                     : changeEvent . target . value , 
381+                 ) 
382+ 
383+                 queryClient . setQueryData ( props . activeQuery ! . queryKey ,  newData ) 
384+               } } 
385+             /> 
386+           </ Show > 
387+ 
388+           < Show 
389+             when = { 
390+               props . editable  && 
391+               props . itemsDeletable  && 
392+               props . activeQuery  !==  undefined 
393+             } 
394+           > 
395+             < DeleteItemButton 
396+               activeQuery = { props . activeQuery ! } 
397+               dataPath = { currentDataPath } 
398+             /> 
399+           </ Show > 
275400        </ div > 
276401      </ Show > 
277402    </ div > 
@@ -315,6 +440,7 @@ const getStyles = () => {
315440      align-items :  center; 
316441      line-height :  1.125rem  ; 
317442      min-height :  1.125rem  ; 
443+       gap : ${ size [ 2 ] }  
318444    ` , 
319445    expanderButton : css ` 
320446      cursor :  pointer; 
@@ -352,8 +478,30 @@ const getStyles = () => {
352478    ` , 
353479    value : css ` 
354480      color : ${ colors . purple [ 400 ] }  
481+       flex-grow :  1 ; 
355482    ` , 
356-     copyButton : css ` 
483+     actions : css ` 
484+       display :  inline-flex; 
485+       gap : ${ size [ 2 ] }  
486+     ` , 
487+     row : css ` 
488+       display :  inline-flex; 
489+       gap : ${ size [ 2 ] }  
490+       width :  100%  ; 
491+       margin-bottom : ${ size [ 0.5 ] }  
492+       line-height :  1.125rem  ; 
493+     ` , 
494+     editableInput : css ` 
495+       border :  none; 
496+       padding :  0px   ${ size [ 1 ] }  
497+       flex-grow :  1 ; 
498+       background-color : ${ colors . gray [ 900 ] }  
499+ 
500+       & : hover  { 
501+         background-color : ${ colors . gray [ 800 ] }  
502+       } 
503+     ` , 
504+     actionButton : css ` 
357505      background-color :  transparent; 
358506      border :  none; 
359507      display :  inline-flex; 
@@ -364,11 +512,17 @@ const getStyles = () => {
364512      width : ${ size [ 3 ] }  
365513      height : ${ size [ 3 ] }  
366514      position :  relative; 
367-       left : ${ size [ 2 ] }  
368515      z-index :  1 ; 
369516
370-       & : hover  svg  .copier  { 
371-         stroke :  ${ colors . gray [ 500 ] } !important ; 
517+       & : hover  svg  { 
518+         .copier ,  
519+         .list  { 
520+           stroke :  ${ colors . gray [ 500 ] } !important ; 
521+         } 
522+ 
523+         .list-item  { 
524+           stroke : ${ colors . gray [ 700 ] }  
525+         } 
372526      } 
373527
374528      & : focus-visible  { 
0 commit comments