Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ export function TableGrid({
rows,
rowTotal,
isLoadingRows,
rowsError,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
Expand Down Expand Up @@ -4973,104 +4974,114 @@ export function TableGrid({
)}
</thead>
<tbody ref={tbodyRef}>
{isLoadingTable || isLoadingRows
? null
: (() => {
const virtualItems = rowVirtualizer.getVirtualItems()
// `item.start`/`item.end` include `scrollMargin` (the sticky-header
// offset) but `getTotalSize()` already nets it out, so both spacer
// heights are computed relative to `scrollMargin`.
const scrollMargin = rowVirtualizer.options.scrollMargin
const paddingTop =
virtualItems.length > 0 ? virtualItems[0].start - scrollMargin : 0
const paddingBottom =
virtualItems.length > 0
? rowVirtualizer.getTotalSize() -
(virtualItems[virtualItems.length - 1].end - scrollMargin)
: 0
return (
<>
{paddingTop > 0 && (
<tr aria-hidden>
<td
colSpan={displayColumns.length + 1}
style={{ height: paddingTop }}
/>
</tr>
)}
{virtualItems.map((virtualRow) => {
const index = virtualRow.index
const row = rows[index]
if (!row) return null
return (
<DataRow
key={row.id}
row={row}
columns={displayColumns}
workspaceId={workspaceId}
timezoneStatus={timezoneState.status}
rowIndex={index}
isFirstRow={index === 0}
editingColumnName={
editingCell?.rowId === row.id ? editingCell.columnName : null
}
initialCharacter={
editingCell?.rowId === row.id ? initialCharacter : null
}
editorsReadOnly={Boolean(locks?.updateLocked)}
pendingCellValue={
pendingUpdate && pendingUpdate.rowId === row.id
? pendingUpdate.data
: null
}
normalizedSelection={normalizedSelection}
onClick={handleCellClick}
onDoubleClick={handleCellDoubleClick}
onSave={handleInlineSave}
onCancel={handleInlineCancel}
onContextMenu={handleRowContextMenu}
onCellMouseDown={handleCellMouseDown}
onCellMouseEnter={handleCellMouseEnter}
isRowChecked={rowSelectionIncludes(rowSelection, row.id)}
onRowToggle={handleRowToggle}
onRowMouseDown={handleRowMouseDown}
onRowMouseEnter={handleRowMouseEnter}
runningCount={runningByRowId[row.id] ?? 0}
hasWorkflowColumns={hasWorkflowColumns}
numRegionWidth={numRegionWidth}
onStopRow={onStopRow}
onRunRow={onRunRow}
workflowGroups={tableWorkflowGroups}
activeDispatches={activeDispatches}
pinnedOffsets={pinnedOffsets.size > 0 ? pinnedOffsets : undefined}
lastPinnedColKey={lastPinnedColKey}
findMatchColumns={findMatchColumnsByRowId.get(row.id)}
/>
)
})}
{paddingBottom > 0 && (
<tr aria-hidden>
<td
colSpan={displayColumns.length + 1}
style={{ height: paddingBottom }}
/>
</tr>
)}
{isFetchingNextPage && (
<tr>
<td colSpan={displayColumns.length + 1} className='h-[35px] p-0'>
<div className='flex items-center justify-center'>
<Loader
animate
className='size-[14px] shrink-0 text-[var(--text-tertiary)]'
/>
</div>
</td>
</tr>
)}
</>
)
})()}
{isLoadingTable || isLoadingRows ? null : rowsError ? (
<tr>
<td
colSpan={displayColumns.length + 1}
role='alert'
className='px-4 py-3 text-[var(--text-error)] text-caption'
>
{rowsError.message}
</td>
</tr>
) : (
(() => {
const virtualItems = rowVirtualizer.getVirtualItems()
// `item.start`/`item.end` include `scrollMargin` (the sticky-header
// offset) but `getTotalSize()` already nets it out, so both spacer
// heights are computed relative to `scrollMargin`.
const scrollMargin = rowVirtualizer.options.scrollMargin
const paddingTop =
virtualItems.length > 0 ? virtualItems[0].start - scrollMargin : 0
const paddingBottom =
virtualItems.length > 0
? rowVirtualizer.getTotalSize() -
(virtualItems[virtualItems.length - 1].end - scrollMargin)
: 0
return (
<>
{paddingTop > 0 && (
<tr aria-hidden>
<td
colSpan={displayColumns.length + 1}
style={{ height: paddingTop }}
/>
</tr>
)}
{virtualItems.map((virtualRow) => {
const index = virtualRow.index
const row = rows[index]
if (!row) return null
return (
<DataRow
key={row.id}
row={row}
columns={displayColumns}
workspaceId={workspaceId}
timezoneStatus={timezoneState.status}
rowIndex={index}
isFirstRow={index === 0}
editingColumnName={
editingCell?.rowId === row.id ? editingCell.columnName : null
}
initialCharacter={
editingCell?.rowId === row.id ? initialCharacter : null
}
editorsReadOnly={Boolean(locks?.updateLocked)}
pendingCellValue={
pendingUpdate && pendingUpdate.rowId === row.id
? pendingUpdate.data
: null
}
normalizedSelection={normalizedSelection}
onClick={handleCellClick}
onDoubleClick={handleCellDoubleClick}
onSave={handleInlineSave}
onCancel={handleInlineCancel}
onContextMenu={handleRowContextMenu}
onCellMouseDown={handleCellMouseDown}
onCellMouseEnter={handleCellMouseEnter}
isRowChecked={rowSelectionIncludes(rowSelection, row.id)}
onRowToggle={handleRowToggle}
onRowMouseDown={handleRowMouseDown}
onRowMouseEnter={handleRowMouseEnter}
runningCount={runningByRowId[row.id] ?? 0}
hasWorkflowColumns={hasWorkflowColumns}
numRegionWidth={numRegionWidth}
onStopRow={onStopRow}
onRunRow={onRunRow}
workflowGroups={tableWorkflowGroups}
activeDispatches={activeDispatches}
pinnedOffsets={pinnedOffsets.size > 0 ? pinnedOffsets : undefined}
lastPinnedColKey={lastPinnedColKey}
findMatchColumns={findMatchColumnsByRowId.get(row.id)}
/>
)
})}
{paddingBottom > 0 && (
<tr aria-hidden>
<td
colSpan={displayColumns.length + 1}
style={{ height: paddingBottom }}
/>
</tr>
)}
{isFetchingNextPage && (
<tr>
<td colSpan={displayColumns.length + 1} className='h-[35px] p-0'>
<div className='flex items-center justify-center'>
<Loader
animate
className='size-[14px] shrink-0 text-[var(--text-tertiary)]'
/>
</div>
</td>
</tr>
)}
</>
)
})()
)}
</tbody>
</table>
{remoteSelections.length > 0 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export interface UseTableReturn {
*/
filter: TablePredicate | null
isLoadingRows: boolean
/**
* Why the first page of rows failed to load (e.g. a filter the server
* rejects), or null. Without it a rejected query is indistinguishable from
* an empty result.
*/
rowsError: Error | null
refetchRows: () => void
/**
* The resolved value's `hasNextPage` reflects the post-fetch cache state —
Expand Down Expand Up @@ -106,6 +112,7 @@ export function useTable({ workspaceId, tableId, queryOptions }: UseTableParams)
const {
data: rowsData,
isLoading: isLoadingRows,
error: rowsQueryError,
refetch,
fetchNextPage,
hasNextPage,
Expand All @@ -124,6 +131,10 @@ export function useTable({ workspaceId, tableId, queryOptions }: UseTableParams)
[rowsData?.pages]
)

// Only a failed first page leaves the grid with nothing to show; a failed
// next page or background refetch keeps the rows already loaded.
const rowsError = rowsData === undefined ? rowsQueryError : null

// Server-side COUNT(*) for the active filter (page 0 only). Null until the first page lands;
// callers fall back to the table's unfiltered `rowCount`. This is the true "select all" total —
// it reflects the filter, unlike `tableData.rowCount`.
Expand Down Expand Up @@ -264,6 +275,7 @@ export function useTable({ workspaceId, tableId, queryOptions }: UseTableParams)
rowTotal,
filter,
isLoadingRows,
rowsError,
refetchRows,
fetchNextPage: fetchNextPageWrapped,
hasNextPage: Boolean(hasNextPage),
Expand Down
Loading
Loading