@@ -72,6 +72,7 @@ import { COLUMN_SIDEBAR_WIDTH } from './components/table-grid/constants'
7272import { COLUMN_TYPE_ICONS } from './components/table-grid/headers'
7373import { useTable , useTableEventStream } from './hooks'
7474import {
75+ ALL_VIEW_PARAM ,
7576 DEFAULT_TABLE_DETAIL_SORT_DIRECTION ,
7677 tableDetailParsers ,
7778 tableDetailUrlKeys ,
@@ -382,6 +383,10 @@ export function Table({
382383 seededViewIdRef . current = null
383384 return
384385 }
386+ if ( activeViewId === ALL_VIEW_PARAM ) {
387+ seededViewIdRef . current = null
388+ return
389+ }
385390 // A `?view=` that resolves to nothing (deleted view, stale bookmark) falls
386391 // back to "All" without touching state, for the same reason. An explicit
387392 // `?sort=` alongside `?view=` also wins over the view's stored sort.
@@ -394,7 +399,7 @@ export function Table({
394399 // "Save as view" the URL names the new view before the list has refetched, and
395400 // clearing there would wipe the very filter that was just saved. Only an
396401 // explicit switch to "All" (`activeViewId === null`) resets.
397- if ( activeViewId !== null && ! activeView ) return
402+ if ( activeViewId !== null && activeViewId !== ALL_VIEW_PARAM && ! activeView ) return
398403
399404 const nextViewId = activeView ?. id ?? null
400405 if ( seededViewIdRef . current === nextViewId ) return
@@ -411,6 +416,30 @@ export function Table({
411416 setTableParams ,
412417 ] )
413418
419+ /**
420+ * Live state pruned the same way `pruneViewConfig` prunes the stored config on
421+ * read. Without this, deleting a hidden or sorted column leaves the local ids
422+ * behind while the server drops them, so the dirty check never balances again —
423+ * Save writes the stale id, the response comes back pruned, and the chip is
424+ * stuck on. Guarded on the schema being loaded so an empty first render doesn't
425+ * prune everything.
426+ */
427+ const liveColumnIds = useMemo ( ( ) => new Set ( columns . map ( getColumnId ) ) , [ columns ] )
428+ const effectiveHiddenColumns = useMemo (
429+ ( ) =>
430+ columns . length === 0 ? hiddenColumns : hiddenColumns . filter ( ( id ) => liveColumnIds . has ( id ) ) ,
431+ [ columns . length , hiddenColumns , liveColumnIds ]
432+ )
433+ const effectiveSort = useMemo < Sort | null > (
434+ ( ) =>
435+ ! sortQuery || columns . length === 0
436+ ? sortQuery
437+ : Object . keys ( sortQuery ) . every ( ( id ) => liveColumnIds . has ( id ) )
438+ ? sortQuery
439+ : null ,
440+ [ sortQuery , columns . length , liveColumnIds ]
441+ )
442+
414443 /** The payload for creating a view, and the left-hand side of the dirty check.
415444 * Carries the current layout so "Save as view" from "All" captures the widths /
416445 * order / pins the grid is rendering (they live in the table's shared metadata
@@ -420,10 +449,10 @@ export function Table({
420449 ( ) => ( {
421450 ...( activeView ?. config ?? tableData ?. metadata ) ,
422451 filter,
423- sort : sortQuery ,
424- hiddenColumns,
452+ sort : effectiveSort ,
453+ hiddenColumns : effectiveHiddenColumns ,
425454 } ) ,
426- [ activeView , tableData ?. metadata , filter , sortQuery , hiddenColumns ]
455+ [ activeView , tableData ?. metadata , filter , effectiveSort , effectiveHiddenColumns ]
427456 )
428457
429458 /**
@@ -433,7 +462,7 @@ export function Table({
433462 */
434463 const isViewDirty = activeView
435464 ? ! isSameViewConfig ( currentViewConfig , activeView . config )
436- : Boolean ( filter ) || Boolean ( sortQuery ) || hiddenColumns . length > 0
465+ : Boolean ( filter ) || Boolean ( effectiveSort ) || effectiveHiddenColumns . length > 0
437466
438467 /** Rename targets a live view rather than a snapshot, so a concurrent rename or
439468 * delete can't leave the modal editing stale data. */
@@ -442,7 +471,7 @@ export function Table({
442471
443472 const handleSelectView = useCallback (
444473 ( viewId : string | null ) => {
445- setTableParams ( { view : viewId } )
474+ setTableParams ( { view : viewId ?? ALL_VIEW_PARAM } )
446475 } ,
447476 [ setTableParams ]
448477 )
@@ -474,7 +503,14 @@ export function Table({
474503 // still in flight (and vice versa). `null`/`[]` merge as explicit values, so
475504 // clearing a filter or unhiding every column still persists as a removal.
476505 updateViewMutation . mutate (
477- { viewId : activeView . id , configPatch : { filter, sort : sortQuery , hiddenColumns } } ,
506+ {
507+ viewId : activeView . id ,
508+ configPatch : {
509+ filter,
510+ sort : effectiveSort ,
511+ hiddenColumns : effectiveHiddenColumns ,
512+ } ,
513+ } ,
478514 { onError : ( error ) => toast . error ( getErrorMessage ( error , 'Failed to save view' ) ) }
479515 )
480516 return
@@ -511,7 +547,7 @@ export function Table({
511547 ( viewId : string ) => {
512548 deleteViewMutation . mutate ( viewId , {
513549 onSuccess : ( ) => {
514- if ( viewId === activeViewId ) setTableParams ( { view : null } )
550+ if ( viewId === activeViewId ) setTableParams ( { view : ALL_VIEW_PARAM } )
515551 } ,
516552 onError : ( error ) => toast . error ( getErrorMessage ( error , 'Failed to delete view' ) ) ,
517553 } )
@@ -1002,7 +1038,7 @@ export function Table({
10021038 onStopRow = { onStopRow }
10031039 onSelectionChange = { onSelectionChange }
10041040 queryOptions = { queryOptions }
1005- hiddenColumns = { hiddenColumns }
1041+ hiddenColumns = { effectiveHiddenColumns }
10061042 viewLayout = { activeView ?. config ?? null }
10071043 viewLayoutKey = { activeView ?. id ?? null }
10081044 onPersistLayout = { activeView ? handlePersistLayout : undefined }
0 commit comments