Skip to content

Commit 17c16ea

Browse files
committed
fix(tables): preserve edits through view hydration
1 parent b7468d9 commit 17c16ea

1 file changed

Lines changed: 106 additions & 30 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]

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

Lines changed: 106 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ const NO_VIEWS: TableViewWire[] = []
170170
/** New views are named before configuration; rename targets an existing view. */
171171
type ViewModalState = { mode: 'new' } | { mode: 'rename'; viewId: string } | null
172172

173+
interface ViewConfigKeep {
174+
sort?: boolean
175+
filter?: boolean
176+
hiddenColumns?: boolean
177+
}
178+
173179
/**
174180
* Page-level wrapper for the table detail view. Mirrors the shape of
175181
* `logs/logs.tsx`: a thin orchestrator that composes the data grid (`<TableGrid>`)
@@ -395,6 +401,16 @@ export function Table({
395401
*/
396402
const pendingCreatedViewIdRef = useRef<string | null>(null)
397403

404+
/** View config gestures made before the views query identifies their owner. */
405+
const pendingViewConfigRef = useRef<TableViewConfig | null>(null)
406+
407+
/**
408+
* State deliberately kept over the first view seed. Deep-linked sort remains
409+
* authoritative until the user changes it; early filter/column gestures stay
410+
* protected until their queued patch succeeds.
411+
*/
412+
const preservedViewStateRef = useRef<{ viewId: string; keep: ViewConfigKeep } | null>(null)
413+
398414
/**
399415
* Replaces the filter from OUTSIDE the filter panel — a view switch, or
400416
* "Filter by cell value". Bumps {@link filterSeed} so the panel re-seeds: it
@@ -411,17 +427,13 @@ export function Table({
411427

412428
/**
413429
* Applies a view's config to the live state. `keep` marks slices the user has
414-
* already set by hand, which win over the view's stored values on the FIRST
415-
* resolve only — a deep-linked `?sort=` is more specific than the view's default,
416-
* and a filter typed while the views query was still in flight shouldn't be
417-
* thrown away when it lands. Switching views later passes no `keep`, so the
418-
* incoming view fully replaces the outgoing one.
430+
* already set by hand. A deep-linked `?sort=` is more specific than the view's
431+
* default, and a filter typed while the views query was still in flight should
432+
* not be thrown away when it lands. Switching views later passes no `keep`, so
433+
* the incoming view fully replaces the outgoing one.
419434
*/
420435
const applyViewConfig = useCallback(
421-
(
422-
config: TableViewConfig | null,
423-
keep?: { sort?: boolean; filter?: boolean; hiddenColumns?: boolean }
424-
) => {
436+
(config: TableViewConfig | null, keep?: ViewConfigKeep) => {
425437
if (!keep?.filter) replaceFilter(config?.filter ?? null)
426438
if (!keep?.hiddenColumns) setHiddenColumns(config?.hiddenColumns ?? [])
427439
if (keep?.sort) return
@@ -484,12 +496,51 @@ export function Table({
484496
[userPermissions.canEdit, readLayout]
485497
)
486498

487-
/** What the user has already set by hand, for the first-resolve `keep`. */
488-
const localWork = () => ({
489-
sort: sortColumn !== null,
490-
filter: filterRef.current !== null,
491-
hiddenColumns: hiddenColumnsRef.current.length > 0,
492-
})
499+
/** What the user has already set by hand when the first view resolves. */
500+
const localWork = () => {
501+
const pending = pendingViewConfigRef.current
502+
return {
503+
sort: sortColumn !== null || Boolean(pending && 'sort' in pending),
504+
filter: filterRef.current !== null || Boolean(pending && 'filter' in pending),
505+
hiddenColumns:
506+
hiddenColumnsRef.current.length > 0 || Boolean(pending && 'hiddenColumns' in pending),
507+
}
508+
}
509+
510+
const preserveViewState = useCallback((viewId: string, keep: ViewConfigKeep | undefined) => {
511+
if (!keep || (!keep.sort && !keep.filter && !keep.hiddenColumns)) {
512+
preservedViewStateRef.current = null
513+
return
514+
}
515+
preservedViewStateRef.current = { viewId, keep }
516+
}, [])
517+
518+
const releasePersistedViewState = useCallback((viewId: string, patch: TableViewConfig) => {
519+
const preserved = preservedViewStateRef.current
520+
if (!preserved || preserved.viewId !== viewId) return
521+
const keep = { ...preserved.keep }
522+
if ('sort' in patch) keep.sort = undefined
523+
if ('filter' in patch) keep.filter = undefined
524+
if ('hiddenColumns' in patch) keep.hiddenColumns = undefined
525+
preservedViewStateRef.current =
526+
keep.sort || keep.filter || keep.hiddenColumns ? { viewId, keep } : null
527+
}, [])
528+
529+
const flushPendingViewConfig = useCallback(
530+
(viewId: string) => {
531+
const configPatch = pendingViewConfigRef.current
532+
if (!configPatch || !userPermissions.canEdit) return
533+
pendingViewConfigRef.current = null
534+
updateViewMutation.mutate(
535+
{ viewId, configPatch },
536+
{
537+
onSuccess: () => releasePersistedViewState(viewId, configPatch),
538+
onError: (error) => toast.error(getErrorMessage(error, 'Failed to save view')),
539+
}
540+
)
541+
},
542+
[userPermissions.canEdit, releasePersistedViewState]
543+
)
493544

494545
/**
495546
* Resolves the active view and seeds the local filter/sort/hidden-column state
@@ -534,9 +585,6 @@ export function Table({
534585
const legacyAllWithDefault = activeViewId === ALL_VIEW_PARAM && defaultView !== null
535586

536587
if (activeViewId === null || inheritedParams || legacyAllWithDefault) {
537-
const pinnedView =
538-
embedded && initialViewId ? views.find((view) => view.id === initialViewId) : undefined
539-
const viewToAdopt = pinnedView ?? defaultView
540588
// `sort` rides the same host URL, so when the view id is inherited the
541589
// sort beside it is too — not local work, and it must not suppress the
542590
// default view's own sort.
@@ -545,11 +593,13 @@ export function Table({
545593
: legacyAllWithDefault
546594
? undefined
547595
: localWork()
548-
if (viewToAdopt) {
549-
appliedViewRevisionRef.current = getTableViewRevision(viewToAdopt)
550-
setTableParams({ view: viewToAdopt.id })
551-
applyViewConfig(viewToAdopt.config, keep)
596+
if (defaultView) {
597+
appliedViewRevisionRef.current = getTableViewRevision(defaultView)
598+
setTableParams({ view: defaultView.id })
599+
preserveViewState(defaultView.id, keep)
600+
applyViewConfig(defaultView.config, keep)
552601
resolvePendingLayout(true)
602+
flushPendingViewConfig(defaultView.id)
553603
return
554604
}
555605
// No view to adopt. Deliberately does NOT apply an empty config — that
@@ -567,13 +617,19 @@ export function Table({
567617
}
568618
// A `?view=` that resolves to nothing adopts the persisted default when
569619
// one exists; tables awaiting backfill retain the legacy All fallback.
570-
appliedViewRevisionRef.current = getTableViewRevision(activeView)
571-
resolvePendingLayout(activeView !== null)
620+
const viewToAdopt = selectedView ?? defaultView
621+
const keep = localWork()
622+
appliedViewRevisionRef.current = getTableViewRevision(viewToAdopt)
623+
resolvePendingLayout(viewToAdopt !== null)
572624
if (selectedView) {
573-
applyViewConfig(selectedView.config, localWork())
625+
preserveViewState(selectedView.id, keep)
626+
applyViewConfig(selectedView.config, keep)
627+
flushPendingViewConfig(selectedView.id)
574628
} else if (defaultView) {
575629
setTableParams({ view: defaultView.id })
576-
applyViewConfig(defaultView.config)
630+
preserveViewState(defaultView.id, keep)
631+
applyViewConfig(defaultView.config, keep)
632+
flushPendingViewConfig(defaultView.id)
577633
} else {
578634
// Nothing to apply, but the URL still names a view that no longer exists.
579635
// Rewrite it so a stale bookmark can't be copied on, and so the param
@@ -595,6 +651,7 @@ export function Table({
595651
// wrong label because the menu resolves the same missing view to null.
596652
if (activeViewId !== null && activeViewId !== ALL_VIEW_PARAM && !selectedView) {
597653
if (pendingCreatedViewIdRef.current === activeViewId) return
654+
preservedViewStateRef.current = null
598655
appliedViewRevisionRef.current = getTableViewRevision(defaultView)
599656
setTableParams({ view: defaultView?.id ?? ALL_VIEW_PARAM })
600657
applyViewConfig(defaultView?.config ?? null)
@@ -613,6 +670,10 @@ export function Table({
613670
}
614671
appliedViewRevisionRef.current = nextViewRevision
615672
const nextViewId = nextViewRevision.id
673+
const preserved = preservedViewStateRef.current
674+
if (preserved && preserved.viewId !== nextViewId) {
675+
preservedViewStateRef.current = null
676+
}
616677
if (activeView && (activeViewId === null || activeViewId === ALL_VIEW_PARAM)) {
617678
setTableParams({ view: activeView.id })
618679
}
@@ -621,7 +682,9 @@ export function Table({
621682
if (pendingCreatedViewIdRef.current && pendingCreatedViewIdRef.current !== nextViewId) {
622683
pendingCreatedViewIdRef.current = null
623684
}
624-
applyViewConfig(activeView?.config ?? null)
685+
const keep = preserved?.viewId === nextViewId ? preserved.keep : undefined
686+
applyViewConfig(activeView?.config ?? null, keep)
687+
if (activeView) flushPendingViewConfig(activeView.id)
625688
}, [
626689
viewsEnabled,
627690
viewsAvailable,
@@ -637,6 +700,8 @@ export function Table({
637700
applyViewConfig,
638701
setTableParams,
639702
resolvePendingLayout,
703+
preserveViewState,
704+
flushPendingViewConfig,
640705
])
641706

642707
/**
@@ -672,6 +737,7 @@ export function Table({
672737

673738
const handleSelectView = useCallback(
674739
(viewId: string | null) => {
740+
preservedViewStateRef.current = null
675741
setTableParams({ view: viewId ?? ALL_VIEW_PARAM })
676742
},
677743
[setTableParams]
@@ -694,17 +760,27 @@ export function Table({
694760
*/
695761
const persistActiveViewConfig = useCallback(
696762
(configPatch: TableViewConfig) => {
697-
const viewId = activeView?.id
698-
if (!viewId || !userPermissions.canEdit) return
763+
if (!userPermissions.canEdit) return
764+
const viewId = activeView?.id ?? pendingCreatedViewIdRef.current
765+
if (!viewId) {
766+
if (!ownerResolvedRef.current) {
767+
pendingViewConfigRef.current = {
768+
...pendingViewConfigRef.current,
769+
...configPatch,
770+
}
771+
}
772+
return
773+
}
699774

700775
updateViewMutation.mutate(
701776
{ viewId, configPatch },
702777
{
778+
onSuccess: () => releasePersistedViewState(viewId, configPatch),
703779
onError: (error) => toast.error(getErrorMessage(error, 'Failed to save view')),
704780
}
705781
)
706782
},
707-
[activeView?.id, userPermissions.canEdit]
783+
[activeView?.id, userPermissions.canEdit, releasePersistedViewState]
708784
)
709785

710786
/** Column order/width/pinning auto-saves into the active view as the user drags.

0 commit comments

Comments
 (0)