1- import React , { useCallback , useEffect , useMemo , useState } from 'react' ;
1+ import React , {
2+ useCallback ,
3+ useEffect ,
4+ useMemo ,
5+ useRef ,
6+ useState ,
7+ } from 'react' ;
28import { useSelector } from 'react-redux' ;
39import classNames from 'classnames' ;
410import {
@@ -8,6 +14,10 @@ import {
814 type IrisGridContextMenuData ,
915 IrisGridProps ,
1016 IrisGridUtils ,
17+ IrisGridCacheUtils ,
18+ IrisGridState ,
19+ type DehydratedIrisGridState ,
20+ type DehydratedGridState ,
1121} from '@deephaven/iris-grid' ;
1222import {
1323 ColorValues ,
@@ -18,12 +28,12 @@ import {
1828 viewStyleProps ,
1929} from '@deephaven/components' ;
2030import { useApi } from '@deephaven/jsapi-bootstrap' ;
21- import { TableUtils } from '@deephaven/jsapi-utils' ;
2231import type { dh as DhType } from '@deephaven/jsapi-types' ;
2332import Log from '@deephaven/log' ;
2433import { getSettings , RootState } from '@deephaven/redux' ;
25- import { GridMouseHandler } from '@deephaven/grid' ;
34+ import { GridMouseHandler , GridState } from '@deephaven/grid' ;
2635import { EMPTY_ARRAY , ensureArray } from '@deephaven/utils' ;
36+ import { usePersistentState } from '@deephaven/plugin' ;
2737import {
2838 DatabarConfig ,
2939 FormattingRule ,
@@ -275,6 +285,35 @@ export function UITable({
275285 model . setColorMap ( colorMap ) ;
276286 }
277287
288+ const [ dehydratedState , setDehydratedState ] = usePersistentState <
289+ ( DehydratedIrisGridState & DehydratedGridState ) | undefined
290+ > ( undefined , { type : 'UITable' , version : 1 } ) ;
291+ const initialState = useRef ( dehydratedState ) ;
292+
293+ const memoizedStateFn = useMemo (
294+ ( ) => IrisGridCacheUtils . makeMemoizedCombinedGridStateDehydrator ( ) ,
295+ [ ]
296+ ) ;
297+
298+ const onStateChange = useCallback (
299+ ( irisGridState : IrisGridState , gridState : GridState ) => {
300+ if ( model == null ) {
301+ return ;
302+ }
303+ setDehydratedState ( memoizedStateFn ( model , irisGridState , gridState ) ) ;
304+ } ,
305+ [ memoizedStateFn , model , setDehydratedState ]
306+ ) ;
307+
308+ const initialHydratedState = useMemo ( ( ) => {
309+ if ( model && initialState . current != null ) {
310+ return {
311+ ...utils . hydrateIrisGridState ( model , initialState . current ) ,
312+ ...IrisGridUtils . hydrateGridState ( model , initialState . current ) ,
313+ } ;
314+ }
315+ } , [ model , utils ] ) ;
316+
278317 const hydratedSorts = useMemo ( ( ) => {
279318 if ( sorts !== undefined && columns !== undefined ) {
280319 log . debug ( 'Hydrating sorts' , sorts ) ;
@@ -401,59 +440,92 @@ export function UITable({
401440 [ contextMenu , alwaysFetchColumns ]
402441 ) ;
403442
404- const irisGridProps = useMemo (
405- ( ) =>
406- ( {
407- mouseHandlers,
408- alwaysFetchColumns,
409- showSearchBar,
410- sorts : hydratedSorts ,
411- quickFilters : hydratedQuickFilters ,
412- isFilterBarShown : showQuickFilters ,
413- reverseType : reverse
414- ? TableUtils . REVERSE_TYPE . POST_SORT
415- : TableUtils . REVERSE_TYPE . NONE ,
416- density,
417- settings : { ...settings , showExtraGroupColumn : showGroupingColumn } ,
418- onContextMenu,
419- aggregationSettings : {
420- aggregations :
421- aggregations != null
422- ? ensureArray ( aggregations ) . map ( agg => {
423- if ( agg . cols != null && agg . ignore_cols != null ) {
424- throw new Error (
425- 'Cannot specify both cols and ignore_cols in a UI table aggregation'
426- ) ;
427- }
428- return {
429- operation : getAggregationOperation ( agg . agg ) ,
430- selected : ensureArray ( agg . cols ?? agg . ignore_cols ?? [ ] ) ,
431- // If agg.cols is set, we don't want to invert
432- // If it is not set, then the only other options are ignore_cols or neither
433- // In both cases, we want to invert since we are either ignoring, or selecting all as [] inverted
434- invert : agg . cols == null ,
435- } ;
436- } )
437- : [ ] ,
438- showOnTop : aggregationsPosition === 'top' ,
439- } ,
440- } ) satisfies Partial < IrisGridProps > ,
441- [
443+ const irisGridServerProps = useMemo ( ( ) => {
444+ const props = {
442445 mouseHandlers,
443446 alwaysFetchColumns,
444447 showSearchBar,
445- showQuickFilters ,
446- hydratedSorts ,
447- hydratedQuickFilters ,
448+ sorts : hydratedSorts ,
449+ quickFilters : hydratedQuickFilters ,
450+ isFilterBarShown : showQuickFilters ,
448451 reverse,
449452 density,
450- settings ,
451- showGroupingColumn ,
453+ settings : { ...settings , showExtraGroupColumn : showGroupingColumn } ,
452454 onContextMenu,
453- aggregations ,
454- aggregationsPosition ,
455- ]
456- ) ;
455+ aggregationSettings : {
456+ aggregations :
457+ aggregations != null
458+ ? ensureArray ( aggregations ) . map ( agg => {
459+ if ( agg . cols != null && agg . ignore_cols != null ) {
460+ throw new Error (
461+ 'Cannot specify both cols and ignore_cols in a UI table aggregation'
462+ ) ;
463+ }
464+ return {
465+ operation : getAggregationOperation ( agg . agg ) ,
466+ selected : ensureArray ( agg . cols ?? agg . ignore_cols ?? [ ] ) ,
467+ // If agg.cols is set, we don't want to invert
468+ // If it is not set, then the only other options are ignore_cols or neither
469+ // In both cases, we want to invert since we are either ignoring, or selecting all as [] inverted
470+ invert : agg . cols == null ,
471+ } ;
472+ } )
473+ : [ ] ,
474+ showOnTop : aggregationsPosition === 'top' ,
475+ } ,
476+ } satisfies Partial < IrisGridProps > ;
477+
478+ // Remove any explicit undefined values so we can use client state if available
479+ (
480+ Object . entries ( props ) as [
481+ keyof typeof props ,
482+ ( typeof props ) [ keyof typeof props ] ,
483+ ] [ ]
484+ ) . forEach ( ( [ key , value ] ) => {
485+ if ( value === undefined ) {
486+ delete props [ key ] ;
487+ }
488+ } ) ;
489+
490+ return props ;
491+ } , [
492+ mouseHandlers ,
493+ alwaysFetchColumns ,
494+ showSearchBar ,
495+ showQuickFilters ,
496+ hydratedSorts ,
497+ hydratedQuickFilters ,
498+ reverse ,
499+ density ,
500+ settings ,
501+ showGroupingColumn ,
502+ onContextMenu ,
503+ aggregations ,
504+ aggregationsPosition ,
505+ ] ) ;
506+
507+ const initialIrisGridServerProps = useRef ( irisGridServerProps ) ;
508+
509+ /**
510+ * We want to set the props based on a combination of server state and client state.
511+ * If the server state is the same as its initial state, then we are rehydrating and
512+ * the client state should take precedence.
513+ * Otherwise, we have received changes from the server and we should use those over client state.
514+ * In the future we may want to do a smarter merge of these.
515+ */
516+ const mergedIrisGridProps = useMemo ( ( ) => {
517+ if ( initialIrisGridServerProps . current === irisGridServerProps ) {
518+ return {
519+ ...irisGridServerProps ,
520+ ...initialHydratedState ,
521+ } ;
522+ }
523+
524+ return {
525+ ...initialHydratedState ,
526+ ...irisGridServerProps ,
527+ } ;
528+ } , [ irisGridServerProps , initialHydratedState ] ) ;
457529
458530 return model ? (
459531 < div
@@ -464,8 +536,9 @@ export function UITable({
464536 < IrisGrid
465537 ref = { ref => setIrisGrid ( ref ) }
466538 model = { model }
539+ onStateChange = { onStateChange }
467540 // eslint-disable-next-line react/jsx-props-no-spreading
468- { ...irisGridProps }
541+ { ...mergedIrisGridProps }
469542 />
470543 </ div >
471544 ) : null ;
0 commit comments