fix(tables): re-broadcast selection when a peer's row change shifts it

End-to-end lifecycle audit (Low-Med): the selection emit resolved the stable
(rowId, columnId) only on selection/editing change, not when a live edit/schema
refetch inserted/deleted/reordered rows. The index-based local selection then sat
on a different logical row than the rowId peers held, so your outline showed on the
old row until you moved. Re-run the emit on rows/displayColumns change and dedup an
unchanged result (also drops the redundant null-on-open emit) so the broadcast stays
consistent with the local highlight.
This commit is contained in:
Waleed Latif
2026-07-25 01:01:54 -07:00
parent a56ffe62bf
commit cdc8796b8f
2 changed files with 16 additions and 8 deletions
@@ -836,16 +836,15 @@ export function TableGrid({
: null
// Broadcast the local viewer's cell selection to the presence room. Resolves the
// index-based selection to stable (rowId, columnId) through refs so it re-emits only
// on a real selection/editing change, not on every data update. `editing` marks the
// active cell so peers darken it (the "someone is typing here" signal).
// index-based selection to stable (rowId, columnId), re-running on `rows`/`displayColumns`
// too so a peer's row insert/delete/reorder re-broadcasts the shifted id under the same
// index (the emitter dedups an unchanged result). `editing` marks the active cell so
// peers darken it (the "someone is typing here" signal).
useEffect(() => {
const currentRows = rowsRef.current
const currentCols = columnsRef.current
const resolve = (coord: CellCoord | null) => {
if (!coord) return null
const rowId = currentRows[coord.rowIndex]?.id
const columnId = currentCols[coord.colIndex]?.key
const rowId = rows[coord.rowIndex]?.id
const columnId = displayColumns[coord.colIndex]?.key
return rowId && columnId ? { rowId, columnId } : null
}
const anchor = resolve(selectionAnchor)
@@ -858,7 +857,7 @@ export function TableGrid({
// most common selection would never broadcast and would clear the prior outline.
const focus = resolve(selectionFocus) ?? anchor
emitCellSelection({ anchor, focus, editing: editingCell !== null })
}, [selectionAnchor, selectionFocus, editingCell, emitCellSelection])
}, [selectionAnchor, selectionFocus, editingCell, rows, displayColumns, emitCellSelection])
const { data: findData, isFetching: isFindFetching } = useFindTableRows({
workspaceId,
@@ -150,6 +150,8 @@ export function useTableRoom(tableId: string): UseTableRoomResult {
const lastEmitRef = useRef(0)
const trailingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const pendingCellRef = useRef<TableCellSelection>(null)
/** Key of the last selection sent, to skip re-emitting an unchanged selection. */
const lastSentKeyRef = useRef<string | null>(null)
// Reset the throttle when the table changes (or on unmount): a pending selection for
// the table we're leaving must not flush into the next table's room after a switch.
@@ -161,12 +163,19 @@ export function useTableRoom(tableId: string): UseTableRoomResult {
}
pendingCellRef.current = null
currentCellRef.current = null
lastSentKeyRef.current = null
lastEmitRef.current = 0
},
[tableId]
)
const emitCellSelection = useCallback((cell: TableCellSelection) => {
// Skip re-emitting an unchanged selection: the caller re-resolves on every data
// refetch (so a peer's row insert re-broadcasts the shifted rowId), but most refetches
// don't move the selection — dedup those, and the no-selection state on table open.
const key = cell === null ? 'null' : JSON.stringify(cell)
if (key === lastSentKeyRef.current) return
lastSentKeyRef.current = key
currentCellRef.current = cell
pendingCellRef.current = cell
const flush = () => {