Skip to content

Commit 8ab2d52

Browse files
fix(tables): persist explicit All selection, prune view state for deleted columns
1 parent 9fb054c commit 8ab2d52

2 files changed

Lines changed: 59 additions & 15 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,26 @@ export const tableDetailParsers = {
2222
sort: parseAsString,
2323
dir: parseAsStringLiteral(SORT_DIRECTIONS).withDefault(DEFAULT_TABLE_DETAIL_SORT_DIRECTION),
2424
/**
25-
* Active saved view id. Nullable with no default: `null` is the built-in "All"
26-
* state (no view), which is behaviourally distinct from any saved view and is
27-
* what a table with zero views always shows.
25+
* Active view, as a tri-state:
26+
* - absent (`null`) — nothing chosen yet, so the table's default view is adopted
27+
* - {@link ALL_VIEW_PARAM} — "All" chosen *explicitly*; never overridden by a default
28+
* - any other value — a saved view id
29+
*
30+
* The sentinel exists because clearing the param and explicitly picking "All"
31+
* would otherwise be the same URL, so a default view would silently reclaim the
32+
* table on every reload.
2833
*
2934
* Only the id lives here — the view's filter/sort/layout are looked up from the
30-
* loaded list, per the store-the-id-derive-the-object convention. A table's
31-
* default view is resolved on mount and written back explicitly, so a shared
32-
* link keeps pointing at the same view even if someone changes the default.
35+
* loaded list, per the store-the-id-derive-the-object convention. A default view
36+
* is written back explicitly on adoption, so a shared link keeps resolving to the
37+
* same view even if someone later changes which one is default.
3338
*/
3439
view: parseAsString,
3540
} as const
3641

42+
/** Sentinel for an explicit "All" selection. See `tableDetailParsers.view`. */
43+
export const ALL_VIEW_PARAM = 'all'
44+
3745
/**
3846
* Sort + view state: clean URLs, no back-stack churn.
3947
*

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { COLUMN_SIDEBAR_WIDTH } from './components/table-grid/constants'
7272
import { COLUMN_TYPE_ICONS } from './components/table-grid/headers'
7373
import { useTable, useTableEventStream } from './hooks'
7474
import {
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

Comments
 (0)