-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathQueryTable.tsx
More file actions
121 lines (109 loc) · 4.15 KB
/
QueryTable.tsx
File metadata and controls
121 lines (109 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { getCoreRowModel, useReactTable, type ColumnDef } from '@tanstack/react-table'
import { useEffect, useMemo, useRef } from 'react'
import { ensurePrefetched, type PaginatedQuery, type ResultsPage } from '@oxide/api'
import { Pagination } from '~/components/Pagination'
import { usePagination } from '~/hooks/use-pagination'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { TableEmptyBox } from '~/ui/lib/Table'
import { Table } from './Table'
type QueryTableProps<TItem> = {
query: PaginatedQuery<ResultsPage<TItem>>
rowHeight?: 'small' | 'large'
emptyState: React.ReactElement
// React Table does the same in the type of `columns` on `useReactTable`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
columns: ColumnDef<TItem, any>[]
// Require getId if and only if TItem does not have an id field. Something
// to keep in mind for the future: if instead we used the `select` transform
// function on the query to add an ID to every row, we could just require TItem
// to extend `{ id: string }`, and we wouldn't need this `getId` function. The
// difficulty I ran into was propagating the result of `select` through the API
// query options helpers. But I think it can be done.
} & (TItem extends { id: string }
? { getId?: never }
: {
/** Needed if and only if `TItem` has no `id` field */
getId: (row: TItem) => string
})
/**
* Reset scroll to top when clicking * next/prev to change page but not,
* for example, on initial pageload after browser forward/back.
*/
function useScrollReset(triggerDep: string | undefined) {
const resetRequested = useRef(false)
useEffect(() => {
if (resetRequested.current) {
window.scrollTo(0, 0)
resetRequested.current = false
}
}, [triggerDep])
return () => {
resetRequested.current = true
}
}
// require ID only so we can use it in getRowId
export function useQueryTable<TItem>({
query,
rowHeight = 'small',
emptyState,
columns,
getId,
}: QueryTableProps<TItem>) {
const { currentPage, goToNextPage, goToPrevPage, hasPrev } = usePagination()
const queryOptions = query.optionsFn(currentPage)
const queryResult = useQuery(queryOptions)
// only ensure prefetched if we're on the first page
if (currentPage === undefined) ensurePrefetched(queryResult, queryOptions.queryKey)
const { data, isPlaceholderData } = queryResult
const tableData = useMemo(() => data?.items || [], [data])
const getRowId = getId
? getId
: // @ts-expect-error we know from the types that getId is only defined when there is no ID
(row: TItem) => row.id as string
// trigger by first item ID and not, e.g., currentPage because currentPage
// changes as soon as you click Next, while the item ID doesn't change until
// the page actually changes.
const first = tableData.at(0)
const requestScrollReset = useScrollReset(first ? getRowId(first) : undefined)
const table = useReactTable({
columns,
data: tableData,
getRowId,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
})
const isEmpty = tableData.length === 0 && !hasPrev
const tableElement = isEmpty ? (
<TableEmptyBox>{emptyState || <EmptyMessage title="No results" />}</TableEmptyBox>
) : (
<>
<Table table={table} rowHeight={rowHeight} />
<Pagination
pageSize={query.pageSize}
hasNext={tableData.length === query.pageSize}
hasPrev={hasPrev}
nextPage={data?.nextPage}
onNext={(p) => {
requestScrollReset()
goToNextPage(p)
}}
onPrev={() => {
requestScrollReset()
goToPrevPage()
}}
// I can't believe how well this works, but it exactly matches when
// we want to show the spinner. Cached page changes don't need it.
loading={isPlaceholderData}
/>
</>
)
return { table: tableElement, query: queryResult }
}