@@ -15,23 +15,21 @@ import {
1515} from '@deephaven/components' ;
1616import { vsEdit , vsHistory , vsTrash } from '@deephaven/icons' ;
1717import { usePersistentState } from '@deephaven/dashboard' ;
18- import { type MoveOperation } from '@deephaven/grid' ;
1918import {
2019 type TableOption ,
2120 type TableOptionPanelProps ,
2221 type GridStateSnapshot ,
2322 useTableOptionsHost ,
2423 defaultTableOptionsRegistry ,
2524 IrisGridUtils ,
26- type DehydratedQuickFilter ,
27- type DehydratedAdvancedFilter ,
28- type DehydratedSort ,
25+ type DehydratedIrisGridState ,
26+ type DehydratedGridState ,
2927} from '@deephaven/iris-grid' ;
30- import type { ColumnName } from '@deephaven/iris-grid' ;
3128
3229/**
3330 * Dehydrated (JSON-serializable) snapshot of table state.
3431 * Stored via usePersistentState for cross-session persistence.
32+ * Uses the same structure as IrisGridUtils dehydration for proper hydration.
3533 */
3634interface DehydratedStateSnapshot {
3735 /** Unique identifier */
@@ -40,26 +38,10 @@ interface DehydratedStateSnapshot {
4038 timestamp : string ;
4139 /** User-defined name for the snapshot (optional) */
4240 name ?: string ;
43- /** Quick filters state (dehydrated) */
44- quickFilters : readonly DehydratedQuickFilter [ ] ;
45- /** Advanced filters state (dehydrated) */
46- advancedFilters : readonly DehydratedAdvancedFilter [ ] ;
47- /** Sort configuration (dehydrated) */
48- sorts : readonly DehydratedSort [ ] ;
49- /** Reverse sort order */
50- reverse : boolean ;
51- /** Cross-column search value */
52- searchValue : string ;
53- /** Columns for cross-column search */
54- selectedSearchColumns : readonly ColumnName [ ] ;
55- /** Invert search column selection */
56- invertSearchColumns : boolean ;
57- /** Select distinct columns */
58- selectDistinctColumns : readonly ColumnName [ ] ;
59- /** Custom columns */
60- customColumns : readonly ColumnName [ ] ;
61- /** Re-arranged columns (move operations) */
62- movedColumns : readonly MoveOperation [ ] ;
41+ /** Dehydrated IrisGrid state (filters, sorts, custom columns, etc.) */
42+ irisGridState : Partial < DehydratedIrisGridState > ;
43+ /** Dehydrated Grid state (movedColumns, movedRows, etc.) */
44+ gridState : Partial < DehydratedGridState > ;
6345}
6446
6547/**
@@ -76,16 +58,20 @@ const TABLE_HISTORY_OPTION_TYPE = 'table-history-option';
7658
7759/**
7860 * Dehydrates current grid state to a JSON-serializable snapshot.
61+ * Uses IrisGridUtils dehydration methods for proper serialization.
7962 */
8063function dehydrateSnapshot (
8164 gridState : GridStateSnapshot ,
8265 irisGridUtils : IrisGridUtils
83- ) : Omit < DehydratedStateSnapshot , 'id' | 'timestamp' > {
66+ ) : Omit < DehydratedStateSnapshot , 'id' | 'timestamp' | 'name' > {
8467 const { model, quickFilters, advancedFilters, sorts } = gridState ;
85- return {
68+ const { columns } = model ;
69+
70+ // Dehydrate IrisGrid state (filters, sorts, custom columns, etc.)
71+ const irisGridDehydrated : Partial < DehydratedIrisGridState > = {
8672 quickFilters : IrisGridUtils . dehydrateQuickFilters ( quickFilters ) ,
8773 advancedFilters : irisGridUtils . dehydrateAdvancedFilters (
88- model . columns ,
74+ columns ,
8975 advancedFilters
9076 ) ,
9177 sorts : IrisGridUtils . dehydrateSort ( sorts ) ,
@@ -95,7 +81,20 @@ function dehydrateSnapshot(
9581 invertSearchColumns : gridState . invertSearchColumns ,
9682 selectDistinctColumns : [ ...gridState . selectDistinctColumns ] ,
9783 customColumns : [ ...gridState . customColumns ] ,
84+ } ;
85+
86+ // Dehydrate Grid state (movedColumns)
87+ // Use dehydrateGridState to properly convert column indices to names
88+ const gridDehydrated = IrisGridUtils . dehydrateGridState ( model , {
89+ isStuckToBottom : false ,
90+ isStuckToRight : false ,
9891 movedColumns : [ ...gridState . movedColumns ] ,
92+ movedRows : [ ] ,
93+ } ) ;
94+
95+ return {
96+ irisGridState : irisGridDehydrated ,
97+ gridState : gridDehydrated ,
9998 } ;
10099}
101100
@@ -130,7 +129,7 @@ function TableHistoryPanel(_props: TableOptionPanelProps): JSX.Element {
130129 }
131130 ) ;
132131
133- const snapshots = state . snapshots ;
132+ const { snapshots } = state ;
134133
135134 const handleSaveSnapshot = useCallback ( ( ) => {
136135 const snapshot : DehydratedStateSnapshot = {
@@ -145,80 +144,18 @@ function TableHistoryPanel(_props: TableOptionPanelProps): JSX.Element {
145144
146145 const handleRestoreSnapshot = useCallback (
147146 ( snapshot : DehydratedStateSnapshot ) => {
148- const { columns, formatter } = model ;
149-
150- // Get timezone from the model's formatter
151- const timeZone = formatter . timeZone ;
152-
153- // Hydrate quick filters
154- const hydratedQuickFilters = irisGridUtils . hydrateQuickFilters (
155- columns ,
156- snapshot . quickFilters ,
157- timeZone
158- ) ;
159-
160- // Hydrate advanced filters
161- const hydratedAdvancedFilters = irisGridUtils . hydrateAdvancedFilters (
162- columns ,
163- snapshot . advancedFilters ,
164- timeZone
165- ) ;
166-
167- // Hydrate sorts
168- const hydratedSorts = irisGridUtils . hydrateSort ( columns , snapshot . sorts ) ;
169-
170- // Note: Dispatch order matters! SET_SELECT_DISTINCT_COLUMNS and SET_CUSTOM_COLUMNS
171- // trigger handlers that reset sorts/filters, so we dispatch them first (if changing),
172- // then restore everything else.
173-
174- // Only dispatch SELECT_DISTINCT if actually changing (to avoid clearing everything)
175- const currentSelectDistinct = gridState . selectDistinctColumns ;
176- const newSelectDistinct = snapshot . selectDistinctColumns ;
177- const selectDistinctChanging =
178- currentSelectDistinct . length !== newSelectDistinct . length ||
179- ! currentSelectDistinct . every ( ( col , i ) => col === newSelectDistinct [ i ] ) ;
180-
181- if ( selectDistinctChanging ) {
182- dispatch ( {
183- type : 'SET_SELECT_DISTINCT_COLUMNS' ,
184- columns : [ ...snapshot . selectDistinctColumns ] ,
185- } ) ;
186- }
187-
188- dispatch ( {
189- type : 'SET_CUSTOM_COLUMNS' ,
190- columns : [ ...snapshot . customColumns ] ,
191- } ) ;
192-
193- // Now restore filters, sorts, and search (after select distinct is handled)
194- dispatch ( {
195- type : 'SET_QUICK_FILTERS' ,
196- filters : hydratedQuickFilters ,
197- } ) ;
198- dispatch ( {
199- type : 'SET_ADVANCED_FILTERS' ,
200- filters : hydratedAdvancedFilters ,
201- } ) ;
147+ // Use RESTORE_DEHYDRATED_STATE action which handles hydration properly,
148+ // including graceful handling of missing columns when the table structure
149+ // has changed (e.g., columns hidden via Organize Columns).
202150 dispatch ( {
203- type : 'SET_CROSS_COLUMN_SEARCH' ,
204- searchValue : snapshot . searchValue ,
205- selectedSearchColumns : [ ...snapshot . selectedSearchColumns ] ,
206- invertSearchColumns : snapshot . invertSearchColumns ,
207- } ) ;
208-
209- // SET_SORTS and SET_REVERSE must be last since other dispatches can clear them
210- dispatch ( { type : 'SET_SORTS' , sorts : hydratedSorts } ) ;
211- dispatch ( { type : 'SET_REVERSE' , reverse : snapshot . reverse } ) ;
212-
213- // Restore moved columns (re-arranged column order)
214- dispatch ( {
215- type : 'SET_MOVED_COLUMNS' ,
216- columns : [ ...snapshot . movedColumns ] ,
151+ type : 'RESTORE_DEHYDRATED_STATE' ,
152+ irisGridState : snapshot . irisGridState ,
153+ gridState : snapshot . gridState ,
217154 } ) ;
218155
219156 // Stay on the Table History screen after restoring
220157 } ,
221- [ dispatch , model , irisGridUtils , gridState . selectDistinctColumns ]
158+ [ dispatch ]
222159 ) ;
223160
224161 const handleDeleteSnapshot = useCallback (
@@ -314,74 +251,82 @@ function TableHistoryPanel(_props: TableOptionPanelProps): JSX.Element {
314251 }
315252
316253 const currentDehydrated = dehydrateSnapshot ( gridState , irisGridUtils ) ;
254+ const currentIris = currentDehydrated . irisGridState ;
255+ const currentGrid = currentDehydrated . gridState ;
256+
257+ // Handle backward compatibility with old snapshot format
258+ // Old format had properties at top level, new format nests them in irisGridState/gridState
259+ const lastIris = lastSnapshot . irisGridState ?? { } ;
260+ const lastGrid = lastSnapshot . gridState ?? { } ;
317261 const changes : string [ ] = [ ] ;
318262
319263 // Compare sorts
320264 const sortsChanged =
321- JSON . stringify ( currentDehydrated . sorts ) !==
322- JSON . stringify ( lastSnapshot . sorts ) ;
323- if ( sortsChanged ) {
324- changes . push ( `Sorts: ${ currentDehydrated . sorts . length } column(s)` ) ;
265+ JSON . stringify ( currentIris . sorts ) !== JSON . stringify ( lastIris . sorts ) ;
266+ if ( sortsChanged && currentIris . sorts != null ) {
267+ changes . push ( `Sorts: ${ currentIris . sorts . length } column(s)` ) ;
325268 }
326269
327270 // Compare quick filters
328271 const quickFiltersChanged =
329- JSON . stringify ( currentDehydrated . quickFilters ) !==
330- JSON . stringify ( lastSnapshot . quickFilters ) ;
331- if ( quickFiltersChanged ) {
272+ JSON . stringify ( currentIris . quickFilters ) !==
273+ JSON . stringify ( lastIris . quickFilters ) ;
274+ if ( quickFiltersChanged && currentIris . quickFilters != null ) {
332275 changes . push (
333- `Quick Filters: ${ currentDehydrated . quickFilters . length } filter(s)`
276+ `Quick Filters: ${ currentIris . quickFilters . length } filter(s)`
334277 ) ;
335278 }
336279
337280 // Compare advanced filters
338281 const advancedFiltersChanged =
339- JSON . stringify ( currentDehydrated . advancedFilters ) !==
340- JSON . stringify ( lastSnapshot . advancedFilters ) ;
341- if ( advancedFiltersChanged ) {
282+ JSON . stringify ( currentIris . advancedFilters ) !==
283+ JSON . stringify ( lastIris . advancedFilters ) ;
284+ if ( advancedFiltersChanged && currentIris . advancedFilters != null ) {
342285 changes . push (
343- `Advanced Filters: ${ currentDehydrated . advancedFilters . length } filter(s)`
286+ `Advanced Filters: ${ currentIris . advancedFilters . length } filter(s)`
344287 ) ;
345288 }
346289
347290 // Compare search
348- if ( currentDehydrated . searchValue !== lastSnapshot . searchValue ) {
349- changes . push ( `Search: "${ currentDehydrated . searchValue || '(empty)' } "` ) ;
291+ if ( currentIris . searchValue !== lastIris . searchValue ) {
292+ const searchDisplay =
293+ currentIris . searchValue != null && currentIris . searchValue . length > 0
294+ ? currentIris . searchValue
295+ : '(empty)' ;
296+ changes . push ( `Search: "${ searchDisplay } "` ) ;
350297 }
351298
352299 // Compare reverse
353- if ( currentDehydrated . reverse !== lastSnapshot . reverse ) {
354- changes . push ( `Reverse: ${ currentDehydrated . reverse } ` ) ;
300+ if ( currentIris . reverse !== lastIris . reverse ) {
301+ changes . push ( `Reverse: ${ currentIris . reverse } ` ) ;
355302 }
356303
357304 // Compare select distinct
358305 const selectDistinctChanged =
359- JSON . stringify ( currentDehydrated . selectDistinctColumns ) !==
360- JSON . stringify ( lastSnapshot . selectDistinctColumns ) ;
361- if ( selectDistinctChanged ) {
306+ JSON . stringify ( currentIris . selectDistinctColumns ) !==
307+ JSON . stringify ( lastIris . selectDistinctColumns ) ;
308+ if ( selectDistinctChanged && currentIris . selectDistinctColumns != null ) {
362309 changes . push (
363- `Select Distinct: ${ currentDehydrated . selectDistinctColumns . length } column(s)`
310+ `Select Distinct: ${ currentIris . selectDistinctColumns . length } column(s)`
364311 ) ;
365312 }
366313
367314 // Compare custom columns
368315 const customColumnsChanged =
369- JSON . stringify ( currentDehydrated . customColumns ) !==
370- JSON . stringify ( lastSnapshot . customColumns ) ;
371- if ( customColumnsChanged ) {
316+ JSON . stringify ( currentIris . customColumns ) !==
317+ JSON . stringify ( lastIris . customColumns ) ;
318+ if ( customColumnsChanged && currentIris . customColumns != null ) {
372319 changes . push (
373- `Custom Columns: ${ currentDehydrated . customColumns . length } column(s)`
320+ `Custom Columns: ${ currentIris . customColumns . length } column(s)`
374321 ) ;
375322 }
376323
377324 // Compare moved columns
378325 const movedColumnsChanged =
379- JSON . stringify ( currentDehydrated . movedColumns ) !==
380- JSON . stringify ( lastSnapshot . movedColumns ) ;
381- if ( movedColumnsChanged ) {
382- changes . push (
383- `Column Order: ${ currentDehydrated . movedColumns . length } move(s)`
384- ) ;
326+ JSON . stringify ( currentGrid . movedColumns ) !==
327+ JSON . stringify ( lastGrid . movedColumns ) ;
328+ if ( movedColumnsChanged && currentGrid . movedColumns != null ) {
329+ changes . push ( `Column Order: ${ currentGrid . movedColumns . length } move(s)` ) ;
385330 }
386331
387332 return changes ;
0 commit comments