@@ -177,6 +177,12 @@ const NO_VIEWS: TableViewWire[] = []
177177/** New views are named before configuration; rename targets an existing view. */
178178type ViewModalState = { mode : 'new' } | { mode : 'rename' ; viewId : string } | null
179179
180+ interface ViewConfigKeep {
181+ sort ?: boolean
182+ filter ?: boolean
183+ hiddenColumns ?: boolean
184+ }
185+
180186/**
181187 * Page-level wrapper for the table detail view. Mirrors the shape of
182188 * `logs/logs.tsx`: a thin orchestrator that composes the data grid (`<TableGrid>`)
@@ -403,6 +409,16 @@ export function Table({
403409 */
404410 const pendingCreatedViewIdRef = useRef < string | null > ( null )
405411
412+ /** View config gestures made before the views query identifies their owner. */
413+ const pendingViewConfigRef = useRef < TableViewConfig | null > ( null )
414+
415+ /**
416+ * State deliberately kept over the first view seed. Deep-linked sort remains
417+ * authoritative until the user changes it; early filter/column gestures stay
418+ * protected until their queued patch succeeds.
419+ */
420+ const preservedViewStateRef = useRef < { viewId : string ; keep : ViewConfigKeep } | null > ( null )
421+
406422 /**
407423 * Replaces the filter from OUTSIDE the filter panel — a view switch, or
408424 * "Filter by cell value". Bumps {@link filterSeed} so the panel re-seeds: it
@@ -419,17 +435,13 @@ export function Table({
419435
420436 /**
421437 * Applies a view's config to the live state. `keep` marks slices the user has
422- * already set by hand, which win over the view's stored values on the FIRST
423- * resolve only — a deep-linked `?sort=` is more specific than the view's default,
424- * and a filter typed while the views query was still in flight shouldn't be
425- * thrown away when it lands. Switching views later passes no `keep`, so the
426- * incoming view fully replaces the outgoing one.
438+ * already set by hand. A deep-linked `?sort=` is more specific than the view's
439+ * default, and a filter typed while the views query was still in flight should
440+ * not be thrown away when it lands. Switching views later passes no `keep`, so
441+ * the incoming view fully replaces the outgoing one.
427442 */
428443 const applyViewConfig = useCallback (
429- (
430- config : TableViewConfig | null ,
431- keep ?: { sort ?: boolean ; filter ?: boolean ; hiddenColumns ?: boolean }
432- ) => {
444+ ( config : TableViewConfig | null , keep ?: ViewConfigKeep ) => {
433445 if ( ! keep ?. filter ) replaceFilter ( config ?. filter ?? null )
434446 if ( ! keep ?. hiddenColumns ) setHiddenColumns ( config ?. hiddenColumns ?? [ ] )
435447 if ( keep ?. sort ) return
@@ -492,12 +504,51 @@ export function Table({
492504 [ userPermissions . canEdit , readLayout ]
493505 )
494506
495- /** What the user has already set by hand, for the first-resolve `keep`. */
496- const localWork = ( ) => ( {
497- sort : sortColumn !== null ,
498- filter : filterRef . current !== null ,
499- hiddenColumns : hiddenColumnsRef . current . length > 0 ,
500- } )
507+ /** What the user has already set by hand when the first view resolves. */
508+ const localWork = ( ) => {
509+ const pending = pendingViewConfigRef . current
510+ return {
511+ sort : sortColumn !== null || Boolean ( pending && 'sort' in pending ) ,
512+ filter : filterRef . current !== null || Boolean ( pending && 'filter' in pending ) ,
513+ hiddenColumns :
514+ hiddenColumnsRef . current . length > 0 || Boolean ( pending && 'hiddenColumns' in pending ) ,
515+ }
516+ }
517+
518+ const preserveViewState = useCallback ( ( viewId : string , keep : ViewConfigKeep | undefined ) => {
519+ if ( ! keep || ( ! keep . sort && ! keep . filter && ! keep . hiddenColumns ) ) {
520+ preservedViewStateRef . current = null
521+ return
522+ }
523+ preservedViewStateRef . current = { viewId, keep }
524+ } , [ ] )
525+
526+ const releasePersistedViewState = useCallback ( ( viewId : string , patch : TableViewConfig ) => {
527+ const preserved = preservedViewStateRef . current
528+ if ( ! preserved || preserved . viewId !== viewId ) return
529+ const keep = { ...preserved . keep }
530+ if ( 'sort' in patch ) keep . sort = undefined
531+ if ( 'filter' in patch ) keep . filter = undefined
532+ if ( 'hiddenColumns' in patch ) keep . hiddenColumns = undefined
533+ preservedViewStateRef . current =
534+ keep . sort || keep . filter || keep . hiddenColumns ? { viewId, keep } : null
535+ } , [ ] )
536+
537+ const flushPendingViewConfig = useCallback (
538+ ( viewId : string ) => {
539+ const configPatch = pendingViewConfigRef . current
540+ if ( ! configPatch || ! userPermissions . canEdit ) return
541+ pendingViewConfigRef . current = null
542+ updateViewMutation . mutate (
543+ { viewId, configPatch } ,
544+ {
545+ onSuccess : ( ) => releasePersistedViewState ( viewId , configPatch ) ,
546+ onError : ( error ) => toast . error ( getErrorMessage ( error , 'Failed to save view' ) ) ,
547+ }
548+ )
549+ } ,
550+ [ userPermissions . canEdit , releasePersistedViewState ]
551+ )
501552
502553 /**
503554 * Resolves the active view and seeds the local filter/sort/hidden-column state
@@ -556,8 +607,10 @@ export function Table({
556607 if ( viewToAdopt ) {
557608 appliedViewRevisionRef . current = getTableViewRevision ( viewToAdopt )
558609 setTableParams ( { view : viewToAdopt . id } )
610+ preserveViewState ( viewToAdopt . id , keep )
559611 applyViewConfig ( viewToAdopt . config , keep )
560612 resolvePendingLayout ( true )
613+ flushPendingViewConfig ( viewToAdopt . id )
561614 return
562615 }
563616 // No view to adopt. Deliberately does NOT apply an empty config — that
@@ -575,13 +628,19 @@ export function Table({
575628 }
576629 // A `?view=` that resolves to nothing adopts the persisted default when
577630 // one exists; tables awaiting backfill retain the legacy All fallback.
578- appliedViewRevisionRef . current = getTableViewRevision ( activeView )
579- resolvePendingLayout ( activeView !== null )
631+ const viewToAdopt = selectedView ?? defaultView
632+ const keep = localWork ( )
633+ appliedViewRevisionRef . current = getTableViewRevision ( viewToAdopt )
634+ resolvePendingLayout ( viewToAdopt !== null )
580635 if ( selectedView ) {
581- applyViewConfig ( selectedView . config , localWork ( ) )
636+ preserveViewState ( selectedView . id , keep )
637+ applyViewConfig ( selectedView . config , keep )
638+ flushPendingViewConfig ( selectedView . id )
582639 } else if ( defaultView ) {
583640 setTableParams ( { view : defaultView . id } )
584- applyViewConfig ( defaultView . config )
641+ preserveViewState ( defaultView . id , keep )
642+ applyViewConfig ( defaultView . config , keep )
643+ flushPendingViewConfig ( defaultView . id )
585644 } else {
586645 // Nothing to apply, but the URL still names a view that no longer exists.
587646 // Rewrite it so a stale bookmark can't be copied on, and so the param
@@ -603,6 +662,7 @@ export function Table({
603662 // wrong label because the menu resolves the same missing view to null.
604663 if ( activeViewId !== null && activeViewId !== ALL_VIEW_PARAM && ! selectedView ) {
605664 if ( pendingCreatedViewIdRef . current === activeViewId ) return
665+ preservedViewStateRef . current = null
606666 appliedViewRevisionRef . current = getTableViewRevision ( defaultView )
607667 setTableParams ( { view : defaultView ?. id ?? ALL_VIEW_PARAM } )
608668 applyViewConfig ( defaultView ?. config ?? null )
@@ -621,6 +681,10 @@ export function Table({
621681 }
622682 appliedViewRevisionRef . current = nextViewRevision
623683 const nextViewId = nextViewRevision . id
684+ const preserved = preservedViewStateRef . current
685+ if ( preserved && preserved . viewId !== nextViewId ) {
686+ preservedViewStateRef . current = null
687+ }
624688 if ( activeView && ( activeViewId === null || activeViewId === ALL_VIEW_PARAM ) ) {
625689 setTableParams ( { view : activeView . id } )
626690 }
@@ -629,7 +693,9 @@ export function Table({
629693 if ( pendingCreatedViewIdRef . current && pendingCreatedViewIdRef . current !== nextViewId ) {
630694 pendingCreatedViewIdRef . current = null
631695 }
632- applyViewConfig ( activeView ?. config ?? null )
696+ const keep = preserved ?. viewId === nextViewId ? preserved . keep : undefined
697+ applyViewConfig ( activeView ?. config ?? null , keep )
698+ if ( activeView ) flushPendingViewConfig ( activeView . id )
633699 } , [
634700 viewsEnabled ,
635701 viewsAvailable ,
@@ -645,6 +711,8 @@ export function Table({
645711 applyViewConfig ,
646712 setTableParams ,
647713 resolvePendingLayout ,
714+ preserveViewState ,
715+ flushPendingViewConfig ,
648716 ] )
649717
650718 /**
@@ -680,6 +748,7 @@ export function Table({
680748
681749 const handleSelectView = useCallback (
682750 ( viewId : string | null ) => {
751+ preservedViewStateRef . current = null
683752 setTableParams ( { view : viewId ?? ALL_VIEW_PARAM } )
684753 } ,
685754 [ setTableParams ]
@@ -702,17 +771,27 @@ export function Table({
702771 */
703772 const persistActiveViewConfig = useCallback (
704773 ( configPatch : TableViewConfig ) => {
705- const viewId = activeView ?. id
706- if ( ! viewId || ! userPermissions . canEdit ) return
774+ if ( ! userPermissions . canEdit ) return
775+ const viewId = activeView ?. id ?? pendingCreatedViewIdRef . current
776+ if ( ! viewId ) {
777+ if ( ! ownerResolvedRef . current ) {
778+ pendingViewConfigRef . current = {
779+ ...pendingViewConfigRef . current ,
780+ ...configPatch ,
781+ }
782+ }
783+ return
784+ }
707785
708786 updateViewMutation . mutate (
709787 { viewId, configPatch } ,
710788 {
789+ onSuccess : ( ) => releasePersistedViewState ( viewId , configPatch ) ,
711790 onError : ( error ) => toast . error ( getErrorMessage ( error , 'Failed to save view' ) ) ,
712791 }
713792 )
714793 } ,
715- [ activeView ?. id , userPermissions . canEdit ]
794+ [ activeView ?. id , userPermissions . canEdit , releasePersistedViewState ]
716795 )
717796
718797 /** Column order/width/pinning auto-saves into the active view as the user drags.
0 commit comments