fix(base): adopt server view state when no local edit is pending

This commit is contained in:
Philipinho
2026-04-18 22:03:25 +01:00
parent 1aa92b1bb5
commit 93b1fc534b
@@ -222,6 +222,12 @@ export function useBaseTable(
): UseBaseTableResult { ): UseBaseTableResult {
const updateViewMutation = useUpdateViewMutation(); const updateViewMutation = useUpdateViewMutation();
const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// While a local edit is pending (debounce scheduled OR mutation in
// flight), the reconcile effect preserves local state so we don't
// stomp the user's in-flight toggle. When no local edit is pending,
// the effect adopts server state — that's what makes remote updates
// (another client hiding a column) actually show up on this client.
const [hasPendingEdit, setHasPendingEdit] = useState(false);
// `base?.properties ?? []` minted a fresh `[]` every render while the // `base?.properties ?? []` minted a fresh `[]` every render while the
// base query was loading, which invalidated every downstream memo and // base query was loading, which invalidated every downstream memo and
@@ -276,47 +282,55 @@ export function useBaseTable(
return; return;
} }
// Same view — preserve user toggles, but reconcile the id set: // Same view. If a local edit is pending (user just toggled and
// append properties that were just created, drop properties that // the debounce hasn't flushed yet, or the mutation is in flight),
// were deleted. Without this, creating a new column leaves it // preserve local state — only reconcile the id set so that newly
// invisible to `table.getState().columnOrder` / `gridTemplateColumns`, // created columns show up and deleted columns drop out without
// and the grid's scrollWidth never grows to include it. // stomping the user's toggle. If nothing local is pending, adopt
// the server's state — this is what lets remote updates from
// other clients show up here.
const validIds = new Set<string>(["__row_number"]); const validIds = new Set<string>(["__row_number"]);
for (const p of properties) validIds.add(p.id); for (const p of properties) validIds.add(p.id);
setColumnOrder((prev) => { if (hasPendingEdit) {
const prevSet = new Set(prev); setColumnOrder((prev) => {
const kept = prev.filter((id) => validIds.has(id)); const prevSet = new Set(prev);
const appended = derivedColumnOrder.filter( const kept = prev.filter((id) => validIds.has(id));
(id) => !prevSet.has(id) && validIds.has(id), const appended = derivedColumnOrder.filter(
); (id) => !prevSet.has(id) && validIds.has(id),
if (appended.length === 0 && kept.length === prev.length) return prev; );
return [...kept, ...appended]; if (appended.length === 0 && kept.length === prev.length) return prev;
}); return [...kept, ...appended];
});
setColumnVisibility((prev) => { setColumnVisibility((prev) => {
let changed = false; let changed = false;
const next: VisibilityState = {}; const next: VisibilityState = {};
for (const [id, visible] of Object.entries(prev)) { for (const [id, visible] of Object.entries(prev)) {
if (validIds.has(id)) { if (validIds.has(id)) {
next[id] = visible; next[id] = visible;
} else { } else {
changed = true; changed = true;
}
} }
} for (const id of derivedColumnOrder) {
for (const id of derivedColumnOrder) { if (!(id in next)) {
if (!(id in next)) { next[id] = derivedColumnVisibility[id] ?? true;
next[id] = derivedColumnVisibility[id] ?? true; changed = true;
changed = true; }
} }
} return changed ? next : prev;
return changed ? next : prev; });
}); } else {
setColumnOrder(derivedColumnOrder);
setColumnVisibility(derivedColumnVisibility);
}
}, [ }, [
activeView?.id, activeView?.id,
derivedColumnOrder, derivedColumnOrder,
derivedColumnVisibility, derivedColumnVisibility,
properties, properties,
hasPendingEdit,
]); ]);
const columnPinning = useMemo( const columnPinning = useMemo(
@@ -355,9 +369,23 @@ export function useBaseTable(
clearTimeout(persistTimerRef.current); clearTimeout(persistTimerRef.current);
} }
setHasPendingEdit(true);
persistTimerRef.current = setTimeout(() => { persistTimerRef.current = setTimeout(() => {
persistTimerRef.current = null;
const config = buildViewConfigFromTable(table, activeView.config); const config = buildViewConfigFromTable(table, activeView.config);
updateViewMutation.mutate({ viewId: activeView.id, baseId: base.id, config }); updateViewMutation.mutate(
{ viewId: activeView.id, baseId: base.id, config },
{
onSettled: () => {
// Don't clear if the user has already scheduled another
// debounce while this one was in flight.
if (persistTimerRef.current === null) {
setHasPendingEdit(false);
}
},
},
);
}, 300); }, 300);
}, [activeView, base, table, updateViewMutation]); }, [activeView, base, table, updateViewMutation]);